jinzd-ai-cli 0.4.212 → 0.4.214

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.
@@ -5,10 +5,10 @@ import {
5
5
  } from "./chunk-T2NL5ZIA.js";
6
6
  import {
7
7
  runTestsTool
8
- } from "./chunk-HX6N6QEY.js";
8
+ } from "./chunk-TR6MSPSS.js";
9
9
  import {
10
10
  runTool
11
- } from "./chunk-IFZX26K7.js";
11
+ } from "./chunk-YMFGGCUP.js";
12
12
  import {
13
13
  getDangerLevel,
14
14
  isFileWriteTool,
@@ -26,7 +26,7 @@ import {
26
26
  SUBAGENT_ALLOWED_TOOLS,
27
27
  SUBAGENT_DEFAULT_MAX_ROUNDS,
28
28
  SUBAGENT_MAX_ROUNDS_LIMIT
29
- } from "./chunk-GBBVCZ4W.js";
29
+ } from "./chunk-DBYMX2ZC.js";
30
30
  import {
31
31
  fileCheckpoints
32
32
  } from "./chunk-4BKXL7SM.js";
@@ -1055,7 +1055,7 @@ import { dirname as dirname2 } from "path";
1055
1055
 
1056
1056
  // src/tools/executor.ts
1057
1057
  import chalk3 from "chalk";
1058
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1058
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1059
1059
  import { tmpdir } from "os";
1060
1060
 
1061
1061
  // src/core/readline-internal.ts
@@ -1860,6 +1860,157 @@ var theme = new Proxy(DARK_THEME, {
1860
1860
  }
1861
1861
  });
1862
1862
 
1863
+ // src/tools/action-classifier.ts
1864
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1865
+ import { isAbsolute as isAbsolute2, join as join2, resolve as resolve4 } from "path";
1866
+ var RECENT_DENIED_LIMIT = 50;
1867
+ var recentlyDenied = [];
1868
+ function recordRecentlyDeniedAutoAction(call, classification) {
1869
+ recentlyDenied.unshift({
1870
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1871
+ tool: call.name,
1872
+ reason: classification.reason,
1873
+ ruleId: classification.ruleId,
1874
+ argsPreview: JSON.stringify(call.arguments).slice(0, 240)
1875
+ });
1876
+ recentlyDenied.splice(RECENT_DENIED_LIMIT);
1877
+ }
1878
+ function getRecentlyDeniedAutoActions() {
1879
+ return [...recentlyDenied];
1880
+ }
1881
+ function clearRecentlyDeniedAutoActions() {
1882
+ recentlyDenied.splice(0, recentlyDenied.length);
1883
+ }
1884
+ function normalizePath(value) {
1885
+ return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
1886
+ }
1887
+ function isInside(root, target) {
1888
+ const absoluteRoot = resolve4(root);
1889
+ const absoluteTarget = isAbsolute2(target) ? target : resolve4(root, target);
1890
+ const base = normalizePath(absoluteRoot);
1891
+ const t = normalizePath(absoluteTarget);
1892
+ return t === base || t.startsWith(base + "/");
1893
+ }
1894
+ function getPathArg(call) {
1895
+ const raw = call.arguments.path ?? call.arguments.filePath ?? call.arguments.outputPath ?? call.arguments.file;
1896
+ return typeof raw === "string" && raw.trim() ? raw.trim() : null;
1897
+ }
1898
+ function splitShellWords(command) {
1899
+ const out = [];
1900
+ const re = /"([^"]*)"|'([^']*)'|([^\s]+)/g;
1901
+ let m;
1902
+ while (m = re.exec(command)) out.push(m[1] ?? m[2] ?? m[3] ?? "");
1903
+ return out.filter(Boolean);
1904
+ }
1905
+ function readPackageManifest(root) {
1906
+ const p = join2(root, "package.json");
1907
+ if (!existsSync5(p)) return null;
1908
+ try {
1909
+ return JSON.parse(readFileSync4(p, "utf-8"));
1910
+ } catch {
1911
+ return null;
1912
+ }
1913
+ }
1914
+ function manifestHasDependency(root, name) {
1915
+ const manifest = readPackageManifest(root);
1916
+ if (!manifest) return false;
1917
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
1918
+ const deps = manifest[key];
1919
+ if (deps && typeof deps === "object" && Object.prototype.hasOwnProperty.call(deps, name)) return true;
1920
+ }
1921
+ return false;
1922
+ }
1923
+ function lockfileMentions(root, name) {
1924
+ for (const file of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
1925
+ const p = join2(root, file);
1926
+ if (!existsSync5(p)) continue;
1927
+ try {
1928
+ if (readFileSync4(p, "utf-8").includes(name)) return true;
1929
+ } catch {
1930
+ }
1931
+ }
1932
+ return false;
1933
+ }
1934
+ function getInstallPackages(command) {
1935
+ const words = splitShellWords(command);
1936
+ const cmd = words[0]?.toLowerCase();
1937
+ const sub = words[1]?.toLowerCase();
1938
+ if (!cmd || !sub) return [];
1939
+ const isInstall = cmd === "npm" && (sub === "install" || sub === "i" || sub === "add") || (cmd === "pnpm" || cmd === "yarn") && (sub === "add" || sub === "install");
1940
+ if (!isInstall) return [];
1941
+ return words.slice(2).filter((w) => !w.startsWith("-")).filter((w) => w !== "." && w !== "./").map((w) => w.replace(/^@([^/]+)\/([^@]+)(?:@.+)?$/, "@$1/$2").replace(/^([^@]+)@.+$/, "$1"));
1942
+ }
1943
+ function isPlainGitPush(command) {
1944
+ if (!/^\s*git\s+push\b/i.test(command)) return false;
1945
+ if (/--force|-f\b|\+[^\s]+/i.test(command)) return false;
1946
+ return !/[;&|`$<>\n\r]/.test(command);
1947
+ }
1948
+ function hasSecretExfiltrationShape(command) {
1949
+ return /(?:\.env|config\.json|id_rsa|id_ed25519|AWS_SECRET|AICLI_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY)/i.test(command) && /\b(curl|wget|scp|rsync|nc|netcat|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command);
1950
+ }
1951
+ var RuleBasedActionClassifier = class {
1952
+ classify(call, dangerLevel, ctx) {
1953
+ if (call.name === "bash") return this.classifyShell(call, dangerLevel, ctx);
1954
+ if (call.name === "web_fetch" || call.name === "web_search" || call.name === "google_search") {
1955
+ return { decision: "auto-approve", reason: "read-only HTTP tool", ruleId: "auto.readonly-http", confidence: "high" };
1956
+ }
1957
+ if (dangerLevel === "safe") {
1958
+ return { decision: "auto-approve", reason: "safe read-only tool", ruleId: "auto.safe-tool", confidence: "high" };
1959
+ }
1960
+ if (dangerLevel === "write") {
1961
+ const p = getPathArg(call);
1962
+ if (p && (isInside(ctx.workspaceRoot, p) || (ctx.tempDirs ?? []).some((root) => isInside(root, p)))) {
1963
+ return { decision: "auto-approve", reason: "explicit write inside workspace/temp roots", ruleId: "auto.workspace-write", confidence: "high" };
1964
+ }
1965
+ return { decision: "confirm", reason: "write action is not proven low-risk", ruleId: "auto.write-confirm", confidence: "medium" };
1966
+ }
1967
+ return { decision: "confirm", reason: "destructive actions require confirmation", ruleId: "auto.destructive-confirm", confidence: "high" };
1968
+ }
1969
+ classifyShell(call, dangerLevel, ctx) {
1970
+ const command = String(call.arguments.command ?? "");
1971
+ if (/\bcurl\b[^\n|;&]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/i.test(command)) {
1972
+ return { decision: "deny", reason: "curl pipe shell execution is blocked in auto mode", ruleId: "deny.curl-pipe-shell", confidence: "high" };
1973
+ }
1974
+ if (/\bgit\s+push\b/i.test(command) && /--force|-f\b|\+[^\s]+/i.test(command)) {
1975
+ return { decision: "deny", reason: "force push is blocked in auto mode", ruleId: "deny.force-push", confidence: "high" };
1976
+ }
1977
+ if (hasSecretExfiltrationShape(command)) {
1978
+ return { decision: "deny", reason: "possible secret exfiltration is blocked in auto mode", ruleId: "deny.secret-exfiltration", confidence: "high" };
1979
+ }
1980
+ if (/\b(terraform|tofu)\s+destroy\b|\bkubectl\s+delete\b|\bcdk\s+destroy\b/i.test(command)) {
1981
+ return { decision: "confirm", reason: "infrastructure destroy requires confirmation", ruleId: "confirm.iac-destroy", confidence: "high" };
1982
+ }
1983
+ if (/\b(prisma|sequelize|knex|typeorm)\b[^\n]*\b(migrate|migration)\b|\bdb:migrate\b/i.test(command)) {
1984
+ return { decision: "confirm", reason: "database migration requires confirmation", ruleId: "confirm.database-migration", confidence: "high" };
1985
+ }
1986
+ if (/\b(vercel|netlify|flyctl|railway|wrangler|serverless|firebase)\b[^\n]*\bdeploy\b|\bdeploy\b[^\n]*\b(prod|production)\b/i.test(command)) {
1987
+ return { decision: "confirm", reason: "production deployment requires confirmation", ruleId: "confirm.production-deploy", confidence: "high" };
1988
+ }
1989
+ if (/\b(iam|policy|token|secret|api[-_]?key|credential|permission|role)\b/i.test(command)) {
1990
+ return { decision: "confirm", reason: "credential or permission change requires confirmation", ruleId: "confirm.credentials-permissions", confidence: "medium" };
1991
+ }
1992
+ if (/\b(npx|pnpm\s+dlx|yarn\s+dlx)\s+(claude|codex|aider|cursor-agent|gemini)\b/i.test(command)) {
1993
+ return { decision: "confirm", reason: "third-party agent loop requires confirmation", ruleId: "confirm.third-party-agent", confidence: "high" };
1994
+ }
1995
+ if (isPlainGitPush(command)) {
1996
+ return { decision: "auto-approve", reason: "plain git push without force", ruleId: "auto.git-push", confidence: "high" };
1997
+ }
1998
+ const pkgs = getInstallPackages(command);
1999
+ if (pkgs.length > 0) {
2000
+ const allDeclared = pkgs.every((p) => manifestHasDependency(ctx.workspaceRoot, p) || lockfileMentions(ctx.workspaceRoot, p));
2001
+ return allDeclared ? { decision: "auto-approve", reason: "installing dependencies already declared in manifest/lockfile", ruleId: "auto.declared-dependency-install", confidence: "medium" } : { decision: "confirm", reason: "dependency install includes undeclared packages", ruleId: "confirm.undeclared-dependency-install", confidence: "medium" };
2002
+ }
2003
+ if (/\b(curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command) && !/[>|;&`$]/.test(command)) {
2004
+ return { decision: "auto-approve", reason: "read-only HTTP shell command", ruleId: "auto.readonly-http-shell", confidence: "medium" };
2005
+ }
2006
+ if (dangerLevel === "safe") {
2007
+ return { decision: "auto-approve", reason: "safe shell action", ruleId: "auto.safe-shell", confidence: "medium" };
2008
+ }
2009
+ return { decision: "confirm", reason: "shell action is not proven low-risk", ruleId: "confirm.shell-default", confidence: "medium" };
2010
+ }
2011
+ };
2012
+ var defaultActionClassifier = new RuleBasedActionClassifier();
2013
+
1863
2014
  // src/tools/executor.ts
1864
2015
  function buildErrorPreview(content) {
1865
2016
  const meaningful = content.split("\n").filter(
@@ -1899,6 +2050,8 @@ var ToolExecutor = class {
1899
2050
  * destructive 操作仍然必须逐次确认。
1900
2051
  */
1901
2052
  sessionAutoApprove = false;
2053
+ /** Session-level Auto Mode: rule-based low-risk auto approval. */
2054
+ sessionAutoMode = false;
1902
2055
  /**
1903
2056
  * Logical session key used to scope per-session state in stateful tools
1904
2057
  * (currently only `bash`'s persistent cwd). Web mode sets this per-tab so
@@ -2058,6 +2211,40 @@ var ToolExecutor = class {
2058
2211
  };
2059
2212
  }
2060
2213
  }
2214
+ if (this.sessionAutoMode && networkPermission?.action !== "confirm") {
2215
+ const classification = defaultActionClassifier.classify(call, dangerLevel, {
2216
+ workspaceRoot: this.workspaceRoot,
2217
+ tempDirs: [tmpdir()]
2218
+ });
2219
+ if (classification.decision === "deny") {
2220
+ recordRecentlyDeniedAutoAction(call, classification);
2221
+ return {
2222
+ callId: call.id,
2223
+ content: `[Auto denied] ${classification.reason}. Do not retry without asking.`,
2224
+ isError: true
2225
+ };
2226
+ }
2227
+ if (classification.decision === "auto-approve") {
2228
+ this.printToolCall(call);
2229
+ if (dangerLevel === "write") this.printDiffPreview(call);
2230
+ console.log(theme.warning(` \u26A1 Auto-approved (/auto: ${classification.ruleId})`));
2231
+ try {
2232
+ const rawContent = await runTool(tool, call.arguments, call.name);
2233
+ const content = truncateOutput(rawContent, call.name);
2234
+ const wasTruncated = content !== rawContent;
2235
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
2236
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2237
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
2238
+ return { callId: call.id, content, isError: false };
2239
+ } catch (err) {
2240
+ const message = err instanceof Error ? err.message : String(err);
2241
+ this.printToolResult(call.name, message, true, false);
2242
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2243
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
2244
+ return { callId: call.id, content: message, isError: true };
2245
+ }
2246
+ }
2247
+ }
2061
2248
  if (this.sessionAutoApprove && dangerLevel === "write") {
2062
2249
  this.printToolCall(call);
2063
2250
  if (dangerLevel === "write") this.printDiffPreview(call);
@@ -2225,7 +2412,7 @@ var ToolExecutor = class {
2225
2412
  rl.resume();
2226
2413
  process.stdout.write(prompt);
2227
2414
  this.confirming = true;
2228
- return new Promise((resolve6) => {
2415
+ return new Promise((resolve7) => {
2229
2416
  let completed = false;
2230
2417
  const cleanup = (result) => {
2231
2418
  if (completed) return;
@@ -2235,7 +2422,7 @@ var ToolExecutor = class {
2235
2422
  rl.pause();
2236
2423
  rlAny.output = savedOutput;
2237
2424
  this.confirming = false;
2238
- resolve6(result);
2425
+ resolve7(result);
2239
2426
  };
2240
2427
  const onLine = (line) => {
2241
2428
  const trimmed = line.trim();
@@ -2302,10 +2489,10 @@ var ToolExecutor = class {
2302
2489
  const filePath = String(call.arguments["path"] ?? "");
2303
2490
  const newContent = String(call.arguments["content"] ?? "");
2304
2491
  if (!filePath) return;
2305
- if (existsSync5(filePath)) {
2492
+ if (existsSync6(filePath)) {
2306
2493
  let oldContent;
2307
2494
  try {
2308
- oldContent = readFileSync4(filePath, "utf-8");
2495
+ oldContent = readFileSync5(filePath, "utf-8");
2309
2496
  } catch {
2310
2497
  return;
2311
2498
  }
@@ -2328,7 +2515,7 @@ var ToolExecutor = class {
2328
2515
  }
2329
2516
  } else if (call.name === "edit_file") {
2330
2517
  const filePath = String(call.arguments["path"] ?? "");
2331
- if (!filePath || !existsSync5(filePath)) return;
2518
+ if (!filePath || !existsSync6(filePath)) return;
2332
2519
  const oldStr = call.arguments["old_str"];
2333
2520
  const newStr = call.arguments["new_str"];
2334
2521
  if (oldStr !== void 0) {
@@ -2355,7 +2542,7 @@ var ToolExecutor = class {
2355
2542
  const to = Number(call.arguments["delete_to_line"] ?? from);
2356
2543
  let fileContent;
2357
2544
  try {
2358
- fileContent = readFileSync4(filePath, "utf-8");
2545
+ fileContent = readFileSync5(filePath, "utf-8");
2359
2546
  } catch {
2360
2547
  return;
2361
2548
  }
@@ -2409,7 +2596,7 @@ var ToolExecutor = class {
2409
2596
  rl.resume();
2410
2597
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
2411
2598
  this.confirming = true;
2412
- return new Promise((resolve6) => {
2599
+ return new Promise((resolve7) => {
2413
2600
  let completed = false;
2414
2601
  const cleanup = (answer) => {
2415
2602
  if (completed) return;
@@ -2419,7 +2606,7 @@ var ToolExecutor = class {
2419
2606
  rl.pause();
2420
2607
  rlAny.output = savedOutput;
2421
2608
  this.confirming = false;
2422
- resolve6(answer === "y");
2609
+ resolve7(answer === "y");
2423
2610
  };
2424
2611
  const onLine = (line) => {
2425
2612
  const trimmed = line.trim();
@@ -2447,11 +2634,11 @@ var ToolExecutor = class {
2447
2634
  };
2448
2635
 
2449
2636
  // src/tools/sensitive-paths.ts
2450
- import { resolve as resolve4, sep as sep2, basename as basename2 } from "path";
2637
+ import { resolve as resolve5, sep as sep2, basename as basename2 } from "path";
2451
2638
  import { homedir as homedir2 } from "os";
2452
2639
  var home = homedir2();
2453
2640
  function norm(p) {
2454
- const abs = resolve4(p);
2641
+ const abs = resolve5(p);
2455
2642
  return process.platform === "win32" ? abs.toLowerCase() : abs;
2456
2643
  }
2457
2644
  function homeRel(p) {
@@ -2610,7 +2797,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
2610
2797
  };
2611
2798
 
2612
2799
  // src/tools/builtin/edit-file.ts
2613
- import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
2800
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
2614
2801
 
2615
2802
  // src/tools/builtin/patch-apply.ts
2616
2803
  function parseUnifiedDiff(patch) {
@@ -2971,7 +3158,7 @@ Note: Path can be absolute or relative to cwd.`,
2971
3158
  const filePath = String(args["path"] ?? "");
2972
3159
  const encoding = args["encoding"] ?? "utf-8";
2973
3160
  if (!filePath) throw new ToolError("edit_file", "path is required");
2974
- if (!existsSync6(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
3161
+ if (!existsSync7(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
2975
3162
  const verdict = classifyWritePath(filePath);
2976
3163
  if (verdict.sensitive && subAgentGuard.active) {
2977
3164
  throw new ToolError(
@@ -2979,7 +3166,7 @@ Note: Path can be absolute or relative to cwd.`,
2979
3166
  `Refused: sub-agents cannot edit sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the edit so the user can approve it.`
2980
3167
  );
2981
3168
  }
2982
- const original = readFileSync5(filePath, encoding);
3169
+ const original = readFileSync6(filePath, encoding);
2983
3170
  if (args["patch"] !== void 0) {
2984
3171
  const patchText = String(args["patch"] ?? "");
2985
3172
  const stopOnError = args["stop_on_error"] !== false;
@@ -3137,8 +3324,8 @@ function truncatePreview(str, maxLen = 80) {
3137
3324
  }
3138
3325
 
3139
3326
  // src/tools/builtin/list-dir.ts
3140
- import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync7 } from "fs";
3141
- import { join as join2, basename as basename3 } from "path";
3327
+ import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync8 } from "fs";
3328
+ import { join as join3, basename as basename3 } from "path";
3142
3329
  var listDirTool = {
3143
3330
  definition: {
3144
3331
  name: "list_dir",
@@ -3160,7 +3347,7 @@ var listDirTool = {
3160
3347
  async execute(args) {
3161
3348
  const dirPath = String(args["path"] ?? process.cwd());
3162
3349
  const recursive = Boolean(args["recursive"] ?? false);
3163
- if (!existsSync7(dirPath)) {
3350
+ if (!existsSync8(dirPath)) {
3164
3351
  const targetName = basename3(dirPath).toLowerCase();
3165
3352
  const cwd = process.cwd();
3166
3353
  const suggestions = [];
@@ -3218,11 +3405,11 @@ function listRecursive(basePath, indent, recursive, lines) {
3218
3405
  if (entry.isDirectory()) {
3219
3406
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
3220
3407
  if (recursive) {
3221
- listRecursive(join2(basePath, entry.name), indent + " ", true, lines);
3408
+ listRecursive(join3(basePath, entry.name), indent + " ", true, lines);
3222
3409
  }
3223
3410
  } else {
3224
3411
  try {
3225
- const stat = statSync3(join2(basePath, entry.name));
3412
+ const stat = statSync3(join3(basePath, entry.name));
3226
3413
  const size = formatSize(stat.size);
3227
3414
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
3228
3415
  } catch {
@@ -3238,9 +3425,9 @@ function formatSize(bytes) {
3238
3425
  }
3239
3426
 
3240
3427
  // src/tools/builtin/grep-files.ts
3241
- import { readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync4, existsSync as existsSync8 } from "fs";
3428
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4, existsSync as existsSync9 } from "fs";
3242
3429
  import { readFile } from "fs/promises";
3243
- import { join as join3, relative } from "path";
3430
+ import { join as join4, relative } from "path";
3244
3431
  var grepFilesTool = {
3245
3432
  definition: {
3246
3433
  name: "grep_files",
@@ -3291,7 +3478,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
3291
3478
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
3292
3479
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
3293
3480
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
3294
- if (!existsSync8(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
3481
+ if (!existsSync9(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
3295
3482
  const MAX_PATTERN_LENGTH = 1e3;
3296
3483
  if (pattern.length > MAX_PATTERN_LENGTH) {
3297
3484
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -3385,11 +3572,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
3385
3572
  if (paths.length >= maxFiles) return;
3386
3573
  if (entry.isDirectory()) {
3387
3574
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
3388
- walk(join3(dirPath, entry.name));
3575
+ walk(join4(dirPath, entry.name));
3389
3576
  } else if (entry.isFile()) {
3390
3577
  if (isBinary(entry.name)) continue;
3391
3578
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
3392
- const fullPath = join3(dirPath, entry.name);
3579
+ const fullPath = join4(dirPath, entry.name);
3393
3580
  try {
3394
3581
  if (statSync4(fullPath).size > 1e6) continue;
3395
3582
  } catch {
@@ -3444,7 +3631,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
3444
3631
  }
3445
3632
  let content;
3446
3633
  try {
3447
- content = readFileSync6(fullPath, "utf-8");
3634
+ content = readFileSync7(fullPath, "utf-8");
3448
3635
  } catch {
3449
3636
  return;
3450
3637
  }
@@ -3475,8 +3662,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
3475
3662
  }
3476
3663
 
3477
3664
  // src/tools/builtin/glob-files.ts
3478
- import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync9 } from "fs";
3479
- import { join as join4, relative as relative2, basename as basename4 } from "path";
3665
+ import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync10 } from "fs";
3666
+ import { join as join5, relative as relative2, basename as basename4 } from "path";
3480
3667
  var globFilesTool = {
3481
3668
  definition: {
3482
3669
  name: "glob_files",
@@ -3509,7 +3696,7 @@ Results sorted by most recent modification time. Automatically skips node_module
3509
3696
  const rootPath = String(args["path"] ?? process.cwd());
3510
3697
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
3511
3698
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
3512
- if (!existsSync9(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
3699
+ if (!existsSync10(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
3513
3700
  const regex = globToRegex(pattern);
3514
3701
  const matches = [];
3515
3702
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -3586,7 +3773,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3586
3773
  }
3587
3774
  for (const entry of entries) {
3588
3775
  if (results.length >= maxResults) break;
3589
- const fullPath = join4(dirPath, entry.name);
3776
+ const fullPath = join5(dirPath, entry.name);
3590
3777
  if (entry.isDirectory()) {
3591
3778
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
3592
3779
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -3668,7 +3855,7 @@ var runInteractiveTool = {
3668
3855
  PYTHONDONTWRITEBYTECODE: "1"
3669
3856
  };
3670
3857
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
3671
- return new Promise((resolve6) => {
3858
+ return new Promise((resolve7) => {
3672
3859
  const child = spawn2(executable, cmdArgs.map(String), {
3673
3860
  cwd: process.cwd(),
3674
3861
  env,
@@ -3701,22 +3888,22 @@ var runInteractiveTool = {
3701
3888
  setTimeout(writeNextLine, 400);
3702
3889
  const timer = setTimeout(() => {
3703
3890
  child.kill();
3704
- resolve6(`${prefixWarnings}[Timeout after ${timeout}ms]
3891
+ resolve7(`${prefixWarnings}[Timeout after ${timeout}ms]
3705
3892
  ${buildOutput(stdout, stderr)}`);
3706
3893
  }, timeout);
3707
3894
  child.on("close", (code) => {
3708
3895
  clearTimeout(timer);
3709
3896
  const output = buildOutput(stdout, stderr);
3710
3897
  if (code !== 0 && code !== null) {
3711
- resolve6(`${prefixWarnings}Exit code ${code}:
3898
+ resolve7(`${prefixWarnings}Exit code ${code}:
3712
3899
  ${output}`);
3713
3900
  } else {
3714
- resolve6(`${prefixWarnings}${output || "(no output)"}`);
3901
+ resolve7(`${prefixWarnings}${output || "(no output)"}`);
3715
3902
  }
3716
3903
  });
3717
3904
  child.on("error", (err) => {
3718
3905
  clearTimeout(timer);
3719
- resolve6(
3906
+ resolve7(
3720
3907
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
3721
3908
  Hint: On Windows, use the full path to the executable, e.g.:
3722
3909
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -4384,11 +4571,11 @@ Any of these triggers means use save_last_response, NOT write_file:
4384
4571
  };
4385
4572
 
4386
4573
  // src/tools/builtin/save-memory.ts
4387
- import { existsSync as existsSync10, readFileSync as readFileSync7, statSync as statSync6, mkdirSync as mkdirSync4 } from "fs";
4388
- import { join as join5 } from "path";
4574
+ import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync6, mkdirSync as mkdirSync4 } from "fs";
4575
+ import { join as join6 } from "path";
4389
4576
  import { homedir as homedir3 } from "os";
4390
4577
  function getMemoryFilePath() {
4391
- return join5(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4578
+ return join6(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4392
4579
  }
4393
4580
  function formatTimestamp() {
4394
4581
  const now = /* @__PURE__ */ new Date();
@@ -4412,8 +4599,8 @@ var saveMemoryTool = {
4412
4599
  const content = String(args["content"] ?? "").trim();
4413
4600
  if (!content) throw new ToolError("save_memory", "content is required");
4414
4601
  const memoryPath = getMemoryFilePath();
4415
- const configDir = join5(homedir3(), CONFIG_DIR_NAME);
4416
- if (!existsSync10(configDir)) {
4602
+ const configDir = join6(homedir3(), CONFIG_DIR_NAME);
4603
+ if (!existsSync11(configDir)) {
4417
4604
  mkdirSync4(configDir, { recursive: true });
4418
4605
  }
4419
4606
  const timestamp = formatTimestamp();
@@ -4421,7 +4608,7 @@ var saveMemoryTool = {
4421
4608
  ## ${timestamp}
4422
4609
  ${content}
4423
4610
  `;
4424
- const previous = existsSync10(memoryPath) ? readFileSync7(memoryPath, "utf-8") : "";
4611
+ const previous = existsSync11(memoryPath) ? readFileSync8(memoryPath, "utf-8") : "";
4425
4612
  atomicWriteFileSync(memoryPath, previous + entry);
4426
4613
  const byteSize = statSync6(memoryPath).size;
4427
4614
  return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -4468,7 +4655,7 @@ function promptUser(rl, question) {
4468
4655
  console.log();
4469
4656
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
4470
4657
  process.stdout.write(chalk4.cyan("> "));
4471
- return new Promise((resolve6) => {
4658
+ return new Promise((resolve7) => {
4472
4659
  let completed = false;
4473
4660
  const cleanup = (answer) => {
4474
4661
  if (completed) return;
@@ -4478,7 +4665,7 @@ function promptUser(rl, question) {
4478
4665
  rl.pause();
4479
4666
  rlAny.output = savedOutput;
4480
4667
  askUserContext.prompting = false;
4481
- resolve6(answer);
4668
+ resolve7(answer);
4482
4669
  };
4483
4670
  const onLine = (line) => {
4484
4671
  cleanup(line);
@@ -4709,6 +4896,218 @@ function formatResults2(query, data, _requested) {
4709
4896
  return header + "\n" + results.join("\n\n");
4710
4897
  }
4711
4898
 
4899
+ // src/agents/agent-config.ts
4900
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
4901
+ import { join as join7 } from "path";
4902
+ import { homedir as homedir4 } from "os";
4903
+ var READ_ONLY_TOOLS2 = /* @__PURE__ */ new Set([
4904
+ "read_file",
4905
+ "list_dir",
4906
+ "grep_files",
4907
+ "glob_files",
4908
+ "web_fetch",
4909
+ "google_search",
4910
+ "write_todos",
4911
+ "find_symbol",
4912
+ "get_outline",
4913
+ "find_references",
4914
+ "search_code"
4915
+ ]);
4916
+ var BUILTIN_AGENTS = [
4917
+ {
4918
+ name: "explorer",
4919
+ description: "Read-only code exploration and evidence gathering.",
4920
+ permissionProfile: "read-only",
4921
+ allowedTools: [...READ_ONLY_TOOLS2],
4922
+ maxToolRounds: 10,
4923
+ contextPolicy: "task-only",
4924
+ system: "Act as a read-only explorer. Inspect code, collect evidence, and do not modify files.",
4925
+ source: "builtin"
4926
+ },
4927
+ {
4928
+ name: "worker",
4929
+ description: "Implementation and focused fixes within sub-agent safety limits.",
4930
+ permissionProfile: "workspace-write",
4931
+ maxToolRounds: 15,
4932
+ contextPolicy: "task-only",
4933
+ system: "Act as a focused implementation worker. Keep edits scoped and report changed files.",
4934
+ source: "builtin"
4935
+ },
4936
+ {
4937
+ name: "reviewer",
4938
+ description: "Code review focused on bugs, regressions, and missing tests.",
4939
+ permissionProfile: "read-only",
4940
+ allowedTools: [...READ_ONLY_TOOLS2],
4941
+ maxToolRounds: 12,
4942
+ contextPolicy: "task-only",
4943
+ system: "Act as a code reviewer. Lead with concrete findings, file evidence, and residual risks.",
4944
+ source: "builtin"
4945
+ },
4946
+ {
4947
+ name: "security",
4948
+ description: "Security audit with read-only evidence collection.",
4949
+ permissionProfile: "read-only",
4950
+ allowedTools: [...READ_ONLY_TOOLS2],
4951
+ maxToolRounds: 12,
4952
+ contextPolicy: "task-only",
4953
+ system: "Act as a security reviewer. Look for trust-boundary, injection, filesystem, network, and secret-handling issues.",
4954
+ source: "builtin"
4955
+ },
4956
+ {
4957
+ name: "tester",
4958
+ description: "Test planning and regression analysis without direct shell execution.",
4959
+ permissionProfile: "read-only",
4960
+ allowedTools: [...READ_ONLY_TOOLS2],
4961
+ maxToolRounds: 12,
4962
+ contextPolicy: "task-only",
4963
+ system: "Act as a tester. Inspect tests, identify coverage gaps, and propose precise regression checks.",
4964
+ source: "builtin"
4965
+ }
4966
+ ];
4967
+ function normalizeName(name) {
4968
+ return name.trim().toLowerCase();
4969
+ }
4970
+ function isStringArray(value) {
4971
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
4972
+ }
4973
+ function parseAgentConfig(raw, source, path3) {
4974
+ if (!raw || typeof raw !== "object") return null;
4975
+ const obj = raw;
4976
+ if (typeof obj.name !== "string" || obj.name.trim() === "") return null;
4977
+ const cfg = {
4978
+ name: obj.name.trim(),
4979
+ source,
4980
+ path: path3
4981
+ };
4982
+ if (typeof obj.description === "string") cfg.description = obj.description;
4983
+ if (typeof obj.provider === "string") cfg.provider = obj.provider;
4984
+ if (typeof obj.model === "string") cfg.model = obj.model;
4985
+ if (typeof obj.system === "string") cfg.system = obj.system;
4986
+ if (typeof obj.instructions === "string") cfg.instructions = obj.instructions;
4987
+ if (typeof obj.systemInstructions === "string") cfg.systemInstructions = obj.systemInstructions;
4988
+ if (isStringArray(obj.allowedTools)) cfg.allowedTools = obj.allowedTools;
4989
+ if (isStringArray(obj.blockedTools)) cfg.blockedTools = obj.blockedTools;
4990
+ if (typeof obj.permissionProfile === "string") cfg.permissionProfile = obj.permissionProfile;
4991
+ if (typeof obj.maxToolRounds === "number" && Number.isFinite(obj.maxToolRounds)) cfg.maxToolRounds = obj.maxToolRounds;
4992
+ if (typeof obj.contextPolicy === "string") cfg.contextPolicy = obj.contextPolicy;
4993
+ return cfg;
4994
+ }
4995
+ function loadAgentDir(dir, source) {
4996
+ if (!existsSync12(dir)) return [];
4997
+ const out = [];
4998
+ for (const file of readdirSync6(dir)) {
4999
+ if (!file.endsWith(".json")) continue;
5000
+ const path3 = join7(dir, file);
5001
+ try {
5002
+ const parsed = JSON.parse(readFileSync9(path3, "utf-8"));
5003
+ const cfg = parseAgentConfig(parsed, source, path3);
5004
+ if (cfg) out.push(cfg);
5005
+ } catch {
5006
+ }
5007
+ }
5008
+ return out;
5009
+ }
5010
+ function getAgentDirs(configDir, cwd = process.cwd()) {
5011
+ return {
5012
+ user: join7(configDir ?? join7(homedir4(), CONFIG_DIR_NAME), "agents"),
5013
+ project: join7(cwd, CONFIG_DIR_NAME, "agents")
5014
+ };
5015
+ }
5016
+ function listAgentConfigs(configDir, cwd = process.cwd()) {
5017
+ const dirs = getAgentDirs(configDir, cwd);
5018
+ const byName = /* @__PURE__ */ new Map();
5019
+ for (const cfg of BUILTIN_AGENTS) byName.set(normalizeName(cfg.name), { ...cfg });
5020
+ for (const cfg of loadAgentDir(dirs.user, "user")) byName.set(normalizeName(cfg.name), cfg);
5021
+ for (const cfg of loadAgentDir(dirs.project, "project")) byName.set(normalizeName(cfg.name), cfg);
5022
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
5023
+ }
5024
+ function resolveAgentConfig(name, configDir, cwd = process.cwd()) {
5025
+ const target = normalizeName(name);
5026
+ return listAgentConfigs(configDir, cwd).find((cfg) => normalizeName(cfg.name) === target);
5027
+ }
5028
+ function getAgentInstructions(agent) {
5029
+ return agent.system ?? agent.systemInstructions ?? agent.instructions;
5030
+ }
5031
+ function getEffectiveAgentTools(agent) {
5032
+ let allowed = new Set(SUBAGENT_ALLOWED_TOOLS);
5033
+ if (agent.permissionProfile === "read-only") {
5034
+ allowed = new Set([...allowed].filter((name) => READ_ONLY_TOOLS2.has(name)));
5035
+ }
5036
+ if (agent.allowedTools && agent.allowedTools.length > 0) {
5037
+ const requested = new Set(agent.allowedTools);
5038
+ allowed = new Set([...allowed].filter((name) => requested.has(name)));
5039
+ }
5040
+ if (agent.blockedTools && agent.blockedTools.length > 0) {
5041
+ for (const name of agent.blockedTools) allowed.delete(name);
5042
+ }
5043
+ return allowed;
5044
+ }
5045
+ function getEffectiveMaxRounds(requestedMaxRounds, agent) {
5046
+ const agentMax = agent.maxToolRounds && agent.maxToolRounds > 0 ? Math.round(agent.maxToolRounds) : SUBAGENT_MAX_ROUNDS_LIMIT;
5047
+ return Math.min(requestedMaxRounds, agentMax, SUBAGENT_MAX_ROUNDS_LIMIT);
5048
+ }
5049
+
5050
+ // src/agents/agent-runtime.ts
5051
+ var nextId = 1;
5052
+ var preferredAgentName = "worker";
5053
+ var runs = [];
5054
+ var MAX_RUNS = 50;
5055
+ function nowIso() {
5056
+ return (/* @__PURE__ */ new Date()).toISOString();
5057
+ }
5058
+ function startAgentRun(input) {
5059
+ const run = {
5060
+ id: `agent-${nextId++}`,
5061
+ agentName: input.agentName,
5062
+ task: input.task,
5063
+ status: "running",
5064
+ startedAt: nowIso(),
5065
+ toolNames: [],
5066
+ agentIndex: input.agentIndex,
5067
+ stopRequested: false
5068
+ };
5069
+ runs.unshift(run);
5070
+ if (runs.length > MAX_RUNS) runs.splice(MAX_RUNS);
5071
+ return run;
5072
+ }
5073
+ function recordAgentTool(runId, toolName) {
5074
+ const run = runs.find((r) => r.id === runId);
5075
+ if (!run) return;
5076
+ if (!run.toolNames.includes(toolName)) run.toolNames.push(toolName);
5077
+ }
5078
+ function finishAgentRun(runId, status, summary, error) {
5079
+ const run = runs.find((r) => r.id === runId);
5080
+ if (!run) return;
5081
+ run.status = status;
5082
+ run.finishedAt = nowIso();
5083
+ if (summary) run.summary = summary;
5084
+ if (error) run.error = error;
5085
+ }
5086
+ function listAgentRuns() {
5087
+ return runs.map((run) => ({ ...run, toolNames: [...run.toolNames] }));
5088
+ }
5089
+ function requestAgentStop(idOrAll) {
5090
+ const target = idOrAll.trim().toLowerCase();
5091
+ let count = 0;
5092
+ for (const run of runs) {
5093
+ if (run.status !== "running") continue;
5094
+ if (target === "all" || run.id.toLowerCase() === target) {
5095
+ run.stopRequested = true;
5096
+ count++;
5097
+ }
5098
+ }
5099
+ return count;
5100
+ }
5101
+ function isAgentStopRequested(runId) {
5102
+ return runs.find((r) => r.id === runId)?.stopRequested ?? false;
5103
+ }
5104
+ function setPreferredAgentName(name) {
5105
+ preferredAgentName = name.trim() || "worker";
5106
+ }
5107
+ function getPreferredAgentName() {
5108
+ return preferredAgentName;
5109
+ }
5110
+
4712
5111
  // src/tools/builtin/spawn-agent.ts
4713
5112
  var spawnAgentContext = {
4714
5113
  provider: null,
@@ -4723,18 +5122,27 @@ function agentPrefix(agentIndex) {
4723
5122
  return theme.dim(` \u2503${agentIndex} `);
4724
5123
  }
4725
5124
  var SubAgentExecutor = class {
4726
- constructor(registry, agentIndex = null) {
5125
+ constructor(registry, agentIndex = null, runId) {
4727
5126
  this.registry = registry;
5127
+ this.runId = runId;
4728
5128
  this.prefix = agentPrefix(agentIndex);
4729
5129
  }
4730
5130
  round = 0;
4731
5131
  totalRounds = 0;
4732
5132
  prefix = PREFIX;
5133
+ toolNames = [];
4733
5134
  setRoundInfo(current, total) {
4734
5135
  this.round = current;
4735
5136
  this.totalRounds = total;
4736
5137
  }
4737
5138
  async execute(call) {
5139
+ if (this.runId && isAgentStopRequested(this.runId)) {
5140
+ return {
5141
+ callId: call.id,
5142
+ content: "Sub-agent stop requested.",
5143
+ isError: true
5144
+ };
5145
+ }
4738
5146
  const tool = this.registry.get(call.name);
4739
5147
  if (!tool) {
4740
5148
  return {
@@ -4755,6 +5163,8 @@ var SubAgentExecutor = class {
4755
5163
  };
4756
5164
  }
4757
5165
  this.printToolCall(call, dangerLevel);
5166
+ if (!this.toolNames.includes(call.name)) this.toolNames.push(call.name);
5167
+ if (this.runId) recordAgentTool(this.runId, call.name);
4758
5168
  try {
4759
5169
  const rawContent = await runTool(tool, call.arguments, call.name);
4760
5170
  const content = truncateOutput(rawContent, call.name);
@@ -4774,7 +5184,6 @@ var SubAgentExecutor = class {
4774
5184
  }
4775
5185
  return results;
4776
5186
  }
4777
- // ── 带前缀的终端输出 ──
4778
5187
  printPrefixed(text) {
4779
5188
  for (const line of text.split("\n")) {
4780
5189
  console.log(this.prefix + line);
@@ -4815,7 +5224,7 @@ var SubAgentExecutor = class {
4815
5224
  }
4816
5225
  }
4817
5226
  };
4818
- function buildSubAgentSystemPrompt(task, parentSystemPrompt) {
5227
+ function buildSubAgentSystemPrompt(task, agent, parentSystemPrompt) {
4819
5228
  const parts = [];
4820
5229
  if (parentSystemPrompt) {
4821
5230
  parts.push(parentSystemPrompt);
@@ -4823,6 +5232,8 @@ function buildSubAgentSystemPrompt(task, parentSystemPrompt) {
4823
5232
  parts.push(
4824
5233
  `# Sub-Agent Mode
4825
5234
 
5235
+ **Agent Role**: ${agent.name}${agent.description ? ` \u2014 ${agent.description}` : ""}
5236
+
4826
5237
  You are a focused sub-agent delegated by the main agent to complete a specific task.
4827
5238
 
4828
5239
  **Your Task**:
@@ -4836,13 +5247,19 @@ ${task}
4836
5247
  5. Destructive operations (rm -rf etc.) will be automatically blocked
4837
5248
  6. Be efficient, minimize unnecessary tool call rounds`
4838
5249
  );
5250
+ const agentInstructions = getAgentInstructions(agent);
5251
+ if (agentInstructions) {
5252
+ parts.push(`# Agent Instructions
5253
+
5254
+ ${agentInstructions}`);
5255
+ }
4839
5256
  return parts.join("\n\n---\n\n");
4840
5257
  }
4841
- function printSubAgentHeader(task, maxRounds, agentIndex = null) {
5258
+ function printSubAgentHeader(task, maxRounds, agentName, agentIndex = null) {
4842
5259
  const prefix = agentPrefix(agentIndex);
4843
5260
  console.log();
4844
5261
  console.log(theme.dim(" \u250F\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
4845
- const label = agentIndex !== null ? `\u{1F916} Sub-Agent #${agentIndex} Spawned` : "\u{1F916} Sub-Agent Spawned";
5262
+ const label = agentIndex !== null ? `\u{1F916} Sub-Agent #${agentIndex} Spawned (${agentName})` : `\u{1F916} Sub-Agent Spawned (${agentName})`;
4846
5263
  console.log(prefix + theme.toolCall(label));
4847
5264
  console.log(
4848
5265
  prefix + theme.dim("Task: ") + (task.slice(0, 120) + (task.length > 120 ? "..." : ""))
@@ -4865,58 +5282,110 @@ function printSubAgentFooter(usage, agentIndex = null) {
4865
5282
  console.log(theme.dim(" \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
4866
5283
  console.log();
4867
5284
  }
4868
- async function runSubAgent(task, maxRounds, agentIndex, ctx) {
5285
+ function summarizeContent(content) {
5286
+ const flat = content.replace(/\s+/g, " ").trim();
5287
+ return flat.length > 500 ? flat.slice(0, 497) + "..." : flat;
5288
+ }
5289
+ function extractEvidence(content, toolNames) {
5290
+ const evidence = /* @__PURE__ */ new Set();
5291
+ if (toolNames.length > 0) evidence.add(`Tools: ${toolNames.join(", ")}`);
5292
+ const matches = content.match(/(?:[\w.-]+[\\/])+[\w.-]+\.(?:ts|tsx|js|jsx|json|md|css|html|py|go|rs|yml|yaml)/g) ?? [];
5293
+ for (const match of matches.slice(0, 8)) evidence.add(match);
5294
+ return [...evidence].slice(0, 10);
5295
+ }
5296
+ function formatAgentResult(input) {
5297
+ const lines = [input.title, "", `**Agent**: ${input.agentName}`];
5298
+ if (input.task) lines.push(`**Task**: ${input.task}`);
5299
+ lines.push("", "### Summary", input.summary || "(no summary)", "", "### Key Evidence");
5300
+ if (input.evidence.length > 0) {
5301
+ for (const item of input.evidence) lines.push(`- ${item}`);
5302
+ } else {
5303
+ lines.push("- No structured evidence extracted.");
5304
+ }
5305
+ lines.push("", "### Tools Used");
5306
+ lines.push(input.toolNames.length > 0 ? input.toolNames.join(", ") : "No tools used.");
5307
+ lines.push("", "### Full Result", input.content, "");
5308
+ lines.push(`*Tokens: ${input.usage.inputTokens} in / ${input.usage.outputTokens} out*`);
5309
+ return lines;
5310
+ }
5311
+ async function runSubAgent(task, maxRounds, agentIndex, ctx, agent) {
4869
5312
  if (!ctx.provider) {
4870
5313
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
4871
5314
  }
5315
+ const providerFromConfig = agent.provider && ctx.providers?.has(agent.provider) ? ctx.providers.get(agent.provider) : void 0;
5316
+ const provider = providerFromConfig ?? ctx.provider;
5317
+ const model = agent.model ?? (providerFromConfig?.info?.defaultModel ?? ctx.model);
5318
+ const run = startAgentRun({
5319
+ agentName: agent.name,
5320
+ task,
5321
+ agentIndex: agentIndex ?? void 0
5322
+ });
5323
+ const effectiveTools = getEffectiveAgentTools(agent);
4872
5324
  const subRegistry = new ToolRegistry();
4873
5325
  for (const tool of subRegistry.listAll()) {
4874
- if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name)) {
5326
+ if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name) || !effectiveTools.has(tool.definition.name)) {
4875
5327
  subRegistry.unregister(tool.definition.name);
4876
5328
  }
4877
5329
  }
4878
- const subExecutor = new SubAgentExecutor(subRegistry, agentIndex);
5330
+ const subExecutor = new SubAgentExecutor(subRegistry, agentIndex, run.id);
4879
5331
  const subMessages = [
4880
5332
  { role: "user", content: task, timestamp: /* @__PURE__ */ new Date() }
4881
5333
  ];
4882
- const subSystemPrompt = buildSubAgentSystemPrompt(task, ctx.systemPrompt);
5334
+ const subSystemPrompt = buildSubAgentSystemPrompt(task, agent, ctx.systemPrompt);
4883
5335
  const toolDefs = subRegistry.getDefinitions();
4884
- printSubAgentHeader(task, maxRounds, agentIndex);
4885
- const loop = await runLeanAgentLoop({
4886
- provider: ctx.provider,
4887
- messages: subMessages,
4888
- model: ctx.model,
4889
- maxRounds,
4890
- chatParams: {
4891
- temperature: ctx.modelParams.temperature,
4892
- maxTokens: ctx.modelParams.maxTokens,
4893
- timeout: ctx.modelParams.timeout,
4894
- thinking: ctx.modelParams.thinking
4895
- },
4896
- executeTools: (calls) => subExecutor.executeAll(calls),
4897
- systemPromptForRound: () => subSystemPrompt,
4898
- toolDefsForRound: () => toolDefs,
4899
- onRoundStart: (round) => {
4900
- subExecutor.setRoundInfo(round + 1, maxRounds);
4901
- if (ctx.configManager) {
4902
- googleSearchContext.configManager = ctx.configManager;
4903
- }
4904
- },
4905
- onRoundsExhausted: () => `(Sub-agent reached maximum rounds (${maxRounds}) without producing a final response)`,
4906
- onError: (errMsg) => {
4907
- process.stderr.write(`
5336
+ printSubAgentHeader(task, maxRounds, agent.name, agentIndex);
5337
+ try {
5338
+ const loop = await runLeanAgentLoop({
5339
+ provider,
5340
+ messages: subMessages,
5341
+ model,
5342
+ maxRounds,
5343
+ chatParams: {
5344
+ temperature: ctx.modelParams.temperature,
5345
+ maxTokens: ctx.modelParams.maxTokens,
5346
+ timeout: ctx.modelParams.timeout,
5347
+ thinking: ctx.modelParams.thinking
5348
+ },
5349
+ executeTools: (calls) => subExecutor.executeAll(calls),
5350
+ systemPromptForRound: () => subSystemPrompt,
5351
+ toolDefsForRound: () => toolDefs,
5352
+ onRoundStart: (round) => {
5353
+ subExecutor.setRoundInfo(round + 1, maxRounds);
5354
+ if (ctx.configManager) {
5355
+ googleSearchContext.configManager = ctx.configManager;
5356
+ }
5357
+ },
5358
+ onRoundsExhausted: () => `(Sub-agent reached maximum rounds (${maxRounds}) without producing a final response)`,
5359
+ onError: (errMsg) => {
5360
+ process.stderr.write(`
4908
5361
  [spawn_agent] Error in sub-agent loop: ${errMsg}
4909
5362
  `);
4910
- return `(Sub-agent error: ${errMsg})`;
4911
- }
4912
- });
4913
- printSubAgentFooter(loop.usage, agentIndex);
4914
- return { content: loop.content, usage: loop.usage };
5363
+ return `(Sub-agent error: ${errMsg})`;
5364
+ }
5365
+ });
5366
+ printSubAgentFooter(loop.usage, agentIndex);
5367
+ const summary = summarizeContent(loop.content);
5368
+ const evidence = extractEvidence(loop.content, subExecutor.toolNames);
5369
+ const stopped = isAgentStopRequested(run.id);
5370
+ finishAgentRun(run.id, stopped ? "stopped" : "ok", summary);
5371
+ return {
5372
+ content: loop.content,
5373
+ usage: loop.usage,
5374
+ toolNames: subExecutor.toolNames,
5375
+ summary,
5376
+ evidence,
5377
+ agentName: agent.name
5378
+ };
5379
+ } catch (err) {
5380
+ const message = err instanceof Error ? err.message : String(err);
5381
+ finishAgentRun(run.id, "error", void 0, message);
5382
+ throw err;
5383
+ }
4915
5384
  }
4916
5385
  var spawnAgentTool = {
4917
5386
  definition: {
4918
5387
  name: "spawn_agent",
4919
- description: "Delegate subtask(s) to independent sub-agent(s). Each sub-agent has its own conversation context and agentic tool-call loop, with access to bash, file read/write, edit, search, etc., but cannot interact with the user or spawn more sub-agents. Suitable for independently completable subtasks like: refactoring a module, writing tests, researching a technical approach, or implementing a specific feature. Pass `task` for a single sub-agent, or `tasks` (array) to run multiple sub-agents IN PARALLEL. Parallel mode is ideal for independent research/analysis tasks \u2014 network-bound API calls overlap while the main thread stays responsive. Returns a summary of each sub-agent result.",
5388
+ description: "Delegate subtask(s) to independent named agents. Each sub-agent has its own conversation context and agentic tool-call loop, inherits the parent safety boundary, and can only narrow the sub-agent tool whitelist. Pass `agent` to select a built-in or configured role. Built-ins: explorer, worker, reviewer, security, tester. Pass `task` for a single sub-agent, or `tasks` (array) to run multiple sub-agents IN PARALLEL. Returns summary, key evidence, and tools used for each sub-agent result.",
4920
5389
  parameters: {
4921
5390
  task: {
4922
5391
  type: "string",
@@ -4934,7 +5403,12 @@ var spawnAgentTool = {
4934
5403
  },
4935
5404
  max_rounds: {
4936
5405
  type: "number",
4937
- description: "Max tool-call rounds per sub-agent (1-15, default 10). Applied to each sub-agent in parallel mode.",
5406
+ description: "Max tool-call rounds per sub-agent (1-30, default 15). Agent config can only reduce this value.",
5407
+ required: false
5408
+ },
5409
+ agent: {
5410
+ type: "string",
5411
+ description: "Agent role name from built-ins or ~/.aicli/agents / .aicli/agents JSON configs. Built-ins: explorer, worker, reviewer, security, tester.",
4938
5412
  required: false
4939
5413
  }
4940
5414
  },
@@ -4954,10 +5428,17 @@ var spawnAgentTool = {
4954
5428
  Math.max(Math.round(rawMaxRounds), 1),
4955
5429
  SUBAGENT_MAX_ROUNDS_LIMIT
4956
5430
  );
5431
+ const requestedAgentName = typeof args["agent"] === "string" && args["agent"].trim() ? args["agent"].trim() : getPreferredAgentName();
4957
5432
  const ctx = spawnAgentContext;
4958
5433
  if (!ctx.provider) {
4959
5434
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
4960
5435
  }
5436
+ const agent = resolveAgentConfig(requestedAgentName, ctx.configManager?.getConfigDir());
5437
+ if (!agent) {
5438
+ const available = listAgentConfigs(ctx.configManager?.getConfigDir()).map((cfg) => cfg.name).join(", ");
5439
+ throw new ToolError("spawn_agent", `unknown agent "${requestedAgentName}". Available agents: ${available}`);
5440
+ }
5441
+ const effectiveMaxRounds = getEffectiveMaxRounds(maxRounds, agent);
4961
5442
  const hookConfig = ctx.configManager?.get("hooks") ?? void 0;
4962
5443
  const hookConfigDir = ctx.configManager?.getConfigDir() ?? process.cwd();
4963
5444
  const taskCount = singleTask ? 1 : tasksArr.length;
@@ -4965,7 +5446,8 @@ var spawnAgentTool = {
4965
5446
  task: singleTask || void 0,
4966
5447
  tasks: tasksArr.length > 0 ? tasksArr : void 0,
4967
5448
  taskCount,
4968
- maxRounds
5449
+ maxRounds: effectiveMaxRounds,
5450
+ agent: agent.name
4969
5451
  };
4970
5452
  const startDecisions = runLifecycleHooks(hookConfig, "SubagentStart", hookPayload, hookConfigDir);
4971
5453
  const startDeny = startDecisions.find((d) => d.action === "deny");
@@ -4977,32 +5459,37 @@ var spawnAgentTool = {
4977
5459
  subAgentGuard.active = true;
4978
5460
  try {
4979
5461
  if (singleTask) {
4980
- const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
5462
+ const result = await runSubAgent(singleTask, effectiveMaxRounds, null, ctx, agent);
4981
5463
  subagentStatus = "ok";
4982
- return [
4983
- "## Sub-Agent Result",
4984
- "",
4985
- content,
4986
- "",
4987
- "---",
4988
- `Token usage: ${usage.inputTokens} input, ${usage.outputTokens} output`
4989
- ].join("\n");
5464
+ return formatAgentResult({
5465
+ title: "## Sub-Agent Result",
5466
+ content: result.content,
5467
+ usage: result.usage,
5468
+ toolNames: result.toolNames,
5469
+ summary: result.summary,
5470
+ evidence: result.evidence,
5471
+ agentName: result.agentName
5472
+ }).join("\n");
4990
5473
  }
4991
5474
  console.log();
4992
- console.log(theme.toolCall(`\u{1F916} Spawning ${tasksArr.length} sub-agents in parallel...`));
5475
+ console.log(theme.toolCall(`\u{1F916} Spawning ${tasksArr.length} sub-agents in parallel as ${agent.name}...`));
4993
5476
  const results = await Promise.all(
4994
- tasksArr.map((t, i) => runSubAgent(t, maxRounds, i + 1, ctx))
5477
+ tasksArr.map((t, i) => runSubAgent(t, effectiveMaxRounds, i + 1, ctx, agent))
4995
5478
  );
4996
5479
  const totalIn = results.reduce((s, r) => s + r.usage.inputTokens, 0);
4997
5480
  const totalOut = results.reduce((s, r) => s + r.usage.outputTokens, 0);
4998
5481
  const lines = [`## Parallel Sub-Agent Results (${results.length} agents)`, ""];
4999
5482
  results.forEach((r, i) => {
5000
- lines.push(`### Sub-Agent #${i + 1}`);
5001
- lines.push(`**Task**: ${tasksArr[i].slice(0, 200)}${tasksArr[i].length > 200 ? "..." : ""}`);
5002
- lines.push("");
5003
- lines.push(r.content);
5004
- lines.push("");
5005
- lines.push(`*Tokens: ${r.usage.inputTokens} in / ${r.usage.outputTokens} out*`);
5483
+ lines.push(...formatAgentResult({
5484
+ title: `### Sub-Agent #${i + 1}`,
5485
+ task: `${tasksArr[i].slice(0, 200)}${tasksArr[i].length > 200 ? "..." : ""}`,
5486
+ content: r.content,
5487
+ usage: r.usage,
5488
+ toolNames: r.toolNames,
5489
+ summary: r.summary,
5490
+ evidence: r.evidence,
5491
+ agentName: r.agentName
5492
+ }));
5006
5493
  lines.push("");
5007
5494
  });
5008
5495
  lines.push("---");
@@ -5233,14 +5720,14 @@ var taskStopTool = {
5233
5720
 
5234
5721
  // src/tools/builtin/git-tools.ts
5235
5722
  import { execFileSync as execFileSync2 } from "child_process";
5236
- import { existsSync as existsSync11 } from "fs";
5237
- import { join as join6 } from "path";
5723
+ import { existsSync as existsSync13 } from "fs";
5724
+ import { join as join8 } from "path";
5238
5725
  function assertGitRepo(cwd) {
5239
5726
  let dir = cwd;
5240
5727
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
5241
5728
  while (dir && dir !== root) {
5242
- if (existsSync11(join6(dir, ".git"))) return;
5243
- const parent = join6(dir, "..");
5729
+ if (existsSync13(join8(dir, ".git"))) return;
5730
+ const parent = join8(dir, "..");
5244
5731
  if (parent === dir) break;
5245
5732
  dir = parent;
5246
5733
  }
@@ -5505,9 +5992,9 @@ ${commitOutput.trim()}`;
5505
5992
  };
5506
5993
 
5507
5994
  // src/tools/builtin/notebook-edit.ts
5508
- import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
5995
+ import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
5509
5996
  import { writeFile } from "fs/promises";
5510
- import { resolve as resolve5, extname as extname2 } from "path";
5997
+ import { resolve as resolve6, extname as extname2 } from "path";
5511
5998
  var notebookEditTool = {
5512
5999
  definition: {
5513
6000
  name: "notebook_edit",
@@ -5556,14 +6043,14 @@ var notebookEditTool = {
5556
6043
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
5557
6044
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
5558
6045
  }
5559
- const absPath = resolve5(filePath);
6046
+ const absPath = resolve6(filePath);
5560
6047
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
5561
6048
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
5562
6049
  }
5563
- if (!existsSync12(absPath)) {
6050
+ if (!existsSync14(absPath)) {
5564
6051
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
5565
6052
  }
5566
- const raw = readFileSync8(absPath, "utf-8");
6053
+ const raw = readFileSync10(absPath, "utf-8");
5567
6054
  const nb = parseNotebook(raw);
5568
6055
  const cellIdx0 = cellIndexRaw - 1;
5569
6056
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -5980,8 +6467,8 @@ function estimateToolDefinitionTokens(def) {
5980
6467
 
5981
6468
  // src/tools/registry.ts
5982
6469
  import { pathToFileURL } from "url";
5983
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync6 } from "fs";
5984
- import { join as join7 } from "path";
6470
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readdirSync as readdirSync7 } from "fs";
6471
+ import { join as join9 } from "path";
5985
6472
  var ToolRegistry = class {
5986
6473
  tools = /* @__PURE__ */ new Map();
5987
6474
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -6142,7 +6629,7 @@ var ToolRegistry = class {
6142
6629
  * Returns the number of successfully loaded plugins.
6143
6630
  */
6144
6631
  async loadPlugins(pluginsDir, allowPlugins = false) {
6145
- if (!existsSync13(pluginsDir)) {
6632
+ if (!existsSync15(pluginsDir)) {
6146
6633
  try {
6147
6634
  mkdirSync5(pluginsDir, { recursive: true });
6148
6635
  } catch {
@@ -6151,7 +6638,7 @@ var ToolRegistry = class {
6151
6638
  }
6152
6639
  let files;
6153
6640
  try {
6154
- files = readdirSync6(pluginsDir).filter((f) => f.endsWith(".js"));
6641
+ files = readdirSync7(pluginsDir).filter((f) => f.endsWith(".js"));
6155
6642
  } catch {
6156
6643
  return 0;
6157
6644
  }
@@ -6168,12 +6655,12 @@ var ToolRegistry = class {
6168
6655
  process.stderr.write(
6169
6656
  `
6170
6657
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
6171
- ` + files.map((f) => ` + ${join7(pluginsDir, f)}`).join("\n") + "\n\n"
6658
+ ` + files.map((f) => ` + ${join9(pluginsDir, f)}`).join("\n") + "\n\n"
6172
6659
  );
6173
6660
  let loaded = 0;
6174
6661
  for (const file of files) {
6175
6662
  try {
6176
- const fileUrl = pathToFileURL(join7(pluginsDir, file)).href;
6663
+ const fileUrl = pathToFileURL(join9(pluginsDir, file)).href;
6177
6664
  const mod = await import(fileUrl);
6178
6665
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
6179
6666
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -6211,6 +6698,10 @@ export {
6211
6698
  getPendingHookTrust,
6212
6699
  runLifecycleHooks,
6213
6700
  runHook,
6701
+ recordRecentlyDeniedAutoAction,
6702
+ getRecentlyDeniedAutoActions,
6703
+ clearRecentlyDeniedAutoActions,
6704
+ defaultActionClassifier,
6214
6705
  isInterrupted,
6215
6706
  requestInterrupt,
6216
6707
  resetInterrupt,
@@ -6230,6 +6721,12 @@ export {
6230
6721
  cleanupRejectedTeeFile,
6231
6722
  askUserContext,
6232
6723
  googleSearchContext,
6724
+ listAgentConfigs,
6725
+ resolveAgentConfig,
6726
+ listAgentRuns,
6727
+ requestAgentStop,
6728
+ setPreferredAgentName,
6729
+ getPreferredAgentName,
6233
6730
  spawnAgentContext,
6234
6731
  estimateTokens,
6235
6732
  ToolRegistry