@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.
- package/README.md +287 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +259 -0
- package/dist/commands/add.d.ts +9 -0
- package/dist/commands/add.js +50 -0
- package/dist/commands/git/commit.d.ts +12 -0
- package/dist/commands/git/commit.js +97 -0
- package/dist/commands/git/status.d.ts +6 -0
- package/dist/commands/git/status.js +42 -0
- package/dist/commands/index.d.ts +16 -0
- package/dist/commands/index.js +376 -0
- package/dist/commands/mcp/status.d.ts +6 -0
- package/dist/commands/mcp/status.js +31 -0
- package/dist/commands/swarm.d.ts +36 -0
- package/dist/commands/swarm.js +236 -0
- package/dist/commands.d.ts +32 -0
- package/dist/commands.js +427 -0
- package/dist/context.d.ts +116 -0
- package/dist/context.js +327 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +109 -0
- package/dist/lib/agent.d.ts +98 -0
- package/dist/lib/agent.js +281 -0
- package/dist/lib/editor.d.ts +74 -0
- package/dist/lib/editor.js +441 -0
- package/dist/lib/git.d.ts +164 -0
- package/dist/lib/git.js +351 -0
- package/dist/lib/ui.d.ts +159 -0
- package/dist/lib/ui.js +252 -0
- package/dist/mcp/client.d.ts +22 -0
- package/dist/mcp/client.js +81 -0
- package/dist/mcp/manager.d.ts +186 -0
- package/dist/mcp/manager.js +442 -0
- package/dist/prompts/provider.d.ts +22 -0
- package/dist/prompts/provider.js +78 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.js +82 -0
- package/dist/providers/multi.d.ts +11 -0
- package/dist/providers/multi.js +28 -0
- package/dist/registry.d.ts +24 -0
- package/dist/registry.js +379 -0
- package/dist/repoMap.d.ts +5 -0
- package/dist/repoMap.js +79 -0
- package/dist/router.d.ts +41 -0
- package/dist/router.js +108 -0
- package/dist/skills.d.ts +25 -0
- package/dist/skills.js +288 -0
- package/dist/swarm/coordinator.d.ts +86 -0
- package/dist/swarm/coordinator.js +257 -0
- package/dist/swarm/index.d.ts +28 -0
- package/dist/swarm/index.js +29 -0
- package/dist/swarm/task.d.ts +104 -0
- package/dist/swarm/task.js +221 -0
- package/dist/swarm/types.d.ts +132 -0
- package/dist/swarm/types.js +37 -0
- package/dist/swarm/worker.d.ts +107 -0
- package/dist/swarm/worker.js +299 -0
- package/dist/tools/analyzeFile.d.ts +16 -0
- package/dist/tools/analyzeFile.js +43 -0
- package/dist/tools/git.d.ts +40 -0
- package/dist/tools/git.js +236 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.js +165 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.js +296 -0
- package/dist/tools/linter.d.ts +35 -0
- package/dist/tools/linter.js +349 -0
- package/dist/tools/listDir.d.ts +29 -0
- package/dist/tools/listDir.js +50 -0
- package/dist/tools/memory.d.ts +34 -0
- package/dist/tools/memory.js +215 -0
- package/dist/tools/readFiles.d.ts +25 -0
- package/dist/tools/readFiles.js +31 -0
- package/dist/tools/reloadTools.d.ts +11 -0
- package/dist/tools/reloadTools.js +22 -0
- package/dist/tools/runCommand.d.ts +32 -0
- package/dist/tools/runCommand.js +79 -0
- package/dist/tools/scraper.d.ts +31 -0
- package/dist/tools/scraper.js +211 -0
- package/dist/tools/writeFiles.d.ts +63 -0
- package/dist/tools/writeFiles.js +87 -0
- package/dist/ui/server.d.ts +5 -0
- package/dist/ui/server.js +74 -0
- package/dist/watcher.d.ts +35 -0
- package/dist/watcher.js +164 -0
- package/docs/assets/logo.jpeg +0 -0
- 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>;
|
package/dist/commands.js
ADDED
|
@@ -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
|
+
}
|