ted-mosby 1.0.0

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +238 -0
  3. package/dist/agent.d.ts +37 -0
  4. package/dist/agent.d.ts.map +1 -0
  5. package/dist/agent.js +247 -0
  6. package/dist/agent.js.map +1 -0
  7. package/dist/claude-config.d.ts +58 -0
  8. package/dist/claude-config.d.ts.map +1 -0
  9. package/dist/claude-config.js +169 -0
  10. package/dist/claude-config.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +1379 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +13 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +42 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/mcp-config.d.ts +45 -0
  20. package/dist/mcp-config.d.ts.map +1 -0
  21. package/dist/mcp-config.js +111 -0
  22. package/dist/mcp-config.js.map +1 -0
  23. package/dist/permissions.d.ts +32 -0
  24. package/dist/permissions.d.ts.map +1 -0
  25. package/dist/permissions.js +165 -0
  26. package/dist/permissions.js.map +1 -0
  27. package/dist/planner.d.ts +42 -0
  28. package/dist/planner.d.ts.map +1 -0
  29. package/dist/planner.js +232 -0
  30. package/dist/planner.js.map +1 -0
  31. package/dist/prompts/wiki-system.d.ts +8 -0
  32. package/dist/prompts/wiki-system.d.ts.map +1 -0
  33. package/dist/prompts/wiki-system.js +249 -0
  34. package/dist/prompts/wiki-system.js.map +1 -0
  35. package/dist/rag/index.d.ts +84 -0
  36. package/dist/rag/index.d.ts.map +1 -0
  37. package/dist/rag/index.js +446 -0
  38. package/dist/rag/index.js.map +1 -0
  39. package/dist/tools/command-runner.d.ts +21 -0
  40. package/dist/tools/command-runner.d.ts.map +1 -0
  41. package/dist/tools/command-runner.js +67 -0
  42. package/dist/tools/command-runner.js.map +1 -0
  43. package/dist/tools/file-operations.d.ts +22 -0
  44. package/dist/tools/file-operations.d.ts.map +1 -0
  45. package/dist/tools/file-operations.js +119 -0
  46. package/dist/tools/file-operations.js.map +1 -0
  47. package/dist/tools/web-tools.d.ts +17 -0
  48. package/dist/tools/web-tools.d.ts.map +1 -0
  49. package/dist/tools/web-tools.js +122 -0
  50. package/dist/tools/web-tools.js.map +1 -0
  51. package/dist/wiki-agent.d.ts +81 -0
  52. package/dist/wiki-agent.d.ts.map +1 -0
  53. package/dist/wiki-agent.js +552 -0
  54. package/dist/wiki-agent.js.map +1 -0
  55. package/dist/workflows.d.ts +53 -0
  56. package/dist/workflows.d.ts.map +1 -0
  57. package/dist/workflows.js +169 -0
  58. package/dist/workflows.js.map +1 -0
  59. package/package.json +67 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1379 @@
1
+ #!/usr/bin/env node
2
+ import { config as loadEnv } from 'dotenv';
3
+ import { resolve } from 'path';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { Command } from 'commander';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import inquirer from 'inquirer';
10
+ import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
11
+ import { DevelopmentAgentAgent } from './agent.js';
12
+ import { ArchitecturalWikiAgent } from './wiki-agent.js';
13
+ import { ConfigManager } from './config.js';
14
+ import { PermissionManager } from './permissions.js';
15
+ import { PlanManager, formatAge } from './planner.js';
16
+ import { MCPConfigManager } from './mcp-config.js';
17
+ import { loadClaudeConfig, getCommand, expandCommand } from './claude-config.js';
18
+ // Register autocomplete prompt
19
+ inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
20
+ // Load .env from current working directory (supports global installation)
21
+ const workingDir = process.cwd();
22
+ loadEnv({ path: resolve(workingDir, '.env') });
23
+ // Load Claude Code configuration (skills, commands, memory)
24
+ const claudeConfig = loadClaudeConfig(workingDir);
25
+ const program = new Command();
26
+ program
27
+ .name('ted-mosby')
28
+ .description('Generate architectural documentation wikis for code repositories with source traceability.')
29
+ .version('1.0.0');
30
+ // Config command
31
+ program
32
+ .command('config')
33
+ .description('Configure the agent')
34
+ .option('--show', 'Show current configuration')
35
+ .action(async (options) => {
36
+ const configManager = new ConfigManager();
37
+ await configManager.load();
38
+ if (options.show) {
39
+ const config = configManager.get();
40
+ console.log(chalk.cyan('\nšŸ“‹ Current Configuration:'));
41
+ console.log(chalk.gray('API Key:'), config.apiKey ? '***' + config.apiKey.slice(-4) : chalk.red('Not set'));
42
+ console.log(chalk.gray('Source:'), process.env.CLAUDE_API_KEY ? 'Environment variable' : 'Config file');
43
+ return;
44
+ }
45
+ console.log(chalk.yellow('\nšŸ” API Key Configuration\n'));
46
+ console.log(chalk.white('To configure your API key, create a .env file in the project root:\n'));
47
+ console.log(chalk.gray(' echo "CLAUDE_API_KEY=your-key-here" > .env\n'));
48
+ console.log(chalk.white('Or set the environment variable directly:\n'));
49
+ console.log(chalk.gray(' export CLAUDE_API_KEY=your-key-here\n'));
50
+ console.log(chalk.cyan('Tip: Copy .env.example to .env and fill in your API key.'));
51
+ });
52
+ // Generate command - main wiki generation functionality
53
+ program
54
+ .command('generate')
55
+ .description('Generate architectural documentation wiki for a repository')
56
+ .requiredOption('-r, --repo <url>', 'Repository URL (GitHub/GitLab) or local path')
57
+ .option('-o, --output <dir>', 'Output directory for wiki', './wiki')
58
+ .option('-c, --config <file>', 'Path to wiki configuration file (wiki.json)')
59
+ .option('-t, --token <token>', 'Access token for private repositories')
60
+ .option('-m, --model <model>', 'Claude model to use', 'claude-sonnet-4-20250514')
61
+ .option('-p, --path <path>', 'Specific path within repo to focus on')
62
+ .option('-f, --force', 'Force regeneration (ignore cache)')
63
+ .option('-v, --verbose', 'Verbose output')
64
+ .option('-e, --estimate', 'Estimate time and cost without running (dry run)')
65
+ .action(async (options) => {
66
+ try {
67
+ const configManager = new ConfigManager();
68
+ const config = await configManager.load();
69
+ if (!configManager.hasApiKey()) {
70
+ console.log(chalk.red('āŒ No API key found.'));
71
+ console.log(chalk.yellow('\nSet your Anthropic API key:'));
72
+ console.log(chalk.gray(' export ANTHROPIC_API_KEY=your-key-here'));
73
+ process.exit(1);
74
+ }
75
+ console.log(chalk.cyan.bold('\nšŸ“š ArchitecturalWiki Generator\n'));
76
+ console.log(chalk.white('Repository:'), chalk.green(options.repo));
77
+ console.log(chalk.white('Output:'), chalk.green(path.resolve(options.output)));
78
+ if (options.path)
79
+ console.log(chalk.white('Focus path:'), chalk.green(options.path));
80
+ console.log();
81
+ const permissionManager = new PermissionManager({ policy: 'permissive' });
82
+ const agent = new ArchitecturalWikiAgent({
83
+ verbose: options.verbose,
84
+ apiKey: config.apiKey,
85
+ permissionManager
86
+ });
87
+ // Handle estimate mode (dry run)
88
+ if (options.estimate) {
89
+ const spinner = ora('Analyzing repository...').start();
90
+ try {
91
+ const estimate = await agent.estimateGeneration({
92
+ repoUrl: options.repo,
93
+ outputDir: options.output,
94
+ accessToken: options.token || process.env.GITHUB_TOKEN
95
+ });
96
+ spinner.succeed('Analysis complete');
97
+ console.log();
98
+ // Display estimate
99
+ console.log(chalk.cyan.bold('šŸ“Š Generation Estimate\n'));
100
+ console.log(chalk.white('Files to process:'), chalk.yellow(estimate.files.toString()));
101
+ console.log(chalk.white('Estimated chunks:'), chalk.yellow(estimate.estimatedChunks.toString()));
102
+ console.log(chalk.white('Estimated tokens:'), chalk.yellow(estimate.estimatedTokens.toLocaleString()));
103
+ console.log();
104
+ console.log(chalk.white.bold('ā±ļø Estimated Time'));
105
+ console.log(chalk.gray(' Indexing:'), chalk.yellow(`${estimate.estimatedTime.indexingMinutes} min`));
106
+ console.log(chalk.gray(' Generation:'), chalk.yellow(`${estimate.estimatedTime.generationMinutes} min`));
107
+ console.log(chalk.gray(' Total:'), chalk.green.bold(`~${estimate.estimatedTime.totalMinutes} min`));
108
+ console.log();
109
+ console.log(chalk.white.bold('šŸ’° Estimated Cost (Claude Sonnet)'));
110
+ console.log(chalk.gray(' Input tokens:'), chalk.yellow(`$${estimate.estimatedCost.input.toFixed(2)}`));
111
+ console.log(chalk.gray(' Output tokens:'), chalk.yellow(`$${estimate.estimatedCost.output.toFixed(2)}`));
112
+ console.log(chalk.gray(' Total:'), chalk.green.bold(`$${estimate.estimatedCost.total.toFixed(2)}`));
113
+ console.log();
114
+ console.log(chalk.white.bold('šŸ“ Files by Type'));
115
+ const sortedExts = Object.entries(estimate.breakdown.byExtension)
116
+ .sort(([, a], [, b]) => b - a)
117
+ .slice(0, 10);
118
+ for (const [ext, count] of sortedExts) {
119
+ console.log(chalk.gray(` ${ext}:`), chalk.yellow(count.toString()));
120
+ }
121
+ console.log();
122
+ if (estimate.breakdown.largestFiles.length > 0) {
123
+ console.log(chalk.white.bold('šŸ“„ Largest Files'));
124
+ for (const file of estimate.breakdown.largestFiles.slice(0, 5)) {
125
+ const sizeKb = Math.round(file.size / 1024);
126
+ console.log(chalk.gray(` ${file.path}`), chalk.yellow(`(${sizeKb} KB)`));
127
+ }
128
+ console.log();
129
+ }
130
+ console.log(chalk.gray('Run without --estimate to start generation.\n'));
131
+ }
132
+ catch (error) {
133
+ spinner.fail('Analysis failed');
134
+ throw error;
135
+ }
136
+ return;
137
+ }
138
+ const spinner = ora('Starting wiki generation...').start();
139
+ let currentPhase = '';
140
+ try {
141
+ for await (const event of agent.generateWiki({
142
+ repoUrl: options.repo,
143
+ outputDir: options.output,
144
+ configPath: options.config,
145
+ accessToken: options.token || process.env.GITHUB_TOKEN,
146
+ model: options.model,
147
+ targetPath: options.path,
148
+ forceRegenerate: options.force,
149
+ verbose: options.verbose
150
+ })) {
151
+ // Handle progress events
152
+ if (event.type === 'phase') {
153
+ // Stop spinner during indexing phase so RAG output is visible
154
+ if (event.message.includes('Indexing')) {
155
+ spinner.stop();
156
+ console.log(chalk.cyan(`\nšŸ“Š ${event.message}`));
157
+ }
158
+ else {
159
+ spinner.text = event.message;
160
+ }
161
+ currentPhase = event.message;
162
+ if (options.verbose) {
163
+ spinner.succeed(currentPhase);
164
+ spinner.start(event.message);
165
+ }
166
+ }
167
+ else if (event.type === 'step') {
168
+ // Resume spinner after indexing completes
169
+ if (event.message.includes('Indexed')) {
170
+ console.log(chalk.green(` āœ“ ${event.message}`));
171
+ spinner.start('Generating architectural documentation');
172
+ }
173
+ else if (options.verbose) {
174
+ spinner.info(event.message);
175
+ spinner.start(currentPhase);
176
+ }
177
+ }
178
+ else if (event.type === 'file') {
179
+ if (options.verbose) {
180
+ console.log(chalk.gray(` šŸ“„ ${event.message}`));
181
+ }
182
+ }
183
+ else if (event.type === 'complete') {
184
+ spinner.succeed(chalk.green('Wiki generation complete!'));
185
+ }
186
+ else if (event.type === 'error') {
187
+ spinner.fail(chalk.red(event.message));
188
+ }
189
+ // Handle agent streaming messages
190
+ else if (event.type === 'stream_event') {
191
+ const streamEvent = event.event;
192
+ if (streamEvent?.type === 'content_block_delta' && streamEvent.delta?.type === 'text_delta') {
193
+ if (options.verbose) {
194
+ process.stdout.write(streamEvent.delta.text || '');
195
+ }
196
+ }
197
+ else if (streamEvent?.type === 'content_block_start' && streamEvent.content_block?.type === 'tool_use') {
198
+ spinner.text = `Using tool: ${streamEvent.content_block.name}`;
199
+ }
200
+ }
201
+ else if (event.type === 'tool_result') {
202
+ if (options.verbose) {
203
+ spinner.info('Tool completed');
204
+ spinner.start(currentPhase);
205
+ }
206
+ }
207
+ else if (event.type === 'result') {
208
+ if (event.subtype === 'success') {
209
+ spinner.succeed(chalk.green('Wiki generation complete!'));
210
+ }
211
+ }
212
+ }
213
+ console.log();
214
+ console.log(chalk.cyan('šŸ“ Wiki generated at:'), chalk.white(path.resolve(options.output)));
215
+ console.log(chalk.gray('Open wiki/README.md to start exploring the documentation.'));
216
+ console.log();
217
+ }
218
+ catch (error) {
219
+ spinner.fail('Wiki generation failed');
220
+ throw error;
221
+ }
222
+ }
223
+ catch (error) {
224
+ console.error(chalk.red('\nError:'), error instanceof Error ? error.message : String(error));
225
+ if (options.verbose && error instanceof Error && error.stack) {
226
+ console.error(chalk.gray(error.stack));
227
+ }
228
+ process.exit(1);
229
+ }
230
+ });
231
+ program
232
+ .argument('[query]', 'Direct query to the agent')
233
+ .option('-i, --interactive', 'Start interactive session')
234
+ .option('-v, --verbose', 'Verbose output')
235
+ .option('-p, --plan', 'Planning mode - create plan before executing')
236
+ .action(async (query, options) => {
237
+ try {
238
+ const configManager = new ConfigManager();
239
+ const config = await configManager.load();
240
+ if (!configManager.hasApiKey()) {
241
+ console.log(chalk.red('āŒ No API key found.'));
242
+ console.log(chalk.yellow('\nCreate a .env file with your API key:'));
243
+ console.log(chalk.gray(' echo "CLAUDE_API_KEY=your-key-here" > .env'));
244
+ console.log(chalk.yellow('\nOr set the environment variable:'));
245
+ console.log(chalk.gray(' export CLAUDE_API_KEY=your-key-here'));
246
+ process.exit(1);
247
+ }
248
+ const permissionManager = new PermissionManager({ policy: 'permissive' });
249
+ const agent = new DevelopmentAgentAgent({
250
+ verbose: options?.verbose || false,
251
+ apiKey: config.apiKey,
252
+ permissionManager
253
+ });
254
+ console.log(chalk.cyan.bold('\nšŸ¤– Development Agent'));
255
+ console.log(chalk.gray('Full-stack development assistant with file operations, build tools, and code analysis capabilities.'));
256
+ console.log(chalk.gray(`šŸ“ Working directory: ${workingDir}\n`));
257
+ if (query && options?.plan) {
258
+ // Planning mode with query
259
+ await handlePlanningMode(agent, query, permissionManager);
260
+ }
261
+ else if (query) {
262
+ await handleSingleQuery(agent, query, options?.verbose);
263
+ }
264
+ else {
265
+ await handleInteractiveMode(agent, permissionManager, options?.verbose);
266
+ }
267
+ }
268
+ catch (error) {
269
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
270
+ process.exit(1);
271
+ }
272
+ });
273
+ function parseSlashCommand(input) {
274
+ // Remove leading slash
275
+ const trimmed = input.slice(1).trim();
276
+ // Split by spaces, but respect quotes
277
+ const parts = [];
278
+ let current = '';
279
+ let inQuotes = false;
280
+ let quoteChar = '';
281
+ for (let i = 0; i < trimmed.length; i++) {
282
+ const char = trimmed[i];
283
+ if ((char === '"' || char === "'") && (i === 0 || trimmed[i - 1] !== '\\')) {
284
+ if (!inQuotes) {
285
+ inQuotes = true;
286
+ quoteChar = char;
287
+ }
288
+ else if (char === quoteChar) {
289
+ inQuotes = false;
290
+ quoteChar = '';
291
+ }
292
+ else {
293
+ current += char;
294
+ }
295
+ }
296
+ else if (char === ' ' && !inQuotes) {
297
+ if (current) {
298
+ parts.push(current);
299
+ current = '';
300
+ }
301
+ }
302
+ else {
303
+ current += char;
304
+ }
305
+ }
306
+ if (current) {
307
+ parts.push(current);
308
+ }
309
+ if (inQuotes) {
310
+ return { command: '', args: {}, error: 'Unclosed quote in command' };
311
+ }
312
+ const command = parts[0];
313
+ const args = {};
314
+ for (let i = 1; i < parts.length; i++) {
315
+ const part = parts[i];
316
+ if (part.startsWith('--')) {
317
+ const key = part.slice(2);
318
+ const nextPart = parts[i + 1];
319
+ if (!nextPart || nextPart.startsWith('--')) {
320
+ args[key] = true;
321
+ }
322
+ else {
323
+ // Try to parse as number
324
+ const numValue = Number(nextPart);
325
+ args[key] = isNaN(numValue) ? nextPart : numValue;
326
+ i++;
327
+ }
328
+ }
329
+ }
330
+ return { command, args };
331
+ }
332
+ /** Global to store stdin history - read at startup before commander parses */
333
+ let stdinHistory = [];
334
+ /** Read conversation history from stdin synchronously using fs */
335
+ function initStdinHistory() {
336
+ // If stdin is a TTY (interactive), no history
337
+ if (process.stdin.isTTY) {
338
+ return;
339
+ }
340
+ try {
341
+ // Read stdin synchronously using fs
342
+ const fs = require('fs');
343
+ const data = fs.readFileSync(0, 'utf8'); // fd 0 is stdin
344
+ if (data.trim()) {
345
+ const history = JSON.parse(data);
346
+ if (Array.isArray(history)) {
347
+ stdinHistory = history;
348
+ }
349
+ }
350
+ }
351
+ catch (e) {
352
+ // No stdin data or invalid JSON, ignore
353
+ }
354
+ }
355
+ async function handleSingleQuery(agent, query, verbose) {
356
+ // Use the global stdin history (read before commander.parse())
357
+ const history = stdinHistory;
358
+ const spinner = ora('Processing...').start();
359
+ try {
360
+ // Pass history to agent for multi-turn context
361
+ const response = agent.query(query, history);
362
+ spinner.stop();
363
+ console.log(chalk.yellow('Query:'), query);
364
+ console.log(chalk.green('Response:') + '\n');
365
+ for await (const message of response) {
366
+ // Handle streaming text deltas for real-time output
367
+ if (message.type === 'stream_event') {
368
+ const event = message.event;
369
+ if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
370
+ process.stdout.write(event.delta.text || '');
371
+ }
372
+ else if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
373
+ // Show tool being called
374
+ console.log(chalk.cyan(`\nšŸ”§ Using tool: ${event.content_block.name}`));
375
+ }
376
+ else if (event?.type === 'content_block_stop') {
377
+ // Tool finished or content block ended
378
+ }
379
+ }
380
+ else if (message.type === 'tool_result') {
381
+ // Show tool result summary
382
+ const result = message.content;
383
+ if (verbose && result) {
384
+ console.log(chalk.gray(` ↳ Tool completed`));
385
+ }
386
+ }
387
+ else if (message.type === 'result') {
388
+ // Display statistics (verbose mode only)
389
+ if (verbose) {
390
+ const stats = message.content || message;
391
+ if (stats.durationMs) {
392
+ console.log(chalk.gray('\n--- Statistics ---'));
393
+ console.log(chalk.gray(`Duration: ${stats.durationMs}ms`));
394
+ console.log(chalk.gray(`Input tokens: ${stats.inputTokens}`));
395
+ console.log(chalk.gray(`Output tokens: ${stats.outputTokens}`));
396
+ if (stats.cacheReadTokens)
397
+ console.log(chalk.gray(`Cache read: ${stats.cacheReadTokens}`));
398
+ }
399
+ }
400
+ }
401
+ else if (message.type === 'system') {
402
+ // System messages (verbose mode only)
403
+ if (verbose)
404
+ console.log(chalk.blue(`[system] ${message.content || message.subtype || ''}`));
405
+ }
406
+ }
407
+ console.log('\n');
408
+ }
409
+ catch (error) {
410
+ spinner.fail('Failed to process query');
411
+ throw error;
412
+ }
413
+ }
414
+ async function handleInteractiveMode(agent, permissionManager, verbose) {
415
+ // Load workflow executor
416
+ const { WorkflowExecutor } = await import('./workflows.js');
417
+ const workflowExecutor = new WorkflowExecutor(agent.permissionManager);
418
+ const planManager = new PlanManager();
419
+ // Build list of all available slash commands
420
+ const builtinCommands = [
421
+ { name: 'help', description: 'Show available commands' },
422
+ { name: 'quit', description: 'Exit the agent' },
423
+ { name: 'exit', description: 'Exit the agent' },
424
+ { name: 'plan', description: 'Create a plan for a task' },
425
+ { name: 'plans', description: 'List all pending plans' },
426
+ { name: 'execute', description: 'Execute plan by number' },
427
+ { name: 'plan-delete', description: 'Delete plans' },
428
+ { name: 'mcp-list', description: 'List configured MCP servers' },
429
+ { name: 'mcp-add', description: 'Add a new MCP server' },
430
+ { name: 'mcp-remove', description: 'Remove an MCP server' },
431
+ { name: 'mcp-toggle', description: 'Enable/disable an MCP server' },
432
+ { name: 'command-add', description: 'Create a new custom slash command' },
433
+ { name: 'command-list', description: 'List all custom commands' },
434
+ { name: 'skill-add', description: 'Create a new skill' },
435
+ { name: 'skill-list', description: 'List all available skills' },
436
+ { name: 'files', description: 'List files in current directory' },
437
+ { name: 'run', description: 'Execute a command' },
438
+ { name: 'code-audit', description: 'Comprehensive code audit for technical debt and security' },
439
+ { name: 'test-suite', description: 'Generate comprehensive test suite' },
440
+ { name: 'refactor-analysis', description: 'Analyze code for refactoring opportunities' },
441
+ ];
442
+ // Add Claude Code commands from .claude/commands/
443
+ const allCommands = [
444
+ ...builtinCommands,
445
+ ...claudeConfig.commands.map(c => ({ name: c.name, description: c.description || 'Custom command' }))
446
+ ];
447
+ // Autocomplete source function
448
+ const commandSource = async (answers, input) => {
449
+ input = input || '';
450
+ // Only show autocomplete when typing slash commands
451
+ if (!input.startsWith('/')) {
452
+ return [];
453
+ }
454
+ const search = input.slice(1).toLowerCase();
455
+ const matches = allCommands.filter(cmd => cmd.name.toLowerCase().startsWith(search));
456
+ return matches.map(cmd => ({
457
+ name: `/${cmd.name} - ${cmd.description}`,
458
+ value: `/${cmd.name}`,
459
+ short: `/${cmd.name}`
460
+ }));
461
+ };
462
+ console.log(chalk.gray('Type your questions, or:'));
463
+ console.log(chalk.gray('• /help - Show available commands'));
464
+ console.log(chalk.gray('• /plan <query> - Create a plan before executing'));
465
+ console.log(chalk.gray('• /quit or Ctrl+C - Exit'));
466
+ if (claudeConfig.commands.length > 0) {
467
+ console.log(chalk.gray(`• ${claudeConfig.commands.length} custom commands available (type / to see them)`));
468
+ }
469
+ console.log();
470
+ while (true) {
471
+ try {
472
+ const { input } = await inquirer.prompt([
473
+ {
474
+ type: 'autocomplete',
475
+ name: 'input',
476
+ message: chalk.cyan('ted-mosby>'),
477
+ prefix: '',
478
+ source: commandSource,
479
+ suggestOnly: true, // Allow free text input
480
+ emptyText: '', // Don't show "no results" message
481
+ }
482
+ ]);
483
+ if (!input.trim())
484
+ continue;
485
+ if (input === '/quit' || input === '/exit') {
486
+ console.log(chalk.yellow('\nšŸ‘‹ Goodbye!'));
487
+ break;
488
+ }
489
+ if (input === '/help') {
490
+ console.log(chalk.cyan.bold('\nšŸ“š Available Commands:'));
491
+ console.log(chalk.gray('• /help - Show this help'));
492
+ console.log(chalk.gray('• /quit - Exit the agent'));
493
+ console.log(chalk.gray('• /files - List files in current directory'));
494
+ console.log(chalk.gray('• /run <command> - Execute a command'));
495
+ console.log(chalk.gray('\nšŸ“‹ Planning Commands:'));
496
+ console.log(chalk.gray('• /plan <query> - Create a plan for a task'));
497
+ console.log(chalk.gray('• /plans - List all pending plans'));
498
+ console.log(chalk.gray('• /execute <num> - Execute plan by number'));
499
+ console.log(chalk.gray('• /plan-delete <num|all|all-completed> - Delete plans'));
500
+ console.log(chalk.gray('• <number> - Quick shortcut to execute plan by number'));
501
+ console.log(chalk.gray('\nšŸ”Œ MCP Server Commands:'));
502
+ console.log(chalk.gray('• /mcp-list - List configured MCP servers'));
503
+ console.log(chalk.gray('• /mcp-add - Add a new MCP server (interactive)'));
504
+ console.log(chalk.gray('• /mcp-remove [name] - Remove an MCP server'));
505
+ console.log(chalk.gray('• /mcp-toggle [name] - Enable/disable an MCP server'));
506
+ console.log(chalk.gray('\n✨ Customization Commands:'));
507
+ console.log(chalk.gray('• /command-add - Create a new custom slash command'));
508
+ console.log(chalk.gray('• /command-list - List all custom commands'));
509
+ console.log(chalk.gray('• /skill-add - Create a new skill'));
510
+ console.log(chalk.gray('• /skill-list - List all available skills'));
511
+ console.log(chalk.gray('\nšŸ”® Workflow Commands:'));
512
+ console.log(chalk.gray('• /code-audit --path <dir> [--output <path>] [--focus <area>]'));
513
+ console.log(chalk.gray(' Comprehensive code audit for technical debt and security'));
514
+ console.log(chalk.gray('• /test-suite --target <file> [--framework <name>] [--output <path>]'));
515
+ console.log(chalk.gray(' Generate comprehensive test suite'));
516
+ console.log(chalk.gray('• /refactor-analysis --target <file> [--goal <objective>]'));
517
+ console.log(chalk.gray(' Analyze code for refactoring opportunities'));
518
+ // Show custom Claude Code commands if any
519
+ if (claudeConfig.commands.length > 0) {
520
+ console.log(chalk.gray('\nšŸ“Œ Custom Commands:'));
521
+ claudeConfig.commands.forEach(cmd => {
522
+ console.log(chalk.gray(`• /${cmd.name} - ${cmd.description || 'Custom command'}`));
523
+ });
524
+ }
525
+ // Show available skills if any
526
+ if (claudeConfig.skills.length > 0) {
527
+ console.log(chalk.gray('\nšŸŽÆ Available Skills:'));
528
+ claudeConfig.skills.forEach(skill => {
529
+ console.log(chalk.gray(`• ${skill.name} - ${skill.description}`));
530
+ });
531
+ console.log(chalk.gray(' (Say "use <skill>" or "run the <skill> skill" to invoke)'));
532
+ }
533
+ console.log(chalk.gray('\nšŸ’” Ask me anything about development!\n'));
534
+ continue;
535
+ }
536
+ // Handle planning commands
537
+ if (input.startsWith('/plan ')) {
538
+ const query = input.slice(6).trim();
539
+ await handlePlanningMode(agent, query, permissionManager);
540
+ continue;
541
+ }
542
+ if (input === '/plans') {
543
+ await listPlans(planManager);
544
+ continue;
545
+ }
546
+ if (input.startsWith('/execute ')) {
547
+ const arg = input.slice(9).trim();
548
+ await executePlanByRef(arg, agent, permissionManager, planManager);
549
+ continue;
550
+ }
551
+ if (input.startsWith('/plan-delete ')) {
552
+ const arg = input.slice(13).trim();
553
+ await deletePlanByRef(arg, planManager);
554
+ continue;
555
+ }
556
+ // Quick shortcut: just type a number to execute that plan
557
+ if (/^\d+$/.test(input.trim())) {
558
+ const planNum = parseInt(input.trim());
559
+ await executePlanByNumber(planNum, agent, permissionManager, planManager);
560
+ continue;
561
+ }
562
+ // Handle MCP commands
563
+ if (input === '/mcp-list') {
564
+ await handleMcpList();
565
+ continue;
566
+ }
567
+ if (input === '/mcp-add') {
568
+ await handleMcpAdd();
569
+ continue;
570
+ }
571
+ if (input.startsWith('/mcp-remove')) {
572
+ const name = input.slice(11).trim();
573
+ await handleMcpRemove(name);
574
+ continue;
575
+ }
576
+ if (input.startsWith('/mcp-toggle')) {
577
+ const name = input.slice(11).trim();
578
+ await handleMcpToggle(name);
579
+ continue;
580
+ }
581
+ // Handle command/skill creation
582
+ if (input === '/command-add') {
583
+ await handleCommandAdd();
584
+ continue;
585
+ }
586
+ if (input === '/command-list') {
587
+ handleCommandList();
588
+ continue;
589
+ }
590
+ if (input === '/skill-add') {
591
+ await handleSkillAdd();
592
+ continue;
593
+ }
594
+ if (input === '/skill-list') {
595
+ handleSkillList();
596
+ continue;
597
+ }
598
+ // Handle slash commands (including custom Claude Code commands)
599
+ if (input.startsWith('/')) {
600
+ const { command, args, error } = parseSlashCommand(input);
601
+ if (error) {
602
+ console.log(chalk.red(`Error: ${error}`));
603
+ continue;
604
+ }
605
+ // Check for custom Claude Code command first
606
+ const customCmd = getCommand(claudeConfig.commands, command);
607
+ if (customCmd) {
608
+ // Get any positional arguments after the command name
609
+ const inputAfterCommand = input.slice(command.length + 2).trim();
610
+ const positionalArgs = inputAfterCommand.split(/\s+/).filter(Boolean);
611
+ // Expand the command template with arguments
612
+ const expandedPrompt = expandCommand(customCmd, inputAfterCommand);
613
+ console.log(chalk.cyan(`\nšŸ“‹ Running /${command}...\n`));
614
+ // Send the expanded prompt to the agent
615
+ const spinner = ora('Processing command...').start();
616
+ try {
617
+ const response = agent.query(expandedPrompt);
618
+ spinner.stop();
619
+ for await (const message of response) {
620
+ if (message.type === 'stream_event') {
621
+ const event = message.event;
622
+ if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
623
+ process.stdout.write(event.delta.text || '');
624
+ }
625
+ else if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
626
+ console.log(chalk.cyan(`\nšŸ”§ Using tool: ${event.content_block.name}`));
627
+ }
628
+ }
629
+ else if (message.type === 'tool_result') {
630
+ if (verbose)
631
+ console.log(chalk.gray(` ↳ Tool completed`));
632
+ }
633
+ }
634
+ console.log('\n');
635
+ }
636
+ catch (error) {
637
+ spinner.fail('Command failed');
638
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
639
+ }
640
+ continue;
641
+ }
642
+ // List of all possible workflow commands
643
+ const validCommands = [
644
+ 'literature-review', 'experiment-log',
645
+ 'code-audit', 'test-suite', 'refactor-analysis',
646
+ 'invoice-batch', 'contract-review', 'meeting-summary',
647
+ 'content-calendar', 'blog-outline', 'campaign-brief',
648
+ 'dataset-profile', 'chart-report'
649
+ ];
650
+ if (validCommands.includes(command)) {
651
+ try {
652
+ const workflow = await workflowExecutor.loadWorkflow(command);
653
+ const context = {
654
+ variables: new Map(),
655
+ agent,
656
+ permissionManager: agent.permissionManager
657
+ };
658
+ await workflowExecutor.execute(workflow, args, context);
659
+ continue;
660
+ }
661
+ catch (error) {
662
+ console.error(chalk.red('Workflow error:'), error instanceof Error ? error.message : String(error));
663
+ continue;
664
+ }
665
+ }
666
+ console.log(chalk.yellow(`Unknown command: /${command}`));
667
+ console.log(chalk.gray('Type /help to see available commands'));
668
+ continue;
669
+ }
670
+ const spinner = ora('Processing...').start();
671
+ try {
672
+ const response = agent.query(input);
673
+ spinner.stop();
674
+ console.log();
675
+ for await (const message of response) {
676
+ // Handle streaming text deltas for real-time output
677
+ if (message.type === 'stream_event') {
678
+ const event = message.event;
679
+ if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
680
+ process.stdout.write(event.delta.text || '');
681
+ }
682
+ else if (event?.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
683
+ // Show tool being called
684
+ console.log(chalk.cyan(`\nšŸ”§ Using tool: ${event.content_block.name}`));
685
+ }
686
+ }
687
+ else if (message.type === 'tool_result') {
688
+ // Show tool result summary
689
+ const result = message.content;
690
+ if (verbose && result) {
691
+ console.log(chalk.gray(` ↳ Tool completed`));
692
+ }
693
+ }
694
+ else if (message.type === 'result') {
695
+ // Display statistics (verbose mode only)
696
+ if (verbose) {
697
+ const stats = message.content || message;
698
+ if (stats.durationMs) {
699
+ console.log(chalk.gray('\n--- Statistics ---'));
700
+ console.log(chalk.gray(`Duration: ${stats.durationMs}ms`));
701
+ console.log(chalk.gray(`Input tokens: ${stats.inputTokens}`));
702
+ console.log(chalk.gray(`Output tokens: ${stats.outputTokens}`));
703
+ if (stats.cacheReadTokens)
704
+ console.log(chalk.gray(`Cache read: ${stats.cacheReadTokens}`));
705
+ }
706
+ }
707
+ }
708
+ else if (message.type === 'system') {
709
+ // System messages (verbose mode only)
710
+ if (verbose)
711
+ console.log(chalk.blue(`[system] ${message.content || message.subtype || ''}`));
712
+ }
713
+ }
714
+ console.log('\n');
715
+ }
716
+ catch (error) {
717
+ spinner.fail('Failed to process query');
718
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
719
+ }
720
+ }
721
+ catch (error) {
722
+ if (error instanceof Error && error.message.includes('User force closed')) {
723
+ console.log(chalk.yellow('\n\nšŸ‘‹ Goodbye!'));
724
+ break;
725
+ }
726
+ console.error(chalk.red('Unexpected error:'), error);
727
+ }
728
+ }
729
+ }
730
+ // MCP Server management functions
731
+ async function handleMcpList() {
732
+ const mcpConfig = new MCPConfigManager();
733
+ await mcpConfig.load();
734
+ console.log(chalk.cyan.bold('\nšŸ“¦ MCP Servers\n'));
735
+ console.log(mcpConfig.formatServerList());
736
+ console.log(chalk.gray(`\nConfig: ${process.cwd()}/.mcp.json\n`));
737
+ }
738
+ async function handleMcpAdd() {
739
+ const mcpConfig = new MCPConfigManager();
740
+ await mcpConfig.load();
741
+ console.log(chalk.cyan.bold('\nšŸ“¦ Add MCP Server\n'));
742
+ // Step 1: Server name
743
+ const { name } = await inquirer.prompt([
744
+ {
745
+ type: 'input',
746
+ name: 'name',
747
+ message: 'Server name (lowercase, alphanumeric, hyphens):',
748
+ validate: (input) => {
749
+ if (!input.trim())
750
+ return 'Name is required';
751
+ if (!/^[a-z][a-z0-9-]*$/.test(input)) {
752
+ return 'Name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens';
753
+ }
754
+ if (mcpConfig.getServers()[input]) {
755
+ return 'A server with this name already exists';
756
+ }
757
+ return true;
758
+ }
759
+ }
760
+ ]);
761
+ // Step 2: Transport type
762
+ const { transportType } = await inquirer.prompt([
763
+ {
764
+ type: 'list',
765
+ name: 'transportType',
766
+ message: 'Transport type:',
767
+ choices: [
768
+ { name: 'Stdio (local command)', value: 'stdio' },
769
+ { name: 'HTTP (REST endpoint)', value: 'http' },
770
+ { name: 'SSE (Server-Sent Events)', value: 'sse' },
771
+ { name: 'SDK (in-process module)', value: 'sdk' }
772
+ ]
773
+ }
774
+ ]);
775
+ let serverConfig;
776
+ if (transportType === 'stdio') {
777
+ const { command, args } = await inquirer.prompt([
778
+ {
779
+ type: 'input',
780
+ name: 'command',
781
+ message: 'Command to run:',
782
+ default: 'npx',
783
+ validate: (input) => input.trim() ? true : 'Command is required'
784
+ },
785
+ {
786
+ type: 'input',
787
+ name: 'args',
788
+ message: 'Arguments (space-separated):',
789
+ default: '-y @modelcontextprotocol/server-filesystem'
790
+ }
791
+ ]);
792
+ serverConfig = {
793
+ type: 'stdio',
794
+ command: command.trim(),
795
+ args: args.trim() ? args.trim().split(/\s+/) : [],
796
+ enabled: true
797
+ };
798
+ }
799
+ else if (transportType === 'http' || transportType === 'sse') {
800
+ const { url } = await inquirer.prompt([
801
+ {
802
+ type: 'input',
803
+ name: 'url',
804
+ message: 'Server URL:',
805
+ validate: (input) => {
806
+ if (!input.trim())
807
+ return 'URL is required';
808
+ try {
809
+ const testUrl = input.replace(/\$\{[^}]+\}/g, 'placeholder');
810
+ new URL(testUrl);
811
+ return true;
812
+ }
813
+ catch {
814
+ return 'Invalid URL format';
815
+ }
816
+ }
817
+ }
818
+ ]);
819
+ serverConfig = {
820
+ type: transportType,
821
+ url: url.trim(),
822
+ enabled: true
823
+ };
824
+ }
825
+ else {
826
+ const { serverModule } = await inquirer.prompt([
827
+ {
828
+ type: 'input',
829
+ name: 'serverModule',
830
+ message: 'Module path:',
831
+ default: './custom-mcp-server.js',
832
+ validate: (input) => input.trim() ? true : 'Module path is required'
833
+ }
834
+ ]);
835
+ serverConfig = {
836
+ type: 'sdk',
837
+ serverModule: serverModule.trim(),
838
+ enabled: true
839
+ };
840
+ }
841
+ // Step 3: Optional description
842
+ const { description } = await inquirer.prompt([
843
+ {
844
+ type: 'input',
845
+ name: 'description',
846
+ message: 'Description (optional):'
847
+ }
848
+ ]);
849
+ if (description.trim()) {
850
+ serverConfig.description = description.trim();
851
+ }
852
+ await mcpConfig.addServer(name, serverConfig);
853
+ console.log(chalk.green(`\nāœ“ Server '${name}' added successfully!\n`));
854
+ console.log(chalk.yellow('Note: Restart the agent to load the new server.\n'));
855
+ }
856
+ async function handleMcpRemove(name) {
857
+ const mcpConfig = new MCPConfigManager();
858
+ await mcpConfig.load();
859
+ const servers = Object.keys(mcpConfig.getServers());
860
+ if (servers.length === 0) {
861
+ console.log(chalk.yellow('\nNo MCP servers configured.\n'));
862
+ return;
863
+ }
864
+ let serverName = name?.trim();
865
+ if (!serverName) {
866
+ const { selected } = await inquirer.prompt([
867
+ {
868
+ type: 'list',
869
+ name: 'selected',
870
+ message: 'Select server to remove:',
871
+ choices: servers
872
+ }
873
+ ]);
874
+ serverName = selected;
875
+ }
876
+ if (!servers.includes(serverName)) {
877
+ console.log(chalk.red(`\nServer '${serverName}' not found.\n`));
878
+ return;
879
+ }
880
+ const { confirm } = await inquirer.prompt([
881
+ {
882
+ type: 'confirm',
883
+ name: 'confirm',
884
+ message: `Remove server '${serverName}'?`,
885
+ default: false
886
+ }
887
+ ]);
888
+ if (confirm) {
889
+ await mcpConfig.removeServer(serverName);
890
+ console.log(chalk.green(`\nāœ“ Server '${serverName}' removed.\n`));
891
+ }
892
+ else {
893
+ console.log(chalk.gray('\nCancelled.\n'));
894
+ }
895
+ }
896
+ async function handleMcpToggle(name) {
897
+ const mcpConfig = new MCPConfigManager();
898
+ await mcpConfig.load();
899
+ const servers = mcpConfig.getServers();
900
+ const serverNames = Object.keys(servers);
901
+ if (serverNames.length === 0) {
902
+ console.log(chalk.yellow('\nNo MCP servers configured.\n'));
903
+ return;
904
+ }
905
+ let serverName = name?.trim();
906
+ if (!serverName) {
907
+ const { selected } = await inquirer.prompt([
908
+ {
909
+ type: 'list',
910
+ name: 'selected',
911
+ message: 'Select server to toggle:',
912
+ choices: serverNames.map(n => ({
913
+ name: `${n} (${servers[n].enabled !== false ? 'enabled' : 'disabled'})`,
914
+ value: n
915
+ }))
916
+ }
917
+ ]);
918
+ serverName = selected;
919
+ }
920
+ if (!serverNames.includes(serverName)) {
921
+ console.log(chalk.red(`\nServer '${serverName}' not found.\n`));
922
+ return;
923
+ }
924
+ const wasEnabled = servers[serverName].enabled !== false;
925
+ await mcpConfig.toggleServer(serverName);
926
+ console.log(chalk.green(`\nāœ“ Server '${serverName}' ${wasEnabled ? 'disabled' : 'enabled'}.\n`));
927
+ console.log(chalk.yellow('Note: Restart the agent to apply changes.\n'));
928
+ }
929
+ // Custom command creation handler
930
+ async function handleCommandAdd() {
931
+ console.log(chalk.cyan.bold('\n✨ Create New Slash Command\n'));
932
+ const { name, description, template } = await inquirer.prompt([
933
+ {
934
+ type: 'input',
935
+ name: 'name',
936
+ message: 'Command name (without /):',
937
+ validate: (input) => {
938
+ if (!input.trim())
939
+ return 'Name is required';
940
+ if (!/^[a-z][a-z0-9-]*$/.test(input)) {
941
+ return 'Name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens';
942
+ }
943
+ return true;
944
+ }
945
+ },
946
+ {
947
+ type: 'input',
948
+ name: 'description',
949
+ message: 'Description:',
950
+ validate: (input) => input.trim() ? true : 'Description is required'
951
+ },
952
+ {
953
+ type: 'editor',
954
+ name: 'template',
955
+ message: 'Command template (use $ARGUMENTS for all args, $1, $2 for positional):',
956
+ default: 'Perform the following task:\n\n$ARGUMENTS'
957
+ }
958
+ ]);
959
+ // Create .claude/commands directory if it doesn't exist
960
+ const commandsDir = path.join(workingDir, '.claude', 'commands');
961
+ if (!fs.existsSync(commandsDir)) {
962
+ fs.mkdirSync(commandsDir, { recursive: true });
963
+ }
964
+ // Create the command file
965
+ const content = `---
966
+ description: ${description}
967
+ ---
968
+
969
+ ${template}
970
+ `;
971
+ const filePath = path.join(commandsDir, `${name}.md`);
972
+ fs.writeFileSync(filePath, content);
973
+ console.log(chalk.green(`\nāœ“ Created command /${name}`));
974
+ console.log(chalk.gray(` File: ${filePath}`));
975
+ console.log(chalk.yellow('\nRestart the agent to use the new command.\n'));
976
+ // Reload the config to pick up the new command
977
+ Object.assign(claudeConfig, loadClaudeConfig(workingDir));
978
+ }
979
+ function handleCommandList() {
980
+ console.log(chalk.cyan.bold('\nšŸ“‹ Custom Slash Commands\n'));
981
+ if (claudeConfig.commands.length === 0) {
982
+ console.log(chalk.gray('No custom commands defined.'));
983
+ console.log(chalk.gray('Use /command-add to create one.\n'));
984
+ return;
985
+ }
986
+ claudeConfig.commands.forEach(cmd => {
987
+ console.log(chalk.white(` /${cmd.name}`));
988
+ console.log(chalk.gray(` ${cmd.description || 'No description'}`));
989
+ console.log(chalk.gray(` File: ${cmd.filePath}\n`));
990
+ });
991
+ }
992
+ // Custom skill creation handler
993
+ async function handleSkillAdd() {
994
+ console.log(chalk.cyan.bold('\nšŸŽÆ Create New Skill\n'));
995
+ const { name, description, tools, instructions } = await inquirer.prompt([
996
+ {
997
+ type: 'input',
998
+ name: 'name',
999
+ message: 'Skill name:',
1000
+ validate: (input) => {
1001
+ if (!input.trim())
1002
+ return 'Name is required';
1003
+ if (!/^[a-z][a-z0-9-]*$/.test(input)) {
1004
+ return 'Name must be lowercase, start with a letter, and contain only letters, numbers, and hyphens';
1005
+ }
1006
+ return true;
1007
+ }
1008
+ },
1009
+ {
1010
+ type: 'input',
1011
+ name: 'description',
1012
+ message: 'Description:',
1013
+ validate: (input) => input.trim() ? true : 'Description is required'
1014
+ },
1015
+ {
1016
+ type: 'checkbox',
1017
+ name: 'tools',
1018
+ message: 'Select tools this skill can use:',
1019
+ choices: [
1020
+ { name: 'Read files', value: 'Read' },
1021
+ { name: 'Write files', value: 'Write' },
1022
+ { name: 'Run commands', value: 'Bash' },
1023
+ { name: 'Web search', value: 'WebSearch' },
1024
+ { name: 'Web fetch', value: 'WebFetch' }
1025
+ ],
1026
+ default: ['Read']
1027
+ },
1028
+ {
1029
+ type: 'editor',
1030
+ name: 'instructions',
1031
+ message: 'Skill instructions (what should the agent do when this skill is invoked?):',
1032
+ default: '# Skill Instructions\n\nWhen this skill is invoked:\n\n1. First, understand the user\'s request\n2. Apply your expertise to solve the problem\n3. Provide a clear, actionable response'
1033
+ }
1034
+ ]);
1035
+ // Create .claude/skills/<name> directory
1036
+ const skillDir = path.join(workingDir, '.claude', 'skills', name);
1037
+ if (!fs.existsSync(skillDir)) {
1038
+ fs.mkdirSync(skillDir, { recursive: true });
1039
+ }
1040
+ // Create the SKILL.md file
1041
+ const content = `---
1042
+ description: ${description}
1043
+ tools: ${tools.join(', ')}
1044
+ ---
1045
+
1046
+ ${instructions}
1047
+ `;
1048
+ const filePath = path.join(skillDir, 'SKILL.md');
1049
+ fs.writeFileSync(filePath, content);
1050
+ console.log(chalk.green(`\nāœ“ Created skill: ${name}`));
1051
+ console.log(chalk.gray(` File: ${filePath}`));
1052
+ console.log(chalk.yellow('\nRestart the agent to use the new skill.'));
1053
+ console.log(chalk.gray('Invoke it by saying "use ' + name + '" or "run the ' + name + ' skill"\n'));
1054
+ // Reload the config to pick up the new skill
1055
+ Object.assign(claudeConfig, loadClaudeConfig(workingDir));
1056
+ }
1057
+ function handleSkillList() {
1058
+ console.log(chalk.cyan.bold('\nšŸŽÆ Available Skills\n'));
1059
+ if (claudeConfig.skills.length === 0) {
1060
+ console.log(chalk.gray('No skills defined.'));
1061
+ console.log(chalk.gray('Use /skill-add to create one.\n'));
1062
+ return;
1063
+ }
1064
+ claudeConfig.skills.forEach(skill => {
1065
+ console.log(chalk.white(` ${skill.name}`));
1066
+ console.log(chalk.gray(` ${skill.description}`));
1067
+ console.log(chalk.gray(` Tools: ${skill.tools.join(', ') || 'none'}`));
1068
+ console.log(chalk.gray(` File: ${skill.filePath}\n`));
1069
+ });
1070
+ }
1071
+ // Planning mode helper functions
1072
+ async function handlePlanningMode(agent, query, pm) {
1073
+ const planManager = new PlanManager();
1074
+ console.log(chalk.cyan('\nšŸ“‹ Planning Mode'));
1075
+ console.log(chalk.gray('Analyzing: ' + query + '\n'));
1076
+ const spinner = ora('Creating plan...').start();
1077
+ try {
1078
+ // Query the agent in planning mode to analyze and create a plan
1079
+ const planPrompt = `You are in PLANNING MODE. Analyze this request and create a structured plan.
1080
+
1081
+ REQUEST: ${query}
1082
+
1083
+ Create a plan with the following format:
1084
+ 1. A brief summary (1 sentence)
1085
+ 2. Your analysis of what needs to be done
1086
+ 3. Step-by-step actions with risk assessment
1087
+ 4. Rollback strategy if something goes wrong
1088
+
1089
+ Output your plan in this exact format:
1090
+
1091
+ SUMMARY: [one sentence describing what will be accomplished]
1092
+
1093
+ ANALYSIS:
1094
+ [what you discovered and your approach]
1095
+
1096
+ STEPS:
1097
+ 1. [Step Name] | Action: [read/write/edit/command/query] | Target: [file or command] | Purpose: [why] | Risk: [low/medium/high]
1098
+ 2. [Next step...]
1099
+
1100
+ ROLLBACK:
1101
+ - [How to undo if needed]
1102
+ - [Additional recovery steps]`;
1103
+ let planText = '';
1104
+ const response = agent.query(planPrompt);
1105
+ for await (const message of response) {
1106
+ if (message.type === 'stream_event') {
1107
+ const event = message.event;
1108
+ if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
1109
+ planText += event.delta.text || '';
1110
+ }
1111
+ }
1112
+ }
1113
+ spinner.stop();
1114
+ // Parse the plan response
1115
+ const plan = parsePlanResponse(planText, query, planManager);
1116
+ // Display plan
1117
+ displayPlan(plan);
1118
+ // Save plan
1119
+ const planPath = await planManager.savePlan(plan);
1120
+ console.log(chalk.gray('\nPlan saved: ' + planPath));
1121
+ // Prompt for action
1122
+ const { action } = await inquirer.prompt([{
1123
+ type: 'list',
1124
+ name: 'action',
1125
+ message: 'What would you like to do?',
1126
+ choices: [
1127
+ { name: 'Execute now', value: 'execute' },
1128
+ { name: 'Edit plan first (opens in editor)', value: 'edit' },
1129
+ { name: 'Save for later', value: 'save' },
1130
+ { name: 'Discard', value: 'discard' }
1131
+ ]
1132
+ }]);
1133
+ if (action === 'execute') {
1134
+ await executePlan(plan, agent, pm, planManager);
1135
+ }
1136
+ else if (action === 'edit') {
1137
+ console.log(chalk.yellow('\nEdit: ' + planPath));
1138
+ console.log(chalk.gray('Then run: /execute ' + planPath));
1139
+ }
1140
+ else if (action === 'save') {
1141
+ const pending = (await planManager.listPlans()).filter(p => p.plan.status === 'pending').length;
1142
+ console.log(chalk.green(`\nāœ… Plan saved. You now have ${pending} pending plan(s).`));
1143
+ console.log(chalk.cyan('\nTo return to this plan later:'));
1144
+ console.log(chalk.gray(' /plans - List all pending plans'));
1145
+ console.log(chalk.gray(' /execute 1 - Execute plan #1'));
1146
+ console.log(chalk.gray(' 1 - Shortcut: just type the number'));
1147
+ }
1148
+ else if (action === 'discard') {
1149
+ await planManager.deletePlan(plan.id);
1150
+ console.log(chalk.yellow('Plan discarded.'));
1151
+ }
1152
+ }
1153
+ catch (error) {
1154
+ spinner.fail('Failed to create plan');
1155
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
1156
+ }
1157
+ }
1158
+ function parsePlanResponse(text, query, planManager) {
1159
+ const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?=\n|ANALYSIS:)/s);
1160
+ const analysisMatch = text.match(/ANALYSIS:\s*([\s\S]+?)(?=STEPS:|$)/);
1161
+ const stepsMatch = text.match(/STEPS:\s*([\s\S]+?)(?=ROLLBACK:|$)/);
1162
+ const rollbackMatch = text.match(/ROLLBACK:\s*([\s\S]+?)$/);
1163
+ const steps = [];
1164
+ if (stepsMatch) {
1165
+ const stepLines = stepsMatch[1].trim().split('\n').filter(l => l.trim());
1166
+ let stepNum = 1;
1167
+ for (const line of stepLines) {
1168
+ const match = line.match(/\d+\.\s*(.+?)\s*\|\s*Action:\s*(\w+)\s*\|\s*Target:\s*(.+?)\s*\|\s*Purpose:\s*(.+?)\s*\|\s*Risk:\s*(\w+)/i);
1169
+ if (match) {
1170
+ steps.push({
1171
+ id: `step-${stepNum++}`,
1172
+ name: match[1].trim(),
1173
+ action: match[2].toLowerCase(),
1174
+ target: match[3].trim(),
1175
+ purpose: match[4].trim(),
1176
+ risk: match[5].toLowerCase(),
1177
+ status: 'pending'
1178
+ });
1179
+ }
1180
+ }
1181
+ }
1182
+ const rollbackStrategy = [];
1183
+ if (rollbackMatch) {
1184
+ const rollbackLines = rollbackMatch[1].trim().split('\n');
1185
+ for (const line of rollbackLines) {
1186
+ const clean = line.replace(/^-\s*/, '').trim();
1187
+ if (clean)
1188
+ rollbackStrategy.push(clean);
1189
+ }
1190
+ }
1191
+ const now = new Date();
1192
+ const date = now.toISOString().split('T')[0].replace(/-/g, '');
1193
+ const time = now.toTimeString().split(' ')[0].replace(/:/g, '').slice(0, 6);
1194
+ return {
1195
+ id: `plan-${date}-${time}`,
1196
+ created: now,
1197
+ status: 'pending',
1198
+ query,
1199
+ summary: summaryMatch?.[1]?.trim() || 'Plan for: ' + query.slice(0, 50),
1200
+ analysis: analysisMatch?.[1]?.trim() || '',
1201
+ steps,
1202
+ rollbackStrategy
1203
+ };
1204
+ }
1205
+ function displayPlan(plan) {
1206
+ console.log(chalk.cyan.bold('\nšŸ“‹ Plan Created'));
1207
+ console.log(chalk.white('\nSummary: ') + plan.summary);
1208
+ if (plan.analysis) {
1209
+ console.log(chalk.white('\nAnalysis:'));
1210
+ console.log(chalk.gray(plan.analysis));
1211
+ }
1212
+ console.log(chalk.white('\nSteps:'));
1213
+ plan.steps.forEach((step, i) => {
1214
+ const riskColor = step.risk === 'high' ? chalk.red : step.risk === 'medium' ? chalk.yellow : chalk.green;
1215
+ console.log(chalk.white(` ${i + 1}. ${step.name}`));
1216
+ console.log(chalk.gray(` Action: ${step.action}`) + (step.target ? chalk.gray(` → ${step.target}`) : ''));
1217
+ console.log(chalk.gray(` Purpose: ${step.purpose}`));
1218
+ console.log(` Risk: ` + riskColor(step.risk));
1219
+ });
1220
+ if (plan.rollbackStrategy.length > 0) {
1221
+ console.log(chalk.white('\nRollback Strategy:'));
1222
+ plan.rollbackStrategy.forEach(s => console.log(chalk.gray(` - ${s}`)));
1223
+ }
1224
+ }
1225
+ async function executePlan(plan, agent, pm, planManager) {
1226
+ console.log(chalk.cyan('\n⚔ Executing plan: ' + plan.summary));
1227
+ await planManager.updateStatus(plan.id, 'executing');
1228
+ for (const step of plan.steps) {
1229
+ console.log(chalk.white(`\n→ Step ${step.id.replace('step-', '')}: ${step.name}`));
1230
+ const spinner = ora(`Executing: ${step.action}`).start();
1231
+ try {
1232
+ // Execute based on action type
1233
+ const stepPrompt = `Execute this step of the plan:
1234
+ Step: ${step.name}
1235
+ Action: ${step.action}
1236
+ Target: ${step.target || 'N/A'}
1237
+ Purpose: ${step.purpose}
1238
+
1239
+ Please execute this step now.`;
1240
+ const response = agent.query(stepPrompt);
1241
+ spinner.stop();
1242
+ for await (const message of response) {
1243
+ if (message.type === 'stream_event') {
1244
+ const event = message.event;
1245
+ if (event?.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
1246
+ process.stdout.write(event.delta.text || '');
1247
+ }
1248
+ }
1249
+ }
1250
+ await planManager.updateStepStatus(plan.id, step.id, 'completed');
1251
+ console.log(chalk.green(`\nāœ“ Step ${step.id.replace('step-', '')} completed`));
1252
+ }
1253
+ catch (error) {
1254
+ spinner.fail(`Step ${step.id.replace('step-', '')} failed`);
1255
+ await planManager.updateStepStatus(plan.id, step.id, 'failed');
1256
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
1257
+ const { cont } = await inquirer.prompt([{
1258
+ type: 'confirm',
1259
+ name: 'cont',
1260
+ message: 'Continue with remaining steps?',
1261
+ default: false
1262
+ }]);
1263
+ if (!cont) {
1264
+ await planManager.updateStatus(plan.id, 'failed');
1265
+ return;
1266
+ }
1267
+ }
1268
+ }
1269
+ await planManager.updateStatus(plan.id, 'completed');
1270
+ console.log(chalk.green('\nāœ… Plan completed successfully!'));
1271
+ console.log(chalk.gray('Plan archived. Run /plan-delete all-completed to clean up.'));
1272
+ }
1273
+ async function listPlans(planManager) {
1274
+ const plans = await planManager.listPlans();
1275
+ const pending = plans.filter(p => p.plan.status === 'pending');
1276
+ const completed = plans.filter(p => p.plan.status === 'completed');
1277
+ if (pending.length === 0 && completed.length === 0) {
1278
+ console.log(chalk.gray('\nNo plans found. Use /plan <query> to create one.'));
1279
+ return;
1280
+ }
1281
+ if (pending.length > 0) {
1282
+ console.log(chalk.cyan('\nšŸ“‹ Pending Plans:'));
1283
+ pending.forEach((p, i) => {
1284
+ const age = formatAge(p.plan.created);
1285
+ console.log(chalk.white(` ${i + 1}. ${p.plan.summary}`));
1286
+ console.log(chalk.gray(` Created ${age} • ${p.plan.steps.length} steps`));
1287
+ });
1288
+ console.log(chalk.gray('\n Type a number to execute, or /execute <num>'));
1289
+ }
1290
+ if (completed.length > 0) {
1291
+ console.log(chalk.gray(`\nāœ… ${completed.length} completed plan(s) - run /plan-delete all-completed to clean up`));
1292
+ }
1293
+ }
1294
+ async function executePlanByRef(ref, agent, pm, planManager) {
1295
+ if (/^\d+$/.test(ref)) {
1296
+ await executePlanByNumber(parseInt(ref), agent, pm, planManager);
1297
+ return;
1298
+ }
1299
+ // Assume it's a path
1300
+ try {
1301
+ const plan = await planManager.loadPlan(ref);
1302
+ await executePlan(plan, agent, pm, planManager);
1303
+ }
1304
+ catch (error) {
1305
+ console.log(chalk.red(`Error loading plan: ${ref}`));
1306
+ }
1307
+ }
1308
+ async function executePlanByNumber(num, agent, pm, planManager) {
1309
+ const plans = await planManager.listPlans();
1310
+ const pending = plans.filter(p => p.plan.status === 'pending');
1311
+ if (num < 1 || num > pending.length) {
1312
+ console.log(chalk.red(`Invalid plan number. You have ${pending.length} pending plan(s).`));
1313
+ return;
1314
+ }
1315
+ const planEntry = pending[num - 1];
1316
+ await executePlan(planEntry.plan, agent, pm, planManager);
1317
+ }
1318
+ async function deletePlanByRef(ref, planManager) {
1319
+ if (ref === 'all-completed') {
1320
+ const deleted = await planManager.deleteCompleted();
1321
+ console.log(chalk.green(`\nāœ… Deleted ${deleted} completed plan(s).`));
1322
+ return;
1323
+ }
1324
+ if (ref === 'all') {
1325
+ const { confirm } = await inquirer.prompt([{
1326
+ type: 'confirm',
1327
+ name: 'confirm',
1328
+ message: 'Delete ALL plans (pending and completed)?',
1329
+ default: false
1330
+ }]);
1331
+ if (confirm) {
1332
+ const deleted = await planManager.deleteAll();
1333
+ console.log(chalk.green(`\nāœ… Deleted ${deleted} plan(s).`));
1334
+ }
1335
+ return;
1336
+ }
1337
+ // Delete by number
1338
+ if (/^\d+$/.test(ref)) {
1339
+ const plans = await planManager.listPlans();
1340
+ const pending = plans.filter(p => p.plan.status === 'pending');
1341
+ const num = parseInt(ref);
1342
+ if (num >= 1 && num <= pending.length) {
1343
+ await planManager.deletePlan(pending[num - 1].plan.id);
1344
+ console.log(chalk.green('\nāœ… Plan deleted.'));
1345
+ return;
1346
+ }
1347
+ }
1348
+ // Try as plan ID
1349
+ await planManager.deletePlan(ref);
1350
+ console.log(chalk.green('\nāœ… Plan deleted.'));
1351
+ }
1352
+ // Robust signal handling - force exit even with hanging child processes
1353
+ let isExiting = false;
1354
+ function handleExit(signal) {
1355
+ if (isExiting) {
1356
+ // Force exit on second signal
1357
+ console.log(chalk.red('\n\nForce quitting...'));
1358
+ process.exit(1);
1359
+ }
1360
+ isExiting = true;
1361
+ console.log(chalk.yellow(`\n\nšŸ‘‹ Received ${signal}, shutting down...`));
1362
+ // Give processes 2 seconds to clean up, then force exit
1363
+ setTimeout(() => {
1364
+ console.log(chalk.red('Force exit after timeout'));
1365
+ process.exit(1);
1366
+ }, 2000).unref();
1367
+ // Try graceful exit
1368
+ process.exit(0);
1369
+ }
1370
+ process.on('SIGINT', () => handleExit('SIGINT'));
1371
+ process.on('SIGTERM', () => handleExit('SIGTERM'));
1372
+ // Prevent unhandled rejections from hanging
1373
+ process.on('unhandledRejection', (reason, promise) => {
1374
+ console.error(chalk.red('Unhandled Rejection:'), reason);
1375
+ });
1376
+ // Read stdin history before parsing commander args (synchronous)
1377
+ initStdinHistory();
1378
+ program.parse();
1379
+ //# sourceMappingURL=cli.js.map