feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
89
dexto/packages/image-bundler/CHANGELOG.md
Normal file
89
dexto/packages/image-bundler/CHANGELOG.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# @dexto/image-bundler
|
||||
|
||||
## 1.5.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 042f4f0: ### CLI Improvements
|
||||
- Add `/export` command to export conversations as Markdown or JSON
|
||||
- Add `Ctrl+T` toggle for task list visibility during processing
|
||||
- Improve task list UI with collapsible view near the processing message
|
||||
- Fix race condition causing duplicate rendering (mainly visible with explore tool)
|
||||
- Don't truncate `pattern` and `question` args in tool output display
|
||||
|
||||
### Bug Fixes
|
||||
- Fix build script to preserve `.dexto` storage (conversations, logs) during clean builds
|
||||
- Fix `@dexto/tools-todo` versioning - add to fixed version group in changeset config
|
||||
|
||||
### Configuration Changes
|
||||
- Remove approval timeout defaults - now waits indefinitely (better UX for CLI)
|
||||
- Add package versioning guidelines to AGENTS.md
|
||||
|
||||
- Updated dependencies [042f4f0]
|
||||
- @dexto/core@1.5.6
|
||||
|
||||
## 1.5.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [63fa083]
|
||||
- Updated dependencies [6df3ca9]
|
||||
- @dexto/core@1.5.5
|
||||
|
||||
## 1.5.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0016cd3]
|
||||
- Updated dependencies [499b890]
|
||||
- Updated dependencies [aa2c9a0]
|
||||
- @dexto/core@1.5.4
|
||||
|
||||
## 1.5.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [4f00295]
|
||||
- Updated dependencies [69c944c]
|
||||
- @dexto/core@1.5.3
|
||||
|
||||
## 1.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [8a85ea4]
|
||||
- Updated dependencies [527f3f9]
|
||||
- @dexto/core@1.5.2
|
||||
|
||||
## 1.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bfcc7b1]
|
||||
- Updated dependencies [4aabdb7]
|
||||
- @dexto/core@1.5.1
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- e7722e5: Minor version bump for new release with bundler, custom tool pkgs, etc.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1e7e974: Added image bundler, @dexto/image-local and moved tool services outside core. Added registry providers to select core services.
|
||||
- 5fa79fa: Renamed compression to compaction, added context-awareness to hono, updated cli tool display formatting and added integration test for image-local.
|
||||
- ef40e60: Upgrades package versions and related changes to MCP SDK. CLI colors improved and token streaming added to status bar.
|
||||
|
||||
Security: Resolve all Dependabot security vulnerabilities. Updated @modelcontextprotocol/sdk to 1.25.2, esbuild to 0.25.0, langchain to 0.3.37, and @langchain/core to 0.3.80. Added pnpm overrides for indirect vulnerabilities (preact@10.27.3, qs@6.14.1, jws@3.2.3, mdast-util-to-hast@13.2.1). Fixed type errors from MCP SDK breaking changes.
|
||||
|
||||
- Updated dependencies [ee12727]
|
||||
- Updated dependencies [1e7e974]
|
||||
- Updated dependencies [4c05310]
|
||||
- Updated dependencies [5fa79fa]
|
||||
- Updated dependencies [ef40e60]
|
||||
- Updated dependencies [e714418]
|
||||
- Updated dependencies [e7722e5]
|
||||
- Updated dependencies [7d5ab19]
|
||||
- Updated dependencies [436a900]
|
||||
- @dexto/core@1.5.0
|
||||
33
dexto/packages/image-bundler/package.json
Normal file
33
dexto/packages/image-bundler/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@dexto/image-bundler",
|
||||
"version": "1.5.6",
|
||||
"description": "Bundler for Dexto base images",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"bin": {
|
||||
"dexto-bundle": "./dist/cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dexto/core": "workspace:*",
|
||||
"commander": "^12.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.5",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
361
dexto/packages/image-bundler/src/bundler.ts
Normal file
361
dexto/packages/image-bundler/src/bundler.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Main bundler logic
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import { dirname, join, resolve, relative, extname } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { validateImageDefinition } from '@dexto/core';
|
||||
import type { ImageDefinition } from '@dexto/core';
|
||||
import type { BundleOptions, BundleResult } from './types.js';
|
||||
import { generateEntryPoint } from './generator.js';
|
||||
import ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Bundle a Dexto base image
|
||||
*/
|
||||
export async function bundle(options: BundleOptions): Promise<BundleResult> {
|
||||
const warnings: string[] = [];
|
||||
|
||||
// 1. Load and validate image definition
|
||||
console.log(`📦 Loading image definition from ${options.imagePath}`);
|
||||
const definition = await loadImageDefinition(options.imagePath);
|
||||
|
||||
console.log(`✅ Loaded image: ${definition.name} v${definition.version}`);
|
||||
|
||||
// 2. Validate definition
|
||||
console.log(`🔍 Validating image definition...`);
|
||||
try {
|
||||
validateImageDefinition(definition);
|
||||
console.log(`✅ Image definition is valid`);
|
||||
} catch (error) {
|
||||
throw new Error(`Image validation failed: ${error}`);
|
||||
}
|
||||
|
||||
// 3. Get core version (from package.json)
|
||||
const coreVersion = getCoreVersion();
|
||||
|
||||
// 3.5. Discover providers from convention-based folders
|
||||
console.log(`🔍 Discovering providers from folders...`);
|
||||
const imageDir = dirname(options.imagePath);
|
||||
const discoveredProviders = discoverProviders(imageDir);
|
||||
console.log(`✅ Discovered ${discoveredProviders.totalCount} provider(s)`);
|
||||
|
||||
// 4. Generate code
|
||||
console.log(`🔨 Generating entry point...`);
|
||||
const generated = generateEntryPoint(definition, coreVersion, discoveredProviders);
|
||||
|
||||
// 5. Ensure output directory exists
|
||||
const outDir = resolve(options.outDir);
|
||||
if (!existsSync(outDir)) {
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 5.5. Compile provider category folders
|
||||
console.log(`🔨 Compiling provider source files...`);
|
||||
const categories = ['blob-store', 'tools', 'compaction', 'plugins'];
|
||||
let compiledCount = 0;
|
||||
|
||||
for (const category of categories) {
|
||||
const categoryDir = join(imageDir, category);
|
||||
if (existsSync(categoryDir)) {
|
||||
compileSourceFiles(categoryDir, join(outDir, category));
|
||||
compiledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (compiledCount > 0) {
|
||||
console.log(
|
||||
`✅ Compiled ${compiledCount} provider categor${compiledCount === 1 ? 'y' : 'ies'}`
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Write generated files
|
||||
const entryFile = join(outDir, 'index.js');
|
||||
const typesFile = join(outDir, 'index.d.ts');
|
||||
|
||||
console.log(`📝 Writing ${entryFile}...`);
|
||||
writeFileSync(entryFile, generated.js, 'utf-8');
|
||||
|
||||
console.log(`📝 Writing ${typesFile}...`);
|
||||
writeFileSync(typesFile, generated.dts, 'utf-8');
|
||||
|
||||
// 7. Generate package.json exports
|
||||
updatePackageJson(dirname(options.imagePath), outDir);
|
||||
|
||||
console.log(`✨ Build complete!`);
|
||||
console.log(` Entry: ${entryFile}`);
|
||||
console.log(` Types: ${typesFile}`);
|
||||
|
||||
const metadata = {
|
||||
name: definition.name,
|
||||
version: definition.version,
|
||||
description: definition.description,
|
||||
target: definition.target || 'custom',
|
||||
constraints: definition.constraints || [],
|
||||
builtAt: new Date().toISOString(),
|
||||
coreVersion,
|
||||
};
|
||||
|
||||
return {
|
||||
entryFile,
|
||||
typesFile,
|
||||
metadata,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image definition from file
|
||||
*/
|
||||
async function loadImageDefinition(imagePath: string): Promise<ImageDefinition> {
|
||||
const absolutePath = resolve(imagePath);
|
||||
|
||||
if (!existsSync(absolutePath)) {
|
||||
throw new Error(`Image file not found: ${absolutePath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert to file:// URL for ESM import
|
||||
const fileUrl = pathToFileURL(absolutePath).href;
|
||||
|
||||
// Dynamic import
|
||||
const module = await import(fileUrl);
|
||||
|
||||
// Get default export
|
||||
const definition = module.default as ImageDefinition;
|
||||
|
||||
if (!definition) {
|
||||
throw new Error('Image file must have a default export');
|
||||
}
|
||||
|
||||
return definition;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to load image definition: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get @dexto/core version
|
||||
*/
|
||||
function getCoreVersion(): string {
|
||||
try {
|
||||
// Try to read from node_modules
|
||||
const corePackageJson = join(process.cwd(), 'node_modules/@dexto/core/package.json');
|
||||
if (existsSync(corePackageJson)) {
|
||||
const pkg = JSON.parse(readFileSync(corePackageJson, 'utf-8'));
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
// Fallback to workspace version
|
||||
return '1.0.0';
|
||||
} catch {
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create package.json with proper exports
|
||||
*/
|
||||
function updatePackageJson(imageDir: string, outDir: string): void {
|
||||
const packageJsonPath = join(imageDir, 'package.json');
|
||||
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
console.log(`⚠️ No package.json found, skipping exports update`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
|
||||
// Update exports
|
||||
pkg.exports = {
|
||||
'.': {
|
||||
types: './dist/index.d.ts',
|
||||
import: './dist/index.js',
|
||||
},
|
||||
};
|
||||
|
||||
// Update main and types fields
|
||||
pkg.main = './dist/index.js';
|
||||
pkg.types = './dist/index.d.ts';
|
||||
|
||||
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2), 'utf-8');
|
||||
console.log(`✅ Updated package.json exports`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to update package.json: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile TypeScript source files to JavaScript
|
||||
*/
|
||||
function compileSourceFiles(srcDir: string, outDir: string): void {
|
||||
// Find all .ts files
|
||||
const tsFiles = findTypeScriptFiles(srcDir);
|
||||
|
||||
if (tsFiles.length === 0) {
|
||||
console.log(` No TypeScript files found in ${srcDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` Found ${tsFiles.length} TypeScript file(s) to compile`);
|
||||
|
||||
// TypeScript compiler options
|
||||
const compilerOptions: ts.CompilerOptions = {
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
||||
outDir: outDir,
|
||||
rootDir: srcDir, // Use srcDir as root
|
||||
declaration: true,
|
||||
esModuleInterop: true,
|
||||
skipLibCheck: true,
|
||||
strict: true,
|
||||
resolveJsonModule: true,
|
||||
};
|
||||
|
||||
// Create program
|
||||
const program = ts.createProgram(tsFiles, compilerOptions);
|
||||
|
||||
// Emit compiled files
|
||||
const emitResult = program.emit();
|
||||
|
||||
// Check for errors
|
||||
const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
|
||||
|
||||
if (allDiagnostics.length > 0) {
|
||||
allDiagnostics.forEach((diagnostic) => {
|
||||
if (diagnostic.file) {
|
||||
const { line, character } = ts.getLineAndCharacterOfPosition(
|
||||
diagnostic.file,
|
||||
diagnostic.start!
|
||||
);
|
||||
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
||||
console.error(
|
||||
` ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
` ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (emitResult.emitSkipped) {
|
||||
throw new Error('TypeScript compilation failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all TypeScript files in a directory
|
||||
*/
|
||||
function findTypeScriptFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
function walk(currentDir: string) {
|
||||
const entries = readdirSync(currentDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(currentDir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
walk(fullPath);
|
||||
} else if (stat.isFile() && extname(entry) === '.ts') {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider discovery result for a single category
|
||||
*/
|
||||
export interface DiscoveredProviders {
|
||||
blobStore: string[];
|
||||
customTools: string[];
|
||||
compaction: string[];
|
||||
plugins: string[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover providers from convention-based folder structure
|
||||
*
|
||||
* Convention (folder-based with index.ts):
|
||||
* tools/ - CustomToolProvider folders
|
||||
* weather/ - Provider folder
|
||||
* index.ts - Provider implementation (auto-discovered)
|
||||
* helpers.ts - Optional helper files
|
||||
* types.ts - Optional type definitions
|
||||
* blob-store/ - BlobStoreProvider folders
|
||||
* compaction/ - CompactionProvider folders
|
||||
* plugins/ - PluginProvider folders
|
||||
*
|
||||
* Naming Convention (Node.js standard):
|
||||
* <folder>/index.ts - Auto-discovered and registered
|
||||
* <folder>/other.ts - Ignored unless imported by index.ts
|
||||
*/
|
||||
function discoverProviders(imageDir: string): DiscoveredProviders {
|
||||
const result: DiscoveredProviders = {
|
||||
blobStore: [],
|
||||
customTools: [],
|
||||
compaction: [],
|
||||
plugins: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
|
||||
// Category mapping: folder name -> property name
|
||||
const categories = {
|
||||
'blob-store': 'blobStore',
|
||||
tools: 'customTools',
|
||||
compaction: 'compaction',
|
||||
plugins: 'plugins',
|
||||
} as const;
|
||||
|
||||
for (const [folderName, propName] of Object.entries(categories)) {
|
||||
const categoryDir = join(imageDir, folderName);
|
||||
|
||||
if (!existsSync(categoryDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find all provider folders (those with index.ts)
|
||||
const providerFolders = readdirSync(categoryDir)
|
||||
.filter((entry) => {
|
||||
const entryPath = join(categoryDir, entry);
|
||||
const stat = statSync(entryPath);
|
||||
|
||||
// Must be a directory
|
||||
if (!stat.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must contain index.ts
|
||||
const indexPath = join(entryPath, 'index.ts');
|
||||
return existsSync(indexPath);
|
||||
})
|
||||
.map((folder) => {
|
||||
// Return relative path for imports
|
||||
return `./${folderName}/${folder}/index.js`;
|
||||
});
|
||||
|
||||
if (providerFolders.length > 0) {
|
||||
result[propName as keyof Omit<DiscoveredProviders, 'totalCount'>].push(
|
||||
...providerFolders
|
||||
);
|
||||
result.totalCount += providerFolders.length;
|
||||
console.log(` Found ${providerFolders.length} provider(s) in ${folderName}/`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
93
dexto/packages/image-bundler/src/cli.ts
Normal file
93
dexto/packages/image-bundler/src/cli.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI for bundling Dexto base images
|
||||
*/
|
||||
|
||||
// Suppress experimental warnings (e.g., Type Stripping)
|
||||
process.removeAllListeners('warning');
|
||||
process.on('warning', (warning) => {
|
||||
if (warning.name !== 'ExperimentalWarning') {
|
||||
console.warn(warning);
|
||||
}
|
||||
});
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { bundle } from './bundler.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import pc from 'picocolors';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Read version from package.json
|
||||
const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.name('dexto-bundle').description('Bundle Dexto base images').version(packageJson.version);
|
||||
|
||||
program
|
||||
.command('build')
|
||||
.description('Build a base image from dexto.image.ts')
|
||||
.option('-i, --image <path>', 'Path to dexto.image.ts file', 'dexto.image.ts')
|
||||
.option('-o, --out <dir>', 'Output directory', 'dist')
|
||||
.option('--sourcemap', 'Generate source maps', false)
|
||||
.option('--minify', 'Minify output', false)
|
||||
.action(async (options) => {
|
||||
try {
|
||||
console.log(pc.cyan('🚀 Dexto Image Bundler\n'));
|
||||
|
||||
const result = await bundle({
|
||||
imagePath: options.image,
|
||||
outDir: options.out,
|
||||
sourcemap: options.sourcemap,
|
||||
minify: options.minify,
|
||||
});
|
||||
|
||||
console.log(pc.green('\n✨ Build successful!\n'));
|
||||
console.log(pc.bold('Image Details:'));
|
||||
console.log(` Name: ${result.metadata.name}`);
|
||||
console.log(` Version: ${result.metadata.version}`);
|
||||
console.log(` Target: ${result.metadata.target}`);
|
||||
console.log(` Built at: ${result.metadata.builtAt}`);
|
||||
console.log(` Core: v${result.metadata.coreVersion}`);
|
||||
|
||||
if (result.metadata.constraints.length > 0) {
|
||||
console.log(` Constraints: ${result.metadata.constraints.join(', ')}`);
|
||||
}
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log(pc.yellow('\n⚠️ Warnings:'));
|
||||
result.warnings.forEach((w) => console.log(` - ${w}`));
|
||||
}
|
||||
|
||||
// Read package.json to get the actual package name
|
||||
const packageJsonPath = join(process.cwd(), 'package.json');
|
||||
let packageName = result.metadata.name;
|
||||
try {
|
||||
if (readFileSync) {
|
||||
const pkgJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
||||
packageName = pkgJson.name || result.metadata.name;
|
||||
}
|
||||
} catch {
|
||||
// Use metadata name as fallback
|
||||
}
|
||||
|
||||
console.log(pc.green('\n✅ Image is ready to use!'));
|
||||
console.log(' To use this image in an app:');
|
||||
console.log(
|
||||
pc.dim(
|
||||
` 1. Install it: pnpm add ${packageName}@file:../${packageName.split('/').pop()}`
|
||||
)
|
||||
);
|
||||
console.log(pc.dim(` 2. Import it: import { createAgent } from '${packageName}';`));
|
||||
console.log(pc.dim(`\n Or publish to npm and install normally.`));
|
||||
} catch (error) {
|
||||
console.error(pc.red('\n❌ Build failed:'), error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
336
dexto/packages/image-bundler/src/generator.ts
Normal file
336
dexto/packages/image-bundler/src/generator.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Code generator for base images
|
||||
*
|
||||
* Transforms image definitions into importable packages with:
|
||||
* - Side-effect provider registration
|
||||
* - createAgent() factory
|
||||
* - Utility exports
|
||||
* - Metadata exports
|
||||
*/
|
||||
|
||||
import type { ImageDefinition } from '@dexto/core';
|
||||
import type { GeneratedCode } from './types.js';
|
||||
import type { DiscoveredProviders } from './bundler.js';
|
||||
|
||||
/**
|
||||
* Generate JavaScript entry point for an image
|
||||
*/
|
||||
export function generateEntryPoint(
|
||||
definition: ImageDefinition,
|
||||
coreVersion: string,
|
||||
discoveredProviders?: DiscoveredProviders
|
||||
): GeneratedCode {
|
||||
// Generate imports section
|
||||
const imports = generateImports(definition, discoveredProviders);
|
||||
|
||||
// Generate provider registration section
|
||||
const registrations = generateProviderRegistrations(definition, discoveredProviders);
|
||||
|
||||
// Generate factory function
|
||||
const factory = generateFactory();
|
||||
|
||||
// Generate utility exports
|
||||
const utilityExports = generateUtilityExports(definition);
|
||||
|
||||
// Generate metadata export
|
||||
const metadata = generateMetadata(definition, coreVersion);
|
||||
|
||||
// Combine all sections
|
||||
const js = `// AUTO-GENERATED by @dexto/bundler
|
||||
// Do not edit this file directly. Edit dexto.image.ts instead.
|
||||
|
||||
${imports}
|
||||
|
||||
${registrations}
|
||||
|
||||
${factory}
|
||||
|
||||
${utilityExports}
|
||||
|
||||
${metadata}
|
||||
`;
|
||||
|
||||
// Generate TypeScript definitions
|
||||
const dts = generateTypeDefinitions(definition);
|
||||
|
||||
return { js, dts };
|
||||
}
|
||||
|
||||
function generateImports(
|
||||
definition: ImageDefinition,
|
||||
discoveredProviders?: DiscoveredProviders
|
||||
): string {
|
||||
const imports: string[] = [];
|
||||
|
||||
// Import base image first (if extending) - triggers side-effect provider registration
|
||||
if (definition.extends) {
|
||||
imports.push(`// Import base image for provider registration (side effect)`);
|
||||
imports.push(`import '${definition.extends}';`);
|
||||
imports.push(``);
|
||||
}
|
||||
|
||||
// Core imports
|
||||
imports.push(`import { DextoAgent } from '@dexto/core';`);
|
||||
|
||||
// Always import all registries since we re-export them in generateFactory()
|
||||
// This ensures the re-exports don't reference unimported identifiers
|
||||
imports.push(
|
||||
`import { customToolRegistry, pluginRegistry, compactionRegistry, blobStoreRegistry } from '@dexto/core';`
|
||||
);
|
||||
|
||||
// Import discovered providers
|
||||
if (discoveredProviders) {
|
||||
const categories = [
|
||||
{ key: 'blobStore', label: 'Blob Storage' },
|
||||
{ key: 'customTools', label: 'Custom Tools' },
|
||||
{ key: 'compaction', label: 'Compaction' },
|
||||
{ key: 'plugins', label: 'Plugins' },
|
||||
] as const;
|
||||
|
||||
for (const { key, label } of categories) {
|
||||
const providers = discoveredProviders[key];
|
||||
if (providers.length > 0) {
|
||||
imports.push(``);
|
||||
imports.push(`// ${label} providers (auto-discovered)`);
|
||||
providers.forEach((path, index) => {
|
||||
const varName = `${key}Provider${index}`;
|
||||
imports.push(`import * as ${varName} from '${path}';`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imports.join('\n');
|
||||
}
|
||||
|
||||
function generateProviderRegistrations(
|
||||
definition: ImageDefinition,
|
||||
discoveredProviders?: DiscoveredProviders
|
||||
): string {
|
||||
const registrations: string[] = [];
|
||||
|
||||
if (definition.extends) {
|
||||
registrations.push(
|
||||
`// Base image providers already registered via import of '${definition.extends}'`
|
||||
);
|
||||
registrations.push('');
|
||||
}
|
||||
|
||||
registrations.push('// SIDE EFFECT: Register providers on import');
|
||||
registrations.push('');
|
||||
|
||||
// Auto-register discovered providers
|
||||
if (discoveredProviders) {
|
||||
const categoryMap = [
|
||||
{ key: 'blobStore', registry: 'blobStoreRegistry', label: 'Blob Storage' },
|
||||
{ key: 'customTools', registry: 'customToolRegistry', label: 'Custom Tools' },
|
||||
{ key: 'compaction', registry: 'compactionRegistry', label: 'Compaction' },
|
||||
{ key: 'plugins', registry: 'pluginRegistry', label: 'Plugins' },
|
||||
] as const;
|
||||
|
||||
for (const { key, registry, label } of categoryMap) {
|
||||
const providers = discoveredProviders[key];
|
||||
if (providers.length === 0) continue;
|
||||
|
||||
registrations.push(`// Auto-register ${label} providers`);
|
||||
providers.forEach((path, index) => {
|
||||
const varName = `${key}Provider${index}`;
|
||||
registrations.push(`// From ${path}`);
|
||||
registrations.push(`for (const exported of Object.values(${varName})) {`);
|
||||
registrations.push(
|
||||
` if (exported && typeof exported === 'object' && 'type' in exported && 'create' in exported) {`
|
||||
);
|
||||
registrations.push(` try {`);
|
||||
registrations.push(` ${registry}.register(exported);`);
|
||||
registrations.push(
|
||||
` console.log(\`✓ Registered ${key}: \${exported.type}\`);`
|
||||
);
|
||||
registrations.push(` } catch (err) {`);
|
||||
registrations.push(` // Ignore duplicate registration errors`);
|
||||
registrations.push(
|
||||
` if (!err.message?.includes('already registered')) throw err;`
|
||||
);
|
||||
registrations.push(` }`);
|
||||
registrations.push(` }`);
|
||||
registrations.push(`}`);
|
||||
});
|
||||
registrations.push('');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle manual registration functions (backwards compatibility)
|
||||
for (const [category, config] of Object.entries(definition.providers)) {
|
||||
if (!config) continue;
|
||||
|
||||
if (config.register) {
|
||||
// Async registration function with duplicate prevention
|
||||
registrations.push(`// Register ${category} via custom function (from dexto.image.ts)`);
|
||||
registrations.push(`await (async () => {`);
|
||||
registrations.push(` try {`);
|
||||
registrations.push(
|
||||
` ${config.register
|
||||
.toString()
|
||||
.replace(/^async\s*\(\)\s*=>\s*{/, '')
|
||||
.replace(/}$/, '')}`
|
||||
);
|
||||
registrations.push(` } catch (err) {`);
|
||||
registrations.push(` // Ignore duplicate registration errors`);
|
||||
registrations.push(` if (!err.message?.includes('already registered')) {`);
|
||||
registrations.push(` throw err;`);
|
||||
registrations.push(` }`);
|
||||
registrations.push(` }`);
|
||||
registrations.push(`})();`);
|
||||
registrations.push('');
|
||||
}
|
||||
}
|
||||
|
||||
return registrations.join('\n');
|
||||
}
|
||||
|
||||
function generateFactory(): string {
|
||||
return `/**
|
||||
* Create a Dexto agent using this image's registered providers.
|
||||
*
|
||||
* @param config - Agent configuration
|
||||
* @param configPath - Optional path to config file
|
||||
* @returns DextoAgent instance with providers already registered
|
||||
*/
|
||||
export function createAgent(config, configPath) {
|
||||
return new DextoAgent(config, configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-export registries for runtime customization.
|
||||
* This allows apps to add custom providers without depending on @dexto/core directly.
|
||||
*/
|
||||
export {
|
||||
customToolRegistry,
|
||||
pluginRegistry,
|
||||
compactionRegistry,
|
||||
blobStoreRegistry,
|
||||
} from '@dexto/core';`;
|
||||
}
|
||||
|
||||
function generateUtilityExports(definition: ImageDefinition): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Generate wildcard utility exports
|
||||
if (definition.utils && Object.keys(definition.utils).length > 0) {
|
||||
sections.push('// Utility exports');
|
||||
for (const [name, path] of Object.entries(definition.utils)) {
|
||||
sections.push(`export * from '${path}';`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate selective named exports (filter out type-only exports for runtime JS)
|
||||
if (definition.exports && Object.keys(definition.exports).length > 0) {
|
||||
if (sections.length > 0) sections.push('');
|
||||
sections.push('// Selective package re-exports');
|
||||
for (const [packageName, exports] of Object.entries(definition.exports)) {
|
||||
// Check for wildcard re-export
|
||||
if (exports.length === 1 && exports[0] === '*') {
|
||||
sections.push(`export * from '${packageName}';`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter out type-only exports (those starting with 'type ')
|
||||
const runtimeExports = exports.filter((exp) => !exp.startsWith('type '));
|
||||
if (runtimeExports.length > 0) {
|
||||
sections.push(`export {`);
|
||||
sections.push(` ${runtimeExports.join(',\n ')}`);
|
||||
sections.push(`} from '${packageName}';`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.length === 0) {
|
||||
return '// No utilities or exports defined for this image';
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
function generateMetadata(definition: ImageDefinition, coreVersion: string): string {
|
||||
const metadata: Record<string, any> = {
|
||||
name: definition.name,
|
||||
version: definition.version,
|
||||
description: definition.description,
|
||||
target: definition.target || 'custom',
|
||||
constraints: definition.constraints || [],
|
||||
builtAt: new Date().toISOString(),
|
||||
coreVersion: coreVersion,
|
||||
};
|
||||
|
||||
// Include extends information if present
|
||||
if (definition.extends) {
|
||||
metadata.extends = definition.extends;
|
||||
}
|
||||
|
||||
// Include bundled plugins if present
|
||||
if (definition.bundledPlugins && definition.bundledPlugins.length > 0) {
|
||||
metadata.bundledPlugins = definition.bundledPlugins;
|
||||
}
|
||||
|
||||
return `/**
|
||||
* Image metadata
|
||||
* Generated at build time
|
||||
*/
|
||||
export const imageMetadata = ${JSON.stringify(metadata, null, 4)};`;
|
||||
}
|
||||
|
||||
function generateTypeDefinitions(definition: ImageDefinition): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Wildcard utility exports
|
||||
if (definition.utils && Object.keys(definition.utils).length > 0) {
|
||||
sections.push('// Utility re-exports');
|
||||
for (const path of Object.values(definition.utils)) {
|
||||
sections.push(`export * from '${path}';`);
|
||||
}
|
||||
}
|
||||
|
||||
// Selective named exports
|
||||
if (definition.exports && Object.keys(definition.exports).length > 0) {
|
||||
if (sections.length > 0) sections.push('');
|
||||
sections.push('// Selective package re-exports');
|
||||
for (const [packageName, exports] of Object.entries(definition.exports)) {
|
||||
// Check for wildcard re-export
|
||||
if (exports.length === 1 && exports[0] === '*') {
|
||||
sections.push(`export * from '${packageName}';`);
|
||||
continue;
|
||||
}
|
||||
|
||||
sections.push(`export {`);
|
||||
sections.push(` ${exports.join(',\n ')}`);
|
||||
sections.push(`} from '${packageName}';`);
|
||||
}
|
||||
}
|
||||
|
||||
const utilityExports = sections.length > 0 ? '\n\n' + sections.join('\n') : '';
|
||||
|
||||
return `// AUTO-GENERATED TypeScript definitions
|
||||
// Do not edit this file directly
|
||||
|
||||
import type { DextoAgent, AgentConfig, ImageMetadata } from '@dexto/core';
|
||||
|
||||
/**
|
||||
* Create a Dexto agent using this image's registered providers.
|
||||
*/
|
||||
export declare function createAgent(config: AgentConfig, configPath?: string): DextoAgent;
|
||||
|
||||
/**
|
||||
* Image metadata
|
||||
*/
|
||||
export declare const imageMetadata: ImageMetadata;
|
||||
|
||||
/**
|
||||
* Re-exported registries for runtime customization
|
||||
*/
|
||||
export {
|
||||
customToolRegistry,
|
||||
pluginRegistry,
|
||||
compactionRegistry,
|
||||
blobStoreRegistry,
|
||||
} from '@dexto/core';${utilityExports}
|
||||
`;
|
||||
}
|
||||
9
dexto/packages/image-bundler/src/index.ts
Normal file
9
dexto/packages/image-bundler/src/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @dexto/bundler
|
||||
*
|
||||
* Bundles Dexto base images from dexto.image.ts definitions
|
||||
* into importable packages with side-effect provider registration.
|
||||
*/
|
||||
|
||||
export { bundle } from './bundler.js';
|
||||
export type { BundleOptions, BundleResult, GeneratedCode } from './types.js';
|
||||
30
dexto/packages/image-bundler/src/types.ts
Normal file
30
dexto/packages/image-bundler/src/types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ImageDefinition, ImageMetadata } from '@dexto/core';
|
||||
|
||||
export interface BundleOptions {
|
||||
/** Path to dexto.image.ts file */
|
||||
imagePath: string;
|
||||
/** Output directory for built image */
|
||||
outDir: string;
|
||||
/** Whether to generate source maps */
|
||||
sourcemap?: boolean;
|
||||
/** Whether to minify output */
|
||||
minify?: boolean;
|
||||
}
|
||||
|
||||
export interface BundleResult {
|
||||
/** Path to generated entry file */
|
||||
entryFile: string;
|
||||
/** Path to generated types file */
|
||||
typesFile: string;
|
||||
/** Image metadata */
|
||||
metadata: ImageMetadata;
|
||||
/** Warnings encountered during build */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface GeneratedCode {
|
||||
/** Generated JavaScript code */
|
||||
js: string;
|
||||
/** Generated TypeScript definitions */
|
||||
dts: string;
|
||||
}
|
||||
19
dexto/packages/image-bundler/tsconfig.json
Normal file
19
dexto/packages/image-bundler/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
13
dexto/packages/image-bundler/tsup.config.ts
Normal file
13
dexto/packages/image-bundler/tsup.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts', 'src/cli.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
splitting: false,
|
||||
shims: true,
|
||||
external: ['typescript', '@dexto/core', 'picocolors', 'commander'],
|
||||
noExternal: [],
|
||||
});
|
||||
Reference in New Issue
Block a user