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.
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/agent.d.ts +37 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +247 -0
- package/dist/agent.js.map +1 -0
- package/dist/claude-config.d.ts +58 -0
- package/dist/claude-config.d.ts.map +1 -0
- package/dist/claude-config.js +169 -0
- package/dist/claude-config.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1379 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/mcp-config.d.ts +45 -0
- package/dist/mcp-config.d.ts.map +1 -0
- package/dist/mcp-config.js +111 -0
- package/dist/mcp-config.js.map +1 -0
- package/dist/permissions.d.ts +32 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +165 -0
- package/dist/permissions.js.map +1 -0
- package/dist/planner.d.ts +42 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +232 -0
- package/dist/planner.js.map +1 -0
- package/dist/prompts/wiki-system.d.ts +8 -0
- package/dist/prompts/wiki-system.d.ts.map +1 -0
- package/dist/prompts/wiki-system.js +249 -0
- package/dist/prompts/wiki-system.js.map +1 -0
- package/dist/rag/index.d.ts +84 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/index.js +446 -0
- package/dist/rag/index.js.map +1 -0
- package/dist/tools/command-runner.d.ts +21 -0
- package/dist/tools/command-runner.d.ts.map +1 -0
- package/dist/tools/command-runner.js +67 -0
- package/dist/tools/command-runner.js.map +1 -0
- package/dist/tools/file-operations.d.ts +22 -0
- package/dist/tools/file-operations.d.ts.map +1 -0
- package/dist/tools/file-operations.js +119 -0
- package/dist/tools/file-operations.js.map +1 -0
- package/dist/tools/web-tools.d.ts +17 -0
- package/dist/tools/web-tools.d.ts.map +1 -0
- package/dist/tools/web-tools.js +122 -0
- package/dist/tools/web-tools.js.map +1 -0
- package/dist/wiki-agent.d.ts +81 -0
- package/dist/wiki-agent.d.ts.map +1 -0
- package/dist/wiki-agent.js +552 -0
- package/dist/wiki-agent.js.map +1 -0
- package/dist/workflows.d.ts +53 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +169 -0
- package/dist/workflows.js.map +1 -0
- 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
|