chainlesschain 0.37.8 → 0.37.10
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 +403 -8
- package/bin/chainlesschain.js +4 -0
- package/package.json +7 -2
- package/src/commands/agent.js +30 -0
- package/src/commands/ask.js +114 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/chat.js +35 -0
- package/src/commands/db.js +152 -0
- package/src/commands/did.js +376 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/import.js +259 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +288 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +489 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +398 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +479 -0
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/index.js +65 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/platform.js +15 -0
- package/src/lib/plugin-manager.js +312 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/repl/agent-repl.js +912 -0
- package/src/repl/chat-repl.js +262 -0
- package/src/runtime/bootstrap.js +159 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic REPL - Claude Code / Codex style
|
|
3
|
+
*
|
|
4
|
+
* User speaks naturally → AI understands intent → picks tools → executes → shows result
|
|
5
|
+
*
|
|
6
|
+
* Built-in tools:
|
|
7
|
+
* - read_file: Read a file
|
|
8
|
+
* - write_file: Write/create a file
|
|
9
|
+
* - edit_file: Edit part of a file
|
|
10
|
+
* - run_shell: Execute a shell command
|
|
11
|
+
* - search_files: Search for files by name/content
|
|
12
|
+
* - list_dir: List directory contents
|
|
13
|
+
* - db_query: Query the ChainlessChain database
|
|
14
|
+
* - note_add: Add a note
|
|
15
|
+
* - note_search: Search notes
|
|
16
|
+
*
|
|
17
|
+
* The AI decides which tools to call based on user intent.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import readline from "readline";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import fs from "fs";
|
|
23
|
+
import path from "path";
|
|
24
|
+
import { execSync } from "child_process";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
import { logger } from "../lib/logger.js";
|
|
27
|
+
import { getPlanModeManager, PlanState } from "../lib/plan-mode.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tool definitions for function calling
|
|
31
|
+
*/
|
|
32
|
+
const TOOLS = [
|
|
33
|
+
{
|
|
34
|
+
type: "function",
|
|
35
|
+
function: {
|
|
36
|
+
name: "read_file",
|
|
37
|
+
description: "Read a file's content",
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
path: { type: "string", description: "File path to read" },
|
|
42
|
+
},
|
|
43
|
+
required: ["path"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "function",
|
|
49
|
+
function: {
|
|
50
|
+
name: "write_file",
|
|
51
|
+
description: "Write content to a file (create or overwrite)",
|
|
52
|
+
parameters: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
path: { type: "string", description: "File path" },
|
|
56
|
+
content: { type: "string", description: "File content" },
|
|
57
|
+
},
|
|
58
|
+
required: ["path", "content"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: "function",
|
|
64
|
+
function: {
|
|
65
|
+
name: "edit_file",
|
|
66
|
+
description: "Replace a specific string in a file with new content",
|
|
67
|
+
parameters: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
path: { type: "string", description: "File path" },
|
|
71
|
+
old_string: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Exact string to find and replace",
|
|
74
|
+
},
|
|
75
|
+
new_string: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Replacement string",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
required: ["path", "old_string", "new_string"],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "function",
|
|
86
|
+
function: {
|
|
87
|
+
name: "run_shell",
|
|
88
|
+
description:
|
|
89
|
+
"Execute a shell command and return the output. Use for running tests, installing packages, git operations, etc.",
|
|
90
|
+
parameters: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
94
|
+
cwd: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Working directory (optional)",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["command"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "function",
|
|
105
|
+
function: {
|
|
106
|
+
name: "search_files",
|
|
107
|
+
description: "Search for files by name pattern or content",
|
|
108
|
+
parameters: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
pattern: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Glob pattern or search string",
|
|
114
|
+
},
|
|
115
|
+
directory: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "Directory to search in (default: cwd)",
|
|
118
|
+
},
|
|
119
|
+
content_search: {
|
|
120
|
+
type: "boolean",
|
|
121
|
+
description: "If true, search file contents instead of names",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ["pattern"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: "function",
|
|
130
|
+
function: {
|
|
131
|
+
name: "list_dir",
|
|
132
|
+
description: "List contents of a directory",
|
|
133
|
+
parameters: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
path: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Directory path (default: cwd)",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "function",
|
|
146
|
+
function: {
|
|
147
|
+
name: "run_skill",
|
|
148
|
+
description:
|
|
149
|
+
"Run a built-in ChainlessChain skill. Available skills include: code-review, summarize, translate, refactor, unit-test, debug, explain-code, browser-automation, data-analysis, git-history-analyzer, and 130+ more. Use list_skills first to discover available skills.",
|
|
150
|
+
parameters: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
skill_name: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description:
|
|
156
|
+
"Name of the skill to run (e.g. code-review, summarize, translate)",
|
|
157
|
+
},
|
|
158
|
+
input: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "Input text or parameters for the skill",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["skill_name", "input"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: "function",
|
|
169
|
+
function: {
|
|
170
|
+
name: "list_skills",
|
|
171
|
+
description:
|
|
172
|
+
"List available built-in skills, optionally filtered by category or keyword",
|
|
173
|
+
parameters: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
category: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description:
|
|
179
|
+
"Filter by category (e.g. development, automation, data)",
|
|
180
|
+
},
|
|
181
|
+
query: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "Search keyword to filter skills",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find and load bundled skills (shared with skill command)
|
|
193
|
+
*/
|
|
194
|
+
const __agentDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
195
|
+
|
|
196
|
+
function findSkillsDir() {
|
|
197
|
+
const candidates = [
|
|
198
|
+
path.resolve(
|
|
199
|
+
__agentDirname,
|
|
200
|
+
"../../../../../desktop-app-vue/src/main/ai-engine/cowork/skills/builtin",
|
|
201
|
+
),
|
|
202
|
+
path.resolve(
|
|
203
|
+
process.cwd(),
|
|
204
|
+
"desktop-app-vue/src/main/ai-engine/cowork/skills/builtin",
|
|
205
|
+
),
|
|
206
|
+
];
|
|
207
|
+
for (const dir of candidates) {
|
|
208
|
+
if (fs.existsSync(dir)) return dir;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function loadSkillList(skillsDir) {
|
|
214
|
+
const skills = [];
|
|
215
|
+
try {
|
|
216
|
+
const dirs = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
217
|
+
for (const dir of dirs) {
|
|
218
|
+
if (!dir.isDirectory()) continue;
|
|
219
|
+
const skillMd = path.join(skillsDir, dir.name, "SKILL.md");
|
|
220
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
221
|
+
try {
|
|
222
|
+
const content = fs.readFileSync(skillMd, "utf8");
|
|
223
|
+
const lines = content.split("\n");
|
|
224
|
+
if (lines[0].trim() !== "---") continue;
|
|
225
|
+
let endIndex = -1;
|
|
226
|
+
for (let i = 1; i < lines.length; i++) {
|
|
227
|
+
if (lines[i].trim() === "---") {
|
|
228
|
+
endIndex = i;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (endIndex === -1) continue;
|
|
233
|
+
const data = {};
|
|
234
|
+
for (const line of lines.slice(1, endIndex)) {
|
|
235
|
+
const ci = line.indexOf(":");
|
|
236
|
+
if (ci > 0) {
|
|
237
|
+
const key = line.slice(0, ci).trim();
|
|
238
|
+
const val = line
|
|
239
|
+
.slice(ci + 1)
|
|
240
|
+
.trim()
|
|
241
|
+
.replace(/^['"]|['"]$/g, "");
|
|
242
|
+
data[key] = val;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
skills.push({
|
|
246
|
+
id: data.name || dir.name,
|
|
247
|
+
description: data.description || "",
|
|
248
|
+
category: data.category || "uncategorized",
|
|
249
|
+
dirName: dir.name,
|
|
250
|
+
hasHandler: fs.existsSync(
|
|
251
|
+
path.join(skillsDir, dir.name, "handler.js"),
|
|
252
|
+
),
|
|
253
|
+
});
|
|
254
|
+
} catch {
|
|
255
|
+
// Skip malformed skill files
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Skills dir unreadable
|
|
260
|
+
}
|
|
261
|
+
return skills;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Execute a tool call (with plan mode filtering)
|
|
266
|
+
*/
|
|
267
|
+
async function executeTool(name, args) {
|
|
268
|
+
// Plan mode: check if tool is allowed
|
|
269
|
+
const planManager = getPlanModeManager();
|
|
270
|
+
if (planManager.isActive() && !planManager.isToolAllowed(name)) {
|
|
271
|
+
// In plan mode, log the blocked tool as a plan item
|
|
272
|
+
planManager.addPlanItem({
|
|
273
|
+
title: `${name}: ${formatToolArgs(name, args)}`,
|
|
274
|
+
tool: name,
|
|
275
|
+
params: args,
|
|
276
|
+
estimatedImpact:
|
|
277
|
+
name === "run_shell"
|
|
278
|
+
? "high"
|
|
279
|
+
: name === "write_file"
|
|
280
|
+
? "medium"
|
|
281
|
+
: "low",
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
error: `[Plan Mode] Tool "${name}" is blocked during planning. It has been added to the plan. Use /plan approve to execute.`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
switch (name) {
|
|
289
|
+
case "read_file": {
|
|
290
|
+
const filePath = path.resolve(args.path);
|
|
291
|
+
if (!fs.existsSync(filePath)) {
|
|
292
|
+
return { error: `File not found: ${filePath}` };
|
|
293
|
+
}
|
|
294
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
295
|
+
// Truncate very long files
|
|
296
|
+
if (content.length > 50000) {
|
|
297
|
+
return {
|
|
298
|
+
content: content.substring(0, 50000) + "\n...(truncated)",
|
|
299
|
+
size: content.length,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { content };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
case "write_file": {
|
|
306
|
+
const filePath = path.resolve(args.path);
|
|
307
|
+
const dir = path.dirname(filePath);
|
|
308
|
+
if (!fs.existsSync(dir)) {
|
|
309
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
310
|
+
}
|
|
311
|
+
fs.writeFileSync(filePath, args.content, "utf8");
|
|
312
|
+
return { success: true, path: filePath, size: args.content.length };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case "edit_file": {
|
|
316
|
+
const filePath = path.resolve(args.path);
|
|
317
|
+
if (!fs.existsSync(filePath)) {
|
|
318
|
+
return { error: `File not found: ${filePath}` };
|
|
319
|
+
}
|
|
320
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
321
|
+
if (!content.includes(args.old_string)) {
|
|
322
|
+
return { error: "old_string not found in file" };
|
|
323
|
+
}
|
|
324
|
+
const newContent = content.replace(args.old_string, args.new_string);
|
|
325
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
326
|
+
return { success: true, path: filePath };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case "run_shell": {
|
|
330
|
+
try {
|
|
331
|
+
const output = execSync(args.command, {
|
|
332
|
+
cwd: args.cwd || process.cwd(),
|
|
333
|
+
encoding: "utf8",
|
|
334
|
+
timeout: 30000,
|
|
335
|
+
maxBuffer: 1024 * 1024,
|
|
336
|
+
});
|
|
337
|
+
return { stdout: output.substring(0, 10000) };
|
|
338
|
+
} catch (err) {
|
|
339
|
+
return {
|
|
340
|
+
error: err.message.substring(0, 2000),
|
|
341
|
+
stderr: (err.stderr || "").substring(0, 2000),
|
|
342
|
+
exitCode: err.status,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case "search_files": {
|
|
348
|
+
const dir = args.directory ? path.resolve(args.directory) : process.cwd();
|
|
349
|
+
try {
|
|
350
|
+
if (args.content_search) {
|
|
351
|
+
// Use grep/findstr for content search
|
|
352
|
+
const cmd =
|
|
353
|
+
process.platform === "win32"
|
|
354
|
+
? `findstr /s /i /n "${args.pattern}" *`
|
|
355
|
+
: `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`;
|
|
356
|
+
const output = execSync(cmd, {
|
|
357
|
+
cwd: dir,
|
|
358
|
+
encoding: "utf8",
|
|
359
|
+
timeout: 10000,
|
|
360
|
+
});
|
|
361
|
+
return { matches: output.trim().split("\n").slice(0, 20) };
|
|
362
|
+
} else {
|
|
363
|
+
// File name search
|
|
364
|
+
const cmd =
|
|
365
|
+
process.platform === "win32"
|
|
366
|
+
? `dir /s /b *${args.pattern}* 2>NUL`
|
|
367
|
+
: `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
|
|
368
|
+
const output = execSync(cmd, {
|
|
369
|
+
cwd: dir,
|
|
370
|
+
encoding: "utf8",
|
|
371
|
+
timeout: 10000,
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
files: output.trim().split("\n").filter(Boolean).slice(0, 20),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
return { files: [], message: "No matches found" };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case "list_dir": {
|
|
383
|
+
const dirPath = args.path ? path.resolve(args.path) : process.cwd();
|
|
384
|
+
if (!fs.existsSync(dirPath)) {
|
|
385
|
+
return { error: `Directory not found: ${dirPath}` };
|
|
386
|
+
}
|
|
387
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
388
|
+
return {
|
|
389
|
+
entries: entries.map((e) => ({
|
|
390
|
+
name: e.name,
|
|
391
|
+
type: e.isDirectory() ? "dir" : "file",
|
|
392
|
+
})),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
case "run_skill": {
|
|
397
|
+
const skillsDir = findSkillsDir();
|
|
398
|
+
if (!skillsDir) {
|
|
399
|
+
return {
|
|
400
|
+
error:
|
|
401
|
+
"Skills directory not found. Make sure you're in the ChainlessChain project root.",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const handlerPath = path.join(skillsDir, args.skill_name, "handler.js");
|
|
405
|
+
if (!fs.existsSync(handlerPath)) {
|
|
406
|
+
// Try fuzzy match
|
|
407
|
+
const skills = loadSkillList(skillsDir);
|
|
408
|
+
const match = skills.find(
|
|
409
|
+
(s) => s.id === args.skill_name || s.dirName === args.skill_name,
|
|
410
|
+
);
|
|
411
|
+
if (match && match.hasHandler) {
|
|
412
|
+
const matchedPath = path.join(skillsDir, match.dirName, "handler.js");
|
|
413
|
+
try {
|
|
414
|
+
const handler = (
|
|
415
|
+
await import(`file://${matchedPath.replace(/\\/g, "/")}`)
|
|
416
|
+
).default;
|
|
417
|
+
const task = {
|
|
418
|
+
params: { input: args.input },
|
|
419
|
+
input: args.input,
|
|
420
|
+
action: args.input,
|
|
421
|
+
};
|
|
422
|
+
const context = {
|
|
423
|
+
projectRoot: process.cwd(),
|
|
424
|
+
workspacePath: process.cwd(),
|
|
425
|
+
};
|
|
426
|
+
const result = await handler.execute(task, context);
|
|
427
|
+
return result;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
return { error: `Skill execution failed: ${err.message}` };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
error: `Skill "${args.skill_name}" not found or has no handler. Use list_skills to see available skills.`,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const handler = (
|
|
438
|
+
await import(`file://${handlerPath.replace(/\\/g, "/")}`)
|
|
439
|
+
).default;
|
|
440
|
+
if (handler.init) await handler.init({});
|
|
441
|
+
const task = {
|
|
442
|
+
params: { input: args.input },
|
|
443
|
+
input: args.input,
|
|
444
|
+
action: args.input,
|
|
445
|
+
};
|
|
446
|
+
const context = {
|
|
447
|
+
projectRoot: process.cwd(),
|
|
448
|
+
workspacePath: process.cwd(),
|
|
449
|
+
};
|
|
450
|
+
const result = await handler.execute(task, context);
|
|
451
|
+
return result;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
return { error: `Skill execution failed: ${err.message}` };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case "list_skills": {
|
|
458
|
+
const skillsDir = findSkillsDir();
|
|
459
|
+
if (!skillsDir) {
|
|
460
|
+
return { error: "Skills directory not found." };
|
|
461
|
+
}
|
|
462
|
+
let skills = loadSkillList(skillsDir);
|
|
463
|
+
if (args.category) {
|
|
464
|
+
skills = skills.filter(
|
|
465
|
+
(s) => s.category.toLowerCase() === args.category.toLowerCase(),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (args.query) {
|
|
469
|
+
const q = args.query.toLowerCase();
|
|
470
|
+
skills = skills.filter(
|
|
471
|
+
(s) =>
|
|
472
|
+
s.id.includes(q) ||
|
|
473
|
+
s.description.toLowerCase().includes(q) ||
|
|
474
|
+
s.category.toLowerCase().includes(q),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
count: skills.length,
|
|
479
|
+
skills: skills.map((s) => ({
|
|
480
|
+
id: s.id,
|
|
481
|
+
category: s.category,
|
|
482
|
+
hasHandler: s.hasHandler,
|
|
483
|
+
description: s.description.substring(0, 80),
|
|
484
|
+
})),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
default:
|
|
489
|
+
return { error: `Unknown tool: ${name}` };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Send a chat completion request with tools
|
|
495
|
+
*/
|
|
496
|
+
async function chatWithTools(messages, options) {
|
|
497
|
+
const { provider, model, baseUrl, apiKey } = options;
|
|
498
|
+
|
|
499
|
+
if (provider === "ollama") {
|
|
500
|
+
// Ollama supports tool calling for some models
|
|
501
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers: { "Content-Type": "application/json" },
|
|
504
|
+
body: JSON.stringify({
|
|
505
|
+
model,
|
|
506
|
+
messages,
|
|
507
|
+
tools: TOOLS,
|
|
508
|
+
stream: false,
|
|
509
|
+
}),
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
throw new Error(`Ollama error: ${response.status}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return await response.json();
|
|
517
|
+
} else if (provider === "openai") {
|
|
518
|
+
const url =
|
|
519
|
+
baseUrl && baseUrl !== "http://localhost:11434"
|
|
520
|
+
? baseUrl
|
|
521
|
+
: "https://api.openai.com/v1";
|
|
522
|
+
const key = apiKey || process.env.OPENAI_API_KEY;
|
|
523
|
+
if (!key) throw new Error("API key required");
|
|
524
|
+
|
|
525
|
+
const response = await fetch(`${url}/chat/completions`, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: {
|
|
528
|
+
"Content-Type": "application/json",
|
|
529
|
+
Authorization: `Bearer ${key}`,
|
|
530
|
+
},
|
|
531
|
+
body: JSON.stringify({
|
|
532
|
+
model: model || "gpt-4o-mini",
|
|
533
|
+
messages,
|
|
534
|
+
tools: TOOLS,
|
|
535
|
+
}),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
throw new Error(`API error: ${response.status}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const data = await response.json();
|
|
543
|
+
// Normalize to Ollama-like format
|
|
544
|
+
if (!data.choices || !data.choices[0]) {
|
|
545
|
+
throw new Error("Invalid API response: no choices returned");
|
|
546
|
+
}
|
|
547
|
+
const choice = data.choices[0];
|
|
548
|
+
return {
|
|
549
|
+
message: choice.message,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Agentic loop - keeps calling tools until the AI gives a final text response
|
|
558
|
+
*/
|
|
559
|
+
async function agentLoop(messages, options) {
|
|
560
|
+
const MAX_ITERATIONS = 10;
|
|
561
|
+
|
|
562
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
563
|
+
const result = await chatWithTools(messages, options);
|
|
564
|
+
const msg = result?.message;
|
|
565
|
+
|
|
566
|
+
if (!msg) {
|
|
567
|
+
return "(No response from LLM)";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check for tool calls
|
|
571
|
+
const toolCalls = msg.tool_calls;
|
|
572
|
+
|
|
573
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
574
|
+
// No tool calls — final text response
|
|
575
|
+
return msg.content || "";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Add assistant message with tool calls
|
|
579
|
+
messages.push(msg);
|
|
580
|
+
|
|
581
|
+
// Execute each tool call
|
|
582
|
+
for (const call of toolCalls) {
|
|
583
|
+
const fn = call.function;
|
|
584
|
+
const toolName = fn.name;
|
|
585
|
+
let toolArgs;
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
toolArgs =
|
|
589
|
+
typeof fn.arguments === "string"
|
|
590
|
+
? JSON.parse(fn.arguments)
|
|
591
|
+
: fn.arguments;
|
|
592
|
+
} catch {
|
|
593
|
+
toolArgs = {};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Show what the AI is doing
|
|
597
|
+
process.stdout.write(
|
|
598
|
+
chalk.gray(` [${toolName}] ${formatToolArgs(toolName, toolArgs)}\n`),
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
const toolResult = await executeTool(toolName, toolArgs);
|
|
602
|
+
|
|
603
|
+
// Show brief result
|
|
604
|
+
if (toolResult.error) {
|
|
605
|
+
process.stdout.write(chalk.red(` Error: ${toolResult.error}\n`));
|
|
606
|
+
} else if (toolResult.success) {
|
|
607
|
+
process.stdout.write(chalk.green(` Done\n`));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Add tool result to messages
|
|
611
|
+
messages.push({
|
|
612
|
+
role: "tool",
|
|
613
|
+
content: JSON.stringify(toolResult).substring(0, 5000),
|
|
614
|
+
tool_call_id: call.id,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return "(Reached max tool call iterations)";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Format tool args for display
|
|
624
|
+
*/
|
|
625
|
+
function formatToolArgs(name, args) {
|
|
626
|
+
switch (name) {
|
|
627
|
+
case "read_file":
|
|
628
|
+
return args.path;
|
|
629
|
+
case "write_file":
|
|
630
|
+
return `${args.path} (${args.content?.length || 0} chars)`;
|
|
631
|
+
case "edit_file":
|
|
632
|
+
return args.path;
|
|
633
|
+
case "run_shell":
|
|
634
|
+
return args.command;
|
|
635
|
+
case "search_files":
|
|
636
|
+
return args.pattern;
|
|
637
|
+
case "list_dir":
|
|
638
|
+
return args.path || ".";
|
|
639
|
+
case "run_skill":
|
|
640
|
+
return `${args.skill_name}: ${(args.input || "").substring(0, 50)}`;
|
|
641
|
+
case "list_skills":
|
|
642
|
+
return args.category || args.query || "all";
|
|
643
|
+
default:
|
|
644
|
+
return JSON.stringify(args).substring(0, 60);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const SYSTEM_PROMPT = `You are ChainlessChain AI Assistant, a powerful agentic coding assistant running in the terminal.
|
|
649
|
+
|
|
650
|
+
You have access to tools that let you read files, write files, edit files, run shell commands, and search the codebase. When the user asks you to do something, USE THE TOOLS to actually do it — don't just describe what should be done.
|
|
651
|
+
|
|
652
|
+
Key behaviors:
|
|
653
|
+
- When asked to modify code, read the file first, then edit it
|
|
654
|
+
- When asked to create something, use write_file to create it
|
|
655
|
+
- When asked to run/test something, use run_shell to execute it
|
|
656
|
+
- When asked about files or code, use read_file and search_files to find information
|
|
657
|
+
- You have 138 built-in skills (code-review, summarize, translate, refactor, etc.) — use list_skills to discover them and run_skill to execute them
|
|
658
|
+
- Always explain what you're doing and show results
|
|
659
|
+
- Be concise but thorough
|
|
660
|
+
|
|
661
|
+
Current working directory: ${process.cwd()}`;
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Start the agentic REPL
|
|
665
|
+
*/
|
|
666
|
+
export async function startAgentRepl(options = {}) {
|
|
667
|
+
let model = options.model || "qwen2:7b";
|
|
668
|
+
let provider = options.provider || "ollama";
|
|
669
|
+
const baseUrl = options.baseUrl || "http://localhost:11434";
|
|
670
|
+
const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
|
|
671
|
+
|
|
672
|
+
const messages = [{ role: "system", content: SYSTEM_PROMPT }];
|
|
673
|
+
|
|
674
|
+
const getPrompt = () => {
|
|
675
|
+
const planManager = getPlanModeManager();
|
|
676
|
+
if (planManager.isActive()) {
|
|
677
|
+
const state = planManager.state;
|
|
678
|
+
if (state === PlanState.APPROVED || state === PlanState.EXECUTING) {
|
|
679
|
+
return chalk.green("[plan:exec] > ");
|
|
680
|
+
}
|
|
681
|
+
return chalk.yellow("[plan] > ");
|
|
682
|
+
}
|
|
683
|
+
return chalk.green("> ");
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const rl = readline.createInterface({
|
|
687
|
+
input: process.stdin,
|
|
688
|
+
output: process.stdout,
|
|
689
|
+
prompt: getPrompt(),
|
|
690
|
+
terminal: true,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
logger.log(chalk.bold("\nChainlessChain Agent"));
|
|
694
|
+
logger.log(
|
|
695
|
+
chalk.gray(`Model: ${model} Provider: ${provider} CWD: ${process.cwd()}`),
|
|
696
|
+
);
|
|
697
|
+
logger.log(
|
|
698
|
+
chalk.gray(
|
|
699
|
+
"Describe what you want to do. I can read/write files, run commands, and more.",
|
|
700
|
+
),
|
|
701
|
+
);
|
|
702
|
+
logger.log(chalk.gray("Type /exit to quit, /help for commands\n"));
|
|
703
|
+
|
|
704
|
+
const prompt = () => {
|
|
705
|
+
rl.setPrompt(getPrompt());
|
|
706
|
+
rl.prompt();
|
|
707
|
+
};
|
|
708
|
+
prompt();
|
|
709
|
+
|
|
710
|
+
rl.on("line", async (input) => {
|
|
711
|
+
const trimmed = input.trim();
|
|
712
|
+
if (!trimmed) {
|
|
713
|
+
prompt();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Slash commands
|
|
718
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
719
|
+
logger.log(chalk.gray("\nGoodbye!"));
|
|
720
|
+
rl.close();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (trimmed === "/help") {
|
|
725
|
+
logger.log(chalk.bold("\nCommands:"));
|
|
726
|
+
logger.log(` ${chalk.cyan("/exit")} Exit the agent`);
|
|
727
|
+
logger.log(
|
|
728
|
+
` ${chalk.cyan("/model")} Show/change model (/model <name>)`,
|
|
729
|
+
);
|
|
730
|
+
logger.log(` ${chalk.cyan("/provider")} Show/change provider`);
|
|
731
|
+
logger.log(` ${chalk.cyan("/clear")} Clear conversation`);
|
|
732
|
+
logger.log(` ${chalk.cyan("/compact")} Keep only last 4 messages`);
|
|
733
|
+
logger.log(
|
|
734
|
+
` ${chalk.cyan("/plan")} Enter plan mode (read-only analysis first)`,
|
|
735
|
+
);
|
|
736
|
+
logger.log(` ${chalk.cyan("/plan show")} Show current plan`);
|
|
737
|
+
logger.log(
|
|
738
|
+
` ${chalk.cyan("/plan approve")} Approve and execute the plan`,
|
|
739
|
+
);
|
|
740
|
+
logger.log(` ${chalk.cyan("/plan reject")} Reject the plan`);
|
|
741
|
+
logger.log(chalk.bold("\nCapabilities:"));
|
|
742
|
+
logger.log(" Read, write, and edit files");
|
|
743
|
+
logger.log(" Run shell commands (git, npm, etc.)");
|
|
744
|
+
logger.log(" Search codebase by filename or content");
|
|
745
|
+
logger.log(" Run 138 built-in skills (code-review, summarize, etc.)");
|
|
746
|
+
logger.log(" Plan mode: analyze first, execute after approval\n");
|
|
747
|
+
prompt();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (trimmed.startsWith("/model")) {
|
|
752
|
+
const arg = trimmed.slice(6).trim();
|
|
753
|
+
if (arg) {
|
|
754
|
+
model = arg;
|
|
755
|
+
logger.info(`Model: ${chalk.cyan(model)}`);
|
|
756
|
+
} else {
|
|
757
|
+
logger.info(`Current model: ${chalk.cyan(model)}`);
|
|
758
|
+
}
|
|
759
|
+
prompt();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (trimmed.startsWith("/provider")) {
|
|
764
|
+
const arg = trimmed.slice(9).trim();
|
|
765
|
+
if (arg) {
|
|
766
|
+
provider = arg;
|
|
767
|
+
logger.info(`Provider: ${chalk.cyan(provider)}`);
|
|
768
|
+
} else {
|
|
769
|
+
logger.info(`Current provider: ${chalk.cyan(provider)}`);
|
|
770
|
+
}
|
|
771
|
+
prompt();
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (trimmed === "/clear") {
|
|
776
|
+
messages.length = 1; // Keep system prompt
|
|
777
|
+
logger.info("Conversation cleared");
|
|
778
|
+
prompt();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (trimmed === "/compact") {
|
|
783
|
+
// Keep system prompt + last 4 messages
|
|
784
|
+
if (messages.length > 5) {
|
|
785
|
+
const systemMsg = messages[0];
|
|
786
|
+
const recent = messages.slice(-4);
|
|
787
|
+
messages.length = 0;
|
|
788
|
+
messages.push(systemMsg, ...recent);
|
|
789
|
+
logger.info("Conversation compacted to last 4 messages");
|
|
790
|
+
}
|
|
791
|
+
prompt();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Plan mode commands
|
|
796
|
+
if (trimmed.startsWith("/plan")) {
|
|
797
|
+
const planManager = getPlanModeManager();
|
|
798
|
+
const subCmd = trimmed.slice(5).trim();
|
|
799
|
+
|
|
800
|
+
if (!subCmd || subCmd === "enter") {
|
|
801
|
+
if (planManager.isActive()) {
|
|
802
|
+
logger.info(
|
|
803
|
+
"Already in plan mode. Use /plan show, /plan approve, or /plan reject.",
|
|
804
|
+
);
|
|
805
|
+
} else {
|
|
806
|
+
planManager.enterPlanMode({ title: "Agent Plan" });
|
|
807
|
+
logger.success(
|
|
808
|
+
"Entered plan mode. Write tools are blocked until you approve the plan.",
|
|
809
|
+
);
|
|
810
|
+
logger.info(
|
|
811
|
+
"The AI can still read files and search. Blocked tools become plan items.",
|
|
812
|
+
);
|
|
813
|
+
logger.info(
|
|
814
|
+
"Use /plan show to see the plan, /plan approve to execute.",
|
|
815
|
+
);
|
|
816
|
+
// Inject plan mode context into system prompt
|
|
817
|
+
messages.push({
|
|
818
|
+
role: "system",
|
|
819
|
+
content:
|
|
820
|
+
"[PLAN MODE ACTIVE] You are now in plan mode. You can read files, search, and analyze — but write/execute tools are blocked. Any blocked tool calls will be recorded as plan items. Analyze the task thoroughly, then the user will approve your plan.",
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
} else if (subCmd === "show") {
|
|
824
|
+
if (!planManager.isActive()) {
|
|
825
|
+
logger.info("Not in plan mode. Use /plan to enter.");
|
|
826
|
+
} else {
|
|
827
|
+
logger.log("\n" + planManager.generatePlanSummary() + "\n");
|
|
828
|
+
}
|
|
829
|
+
} else if (subCmd === "approve" || subCmd === "yes") {
|
|
830
|
+
if (!planManager.isActive()) {
|
|
831
|
+
logger.info("No plan to approve.");
|
|
832
|
+
} else if (planManager.currentPlan.items.length === 0) {
|
|
833
|
+
logger.info(
|
|
834
|
+
"Plan has no items yet. Let the AI analyze the task first.",
|
|
835
|
+
);
|
|
836
|
+
} else {
|
|
837
|
+
planManager.approvePlan();
|
|
838
|
+
logger.success(
|
|
839
|
+
`Plan approved! ${planManager.currentPlan.items.length} items ready for execution.`,
|
|
840
|
+
);
|
|
841
|
+
logger.info(
|
|
842
|
+
"Write/execute tools are now unlocked. The AI can proceed.",
|
|
843
|
+
);
|
|
844
|
+
messages.push({
|
|
845
|
+
role: "system",
|
|
846
|
+
content: `[PLAN APPROVED] The user has approved your plan with ${planManager.currentPlan.items.length} items. You can now use all tools including write_file, edit_file, run_shell, and run_skill. Execute the plan items in order.`,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
} else if (subCmd === "reject" || subCmd === "no") {
|
|
850
|
+
if (!planManager.isActive()) {
|
|
851
|
+
logger.info("No plan to reject.");
|
|
852
|
+
} else {
|
|
853
|
+
planManager.rejectPlan("User rejected");
|
|
854
|
+
logger.info("Plan rejected. Exited plan mode.");
|
|
855
|
+
}
|
|
856
|
+
} else if (subCmd === "exit") {
|
|
857
|
+
if (planManager.isActive()) {
|
|
858
|
+
planManager.exitPlanMode({ savePlan: true });
|
|
859
|
+
logger.info("Exited plan mode.");
|
|
860
|
+
} else {
|
|
861
|
+
logger.info("Not in plan mode.");
|
|
862
|
+
}
|
|
863
|
+
} else {
|
|
864
|
+
logger.info(
|
|
865
|
+
"Unknown /plan subcommand. Try: /plan, /plan show, /plan approve, /plan reject, /plan exit",
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
prompt();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Add user message
|
|
874
|
+
messages.push({ role: "user", content: trimmed });
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
process.stdout.write("\n");
|
|
878
|
+
const response = await agentLoop(messages, {
|
|
879
|
+
provider,
|
|
880
|
+
model,
|
|
881
|
+
baseUrl,
|
|
882
|
+
apiKey,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
if (response) {
|
|
886
|
+
process.stdout.write(`\n${response}\n\n`);
|
|
887
|
+
messages.push({ role: "assistant", content: response });
|
|
888
|
+
} else {
|
|
889
|
+
process.stdout.write("\n");
|
|
890
|
+
}
|
|
891
|
+
} catch (err) {
|
|
892
|
+
logger.error(`Error: ${err.message}`);
|
|
893
|
+
|
|
894
|
+
// If connection error, provide helpful message
|
|
895
|
+
if (
|
|
896
|
+
err.message.includes("ECONNREFUSED") ||
|
|
897
|
+
err.message.includes("fetch failed")
|
|
898
|
+
) {
|
|
899
|
+
logger.info(`Make sure ${provider} is running at ${baseUrl}`);
|
|
900
|
+
if (provider === "ollama") {
|
|
901
|
+
logger.info("Start Ollama: ollama serve");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
prompt();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
rl.on("close", () => {
|
|
910
|
+
process.exit(0);
|
|
911
|
+
});
|
|
912
|
+
}
|