chainlesschain 0.40.2 → 0.41.0

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.
@@ -10,9 +10,9 @@
10
10
  * - run_shell: Execute a shell command
11
11
  * - search_files: Search for files by name/content
12
12
  * - list_dir: List directory contents
13
- * - db_query: Query the ChainlessChain database
14
- * - note_add: Add a note
15
- * - note_search: Search notes
13
+ * - run_skill: Run a built-in skill
14
+ * - list_skills: List available skills
15
+ * - run_code: Write and execute code (Python/Node.js/Bash)
16
16
  *
17
17
  * The AI decides which tools to call based on user intent.
18
18
  */
@@ -21,11 +21,8 @@ import readline from "readline";
21
21
  import chalk from "chalk";
22
22
  import fs from "fs";
23
23
  import path from "path";
24
- import { execSync } from "child_process";
25
- import { fileURLToPath } from "url";
26
24
  import { logger } from "../lib/logger.js";
27
25
  import { getPlanModeManager, PlanState } from "../lib/plan-mode.js";
28
- import { CLISkillLoader } from "../lib/skill-loader.js";
29
26
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
30
27
  import {
31
28
  createSession,
@@ -39,176 +36,15 @@ import {
39
36
  detectTaskType,
40
37
  selectModelForTask,
41
38
  } from "../lib/task-model-selector.js";
42
- import { executeHooks, HookEvents } from "../lib/hook-manager.js";
43
39
  import { CLIPermanentMemory } from "../lib/permanent-memory.js";
44
40
  import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
45
-
46
- /**
47
- * Tool definitions for function calling
48
- */
49
- const TOOLS = [
50
- {
51
- type: "function",
52
- function: {
53
- name: "read_file",
54
- description: "Read a file's content",
55
- parameters: {
56
- type: "object",
57
- properties: {
58
- path: { type: "string", description: "File path to read" },
59
- },
60
- required: ["path"],
61
- },
62
- },
63
- },
64
- {
65
- type: "function",
66
- function: {
67
- name: "write_file",
68
- description: "Write content to a file (create or overwrite)",
69
- parameters: {
70
- type: "object",
71
- properties: {
72
- path: { type: "string", description: "File path" },
73
- content: { type: "string", description: "File content" },
74
- },
75
- required: ["path", "content"],
76
- },
77
- },
78
- },
79
- {
80
- type: "function",
81
- function: {
82
- name: "edit_file",
83
- description: "Replace a specific string in a file with new content",
84
- parameters: {
85
- type: "object",
86
- properties: {
87
- path: { type: "string", description: "File path" },
88
- old_string: {
89
- type: "string",
90
- description: "Exact string to find and replace",
91
- },
92
- new_string: {
93
- type: "string",
94
- description: "Replacement string",
95
- },
96
- },
97
- required: ["path", "old_string", "new_string"],
98
- },
99
- },
100
- },
101
- {
102
- type: "function",
103
- function: {
104
- name: "run_shell",
105
- description:
106
- "Execute a shell command and return the output. Use for running tests, installing packages, git operations, etc.",
107
- parameters: {
108
- type: "object",
109
- properties: {
110
- command: { type: "string", description: "Shell command to execute" },
111
- cwd: {
112
- type: "string",
113
- description: "Working directory (optional)",
114
- },
115
- },
116
- required: ["command"],
117
- },
118
- },
119
- },
120
- {
121
- type: "function",
122
- function: {
123
- name: "search_files",
124
- description: "Search for files by name pattern or content",
125
- parameters: {
126
- type: "object",
127
- properties: {
128
- pattern: {
129
- type: "string",
130
- description: "Glob pattern or search string",
131
- },
132
- directory: {
133
- type: "string",
134
- description: "Directory to search in (default: cwd)",
135
- },
136
- content_search: {
137
- type: "boolean",
138
- description: "If true, search file contents instead of names",
139
- },
140
- },
141
- required: ["pattern"],
142
- },
143
- },
144
- },
145
- {
146
- type: "function",
147
- function: {
148
- name: "list_dir",
149
- description: "List contents of a directory",
150
- parameters: {
151
- type: "object",
152
- properties: {
153
- path: {
154
- type: "string",
155
- description: "Directory path (default: cwd)",
156
- },
157
- },
158
- },
159
- },
160
- },
161
- {
162
- type: "function",
163
- function: {
164
- name: "run_skill",
165
- description:
166
- "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.",
167
- parameters: {
168
- type: "object",
169
- properties: {
170
- skill_name: {
171
- type: "string",
172
- description:
173
- "Name of the skill to run (e.g. code-review, summarize, translate)",
174
- },
175
- input: {
176
- type: "string",
177
- description: "Input text or parameters for the skill",
178
- },
179
- },
180
- required: ["skill_name", "input"],
181
- },
182
- },
183
- },
184
- {
185
- type: "function",
186
- function: {
187
- name: "list_skills",
188
- description:
189
- "List available built-in skills, optionally filtered by category or keyword",
190
- parameters: {
191
- type: "object",
192
- properties: {
193
- category: {
194
- type: "string",
195
- description:
196
- "Filter by category (e.g. development, automation, data)",
197
- },
198
- query: {
199
- type: "string",
200
- description: "Search keyword to filter skills",
201
- },
202
- },
203
- },
204
- },
205
- },
206
- ];
207
-
208
- /**
209
- * Shared multi-layer skill loader
210
- */
211
- const skillLoader = new CLISkillLoader();
41
+ import {
42
+ AGENT_TOOLS,
43
+ getBaseSystemPrompt,
44
+ executeTool as coreExecuteTool,
45
+ agentLoop as coreAgentLoop,
46
+ formatToolArgs,
47
+ } from "../lib/agent-core.js";
212
48
 
213
49
  /**
214
50
  * Reference to the runtime DB for hook execution (set during startAgentRepl)
@@ -216,564 +52,43 @@ const skillLoader = new CLISkillLoader();
216
52
  let _hookDb = null;
217
53
 
218
54
  /**
219
- * Execute a tool call (with plan mode filtering and hook pipeline)
55
+ * Execute a tool call — delegates to agent-core with REPL's hookDb and cwd.
220
56
  */
221
57
  async function executeTool(name, args) {
222
- // Plan mode: check if tool is allowed
223
- const planManager = getPlanModeManager();
224
- if (planManager.isActive() && !planManager.isToolAllowed(name)) {
225
- // In plan mode, log the blocked tool as a plan item
226
- planManager.addPlanItem({
227
- title: `${name}: ${formatToolArgs(name, args)}`,
228
- tool: name,
229
- params: args,
230
- estimatedImpact:
231
- name === "run_shell"
232
- ? "high"
233
- : name === "write_file"
234
- ? "medium"
235
- : "low",
236
- });
237
- return {
238
- error: `[Plan Mode] Tool "${name}" is blocked during planning. It has been added to the plan. Use /plan approve to execute.`,
239
- };
240
- }
241
-
242
- // PreToolUse hook
243
- if (_hookDb) {
244
- try {
245
- await executeHooks(_hookDb, HookEvents.PreToolUse, {
246
- tool: name,
247
- args,
248
- timestamp: new Date().toISOString(),
249
- });
250
- } catch (_err) {
251
- // Hook failure should not block tool execution
252
- }
253
- }
254
-
255
- let toolResult;
256
- try {
257
- toolResult = await _executeToolInner(name, args);
258
- } catch (err) {
259
- // ToolError hook
260
- if (_hookDb) {
261
- try {
262
- await executeHooks(_hookDb, HookEvents.ToolError, {
263
- tool: name,
264
- args,
265
- error: err.message,
266
- });
267
- } catch (_err) {
268
- // Non-critical
269
- }
270
- }
271
- throw err;
272
- }
273
-
274
- // PostToolUse hook
275
- if (_hookDb) {
276
- try {
277
- await executeHooks(_hookDb, HookEvents.PostToolUse, {
278
- tool: name,
279
- args,
280
- result:
281
- typeof toolResult === "object"
282
- ? JSON.stringify(toolResult).substring(0, 500)
283
- : String(toolResult).substring(0, 500),
284
- });
285
- } catch (_err) {
286
- // Non-critical
287
- }
288
- }
289
-
290
- return toolResult;
291
- }
292
-
293
- /**
294
- * Inner tool execution logic (separated for hook wrapping)
295
- */
296
- async function _executeToolInner(name, args) {
297
- switch (name) {
298
- case "read_file": {
299
- const filePath = path.resolve(args.path);
300
- if (!fs.existsSync(filePath)) {
301
- return { error: `File not found: ${filePath}` };
302
- }
303
- const content = fs.readFileSync(filePath, "utf8");
304
- // Truncate very long files
305
- if (content.length > 50000) {
306
- return {
307
- content: content.substring(0, 50000) + "\n...(truncated)",
308
- size: content.length,
309
- };
310
- }
311
- return { content };
312
- }
313
-
314
- case "write_file": {
315
- const filePath = path.resolve(args.path);
316
- const dir = path.dirname(filePath);
317
- if (!fs.existsSync(dir)) {
318
- fs.mkdirSync(dir, { recursive: true });
319
- }
320
- fs.writeFileSync(filePath, args.content, "utf8");
321
- return { success: true, path: filePath, size: args.content.length };
322
- }
323
-
324
- case "edit_file": {
325
- const filePath = path.resolve(args.path);
326
- if (!fs.existsSync(filePath)) {
327
- return { error: `File not found: ${filePath}` };
328
- }
329
- const content = fs.readFileSync(filePath, "utf8");
330
- if (!content.includes(args.old_string)) {
331
- return { error: "old_string not found in file" };
332
- }
333
- const newContent = content.replace(args.old_string, args.new_string);
334
- fs.writeFileSync(filePath, newContent, "utf8");
335
- return { success: true, path: filePath };
336
- }
337
-
338
- case "run_shell": {
339
- try {
340
- const output = execSync(args.command, {
341
- cwd: args.cwd || process.cwd(),
342
- encoding: "utf8",
343
- timeout: 30000,
344
- maxBuffer: 1024 * 1024,
345
- });
346
- return { stdout: output.substring(0, 10000) };
347
- } catch (err) {
348
- return {
349
- error: err.message.substring(0, 2000),
350
- stderr: (err.stderr || "").substring(0, 2000),
351
- exitCode: err.status,
352
- };
353
- }
354
- }
355
-
356
- case "search_files": {
357
- const dir = args.directory ? path.resolve(args.directory) : process.cwd();
358
- try {
359
- if (args.content_search) {
360
- // Use grep/findstr for content search
361
- const cmd =
362
- process.platform === "win32"
363
- ? `findstr /s /i /n "${args.pattern}" *`
364
- : `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`;
365
- const output = execSync(cmd, {
366
- cwd: dir,
367
- encoding: "utf8",
368
- timeout: 10000,
369
- });
370
- return { matches: output.trim().split("\n").slice(0, 20) };
371
- } else {
372
- // File name search
373
- const cmd =
374
- process.platform === "win32"
375
- ? `dir /s /b *${args.pattern}* 2>NUL`
376
- : `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
377
- const output = execSync(cmd, {
378
- cwd: dir,
379
- encoding: "utf8",
380
- timeout: 10000,
381
- });
382
- return {
383
- files: output.trim().split("\n").filter(Boolean).slice(0, 20),
384
- };
385
- }
386
- } catch {
387
- return { files: [], message: "No matches found" };
388
- }
389
- }
390
-
391
- case "list_dir": {
392
- const dirPath = args.path ? path.resolve(args.path) : process.cwd();
393
- if (!fs.existsSync(dirPath)) {
394
- return { error: `Directory not found: ${dirPath}` };
395
- }
396
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
397
- return {
398
- entries: entries.map((e) => ({
399
- name: e.name,
400
- type: e.isDirectory() ? "dir" : "file",
401
- })),
402
- };
403
- }
404
-
405
- case "run_skill": {
406
- const allSkills = skillLoader.getResolvedSkills();
407
- if (allSkills.length === 0) {
408
- return {
409
- error:
410
- "No skills found. Make sure you're in the ChainlessChain project root or have skills installed.",
411
- };
412
- }
413
- const match = allSkills.find(
414
- (s) => s.id === args.skill_name || s.dirName === args.skill_name,
415
- );
416
- if (!match || !match.hasHandler) {
417
- return {
418
- error: `Skill "${args.skill_name}" not found or has no handler. Use list_skills to see available skills.`,
419
- };
420
- }
421
- try {
422
- const handlerPath = path.join(match.skillDir, "handler.js");
423
- const imported = await import(
424
- `file://${handlerPath.replace(/\\/g, "/")}`
425
- );
426
- const handler = imported.default || imported;
427
- if (handler.init) await handler.init(match);
428
- const task = {
429
- params: { input: args.input },
430
- input: args.input,
431
- action: args.input,
432
- };
433
- const context = {
434
- projectRoot: process.cwd(),
435
- workspacePath: process.cwd(),
436
- };
437
- const result = await handler.execute(task, context, match);
438
- return result;
439
- } catch (err) {
440
- return { error: `Skill execution failed: ${err.message}` };
441
- }
442
- }
443
-
444
- case "list_skills": {
445
- let skills = skillLoader.getResolvedSkills();
446
- if (skills.length === 0) {
447
- return { error: "No skills found." };
448
- }
449
- if (args.category) {
450
- skills = skills.filter(
451
- (s) => s.category.toLowerCase() === args.category.toLowerCase(),
452
- );
453
- }
454
- if (args.query) {
455
- const q = args.query.toLowerCase();
456
- skills = skills.filter(
457
- (s) =>
458
- s.id.includes(q) ||
459
- s.description.toLowerCase().includes(q) ||
460
- s.category.toLowerCase().includes(q),
461
- );
462
- }
463
- return {
464
- count: skills.length,
465
- skills: skills.map((s) => ({
466
- id: s.id,
467
- category: s.category,
468
- source: s.source,
469
- hasHandler: s.hasHandler,
470
- description: (s.description || "").substring(0, 80),
471
- })),
472
- };
473
- }
474
-
475
- default:
476
- return { error: `Unknown tool: ${name}` };
477
- }
58
+ return coreExecuteTool(name, args, { hookDb: _hookDb, cwd: process.cwd() });
478
59
  }
479
60
 
480
61
  /**
481
- * Send a chat completion request with tools.
482
- * Supports all 7 providers via cowork-adapter: ollama, anthropic, openai, deepseek, dashscope, gemini, mistral
483
- */
484
- async function chatWithTools(rawMessages, options) {
485
- const { provider, model, baseUrl, apiKey, contextEngine: ce } = options;
486
-
487
- // Build optimized messages via context engine (or use raw)
488
- // Find last user message for relevance matching (not tool/assistant)
489
- const lastUserMsg = [...rawMessages].reverse().find((m) => m.role === "user");
490
- const messages = ce
491
- ? ce.buildOptimizedMessages(rawMessages, {
492
- userQuery: lastUserMsg?.content,
493
- })
494
- : rawMessages;
495
-
496
- if (provider === "ollama") {
497
- // Ollama supports tool calling natively
498
- const response = await fetch(`${baseUrl}/api/chat`, {
499
- method: "POST",
500
- headers: { "Content-Type": "application/json" },
501
- body: JSON.stringify({
502
- model,
503
- messages,
504
- tools: TOOLS,
505
- stream: false,
506
- }),
507
- });
508
-
509
- if (!response.ok) {
510
- throw new Error(`Ollama error: ${response.status}`);
511
- }
512
-
513
- return await response.json();
514
- }
515
-
516
- if (provider === "anthropic") {
517
- // Anthropic: extract system messages, use tools format
518
- const key = apiKey || process.env.ANTHROPIC_API_KEY;
519
- if (!key) throw new Error("ANTHROPIC_API_KEY required");
520
-
521
- const systemMsgs = messages.filter((m) => m.role === "system");
522
- const otherMsgs = messages.filter((m) => m.role !== "system");
523
-
524
- // Convert TOOLS to Anthropic format
525
- const anthropicTools = TOOLS.map((t) => ({
526
- name: t.function.name,
527
- description: t.function.description,
528
- input_schema: t.function.parameters,
529
- }));
530
-
531
- const body = {
532
- model: model || "claude-sonnet-4-20250514",
533
- max_tokens: 4096,
534
- messages: otherMsgs,
535
- tools: anthropicTools,
536
- };
537
- if (systemMsgs.length > 0) {
538
- body.system = systemMsgs.map((m) => m.content).join("\n");
539
- }
540
-
541
- const url =
542
- baseUrl && baseUrl !== "http://localhost:11434"
543
- ? baseUrl
544
- : "https://api.anthropic.com/v1";
545
-
546
- const response = await fetch(`${url}/messages`, {
547
- method: "POST",
548
- headers: {
549
- "Content-Type": "application/json",
550
- "x-api-key": key,
551
- "anthropic-version": "2023-06-01",
552
- },
553
- body: JSON.stringify(body),
554
- });
555
-
556
- if (!response.ok) {
557
- throw new Error(`Anthropic error: ${response.status}`);
558
- }
559
-
560
- const data = await response.json();
561
- // Normalize Anthropic response to Ollama-like format
562
- return _normalizeAnthropicResponse(data);
563
- }
564
-
565
- // OpenAI-compatible providers (openai, deepseek, dashscope, mistral, gemini)
566
- const providerUrls = {
567
- openai: "https://api.openai.com/v1",
568
- deepseek: "https://api.deepseek.com/v1",
569
- dashscope: "https://dashscope.aliyuncs.com/compatible-mode/v1",
570
- mistral: "https://api.mistral.ai/v1",
571
- gemini: "https://generativelanguage.googleapis.com/v1beta/openai",
572
- volcengine: "https://ark.cn-beijing.volces.com/api/v3",
573
- };
574
-
575
- const providerApiKeyEnvs = {
576
- openai: "OPENAI_API_KEY",
577
- deepseek: "DEEPSEEK_API_KEY",
578
- dashscope: "DASHSCOPE_API_KEY",
579
- mistral: "MISTRAL_API_KEY",
580
- gemini: "GEMINI_API_KEY",
581
- volcengine: "VOLCENGINE_API_KEY",
582
- };
583
-
584
- const url =
585
- baseUrl && baseUrl !== "http://localhost:11434"
586
- ? baseUrl
587
- : providerUrls[provider];
588
-
589
- if (!url) {
590
- throw new Error(
591
- `Unsupported provider: ${provider}. Supported: ollama, anthropic, openai, deepseek, dashscope, mistral, gemini, volcengine`,
592
- );
593
- }
594
-
595
- const envKey = providerApiKeyEnvs[provider] || "OPENAI_API_KEY";
596
- const key = apiKey || process.env[envKey];
597
- if (!key) throw new Error(`${envKey} required for provider ${provider}`);
598
-
599
- const defaultModels = {
600
- openai: "gpt-4o-mini",
601
- deepseek: "deepseek-chat",
602
- dashscope: "qwen-turbo",
603
- mistral: "mistral-large-latest",
604
- gemini: "gemini-2.0-flash",
605
- volcengine: "doubao-seed-1-6-251015",
606
- };
607
-
608
- const response = await fetch(`${url}/chat/completions`, {
609
- method: "POST",
610
- headers: {
611
- "Content-Type": "application/json",
612
- Authorization: `Bearer ${key}`,
613
- },
614
- body: JSON.stringify({
615
- model: model || defaultModels[provider] || "gpt-4o-mini",
616
- messages,
617
- tools: TOOLS,
618
- }),
619
- });
620
-
621
- if (!response.ok) {
622
- throw new Error(`${provider} API error: ${response.status}`);
623
- }
624
-
625
- const data = await response.json();
626
- // Normalize to Ollama-like format
627
- if (!data.choices || !data.choices[0]) {
628
- throw new Error("Invalid API response: no choices returned");
629
- }
630
- const choice = data.choices[0];
631
- return {
632
- message: choice.message,
633
- };
634
- }
635
-
636
- /**
637
- * Normalize Anthropic API response to Ollama-like format for uniform handling
638
- */
639
- function _normalizeAnthropicResponse(data) {
640
- const content = data.content || [];
641
- const textBlocks = content.filter((b) => b.type === "text");
642
- const toolBlocks = content.filter((b) => b.type === "tool_use");
643
-
644
- const message = {
645
- role: "assistant",
646
- content: textBlocks.map((b) => b.text).join("\n") || "",
647
- };
648
-
649
- if (toolBlocks.length > 0) {
650
- message.tool_calls = toolBlocks.map((b) => ({
651
- id: b.id,
652
- type: "function",
653
- function: {
654
- name: b.name,
655
- arguments: JSON.stringify(b.input),
656
- },
657
- }));
658
- }
659
-
660
- return { message };
661
- }
662
-
663
- /**
664
- * Agentic loop - keeps calling tools until the AI gives a final text response
62
+ * Agentic loop wraps agent-core's async generator with REPL display output.
665
63
  */
666
64
  async function agentLoop(messages, options) {
667
- const MAX_ITERATIONS = 10;
668
-
669
- for (let i = 0; i < MAX_ITERATIONS; i++) {
670
- const result = await chatWithTools(messages, options);
671
- const msg = result?.message;
672
-
673
- if (!msg) {
674
- return "(No response from LLM)";
675
- }
676
-
677
- // Check for tool calls
678
- const toolCalls = msg.tool_calls;
679
-
680
- if (!toolCalls || toolCalls.length === 0) {
681
- // No tool calls — final text response
682
- return msg.content || "";
683
- }
684
-
685
- // Add assistant message with tool calls
686
- messages.push(msg);
687
-
688
- // Execute each tool call
689
- for (const call of toolCalls) {
690
- const fn = call.function;
691
- const toolName = fn.name;
692
- let toolArgs;
693
-
694
- try {
695
- toolArgs =
696
- typeof fn.arguments === "string"
697
- ? JSON.parse(fn.arguments)
698
- : fn.arguments;
699
- } catch {
700
- toolArgs = {};
701
- }
702
-
703
- // Show what the AI is doing
65
+ for await (const event of coreAgentLoop(messages, options)) {
66
+ if (event.type === "tool-executing") {
704
67
  process.stdout.write(
705
- chalk.gray(` [${toolName}] ${formatToolArgs(toolName, toolArgs)}\n`),
68
+ chalk.gray(
69
+ ` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
70
+ ),
706
71
  );
707
-
708
- const toolResult = await executeTool(toolName, toolArgs);
709
-
710
- // Show brief result
711
- if (toolResult.error) {
712
- process.stdout.write(chalk.red(` Error: ${toolResult.error}\n`));
713
- } else if (toolResult.success) {
72
+ } else if (event.type === "tool-result") {
73
+ if (event.error || event.result?.error) {
74
+ process.stdout.write(
75
+ chalk.red(` Error: ${event.error || event.result?.error}\n`),
76
+ );
77
+ } else if (event.result?.success) {
714
78
  process.stdout.write(chalk.green(` Done\n`));
715
79
  }
716
-
717
- // Add tool result to messages
718
- messages.push({
719
- role: "tool",
720
- content: JSON.stringify(toolResult).substring(0, 5000),
721
- tool_call_id: call.id,
722
- });
80
+ } else if (event.type === "response-complete") {
81
+ return event.content;
723
82
  }
724
83
  }
725
-
726
- return "(Reached max tool call iterations)";
727
- }
728
-
729
- /**
730
- * Format tool args for display
731
- */
732
- function formatToolArgs(name, args) {
733
- switch (name) {
734
- case "read_file":
735
- return args.path;
736
- case "write_file":
737
- return `${args.path} (${args.content?.length || 0} chars)`;
738
- case "edit_file":
739
- return args.path;
740
- case "run_shell":
741
- return args.command;
742
- case "search_files":
743
- return args.pattern;
744
- case "list_dir":
745
- return args.path || ".";
746
- case "run_skill":
747
- return `${args.skill_name}: ${(args.input || "").substring(0, 50)}`;
748
- case "list_skills":
749
- return args.category || args.query || "all";
750
- default:
751
- return JSON.stringify(args).substring(0, 60);
752
- }
753
- }
754
-
755
- function getBaseSystemPrompt() {
756
- return `You are ChainlessChain AI Assistant, a powerful agentic coding assistant running in the terminal.
757
-
758
- 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.
759
-
760
- Key behaviors:
761
- - When asked to modify code, read the file first, then edit it
762
- - When asked to create something, use write_file to create it
763
- - When asked to run/test something, use run_shell to execute it
764
- - When asked about files or code, use read_file and search_files to find information
765
- - You have multi-layer skills (built-in, marketplace, global, project-level) — use list_skills to discover them and run_skill to execute them
766
- - Always explain what you're doing and show results
767
- - Be concise but thorough
768
-
769
- Current working directory: ${process.cwd()}`;
84
+ return "";
770
85
  }
771
86
 
772
87
  /**
773
88
  * Start the agentic REPL
774
89
  */
775
90
  export async function startAgentRepl(options = {}) {
776
- let model = options.model || "qwen2:7b";
91
+ let model = options.model || "qwen2.5:7b";
777
92
  let provider = options.provider || "ollama";
778
93
  const baseUrl = options.baseUrl || "http://localhost:11434";
779
94
  const apiKey = options.apiKey || null;
@@ -834,7 +149,9 @@ export async function startAgentRepl(options = {}) {
834
149
  }
835
150
  }
836
151
 
837
- const messages = [{ role: "system", content: getBaseSystemPrompt() }];
152
+ const messages = [
153
+ { role: "system", content: getBaseSystemPrompt(process.cwd()) },
154
+ ];
838
155
 
839
156
  // Load resumed session messages
840
157
  if (db && options.sessionId && sessionId) {
@@ -1551,9 +868,59 @@ export async function startAgentRepl(options = {}) {
1551
868
  } else {
1552
869
  logger.info("Not in plan mode.");
1553
870
  }
871
+ } else if (subCmd.startsWith("interactive")) {
872
+ // Interactive planning with LLM-generated plan + skill recommendations
873
+ const planRequest =
874
+ subCmd.slice(11).trim() || "Help me with the current task";
875
+ try {
876
+ const { CLIInteractivePlanner } =
877
+ await import("../lib/interactive-planner.js");
878
+ const { TerminalInteractionAdapter } =
879
+ await import("../lib/interaction-adapter.js");
880
+ const chatFn = createChatFn({ provider, model, baseUrl, apiKey });
881
+ const planner = new CLIInteractivePlanner({
882
+ llmChat: chatFn,
883
+ db,
884
+ interaction: new TerminalInteractionAdapter(),
885
+ });
886
+
887
+ logger.info("Generating interactive plan...");
888
+ const result = await planner.startPlanSession(planRequest, {
889
+ cwd: process.cwd(),
890
+ });
891
+
892
+ if (result.plan) {
893
+ logger.log(
894
+ chalk.bold(
895
+ `\n Plan: ${result.plan.overview?.title || "Untitled"}`,
896
+ ),
897
+ );
898
+ logger.log(
899
+ chalk.gray(` ${result.plan.overview?.description || ""}\n`),
900
+ );
901
+ for (const step of result.plan.steps || []) {
902
+ const toolStr = step.tool ? chalk.cyan(` [${step.tool}]`) : "";
903
+ logger.log(` ${step.step}. ${step.title}${toolStr}`);
904
+ }
905
+ if (result.plan.recommendations?.skills?.length > 0) {
906
+ logger.log(chalk.bold("\n Recommended skills:"));
907
+ for (const s of result.plan.recommendations.skills) {
908
+ logger.log(` - ${chalk.cyan(s.id)}: ${s.description}`);
909
+ }
910
+ }
911
+ logger.log("");
912
+ logger.info(
913
+ "Use /plan interactive:confirm, /plan interactive:cancel, or /plan interactive:regenerate",
914
+ );
915
+ } else {
916
+ logger.info(result.message || "Failed to generate plan");
917
+ }
918
+ } catch (err) {
919
+ logger.error(`Interactive plan failed: ${err.message}`);
920
+ }
1554
921
  } else {
1555
922
  logger.info(
1556
- "Unknown /plan subcommand. Try: /plan, /plan show, /plan approve, /plan reject, /plan exit",
923
+ "Unknown /plan subcommand. Try: /plan, /plan show, /plan approve, /plan reject, /plan exit, /plan interactive <request>",
1557
924
  );
1558
925
  }
1559
926
 
@@ -1564,6 +931,36 @@ export async function startAgentRepl(options = {}) {
1564
931
  // Add user message
1565
932
  messages.push({ role: "user", content: trimmed });
1566
933
 
934
+ // Slot-filling: detect intent and fill missing parameters interactively
935
+ try {
936
+ const { CLISlotFiller } = await import("../lib/slot-filler.js");
937
+ const intent = CLISlotFiller.detectIntent(trimmed);
938
+ if (intent) {
939
+ const defs = CLISlotFiller.getSlotDefinitions(intent.type);
940
+ const missing = defs.required.filter((s) => !intent.entities[s]);
941
+ if (missing.length > 0) {
942
+ const { TerminalInteractionAdapter } =
943
+ await import("../lib/interaction-adapter.js");
944
+ const interaction = new TerminalInteractionAdapter();
945
+ const filler = new CLISlotFiller({ interaction });
946
+ const result = await filler.fillSlots(intent, {
947
+ cwd: process.cwd(),
948
+ });
949
+ if (result.filledSlots.length > 0) {
950
+ const parts = Object.entries(result.entities)
951
+ .filter(([, v]) => v)
952
+ .map(([k, v]) => `${k}: ${v}`);
953
+ // Append context to the last user message
954
+ const lastMsg = messages[messages.length - 1];
955
+ lastMsg.content += `\n\n[Context — user provided: ${parts.join(", ")}]`;
956
+ logger.info(chalk.gray(`[slot-fill] ${parts.join(", ")}`));
957
+ }
958
+ }
959
+ }
960
+ } catch (_err) {
961
+ // Slot-filling failure is non-critical
962
+ }
963
+
1567
964
  // Auto-select best model based on task type
1568
965
  let activeModel = model;
1569
966
  const taskDetection = detectTaskType(trimmed);