@stan-chen/simple-cli 0.2.2 → 0.2.4
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 +58 -271
- package/dist/anyllm.py +62 -0
- package/dist/builtins.d.ts +726 -0
- package/dist/builtins.js +481 -0
- package/dist/cli.d.ts +0 -4
- package/dist/cli.js +37 -279
- package/dist/engine.d.ts +33 -0
- package/dist/engine.js +138 -0
- package/dist/learnings.d.ts +15 -0
- package/dist/learnings.js +54 -0
- package/dist/llm.d.ts +18 -0
- package/dist/llm.js +66 -0
- package/dist/mcp.d.ts +132 -0
- package/dist/mcp.js +43 -0
- package/dist/skills.d.ts +5 -16
- package/dist/skills.js +91 -253
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +10 -0
- package/package.json +88 -78
- package/dist/commands/add.d.ts +0 -9
- package/dist/commands/add.js +0 -50
- package/dist/commands/git/commit.d.ts +0 -12
- package/dist/commands/git/commit.js +0 -97
- package/dist/commands/git/status.d.ts +0 -6
- package/dist/commands/git/status.js +0 -42
- package/dist/commands/index.d.ts +0 -16
- package/dist/commands/index.js +0 -376
- package/dist/commands/mcp/status.d.ts +0 -6
- package/dist/commands/mcp/status.js +0 -31
- package/dist/commands/swarm.d.ts +0 -36
- package/dist/commands/swarm.js +0 -236
- package/dist/commands.d.ts +0 -32
- package/dist/commands.js +0 -427
- package/dist/context.d.ts +0 -116
- package/dist/context.js +0 -327
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -109
- package/dist/lib/agent.d.ts +0 -98
- package/dist/lib/agent.js +0 -281
- package/dist/lib/editor.d.ts +0 -74
- package/dist/lib/editor.js +0 -441
- package/dist/lib/git.d.ts +0 -164
- package/dist/lib/git.js +0 -351
- package/dist/lib/ui.d.ts +0 -159
- package/dist/lib/ui.js +0 -252
- package/dist/mcp/client.d.ts +0 -22
- package/dist/mcp/client.js +0 -81
- package/dist/mcp/manager.d.ts +0 -186
- package/dist/mcp/manager.js +0 -446
- package/dist/prompts/provider.d.ts +0 -22
- package/dist/prompts/provider.js +0 -78
- package/dist/providers/index.d.ts +0 -15
- package/dist/providers/index.js +0 -82
- package/dist/providers/multi.d.ts +0 -11
- package/dist/providers/multi.js +0 -28
- package/dist/registry.d.ts +0 -24
- package/dist/registry.js +0 -379
- package/dist/repoMap.d.ts +0 -5
- package/dist/repoMap.js +0 -79
- package/dist/router.d.ts +0 -41
- package/dist/router.js +0 -108
- package/dist/swarm/coordinator.d.ts +0 -86
- package/dist/swarm/coordinator.js +0 -257
- package/dist/swarm/index.d.ts +0 -28
- package/dist/swarm/index.js +0 -29
- package/dist/swarm/task.d.ts +0 -104
- package/dist/swarm/task.js +0 -221
- package/dist/swarm/types.d.ts +0 -132
- package/dist/swarm/types.js +0 -37
- package/dist/swarm/worker.d.ts +0 -107
- package/dist/swarm/worker.js +0 -299
- package/dist/tools/analyzeFile.d.ts +0 -16
- package/dist/tools/analyzeFile.js +0 -43
- package/dist/tools/git.d.ts +0 -40
- package/dist/tools/git.js +0 -236
- package/dist/tools/glob.d.ts +0 -34
- package/dist/tools/glob.js +0 -165
- package/dist/tools/grep.d.ts +0 -53
- package/dist/tools/grep.js +0 -296
- package/dist/tools/linter.d.ts +0 -35
- package/dist/tools/linter.js +0 -349
- package/dist/tools/listDir.d.ts +0 -29
- package/dist/tools/listDir.js +0 -50
- package/dist/tools/memory.d.ts +0 -34
- package/dist/tools/memory.js +0 -215
- package/dist/tools/readFiles.d.ts +0 -25
- package/dist/tools/readFiles.js +0 -31
- package/dist/tools/reloadTools.d.ts +0 -11
- package/dist/tools/reloadTools.js +0 -22
- package/dist/tools/runCommand.d.ts +0 -32
- package/dist/tools/runCommand.js +0 -79
- package/dist/tools/scraper.d.ts +0 -31
- package/dist/tools/scraper.js +0 -211
- package/dist/tools/writeFiles.d.ts +0 -63
- package/dist/tools/writeFiles.js +0 -87
- package/dist/ui/server.d.ts +0 -5
- package/dist/ui/server.js +0 -74
- package/dist/watcher.d.ts +0 -35
- package/dist/watcher.js +0 -164
- /package/{docs/assets → assets}/logo.jpeg +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,287 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Simple-CLI - Premium TUI agentic coding assistant
|
|
4
|
-
* Powered by @clack/prompts.
|
|
5
|
-
*/
|
|
6
2
|
import 'dotenv/config';
|
|
7
|
-
import { text, confirm as clackConfirm, isCancel, select } from '@clack/prompts';
|
|
8
|
-
import pc from 'picocolors';
|
|
9
|
-
import { getContextManager } from './context.js';
|
|
10
|
-
import { createProvider } from './providers/index.js';
|
|
11
|
-
import { createMultiProvider } from './providers/multi.js';
|
|
12
|
-
import { routeTask, loadTierConfig } from './router.js';
|
|
13
|
-
import { executeCommand } from './commands.js';
|
|
14
|
-
import { getMCPManager } from './mcp/manager.js';
|
|
15
|
-
import { listSkills, setActiveSkill, getActiveSkill } from './skills.js';
|
|
16
3
|
import { statSync } from 'fs';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const SWARM_MODE = process.argv.includes('--swarm');
|
|
24
|
-
const CLAW_MODE = process.argv.includes('--claw') || process.argv.includes('-claw');
|
|
25
|
-
const DEBUG = process.argv.includes('--debug') || process.env.DEBUG === 'true';
|
|
26
|
-
const VERSION = '0.2.2';
|
|
27
|
-
// Handle --version and --help immediately
|
|
28
|
-
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
29
|
-
console.log(`Simple-CLI v${VERSION}`);
|
|
30
|
-
process.exit(0);
|
|
31
|
-
}
|
|
32
|
-
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
33
|
-
console.log(`
|
|
34
|
-
${pc.bgCyan(pc.black(' SIMPLE-CLI '))} ${pc.dim(`v${VERSION}`)}
|
|
35
|
-
|
|
36
|
-
${pc.bold('Usage:')}
|
|
37
|
-
simple [target_dir] [prompt] [options]
|
|
38
|
-
|
|
39
|
-
${pc.bold('Options:')}
|
|
40
|
-
--version, -v Show version
|
|
41
|
-
--help, -h Show help
|
|
42
|
-
--yolo Skip all confirmation prompts
|
|
43
|
-
--moe Enable Mixture of Experts (multi-model)
|
|
44
|
-
--swarm Enable Swarm orchestration mode
|
|
45
|
-
--claw "intent" Enable OpenClaw JIT agent generation
|
|
46
|
-
--debug Enable debug logging
|
|
47
|
-
|
|
48
|
-
${pc.bold('Examples:')}
|
|
49
|
-
simple . "Build a login page"
|
|
50
|
-
simple --claw "Security audit this project"
|
|
51
|
-
simple --moe "Refactor this entire folder"
|
|
52
|
-
`);
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
55
|
-
// Handle --claw mode (JIT Agent Generation)
|
|
56
|
-
if (CLAW_MODE) {
|
|
57
|
-
const { execSync } = await import('child_process');
|
|
58
|
-
const args = process.argv.slice(2).filter(a => !a.startsWith('-'));
|
|
59
|
-
const intent = args.join(' ') || 'unspecified task';
|
|
60
|
-
console.log(pc.cyan('🧬 Initiating JIT Agent Generation...'));
|
|
61
|
-
console.log(pc.dim(`Intent: "${intent}"`));
|
|
62
|
-
try {
|
|
63
|
-
const output = execSync(`npx tsx tools/claw.ts run clawJit intent="${intent}"`, {
|
|
64
|
-
cwd: process.cwd(),
|
|
65
|
-
encoding: 'utf-8',
|
|
66
|
-
stdio: 'inherit'
|
|
67
|
-
});
|
|
68
|
-
console.log(pc.green('\n✅ JIT Agent ready. Run `simple` to begin.'));
|
|
69
|
-
process.exit(0);
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
console.error(pc.red('❌ Failed to initialize Claw mode:'), error);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Handle --swarm mode
|
|
77
|
-
if (SWARM_MODE) {
|
|
78
|
-
if (process.argv.includes('--help')) {
|
|
79
|
-
printSwarmHelp();
|
|
80
|
-
process.exit(0);
|
|
81
|
-
}
|
|
82
|
-
const swarmOptions = parseSwarmArgs(process.argv.slice(2));
|
|
83
|
-
swarmOptions.yolo = swarmOptions.yolo || YOLO_MODE;
|
|
84
|
-
runSwarm(swarmOptions).catch(err => {
|
|
85
|
-
console.error('Swarm error:', err);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
main().catch(console.error);
|
|
91
|
-
}
|
|
92
|
-
function parseResponse(response) {
|
|
93
|
-
const thought = response.match(/<thought>([\s\S]*?)<\/thought>/)?.[1]?.trim() || '';
|
|
94
|
-
const jsonMatch = response.match(/\{[\s\S]*"tool"[\s\S]*\}/);
|
|
95
|
-
let action = { tool: 'none', message: '', args: {} };
|
|
96
|
-
if (jsonMatch) {
|
|
97
|
-
try {
|
|
98
|
-
action = JSON.parse(jsonrepair(jsonMatch[0]));
|
|
99
|
-
}
|
|
100
|
-
catch { /* skip */ }
|
|
101
|
-
}
|
|
102
|
-
return { thought, action };
|
|
103
|
-
}
|
|
104
|
-
async function confirm(tool, args, ctx) {
|
|
105
|
-
if (YOLO_MODE)
|
|
106
|
-
return true;
|
|
107
|
-
const t = ctx.getTools().get(tool);
|
|
108
|
-
if (!t || t.permission === 'read')
|
|
109
|
-
return true;
|
|
110
|
-
const confirmed = await clackConfirm({
|
|
111
|
-
message: `Allow ${pc.cyan(tool)} with args ${pc.dim(JSON.stringify(args))}?`,
|
|
112
|
-
initialValue: true,
|
|
113
|
-
});
|
|
114
|
-
return !isCancel(confirmed) && confirmed;
|
|
115
|
-
}
|
|
116
|
-
async function executeTool(name, args, ctx) {
|
|
117
|
-
const tool = ctx.getTools().get(name);
|
|
118
|
-
if (!tool)
|
|
119
|
-
return `Error: Tool "${name}" not found`;
|
|
120
|
-
try {
|
|
121
|
-
const result = await tool.execute(args);
|
|
122
|
-
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
return `Error: ${error instanceof Error ? error.message : error}`;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
4
|
+
import { Engine, Context, Registry } from './engine.js';
|
|
5
|
+
import { allBuiltins } from './builtins.js';
|
|
6
|
+
import { createLLM } from './llm.js';
|
|
7
|
+
import { MCP } from './mcp.js';
|
|
8
|
+
import { getActiveSkill } from './skills.js';
|
|
9
|
+
import { showBanner } from './tui.js';
|
|
128
10
|
async function main() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Reload .env from the new directory
|
|
138
|
-
const { config } = await import('dotenv');
|
|
139
|
-
config();
|
|
140
|
-
args.shift();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
catch { /* ignored */ }
|
|
144
|
-
}
|
|
145
|
-
console.log(`\n ${pc.bgCyan(pc.black(' SIMPLE-CLI '))} ${pc.dim(`v${VERSION}`)} ${pc.green('●')} ${pc.cyan(targetDir)}\n`);
|
|
146
|
-
console.log(`${pc.dim('○')} Initializing...`);
|
|
147
|
-
const ctx = getContextManager(targetDir);
|
|
148
|
-
await ctx.initialize();
|
|
149
|
-
console.log(`${pc.green('●')} Ready.`);
|
|
150
|
-
const mcpManager = getMCPManager();
|
|
151
|
-
try {
|
|
152
|
-
const configs = await mcpManager.loadConfig();
|
|
153
|
-
if (configs.length > 0)
|
|
154
|
-
await mcpManager.connectAll(configs);
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
if (DEBUG)
|
|
158
|
-
console.error('MCP init error:', error);
|
|
159
|
-
}
|
|
160
|
-
const tierConfigs = MOE_MODE ? loadTierConfig() : null;
|
|
161
|
-
const multiProvider = tierConfigs ? createMultiProvider(tierConfigs) : null;
|
|
162
|
-
const singleProvider = !MOE_MODE ? createProvider() : null;
|
|
163
|
-
const generate = async (input) => {
|
|
164
|
-
const history = ctx.getHistory();
|
|
165
|
-
const fullPrompt = await ctx.buildSystemPrompt();
|
|
166
|
-
if (MOE_MODE && multiProvider && tierConfigs) {
|
|
167
|
-
const routing = await routeTask(input, async (prompt) => {
|
|
168
|
-
return multiProvider.generateWithTier(1, prompt, [{ role: 'user', content: input }]);
|
|
169
|
-
});
|
|
170
|
-
if (DEBUG)
|
|
171
|
-
console.log(pc.dim(`[Routing] Tier: ${routing.tier}`));
|
|
172
|
-
return multiProvider.generateWithTier(routing.tier, fullPrompt, history.map(m => ({ role: m.role, content: m.content })));
|
|
173
|
-
}
|
|
174
|
-
return singleProvider.generateResponse(fullPrompt, history.map(m => ({ role: m.role, content: m.content })));
|
|
175
|
-
};
|
|
176
|
-
let isFirstPrompt = true;
|
|
177
|
-
while (true) {
|
|
178
|
-
const skill = getActiveSkill();
|
|
179
|
-
let input;
|
|
180
|
-
// Support initial prompt from command line
|
|
181
|
-
if (isFirstPrompt && args.length > 0) {
|
|
182
|
-
input = args.join(' ');
|
|
183
|
-
console.log(`\n${pc.magenta('➤')} ${pc.bold(input)}`);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
input = await text({
|
|
187
|
-
message: pc.dim(`[@${skill.name}]`) + ' Chat with Simple-CLI',
|
|
188
|
-
placeholder: 'Ask anything or use /help',
|
|
189
|
-
validate(value) {
|
|
190
|
-
if (value.trim().length === 0)
|
|
191
|
-
return 'Input required';
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
isFirstPrompt = false;
|
|
196
|
-
if (isCancel(input)) {
|
|
197
|
-
console.log(`\n${pc.dim('—')} Goodbye!`);
|
|
198
|
-
mcpManager.disconnectAll();
|
|
199
|
-
process.exit(0);
|
|
200
|
-
}
|
|
201
|
-
const trimmedInput = input.trim();
|
|
202
|
-
// Slash command
|
|
203
|
-
if (trimmedInput.startsWith('/')) {
|
|
204
|
-
try {
|
|
205
|
-
await executeCommand(trimmedInput, {
|
|
206
|
-
cwd: ctx.getCwd(),
|
|
207
|
-
activeFiles: ctx.getState().activeFiles,
|
|
208
|
-
readOnlyFiles: ctx.getState().readOnlyFiles,
|
|
209
|
-
history: ctx.getHistory(),
|
|
210
|
-
io: {
|
|
211
|
-
output: (m) => console.log(`\n${pc.dim('○')} ${m}`),
|
|
212
|
-
error: (m) => console.log(`\n${pc.red('✖')} ${m}`),
|
|
213
|
-
confirm: async (m) => {
|
|
214
|
-
const c = await clackConfirm({ message: m });
|
|
215
|
-
return !isCancel(c) && c;
|
|
216
|
-
},
|
|
217
|
-
prompt: async (m) => {
|
|
218
|
-
const p = await text({ message: m });
|
|
219
|
-
return isCancel(p) ? '' : p;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
catch (err) {
|
|
225
|
-
console.log(`\n${pc.red('✖')} ${pc.red(String(err))}`);
|
|
226
|
-
}
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
// Skill switching
|
|
230
|
-
if (trimmedInput.startsWith('@')) {
|
|
231
|
-
const skillName = trimmedInput.slice(1).trim();
|
|
232
|
-
if (skillName === 'list') {
|
|
233
|
-
const skills = listSkills();
|
|
234
|
-
const selected = await select({
|
|
235
|
-
message: 'Select a skill',
|
|
236
|
-
options: skills.map(s => ({ label: `@${s.name} - ${s.description}`, value: s.name }))
|
|
237
|
-
});
|
|
238
|
-
if (!isCancel(selected)) {
|
|
239
|
-
const newSkill = setActiveSkill(selected);
|
|
240
|
-
if (newSkill)
|
|
241
|
-
ctx.setSkill(newSkill);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
const newSkill = setActiveSkill(skillName);
|
|
246
|
-
if (newSkill) {
|
|
247
|
-
ctx.setSkill(newSkill);
|
|
248
|
-
console.log(`\n${pc.cyan('★')} Switched to @${newSkill.name}`);
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
console.log(`\n${pc.red('✖')} Skill @${skillName} not found.`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
// Handle optional directory argument
|
|
13
|
+
let cwd = process.cwd();
|
|
14
|
+
let interactive = true;
|
|
15
|
+
const remainingArgs = [];
|
|
16
|
+
for (const arg of args) {
|
|
17
|
+
if (arg === '--non-interactive') {
|
|
18
|
+
interactive = false;
|
|
254
19
|
continue;
|
|
255
20
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
console.log(`\n${pc.dim('💭')} ${pc.cyan(thought)}`);
|
|
263
|
-
if (action.tool !== 'none') {
|
|
264
|
-
if (await confirm(action.tool, action.args || {}, ctx)) {
|
|
265
|
-
console.log(`${pc.yellow('⚙')} ${pc.dim(`Executing ${action.tool}...`)}`);
|
|
266
|
-
const result = await executeTool(action.tool, action.args || {}, ctx);
|
|
267
|
-
console.log(`${pc.green('✔')} ${pc.dim(result.length > 500 ? result.slice(0, 500) + '...' : result)}`);
|
|
268
|
-
ctx.addMessage('assistant', response);
|
|
269
|
-
ctx.addMessage('user', `Tool result: ${result}`);
|
|
270
|
-
steps++;
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
console.log(`${pc.yellow('⚠')} Skipped.`);
|
|
274
|
-
ctx.addMessage('assistant', response);
|
|
275
|
-
break;
|
|
21
|
+
if (!arg.startsWith('-')) {
|
|
22
|
+
try {
|
|
23
|
+
if (statSync(arg).isDirectory()) {
|
|
24
|
+
cwd = arg;
|
|
25
|
+
process.chdir(cwd);
|
|
26
|
+
continue;
|
|
276
27
|
}
|
|
277
28
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
29
|
+
catch { }
|
|
30
|
+
}
|
|
31
|
+
remainingArgs.push(arg);
|
|
32
|
+
}
|
|
33
|
+
const prompt = remainingArgs.filter(a => !a.startsWith('-')).join(' ');
|
|
34
|
+
const registry = new Registry();
|
|
35
|
+
allBuiltins.forEach(t => registry.tools.set(t.name, t));
|
|
36
|
+
await registry.loadProjectTools(cwd);
|
|
37
|
+
const mcp = new MCP();
|
|
38
|
+
const provider = createLLM();
|
|
39
|
+
const engine = new Engine(provider, registry, mcp);
|
|
40
|
+
const skill = await getActiveSkill(cwd);
|
|
41
|
+
const ctx = new Context(cwd, skill);
|
|
42
|
+
showBanner();
|
|
43
|
+
await engine.run(ctx, prompt || undefined, { interactive });
|
|
44
|
+
}
|
|
45
|
+
main().catch(console.error);
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MCP } from './mcp.js';
|
|
2
|
+
import { Skill } from './skills.js';
|
|
3
|
+
export interface Message {
|
|
4
|
+
role: 'user' | 'assistant' | 'system';
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Tool {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
execute: (args: any) => Promise<any>;
|
|
11
|
+
}
|
|
12
|
+
export declare class Context {
|
|
13
|
+
history: Message[];
|
|
14
|
+
activeFiles: Set<string>;
|
|
15
|
+
cwd: string;
|
|
16
|
+
skill: Skill;
|
|
17
|
+
constructor(cwd: string, skill: Skill);
|
|
18
|
+
buildPrompt(tools: Map<string, Tool>): Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
export declare class Registry {
|
|
21
|
+
tools: Map<string, Tool>;
|
|
22
|
+
loadProjectTools(cwd: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare class Engine {
|
|
25
|
+
private llm;
|
|
26
|
+
private registry;
|
|
27
|
+
private mcp;
|
|
28
|
+
private learningManager;
|
|
29
|
+
constructor(llm: any, registry: Registry, mcp: MCP);
|
|
30
|
+
run(ctx: Context, initialPrompt?: string, options?: {
|
|
31
|
+
interactive: boolean;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readdir } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { text, isCancel } from '@clack/prompts';
|
|
7
|
+
import { LearningManager } from './learnings.js';
|
|
8
|
+
async function getRepoMap(cwd) {
|
|
9
|
+
const files = [];
|
|
10
|
+
async function walk(dir) {
|
|
11
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
12
|
+
for (const e of entries) {
|
|
13
|
+
if (['node_modules', '.git', 'dist'].includes(e.name))
|
|
14
|
+
continue;
|
|
15
|
+
const res = join(dir, e.name);
|
|
16
|
+
if (e.isDirectory())
|
|
17
|
+
await walk(res);
|
|
18
|
+
else if (['.ts', '.js', '.py', '.md'].includes(res.slice(-3)))
|
|
19
|
+
files.push(relative(cwd, res));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await walk(cwd);
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
return files.slice(0, 50).join('\n');
|
|
27
|
+
}
|
|
28
|
+
export class Context {
|
|
29
|
+
history = [];
|
|
30
|
+
activeFiles = new Set();
|
|
31
|
+
cwd;
|
|
32
|
+
skill;
|
|
33
|
+
constructor(cwd, skill) {
|
|
34
|
+
this.cwd = cwd;
|
|
35
|
+
this.skill = skill;
|
|
36
|
+
}
|
|
37
|
+
async buildPrompt(tools) {
|
|
38
|
+
const repoMap = await getRepoMap(this.cwd);
|
|
39
|
+
const toolDefs = Array.from(tools.values()).map(t => `- ${t.name}: ${t.description}`).join('\n');
|
|
40
|
+
return `${this.skill.systemPrompt}\n\n## Tools\n${toolDefs}\n\n## Repository\n${repoMap}\n\n## Active Files\n${Array.from(this.activeFiles).map(f => relative(this.cwd, f)).join(', ')}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class Registry {
|
|
44
|
+
tools = new Map();
|
|
45
|
+
async loadProjectTools(cwd) {
|
|
46
|
+
const dir = join(cwd, '.agent', 'tools');
|
|
47
|
+
if (!existsSync(dir))
|
|
48
|
+
return;
|
|
49
|
+
for (const f of await readdir(dir)) {
|
|
50
|
+
if (f.endsWith('.ts') || f.endsWith('.js')) {
|
|
51
|
+
const mod = await import(pathToFileURL(join(dir, f)).href);
|
|
52
|
+
const t = mod.tool || mod.default;
|
|
53
|
+
if (t?.name)
|
|
54
|
+
this.tools.set(t.name, t);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class Engine {
|
|
60
|
+
llm;
|
|
61
|
+
registry;
|
|
62
|
+
mcp;
|
|
63
|
+
learningManager;
|
|
64
|
+
constructor(llm, registry, mcp) {
|
|
65
|
+
this.llm = llm;
|
|
66
|
+
this.registry = registry;
|
|
67
|
+
this.mcp = mcp;
|
|
68
|
+
this.learningManager = new LearningManager(process.cwd());
|
|
69
|
+
}
|
|
70
|
+
async run(ctx, initialPrompt, options = { interactive: true }) {
|
|
71
|
+
await this.learningManager.load();
|
|
72
|
+
let input = initialPrompt;
|
|
73
|
+
await this.mcp.init();
|
|
74
|
+
(await this.mcp.getTools()).forEach(t => this.registry.tools.set(t.name, t));
|
|
75
|
+
// Ensure tools are loaded for the context cwd
|
|
76
|
+
await this.registry.loadProjectTools(ctx.cwd);
|
|
77
|
+
while (true) {
|
|
78
|
+
if (!input) {
|
|
79
|
+
if (!options.interactive)
|
|
80
|
+
break;
|
|
81
|
+
const res = await text({ message: pc.cyan('Chat') });
|
|
82
|
+
if (isCancel(res))
|
|
83
|
+
break;
|
|
84
|
+
input = res;
|
|
85
|
+
}
|
|
86
|
+
ctx.history.push({ role: 'user', content: input });
|
|
87
|
+
// RAG: Inject learnings
|
|
88
|
+
let prompt = await ctx.buildPrompt(this.registry.tools);
|
|
89
|
+
const userHistory = ctx.history.filter(m => m.role === 'user' && !['Continue.', 'Fix the error.'].includes(m.content));
|
|
90
|
+
const lastUserMsg = userHistory[userHistory.length - 1]?.content || '';
|
|
91
|
+
const query = (input && !['Continue.', 'Fix the error.'].includes(input)) ? input : lastUserMsg;
|
|
92
|
+
const learnings = await this.learningManager.search(query);
|
|
93
|
+
if (learnings.length > 0) {
|
|
94
|
+
prompt += `\n\n## Past Learnings\n${learnings.map(l => `- ${l}`).join('\n')}`;
|
|
95
|
+
}
|
|
96
|
+
const response = await this.llm.generate(prompt, ctx.history);
|
|
97
|
+
const { thought, tool, args, message } = response;
|
|
98
|
+
if (thought)
|
|
99
|
+
console.log(pc.dim(`💭 ${thought}`));
|
|
100
|
+
if (tool && tool !== 'none') {
|
|
101
|
+
const t = this.registry.tools.get(tool);
|
|
102
|
+
if (t) {
|
|
103
|
+
console.log(pc.yellow(`⚙ Executing ${tool}...`));
|
|
104
|
+
try {
|
|
105
|
+
const result = await t.execute(args);
|
|
106
|
+
// Reload tools if create_tool was used
|
|
107
|
+
if (tool === 'create_tool') {
|
|
108
|
+
await this.registry.loadProjectTools(ctx.cwd);
|
|
109
|
+
console.log(pc.magenta('🔄 Tools reloaded.'));
|
|
110
|
+
}
|
|
111
|
+
ctx.history.push({ role: 'assistant', content: JSON.stringify(response) });
|
|
112
|
+
ctx.history.push({ role: 'user', content: `Result: ${JSON.stringify(result)}` });
|
|
113
|
+
// Reflection: Learning loop
|
|
114
|
+
const reflectPrompt = "Analyze the previous tool execution. What went well? What failed? Summarize as a concise learning point for future reference.";
|
|
115
|
+
const reflection = await this.llm.generate(reflectPrompt, [...ctx.history, { role: 'user', content: reflectPrompt }]);
|
|
116
|
+
if (reflection.message) {
|
|
117
|
+
// Find the relevant task description
|
|
118
|
+
const userHistory = ctx.history.filter(m => m.role === 'user' && !['Continue.', 'Fix the error.'].includes(m.content));
|
|
119
|
+
const task = (input && !['Continue.', 'Fix the error.'].includes(input)) ? input : (userHistory[userHistory.length - 1]?.content || 'Task');
|
|
120
|
+
await this.learningManager.add(task, reflection.message);
|
|
121
|
+
console.log(pc.blue(`📝 Learning stored: ${reflection.message}`));
|
|
122
|
+
}
|
|
123
|
+
input = 'The previous tool execution was successful. Proceed with the next step.';
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
ctx.history.push({ role: 'user', content: `Error: ${e.message}` });
|
|
128
|
+
input = 'Fix the error.';
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
console.log(`\n${pc.green('🤖')} ${message || response.raw}\n`);
|
|
134
|
+
ctx.history.push({ role: 'assistant', content: message || response.raw });
|
|
135
|
+
input = undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Learning {
|
|
2
|
+
id: string;
|
|
3
|
+
task: string;
|
|
4
|
+
reflection: string;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class LearningManager {
|
|
8
|
+
private learnings;
|
|
9
|
+
private path;
|
|
10
|
+
constructor(cwd: string);
|
|
11
|
+
load(): Promise<void>;
|
|
12
|
+
save(): Promise<void>;
|
|
13
|
+
add(task: string, reflection: string): Promise<void>;
|
|
14
|
+
search(query: string): Promise<string[]>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
export class LearningManager {
|
|
5
|
+
learnings = [];
|
|
6
|
+
path;
|
|
7
|
+
constructor(cwd) {
|
|
8
|
+
const agentDir = join(cwd, '.agent');
|
|
9
|
+
if (!existsSync(agentDir)) {
|
|
10
|
+
try {
|
|
11
|
+
mkdirSync(agentDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
}
|
|
15
|
+
this.path = join(agentDir, 'learnings.json');
|
|
16
|
+
}
|
|
17
|
+
async load() {
|
|
18
|
+
if (!existsSync(this.path))
|
|
19
|
+
return;
|
|
20
|
+
try {
|
|
21
|
+
const data = await readFile(this.path, 'utf-8');
|
|
22
|
+
this.learnings = JSON.parse(data);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
this.learnings = [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async save() {
|
|
29
|
+
await writeFile(this.path, JSON.stringify(this.learnings, null, 2));
|
|
30
|
+
}
|
|
31
|
+
async add(task, reflection) {
|
|
32
|
+
this.learnings.push({
|
|
33
|
+
id: Date.now().toString(),
|
|
34
|
+
task,
|
|
35
|
+
reflection,
|
|
36
|
+
timestamp: Date.now()
|
|
37
|
+
});
|
|
38
|
+
await this.save();
|
|
39
|
+
}
|
|
40
|
+
async search(query) {
|
|
41
|
+
if (!query)
|
|
42
|
+
return [];
|
|
43
|
+
const keywords = query.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
44
|
+
if (keywords.length === 0)
|
|
45
|
+
return [];
|
|
46
|
+
return this.learnings
|
|
47
|
+
.filter(l => {
|
|
48
|
+
const text = (l.task + ' ' + l.reflection).toLowerCase();
|
|
49
|
+
return keywords.some(k => text.includes(k));
|
|
50
|
+
})
|
|
51
|
+
.map(l => l.reflection)
|
|
52
|
+
.slice(0, 5);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface LLMResponse {
|
|
2
|
+
thought: string;
|
|
3
|
+
tool: string;
|
|
4
|
+
args: any;
|
|
5
|
+
message?: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class LLM {
|
|
9
|
+
private config;
|
|
10
|
+
constructor(config: {
|
|
11
|
+
provider: string;
|
|
12
|
+
model: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
});
|
|
15
|
+
generate(system: string, history: any[]): Promise<LLMResponse>;
|
|
16
|
+
private parse;
|
|
17
|
+
}
|
|
18
|
+
export declare const createLLM: (model?: string) => LLM;
|
package/dist/llm.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { jsonrepair } from 'jsonrepair';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export class LLM {
|
|
8
|
+
config;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
async generate(system, history) {
|
|
13
|
+
const payload = {
|
|
14
|
+
...this.config,
|
|
15
|
+
messages: [{ role: 'system', content: system }, ...history],
|
|
16
|
+
api_key: this.config.apiKey || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY || process.env.ANTHROPIC_API_KEY
|
|
17
|
+
};
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
// Find python bridge
|
|
20
|
+
let py = join(__dirname, 'anyllm.py');
|
|
21
|
+
if (!fs.existsSync(py))
|
|
22
|
+
py = join(process.cwd(), 'src/lib/anyllm.py'); // Fallback
|
|
23
|
+
const child = spawn('python3', [py]);
|
|
24
|
+
let out = '';
|
|
25
|
+
let err = '';
|
|
26
|
+
child.stdout.on('data', d => out += d);
|
|
27
|
+
child.stderr.on('data', d => err += d);
|
|
28
|
+
child.on('close', code => {
|
|
29
|
+
if (code !== 0)
|
|
30
|
+
return reject(new Error(err));
|
|
31
|
+
try {
|
|
32
|
+
const res = JSON.parse(out);
|
|
33
|
+
if (res.error)
|
|
34
|
+
return reject(new Error(res.error));
|
|
35
|
+
resolve(this.parse(res.content));
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
reject(e);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
child.stdin.write(JSON.stringify(payload));
|
|
42
|
+
child.stdin.end();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
parse(raw) {
|
|
46
|
+
try {
|
|
47
|
+
const repaired = jsonrepair(raw.trim().match(/\{[\s\S]*\}/)?.[0] || raw);
|
|
48
|
+
const p = JSON.parse(repaired);
|
|
49
|
+
return {
|
|
50
|
+
thought: p.thought || '',
|
|
51
|
+
tool: (p.tool || p.command || 'none').toLowerCase(),
|
|
52
|
+
args: p.args || p.parameters || {},
|
|
53
|
+
message: p.message || '',
|
|
54
|
+
raw
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { thought: '', tool: 'none', args: {}, message: raw, raw };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const createLLM = (model) => {
|
|
63
|
+
const m = model || process.env.MODEL || 'openai:gpt-5.2-codex';
|
|
64
|
+
const [p, n] = m.includes(':') ? m.split(':') : ['openai', m];
|
|
65
|
+
return new LLM({ provider: p, model: n });
|
|
66
|
+
};
|