@stan-chen/simple-cli 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +287 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.js +259 -0
  4. package/dist/commands/add.d.ts +9 -0
  5. package/dist/commands/add.js +50 -0
  6. package/dist/commands/git/commit.d.ts +12 -0
  7. package/dist/commands/git/commit.js +97 -0
  8. package/dist/commands/git/status.d.ts +6 -0
  9. package/dist/commands/git/status.js +42 -0
  10. package/dist/commands/index.d.ts +16 -0
  11. package/dist/commands/index.js +376 -0
  12. package/dist/commands/mcp/status.d.ts +6 -0
  13. package/dist/commands/mcp/status.js +31 -0
  14. package/dist/commands/swarm.d.ts +36 -0
  15. package/dist/commands/swarm.js +236 -0
  16. package/dist/commands.d.ts +32 -0
  17. package/dist/commands.js +427 -0
  18. package/dist/context.d.ts +116 -0
  19. package/dist/context.js +327 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +109 -0
  22. package/dist/lib/agent.d.ts +98 -0
  23. package/dist/lib/agent.js +281 -0
  24. package/dist/lib/editor.d.ts +74 -0
  25. package/dist/lib/editor.js +441 -0
  26. package/dist/lib/git.d.ts +164 -0
  27. package/dist/lib/git.js +351 -0
  28. package/dist/lib/ui.d.ts +159 -0
  29. package/dist/lib/ui.js +252 -0
  30. package/dist/mcp/client.d.ts +22 -0
  31. package/dist/mcp/client.js +81 -0
  32. package/dist/mcp/manager.d.ts +186 -0
  33. package/dist/mcp/manager.js +442 -0
  34. package/dist/prompts/provider.d.ts +22 -0
  35. package/dist/prompts/provider.js +78 -0
  36. package/dist/providers/index.d.ts +15 -0
  37. package/dist/providers/index.js +82 -0
  38. package/dist/providers/multi.d.ts +11 -0
  39. package/dist/providers/multi.js +28 -0
  40. package/dist/registry.d.ts +24 -0
  41. package/dist/registry.js +379 -0
  42. package/dist/repoMap.d.ts +5 -0
  43. package/dist/repoMap.js +79 -0
  44. package/dist/router.d.ts +41 -0
  45. package/dist/router.js +108 -0
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +288 -0
  48. package/dist/swarm/coordinator.d.ts +86 -0
  49. package/dist/swarm/coordinator.js +257 -0
  50. package/dist/swarm/index.d.ts +28 -0
  51. package/dist/swarm/index.js +29 -0
  52. package/dist/swarm/task.d.ts +104 -0
  53. package/dist/swarm/task.js +221 -0
  54. package/dist/swarm/types.d.ts +132 -0
  55. package/dist/swarm/types.js +37 -0
  56. package/dist/swarm/worker.d.ts +107 -0
  57. package/dist/swarm/worker.js +299 -0
  58. package/dist/tools/analyzeFile.d.ts +16 -0
  59. package/dist/tools/analyzeFile.js +43 -0
  60. package/dist/tools/git.d.ts +40 -0
  61. package/dist/tools/git.js +236 -0
  62. package/dist/tools/glob.d.ts +34 -0
  63. package/dist/tools/glob.js +165 -0
  64. package/dist/tools/grep.d.ts +53 -0
  65. package/dist/tools/grep.js +296 -0
  66. package/dist/tools/linter.d.ts +35 -0
  67. package/dist/tools/linter.js +349 -0
  68. package/dist/tools/listDir.d.ts +29 -0
  69. package/dist/tools/listDir.js +50 -0
  70. package/dist/tools/memory.d.ts +34 -0
  71. package/dist/tools/memory.js +215 -0
  72. package/dist/tools/readFiles.d.ts +25 -0
  73. package/dist/tools/readFiles.js +31 -0
  74. package/dist/tools/reloadTools.d.ts +11 -0
  75. package/dist/tools/reloadTools.js +22 -0
  76. package/dist/tools/runCommand.d.ts +32 -0
  77. package/dist/tools/runCommand.js +79 -0
  78. package/dist/tools/scraper.d.ts +31 -0
  79. package/dist/tools/scraper.js +211 -0
  80. package/dist/tools/writeFiles.d.ts +63 -0
  81. package/dist/tools/writeFiles.js +87 -0
  82. package/dist/ui/server.d.ts +5 -0
  83. package/dist/ui/server.js +74 -0
  84. package/dist/watcher.d.ts +35 -0
  85. package/dist/watcher.js +164 -0
  86. package/docs/assets/logo.jpeg +0 -0
  87. package/package.json +78 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tool Registry: Auto-imports tools from src/tools/, skills/, and scripts/
3
+ * Supports built-in tools, MCP tools, and project-specific custom tools
4
+ */
5
+ import { z } from 'zod';
6
+ export type Permission = 'read' | 'write' | 'execute';
7
+ export interface Tool {
8
+ name: string;
9
+ description: string;
10
+ permission: Permission;
11
+ inputSchema: z.ZodType;
12
+ execute: (args: Record<string, unknown>) => Promise<unknown>;
13
+ source?: 'builtin' | 'mcp' | 'project';
14
+ serverName?: string;
15
+ specification?: string;
16
+ }
17
+ export type { Tool as ToolModule };
18
+ export declare const loadTools: () => Promise<Map<string, Tool>>;
19
+ export declare const loadAllTools: () => Promise<Map<string, Tool>>;
20
+ export declare const getToolDefinitions: (tools: Map<string, Tool>) => string;
21
+ export declare const validateToolArgs: (tool: Tool, args: unknown) => {
22
+ valid: boolean;
23
+ error?: string;
24
+ };
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Tool Registry: Auto-imports tools from src/tools/, skills/, and scripts/
3
+ * Supports built-in tools, MCP tools, and project-specific custom tools
4
+ */
5
+ import { readdir, readFile, stat } from 'fs/promises';
6
+ import { spawn } from 'child_process';
7
+ import { join, dirname, basename, extname } from 'path';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
9
+ import { z } from 'zod';
10
+ import { getMCPManager } from './mcp/manager.js';
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const TOOLS_DIR = join(__dirname, 'tools');
13
+ /**
14
+ * Extract documentation from the beginning of a file (comments)
15
+ */
16
+ function extractDocFromComments(content) {
17
+ const lines = content.split('\n');
18
+ let doc = '';
19
+ let inDoc = false;
20
+ for (const line of lines) {
21
+ const trimmed = line.trim();
22
+ if (trimmed.startsWith('/**') || trimmed.startsWith('"""')) {
23
+ inDoc = true;
24
+ continue;
25
+ }
26
+ if (inDoc && (trimmed.endsWith('*/') || trimmed.endsWith('"""'))) {
27
+ inDoc = false;
28
+ break;
29
+ }
30
+ if (inDoc) {
31
+ doc += trimmed.replace(/^\* ?/, '') + '\n';
32
+ }
33
+ else if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
34
+ doc += trimmed.replace(/^(\/\/|#) ?/, '') + '\n';
35
+ }
36
+ else if (trimmed && !trimmed.startsWith('import') && !trimmed.startsWith('from')) {
37
+ break;
38
+ }
39
+ }
40
+ return doc.trim();
41
+ }
42
+ /**
43
+ * Parses a tool definition from a Markdown file (.md) or string
44
+ */
45
+ function parseMarkdownTool(content, filename) {
46
+ const lines = content.split('\n');
47
+ const meta = {
48
+ name: basename(filename, extname(filename)),
49
+ description: '',
50
+ command: '',
51
+ parameters: {},
52
+ permission: 'execute'
53
+ };
54
+ let currentSection = '';
55
+ for (const line of lines) {
56
+ if (line.startsWith('# ')) {
57
+ meta.name = line.replace('# ', '').trim();
58
+ }
59
+ else if (line.startsWith('## ')) {
60
+ currentSection = line.replace('## ', '').trim().toLowerCase();
61
+ }
62
+ else if (currentSection === 'command' && line.trim()) {
63
+ meta.command = line.trim();
64
+ }
65
+ else if (currentSection === 'parameters' && line.trim().startsWith('- ')) {
66
+ const match = line.match(/- (\w+): (\w+)(?: - (.+))?/);
67
+ if (match) {
68
+ meta.parameters[match[1]] = { type: match[2], description: match[3] || '' };
69
+ }
70
+ }
71
+ else if (!currentSection && line.trim() && !line.startsWith('#')) {
72
+ meta.description += line.trim() + ' ';
73
+ }
74
+ }
75
+ meta.description = meta.description.trim();
76
+ return meta;
77
+ }
78
+ // Helper to create a Tool from metadata (JSON or MD)
79
+ function createScriptTool(meta, source, spec) {
80
+ return {
81
+ name: meta.name,
82
+ description: meta.description || `Script tool: ${meta.command}`,
83
+ permission: meta.permission || 'execute',
84
+ inputSchema: z.object({}).passthrough(), // Flexible schema for scripts
85
+ source,
86
+ specification: spec,
87
+ execute: async (args) => {
88
+ return new Promise((resolve, reject) => {
89
+ let finalCommand = meta.command;
90
+ const isWindows = process.platform === 'win32';
91
+ if (isWindows && finalCommand.endsWith('.ps1')) {
92
+ finalCommand = `powershell -ExecutionPolicy Bypass -File ${finalCommand}`;
93
+ }
94
+ const child = spawn(finalCommand, {
95
+ shell: true,
96
+ cwd: process.cwd(),
97
+ env: { ...process.env, TOOL_INPUT: JSON.stringify(args) },
98
+ });
99
+ let stdout = '';
100
+ let stderr = '';
101
+ child.stdout?.on('data', (data) => stdout += data.toString());
102
+ child.stderr?.on('data', (data) => stderr += data.toString());
103
+ child.on('close', (code) => {
104
+ if (code === 0)
105
+ resolve(stdout.trim());
106
+ else
107
+ reject(new Error(`Exit ${code}: ${stderr.trim()}`));
108
+ });
109
+ child.stdin?.write(JSON.stringify(args));
110
+ child.stdin?.end();
111
+ });
112
+ }
113
+ };
114
+ }
115
+ /**
116
+ * Recursively search for a documentation file (.md or .json) for a given tool name
117
+ */
118
+ async function findDocInDir(dir, baseName) {
119
+ const files = await readdir(dir);
120
+ for (const f of files) {
121
+ if (basename(f, extname(f)) === baseName && (f.endsWith('.md') || f.endsWith('.json') || f.endsWith('.txt'))) {
122
+ const content = await readFile(join(dir, f), 'utf-8');
123
+ return { content, file: f };
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ // Helper to load tools from a specific directory (recursive)
129
+ async function loadToolsFromDir(dir, source) {
130
+ const tools = new Map();
131
+ try {
132
+ const items = await readdir(dir);
133
+ for (const item of items) {
134
+ const fullPath = join(dir, item);
135
+ const s = await stat(fullPath);
136
+ if (s.isDirectory()) {
137
+ // Recursive call for subdirectories
138
+ const subTools = await loadToolsFromDir(fullPath, source);
139
+ for (const [name, tool] of subTools) {
140
+ tools.set(name, tool);
141
+ }
142
+ // Special case: Folder named ' heavy_lifting' might have 'heavy_lifting.md' inside it
143
+ if (!tools.has(item)) {
144
+ const doc = await findDocInDir(fullPath, item);
145
+ if (doc && doc.file.endsWith('.md')) {
146
+ const meta = parseMarkdownTool(doc.content, doc.file);
147
+ if (meta && meta.command) {
148
+ tools.set(meta.name, createScriptTool(meta, source, doc.content));
149
+ }
150
+ }
151
+ }
152
+ continue;
153
+ }
154
+ const ext = extname(item);
155
+ const base = basename(item, ext);
156
+ // 1. Native Node tools
157
+ if (ext === '.ts' || ext === '.js' || ext === '.mjs') {
158
+ if (item.includes('.test.'))
159
+ continue;
160
+ try {
161
+ const module = await import(pathToFileURL(fullPath).href);
162
+ const toolDef = module.tool || module;
163
+ if (toolDef.name && toolDef.execute) {
164
+ const schema = toolDef.inputSchema || toolDef.schema;
165
+ tools.set(toolDef.name, {
166
+ name: toolDef.name,
167
+ description: toolDef.description || 'No description',
168
+ permission: toolDef.permission || 'read',
169
+ inputSchema: schema || z.object({}),
170
+ execute: toolDef.execute,
171
+ source,
172
+ });
173
+ continue;
174
+ }
175
+ }
176
+ catch { /* might be a script, fall through */ }
177
+ }
178
+ // 2. Generic Script with internal docs or companion meta file
179
+ if (ext !== '.md' && ext !== '.txt' && ext !== '.json') {
180
+ try {
181
+ const content = await readFile(fullPath, 'utf-8');
182
+ const internalDoc = extractDocFromComments(content);
183
+ // Check for companion meta file in the SAME directory
184
+ const companion = await findDocInDir(dir, base);
185
+ let meta = null;
186
+ let specContent = internalDoc;
187
+ if (companion) {
188
+ if (companion.file.endsWith('.json')) {
189
+ meta = JSON.parse(companion.content);
190
+ }
191
+ else {
192
+ meta = parseMarkdownTool(companion.content, companion.file);
193
+ specContent = companion.content;
194
+ }
195
+ }
196
+ else if (internalDoc) {
197
+ meta = parseMarkdownTool(internalDoc, item);
198
+ if (!meta.command) {
199
+ if (ext === '.py')
200
+ meta.command = `python ${fullPath}`;
201
+ else if (ext === '.sh')
202
+ meta.command = `bash ${fullPath}`;
203
+ else if (ext === '.ps1')
204
+ meta.command = `powershell ${fullPath}`;
205
+ else
206
+ meta.command = fullPath;
207
+ }
208
+ }
209
+ if (meta && meta.name && meta.command) {
210
+ tools.set(meta.name, createScriptTool(meta, source, specContent));
211
+ }
212
+ }
213
+ catch { /* skip errors */ }
214
+ }
215
+ // 3. Standalone Meta/Documentation Tools
216
+ if (ext === '.json' || ext === '.md' || ext === '.txt') {
217
+ if (Array.from(tools.values()).some(t => t.name === base))
218
+ continue;
219
+ try {
220
+ const content = await readFile(fullPath, 'utf-8');
221
+ let meta = null;
222
+ if (ext === '.json') {
223
+ meta = JSON.parse(content);
224
+ }
225
+ else {
226
+ meta = parseMarkdownTool(content, item);
227
+ }
228
+ if (meta && meta.name && meta.command) {
229
+ tools.set(meta.name, createScriptTool(meta, source, content));
230
+ }
231
+ }
232
+ catch { /* skip */ }
233
+ }
234
+ }
235
+ }
236
+ catch (error) {
237
+ // Directory might not exist
238
+ }
239
+ return tools;
240
+ }
241
+ // Load all tools from the tools directory
242
+ export const loadTools = async () => {
243
+ const customDirs = ['skills', 'scripts', 'tools', '.simple-cli/tools'];
244
+ const builtinTools = await loadToolsFromDir(TOOLS_DIR, 'builtin');
245
+ const allProjectTools = new Map();
246
+ for (const d of customDirs) {
247
+ const dirPath = join(process.cwd(), d);
248
+ const tools = await loadToolsFromDir(dirPath, 'project');
249
+ for (const [name, tool] of tools) {
250
+ allProjectTools.set(name, tool);
251
+ }
252
+ }
253
+ return new Map([...builtinTools, ...allProjectTools]);
254
+ };
255
+ // Load MCP tools and merge with built-in tools
256
+ export const loadAllTools = async () => {
257
+ const tools = await loadTools();
258
+ try {
259
+ const mcpManager = getMCPManager();
260
+ const mcpTools = mcpManager.getAllTools();
261
+ for (const mcpTool of mcpTools) {
262
+ const toolName = `mcp_${mcpTool.serverName}_${mcpTool.name}`;
263
+ tools.set(toolName, {
264
+ name: toolName,
265
+ description: mcpTool.description,
266
+ permission: 'execute',
267
+ inputSchema: z.object(mcpTool.inputSchema).passthrough(),
268
+ execute: mcpTool.execute,
269
+ source: 'mcp',
270
+ serverName: mcpTool.serverName,
271
+ });
272
+ }
273
+ }
274
+ catch {
275
+ // MCP not configured, skip
276
+ }
277
+ return tools;
278
+ };
279
+ // Get tool definitions for LLM prompt
280
+ export const getToolDefinitions = (tools) => {
281
+ const sections = [];
282
+ const builtinTools = [];
283
+ const projectTools = [];
284
+ const mcpTools = [];
285
+ for (const tool of tools.values()) {
286
+ if (tool.source === 'mcp') {
287
+ mcpTools.push(tool);
288
+ }
289
+ else if (tool.source === 'project') {
290
+ projectTools.push(tool);
291
+ }
292
+ else {
293
+ builtinTools.push(tool);
294
+ }
295
+ }
296
+ if (builtinTools.length > 0) {
297
+ sections.push('## Built-in Tools\n');
298
+ for (const tool of builtinTools) {
299
+ sections.push(formatToolDefinition(tool));
300
+ }
301
+ }
302
+ if (projectTools.length > 0) {
303
+ sections.push('\n## Project Skills (Custom Tools)\n');
304
+ for (const tool of projectTools) {
305
+ sections.push(formatToolDefinition(tool));
306
+ }
307
+ }
308
+ if (mcpTools.length > 0) {
309
+ sections.push('\n## MCP Tools\n');
310
+ for (const tool of mcpTools) {
311
+ sections.push(formatToolDefinition(tool));
312
+ }
313
+ }
314
+ return sections.join('\n');
315
+ };
316
+ // Format a single tool definition
317
+ function formatToolDefinition(tool) {
318
+ const lines = [
319
+ `### ${tool.name}`,
320
+ tool.description,
321
+ `Permission: ${tool.permission}`,
322
+ ];
323
+ if (tool.serverName) {
324
+ lines.push(`Server: ${tool.serverName}`);
325
+ }
326
+ // Extract parameters from schema
327
+ if (tool.inputSchema && 'shape' in tool.inputSchema) {
328
+ const shape = tool.inputSchema.shape;
329
+ const params = Object.entries(shape)
330
+ .map(([key, value]) => {
331
+ const zodType = value;
332
+ const description = zodType.description || '';
333
+ const typeName = getTypeName(zodType);
334
+ const optional = zodType.isOptional() ? '?' : '';
335
+ return ` - ${key}${optional}: ${typeName}${description ? ` - ${description}` : ''}`;
336
+ })
337
+ .join('\n');
338
+ if (params) {
339
+ lines.push('Parameters:');
340
+ lines.push(params);
341
+ }
342
+ }
343
+ return lines.join('\n') + '\n';
344
+ }
345
+ // Get human-readable type name from Zod type
346
+ function getTypeName(zodType) {
347
+ const def = zodType._def;
348
+ if (def.typeName === 'ZodString')
349
+ return 'string';
350
+ if (def.typeName === 'ZodNumber')
351
+ return 'number';
352
+ if (def.typeName === 'ZodBoolean')
353
+ return 'boolean';
354
+ if (def.typeName === 'ZodArray')
355
+ return 'array';
356
+ if (def.typeName === 'ZodObject')
357
+ return 'object';
358
+ if (def.typeName === 'ZodEnum')
359
+ return `enum(${def.values.join('|')})`;
360
+ if (def.typeName === 'ZodOptional')
361
+ return getTypeName(def.innerType);
362
+ if (def.typeName === 'ZodDefault')
363
+ return getTypeName(def.innerType);
364
+ return def.typeName?.replace('Zod', '').toLowerCase() || 'unknown';
365
+ }
366
+ // Validate tool arguments
367
+ export const validateToolArgs = (tool, args) => {
368
+ try {
369
+ tool.inputSchema.parse(args);
370
+ return { valid: true };
371
+ }
372
+ catch (error) {
373
+ if (error instanceof z.ZodError) {
374
+ const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
375
+ return { valid: false, error: messages.join('; ') };
376
+ }
377
+ return { valid: false, error: String(error) };
378
+ }
379
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * RepoMap: Symbol-aware context generation
3
+ * Uses ts-morph for TypeScript/JavaScript and simple parsing for others.
4
+ */
5
+ export declare const generateRepoMap: (rootDir?: string) => Promise<string>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * RepoMap: Symbol-aware context generation
3
+ * Uses ts-morph for TypeScript/JavaScript and simple parsing for others.
4
+ */
5
+ import { Project, ScriptTarget } from 'ts-morph';
6
+ import { readdir } from 'fs/promises';
7
+ import { join, extname, relative } from 'path';
8
+ const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']);
9
+ const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
10
+ export const generateRepoMap = async (rootDir = '.') => {
11
+ const fileMaps = [];
12
+ // Initialize ts-morph project
13
+ const project = new Project({
14
+ compilerOptions: { target: ScriptTarget.ESNext, allowJs: true },
15
+ skipAddingFilesFromTsConfig: true,
16
+ useInMemoryFileSystem: true // Don't actually load files until we say so
17
+ });
18
+ const stack = [rootDir];
19
+ const validFiles = [];
20
+ // 1. Walk directory to find files
21
+ while (stack.length > 0) {
22
+ const dir = stack.pop();
23
+ try {
24
+ const entries = await readdir(dir, { withFileTypes: true });
25
+ for (const entry of entries) {
26
+ if (IGNORED_DIRS.has(entry.name))
27
+ continue;
28
+ const fullPath = join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ stack.push(fullPath);
31
+ }
32
+ else if (entry.isFile()) {
33
+ validFiles.push(fullPath);
34
+ }
35
+ }
36
+ }
37
+ catch { /* ignore access errors */ }
38
+ }
39
+ // 2. Process Files
40
+ // Limit to 50 files for now to avoid context explosion
41
+ const filesToProcess = validFiles.slice(0, 50);
42
+ for (const filePath of filesToProcess) {
43
+ const ext = extname(filePath);
44
+ const relPath = relative(rootDir, filePath);
45
+ if (TS_EXTENSIONS.has(ext)) {
46
+ try {
47
+ // Use ts-morph
48
+ const sourceFile = project.createSourceFile(filePath, await import('fs/promises').then(fs => fs.readFile(filePath, 'utf-8')), { overwrite: true });
49
+ const symbols = [];
50
+ sourceFile.getClasses().forEach(c => symbols.push(`class ${c.getName()}`));
51
+ sourceFile.getFunctions().forEach(f => symbols.push(`func ${f.getName()}`));
52
+ sourceFile.getInterfaces().forEach(i => symbols.push(`interface ${i.getName()}`));
53
+ sourceFile.getTypeAliases().forEach(t => symbols.push(`type ${t.getName()}`));
54
+ sourceFile.getVariableStatements().forEach(v => {
55
+ v.getDeclarations().forEach(d => symbols.push(`const ${d.getName()}`));
56
+ });
57
+ if (symbols.length > 0) {
58
+ fileMaps.push({ path: relPath, symbols });
59
+ }
60
+ }
61
+ catch (e) {
62
+ // Fallback or ignore
63
+ }
64
+ }
65
+ else {
66
+ // Simple listing for non-TS files? Or just skip symbols to keep it clean.
67
+ // For now, let's just list the file path for completeness if it's source code
68
+ fileMaps.push({ path: relPath, symbols: [] });
69
+ }
70
+ }
71
+ if (fileMaps.length === 0)
72
+ return 'No source files found.';
73
+ return fileMaps.map(fm => {
74
+ if (fm.symbols.length === 0)
75
+ return `📄 ${fm.path}`;
76
+ const syms = fm.symbols.map(s => ` ${s}`).join('\n');
77
+ return `📄 ${fm.path}\n${syms}`;
78
+ }).join('\n\n');
79
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MoE Router: Mix of Experts task routing
3
+ * Routes tasks to appropriate model tiers based on complexity
4
+ */
5
+ import { z } from 'zod';
6
+ export type Tier = 1 | 2 | 3 | 4 | 5;
7
+ export interface TierConfig {
8
+ tier: Tier;
9
+ role: string;
10
+ model: string;
11
+ provider: 'openai' | 'anthropic' | 'gemini';
12
+ }
13
+ export interface RoutingDecision {
14
+ tier: Tier;
15
+ complexity: number;
16
+ contextRequired: 'high' | 'low';
17
+ risk: 'high' | 'low';
18
+ reasoning: string;
19
+ }
20
+ export declare const RoutingResponseSchema: z.ZodObject<{
21
+ complexity: z.ZodNumber;
22
+ contextRequired: z.ZodEnum<["high", "low"]>;
23
+ risk: z.ZodEnum<["high", "low"]>;
24
+ recommendedTier: z.ZodNumber;
25
+ reasoning: z.ZodString;
26
+ }, "strip", z.ZodTypeAny, {
27
+ complexity: number;
28
+ contextRequired: "high" | "low";
29
+ risk: "high" | "low";
30
+ recommendedTier: number;
31
+ reasoning: string;
32
+ }, {
33
+ complexity: number;
34
+ contextRequired: "high" | "low";
35
+ risk: "high" | "low";
36
+ recommendedTier: number;
37
+ reasoning: string;
38
+ }>;
39
+ export declare const loadTierConfig: () => Map<Tier, TierConfig>;
40
+ export declare const routeTask: (task: string, orchestratorCall: (prompt: string) => Promise<string>) => Promise<RoutingDecision>;
41
+ export declare const formatRoutingDecision: (decision: RoutingDecision, tiers: Map<Tier, TierConfig>) => string;
package/dist/router.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * MoE Router: Mix of Experts task routing
3
+ * Routes tasks to appropriate model tiers based on complexity
4
+ */
5
+ import { z } from 'zod';
6
+ // Schema for Orchestrator's routing response
7
+ export const RoutingResponseSchema = z.object({
8
+ complexity: z.number().min(1).max(10),
9
+ contextRequired: z.enum(['high', 'low']),
10
+ risk: z.enum(['high', 'low']),
11
+ recommendedTier: z.number().min(1).max(5),
12
+ reasoning: z.string()
13
+ });
14
+ // Default tier configurations
15
+ const DEFAULT_TIERS = {
16
+ 1: { role: 'Orchestrator / Architect', defaultModel: 'gpt-5.2-pro' },
17
+ 2: { role: 'Senior Engineer', defaultModel: 'gpt-5.2-codex' },
18
+ 3: { role: 'Junior Engineer', defaultModel: 'gpt-5-mini' },
19
+ 4: { role: 'Intern / Linter', defaultModel: 'gpt-5-nano' },
20
+ 5: { role: 'Safety / Utility', defaultModel: 'gemini-2.5-flash' }
21
+ };
22
+ // Load tier configuration from environment
23
+ export const loadTierConfig = () => {
24
+ const tiers = new Map();
25
+ for (const tier of [1, 2, 3, 4, 5]) {
26
+ const envModel = process.env[`MOE_TIER_${tier}_MODEL`];
27
+ const model = envModel || DEFAULT_TIERS[tier].defaultModel;
28
+ // LiteLLM auto-detects provider from model name prefix (e.g., "anthropic/claude-3", "gemini/gemini-pro")
29
+ // For OpenAI models without prefix, it defaults to OpenAI
30
+ const provider = model.includes('/') ? model.split('/')[0] : 'openai';
31
+ tiers.set(tier, {
32
+ tier,
33
+ role: DEFAULT_TIERS[tier].role,
34
+ model,
35
+ provider
36
+ });
37
+ }
38
+ return tiers;
39
+ };
40
+ // Routing prompt template
41
+ const ROUTING_PROMPT = `You are a task router for an AI coding assistant. Analyze the following task and determine its complexity.
42
+
43
+ Respond ONLY with valid JSON in this exact format:
44
+ {
45
+ "complexity": <1-10>,
46
+ "contextRequired": "<high|low>",
47
+ "risk": "<high|low>",
48
+ "recommendedTier": <1-5>,
49
+ "reasoning": "<brief explanation>"
50
+ }
51
+
52
+ Tier Guidelines:
53
+ - Tier 1: Complex architecture, critical decisions, multi-system refactoring
54
+ - Tier 2: Feature implementation, significant code changes, debugging complex issues
55
+ - Tier 3: Routine tasks, unit tests, boilerplate code, simple features
56
+ - Tier 4: Typo fixes, formatting, simple imports, trivial changes
57
+ - Tier 5: Basic text operations, summaries
58
+
59
+ Task to analyze:
60
+ `;
61
+ // Determine routing using Tier 1 orchestrator
62
+ export const routeTask = async (task, orchestratorCall) => {
63
+ try {
64
+ const response = await orchestratorCall(ROUTING_PROMPT + task);
65
+ // Extract JSON from response
66
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
67
+ if (!jsonMatch) {
68
+ return getDefaultRouting(task);
69
+ }
70
+ const parsed = RoutingResponseSchema.parse(JSON.parse(jsonMatch[0]));
71
+ return {
72
+ tier: parsed.recommendedTier,
73
+ complexity: parsed.complexity,
74
+ contextRequired: parsed.contextRequired,
75
+ risk: parsed.risk,
76
+ reasoning: parsed.reasoning
77
+ };
78
+ }
79
+ catch (error) {
80
+ console.error('Routing error, using default:', error);
81
+ return getDefaultRouting(task);
82
+ }
83
+ };
84
+ // Fallback routing based on simple heuristics
85
+ const getDefaultRouting = (task) => {
86
+ const taskLower = task.toLowerCase();
87
+ // Simple keyword-based routing
88
+ if (taskLower.match(/refactor|architect|design|migrate|security|auth/)) {
89
+ return { tier: 2, complexity: 7, contextRequired: 'high', risk: 'high', reasoning: 'Complex task keywords detected' };
90
+ }
91
+ if (taskLower.match(/implement|feature|add|create|build|debug/)) {
92
+ return { tier: 2, complexity: 6, contextRequired: 'high', risk: 'low', reasoning: 'Implementation task detected' };
93
+ }
94
+ if (taskLower.match(/test|boilerplate|template|simple/)) {
95
+ return { tier: 3, complexity: 4, contextRequired: 'low', risk: 'low', reasoning: 'Routine task detected' };
96
+ }
97
+ if (taskLower.match(/typo|fix|format|import|rename/)) {
98
+ return { tier: 4, complexity: 2, contextRequired: 'low', risk: 'low', reasoning: 'Minor task detected' };
99
+ }
100
+ // Default to Tier 3
101
+ return { tier: 3, complexity: 5, contextRequired: 'low', risk: 'low', reasoning: 'Default routing' };
102
+ };
103
+ // Format routing decision for logging
104
+ export const formatRoutingDecision = (decision, tiers) => {
105
+ const tierConfig = tiers.get(decision.tier);
106
+ return `[Router] Tier ${decision.tier} (${tierConfig?.role}) | Complexity: ${decision.complexity}/10 | Model: ${tierConfig?.model}
107
+ Reasoning: ${decision.reasoning}`;
108
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Skills/Presets System
3
+ * Based on OpenHands skills and Aider prompts
4
+ * Provides specialized behavior presets for different tasks
5
+ */
6
+ export interface Skill {
7
+ name: string;
8
+ description: string;
9
+ systemPrompt: string;
10
+ tools?: string[];
11
+ modelPreference?: string;
12
+ autoActions?: string[];
13
+ }
14
+ export declare const builtinSkills: Record<string, Skill>;
15
+ export declare function getActiveSkill(): Skill;
16
+ export declare function setActiveSkill(name: string): Skill | undefined;
17
+ export declare function listSkills(): Skill[];
18
+ export declare function loadSkillFromFile(path: string): Promise<Skill | null>;
19
+ export declare function saveSkillToFile(skill: Skill, path: string): Promise<void>;
20
+ export declare function loadCustomSkills(dir: string): Promise<Record<string, Skill>>;
21
+ export declare function buildSkillPrompt(skill: Skill, context?: {
22
+ files?: string[];
23
+ repoMap?: string;
24
+ history?: string;
25
+ }): string;