@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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Core - Main agent loop with reflection and retry
|
|
3
|
+
* Implements Aider-style reasoning and error recovery
|
|
4
|
+
*/
|
|
5
|
+
import { applyFileEdits, parseEditBlocks } from './editor.js';
|
|
6
|
+
import { generateCommitMessage } from './git.js';
|
|
7
|
+
import * as ui from './ui.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse LLM response into structured format
|
|
10
|
+
*/
|
|
11
|
+
export function parseResponse(response) {
|
|
12
|
+
// Extract thought/reasoning
|
|
13
|
+
const thoughtMatch = response.match(/<thought>([\s\S]*?)<\/thought>/i);
|
|
14
|
+
const thought = thoughtMatch?.[1]?.trim();
|
|
15
|
+
// Extract edit blocks
|
|
16
|
+
const editBlocks = parseEditBlocks(response);
|
|
17
|
+
// Extract tool action
|
|
18
|
+
const jsonMatch = response.match(/\{[\s\S]*"tool"[\s\S]*\}/);
|
|
19
|
+
let action = { tool: 'none', message: 'No action parsed' };
|
|
20
|
+
if (jsonMatch) {
|
|
21
|
+
try {
|
|
22
|
+
action = JSON.parse(jsonMatch[0]);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Keep default
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { thought, action, editBlocks };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build reflection prompt for retry
|
|
32
|
+
*/
|
|
33
|
+
export function buildReflectionPrompt(context) {
|
|
34
|
+
let prompt = `## Reflection (Attempt ${context.attempt})
|
|
35
|
+
|
|
36
|
+
Your previous response had errors that need to be fixed.
|
|
37
|
+
|
|
38
|
+
### Previous Error
|
|
39
|
+
${context.previousError}
|
|
40
|
+
|
|
41
|
+
`;
|
|
42
|
+
if (context.failedEdits.length > 0) {
|
|
43
|
+
prompt += `### Failed Edits\n`;
|
|
44
|
+
for (const edit of context.failedEdits) {
|
|
45
|
+
prompt += `
|
|
46
|
+
**File:** ${edit.file}
|
|
47
|
+
**Error:** ${edit.error}
|
|
48
|
+
${edit.suggestion ? `**Suggestion:** ${edit.suggestion}` : ''}
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
prompt += `
|
|
53
|
+
### Instructions
|
|
54
|
+
- Review the error carefully
|
|
55
|
+
- The SEARCH block must EXACTLY match existing content
|
|
56
|
+
- Include enough context to make the match unique
|
|
57
|
+
- Try again with corrected SEARCH/REPLACE blocks
|
|
58
|
+
`;
|
|
59
|
+
return prompt;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build lint error prompt
|
|
63
|
+
*/
|
|
64
|
+
export function buildLintErrorPrompt(file, errors) {
|
|
65
|
+
return `## Lint Errors Detected
|
|
66
|
+
|
|
67
|
+
The file \`${file}\` has syntax errors after your changes:
|
|
68
|
+
|
|
69
|
+
\`\`\`
|
|
70
|
+
${errors}
|
|
71
|
+
\`\`\`
|
|
72
|
+
|
|
73
|
+
Please fix these errors by providing corrected SEARCH/REPLACE blocks.
|
|
74
|
+
Focus on the specific lines mentioned in the errors.
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build test failure prompt
|
|
79
|
+
*/
|
|
80
|
+
export function buildTestFailurePrompt(output) {
|
|
81
|
+
return `## Test Failure
|
|
82
|
+
|
|
83
|
+
The tests failed after your changes:
|
|
84
|
+
|
|
85
|
+
\`\`\`
|
|
86
|
+
${output.slice(0, 2000)}${output.length > 2000 ? '\n... (truncated)' : ''}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
Please analyze the failure and fix the code.
|
|
90
|
+
Provide SEARCH/REPLACE blocks for the necessary changes.
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Agent class - Main orchestration logic
|
|
95
|
+
*/
|
|
96
|
+
export class Agent {
|
|
97
|
+
config;
|
|
98
|
+
git;
|
|
99
|
+
generateFn;
|
|
100
|
+
executeTool;
|
|
101
|
+
lintFn;
|
|
102
|
+
testFn;
|
|
103
|
+
constructor(options) {
|
|
104
|
+
this.config = options.config;
|
|
105
|
+
this.git = options.git;
|
|
106
|
+
this.generateFn = options.generateFn;
|
|
107
|
+
this.executeTool = options.executeTool;
|
|
108
|
+
this.lintFn = options.lintFn;
|
|
109
|
+
this.testFn = options.testFn;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Process a user message with reflection loop
|
|
113
|
+
*/
|
|
114
|
+
async process(userMessage, history, systemPrompt) {
|
|
115
|
+
let messages = [
|
|
116
|
+
{ role: 'system', content: systemPrompt },
|
|
117
|
+
...history,
|
|
118
|
+
{ role: 'user', content: userMessage },
|
|
119
|
+
];
|
|
120
|
+
let attempt = 0;
|
|
121
|
+
let editResults = [];
|
|
122
|
+
let lintResults = [];
|
|
123
|
+
let testResult;
|
|
124
|
+
let commitResult;
|
|
125
|
+
while (attempt < this.config.maxReflections) {
|
|
126
|
+
attempt++;
|
|
127
|
+
// Generate response
|
|
128
|
+
const llmResponse = await ui.spin(attempt === 1 ? 'Thinking...' : `Reflecting (attempt ${attempt})...`, () => this.generateFn(messages));
|
|
129
|
+
// Parse response
|
|
130
|
+
const response = parseResponse(llmResponse);
|
|
131
|
+
// Show thought if present
|
|
132
|
+
if (response.thought) {
|
|
133
|
+
ui.showThought(response.thought);
|
|
134
|
+
}
|
|
135
|
+
// Apply edit blocks if present
|
|
136
|
+
if (response.editBlocks && response.editBlocks.length > 0) {
|
|
137
|
+
ui.step(`Applying ${response.editBlocks.length} edit(s)...`);
|
|
138
|
+
editResults = await applyFileEdits(response.editBlocks);
|
|
139
|
+
const failed = editResults.filter(r => !r.success);
|
|
140
|
+
const succeeded = editResults.filter(r => r.success);
|
|
141
|
+
// Show results
|
|
142
|
+
for (const result of succeeded) {
|
|
143
|
+
ui.success(`✓ ${result.file}`);
|
|
144
|
+
if (result.diff) {
|
|
145
|
+
ui.showDiff(result.diff);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const result of failed) {
|
|
149
|
+
ui.error(`✗ ${result.file}: ${result.error}`);
|
|
150
|
+
if (result.suggestion) {
|
|
151
|
+
ui.note(result.suggestion, 'Suggestion');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// If any edits failed, reflect and retry
|
|
155
|
+
if (failed.length > 0 && attempt < this.config.maxReflections) {
|
|
156
|
+
const reflectionPrompt = buildReflectionPrompt({
|
|
157
|
+
attempt,
|
|
158
|
+
previousError: failed.map(f => f.error).join('\n'),
|
|
159
|
+
previousResponse: llmResponse,
|
|
160
|
+
failedEdits: failed,
|
|
161
|
+
});
|
|
162
|
+
messages = [
|
|
163
|
+
...messages,
|
|
164
|
+
{ role: 'assistant', content: llmResponse },
|
|
165
|
+
{ role: 'user', content: reflectionPrompt },
|
|
166
|
+
];
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// Run linting if enabled and edits succeeded
|
|
170
|
+
if (this.config.autoLint && this.lintFn && succeeded.length > 0) {
|
|
171
|
+
const filesToLint = [...new Set(succeeded.map(r => r.file))];
|
|
172
|
+
for (const file of filesToLint) {
|
|
173
|
+
const lintResult = await ui.spin(`Linting ${file}...`, () => this.lintFn(file));
|
|
174
|
+
lintResults.push({ file, ...lintResult });
|
|
175
|
+
if (!lintResult.passed && attempt < this.config.maxReflections) {
|
|
176
|
+
ui.error(`Lint errors in ${file}`);
|
|
177
|
+
const lintPrompt = buildLintErrorPrompt(file, lintResult.output);
|
|
178
|
+
messages = [
|
|
179
|
+
...messages,
|
|
180
|
+
{ role: 'assistant', content: llmResponse },
|
|
181
|
+
{ role: 'user', content: lintPrompt },
|
|
182
|
+
];
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Run tests if enabled
|
|
188
|
+
if (this.config.autoTest && this.testFn && succeeded.length > 0) {
|
|
189
|
+
testResult = await ui.spin('Running tests...', () => this.testFn());
|
|
190
|
+
if (!testResult.passed && attempt < this.config.maxReflections) {
|
|
191
|
+
ui.error('Tests failed');
|
|
192
|
+
const testPrompt = buildTestFailurePrompt(testResult.output);
|
|
193
|
+
messages = [
|
|
194
|
+
...messages,
|
|
195
|
+
{ role: 'assistant', content: llmResponse },
|
|
196
|
+
{ role: 'user', content: testPrompt },
|
|
197
|
+
];
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Auto-commit if enabled and all checks passed
|
|
202
|
+
if (this.config.autoCommit && succeeded.length > 0) {
|
|
203
|
+
const allLintsPassed = lintResults.every(r => r.passed);
|
|
204
|
+
const testsPassed = !testResult || testResult.passed;
|
|
205
|
+
if (allLintsPassed && testsPassed) {
|
|
206
|
+
const diff = await this.git.diff();
|
|
207
|
+
if (diff) {
|
|
208
|
+
const commitMessage = await ui.spin('Generating commit message...', () => generateCommitMessage(diff, async (prompt) => {
|
|
209
|
+
const result = await this.generateFn([
|
|
210
|
+
{ role: 'user', content: prompt },
|
|
211
|
+
]);
|
|
212
|
+
return result;
|
|
213
|
+
}));
|
|
214
|
+
commitResult = await this.git.commit({
|
|
215
|
+
message: commitMessage,
|
|
216
|
+
files: succeeded.map(r => r.file),
|
|
217
|
+
}) || undefined;
|
|
218
|
+
if (commitResult) {
|
|
219
|
+
ui.success(`Committed: ${commitResult.hash} ${commitResult.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Execute tool action if not edit blocks
|
|
226
|
+
if (response.action && 'tool' in response.action && response.action.tool !== 'none' && !response.editBlocks?.length) {
|
|
227
|
+
const { tool, args } = response.action;
|
|
228
|
+
ui.showToolCall(tool, args || {});
|
|
229
|
+
try {
|
|
230
|
+
const result = await this.executeTool(tool, args || {});
|
|
231
|
+
const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
232
|
+
ui.showToolResult(resultStr);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
ui.error(`Tool error: ${error instanceof Error ? error.message : error}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { response, editResults, lintResults, testResult, commitResult };
|
|
239
|
+
}
|
|
240
|
+
// Max reflections reached
|
|
241
|
+
ui.warning(`Max reflections (${this.config.maxReflections}) reached`);
|
|
242
|
+
return {
|
|
243
|
+
response: { action: { tool: 'none', message: 'Max reflections reached' } },
|
|
244
|
+
editResults,
|
|
245
|
+
lintResults,
|
|
246
|
+
testResult,
|
|
247
|
+
commitResult,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Summarize conversation history to reduce tokens
|
|
253
|
+
*/
|
|
254
|
+
export async function summarizeHistory(history, generateFn, maxMessages = 10) {
|
|
255
|
+
if (history.length <= maxMessages) {
|
|
256
|
+
return history;
|
|
257
|
+
}
|
|
258
|
+
// Keep first message (usually important context) and last few messages
|
|
259
|
+
const keepFirst = 1;
|
|
260
|
+
const keepLast = maxMessages - 2;
|
|
261
|
+
const toSummarize = history.slice(keepFirst, -keepLast);
|
|
262
|
+
if (toSummarize.length === 0) {
|
|
263
|
+
return history;
|
|
264
|
+
}
|
|
265
|
+
const summaryPrompt = `Summarize this conversation history concisely, preserving key information:
|
|
266
|
+
|
|
267
|
+
${toSummarize.map(m => `${m.role}: ${m.content}`).join('\n\n')}
|
|
268
|
+
|
|
269
|
+
Provide a brief summary that captures:
|
|
270
|
+
1. Main topics discussed
|
|
271
|
+
2. Key decisions made
|
|
272
|
+
3. Important context for future messages`;
|
|
273
|
+
const summary = await generateFn([
|
|
274
|
+
{ role: 'user', content: summaryPrompt },
|
|
275
|
+
]);
|
|
276
|
+
return [
|
|
277
|
+
history[0], // Keep first
|
|
278
|
+
{ role: 'system', content: `[Conversation Summary]\n${summary}` },
|
|
279
|
+
...history.slice(-keepLast), // Keep last few
|
|
280
|
+
];
|
|
281
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent Code Editor using ts-morph and fuzzy matching
|
|
3
|
+
* Provides Aider-style SEARCH/REPLACE with smart matching
|
|
4
|
+
*/
|
|
5
|
+
import { SourceFile, Node } from 'ts-morph';
|
|
6
|
+
export interface EditBlock {
|
|
7
|
+
file: string;
|
|
8
|
+
search: string;
|
|
9
|
+
replace: string;
|
|
10
|
+
}
|
|
11
|
+
export interface EditResult {
|
|
12
|
+
file: string;
|
|
13
|
+
success: boolean;
|
|
14
|
+
applied: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
diff?: string;
|
|
17
|
+
suggestion?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Apply a single edit to content
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyEdit(content: string, search: string, replace: string): {
|
|
23
|
+
success: boolean;
|
|
24
|
+
content: string;
|
|
25
|
+
method: string;
|
|
26
|
+
suggestion?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Apply edits to a file
|
|
30
|
+
*/
|
|
31
|
+
export declare function applyFileEdits(edits: EditBlock[]): Promise<EditResult[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Parse SEARCH/REPLACE blocks from LLM response
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseEditBlocks(response: string, validFiles?: string[]): EditBlock[];
|
|
36
|
+
/**
|
|
37
|
+
* TypeScript/JavaScript AST-aware editing using ts-morph
|
|
38
|
+
*/
|
|
39
|
+
export declare class ASTEditor {
|
|
40
|
+
private project;
|
|
41
|
+
constructor();
|
|
42
|
+
/**
|
|
43
|
+
* Add a file to the project
|
|
44
|
+
*/
|
|
45
|
+
addFile(path: string, content: string): SourceFile;
|
|
46
|
+
/**
|
|
47
|
+
* Get or create a source file
|
|
48
|
+
*/
|
|
49
|
+
getSourceFile(path: string, content?: string): SourceFile | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Find a function by name
|
|
52
|
+
*/
|
|
53
|
+
findFunction(sourceFile: SourceFile, name: string): Node | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Find a class by name
|
|
56
|
+
*/
|
|
57
|
+
findClass(sourceFile: SourceFile, name: string): Node | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Rename a symbol across the project
|
|
60
|
+
*/
|
|
61
|
+
renameSymbol(sourceFile: SourceFile, oldName: string, newName: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Get the modified content of a file
|
|
64
|
+
*/
|
|
65
|
+
getContent(path: string): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Save all changes
|
|
68
|
+
*/
|
|
69
|
+
saveAll(): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a code editor instance
|
|
73
|
+
*/
|
|
74
|
+
export declare function createEditor(): ASTEditor;
|