chainlesschain 0.42.2 → 0.43.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.
package/README.md CHANGED
@@ -9,6 +9,20 @@ npm install -g chainlesschain
9
9
  chainlesschain setup
10
10
  ```
11
11
 
12
+ After installation, three equivalent commands are available:
13
+
14
+ | Command | Description |
15
+ | ---------------- | ----------------------------------------------------------------- |
16
+ | `chainlesschain` | Full name |
17
+ | `cc` | Shortest alias, recommended for daily use |
18
+ | `clc` | ChainLessChain abbreviation, avoids `cc` conflict with C compiler |
19
+ | `clchain` | chainlesschain abbreviation, easy to recognize |
20
+
21
+ ```bash
22
+ cc setup # equivalent to: chainlesschain setup
23
+ clchain start # equivalent to: chainlesschain start
24
+ ```
25
+
12
26
  ## Requirements
13
27
 
14
28
  - **Node.js** >= 22.12.0
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.42.2",
3
+ "version": "0.43.0",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
7
- "chainlesschain": "./bin/chainlesschain.js"
7
+ "chainlesschain": "./bin/chainlesschain.js",
8
+ "cc": "./bin/chainlesschain.js",
9
+ "clc": "./bin/chainlesschain.js",
10
+ "clchain": "./bin/chainlesschain.js"
8
11
  },
9
12
  "main": "src/index.js",
10
13
  "scripts": {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { startAgentRepl } from "../repl/agent-repl.js";
10
+ import { loadConfig } from "../lib/config-manager.js";
10
11
 
11
12
  export function registerAgentCommand(program) {
12
13
  program
@@ -15,21 +16,21 @@ export function registerAgentCommand(program) {
15
16
  .description(
16
17
  "Start an agentic AI session (reads/writes files, runs commands)",
17
18
  )
18
- .option("--model <model>", "Model name", "qwen2:7b")
19
+ .option("--model <model>", "Model name")
19
20
  .option(
20
21
  "--provider <provider>",
21
22
  "LLM provider (ollama, openai, volcengine, deepseek, ...)",
22
- "ollama",
23
23
  )
24
24
  .option("--base-url <url>", "API base URL")
25
25
  .option("--api-key <key>", "API key")
26
26
  .option("--session <id>", "Resume a previous agent session")
27
27
  .action(async (options) => {
28
+ const config = loadConfig();
28
29
  await startAgentRepl({
29
- model: options.model,
30
- provider: options.provider,
31
- baseUrl: options.baseUrl,
32
- apiKey: options.apiKey,
30
+ model: options.model || config.llm?.model || "qwen2:7b",
31
+ provider: options.provider || config.llm?.provider || "ollama",
32
+ baseUrl: options.baseUrl || config.llm?.baseUrl,
33
+ apiKey: options.apiKey || config.llm?.apiKey,
33
34
  sessionId: options.session,
34
35
  });
35
36
  });
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Single-shot AI question command
3
- * chainlesschain ask "What is..." [--model qwen2:7b] [--provider ollama] [--json]
3
+ * chainlesschain ask "What is..." [--model] [--provider] [--json]
4
4
  */
5
5
 
6
6
  import ora from "ora";
7
7
  import chalk from "chalk";
8
8
  import { logger } from "../lib/logger.js";
9
9
  import { BUILT_IN_PROVIDERS } from "../lib/llm-providers.js";
10
+ import { loadConfig } from "../lib/config-manager.js";
10
11
 
11
12
  /**
12
13
  * Send a single question to an LLM provider
@@ -84,24 +85,25 @@ export function registerAskCommand(program) {
84
85
  .command("ask")
85
86
  .description("Ask a question to the AI (single-shot)")
86
87
  .argument("<question>", "The question to ask")
87
- .option("--model <model>", "Model name", "qwen2:7b")
88
+ .option("--model <model>", "Model name")
88
89
  .option(
89
90
  "--provider <provider>",
90
91
  "LLM provider (ollama, openai, volcengine, deepseek, ...)",
91
- "ollama",
92
92
  )
93
93
  .option("--base-url <url>", "API base URL")
94
94
  .option("--api-key <key>", "API key")
95
95
  .option("--json", "Output as JSON")
96
96
  .action(async (question, options) => {
97
+ const config = loadConfig();
98
+ const resolvedOptions = {
99
+ model: options.model || config.llm?.model || "qwen2:7b",
100
+ provider: options.provider || config.llm?.provider || "ollama",
101
+ baseUrl: options.baseUrl || config.llm?.baseUrl,
102
+ apiKey: options.apiKey || config.llm?.apiKey,
103
+ };
97
104
  const spinner = ora("Thinking...").start();
98
105
  try {
99
- const answer = await queryLLM(question, {
100
- model: options.model,
101
- provider: options.provider,
102
- baseUrl: options.baseUrl,
103
- apiKey: options.apiKey,
104
- });
106
+ const answer = await queryLLM(question, resolvedOptions);
105
107
 
106
108
  spinner.stop();
107
109
 
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * Interactive AI chat command
3
- * chainlesschain chat [--model qwen2:7b] [--provider ollama] [--agent]
3
+ * chainlesschain chat [--model] [--provider] [--agent]
4
4
  */
5
5
 
6
6
  import { startChatRepl } from "../repl/chat-repl.js";
7
7
  import { startAgentRepl } from "../repl/agent-repl.js";
8
+ import { loadConfig } from "../lib/config-manager.js";
8
9
 
9
10
  export function registerChatCommand(program) {
10
11
  program
11
12
  .command("chat")
12
13
  .description("Start an interactive AI chat session")
13
- .option("--model <model>", "Model name", "qwen2:7b")
14
+ .option("--model <model>", "Model name")
14
15
  .option(
15
16
  "--provider <provider>",
16
17
  "LLM provider (ollama, openai, volcengine, deepseek, ...)",
17
- "ollama",
18
18
  )
19
19
  .option("--base-url <url>", "API base URL")
20
20
  .option("--api-key <key>", "API key")
@@ -24,11 +24,12 @@ export function registerChatCommand(program) {
24
24
  )
25
25
  .option("--session <id>", "Resume a previous session (agent mode)")
26
26
  .action(async (options) => {
27
+ const config = loadConfig();
27
28
  const replOptions = {
28
- model: options.model,
29
- provider: options.provider,
30
- baseUrl: options.baseUrl,
31
- apiKey: options.apiKey,
29
+ model: options.model || config.llm?.model || "qwen2:7b",
30
+ provider: options.provider || config.llm?.provider || "ollama",
31
+ baseUrl: options.baseUrl || config.llm?.baseUrl,
32
+ apiKey: options.apiKey || config.llm?.apiKey,
32
33
  sessionId: options.session,
33
34
  };
34
35
 
@@ -82,7 +82,7 @@ const TEMPLATES = {
82
82
  skills: ["summarize"],
83
83
  persona: {
84
84
  name: "智能分诊助手",
85
- role: "你是一个医疗分诊AI助手,帮助诊所工作人员根据症状和紧急��度对患者进行优先级分类。",
85
+ role: "你是一个医疗分诊AI助手,帮助诊所工作人员根据症状和紧急程度对患者进行优先级分类。",
86
86
  behaviors: [
87
87
  "始终先询问患者症状再给出建议",
88
88
  "使用标准分诊分类 (ESI 1-5)",
@@ -8,7 +8,7 @@ import logger from "../lib/logger.js";
8
8
 
9
9
  async function selfUpdateCli(targetVersion) {
10
10
  if (VERSION === targetVersion) {
11
- return; // Already at the target version
11
+ return true; // Already at the target version
12
12
  }
13
13
 
14
14
  try {
@@ -17,12 +17,31 @@ async function selfUpdateCli(targetVersion) {
17
17
  encoding: "utf-8",
18
18
  stdio: "pipe",
19
19
  });
20
- logger.success(`CLI updated to v${targetVersion}`);
20
+ // Verify the update actually took effect
21
+ try {
22
+ const newVersion = execSync("chainlesschain --version", {
23
+ encoding: "utf-8",
24
+ stdio: "pipe",
25
+ }).trim();
26
+ if (newVersion === targetVersion) {
27
+ logger.success(`CLI updated to v${targetVersion}`);
28
+ return true;
29
+ }
30
+ logger.warn(
31
+ `CLI update ran but version is still ${newVersion}. Please run manually:\n npm install -g chainlesschain@${targetVersion}`,
32
+ );
33
+ return false;
34
+ } catch (_verifyErr) {
35
+ // Cannot verify, assume success
36
+ logger.success(`CLI updated to v${targetVersion}`);
37
+ return true;
38
+ }
21
39
  } catch (_err) {
22
40
  // npm global install may fail due to permissions; guide the user
23
41
  logger.warn(
24
42
  `CLI self-update failed. Please run manually:\n npm install -g chainlesschain@${targetVersion}`,
25
43
  );
44
+ return false;
26
45
  }
27
46
  }
28
47
 
@@ -85,11 +104,21 @@ export function registerUpdateCommand(program) {
85
104
  }
86
105
 
87
106
  await downloadRelease(result.latestVersion, { force: options.force });
107
+ logger.success("Application already installed");
88
108
 
89
109
  // Self-update the CLI npm package
90
- await selfUpdateCli(result.latestVersion);
110
+ const cliUpdated = await selfUpdateCli(result.latestVersion);
91
111
 
92
- logger.success(`Updated to v${result.latestVersion}`);
112
+ if (cliUpdated) {
113
+ logger.success(`Updated to v${result.latestVersion}`);
114
+ } else {
115
+ logger.warn(
116
+ `Application binary updated, but CLI version remains at ${VERSION}.`,
117
+ );
118
+ logger.info(
119
+ `To complete the update, run:\n npm install -g chainlesschain@${result.latestVersion}`,
120
+ );
121
+ }
93
122
  logger.info("Restart ChainlessChain to use the new version.");
94
123
  } catch (err) {
95
124
  if (err.name === "ExitPromptError") {
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import crypto from "crypto";
9
+ import { SubAgentContext } from "./sub-agent-context.js";
9
10
 
10
11
  /**
11
12
  * Keyword map for agent type detection.
@@ -271,3 +272,113 @@ export function estimateComplexity(task) {
271
272
  estimatedSubtasks: Math.max(1, matchedTypes),
272
273
  };
273
274
  }
275
+
276
+ // ─── Role-based tool whitelist ──────────────────────────────────────────
277
+
278
+ export const ROLE_TOOL_WHITELIST = {
279
+ "code-review": ["read_file", "search_files", "list_dir"],
280
+ "code-generation": [
281
+ "read_file",
282
+ "write_file",
283
+ "edit_file",
284
+ "run_shell",
285
+ "search_files",
286
+ "list_dir",
287
+ ],
288
+ "data-analysis": [
289
+ "read_file",
290
+ "search_files",
291
+ "list_dir",
292
+ "run_code",
293
+ "run_shell",
294
+ ],
295
+ document: ["read_file", "write_file", "search_files", "list_dir"],
296
+ testing: [
297
+ "read_file",
298
+ "write_file",
299
+ "edit_file",
300
+ "run_shell",
301
+ "search_files",
302
+ "list_dir",
303
+ "run_code",
304
+ ],
305
+ general: null, // all tools
306
+ };
307
+
308
+ /**
309
+ * Execute a decomposed task using isolated sub-agent contexts.
310
+ * Each subtask gets its own SubAgentContext with role-appropriate tool whitelist.
311
+ *
312
+ * @param {{ taskId: string, subtasks: Array }} decomposition - From decomposeTask()
313
+ * @param {object} [options]
314
+ * @param {string} [options.cwd] - Working directory
315
+ * @param {object} [options.db] - Database instance
316
+ * @param {object} [options.llmOptions] - LLM provider options
317
+ * @param {string} [options.parentContext] - Condensed context from parent
318
+ * @returns {Promise<{ taskId: string, status: string, results: Array, summary: string }>}
319
+ */
320
+ export async function executeDecomposedTask(decomposition, options = {}) {
321
+ const { subtasks } = decomposition;
322
+ if (!subtasks || subtasks.length === 0) {
323
+ return {
324
+ taskId: decomposition.taskId,
325
+ status: "empty",
326
+ results: [],
327
+ summary: "No subtasks to execute",
328
+ };
329
+ }
330
+
331
+ const maxConcurrency = options.maxConcurrency || 3;
332
+
333
+ // Run subtasks in parallel batches with concurrency limit
334
+ const results = [];
335
+ for (let i = 0; i < subtasks.length; i += maxConcurrency) {
336
+ const batch = subtasks.slice(i, i + maxConcurrency);
337
+ const batchPromises = batch.map(async (subtask) => {
338
+ const allowedTools = ROLE_TOOL_WHITELIST[subtask.agentType] || null;
339
+
340
+ const subCtx = SubAgentContext.create({
341
+ role: subtask.agentType,
342
+ task: subtask.description,
343
+ inheritedContext: options.parentContext || null,
344
+ allowedTools,
345
+ cwd: options.cwd || process.cwd(),
346
+ db: options.db || null,
347
+ llmOptions: options.llmOptions || {},
348
+ });
349
+
350
+ try {
351
+ const result = await subCtx.run(subtask.description);
352
+ subtask.status = "completed";
353
+ subtask.result = result.summary;
354
+ return {
355
+ id: subtask.id,
356
+ agentType: subtask.agentType,
357
+ status: "completed",
358
+ summary: result.summary,
359
+ toolsUsed: result.toolsUsed,
360
+ };
361
+ } catch (err) {
362
+ subtask.status = "failed";
363
+ subtask.result = err.message;
364
+ return {
365
+ id: subtask.id,
366
+ agentType: subtask.agentType,
367
+ status: "failed",
368
+ error: err.message,
369
+ };
370
+ }
371
+ });
372
+
373
+ const batchResults = await Promise.all(batchPromises);
374
+ results.push(...batchResults);
375
+ }
376
+
377
+ const aggregated = aggregateResults(subtasks);
378
+ return {
379
+ taskId: decomposition.taskId,
380
+ status: aggregated.status,
381
+ results,
382
+ summary: aggregated.summary,
383
+ };
384
+ }
@@ -23,6 +23,7 @@ import { CLISkillLoader } from "./skill-loader.js";
23
23
  import { executeHooks, HookEvents } from "./hook-manager.js";
24
24
  import { detectPython } from "./cli-anything-bridge.js";
25
25
  import { findProjectRoot, loadProjectConfig } from "./project-detector.js";
26
+ import { SubAgentContext } from "./sub-agent-context.js";
26
27
 
27
28
  // ─── Tool definitions ────────────────────────────────────────────────────
28
29
 
@@ -212,6 +213,40 @@ export const AGENT_TOOLS = [
212
213
  },
213
214
  },
214
215
  },
216
+ {
217
+ type: "function",
218
+ function: {
219
+ name: "spawn_sub_agent",
220
+ description:
221
+ "Spawn an isolated sub-agent to handle a subtask. The sub-agent has its own context and message history, and only returns a summary result. Use this for tasks that benefit from focused, independent execution (e.g. code review, summarization, translation).",
222
+ parameters: {
223
+ type: "object",
224
+ properties: {
225
+ role: {
226
+ type: "string",
227
+ description:
228
+ "Sub-agent role (e.g. code-review, summarizer, translator, debugger)",
229
+ },
230
+ task: {
231
+ type: "string",
232
+ description: "Task description for the sub-agent",
233
+ },
234
+ context: {
235
+ type: "string",
236
+ description:
237
+ "Optional condensed context from the parent agent to pass to the sub-agent",
238
+ },
239
+ tools: {
240
+ type: "array",
241
+ items: { type: "string" },
242
+ description:
243
+ 'Optional tool whitelist for the sub-agent (e.g. ["read_file", "search_files"]). If omitted, all tools are available.',
244
+ },
245
+ },
246
+ required: ["role", "task"],
247
+ },
248
+ },
249
+ },
215
250
  ];
216
251
 
217
252
  // ─── Shared skill loader ──────────────────────────────────────────────────
@@ -326,6 +361,16 @@ When the user's problem involves data processing, calculations, file operations,
326
361
 
327
362
  You are not just a chatbot — you are a capable coding agent. Think step by step, write code when needed, and deliver real results.
328
363
 
364
+ ## Sub-Agent Isolation
365
+ When a task involves multiple distinct roles (e.g. code review + code generation), or when you need
366
+ focused analysis without polluting your current context, use the spawn_sub_agent tool. Examples:
367
+ - Code review as a separate perspective while you're implementing
368
+ - Summarizing a large file before incorporating it into your response
369
+ - Running a focused analysis (security, performance) on specific code
370
+ - Translating or reformatting content independently
371
+ The sub-agent has its own message history and only returns a summary — your context stays clean.
372
+ Do NOT spawn sub-agents for trivial tasks that you can handle directly.
373
+
329
374
  ## Environment
330
375
  ${envLines.join("\n")}
331
376
 
@@ -512,7 +557,11 @@ export async function executeTool(name, args, context = {}) {
512
557
 
513
558
  let toolResult;
514
559
  try {
515
- toolResult = await executeToolInner(name, args, { skillLoader, cwd });
560
+ toolResult = await executeToolInner(name, args, {
561
+ skillLoader,
562
+ cwd,
563
+ parentMessages: context.parentMessages,
564
+ });
516
565
  } catch (err) {
517
566
  if (hookDb) {
518
567
  try {
@@ -550,7 +599,11 @@ export async function executeTool(name, args, context = {}) {
550
599
  /**
551
600
  * Inner tool execution — no hooks, no plan-mode checks.
552
601
  */
553
- async function executeToolInner(name, args, { skillLoader, cwd }) {
602
+ async function executeToolInner(
603
+ name,
604
+ args,
605
+ { skillLoader, cwd, parentMessages },
606
+ ) {
554
607
  switch (name) {
555
608
  case "read_file": {
556
609
  const filePath = path.resolve(cwd, args.path);
@@ -613,6 +666,10 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
613
666
  return _executeRunCode(args, cwd);
614
667
  }
615
668
 
669
+ case "spawn_sub_agent": {
670
+ return _executeSpawnSubAgent(args, { skillLoader, cwd, parentMessages });
671
+ }
672
+
616
673
  case "search_files": {
617
674
  const dir = args.directory ? path.resolve(cwd, args.directory) : cwd;
618
675
  try {
@@ -676,6 +733,31 @@ async function executeToolInner(name, args, { skillLoader, cwd }) {
676
733
  error: `Skill "${args.skill_name}" not found or has no handler. Use list_skills to see available skills.`,
677
734
  };
678
735
  }
736
+
737
+ // Check if skill requests isolation (via SKILL.md frontmatter)
738
+ const skillIsolation = match.isolation === true;
739
+ if (skillIsolation) {
740
+ // Run skill through isolated sub-agent context
741
+ const subCtx = SubAgentContext.create({
742
+ role: `skill-${args.skill_name}`,
743
+ task: `Execute the "${args.skill_name}" skill with input: ${(args.input || "").substring(0, 200)}`,
744
+ allowedTools: ["read_file", "search_files", "list_dir"],
745
+ cwd,
746
+ });
747
+ try {
748
+ const result = await subCtx.run(args.input);
749
+ return {
750
+ success: true,
751
+ isolated: true,
752
+ skill: args.skill_name,
753
+ summary: result.summary,
754
+ toolsUsed: result.toolsUsed,
755
+ };
756
+ } catch (err) {
757
+ return { error: `Isolated skill execution failed: ${err.message}` };
758
+ }
759
+ }
760
+
679
761
  try {
680
762
  const handlerPath = path.join(match.skillDir, "handler.js");
681
763
  const imported = await import(
@@ -953,6 +1035,86 @@ async function _executeRunCode(args, cwd) {
953
1035
  }
954
1036
  }
955
1037
 
1038
+ // ─── spawn_sub_agent implementation ──────────────────────────────────────
1039
+
1040
+ /**
1041
+ * Execute a spawn_sub_agent tool call.
1042
+ * Creates an isolated SubAgentContext, runs it, and returns only the summary.
1043
+ *
1044
+ * @param {object} args - { role, task, context?, tools? }
1045
+ * @param {object} ctx - { skillLoader, cwd }
1046
+ * @returns {Promise<object>}
1047
+ */
1048
+ async function _executeSpawnSubAgent(args, ctx) {
1049
+ const { role, task, context: inheritedContext, tools: allowedTools } = args;
1050
+
1051
+ if (!role || !task) {
1052
+ return { error: "Both 'role' and 'task' are required for spawn_sub_agent" };
1053
+ }
1054
+
1055
+ // Auto-condense parent context if caller didn't provide explicit context
1056
+ let resolvedContext = inheritedContext || null;
1057
+ if (!resolvedContext && Array.isArray(ctx.parentMessages)) {
1058
+ const recentMsgs = ctx.parentMessages
1059
+ .filter((m) => m.role === "assistant" && typeof m.content === "string")
1060
+ .slice(-3)
1061
+ .map((m) => m.content.substring(0, 200));
1062
+ if (recentMsgs.length > 0) {
1063
+ resolvedContext = recentMsgs.join("\n---\n");
1064
+ }
1065
+ }
1066
+
1067
+ const subCtx = SubAgentContext.create({
1068
+ role,
1069
+ task,
1070
+ inheritedContext: resolvedContext,
1071
+ allowedTools: allowedTools || null,
1072
+ cwd: ctx.cwd,
1073
+ });
1074
+
1075
+ try {
1076
+ // Notify registry if available
1077
+ const { SubAgentRegistry } = await import("./sub-agent-registry.js").catch(
1078
+ () => ({ SubAgentRegistry: null }),
1079
+ );
1080
+ if (SubAgentRegistry) {
1081
+ try {
1082
+ SubAgentRegistry.getInstance().register(subCtx);
1083
+ } catch (_err) {
1084
+ // Registry not available — non-critical
1085
+ }
1086
+ }
1087
+
1088
+ const result = await subCtx.run(task);
1089
+
1090
+ // Complete in registry
1091
+ if (SubAgentRegistry) {
1092
+ try {
1093
+ SubAgentRegistry.getInstance().complete(subCtx.id, result);
1094
+ } catch (_err) {
1095
+ // Non-critical
1096
+ }
1097
+ }
1098
+
1099
+ return {
1100
+ success: true,
1101
+ subAgentId: subCtx.id,
1102
+ role: subCtx.role,
1103
+ summary: result.summary,
1104
+ toolsUsed: result.toolsUsed,
1105
+ iterationCount: result.iterationCount,
1106
+ artifactCount: result.artifacts.length,
1107
+ };
1108
+ } catch (err) {
1109
+ subCtx.forceComplete(err.message);
1110
+ return {
1111
+ error: `Sub-agent failed: ${err.message}`,
1112
+ subAgentId: subCtx.id,
1113
+ role: subCtx.role,
1114
+ };
1115
+ }
1116
+ }
1117
+
956
1118
  // ─── LLM chat with tools ─────────────────────────────────────────────────
957
1119
 
958
1120
  /**
@@ -1157,6 +1319,7 @@ export async function* agentLoop(messages, options) {
1157
1319
  hookDb: options.hookDb || null,
1158
1320
  skillLoader: options.skillLoader || _defaultSkillLoader,
1159
1321
  cwd: options.cwd || process.cwd(),
1322
+ parentMessages: messages, // pass parent messages for sub-agent auto-condensation
1160
1323
  };
1161
1324
 
1162
1325
  // ── Slot-filling phase ──────────────────────────────────────────────
@@ -1292,6 +1455,8 @@ export function formatToolArgs(name, args) {
1292
1455
  return args.category || args.query || "all";
1293
1456
  case "run_code":
1294
1457
  return `${args.language} (${(args.code || "").length} chars)`;
1458
+ case "spawn_sub_agent":
1459
+ return `[${args.role}] ${(args.task || "").substring(0, 60)}`;
1295
1460
  default:
1296
1461
  return JSON.stringify(args).substring(0, 60);
1297
1462
  }