jinzd-ai-cli 0.4.103 → 0.4.105

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/{agent-client-6GX6QQDU.js → agent-client-25TIQ6AP.js} +1 -0
  4. package/dist/{auth-MSUWO6SE.js → auth-SC6KHHI3.js} +1 -0
  5. package/dist/{batch-LNTG2IRQ.js → batch-NPK4USGH.js} +3 -2
  6. package/dist/{chat-index-W2UZ34ZI.js → chat-index-7OHUKJY5.js} +1 -0
  7. package/dist/{chat-index-QKFH7ZP6.js → chat-index-ADG2GPCC.js} +1 -0
  8. package/dist/chunk-3RG5ZIWI.js +10 -0
  9. package/dist/{chunk-SN56X6RE.js → chunk-B6NUQVYK.js} +1 -1
  10. package/dist/{chunk-VOWVIR2U.js → chunk-F7XJ67XB.js} +30 -112
  11. package/dist/chunk-HOSJZMQS.js +97 -0
  12. package/dist/{chunk-OVYOYUP7.js → chunk-LVX667WL.js} +89 -36
  13. package/dist/{chunk-JHPSWYO3.js → chunk-LX5FXZVP.js} +7 -4
  14. package/dist/chunk-PDX44BCA.js +11 -0
  15. package/dist/{chunk-RZWWODW7.js → chunk-RFKT3T5S.js} +199 -74
  16. package/dist/{chunk-DGXUO7D4.js → chunk-VOF6OTZB.js} +89 -39
  17. package/dist/constants-HK5BB5EZ.js +78 -0
  18. package/dist/electron-server.js +289 -129
  19. package/dist/{file-checkpoint-CGH6OJVI.js → file-checkpoint-UHSMHCRU.js} +1 -0
  20. package/dist/{file-checkpoint-NKBHGC7L.js → file-checkpoint-ZN7KE3TN.js} +1 -0
  21. package/dist/git-context-7KIP4X2V.js +12 -0
  22. package/dist/{hub-4YGZ4XHN.js → hub-5VFGLTHY.js} +3 -2
  23. package/dist/{hub-server-BYXNQGDY.js → hub-server-AUMVPNU6.js} +1 -0
  24. package/dist/index.js +98 -47
  25. package/dist/{indexer-C7QYYHSZ.js → indexer-XGY7XGJM.js} +1 -0
  26. package/dist/{indexer-O5FCGFBJ.js → indexer-Z6AQTGBK.js} +1 -0
  27. package/dist/project-trust-EBGHD7LE.js +67 -0
  28. package/dist/project-trust-IFM7FXEV.js +68 -0
  29. package/dist/{run-tests-SN74WT4Z.js → run-tests-IMVI43CZ.js} +2 -1
  30. package/dist/{run-tests-3YOJEN2Q.js → run-tests-VQ3YZB75.js} +3 -2
  31. package/dist/{semantic-3KJPAUW6.js → semantic-FR2ZSQLY.js} +1 -0
  32. package/dist/{semantic-YDRPPVWK.js → semantic-UFKVYKFE.js} +3 -2
  33. package/dist/{server-BG4WR6RF.js → server-XDBIWNRW.js} +9 -8
  34. package/dist/{server-TNPDHGQT.js → server-ZVY3CKTJ.js} +65 -28
  35. package/dist/{store-S24SPPDZ.js → store-JDEW743P.js} +1 -0
  36. package/dist/{store-247B3TAU.js → store-Q7NMUCPP.js} +1 -0
  37. package/dist/{task-orchestrator-MUIH3XBY.js → task-orchestrator-UEZOFXQX.js} +9 -8
  38. package/dist/{vector-store-NDUFLNGN.js → vector-store-AK6J3RIA.js} +1 -0
  39. package/dist/{vector-store-QARQ2P6D.js → vector-store-MCQ77OOJ.js} +1 -0
  40. package/package.json +1 -1
  41. package/dist/{chunk-KJLJPUY2.js → chunk-3BICTI5M.js} +3 -3
@@ -1,9 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ hasSemanticIndex,
4
+ semanticSearch
5
+ } from "./chunk-3BICTI5M.js";
6
+ import {
7
+ runTestsTool
8
+ } from "./chunk-LVX667WL.js";
2
9
  import {
3
10
  EnvLoader,
4
11
  NetworkError,
5
12
  ToolError
6
13
  } from "./chunk-2ZD3YTVM.js";
14
+ import {
15
+ CONFIG_DIR_NAME,
16
+ DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
17
+ MEMORY_FILE_NAME,
18
+ SUBAGENT_ALLOWED_TOOLS,
19
+ SUBAGENT_DEFAULT_MAX_ROUNDS,
20
+ SUBAGENT_MAX_ROUNDS_LIMIT
21
+ } from "./chunk-LX5FXZVP.js";
7
22
  import {
8
23
  fileCheckpoints
9
24
  } from "./chunk-4BKXL7SM.js";
@@ -14,24 +29,9 @@ import {
14
29
  import {
15
30
  indexProject
16
31
  } from "./chunk-NHNWUBXB.js";
17
- import {
18
- hasSemanticIndex,
19
- semanticSearch
20
- } from "./chunk-KJLJPUY2.js";
21
32
  import {
22
33
  loadIndex
23
34
  } from "./chunk-6VRJGH25.js";
24
- import {
25
- runTestsTool
26
- } from "./chunk-OVYOYUP7.js";
27
- import {
28
- CONFIG_DIR_NAME,
29
- DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
30
- MEMORY_FILE_NAME,
31
- SUBAGENT_ALLOWED_TOOLS,
32
- SUBAGENT_DEFAULT_MAX_ROUNDS,
33
- SUBAGENT_MAX_ROUNDS_LIMIT
34
- } from "./chunk-JHPSWYO3.js";
35
35
 
36
36
  // src/tools/types.ts
37
37
  function isFileWriteTool(name) {
@@ -43,11 +43,17 @@ function getDangerLevel(toolName, args) {
43
43
  const cmd = String(args["command"] ?? "");
44
44
  if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
45
45
  if (/\brm\s+\S/.test(cmd)) return "destructive";
46
- if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
47
- if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
46
+ if (/\brmdir\b|\bformat\b|\bmkfs\b|\bdd\s+if=|\bshred\b|\bfdisk\b|\bparted\b/.test(cmd)) return "destructive";
47
+ if (/\bshutdown\b|\breboot\b|\bhalt\b|\bpoweroff\b/.test(cmd)) return "destructive";
48
+ if (/\bkill\s+-9\b|\bkillall\b/.test(cmd)) return "destructive";
49
+ if (/\bRemove-Item\b|\bri\s+\S/i.test(cmd)) return "destructive";
50
+ if (/\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
48
51
  if (/\bdel\s+\S/.test(cmd)) return "destructive";
49
- if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
50
- if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
52
+ if (/\bShutdown(-Computer)?\b|\bRestart-Computer\b/i.test(cmd)) return "destructive";
53
+ if (/(?:^|[\s|;&])>>?\s*\S/.test(cmd)) return "write";
54
+ if (/\btee\b|\bcp\b|\bmv\b|\bln\s+-s/.test(cmd)) return "write";
55
+ if (/\bchmod\b|\bchown\b/.test(cmd)) return "write";
56
+ if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b|\bSet-ItemProperty\b|\bNew-ItemProperty\b/i.test(cmd)) return "write";
51
57
  return "safe";
52
58
  }
53
59
  if (toolName === "write_file") return "write";
@@ -662,10 +668,10 @@ function createCell(cellType, content) {
662
668
  // src/tools/builtin/read-file.ts
663
669
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
664
670
  function getSensitiveWarning(normalizedPath) {
665
- const home = homedir();
671
+ const home2 = homedir();
666
672
  const p = normalizedPath.toLowerCase();
667
673
  const base = basename(normalizedPath).toLowerCase();
668
- if (normalizedPath.startsWith(home) && p.includes(".aicli") && base === "config.json") {
674
+ if (normalizedPath.startsWith(home2) && p.includes(".aicli") && base === "config.json") {
669
675
  return "[\u26A0 Security Warning: This file contains API keys. Be careful not to share this content.]\n\n";
670
676
  }
671
677
  if (base === ".env" || base.startsWith(".env.") || base.endsWith(".env")) {
@@ -1159,8 +1165,14 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
1159
1165
  if (rule.when) {
1160
1166
  if (rule.when.dangerLevel && rule.when.dangerLevel !== dangerLevel) continue;
1161
1167
  if (rule.when.pathPattern) {
1162
- const path3 = String(args["path"] ?? args["command"] ?? "");
1163
- if (!path3.includes(rule.when.pathPattern)) continue;
1168
+ if (toolName === "bash" && rule.action === "auto-approve") {
1169
+ const cmd = String(args["command"] ?? "").trim();
1170
+ if (/[;&|`$<>]/.test(cmd) || /\$\(/.test(cmd)) continue;
1171
+ if (!cmd.startsWith(rule.when.pathPattern)) continue;
1172
+ } else {
1173
+ const path3 = String(args["path"] ?? args["command"] ?? "");
1174
+ if (!path3.includes(rule.when.pathPattern)) continue;
1175
+ }
1164
1176
  }
1165
1177
  }
1166
1178
  return rule.action;
@@ -1606,7 +1618,7 @@ var ToolExecutor = class {
1606
1618
  rl.resume();
1607
1619
  process.stdout.write(prompt);
1608
1620
  this.confirming = true;
1609
- return new Promise((resolve4) => {
1621
+ return new Promise((resolve5) => {
1610
1622
  let completed = false;
1611
1623
  const cleanup = (result) => {
1612
1624
  if (completed) return;
@@ -1616,7 +1628,7 @@ var ToolExecutor = class {
1616
1628
  rl.pause();
1617
1629
  rlAny.output = savedOutput;
1618
1630
  this.confirming = false;
1619
- resolve4(result);
1631
+ resolve5(result);
1620
1632
  };
1621
1633
  const onLine = (line) => {
1622
1634
  const trimmed = line.trim();
@@ -1786,7 +1798,7 @@ var ToolExecutor = class {
1786
1798
  rl.resume();
1787
1799
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
1788
1800
  this.confirming = true;
1789
- return new Promise((resolve4) => {
1801
+ return new Promise((resolve5) => {
1790
1802
  let completed = false;
1791
1803
  const cleanup = (answer) => {
1792
1804
  if (completed) return;
@@ -1796,7 +1808,7 @@ var ToolExecutor = class {
1796
1808
  rl.pause();
1797
1809
  rlAny.output = savedOutput;
1798
1810
  this.confirming = false;
1799
- resolve4(answer === "y");
1811
+ resolve5(answer === "y");
1800
1812
  };
1801
1813
  const onLine = (line) => {
1802
1814
  const trimmed = line.trim();
@@ -1823,6 +1835,99 @@ var ToolExecutor = class {
1823
1835
  }
1824
1836
  };
1825
1837
 
1838
+ // src/tools/sensitive-paths.ts
1839
+ import { resolve as resolve3, sep as sep2, basename as basename2 } from "path";
1840
+ import { homedir as homedir2 } from "os";
1841
+ var home = homedir2();
1842
+ function norm(p) {
1843
+ const abs = resolve3(p);
1844
+ return process.platform === "win32" ? abs.toLowerCase() : abs;
1845
+ }
1846
+ function homeRel(p) {
1847
+ const abs = norm(p);
1848
+ const h = norm(home);
1849
+ if (abs === h) return ".";
1850
+ if (abs.startsWith(h + (process.platform === "win32" ? "\\" : "/"))) {
1851
+ return abs.slice(h.length + 1);
1852
+ }
1853
+ return null;
1854
+ }
1855
+ function classifyWritePath(path3) {
1856
+ if (!path3) return { sensitive: false };
1857
+ const abs = norm(path3);
1858
+ const base = basename2(abs);
1859
+ const rel = homeRel(path3);
1860
+ if (rel) {
1861
+ const shellRc = /* @__PURE__ */ new Set([
1862
+ ".bashrc",
1863
+ ".bash_profile",
1864
+ ".bash_login",
1865
+ ".profile",
1866
+ ".zshrc",
1867
+ ".zprofile",
1868
+ ".zshenv",
1869
+ ".zlogin",
1870
+ ".kshrc",
1871
+ ".cshrc",
1872
+ ".tcshrc",
1873
+ ".fish_config"
1874
+ ]);
1875
+ if (shellRc.has(base)) {
1876
+ return { sensitive: true, reason: `shell startup file (${rel})` };
1877
+ }
1878
+ }
1879
+ const sshDir = `${sep2}.ssh${sep2}`;
1880
+ if (abs.includes(sshDir.toLowerCase()) || abs.includes(sshDir)) {
1881
+ return { sensitive: true, reason: `SSH directory (${rel ?? abs})` };
1882
+ }
1883
+ const credPaths = [
1884
+ `${sep2}.aws${sep2}credentials`,
1885
+ `${sep2}.aws${sep2}config`,
1886
+ `${sep2}.gcp${sep2}`,
1887
+ `${sep2}.azure${sep2}`,
1888
+ `${sep2}.config${sep2}gcloud${sep2}`,
1889
+ `${sep2}.kube${sep2}config`,
1890
+ `${sep2}.docker${sep2}config.json`,
1891
+ `${sep2}.netrc`
1892
+ ];
1893
+ for (const c of credPaths) {
1894
+ const lc = process.platform === "win32" ? c.toLowerCase() : c;
1895
+ if (abs.includes(lc)) return { sensitive: true, reason: `cloud/auth credential file (${rel ?? abs})` };
1896
+ }
1897
+ if (rel && (rel === ".aicli" || rel.startsWith(".aicli" + (process.platform === "win32" ? "\\" : "/")))) {
1898
+ if (base === "config.json" || base === "users.json") {
1899
+ return { sensitive: true, reason: `ai-cli internal config (${rel})` };
1900
+ }
1901
+ }
1902
+ if (abs.includes(`${sep2}.git${sep2}hooks${sep2}`) || abs.includes(`${sep2}.git${sep2}hooks${sep2}`.toLowerCase())) {
1903
+ return { sensitive: true, reason: ".git/hooks/* \u2014 runs on next git operation" };
1904
+ }
1905
+ if (abs.includes(`${sep2}.git${sep2}config`) || abs.includes(`${sep2}.git${sep2}config`.toLowerCase())) {
1906
+ return { sensitive: true, reason: ".git/config \u2014 repository identity" };
1907
+ }
1908
+ if (process.platform !== "win32") {
1909
+ const sysPrefixes = ["/etc/", "/usr/bin/", "/usr/sbin/", "/bin/", "/sbin/", "/boot/"];
1910
+ for (const pre of sysPrefixes) {
1911
+ if (abs.startsWith(pre)) return { sensitive: true, reason: `system path (${pre})` };
1912
+ }
1913
+ } else {
1914
+ const winSys = ["c:\\windows\\system32\\", "c:\\windows\\syswow64\\", "c:\\program files\\", "c:\\program files (x86)\\"];
1915
+ for (const pre of winSys) {
1916
+ if (abs.startsWith(pre)) return { sensitive: true, reason: `system path (${pre})` };
1917
+ }
1918
+ }
1919
+ if (abs.includes(`${sep2}cron.d${sep2}`) || abs.startsWith("/etc/cron") || abs.startsWith("/var/spool/cron/")) {
1920
+ return { sensitive: true, reason: "cron scheduling" };
1921
+ }
1922
+ if (rel && (rel.startsWith("Library/LaunchAgents/") || rel.startsWith("Library/LaunchDaemons/"))) {
1923
+ return { sensitive: true, reason: "macOS launch agent" };
1924
+ }
1925
+ return { sensitive: false };
1926
+ }
1927
+ var subAgentGuard = {
1928
+ active: false
1929
+ };
1930
+
1826
1931
  // src/tools/builtin/write-file.ts
1827
1932
  var writeFileTool = {
1828
1933
  definition: {
@@ -1865,6 +1970,13 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
1865
1970
  const encoding = args["encoding"] ?? "utf-8";
1866
1971
  const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
1867
1972
  if (!filePath) throw new ToolError("write_file", "path is required");
1973
+ const verdict = classifyWritePath(filePath);
1974
+ if (verdict.sensitive && subAgentGuard.active) {
1975
+ throw new ToolError(
1976
+ "write_file",
1977
+ `Refused: sub-agents cannot write to sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the write so the user can approve it.`
1978
+ );
1979
+ }
1868
1980
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
1869
1981
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
1870
1982
  mkdirSync(dirname2(filePath), { recursive: true });
@@ -1877,7 +1989,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
1877
1989
  const mode = appendMode ? "appended" : "written";
1878
1990
  void (async () => {
1879
1991
  try {
1880
- const { updateFile } = await import("./indexer-C7QYYHSZ.js");
1992
+ const { updateFile } = await import("./indexer-XGY7XGJM.js");
1881
1993
  await updateFile(process.cwd(), filePath);
1882
1994
  } catch {
1883
1995
  }
@@ -2249,6 +2361,13 @@ Note: Path can be absolute or relative to cwd.`,
2249
2361
  const encoding = args["encoding"] ?? "utf-8";
2250
2362
  if (!filePath) throw new ToolError("edit_file", "path is required");
2251
2363
  if (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
2364
+ const verdict = classifyWritePath(filePath);
2365
+ if (verdict.sensitive && subAgentGuard.active) {
2366
+ throw new ToolError(
2367
+ "edit_file",
2368
+ `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.`
2369
+ );
2370
+ }
2252
2371
  const original = readFileSync4(filePath, encoding);
2253
2372
  if (args["patch"] !== void 0) {
2254
2373
  const patchText = String(args["patch"] ?? "");
@@ -2408,7 +2527,7 @@ function truncatePreview(str, maxLen = 80) {
2408
2527
 
2409
2528
  // src/tools/builtin/list-dir.ts
2410
2529
  import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync6 } from "fs";
2411
- import { join, basename as basename2 } from "path";
2530
+ import { join, basename as basename3 } from "path";
2412
2531
  var listDirTool = {
2413
2532
  definition: {
2414
2533
  name: "list_dir",
@@ -2431,7 +2550,7 @@ var listDirTool = {
2431
2550
  const dirPath = String(args["path"] ?? process.cwd());
2432
2551
  const recursive = Boolean(args["recursive"] ?? false);
2433
2552
  if (!existsSync6(dirPath)) {
2434
- const targetName = basename2(dirPath).toLowerCase();
2553
+ const targetName = basename3(dirPath).toLowerCase();
2435
2554
  const cwd = process.cwd();
2436
2555
  const suggestions = [];
2437
2556
  try {
@@ -2746,7 +2865,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
2746
2865
 
2747
2866
  // src/tools/builtin/glob-files.ts
2748
2867
  import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync8 } from "fs";
2749
- import { join as join3, relative as relative2, basename as basename3 } from "path";
2868
+ import { join as join3, relative as relative2, basename as basename4 } from "path";
2750
2869
  var globFilesTool = {
2751
2870
  definition: {
2752
2871
  name: "glob_files",
@@ -2862,7 +2981,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
2862
2981
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
2863
2982
  } else if (entry.isFile()) {
2864
2983
  const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
2865
- if (regex.test(relPath) || regex.test(basename3(relPath))) {
2984
+ if (regex.test(relPath) || regex.test(basename4(relPath))) {
2866
2985
  try {
2867
2986
  const stat = statSync5(fullPath);
2868
2987
  results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
@@ -2938,7 +3057,7 @@ var runInteractiveTool = {
2938
3057
  PYTHONDONTWRITEBYTECODE: "1"
2939
3058
  };
2940
3059
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
2941
- return new Promise((resolve4) => {
3060
+ return new Promise((resolve5) => {
2942
3061
  const child = spawn2(executable, cmdArgs.map(String), {
2943
3062
  cwd: process.cwd(),
2944
3063
  env,
@@ -2971,22 +3090,22 @@ var runInteractiveTool = {
2971
3090
  setTimeout(writeNextLine, 400);
2972
3091
  const timer = setTimeout(() => {
2973
3092
  child.kill();
2974
- resolve4(`${prefixWarnings}[Timeout after ${timeout}ms]
3093
+ resolve5(`${prefixWarnings}[Timeout after ${timeout}ms]
2975
3094
  ${buildOutput(stdout, stderr)}`);
2976
3095
  }, timeout);
2977
3096
  child.on("close", (code) => {
2978
3097
  clearTimeout(timer);
2979
3098
  const output = buildOutput(stdout, stderr);
2980
3099
  if (code !== 0 && code !== null) {
2981
- resolve4(`${prefixWarnings}Exit code ${code}:
3100
+ resolve5(`${prefixWarnings}Exit code ${code}:
2982
3101
  ${output}`);
2983
3102
  } else {
2984
- resolve4(`${prefixWarnings}${output || "(no output)"}`);
3103
+ resolve5(`${prefixWarnings}${output || "(no output)"}`);
2985
3104
  }
2986
3105
  });
2987
3106
  child.on("error", (err) => {
2988
3107
  clearTimeout(timer);
2989
- resolve4(
3108
+ resolve5(
2990
3109
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
2991
3110
  Hint: On Windows, use the full path to the executable, e.g.:
2992
3111
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -3256,9 +3375,9 @@ Any of these triggers means use save_last_response, NOT write_file:
3256
3375
  // src/tools/builtin/save-memory.ts
3257
3376
  import { existsSync as existsSync9, statSync as statSync6, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
3258
3377
  import { join as join4 } from "path";
3259
- import { homedir as homedir2 } from "os";
3378
+ import { homedir as homedir3 } from "os";
3260
3379
  function getMemoryFilePath() {
3261
- return join4(homedir2(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
3380
+ return join4(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
3262
3381
  }
3263
3382
  function formatTimestamp() {
3264
3383
  const now = /* @__PURE__ */ new Date();
@@ -3282,7 +3401,7 @@ var saveMemoryTool = {
3282
3401
  const content = String(args["content"] ?? "").trim();
3283
3402
  if (!content) throw new ToolError("save_memory", "content is required");
3284
3403
  const memoryPath = getMemoryFilePath();
3285
- const configDir = join4(homedir2(), CONFIG_DIR_NAME);
3404
+ const configDir = join4(homedir3(), CONFIG_DIR_NAME);
3286
3405
  if (!existsSync9(configDir)) {
3287
3406
  mkdirSync3(configDir, { recursive: true });
3288
3407
  }
@@ -3337,7 +3456,7 @@ function promptUser(rl, question) {
3337
3456
  console.log();
3338
3457
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
3339
3458
  process.stdout.write(chalk4.cyan("> "));
3340
- return new Promise((resolve4) => {
3459
+ return new Promise((resolve5) => {
3341
3460
  let completed = false;
3342
3461
  const cleanup = (answer) => {
3343
3462
  if (completed) return;
@@ -3347,7 +3466,7 @@ function promptUser(rl, question) {
3347
3466
  rl.pause();
3348
3467
  rlAny.output = savedOutput;
3349
3468
  askUserContext.prompting = false;
3350
- resolve4(answer);
3469
+ resolve5(answer);
3351
3470
  };
3352
3471
  const onLine = (line) => {
3353
3472
  cleanup(line);
@@ -3844,37 +3963,43 @@ var spawnAgentTool = {
3844
3963
  if (!ctx.provider) {
3845
3964
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
3846
3965
  }
3847
- if (singleTask) {
3848
- const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
3849
- return [
3850
- "## Sub-Agent Result",
3851
- "",
3852
- content,
3853
- "",
3854
- "---",
3855
- `Token usage: ${usage.inputTokens} input, ${usage.outputTokens} output`
3856
- ].join("\n");
3966
+ const guardWasActive = subAgentGuard.active;
3967
+ subAgentGuard.active = true;
3968
+ try {
3969
+ if (singleTask) {
3970
+ const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
3971
+ return [
3972
+ "## Sub-Agent Result",
3973
+ "",
3974
+ content,
3975
+ "",
3976
+ "---",
3977
+ `Token usage: ${usage.inputTokens} input, ${usage.outputTokens} output`
3978
+ ].join("\n");
3979
+ }
3980
+ console.log();
3981
+ console.log(theme.toolCall(`\u{1F916} Spawning ${tasksArr.length} sub-agents in parallel...`));
3982
+ const results = await Promise.all(
3983
+ tasksArr.map((t, i) => runSubAgent(t, maxRounds, i + 1, ctx))
3984
+ );
3985
+ const totalIn = results.reduce((s, r) => s + r.usage.inputTokens, 0);
3986
+ const totalOut = results.reduce((s, r) => s + r.usage.outputTokens, 0);
3987
+ const lines = [`## Parallel Sub-Agent Results (${results.length} agents)`, ""];
3988
+ results.forEach((r, i) => {
3989
+ lines.push(`### Sub-Agent #${i + 1}`);
3990
+ lines.push(`**Task**: ${tasksArr[i].slice(0, 200)}${tasksArr[i].length > 200 ? "..." : ""}`);
3991
+ lines.push("");
3992
+ lines.push(r.content);
3993
+ lines.push("");
3994
+ lines.push(`*Tokens: ${r.usage.inputTokens} in / ${r.usage.outputTokens} out*`);
3995
+ lines.push("");
3996
+ });
3997
+ lines.push("---");
3998
+ lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
3999
+ return lines.join("\n");
4000
+ } finally {
4001
+ subAgentGuard.active = guardWasActive;
3857
4002
  }
3858
- console.log();
3859
- console.log(theme.toolCall(`\u{1F916} Spawning ${tasksArr.length} sub-agents in parallel...`));
3860
- const results = await Promise.all(
3861
- tasksArr.map((t, i) => runSubAgent(t, maxRounds, i + 1, ctx))
3862
- );
3863
- const totalIn = results.reduce((s, r) => s + r.usage.inputTokens, 0);
3864
- const totalOut = results.reduce((s, r) => s + r.usage.outputTokens, 0);
3865
- const lines = [`## Parallel Sub-Agent Results (${results.length} agents)`, ""];
3866
- results.forEach((r, i) => {
3867
- lines.push(`### Sub-Agent #${i + 1}`);
3868
- lines.push(`**Task**: ${tasksArr[i].slice(0, 200)}${tasksArr[i].length > 200 ? "..." : ""}`);
3869
- lines.push("");
3870
- lines.push(r.content);
3871
- lines.push("");
3872
- lines.push(`*Tokens: ${r.usage.inputTokens} in / ${r.usage.outputTokens} out*`);
3873
- lines.push("");
3874
- });
3875
- lines.push("---");
3876
- lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
3877
- return lines.join("\n");
3878
4003
  }
3879
4004
  };
3880
4005
 
@@ -4339,7 +4464,7 @@ ${commitOutput.trim()}`;
4339
4464
  // src/tools/builtin/notebook-edit.ts
4340
4465
  import { readFileSync as readFileSync6, existsSync as existsSync11 } from "fs";
4341
4466
  import { writeFile } from "fs/promises";
4342
- import { resolve as resolve3, extname as extname2 } from "path";
4467
+ import { resolve as resolve4, extname as extname2 } from "path";
4343
4468
  var notebookEditTool = {
4344
4469
  definition: {
4345
4470
  name: "notebook_edit",
@@ -4388,7 +4513,7 @@ var notebookEditTool = {
4388
4513
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
4389
4514
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
4390
4515
  }
4391
- const absPath = resolve3(filePath);
4516
+ const absPath = resolve4(filePath);
4392
4517
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
4393
4518
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
4394
4519
  }
@@ -1,12 +1,12 @@
1
1
  // src/tools/builtin/run-tests.ts
2
- import { execSync } from "child_process";
2
+ import { execSync, spawnSync } from "child_process";
3
3
  import { existsSync, readFileSync, readdirSync } from "fs";
4
4
  import { join } from "path";
5
5
  import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.103";
9
+ var VERSION = "0.4.105";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -73,18 +73,15 @@ Once planning is complete, clearly inform the user: type \`/plan execute\` to be
73
73
  var SUBAGENT_DEFAULT_MAX_ROUNDS = 15;
74
74
  var SUBAGENT_MAX_ROUNDS_LIMIT = 30;
75
75
  var SUBAGENT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
76
- "bash",
77
76
  "read_file",
78
77
  "write_file",
79
78
  "edit_file",
80
79
  "list_dir",
81
80
  "grep_files",
82
81
  "glob_files",
83
- "run_interactive",
84
82
  "web_fetch",
85
83
  "google_search",
86
84
  "write_todos",
87
- "run_tests",
88
85
  "find_symbol",
89
86
  "get_outline",
90
87
  "find_references",
@@ -136,6 +133,7 @@ var AUTHOR_EMAIL = "zhengdong.jin@gmail.com";
136
133
  var DESCRIPTION = "Cross-platform REPL-style AI conversation tool with multi-provider and agentic tool calling support";
137
134
 
138
135
  // src/tools/builtin/run-tests.ts
136
+ var FILTER_WHITELIST = /^[\w.\-/:#\s*?|^()\[\]@+]{1,200}$/;
139
137
  var IS_WINDOWS = platform() === "win32";
140
138
  function detectNodeTestFramework(cwd, pkg) {
141
139
  const devDeps = pkg.devDependencies ?? {};
@@ -365,12 +363,62 @@ function renderColorReport(summary, framework) {
365
363
  }
366
364
  console.log();
367
365
  }
366
+ function needsWindowsShell(file) {
367
+ if (!IS_WINDOWS) return false;
368
+ const lower = file.toLowerCase();
369
+ return lower.endsWith(".cmd") || lower.endsWith(".bat") || lower === "mvn" || lower === "gradle" || lower === "npm" || lower === "npx";
370
+ }
371
+ function buildArgv(detected, filter) {
372
+ switch (detected.type) {
373
+ case "java": {
374
+ const parts2 = detected.command.split(/\s+/);
375
+ const file = parts2[0];
376
+ const rest = parts2.slice(1);
377
+ const args = [...rest];
378
+ if (filter) {
379
+ args.push(detected.framework.startsWith("Maven") ? `-Dtest=${filter}` : `--tests=${filter}`);
380
+ }
381
+ return { file, args };
382
+ }
383
+ case "python":
384
+ return { file: "pytest", args: filter ? ["-v", "-k", filter] : ["-v"] };
385
+ case "rust":
386
+ return { file: "cargo", args: filter ? ["test", filter] : ["test"] };
387
+ case "go":
388
+ return filter ? { file: "go", args: ["test", "./...", "-run", filter] } : { file: "go", args: ["test", "./..."] };
389
+ case "node": {
390
+ if (detected.framework === "vitest") {
391
+ return { file: "npx", args: filter ? ["vitest", "run", "-t", filter] : ["vitest", "run"] };
392
+ }
393
+ if (detected.framework === "jest") {
394
+ return { file: "npx", args: filter ? ["jest", "-t", filter] : ["jest"] };
395
+ }
396
+ if (detected.framework === "mocha") {
397
+ return { file: "npx", args: filter ? ["mocha", "--grep", filter] : ["mocha"] };
398
+ }
399
+ if (detected.framework === "playwright") {
400
+ return { file: "npx", args: filter ? ["playwright", "test", "--grep", filter] : ["playwright", "test"] };
401
+ }
402
+ if (detected.framework === "ava") {
403
+ return { file: "npx", args: filter ? ["ava", "--match", filter] : ["ava"] };
404
+ }
405
+ return filter ? { file: "npm", args: ["test", "--", "--grep", filter] } : { file: "npm", args: ["test"] };
406
+ }
407
+ }
408
+ const parts = detected.command.split(/\s+/);
409
+ return { file: parts[0], args: parts.slice(1) };
410
+ }
368
411
  async function executeTests(args) {
369
412
  const cwd = process.cwd();
370
413
  const customCmd = args["command"] ? String(args["command"]).trim() : "";
371
414
  const filter = args["filter"] ? String(args["filter"]).trim() : "";
415
+ if (filter && !FILTER_WHITELIST.test(filter)) {
416
+ return `Error: filter contains characters that are not allowed in test names. Allowed: word chars, dots, dashes, slashes, colons, parens, spaces, and basic glob/regex meta (* ? | ^). Rejected: quotes, semicolons, backticks, redirects, and shell metacharacters.
417
+ Got: ${filter.slice(0, 60)}${filter.length > 60 ? "..." : ""}`;
418
+ }
372
419
  let command;
373
420
  let framework;
421
+ let argv = null;
374
422
  if (customCmd) {
375
423
  command = customCmd;
376
424
  framework = "custom";
@@ -381,50 +429,52 @@ async function executeTests(args) {
381
429
  }
382
430
  command = detected.command;
383
431
  framework = detected.framework;
384
- if (filter) {
385
- if (detected.type === "java" && command.includes("mvn")) {
386
- command += ` -Dtest="${filter}"`;
387
- } else if (detected.type === "python") {
388
- command += ` -k "${filter}"`;
389
- } else if (detected.type === "rust") {
390
- command += ` ${filter}`;
391
- } else if (detected.type === "go") {
392
- command = `go test ./... -run "${filter}"`;
393
- } else if (detected.type === "node") {
394
- if (detected.framework === "vitest") {
395
- command += ` -t "${filter}"`;
396
- } else if (detected.framework === "jest") {
397
- command += ` -t "${filter}"`;
398
- } else if (detected.framework === "mocha") {
399
- command += ` --grep "${filter}"`;
400
- } else if (detected.framework === "playwright") {
401
- command += ` --grep "${filter}"`;
402
- } else {
403
- command += ` -- --grep "${filter}"`;
404
- }
405
- }
406
- }
432
+ argv = buildArgv(detected, filter);
407
433
  }
408
434
  let output;
409
435
  let exitCode = 0;
410
- try {
411
- const buf = execSync(command, {
436
+ if (argv) {
437
+ const r = spawnSync(argv.file, argv.args, {
412
438
  cwd,
413
439
  timeout: TEST_TIMEOUT,
414
- encoding: "buffer",
415
440
  stdio: ["pipe", "pipe", "pipe"],
441
+ // shell:false is the default for spawnSync; on Windows .cmd/.bat
442
+ // requires shell:true, so we opt-in only for known wrapper names.
443
+ shell: needsWindowsShell(argv.file),
416
444
  env: {
417
445
  ...process.env,
418
446
  ...IS_WINDOWS ? {} : { FORCE_COLOR: "0" }
419
- }
447
+ },
448
+ encoding: "utf-8",
449
+ maxBuffer: 64 * 1024 * 1024
420
450
  });
421
- output = buf.toString("utf-8");
422
- } catch (err) {
423
- const e = err;
424
- exitCode = e.status ?? 1;
425
- const stdout = e.stdout?.toString("utf-8") ?? "";
426
- const stderr = e.stderr?.toString("utf-8") ?? "";
427
- output = stdout + (stderr ? "\n" + stderr : "");
451
+ if (r.error) {
452
+ output = `Failed to launch test runner: ${r.error.message}`;
453
+ exitCode = 1;
454
+ } else {
455
+ exitCode = r.status ?? 1;
456
+ output = (r.stdout ?? "") + (r.stderr ? "\n" + r.stderr : "");
457
+ }
458
+ } else {
459
+ try {
460
+ const buf = execSync(command, {
461
+ cwd,
462
+ timeout: TEST_TIMEOUT,
463
+ encoding: "buffer",
464
+ stdio: ["pipe", "pipe", "pipe"],
465
+ env: {
466
+ ...process.env,
467
+ ...IS_WINDOWS ? {} : { FORCE_COLOR: "0" }
468
+ }
469
+ });
470
+ output = buf.toString("utf-8");
471
+ } catch (err) {
472
+ const e = err;
473
+ exitCode = e.status ?? 1;
474
+ const stdout = e.stdout?.toString("utf-8") ?? "";
475
+ const stderr = e.stderr?.toString("utf-8") ?? "";
476
+ output = stdout + (stderr ? "\n" + stderr : "");
477
+ }
428
478
  }
429
479
  let summary = {
430
480
  tests: 0,