@stan-chen/simple-cli 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +287 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.js +259 -0
  4. package/dist/commands/add.d.ts +9 -0
  5. package/dist/commands/add.js +50 -0
  6. package/dist/commands/git/commit.d.ts +12 -0
  7. package/dist/commands/git/commit.js +97 -0
  8. package/dist/commands/git/status.d.ts +6 -0
  9. package/dist/commands/git/status.js +42 -0
  10. package/dist/commands/index.d.ts +16 -0
  11. package/dist/commands/index.js +376 -0
  12. package/dist/commands/mcp/status.d.ts +6 -0
  13. package/dist/commands/mcp/status.js +31 -0
  14. package/dist/commands/swarm.d.ts +36 -0
  15. package/dist/commands/swarm.js +236 -0
  16. package/dist/commands.d.ts +32 -0
  17. package/dist/commands.js +427 -0
  18. package/dist/context.d.ts +116 -0
  19. package/dist/context.js +327 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +109 -0
  22. package/dist/lib/agent.d.ts +98 -0
  23. package/dist/lib/agent.js +281 -0
  24. package/dist/lib/editor.d.ts +74 -0
  25. package/dist/lib/editor.js +441 -0
  26. package/dist/lib/git.d.ts +164 -0
  27. package/dist/lib/git.js +351 -0
  28. package/dist/lib/ui.d.ts +159 -0
  29. package/dist/lib/ui.js +252 -0
  30. package/dist/mcp/client.d.ts +22 -0
  31. package/dist/mcp/client.js +81 -0
  32. package/dist/mcp/manager.d.ts +186 -0
  33. package/dist/mcp/manager.js +442 -0
  34. package/dist/prompts/provider.d.ts +22 -0
  35. package/dist/prompts/provider.js +78 -0
  36. package/dist/providers/index.d.ts +15 -0
  37. package/dist/providers/index.js +82 -0
  38. package/dist/providers/multi.d.ts +11 -0
  39. package/dist/providers/multi.js +28 -0
  40. package/dist/registry.d.ts +24 -0
  41. package/dist/registry.js +379 -0
  42. package/dist/repoMap.d.ts +5 -0
  43. package/dist/repoMap.js +79 -0
  44. package/dist/router.d.ts +41 -0
  45. package/dist/router.js +108 -0
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +288 -0
  48. package/dist/swarm/coordinator.d.ts +86 -0
  49. package/dist/swarm/coordinator.js +257 -0
  50. package/dist/swarm/index.d.ts +28 -0
  51. package/dist/swarm/index.js +29 -0
  52. package/dist/swarm/task.d.ts +104 -0
  53. package/dist/swarm/task.js +221 -0
  54. package/dist/swarm/types.d.ts +132 -0
  55. package/dist/swarm/types.js +37 -0
  56. package/dist/swarm/worker.d.ts +107 -0
  57. package/dist/swarm/worker.js +299 -0
  58. package/dist/tools/analyzeFile.d.ts +16 -0
  59. package/dist/tools/analyzeFile.js +43 -0
  60. package/dist/tools/git.d.ts +40 -0
  61. package/dist/tools/git.js +236 -0
  62. package/dist/tools/glob.d.ts +34 -0
  63. package/dist/tools/glob.js +165 -0
  64. package/dist/tools/grep.d.ts +53 -0
  65. package/dist/tools/grep.js +296 -0
  66. package/dist/tools/linter.d.ts +35 -0
  67. package/dist/tools/linter.js +349 -0
  68. package/dist/tools/listDir.d.ts +29 -0
  69. package/dist/tools/listDir.js +50 -0
  70. package/dist/tools/memory.d.ts +34 -0
  71. package/dist/tools/memory.js +215 -0
  72. package/dist/tools/readFiles.d.ts +25 -0
  73. package/dist/tools/readFiles.js +31 -0
  74. package/dist/tools/reloadTools.d.ts +11 -0
  75. package/dist/tools/reloadTools.js +22 -0
  76. package/dist/tools/runCommand.d.ts +32 -0
  77. package/dist/tools/runCommand.js +79 -0
  78. package/dist/tools/scraper.d.ts +31 -0
  79. package/dist/tools/scraper.js +211 -0
  80. package/dist/tools/writeFiles.d.ts +63 -0
  81. package/dist/tools/writeFiles.js +87 -0
  82. package/dist/ui/server.d.ts +5 -0
  83. package/dist/ui/server.js +74 -0
  84. package/dist/watcher.d.ts +35 -0
  85. package/dist/watcher.js +164 -0
  86. package/docs/assets/logo.jpeg +0 -0
  87. package/package.json +78 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Swarm Command - Run multiple agents in parallel
3
+ */
4
+ import { readFileSync, existsSync } from 'fs';
5
+ import { SwarmCoordinator } from '../swarm/index.js';
6
+ import { execute as globExecute } from '../tools/glob.js';
7
+ /**
8
+ * Parse swarm options from command line arguments
9
+ */
10
+ export function parseSwarmArgs(args) {
11
+ const options = {};
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i];
14
+ if (arg === '--tasks' && args[i + 1]) {
15
+ options.tasksFile = args[++i];
16
+ }
17
+ else if (arg === '--task' && args[i + 1]) {
18
+ options.task = args[++i];
19
+ }
20
+ else if (arg === '--scope' && args[i + 1]) {
21
+ options.scope = args[++i];
22
+ }
23
+ else if (arg === '--concurrency' && args[i + 1]) {
24
+ options.concurrency = parseInt(args[++i], 10);
25
+ }
26
+ else if (arg === '--timeout' && args[i + 1]) {
27
+ options.timeout = parseInt(args[++i], 10);
28
+ }
29
+ else if (arg === '--branch' && args[i + 1]) {
30
+ options.branch = args[++i];
31
+ }
32
+ else if (arg === '--yolo') {
33
+ options.yolo = true;
34
+ }
35
+ else if (!arg.startsWith('--') && !options.tasksFile && existsSync(arg)) {
36
+ // Positional argument - tasks file
37
+ options.tasksFile = arg;
38
+ }
39
+ }
40
+ return options;
41
+ }
42
+ /**
43
+ * Load tasks from a JSON file
44
+ */
45
+ export function loadTasksFromFile(filePath) {
46
+ if (!existsSync(filePath)) {
47
+ throw new Error(`Tasks file not found: ${filePath}`);
48
+ }
49
+ const content = readFileSync(filePath, 'utf-8');
50
+ const data = JSON.parse(content);
51
+ // Support both { tasks: [...] } and [...] formats
52
+ if (Array.isArray(data)) {
53
+ return { tasks: data };
54
+ }
55
+ const tasks = data.tasks || [];
56
+ const options = {};
57
+ if (data.session) {
58
+ if (data.session.concurrency)
59
+ options.concurrency = data.session.concurrency;
60
+ if (data.session.timeout)
61
+ options.timeout = data.session.timeout;
62
+ if (data.session.branch)
63
+ options.branch = data.session.branch;
64
+ }
65
+ return { tasks, options };
66
+ }
67
+ /**
68
+ * Create tasks from a glob pattern
69
+ */
70
+ export async function createTasksFromScope(task, scope, type = 'implement') {
71
+ const result = await globExecute({ pattern: scope, maxResults: 100, includeDirectories: false });
72
+ return result.matches.map((file, i) => ({
73
+ id: `task-${i}`,
74
+ type,
75
+ description: `${task} in ${file}`,
76
+ scope: { files: [file] },
77
+ dependencies: [],
78
+ priority: 2,
79
+ timeout: 300000,
80
+ retries: 2,
81
+ }));
82
+ }
83
+ /**
84
+ * Run swarm orchestrator
85
+ */
86
+ export async function runSwarm(options) {
87
+ console.log('\n🐝 Simple-CLI Swarm Mode\n');
88
+ let tasks = [];
89
+ let coordinatorOptions = {};
90
+ // Load tasks from file or create from options
91
+ if (options.tasksFile) {
92
+ console.log(`📄 Loading tasks from ${options.tasksFile}...`);
93
+ const loaded = loadTasksFromFile(options.tasksFile);
94
+ tasks = loaded.tasks;
95
+ coordinatorOptions = loaded.options || {};
96
+ }
97
+ else if (options.task && options.scope) {
98
+ console.log(`🔍 Creating tasks from scope: ${options.scope}...`);
99
+ tasks = await createTasksFromScope(options.task, options.scope);
100
+ }
101
+ else if (options.task) {
102
+ // Single task
103
+ tasks = [{
104
+ id: 'single-task',
105
+ type: 'implement',
106
+ description: options.task,
107
+ scope: {},
108
+ dependencies: [],
109
+ priority: 1,
110
+ timeout: options.timeout || 300000,
111
+ retries: 2,
112
+ }];
113
+ }
114
+ if (tasks.length === 0) {
115
+ console.error('❌ No tasks to run. Provide --tasks <file> or --task "description"');
116
+ process.exit(1);
117
+ }
118
+ console.log(`📋 ${tasks.length} task(s) to execute\n`);
119
+ // Apply CLI options
120
+ if (options.concurrency)
121
+ coordinatorOptions.concurrency = options.concurrency;
122
+ if (options.timeout)
123
+ coordinatorOptions.timeout = options.timeout;
124
+ if (options.branch)
125
+ coordinatorOptions.branch = options.branch;
126
+ if (options.yolo !== undefined)
127
+ coordinatorOptions.yolo = options.yolo;
128
+ // Create coordinator
129
+ const coordinator = new SwarmCoordinator({
130
+ cwd: process.cwd(),
131
+ ...coordinatorOptions,
132
+ });
133
+ // Add event handlers
134
+ coordinator.on('task:start', (task, workerId) => {
135
+ console.log(`🚀 [${workerId}] Starting: ${task.description.slice(0, 60)}...`);
136
+ });
137
+ coordinator.on('task:complete', (task, result) => {
138
+ const status = result.success ? '✅' : '⚠️';
139
+ console.log(`${status} [${task.id}] Done in ${result.duration}ms`);
140
+ if (result.filesChanged.length > 0) {
141
+ console.log(` Files: ${result.filesChanged.join(', ')}`);
142
+ }
143
+ });
144
+ coordinator.on('task:fail', (task, error) => {
145
+ console.error(`❌ [${task.id}] Failed: ${error.message}`);
146
+ });
147
+ coordinator.on('task:retry', (task, attempt) => {
148
+ console.log(`🔄 [${task.id}] Retry attempt ${attempt}`);
149
+ });
150
+ // Add tasks
151
+ coordinator.addTasks(tasks);
152
+ // Run swarm
153
+ const startTime = Date.now();
154
+ console.log(`⏱️ Starting swarm with concurrency: ${coordinatorOptions.concurrency || 4}\n`);
155
+ try {
156
+ const result = await coordinator.run();
157
+ // Print summary
158
+ console.log('\n' + '═'.repeat(50));
159
+ console.log('📊 SWARM COMPLETE');
160
+ console.log('═'.repeat(50));
161
+ console.log(` Total Tasks: ${result.total}`);
162
+ console.log(` Completed: ${result.completed} ✅`);
163
+ console.log(` Failed: ${result.failed} ❌`);
164
+ console.log(` Skipped: ${result.skipped} ⏭️`);
165
+ console.log(` Duration: ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
166
+ console.log(` Success Rate: ${(result.successRate * 100).toFixed(1)}%`);
167
+ if (result.failedTasks.length > 0) {
168
+ console.log('\n❌ Failed Tasks:');
169
+ for (const f of result.failedTasks) {
170
+ console.log(` - ${f.task.id}: ${f.error}`);
171
+ }
172
+ }
173
+ console.log('═'.repeat(50) + '\n');
174
+ // Exit with error code if any failures
175
+ if (result.failed > 0) {
176
+ process.exit(1);
177
+ }
178
+ }
179
+ catch (error) {
180
+ console.error(`\n❌ Swarm error: ${error instanceof Error ? error.message : error}`);
181
+ process.exit(1);
182
+ }
183
+ }
184
+ /**
185
+ * Print swarm help
186
+ */
187
+ export function printSwarmHelp() {
188
+ console.log(`
189
+ 🐝 Simple-CLI Swarm Mode
190
+
191
+ USAGE
192
+ simple --swarm [options]
193
+
194
+ OPTIONS
195
+ --tasks <file> Load tasks from JSON file
196
+ --task "desc" Single task description
197
+ --scope "pattern" Glob pattern for files (creates task per file)
198
+ --concurrency <n> Max parallel workers (default: 4)
199
+ --timeout <ms> Task timeout in milliseconds
200
+ --branch <name> Git branch for changes
201
+ --yolo Auto-approve all actions
202
+
203
+ EXAMPLES
204
+ # Run tasks from file
205
+ simple --swarm --tasks tasks.json
206
+
207
+ # Single task
208
+ simple --swarm --yolo --task "add tests to all files"
209
+
210
+ # Task per file matching pattern
211
+ simple --swarm --yolo --task "add JSDoc" --scope "src/**/*.ts"
212
+
213
+ # With concurrency limit
214
+ simple --swarm --concurrency 2 --tasks tasks.json
215
+
216
+ TASKS FILE FORMAT
217
+ {
218
+ "session": {
219
+ "concurrency": 4,
220
+ "timeout": 300000,
221
+ "branch": "feature/swarm"
222
+ },
223
+ "tasks": [
224
+ {
225
+ "id": "task-1",
226
+ "type": "implement",
227
+ "description": "Add validation",
228
+ "scope": { "files": ["src/api.ts"] },
229
+ "priority": 1,
230
+ "timeout": 60000,
231
+ "retries": 2
232
+ }
233
+ ]
234
+ }
235
+ `);
236
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Slash Commands System
3
+ * Based on Aider's commands.py
4
+ */
5
+ export interface CommandContext {
6
+ cwd: string;
7
+ activeFiles: Set<string>;
8
+ readOnlyFiles: Set<string>;
9
+ history: Array<{
10
+ role: string;
11
+ content: string;
12
+ }>;
13
+ io: {
14
+ output: (message: string) => void;
15
+ error: (message: string) => void;
16
+ confirm: (message: string) => Promise<boolean>;
17
+ prompt: (message: string) => Promise<string>;
18
+ };
19
+ }
20
+ export interface Command {
21
+ name: string;
22
+ aliases: string[];
23
+ description: string;
24
+ execute: (args: string, context: CommandContext) => Promise<string | void>;
25
+ }
26
+ export declare function parseCommand(input: string): {
27
+ command: string;
28
+ args: string;
29
+ } | null;
30
+ export declare const commands: Command[];
31
+ export declare function findCommand(name: string): Command | undefined;
32
+ export declare function executeCommand(input: string, context: CommandContext): Promise<string | void>;
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Slash Commands System
3
+ * Based on Aider's commands.py
4
+ */
5
+ import { readFile, writeFile } from 'fs/promises';
6
+ import { existsSync } from 'fs';
7
+ import { relative, resolve } from 'path';
8
+ import { getMCPManager } from './mcp/manager.js';
9
+ import { getStagedDiff } from './tools/git.js';
10
+ import { execute as grepExecute } from './tools/grep.js';
11
+ import { execute as globExecute } from './tools/glob.js';
12
+ // Parse command line into command and args
13
+ export function parseCommand(input) {
14
+ if (!input.startsWith('/')) {
15
+ return null;
16
+ }
17
+ const trimmed = input.slice(1).trim();
18
+ const spaceIndex = trimmed.indexOf(' ');
19
+ if (spaceIndex === -1) {
20
+ return { command: trimmed.toLowerCase(), args: '' };
21
+ }
22
+ return {
23
+ command: trimmed.slice(0, spaceIndex).toLowerCase(),
24
+ args: trimmed.slice(spaceIndex + 1).trim(),
25
+ };
26
+ }
27
+ // Built-in commands
28
+ export const commands = [
29
+ // File Management
30
+ {
31
+ name: 'add',
32
+ aliases: ['a'],
33
+ description: 'Add files to the chat context',
34
+ execute: async (args, ctx) => {
35
+ if (!args) {
36
+ ctx.io.output('Usage: /add <file_pattern>');
37
+ return;
38
+ }
39
+ const patterns = args.split(/\s+/);
40
+ let added = 0;
41
+ for (const pattern of patterns) {
42
+ // Check if it's a glob pattern
43
+ if (pattern.includes('*')) {
44
+ const result = await globExecute({ pattern, cwd: ctx.cwd, ignore: [], maxResults: 1000, includeDirectories: false });
45
+ for (const file of result.matches) {
46
+ const fullPath = resolve(ctx.cwd, file);
47
+ if (!ctx.activeFiles.has(fullPath)) {
48
+ ctx.activeFiles.add(fullPath);
49
+ ctx.io.output(`Added ${file}`);
50
+ added++;
51
+ }
52
+ }
53
+ }
54
+ else {
55
+ const fullPath = resolve(ctx.cwd, pattern);
56
+ if (existsSync(fullPath)) {
57
+ if (!ctx.activeFiles.has(fullPath)) {
58
+ ctx.activeFiles.add(fullPath);
59
+ ctx.io.output(`Added ${pattern}`);
60
+ added++;
61
+ }
62
+ }
63
+ else {
64
+ // File doesn't exist - offer to create it
65
+ const create = await ctx.io.confirm(`File ${pattern} doesn't exist. Create it?`);
66
+ if (create) {
67
+ await writeFile(fullPath, '');
68
+ ctx.activeFiles.add(fullPath);
69
+ ctx.io.output(`Created and added ${pattern}`);
70
+ added++;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ ctx.io.output(`Added ${added} file(s) to the chat`);
76
+ },
77
+ },
78
+ {
79
+ name: 'drop',
80
+ aliases: ['d', 'remove'],
81
+ description: 'Remove files from the chat context',
82
+ execute: async (args, ctx) => {
83
+ if (!args) {
84
+ // Drop all files
85
+ const count = ctx.activeFiles.size;
86
+ ctx.activeFiles.clear();
87
+ ctx.io.output(`Dropped all ${count} file(s)`);
88
+ return;
89
+ }
90
+ const patterns = args.split(/\s+/);
91
+ let dropped = 0;
92
+ for (const pattern of patterns) {
93
+ const fullPath = resolve(ctx.cwd, pattern);
94
+ if (ctx.activeFiles.has(fullPath)) {
95
+ ctx.activeFiles.delete(fullPath);
96
+ ctx.io.output(`Dropped ${pattern}`);
97
+ dropped++;
98
+ }
99
+ }
100
+ ctx.io.output(`Dropped ${dropped} file(s)`);
101
+ },
102
+ },
103
+ {
104
+ name: 'ls',
105
+ aliases: ['files', 'list'],
106
+ description: 'List files in the chat context',
107
+ execute: async (args, ctx) => {
108
+ if (ctx.activeFiles.size === 0 && ctx.readOnlyFiles.size === 0) {
109
+ ctx.io.output('No files in chat context');
110
+ return;
111
+ }
112
+ if (ctx.activeFiles.size > 0) {
113
+ ctx.io.output('\nEditable files:');
114
+ for (const file of ctx.activeFiles) {
115
+ ctx.io.output(` ${relative(ctx.cwd, file)}`);
116
+ }
117
+ }
118
+ if (ctx.readOnlyFiles.size > 0) {
119
+ ctx.io.output('\nRead-only files:');
120
+ for (const file of ctx.readOnlyFiles) {
121
+ ctx.io.output(` ${relative(ctx.cwd, file)}`);
122
+ }
123
+ }
124
+ },
125
+ },
126
+ {
127
+ name: 'read-only',
128
+ aliases: ['ro'],
129
+ description: 'Add files as read-only context',
130
+ execute: async (args, ctx) => {
131
+ if (!args) {
132
+ // Convert all active files to read-only
133
+ for (const file of ctx.activeFiles) {
134
+ ctx.readOnlyFiles.add(file);
135
+ }
136
+ ctx.activeFiles.clear();
137
+ ctx.io.output('Converted all files to read-only');
138
+ return;
139
+ }
140
+ const patterns = args.split(/\s+/);
141
+ for (const pattern of patterns) {
142
+ if (pattern.includes('*')) {
143
+ const result = await globExecute({ pattern, cwd: ctx.cwd, maxResults: 1000, includeDirectories: false });
144
+ for (const file of result.matches) {
145
+ const fullPath = resolve(ctx.cwd, file);
146
+ ctx.readOnlyFiles.add(fullPath);
147
+ ctx.io.output(`Added ${file} as read-only`);
148
+ }
149
+ }
150
+ else {
151
+ const fullPath = resolve(ctx.cwd, pattern);
152
+ if (existsSync(fullPath)) {
153
+ ctx.readOnlyFiles.add(fullPath);
154
+ ctx.io.output(`Added ${pattern} as read-only`);
155
+ }
156
+ }
157
+ }
158
+ },
159
+ },
160
+ // Git Integration
161
+ {
162
+ name: 'git',
163
+ aliases: [],
164
+ description: 'Run a git command',
165
+ execute: async (args, ctx) => {
166
+ const { spawnSync } = await import('child_process');
167
+ const result = spawnSync('git', args.split(/\s+/), {
168
+ cwd: ctx.cwd,
169
+ encoding: 'utf-8',
170
+ });
171
+ if (result.stdout)
172
+ ctx.io.output(result.stdout);
173
+ if (result.stderr)
174
+ ctx.io.error(result.stderr);
175
+ },
176
+ },
177
+ {
178
+ name: 'diff',
179
+ aliases: [],
180
+ description: 'Show git diff of changes',
181
+ execute: async (args, ctx) => {
182
+ const { spawnSync } = await import('child_process');
183
+ const gitArgs = args ? args.split(/\s+/) : [];
184
+ const result = spawnSync('git', ['diff', '--no-color', ...gitArgs], {
185
+ cwd: ctx.cwd,
186
+ encoding: 'utf-8',
187
+ });
188
+ if (result.stdout) {
189
+ ctx.io.output(result.stdout);
190
+ }
191
+ else {
192
+ ctx.io.output('No changes');
193
+ }
194
+ },
195
+ },
196
+ {
197
+ name: 'commit',
198
+ aliases: [],
199
+ description: 'Commit staged changes with AI-generated message',
200
+ execute: async (args, ctx) => {
201
+ const diff = getStagedDiff(ctx.cwd);
202
+ if (!diff) {
203
+ ctx.io.output('No staged changes to commit');
204
+ return;
205
+ }
206
+ const message = args || 'Update files';
207
+ const { spawnSync } = await import('child_process');
208
+ const result = spawnSync('git', ['commit', '-m', message], {
209
+ cwd: ctx.cwd,
210
+ encoding: 'utf-8',
211
+ });
212
+ if (result.stdout)
213
+ ctx.io.output(result.stdout);
214
+ if (result.stderr)
215
+ ctx.io.error(result.stderr);
216
+ },
217
+ },
218
+ // Search
219
+ {
220
+ name: 'search',
221
+ aliases: ['grep', 'find'],
222
+ description: 'Search for pattern in files',
223
+ execute: async (args, ctx) => {
224
+ if (!args) {
225
+ ctx.io.output('Usage: /search <pattern> [path]');
226
+ return;
227
+ }
228
+ const parts = args.split(/\s+/);
229
+ const pattern = parts[0];
230
+ const path = parts[1] || ctx.cwd;
231
+ const result = await grepExecute({
232
+ pattern,
233
+ path,
234
+ maxResults: 50,
235
+ ignoreCase: true,
236
+ contextLines: 2,
237
+ filesOnly: false,
238
+ includeHidden: false,
239
+ });
240
+ if (result.matches.length === 0) {
241
+ ctx.io.output('No matches found');
242
+ return;
243
+ }
244
+ ctx.io.output(`Found ${result.count} matches in ${result.files.length} file(s):`);
245
+ for (const match of result.matches.slice(0, 20)) {
246
+ ctx.io.output(` ${match.file}:${match.line}: ${match.text.trim()}`);
247
+ }
248
+ if (result.truncated) {
249
+ ctx.io.output(` ... and more (truncated)`);
250
+ }
251
+ },
252
+ },
253
+ // Chat Management
254
+ {
255
+ name: 'clear',
256
+ aliases: ['reset'],
257
+ description: 'Clear chat history',
258
+ execute: async (args, ctx) => {
259
+ ctx.history.length = 0;
260
+ ctx.io.output('Chat history cleared');
261
+ },
262
+ },
263
+ {
264
+ name: 'undo',
265
+ aliases: [],
266
+ description: 'Undo the last git commit made by the AI',
267
+ execute: async (args, ctx) => {
268
+ const { spawnSync } = await import('child_process');
269
+ const result = spawnSync('git', ['reset', '--soft', 'HEAD~1'], {
270
+ cwd: ctx.cwd,
271
+ encoding: 'utf-8',
272
+ });
273
+ if (result.status === 0) {
274
+ ctx.io.output('Undid last commit');
275
+ }
276
+ else {
277
+ ctx.io.error('Failed to undo commit');
278
+ }
279
+ },
280
+ },
281
+ // Context
282
+ {
283
+ name: 'tokens',
284
+ aliases: [],
285
+ description: 'Show approximate token count',
286
+ execute: async (args, ctx) => {
287
+ let totalChars = 0;
288
+ for (const file of ctx.activeFiles) {
289
+ try {
290
+ const content = await readFile(file, 'utf-8');
291
+ totalChars += content.length;
292
+ }
293
+ catch { }
294
+ }
295
+ for (const file of ctx.readOnlyFiles) {
296
+ try {
297
+ const content = await readFile(file, 'utf-8');
298
+ totalChars += content.length;
299
+ }
300
+ catch { }
301
+ }
302
+ for (const msg of ctx.history) {
303
+ totalChars += msg.content.length;
304
+ }
305
+ // Rough estimate: ~4 chars per token
306
+ const estimatedTokens = Math.ceil(totalChars / 4);
307
+ ctx.io.output(`Approximate tokens: ${estimatedTokens.toLocaleString()}`);
308
+ ctx.io.output(` Files: ${ctx.activeFiles.size} editable, ${ctx.readOnlyFiles.size} read-only`);
309
+ ctx.io.output(` History: ${ctx.history.length} messages`);
310
+ },
311
+ },
312
+ // MCP
313
+ {
314
+ name: 'mcp',
315
+ aliases: [],
316
+ description: 'MCP server management',
317
+ execute: async (args, ctx) => {
318
+ const manager = getMCPManager();
319
+ const parts = args.split(/\s+/);
320
+ const subcommand = parts[0] || 'status';
321
+ switch (subcommand) {
322
+ case 'status': {
323
+ const statuses = manager.getAllServerStatuses();
324
+ if (statuses.size === 0) {
325
+ ctx.io.output('No MCP servers configured');
326
+ return;
327
+ }
328
+ ctx.io.output('MCP Servers:');
329
+ for (const [name, status] of statuses) {
330
+ ctx.io.output(` ${name}: ${status}`);
331
+ }
332
+ break;
333
+ }
334
+ case 'tools': {
335
+ const tools = manager.getAllTools();
336
+ if (tools.length === 0) {
337
+ ctx.io.output('No MCP tools available');
338
+ return;
339
+ }
340
+ ctx.io.output(`MCP Tools (${tools.length}):`);
341
+ for (const tool of tools) {
342
+ ctx.io.output(` ${tool.name} (${tool.serverName}): ${tool.description}`);
343
+ }
344
+ break;
345
+ }
346
+ case 'connect': {
347
+ await manager.connectAll();
348
+ ctx.io.output('Connected to MCP servers');
349
+ break;
350
+ }
351
+ case 'disconnect': {
352
+ await manager.disconnectAll();
353
+ ctx.io.output('Disconnected from MCP servers');
354
+ break;
355
+ }
356
+ default:
357
+ ctx.io.output('Usage: /mcp [status|tools|connect|disconnect]');
358
+ }
359
+ },
360
+ },
361
+ // Web
362
+ {
363
+ name: 'web',
364
+ aliases: ['url', 'fetch'],
365
+ description: 'Fetch a URL and add to context',
366
+ execute: async (args, ctx) => {
367
+ if (!args) {
368
+ ctx.io.output('Usage: /web <url>');
369
+ return;
370
+ }
371
+ const { execute: scrapeExecute } = await import('./tools/scraper.js');
372
+ const result = await scrapeExecute({ url: args, convertToMarkdown: true, verifySSL: true, timeout: 10000 });
373
+ if (result.error) {
374
+ ctx.io.error(`Failed to fetch: ${result.error}`);
375
+ return;
376
+ }
377
+ ctx.io.output(`Fetched ${args} (${result.content.length} chars)`);
378
+ // Add to history as context
379
+ ctx.history.push({
380
+ role: 'user',
381
+ content: `Content from ${args}:\n\n${result.content}`,
382
+ });
383
+ },
384
+ },
385
+ // Help
386
+ {
387
+ name: 'help',
388
+ aliases: ['h', '?'],
389
+ description: 'Show available commands',
390
+ execute: async (args, ctx) => {
391
+ ctx.io.output('\nAvailable commands:\n');
392
+ for (const cmd of commands) {
393
+ const aliases = cmd.aliases.length > 0 ? ` (${cmd.aliases.join(', ')})` : '';
394
+ ctx.io.output(` /${cmd.name}${aliases}`);
395
+ ctx.io.output(` ${cmd.description}\n`);
396
+ }
397
+ },
398
+ },
399
+ // Exit
400
+ {
401
+ name: 'exit',
402
+ aliases: ['quit', 'q'],
403
+ description: 'Exit the CLI',
404
+ execute: async (args, ctx) => {
405
+ ctx.io.output('Goodbye!');
406
+ process.exit(0);
407
+ },
408
+ },
409
+ ];
410
+ // Find command by name or alias
411
+ export function findCommand(name) {
412
+ const lower = name.toLowerCase();
413
+ return commands.find(cmd => cmd.name === lower || cmd.aliases.includes(lower));
414
+ }
415
+ // Execute a command
416
+ export async function executeCommand(input, context) {
417
+ const parsed = parseCommand(input);
418
+ if (!parsed) {
419
+ return undefined; // Not a command
420
+ }
421
+ const command = findCommand(parsed.command);
422
+ if (!command) {
423
+ context.io.error(`Unknown command: /${parsed.command}. Type /help for available commands.`);
424
+ return;
425
+ }
426
+ return command.execute(parsed.args, context);
427
+ }