@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manager - Manages conversation context and file state
|
|
3
|
+
* Based on Aider's coder.py and GeminiCLI's context management
|
|
4
|
+
* Uses gpt-tokenizer for accurate token counting (with fallback)
|
|
5
|
+
*/
|
|
6
|
+
import { type Skill } from './skills.js';
|
|
7
|
+
import { type Tool } from './registry.js';
|
|
8
|
+
export interface Message {
|
|
9
|
+
role: 'user' | 'assistant' | 'system';
|
|
10
|
+
content: string;
|
|
11
|
+
timestamp?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ContextState {
|
|
14
|
+
cwd: string;
|
|
15
|
+
activeFiles: Set<string>;
|
|
16
|
+
readOnlyFiles: Set<string>;
|
|
17
|
+
history: Message[];
|
|
18
|
+
skill: Skill;
|
|
19
|
+
tokenEstimate: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Synchronous token count (uses cached encoder or estimation)
|
|
23
|
+
*/
|
|
24
|
+
declare function countTokens(text: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* ContextManager class
|
|
27
|
+
* Manages the conversation context, file state, and system prompts
|
|
28
|
+
*/
|
|
29
|
+
export declare class ContextManager {
|
|
30
|
+
private cwd;
|
|
31
|
+
private activeFiles;
|
|
32
|
+
private readOnlyFiles;
|
|
33
|
+
private history;
|
|
34
|
+
private skill;
|
|
35
|
+
private tools;
|
|
36
|
+
private repoMapCache;
|
|
37
|
+
private repoMapTimestamp;
|
|
38
|
+
constructor(cwd?: string);
|
|
39
|
+
/**
|
|
40
|
+
* Initialize the context manager
|
|
41
|
+
*/
|
|
42
|
+
initialize(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Add a file to active context
|
|
45
|
+
*/
|
|
46
|
+
addFile(path: string, readOnly?: boolean): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Remove a file from context
|
|
49
|
+
*/
|
|
50
|
+
removeFile(path: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Add a message to history
|
|
53
|
+
*/
|
|
54
|
+
addMessage(role: Message['role'], content: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Clear conversation history
|
|
57
|
+
*/
|
|
58
|
+
clearHistory(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Get all files in context
|
|
61
|
+
*/
|
|
62
|
+
getFiles(): {
|
|
63
|
+
active: string[];
|
|
64
|
+
readOnly: string[];
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Read all files in context
|
|
68
|
+
*/
|
|
69
|
+
getFileContents(): Promise<Map<string, string>>;
|
|
70
|
+
/**
|
|
71
|
+
* Set the active skill
|
|
72
|
+
*/
|
|
73
|
+
setSkill(skill: Skill): void;
|
|
74
|
+
/**
|
|
75
|
+
* Get the current skill
|
|
76
|
+
*/
|
|
77
|
+
getSkill(): Skill;
|
|
78
|
+
/**
|
|
79
|
+
* Refresh the repository map
|
|
80
|
+
*/
|
|
81
|
+
refreshRepoMap(): Promise<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Build the system prompt
|
|
84
|
+
*/
|
|
85
|
+
buildSystemPrompt(): Promise<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Build messages for LLM
|
|
88
|
+
*/
|
|
89
|
+
buildMessages(userMessage: string): Promise<Message[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Estimate token count using accurate tokenizer
|
|
92
|
+
*/
|
|
93
|
+
estimateTokenCount(): Promise<number>;
|
|
94
|
+
/**
|
|
95
|
+
* Get current state
|
|
96
|
+
*/
|
|
97
|
+
getState(): ContextState;
|
|
98
|
+
/**
|
|
99
|
+
* Restore state
|
|
100
|
+
*/
|
|
101
|
+
restoreState(state: Partial<ContextState>): void;
|
|
102
|
+
/**
|
|
103
|
+
* Get conversation history
|
|
104
|
+
*/
|
|
105
|
+
getHistory(): Message[];
|
|
106
|
+
/**
|
|
107
|
+
* Get tools
|
|
108
|
+
*/
|
|
109
|
+
getTools(): Map<string, Tool>;
|
|
110
|
+
/**
|
|
111
|
+
* Get working directory
|
|
112
|
+
*/
|
|
113
|
+
getCwd(): string;
|
|
114
|
+
}
|
|
115
|
+
export declare function getContextManager(cwd?: string): ContextManager;
|
|
116
|
+
export { countTokens };
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Manager - Manages conversation context and file state
|
|
3
|
+
* Based on Aider's coder.py and GeminiCLI's context management
|
|
4
|
+
* Uses gpt-tokenizer for accurate token counting (with fallback)
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'fs/promises';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { relative, resolve } from 'path';
|
|
9
|
+
import { generateRepoMap } from './repoMap.js';
|
|
10
|
+
import { getActiveSkill } from './skills.js';
|
|
11
|
+
import { loadAllTools, getToolDefinitions } from './registry.js';
|
|
12
|
+
import { getPromptProvider } from './prompts/provider.js';
|
|
13
|
+
// Cached tokenizer (typed as any to handle missing module)
|
|
14
|
+
let tokenEncoder = null;
|
|
15
|
+
let tokenizerLoaded = false;
|
|
16
|
+
/**
|
|
17
|
+
* Load tokenizer lazily (handles case where gpt-tokenizer not installed)
|
|
18
|
+
*/
|
|
19
|
+
async function loadTokenizer() {
|
|
20
|
+
if (tokenizerLoaded)
|
|
21
|
+
return;
|
|
22
|
+
tokenizerLoaded = true;
|
|
23
|
+
try {
|
|
24
|
+
const mod = await import('gpt-tokenizer');
|
|
25
|
+
tokenEncoder = mod.encode;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// gpt-tokenizer not installed, will use fallback
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Count tokens using gpt-tokenizer (accurate for GPT models)
|
|
33
|
+
* Falls back to rough estimation if not available
|
|
34
|
+
*/
|
|
35
|
+
async function countTokensAsync(text) {
|
|
36
|
+
await loadTokenizer();
|
|
37
|
+
if (tokenEncoder) {
|
|
38
|
+
try {
|
|
39
|
+
return tokenEncoder(text).length;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Fall through to estimation
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Fallback: ~4 chars per token (rough but reasonable)
|
|
46
|
+
return Math.ceil(text.length / 4);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Synchronous token count (uses cached encoder or estimation)
|
|
50
|
+
*/
|
|
51
|
+
function countTokens(text) {
|
|
52
|
+
if (tokenEncoder) {
|
|
53
|
+
try {
|
|
54
|
+
return tokenEncoder(text).length;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Fall through
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Math.ceil(text.length / 4);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* ContextManager class
|
|
64
|
+
* Manages the conversation context, file state, and system prompts
|
|
65
|
+
*/
|
|
66
|
+
export class ContextManager {
|
|
67
|
+
cwd;
|
|
68
|
+
activeFiles = new Set();
|
|
69
|
+
readOnlyFiles = new Set();
|
|
70
|
+
history = [];
|
|
71
|
+
skill;
|
|
72
|
+
tools = new Map();
|
|
73
|
+
repoMapCache = '';
|
|
74
|
+
repoMapTimestamp = 0;
|
|
75
|
+
constructor(cwd) {
|
|
76
|
+
this.cwd = cwd || process.cwd();
|
|
77
|
+
this.skill = getActiveSkill();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Initialize the context manager
|
|
81
|
+
*/
|
|
82
|
+
async initialize() {
|
|
83
|
+
// Load tools
|
|
84
|
+
this.tools = await loadAllTools();
|
|
85
|
+
// Generate initial repo map
|
|
86
|
+
await this.refreshRepoMap();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Add a file to active context
|
|
90
|
+
*/
|
|
91
|
+
addFile(path, readOnly = false) {
|
|
92
|
+
const fullPath = resolve(this.cwd, path);
|
|
93
|
+
if (!existsSync(fullPath)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (readOnly) {
|
|
97
|
+
this.readOnlyFiles.add(fullPath);
|
|
98
|
+
this.activeFiles.delete(fullPath);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.activeFiles.add(fullPath);
|
|
102
|
+
this.readOnlyFiles.delete(fullPath);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Remove a file from context
|
|
108
|
+
*/
|
|
109
|
+
removeFile(path) {
|
|
110
|
+
const fullPath = resolve(this.cwd, path);
|
|
111
|
+
const wasActive = this.activeFiles.delete(fullPath);
|
|
112
|
+
const wasReadOnly = this.readOnlyFiles.delete(fullPath);
|
|
113
|
+
return wasActive || wasReadOnly;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Add a message to history
|
|
117
|
+
*/
|
|
118
|
+
addMessage(role, content) {
|
|
119
|
+
this.history.push({
|
|
120
|
+
role,
|
|
121
|
+
content,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Clear conversation history
|
|
127
|
+
*/
|
|
128
|
+
clearHistory() {
|
|
129
|
+
this.history = [];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get all files in context
|
|
133
|
+
*/
|
|
134
|
+
getFiles() {
|
|
135
|
+
return {
|
|
136
|
+
active: Array.from(this.activeFiles).map(f => relative(this.cwd, f)),
|
|
137
|
+
readOnly: Array.from(this.readOnlyFiles).map(f => relative(this.cwd, f)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read all files in context
|
|
142
|
+
*/
|
|
143
|
+
async getFileContents() {
|
|
144
|
+
const contents = new Map();
|
|
145
|
+
for (const file of [...this.activeFiles, ...this.readOnlyFiles]) {
|
|
146
|
+
try {
|
|
147
|
+
const content = await readFile(file, 'utf-8');
|
|
148
|
+
const relPath = relative(this.cwd, file);
|
|
149
|
+
contents.set(relPath, content);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Skip files that can't be read
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return contents;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Set the active skill
|
|
159
|
+
*/
|
|
160
|
+
setSkill(skill) {
|
|
161
|
+
this.skill = skill;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the current skill
|
|
165
|
+
*/
|
|
166
|
+
getSkill() {
|
|
167
|
+
return this.skill;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Refresh the repository map
|
|
171
|
+
*/
|
|
172
|
+
async refreshRepoMap() {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
// Cache for 30 seconds
|
|
175
|
+
if (this.repoMapCache && now - this.repoMapTimestamp < 30000) {
|
|
176
|
+
return this.repoMapCache;
|
|
177
|
+
}
|
|
178
|
+
this.repoMapCache = await generateRepoMap(this.cwd);
|
|
179
|
+
this.repoMapTimestamp = now;
|
|
180
|
+
return this.repoMapCache;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Build the system prompt
|
|
184
|
+
*/
|
|
185
|
+
async buildSystemPrompt() {
|
|
186
|
+
const provider = getPromptProvider();
|
|
187
|
+
const systemPrompt = await provider.getSystemPrompt({
|
|
188
|
+
cwd: this.cwd,
|
|
189
|
+
skillPrompt: this.skill.systemPrompt
|
|
190
|
+
});
|
|
191
|
+
const parts = [systemPrompt];
|
|
192
|
+
// Tool definitions
|
|
193
|
+
const toolDefs = getToolDefinitions(this.tools);
|
|
194
|
+
parts.push('\n## Available Tools\n' + toolDefs);
|
|
195
|
+
// Active files
|
|
196
|
+
const files = this.getFiles();
|
|
197
|
+
if (files.active.length > 0 || files.readOnly.length > 0) {
|
|
198
|
+
parts.push('\n## Context Files');
|
|
199
|
+
if (files.active.length > 0) {
|
|
200
|
+
parts.push('\nEditable files:');
|
|
201
|
+
parts.push(files.active.map(f => `- ${f}`).join('\n'));
|
|
202
|
+
}
|
|
203
|
+
if (files.readOnly.length > 0) {
|
|
204
|
+
parts.push('\nRead-only files:');
|
|
205
|
+
parts.push(files.readOnly.map(f => `- ${f}`).join('\n'));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Repository map (condensed)
|
|
209
|
+
const repoMap = await this.refreshRepoMap();
|
|
210
|
+
if (repoMap) {
|
|
211
|
+
const condensed = repoMap.split('\n').slice(0, 50).join('\n');
|
|
212
|
+
parts.push('\n## Repository Structure\n' + condensed);
|
|
213
|
+
if (repoMap.split('\n').length > 50) {
|
|
214
|
+
parts.push('... (truncated)');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return parts.join('\n');
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build messages for LLM
|
|
221
|
+
*/
|
|
222
|
+
async buildMessages(userMessage) {
|
|
223
|
+
const messages = [];
|
|
224
|
+
// System prompt
|
|
225
|
+
const systemPrompt = await this.buildSystemPrompt();
|
|
226
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
227
|
+
// File contents as context
|
|
228
|
+
const fileContents = await this.getFileContents();
|
|
229
|
+
if (fileContents.size > 0) {
|
|
230
|
+
let fileContext = '## File Contents\n\n';
|
|
231
|
+
for (const [path, content] of fileContents) {
|
|
232
|
+
const truncated = content.length > 10000
|
|
233
|
+
? content.slice(0, 10000) + '\n... (truncated)'
|
|
234
|
+
: content;
|
|
235
|
+
fileContext += `### ${path}\n\`\`\`\n${truncated}\n\`\`\`\n\n`;
|
|
236
|
+
}
|
|
237
|
+
messages.push({ role: 'user', content: fileContext });
|
|
238
|
+
messages.push({ role: 'assistant', content: 'I have read the files. How can I help?' });
|
|
239
|
+
}
|
|
240
|
+
// Conversation history
|
|
241
|
+
for (const msg of this.history) {
|
|
242
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
243
|
+
}
|
|
244
|
+
// Current message
|
|
245
|
+
messages.push({ role: 'user', content: userMessage });
|
|
246
|
+
return messages;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Estimate token count using accurate tokenizer
|
|
250
|
+
*/
|
|
251
|
+
async estimateTokenCount() {
|
|
252
|
+
// Ensure tokenizer is loaded
|
|
253
|
+
await loadTokenizer();
|
|
254
|
+
let total = 0;
|
|
255
|
+
// System prompt
|
|
256
|
+
const systemPrompt = await this.buildSystemPrompt();
|
|
257
|
+
total += countTokens(systemPrompt);
|
|
258
|
+
// File contents
|
|
259
|
+
const fileContents = await this.getFileContents();
|
|
260
|
+
for (const content of fileContents.values()) {
|
|
261
|
+
total += countTokens(content);
|
|
262
|
+
}
|
|
263
|
+
// History
|
|
264
|
+
for (const msg of this.history) {
|
|
265
|
+
total += countTokens(msg.content);
|
|
266
|
+
}
|
|
267
|
+
// Add overhead for message formatting (~4 tokens per message)
|
|
268
|
+
total += (this.history.length + 2) * 4;
|
|
269
|
+
return total;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get current state
|
|
273
|
+
*/
|
|
274
|
+
getState() {
|
|
275
|
+
return {
|
|
276
|
+
cwd: this.cwd,
|
|
277
|
+
activeFiles: this.activeFiles,
|
|
278
|
+
readOnlyFiles: this.readOnlyFiles,
|
|
279
|
+
history: this.history,
|
|
280
|
+
skill: this.skill,
|
|
281
|
+
tokenEstimate: 0, // Calculated async
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Restore state
|
|
286
|
+
*/
|
|
287
|
+
restoreState(state) {
|
|
288
|
+
if (state.cwd)
|
|
289
|
+
this.cwd = state.cwd;
|
|
290
|
+
if (state.activeFiles)
|
|
291
|
+
this.activeFiles = state.activeFiles;
|
|
292
|
+
if (state.readOnlyFiles)
|
|
293
|
+
this.readOnlyFiles = state.readOnlyFiles;
|
|
294
|
+
if (state.history)
|
|
295
|
+
this.history = state.history;
|
|
296
|
+
if (state.skill)
|
|
297
|
+
this.skill = state.skill;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get conversation history
|
|
301
|
+
*/
|
|
302
|
+
getHistory() {
|
|
303
|
+
return [...this.history];
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get tools
|
|
307
|
+
*/
|
|
308
|
+
getTools() {
|
|
309
|
+
return this.tools;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get working directory
|
|
313
|
+
*/
|
|
314
|
+
getCwd() {
|
|
315
|
+
return this.cwd;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Create a global context manager instance
|
|
319
|
+
let globalContext = null;
|
|
320
|
+
export function getContextManager(cwd) {
|
|
321
|
+
if (!globalContext) {
|
|
322
|
+
globalContext = new ContextManager(cwd);
|
|
323
|
+
}
|
|
324
|
+
return globalContext;
|
|
325
|
+
}
|
|
326
|
+
// Export token counting utility for use elsewhere
|
|
327
|
+
export { countTokens };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Simple-CLI Orchestrator - Core loop: Observe -> Plan -> Verify -> Act -> Reflect
|
|
4
|
+
* Must remain under 150 lines. Supports MoE routing with --moe flag.
|
|
5
|
+
*/
|
|
6
|
+
import 'dotenv/config';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
import { generateRepoMap } from './repoMap.js';
|
|
9
|
+
import { loadTools } from './registry.js';
|
|
10
|
+
import { createProvider } from './providers/index.js';
|
|
11
|
+
import { createMultiProvider } from './providers/multi.js';
|
|
12
|
+
import { routeTask, loadTierConfig, formatRoutingDecision } from './router.js';
|
|
13
|
+
import { getPromptProvider } from './prompts/provider.js';
|
|
14
|
+
const YOLO_MODE = process.argv.includes('--yolo');
|
|
15
|
+
const MOE_MODE = process.argv.includes('--moe');
|
|
16
|
+
let tools;
|
|
17
|
+
let history = [];
|
|
18
|
+
const buildPrompt = async () => {
|
|
19
|
+
const repoMap = await generateRepoMap();
|
|
20
|
+
const toolDefs = Array.from(tools.values()).map(t => `- ${t.name}: ${t.description}`).join('\n');
|
|
21
|
+
const provider = getPromptProvider();
|
|
22
|
+
const systemPrompt = await provider.getSystemPrompt({ cwd: process.cwd() });
|
|
23
|
+
return `${systemPrompt}
|
|
24
|
+
## Context\n${repoMap}
|
|
25
|
+
## Tools\n${toolDefs}
|
|
26
|
+
## Format: <thought>reasoning</thought> then {"tool": "name", "args": {}} or {"tool": "none", "message": "..."}`;
|
|
27
|
+
};
|
|
28
|
+
import { jsonrepair } from 'jsonrepair';
|
|
29
|
+
const parseResponse = (r) => {
|
|
30
|
+
const thought = r.match(/<thought>([\s\S]*?)<\/thought>/)?.[1]?.trim() || '';
|
|
31
|
+
const jsonMatch = r.match(/\{[\s\S]*"tool"[\s\S]*\}/);
|
|
32
|
+
let action = { tool: 'none', message: 'No action', args: {} };
|
|
33
|
+
if (jsonMatch) {
|
|
34
|
+
try {
|
|
35
|
+
const repaired = jsonrepair(jsonMatch[0]);
|
|
36
|
+
action = JSON.parse(repaired);
|
|
37
|
+
}
|
|
38
|
+
catch { /* use default */ }
|
|
39
|
+
}
|
|
40
|
+
return { thought, action };
|
|
41
|
+
};
|
|
42
|
+
const confirm = async (tool, args) => {
|
|
43
|
+
if (YOLO_MODE)
|
|
44
|
+
return true;
|
|
45
|
+
const t = tools.get(tool);
|
|
46
|
+
if (!t || t.permission === 'read')
|
|
47
|
+
return !!t;
|
|
48
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
+
return new Promise(res => {
|
|
50
|
+
rl.question(`[CONFIRM] ${tool}(${JSON.stringify(args)})? (y/n) `, a => { rl.close(); res(a.toLowerCase() === 'y'); });
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const execTool = async (name, args) => {
|
|
54
|
+
const t = tools.get(name);
|
|
55
|
+
if (!t)
|
|
56
|
+
return `Error: Tool "${name}" not found`;
|
|
57
|
+
try {
|
|
58
|
+
const r = await t.execute(args);
|
|
59
|
+
return typeof r === 'string' ? r : JSON.stringify(r, null, 2);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return `Error: ${e instanceof Error ? e.message : e}`;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const main = async () => {
|
|
66
|
+
console.log(`Simple-CLI v0.1.0 ${MOE_MODE ? '[MoE]' : ''} ${YOLO_MODE ? '[YOLO]' : '[Safe]'}`);
|
|
67
|
+
tools = await loadTools();
|
|
68
|
+
const systemPrompt = await buildPrompt();
|
|
69
|
+
const tierConfigs = MOE_MODE ? loadTierConfig() : null;
|
|
70
|
+
const multiProvider = tierConfigs ? createMultiProvider(tierConfigs) : null;
|
|
71
|
+
const singleProvider = !MOE_MODE ? createProvider() : null;
|
|
72
|
+
const generate = async (input) => {
|
|
73
|
+
if (MOE_MODE && multiProvider && tierConfigs) {
|
|
74
|
+
const routing = await routeTask(input, (p) => multiProvider.generateWithTier(1, p, [{ role: 'user', content: input }]));
|
|
75
|
+
console.log(formatRoutingDecision(routing, tierConfigs));
|
|
76
|
+
return multiProvider.generateWithTier(routing.tier, systemPrompt, history);
|
|
77
|
+
}
|
|
78
|
+
return singleProvider.generateResponse(systemPrompt, history);
|
|
79
|
+
};
|
|
80
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
81
|
+
const prompt = () => {
|
|
82
|
+
rl.question('\n> ', async (input) => {
|
|
83
|
+
if (input.toLowerCase() === 'exit') {
|
|
84
|
+
rl.close();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
history.push({ role: 'user', content: input });
|
|
88
|
+
const response = await generate(input);
|
|
89
|
+
const { thought, action } = parseResponse(response);
|
|
90
|
+
console.log(`\n[Thought] ${thought}`);
|
|
91
|
+
if (action.tool !== 'none') {
|
|
92
|
+
if (await confirm(action.tool, action.args || {})) {
|
|
93
|
+
const result = await execTool(action.tool, action.args || {});
|
|
94
|
+
console.log(`[Result] ${result}`);
|
|
95
|
+
history.push({ role: 'assistant', content: response }, { role: 'user', content: `Tool result: ${result}` });
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
console.log('[Skipped]');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(`[Response] ${action.message}`);
|
|
102
|
+
history.push({ role: 'assistant', content: response });
|
|
103
|
+
}
|
|
104
|
+
prompt();
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
prompt();
|
|
108
|
+
};
|
|
109
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Core - Main agent loop with reflection and retry
|
|
3
|
+
* Implements Aider-style reasoning and error recovery
|
|
4
|
+
*/
|
|
5
|
+
import { Message } from '../context.js';
|
|
6
|
+
import { EditBlock, EditResult } from './editor.js';
|
|
7
|
+
import { GitManager } from './git.js';
|
|
8
|
+
export interface AgentConfig {
|
|
9
|
+
maxReflections: number;
|
|
10
|
+
autoLint: boolean;
|
|
11
|
+
autoTest: boolean;
|
|
12
|
+
autoCommit: boolean;
|
|
13
|
+
testCommand?: string;
|
|
14
|
+
lintCommand?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ToolCall {
|
|
17
|
+
tool: string;
|
|
18
|
+
args: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface AgentResponse {
|
|
21
|
+
thought?: string;
|
|
22
|
+
action: ToolCall | {
|
|
23
|
+
tool: 'none';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
editBlocks?: EditBlock[];
|
|
27
|
+
}
|
|
28
|
+
export interface ReflectionContext {
|
|
29
|
+
attempt: number;
|
|
30
|
+
previousError: string;
|
|
31
|
+
previousResponse: string;
|
|
32
|
+
failedEdits: EditResult[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse LLM response into structured format
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseResponse(response: string): AgentResponse;
|
|
38
|
+
/**
|
|
39
|
+
* Build reflection prompt for retry
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildReflectionPrompt(context: ReflectionContext): string;
|
|
42
|
+
/**
|
|
43
|
+
* Build lint error prompt
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildLintErrorPrompt(file: string, errors: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Build test failure prompt
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildTestFailurePrompt(output: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Agent class - Main orchestration logic
|
|
52
|
+
*/
|
|
53
|
+
export declare class Agent {
|
|
54
|
+
private config;
|
|
55
|
+
private git;
|
|
56
|
+
private generateFn;
|
|
57
|
+
private executeTool;
|
|
58
|
+
private lintFn?;
|
|
59
|
+
private testFn?;
|
|
60
|
+
constructor(options: {
|
|
61
|
+
config: AgentConfig;
|
|
62
|
+
git: GitManager;
|
|
63
|
+
generateFn: (messages: Message[]) => Promise<string>;
|
|
64
|
+
executeTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
65
|
+
lintFn?: (file: string) => Promise<{
|
|
66
|
+
passed: boolean;
|
|
67
|
+
output: string;
|
|
68
|
+
}>;
|
|
69
|
+
testFn?: () => Promise<{
|
|
70
|
+
passed: boolean;
|
|
71
|
+
output: string;
|
|
72
|
+
}>;
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Process a user message with reflection loop
|
|
76
|
+
*/
|
|
77
|
+
process(userMessage: string, history: Message[], systemPrompt: string): Promise<{
|
|
78
|
+
response: AgentResponse;
|
|
79
|
+
editResults: EditResult[];
|
|
80
|
+
lintResults: Array<{
|
|
81
|
+
file: string;
|
|
82
|
+
passed: boolean;
|
|
83
|
+
output: string;
|
|
84
|
+
}>;
|
|
85
|
+
testResult?: {
|
|
86
|
+
passed: boolean;
|
|
87
|
+
output: string;
|
|
88
|
+
};
|
|
89
|
+
commitResult?: {
|
|
90
|
+
hash: string;
|
|
91
|
+
message: string;
|
|
92
|
+
};
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Summarize conversation history to reduce tokens
|
|
97
|
+
*/
|
|
98
|
+
export declare function summarizeHistory(history: Message[], generateFn: (messages: Message[]) => Promise<string>, maxMessages?: number): Promise<Message[]>;
|