jinzd-ai-cli 0.4.26 → 0.4.28

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.
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.4.25";
11
+ var VERSION = "0.4.28";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -9,7 +9,7 @@ import {
9
9
  SUBAGENT_DEFAULT_MAX_ROUNDS,
10
10
  SUBAGENT_MAX_ROUNDS_LIMIT,
11
11
  runTestsTool
12
- } from "./chunk-AHH5I2U6.js";
12
+ } from "./chunk-7SW4XRDJ.js";
13
13
 
14
14
  // src/tools/builtin/bash.ts
15
15
  import { execSync } from "child_process";
@@ -402,6 +402,110 @@ import { readFileSync as readFileSync2, existsSync as existsSync3, statSync as s
402
402
  import { execSync as execSync2 } from "child_process";
403
403
  import { extname, resolve as resolve2, basename, sep, dirname } from "path";
404
404
  import { homedir } from "os";
405
+
406
+ // src/tools/builtin/notebook-utils.ts
407
+ function cellSourceToString(source) {
408
+ if (!source) return "";
409
+ if (Array.isArray(source)) return source.join("");
410
+ return source;
411
+ }
412
+ function stringToCellSource(content) {
413
+ if (content.length === 0) return [];
414
+ const lines = content.split("\n");
415
+ return lines.map((l, i) => i < lines.length - 1 ? l + "\n" : l).filter((l, i, arr) => !(i === arr.length - 1 && l === ""));
416
+ }
417
+ function parseNotebook(content) {
418
+ let data;
419
+ try {
420
+ data = JSON.parse(content);
421
+ } catch (err) {
422
+ throw new Error(`Invalid notebook JSON: ${err.message}`);
423
+ }
424
+ const nb = data;
425
+ if (!nb || !Array.isArray(nb.cells)) {
426
+ throw new Error("Invalid notebook format: missing cells array");
427
+ }
428
+ return {
429
+ cells: nb.cells,
430
+ metadata: nb.metadata,
431
+ nbformat: nb.nbformat ?? 4,
432
+ nbformat_minor: nb.nbformat_minor ?? 5
433
+ };
434
+ }
435
+ function extractOutputText(outputs) {
436
+ if (!outputs || outputs.length === 0) return "";
437
+ const parts = [];
438
+ for (const out of outputs) {
439
+ if (out.output_type === "stream") {
440
+ const text = Array.isArray(out.text) ? out.text.join("") : out.text ?? "";
441
+ if (text) parts.push(text);
442
+ } else if (out.output_type === "error") {
443
+ parts.push(`ERROR: ${out.ename}: ${out.evalue}`);
444
+ if (out.traceback) parts.push(out.traceback.join("\n"));
445
+ } else if (out.output_type === "execute_result" || out.output_type === "display_data") {
446
+ const data = out.data ?? {};
447
+ const plain = data["text/plain"];
448
+ if (plain) {
449
+ parts.push(Array.isArray(plain) ? plain.join("") : String(plain));
450
+ } else if (data["text/html"]) {
451
+ parts.push("[HTML output omitted]");
452
+ } else if (data["image/png"] || data["image/jpeg"]) {
453
+ parts.push("[Image output omitted]");
454
+ }
455
+ }
456
+ }
457
+ return parts.join("\n").trim();
458
+ }
459
+ function renderNotebookAsText(nb, filePath) {
460
+ const lines = [`[Notebook: ${filePath} | ${nb.cells.length} cells | nbformat ${nb.nbformat}.${nb.nbformat_minor}]`, ""];
461
+ const language = nb.metadata?.["kernelspec"]?.["language"] ?? "python";
462
+ nb.cells.forEach((cell, idx) => {
463
+ const cellNum = idx + 1;
464
+ const type = cell.cell_type;
465
+ const source = cellSourceToString(cell.source);
466
+ if (type === "markdown") {
467
+ lines.push(`## Cell ${cellNum} [markdown]`);
468
+ lines.push(source);
469
+ lines.push("");
470
+ } else if (type === "code") {
471
+ const execLabel = cell.execution_count != null ? ` (execution #${cell.execution_count})` : "";
472
+ lines.push(`## Cell ${cellNum} [code${execLabel}]`);
473
+ lines.push("```" + language);
474
+ lines.push(source);
475
+ lines.push("```");
476
+ const outputText = extractOutputText(cell.outputs);
477
+ if (outputText) {
478
+ lines.push("Output:");
479
+ lines.push("```");
480
+ lines.push(outputText);
481
+ lines.push("```");
482
+ }
483
+ lines.push("");
484
+ } else if (type === "raw") {
485
+ lines.push(`## Cell ${cellNum} [raw]`);
486
+ lines.push(source);
487
+ lines.push("");
488
+ }
489
+ });
490
+ return lines.join("\n");
491
+ }
492
+ function serializeNotebook(nb) {
493
+ return JSON.stringify(nb, null, 1);
494
+ }
495
+ function createCell(cellType, content) {
496
+ const cell = {
497
+ cell_type: cellType,
498
+ source: stringToCellSource(content),
499
+ metadata: {}
500
+ };
501
+ if (cellType === "code") {
502
+ cell.outputs = [];
503
+ cell.execution_count = null;
504
+ }
505
+ return cell;
506
+ }
507
+
508
+ // src/tools/builtin/read-file.ts
405
509
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
406
510
  function getSensitiveWarning(normalizedPath) {
407
511
  const home = homedir();
@@ -593,6 +697,16 @@ Please use read_file to read the above files.`;
593
697
  Cannot extract text from this PDF (requires pdftotext or pdfminer.six).
594
698
  Suggestion: read existing text versions (.md / .txt) in the project, or install extraction tools via bash and retry.`;
595
699
  }
700
+ if (ext === ".ipynb") {
701
+ try {
702
+ const raw = readFileSync2(normalizedPath, "utf-8");
703
+ const nb = parseNotebook(raw);
704
+ return renderNotebookAsText(nb, filePath);
705
+ } catch (err) {
706
+ return `[Notebook parse error: ${filePath}]
707
+ ${err.message}`;
708
+ }
709
+ }
596
710
  if (BINARY_EXTENSIONS.has(ext)) {
597
711
  return `[Binary file: ${filePath} (${ext})]
598
712
  This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
@@ -627,7 +741,7 @@ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
627
741
 
628
742
  // src/tools/types.ts
629
743
  function isFileWriteTool(name) {
630
- return name === "write_file" || name === "edit_file";
744
+ return name === "write_file" || name === "edit_file" || name === "notebook_edit";
631
745
  }
632
746
  function getDangerLevel(toolName, args) {
633
747
  if (toolName.startsWith("mcp__")) return "safe";
@@ -653,6 +767,9 @@ function getDangerLevel(toolName, args) {
653
767
  }
654
768
  if (toolName === "task_create" || toolName === "task_stop") return "write";
655
769
  if (toolName === "task_list") return "safe";
770
+ if (toolName === "git_commit") return "write";
771
+ if (toolName === "git_status" || toolName === "git_diff" || toolName === "git_log") return "safe";
772
+ if (toolName === "notebook_edit") return "write";
656
773
  if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
657
774
  return "write";
658
775
  }
@@ -877,18 +994,69 @@ function setContextWindow(contextWindow) {
877
994
  function getActiveMaxChars() {
878
995
  return activeMaxChars;
879
996
  }
997
+ function collapseRepeatedLines(content) {
998
+ const lines = content.split("\n");
999
+ if (lines.length < 5) return { collapsed: content, savedChars: 0 };
1000
+ const out = [];
1001
+ let i = 0;
1002
+ let savedChars = 0;
1003
+ while (i < lines.length) {
1004
+ const cur = lines[i];
1005
+ let runEnd = i + 1;
1006
+ while (runEnd < lines.length && lines[runEnd] === cur) runEnd++;
1007
+ const runLen = runEnd - i;
1008
+ if (runLen >= 4 && cur.length > 0) {
1009
+ out.push(cur);
1010
+ const omitted = runLen - 1;
1011
+ out.push(`[... previous line repeated ${omitted} more times ...]`);
1012
+ savedChars += omitted * (cur.length + 1) - 50;
1013
+ } else {
1014
+ for (let k = i; k < runEnd; k++) out.push(lines[k]);
1015
+ }
1016
+ i = runEnd;
1017
+ }
1018
+ return { collapsed: out.join("\n"), savedChars: Math.max(0, savedChars) };
1019
+ }
1020
+ function snapToLineBoundary(content, target, direction) {
1021
+ const maxSearch = 200;
1022
+ if (direction === "back") {
1023
+ for (let i = 0; i < maxSearch && target - i > 0; i++) {
1024
+ if (content[target - i] === "\n") return target - i;
1025
+ }
1026
+ return target;
1027
+ } else {
1028
+ for (let i = 0; i < maxSearch && target + i < content.length; i++) {
1029
+ if (content[target + i] === "\n") return target + i + 1;
1030
+ }
1031
+ return target;
1032
+ }
1033
+ }
880
1034
  function truncateOutput(content, toolName, maxChars) {
881
1035
  const limit = maxChars ?? activeMaxChars;
882
1036
  if (content.length <= limit) return content;
1037
+ const { collapsed, savedChars } = collapseRepeatedLines(content);
1038
+ if (collapsed.length <= limit) {
1039
+ return savedChars > 0 ? collapsed + `
1040
+
1041
+ [Note: collapsed ${savedChars} chars of repeated lines]` : collapsed;
1042
+ }
1043
+ const working = collapsed;
1044
+ const totalLines = working.split("\n").length;
883
1045
  const keepHead = Math.floor(limit * 0.7);
884
1046
  const keepTail = Math.floor(limit * 0.2);
885
- const omitted = content.length - keepHead - keepTail;
886
- const lines = content.split("\n").length;
887
- const head = content.slice(0, keepHead);
888
- const tail = content.slice(content.length - keepTail);
1047
+ const headEnd = snapToLineBoundary(working, keepHead, "back");
1048
+ const tailStart = snapToLineBoundary(working, working.length - keepTail, "forward");
1049
+ const head = working.slice(0, headEnd);
1050
+ const tail = working.slice(tailStart);
1051
+ const headLines = head.split("\n").length;
1052
+ const tailLines = tail.split("\n").length;
1053
+ const omittedLines = Math.max(0, totalLines - headLines - tailLines);
1054
+ const omittedChars = working.length - head.length - tail.length;
1055
+ const repeatNote = savedChars > 0 ? ` (plus ${savedChars} chars of repeated lines collapsed)` : "";
1056
+ const hint = toolName === "bash" ? "narrow the command scope (e.g., head/tail/grep) or redirect to a file" : toolName === "read_file" ? "read specific line ranges with offset/limit" : toolName === "grep_files" ? "narrow the pattern or use max_results" : "request a narrower query";
889
1057
  return head + `
890
1058
 
891
- ... [Output truncated: ${content.length} chars / ${lines} lines total, ${omitted} chars omitted. Use read_file to view full content in segments` + (toolName === "bash" ? ", or narrow the command scope" : "") + `] ...
1059
+ ... [Output truncated: ${working.length} chars / ${totalLines} lines total. Showing first ${headLines} lines and last ${tailLines} lines; ${omittedLines} lines / ${omittedChars} chars omitted in the middle${repeatNote}. To see the missing content, ${hint}.] ...
892
1060
 
893
1061
  ` + tail;
894
1062
  }
@@ -1214,7 +1382,7 @@ var ToolExecutor = class {
1214
1382
  rl.resume();
1215
1383
  process.stdout.write(prompt);
1216
1384
  this.confirming = true;
1217
- return new Promise((resolve4) => {
1385
+ return new Promise((resolve5) => {
1218
1386
  let completed = false;
1219
1387
  const cleanup = (result) => {
1220
1388
  if (completed) return;
@@ -1224,7 +1392,7 @@ var ToolExecutor = class {
1224
1392
  rl.pause();
1225
1393
  rlAny.output = savedOutput;
1226
1394
  this.confirming = false;
1227
- resolve4(result);
1395
+ resolve5(result);
1228
1396
  };
1229
1397
  const onLine = (line) => {
1230
1398
  const trimmed = line.trim();
@@ -1394,7 +1562,7 @@ var ToolExecutor = class {
1394
1562
  rl.resume();
1395
1563
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
1396
1564
  this.confirming = true;
1397
- return new Promise((resolve4) => {
1565
+ return new Promise((resolve5) => {
1398
1566
  let completed = false;
1399
1567
  const cleanup = (answer) => {
1400
1568
  if (completed) return;
@@ -1404,7 +1572,7 @@ var ToolExecutor = class {
1404
1572
  rl.pause();
1405
1573
  rlAny.output = savedOutput;
1406
1574
  this.confirming = false;
1407
- resolve4(answer === "y");
1575
+ resolve5(answer === "y");
1408
1576
  };
1409
1577
  const onLine = (line) => {
1410
1578
  const trimmed = line.trim();
@@ -2265,7 +2433,7 @@ var runInteractiveTool = {
2265
2433
  PYTHONDONTWRITEBYTECODE: "1"
2266
2434
  };
2267
2435
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
2268
- return new Promise((resolve4) => {
2436
+ return new Promise((resolve5) => {
2269
2437
  const child = spawn(executable, cmdArgs.map(String), {
2270
2438
  cwd: process.cwd(),
2271
2439
  env,
@@ -2298,22 +2466,22 @@ var runInteractiveTool = {
2298
2466
  setTimeout(writeNextLine, 400);
2299
2467
  const timer = setTimeout(() => {
2300
2468
  child.kill();
2301
- resolve4(`${prefixWarnings}[Timeout after ${timeout}ms]
2469
+ resolve5(`${prefixWarnings}[Timeout after ${timeout}ms]
2302
2470
  ${buildOutput(stdout, stderr)}`);
2303
2471
  }, timeout);
2304
2472
  child.on("close", (code) => {
2305
2473
  clearTimeout(timer);
2306
2474
  const output = buildOutput(stdout, stderr);
2307
2475
  if (code !== 0 && code !== null) {
2308
- resolve4(`${prefixWarnings}Exit code ${code}:
2476
+ resolve5(`${prefixWarnings}Exit code ${code}:
2309
2477
  ${output}`);
2310
2478
  } else {
2311
- resolve4(`${prefixWarnings}${output || "(no output)"}`);
2479
+ resolve5(`${prefixWarnings}${output || "(no output)"}`);
2312
2480
  }
2313
2481
  });
2314
2482
  child.on("error", (err) => {
2315
2483
  clearTimeout(timer);
2316
- resolve4(
2484
+ resolve5(
2317
2485
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
2318
2486
  Hint: On Windows, use the full path to the executable, e.g.:
2319
2487
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -2658,7 +2826,7 @@ function promptUser(rl, question) {
2658
2826
  console.log();
2659
2827
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
2660
2828
  process.stdout.write(chalk4.cyan("> "));
2661
- return new Promise((resolve4) => {
2829
+ return new Promise((resolve5) => {
2662
2830
  let completed = false;
2663
2831
  const cleanup = (answer) => {
2664
2832
  if (completed) return;
@@ -2668,7 +2836,7 @@ function promptUser(rl, question) {
2668
2836
  rl.pause();
2669
2837
  rlAny.output = savedOutput;
2670
2838
  askUserContext.prompting = false;
2671
- resolve4(answer);
2839
+ resolve5(answer);
2672
2840
  };
2673
2841
  const onLine = (line) => {
2674
2842
  cleanup(line);
@@ -2947,12 +3115,18 @@ var spawnAgentContext = {
2947
3115
  configManager: null
2948
3116
  };
2949
3117
  var PREFIX = theme.dim(" \u2503 ");
3118
+ function agentPrefix(agentIndex) {
3119
+ if (agentIndex === null) return PREFIX;
3120
+ return theme.dim(` \u2503${agentIndex} `);
3121
+ }
2950
3122
  var SubAgentExecutor = class {
2951
- constructor(registry) {
3123
+ constructor(registry, agentIndex = null) {
2952
3124
  this.registry = registry;
3125
+ this.prefix = agentPrefix(agentIndex);
2953
3126
  }
2954
3127
  round = 0;
2955
3128
  totalRounds = 0;
3129
+ prefix = PREFIX;
2956
3130
  setRoundInfo(current, total) {
2957
3131
  this.round = current;
2958
3132
  this.totalRounds = total;
@@ -3000,14 +3174,14 @@ var SubAgentExecutor = class {
3000
3174
  // ── 带前缀的终端输出 ──
3001
3175
  printPrefixed(text) {
3002
3176
  for (const line of text.split("\n")) {
3003
- console.log(PREFIX + line);
3177
+ console.log(this.prefix + line);
3004
3178
  }
3005
3179
  }
3006
3180
  printToolCall(call, dangerLevel) {
3007
- console.log(PREFIX);
3181
+ console.log(this.prefix);
3008
3182
  const icon = dangerLevel === "write" ? theme.warning("\u270E Tool: ") : theme.toolCall("\u2699 Tool: ");
3009
3183
  const roundBadge = this.totalRounds > 0 ? theme.dim(` [${this.round}/${this.totalRounds}]`) : "";
3010
- console.log(PREFIX + icon + call.name + roundBadge);
3184
+ console.log(this.prefix + icon + call.name + roundBadge);
3011
3185
  for (const [key, val] of Object.entries(call.arguments)) {
3012
3186
  let valStr;
3013
3187
  if (Array.isArray(val)) {
@@ -3018,22 +3192,22 @@ var SubAgentExecutor = class {
3018
3192
  } else {
3019
3193
  valStr = String(val);
3020
3194
  }
3021
- console.log(PREFIX + theme.dim(` ${key}: `) + valStr);
3195
+ console.log(this.prefix + theme.dim(` ${key}: `) + valStr);
3022
3196
  }
3023
3197
  }
3024
3198
  printToolResult(name, content, isError, wasTruncated) {
3025
3199
  if (isError) {
3026
3200
  console.log(
3027
- PREFIX + theme.error(`\u26A0 ${name} error: `) + theme.dim(content.slice(0, 300))
3201
+ this.prefix + theme.error(`\u26A0 ${name} error: `) + theme.dim(content.slice(0, 300))
3028
3202
  );
3029
3203
  } else {
3030
3204
  const lines = content.split("\n");
3031
3205
  const maxLines = name === "run_interactive" ? 40 : 8;
3032
3206
  const preview = lines.slice(0, maxLines);
3033
- const prefixedPreview = preview.map((l) => PREFIX + " " + theme.dim(l)).join("\n");
3034
- const moreLines = lines.length > maxLines ? "\n" + PREFIX + theme.dim(` ... (${lines.length - maxLines} more lines)`) : "";
3035
- const truncNote = wasTruncated ? "\n" + PREFIX + theme.warning(` \u26A1 Output truncated`) : "";
3036
- console.log(PREFIX + theme.success("\u2713 Result:"));
3207
+ const prefixedPreview = preview.map((l) => this.prefix + " " + theme.dim(l)).join("\n");
3208
+ const moreLines = lines.length > maxLines ? "\n" + this.prefix + theme.dim(` ... (${lines.length - maxLines} more lines)`) : "";
3209
+ const truncNote = wasTruncated ? "\n" + this.prefix + theme.warning(` \u26A1 Output truncated`) : "";
3210
+ console.log(this.prefix + theme.success("\u2713 Result:"));
3037
3211
  console.log(prefixedPreview + moreLines + truncNote);
3038
3212
  }
3039
3213
  }
@@ -3061,22 +3235,26 @@ ${task}
3061
3235
  );
3062
3236
  return parts.join("\n\n---\n\n");
3063
3237
  }
3064
- function printSubAgentHeader(task, maxRounds) {
3238
+ function printSubAgentHeader(task, maxRounds, agentIndex = null) {
3239
+ const prefix = agentPrefix(agentIndex);
3065
3240
  console.log();
3066
3241
  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"));
3067
- console.log(PREFIX + theme.toolCall("\u{1F916} Sub-Agent Spawned"));
3242
+ const label = agentIndex !== null ? `\u{1F916} Sub-Agent #${agentIndex} Spawned` : "\u{1F916} Sub-Agent Spawned";
3243
+ console.log(prefix + theme.toolCall(label));
3068
3244
  console.log(
3069
- PREFIX + theme.dim("Task: ") + (task.slice(0, 120) + (task.length > 120 ? "..." : ""))
3245
+ prefix + theme.dim("Task: ") + (task.slice(0, 120) + (task.length > 120 ? "..." : ""))
3070
3246
  );
3071
- console.log(PREFIX + theme.dim(`Max rounds: ${maxRounds}`));
3247
+ console.log(prefix + theme.dim(`Max rounds: ${maxRounds}`));
3072
3248
  console.log(theme.dim(" \u2503"));
3073
3249
  }
3074
- function printSubAgentFooter(usage) {
3075
- console.log(PREFIX);
3076
- console.log(PREFIX + theme.toolCall("Sub-Agent Complete"));
3250
+ function printSubAgentFooter(usage, agentIndex = null) {
3251
+ const prefix = agentPrefix(agentIndex);
3252
+ console.log(prefix);
3253
+ const label = agentIndex !== null ? `Sub-Agent #${agentIndex} Complete` : "Sub-Agent Complete";
3254
+ console.log(prefix + theme.toolCall(label));
3077
3255
  if (usage.inputTokens > 0 || usage.outputTokens > 0) {
3078
3256
  console.log(
3079
- PREFIX + theme.dim(
3257
+ prefix + theme.dim(
3080
3258
  `Tokens: ${usage.inputTokens} in / ${usage.outputTokens} out`
3081
3259
  )
3082
3260
  );
@@ -3084,27 +3262,114 @@ function printSubAgentFooter(usage) {
3084
3262
  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"));
3085
3263
  console.log();
3086
3264
  }
3265
+ async function runSubAgent(task, maxRounds, agentIndex, ctx) {
3266
+ if (!ctx.provider) {
3267
+ throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
3268
+ }
3269
+ const subRegistry = new ToolRegistry();
3270
+ for (const tool of subRegistry.listAll()) {
3271
+ if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name)) {
3272
+ subRegistry.unregister(tool.definition.name);
3273
+ }
3274
+ }
3275
+ const subExecutor = new SubAgentExecutor(subRegistry, agentIndex);
3276
+ const subMessages = [
3277
+ { role: "user", content: task, timestamp: /* @__PURE__ */ new Date() }
3278
+ ];
3279
+ const subSystemPrompt = buildSubAgentSystemPrompt(task, ctx.systemPrompt);
3280
+ const toolDefs = subRegistry.getDefinitions();
3281
+ const extraMessages = [];
3282
+ const totalUsage = { inputTokens: 0, outputTokens: 0 };
3283
+ let finalContent = "";
3284
+ printSubAgentHeader(task, maxRounds, agentIndex);
3285
+ try {
3286
+ for (let round = 0; round < maxRounds; round++) {
3287
+ subExecutor.setRoundInfo(round + 1, maxRounds);
3288
+ const result = await ctx.provider.chatWithTools(
3289
+ {
3290
+ messages: subMessages,
3291
+ model: ctx.model,
3292
+ systemPrompt: subSystemPrompt,
3293
+ stream: false,
3294
+ temperature: ctx.modelParams.temperature,
3295
+ maxTokens: ctx.modelParams.maxTokens,
3296
+ timeout: ctx.modelParams.timeout,
3297
+ thinking: ctx.modelParams.thinking,
3298
+ ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
3299
+ },
3300
+ toolDefs
3301
+ );
3302
+ if (result.usage) {
3303
+ totalUsage.inputTokens += result.usage.inputTokens;
3304
+ totalUsage.outputTokens += result.usage.outputTokens;
3305
+ }
3306
+ if ("content" in result) {
3307
+ finalContent = result.content;
3308
+ break;
3309
+ }
3310
+ if (ctx.configManager) {
3311
+ googleSearchContext.configManager = ctx.configManager;
3312
+ }
3313
+ const toolResults = await subExecutor.executeAll(result.toolCalls);
3314
+ const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
3315
+ const newMsgs = ctx.provider.buildToolResultMessages(
3316
+ result.toolCalls,
3317
+ toolResults,
3318
+ reasoningContent
3319
+ );
3320
+ extraMessages.push(...newMsgs);
3321
+ }
3322
+ if (!finalContent) {
3323
+ finalContent = `(Sub-agent reached maximum rounds (${maxRounds}) without producing a final response)`;
3324
+ }
3325
+ } catch (err) {
3326
+ const errMsg = err instanceof Error ? err.message : String(err);
3327
+ finalContent = `(Sub-agent error: ${errMsg})`;
3328
+ process.stderr.write(
3329
+ `
3330
+ [spawn_agent] Error in sub-agent loop: ${errMsg}
3331
+ `
3332
+ );
3333
+ }
3334
+ printSubAgentFooter(totalUsage, agentIndex);
3335
+ return { content: finalContent, usage: totalUsage };
3336
+ }
3087
3337
  var spawnAgentTool = {
3088
3338
  definition: {
3089
3339
  name: "spawn_agent",
3090
- description: "Delegate a specific subtask to an independent sub-agent. The 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. Returns a summary of the sub-agent execution result.",
3340
+ 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.",
3091
3341
  parameters: {
3092
3342
  task: {
3093
3343
  type: "string",
3094
- description: "Task description for the sub-agent. Must include all necessary context: file paths, requirements, constraints, expected output. The sub-agent cannot access the parent conversation history; all info must be in this parameter.",
3095
- required: true
3344
+ description: "Single task description. Must include all necessary context: file paths, requirements, constraints, expected output. The sub-agent cannot access the parent conversation history; all info must be in this parameter. Use this OR `tasks`, not both.",
3345
+ required: false
3346
+ },
3347
+ tasks: {
3348
+ type: "array",
3349
+ description: "Array of task descriptions for parallel execution. Each entry spawns an independent sub-agent that runs concurrently. Use this only for TRULY independent tasks (no shared state, no order dependencies). Each task must be fully self-contained. Use this OR `task`, not both.",
3350
+ items: {
3351
+ type: "string",
3352
+ description: "A single self-contained task description."
3353
+ },
3354
+ required: false
3096
3355
  },
3097
3356
  max_rounds: {
3098
3357
  type: "number",
3099
- description: "Max tool-call rounds for the sub-agent (1-15, default 10). Use fewer for simple tasks, more for complex ones.",
3358
+ description: "Max tool-call rounds per sub-agent (1-15, default 10). Applied to each sub-agent in parallel mode.",
3100
3359
  required: false
3101
3360
  }
3102
3361
  },
3103
3362
  dangerous: false
3104
3363
  },
3105
3364
  async execute(args) {
3106
- const task = String(args["task"] ?? "").trim();
3107
- if (!task) throw new ToolError("spawn_agent", "task parameter is required");
3365
+ const singleTask = typeof args["task"] === "string" ? args["task"].trim() : "";
3366
+ const tasksArr = Array.isArray(args["tasks"]) ? args["tasks"].map((t) => String(t ?? "").trim()).filter(Boolean) : [];
3367
+ if (!singleTask && tasksArr.length === 0) {
3368
+ throw new ToolError("spawn_agent", "either `task` or `tasks` parameter is required");
3369
+ }
3370
+ if (singleTask && tasksArr.length > 0) {
3371
+ throw new ToolError("spawn_agent", "use `task` OR `tasks`, not both");
3372
+ }
3108
3373
  const rawMaxRounds = Number(args["max_rounds"] ?? SUBAGENT_DEFAULT_MAX_ROUNDS);
3109
3374
  const maxRounds = Math.min(
3110
3375
  Math.max(Math.round(rawMaxRounds), 1),
@@ -3114,80 +3379,36 @@ var spawnAgentTool = {
3114
3379
  if (!ctx.provider) {
3115
3380
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
3116
3381
  }
3117
- const subRegistry = new ToolRegistry();
3118
- for (const tool of subRegistry.listAll()) {
3119
- if (!SUBAGENT_ALLOWED_TOOLS.has(tool.definition.name)) {
3120
- subRegistry.unregister(tool.definition.name);
3121
- }
3122
- }
3123
- const subExecutor = new SubAgentExecutor(subRegistry);
3124
- const subMessages = [
3125
- { role: "user", content: task, timestamp: /* @__PURE__ */ new Date() }
3126
- ];
3127
- const subSystemPrompt = buildSubAgentSystemPrompt(task, ctx.systemPrompt);
3128
- const toolDefs = subRegistry.getDefinitions();
3129
- const extraMessages = [];
3130
- const totalUsage = { inputTokens: 0, outputTokens: 0 };
3131
- let finalContent = "";
3132
- printSubAgentHeader(task, maxRounds);
3133
- try {
3134
- for (let round = 0; round < maxRounds; round++) {
3135
- subExecutor.setRoundInfo(round + 1, maxRounds);
3136
- const result = await ctx.provider.chatWithTools(
3137
- {
3138
- messages: subMessages,
3139
- model: ctx.model,
3140
- systemPrompt: subSystemPrompt,
3141
- stream: false,
3142
- temperature: ctx.modelParams.temperature,
3143
- maxTokens: ctx.modelParams.maxTokens,
3144
- timeout: ctx.modelParams.timeout,
3145
- thinking: ctx.modelParams.thinking,
3146
- ...extraMessages.length > 0 ? { _extraMessages: extraMessages } : {}
3147
- },
3148
- toolDefs
3149
- );
3150
- if (result.usage) {
3151
- totalUsage.inputTokens += result.usage.inputTokens;
3152
- totalUsage.outputTokens += result.usage.outputTokens;
3153
- }
3154
- if ("content" in result) {
3155
- finalContent = result.content;
3156
- break;
3157
- }
3158
- if (ctx.configManager) {
3159
- googleSearchContext.configManager = ctx.configManager;
3160
- }
3161
- const toolResults = await subExecutor.executeAll(result.toolCalls);
3162
- const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
3163
- const newMsgs = ctx.provider.buildToolResultMessages(
3164
- result.toolCalls,
3165
- toolResults,
3166
- reasoningContent
3167
- );
3168
- extraMessages.push(...newMsgs);
3169
- }
3170
- if (!finalContent) {
3171
- finalContent = `(Sub-agent reached maximum rounds (${maxRounds}) without producing a final response)`;
3172
- }
3173
- } catch (err) {
3174
- const errMsg = err instanceof Error ? err.message : String(err);
3175
- finalContent = `(Sub-agent error: ${errMsg})`;
3176
- process.stderr.write(
3177
- `
3178
- [spawn_agent] Error in sub-agent loop: ${errMsg}
3179
- `
3180
- );
3382
+ if (singleTask) {
3383
+ const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
3384
+ return [
3385
+ "## Sub-Agent Result",
3386
+ "",
3387
+ content,
3388
+ "",
3389
+ "---",
3390
+ `Token usage: ${usage.inputTokens} input, ${usage.outputTokens} output`
3391
+ ].join("\n");
3181
3392
  }
3182
- printSubAgentFooter(totalUsage);
3183
- const lines = [
3184
- "## Sub-Agent Result",
3185
- "",
3186
- finalContent,
3187
- "",
3188
- "---",
3189
- `Token usage: ${totalUsage.inputTokens} input, ${totalUsage.outputTokens} output`
3190
- ];
3393
+ console.log();
3394
+ console.log(theme.toolCall(`\u{1F916} Spawning ${tasksArr.length} sub-agents in parallel...`));
3395
+ const results = await Promise.all(
3396
+ tasksArr.map((t, i) => runSubAgent(t, maxRounds, i + 1, ctx))
3397
+ );
3398
+ const totalIn = results.reduce((s, r) => s + r.usage.inputTokens, 0);
3399
+ const totalOut = results.reduce((s, r) => s + r.usage.outputTokens, 0);
3400
+ const lines = [`## Parallel Sub-Agent Results (${results.length} agents)`, ""];
3401
+ results.forEach((r, i) => {
3402
+ lines.push(`### Sub-Agent #${i + 1}`);
3403
+ lines.push(`**Task**: ${tasksArr[i].slice(0, 200)}${tasksArr[i].length > 200 ? "..." : ""}`);
3404
+ lines.push("");
3405
+ lines.push(r.content);
3406
+ lines.push("");
3407
+ lines.push(`*Tokens: ${r.usage.inputTokens} in / ${r.usage.outputTokens} out*`);
3408
+ lines.push("");
3409
+ });
3410
+ lines.push("---");
3411
+ lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
3191
3412
  return lines.join("\n");
3192
3413
  }
3193
3414
  };
@@ -3377,10 +3598,392 @@ var taskStopTool = {
3377
3598
  }
3378
3599
  };
3379
3600
 
3601
+ // src/tools/builtin/git-tools.ts
3602
+ import { execSync as execSync4 } from "child_process";
3603
+ import { existsSync as existsSync10 } from "fs";
3604
+ import { join as join5 } from "path";
3605
+ function assertGitRepo(cwd) {
3606
+ let dir = cwd;
3607
+ const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
3608
+ while (dir && dir !== root) {
3609
+ if (existsSync10(join5(dir, ".git"))) return;
3610
+ const parent = join5(dir, "..");
3611
+ if (parent === dir) break;
3612
+ dir = parent;
3613
+ }
3614
+ throw new ToolError("git", "Not inside a git repository. Run this command from a directory under a git-tracked project.");
3615
+ }
3616
+ function runGit(args, cwd, maxBuffer = 10 * 1024 * 1024) {
3617
+ try {
3618
+ const output = execSync4(`git ${args.map((a) => /\s/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a).join(" ")}`, {
3619
+ cwd,
3620
+ encoding: "utf-8",
3621
+ maxBuffer,
3622
+ stdio: ["ignore", "pipe", "pipe"]
3623
+ });
3624
+ return output;
3625
+ } catch (err) {
3626
+ const e = err;
3627
+ const stderr = e.stderr ? String(e.stderr) : "";
3628
+ const stdout = e.stdout ? String(e.stdout) : "";
3629
+ const msg = stderr || stdout || e.message || "git command failed";
3630
+ throw new ToolError("git", `git ${args[0]} failed: ${msg.trim()}`);
3631
+ }
3632
+ }
3633
+ var gitStatusTool = {
3634
+ definition: {
3635
+ name: "git_status",
3636
+ description: `Show the current git working tree status: staged, modified, untracked files, and current branch. Use this before committing to verify what will be included. Prefer this over "bash git status".`,
3637
+ parameters: {
3638
+ path: {
3639
+ type: "string",
3640
+ description: "Repository path (defaults to current working directory)",
3641
+ required: false
3642
+ }
3643
+ },
3644
+ dangerous: false
3645
+ },
3646
+ async execute(args) {
3647
+ const cwd = String(args["path"] ?? process.cwd());
3648
+ assertGitRepo(cwd);
3649
+ let branch = "(unknown)";
3650
+ try {
3651
+ branch = runGit(["symbolic-ref", "--short", "HEAD"], cwd).trim();
3652
+ } catch {
3653
+ branch = "(detached)";
3654
+ }
3655
+ const porcelain = runGit(["status", "--porcelain=v1", "-b"], cwd);
3656
+ const lines = porcelain.split("\n").filter((l) => l.length > 0);
3657
+ const branchLine = lines[0]?.startsWith("##") ? lines[0].slice(3) : `${branch}`;
3658
+ const fileLines = lines.slice(lines[0]?.startsWith("##") ? 1 : 0);
3659
+ if (fileLines.length === 0) {
3660
+ return `Branch: ${branchLine}
3661
+ Working tree clean.`;
3662
+ }
3663
+ const staged = [];
3664
+ const modified = [];
3665
+ const untracked = [];
3666
+ const conflicts = [];
3667
+ for (const line of fileLines) {
3668
+ const x = line[0];
3669
+ const y = line[1];
3670
+ const file = line.slice(3);
3671
+ if (x === "U" || y === "U" || x === "A" && y === "A" || x === "D" && y === "D") {
3672
+ conflicts.push(file);
3673
+ } else if (x === "?" && y === "?") {
3674
+ untracked.push(file);
3675
+ } else {
3676
+ if (x !== " " && x !== "?") staged.push(`${x} ${file}`);
3677
+ if (y !== " " && y !== "?") modified.push(`${y} ${file}`);
3678
+ }
3679
+ }
3680
+ const parts = [`Branch: ${branchLine}`];
3681
+ if (staged.length) parts.push(`
3682
+ Staged (${staged.length}):
3683
+ ` + staged.join("\n "));
3684
+ if (modified.length) parts.push(`
3685
+ Modified (${modified.length}):
3686
+ ` + modified.join("\n "));
3687
+ if (untracked.length) parts.push(`
3688
+ Untracked (${untracked.length}):
3689
+ ` + untracked.join("\n "));
3690
+ if (conflicts.length) parts.push(`
3691
+ Conflicts (${conflicts.length}):
3692
+ ` + conflicts.join("\n "));
3693
+ return parts.join("\n");
3694
+ }
3695
+ };
3696
+ var gitDiffTool = {
3697
+ definition: {
3698
+ name: "git_diff",
3699
+ description: `Show a unified diff of file changes. Use this to review modifications before committing or after an edit. Supports staged/unstaged diffs, specific files, or commit ranges. Prefer this over "bash git diff".`,
3700
+ parameters: {
3701
+ staged: {
3702
+ type: "boolean",
3703
+ description: "Show staged changes (git diff --cached) instead of unstaged. Default: false",
3704
+ required: false
3705
+ },
3706
+ file: {
3707
+ type: "string",
3708
+ description: "Limit diff to a specific file or directory",
3709
+ required: false
3710
+ },
3711
+ commit_range: {
3712
+ type: "string",
3713
+ description: 'Show diff between commits (e.g. "HEAD~3..HEAD", "main..feature"). Overrides staged/unstaged.',
3714
+ required: false
3715
+ },
3716
+ stat: {
3717
+ type: "boolean",
3718
+ description: "Show summary statistics only (file names + insertions/deletions count), not full diff. Default: false",
3719
+ required: false
3720
+ },
3721
+ path: {
3722
+ type: "string",
3723
+ description: "Repository path (defaults to current working directory)",
3724
+ required: false
3725
+ }
3726
+ },
3727
+ dangerous: false
3728
+ },
3729
+ async execute(args) {
3730
+ const cwd = String(args["path"] ?? process.cwd());
3731
+ assertGitRepo(cwd);
3732
+ const gitArgs = ["diff"];
3733
+ if (args["stat"]) gitArgs.push("--stat");
3734
+ if (args["commit_range"]) {
3735
+ gitArgs.push(String(args["commit_range"]));
3736
+ } else if (args["staged"]) {
3737
+ gitArgs.push("--cached");
3738
+ }
3739
+ if (args["file"]) {
3740
+ gitArgs.push("--", String(args["file"]));
3741
+ }
3742
+ const output = runGit(gitArgs, cwd);
3743
+ if (output.trim().length === 0) {
3744
+ const label = args["staged"] ? "staged" : args["commit_range"] ? String(args["commit_range"]) : "unstaged";
3745
+ return `No ${label} changes.`;
3746
+ }
3747
+ return output;
3748
+ }
3749
+ };
3750
+ var gitLogTool = {
3751
+ definition: {
3752
+ name: "git_log",
3753
+ description: `Show recent git commit history with hash, author, date, and message. Use this to understand project history, find a specific commit, or check who last modified a file. Prefer this over "bash git log".`,
3754
+ parameters: {
3755
+ limit: {
3756
+ type: "number",
3757
+ description: "Number of commits to show (default 10, max 100)",
3758
+ required: false
3759
+ },
3760
+ file: {
3761
+ type: "string",
3762
+ description: "Show only commits that modified this file/directory",
3763
+ required: false
3764
+ },
3765
+ author: {
3766
+ type: "string",
3767
+ description: "Filter commits by author name or email substring",
3768
+ required: false
3769
+ },
3770
+ since: {
3771
+ type: "string",
3772
+ description: 'Show commits more recent than this date (e.g. "2 weeks ago", "2025-01-01")',
3773
+ required: false
3774
+ },
3775
+ oneline: {
3776
+ type: "boolean",
3777
+ description: "Compact one-line format. Default: true (set false for full messages)",
3778
+ required: false
3779
+ },
3780
+ path: {
3781
+ type: "string",
3782
+ description: "Repository path (defaults to current working directory)",
3783
+ required: false
3784
+ }
3785
+ },
3786
+ dangerous: false
3787
+ },
3788
+ async execute(args) {
3789
+ const cwd = String(args["path"] ?? process.cwd());
3790
+ assertGitRepo(cwd);
3791
+ const limit = Math.min(Math.max(1, Number(args["limit"] ?? 10)), 100);
3792
+ const oneline = args["oneline"] !== false;
3793
+ const gitArgs = ["log", `-n${limit}`];
3794
+ if (oneline) {
3795
+ gitArgs.push("--pretty=format:%h | %ad | %an | %s", "--date=short");
3796
+ } else {
3797
+ gitArgs.push("--pretty=format:%h%n Author: %an <%ae>%n Date: %ad%n %s%n%n%b%n---", "--date=iso");
3798
+ }
3799
+ if (args["author"]) gitArgs.push(`--author=${String(args["author"])}`);
3800
+ if (args["since"]) gitArgs.push(`--since=${String(args["since"])}`);
3801
+ if (args["file"]) {
3802
+ gitArgs.push("--", String(args["file"]));
3803
+ }
3804
+ const output = runGit(gitArgs, cwd);
3805
+ if (output.trim().length === 0) {
3806
+ return "No commits found matching the filter.";
3807
+ }
3808
+ return output;
3809
+ }
3810
+ };
3811
+ var gitCommitTool = {
3812
+ definition: {
3813
+ name: "git_commit",
3814
+ description: `Create a git commit with the given message. By default commits already-staged changes; set stage_all=true to also stage all tracked modifications first (equivalent to git commit -am). Untracked files are NEVER auto-staged \u2014 the user must add them explicitly. Prefer this over "bash git commit".`,
3815
+ parameters: {
3816
+ message: {
3817
+ type: "string",
3818
+ description: "Commit message (first line is the subject; blank line + body for details)",
3819
+ required: true
3820
+ },
3821
+ stage_all: {
3822
+ type: "boolean",
3823
+ description: "Stage all tracked modified files before committing (git commit -a). Does NOT add untracked files. Default: false",
3824
+ required: false
3825
+ },
3826
+ files: {
3827
+ type: "array",
3828
+ description: "Specific files to stage before committing (git add <files> then commit). Mutually exclusive with stage_all.",
3829
+ items: { type: "string", description: "File path" },
3830
+ required: false
3831
+ },
3832
+ path: {
3833
+ type: "string",
3834
+ description: "Repository path (defaults to current working directory)",
3835
+ required: false
3836
+ }
3837
+ },
3838
+ dangerous: false
3839
+ },
3840
+ async execute(args) {
3841
+ const cwd = String(args["path"] ?? process.cwd());
3842
+ assertGitRepo(cwd);
3843
+ const message = String(args["message"] ?? "").trim();
3844
+ if (!message) throw new ToolError("git_commit", "Commit message is required.");
3845
+ const stageAll = Boolean(args["stage_all"]);
3846
+ const files = Array.isArray(args["files"]) ? args["files"].map(String) : [];
3847
+ if (stageAll && files.length > 0) {
3848
+ throw new ToolError("git_commit", "Cannot use both stage_all and files at the same time.");
3849
+ }
3850
+ if (files.length > 0) {
3851
+ runGit(["add", "--", ...files], cwd);
3852
+ }
3853
+ const statusBefore = runGit(["status", "--porcelain"], cwd);
3854
+ const hasStaged = statusBefore.split("\n").some((l) => l.length > 0 && l[0] !== " " && l[0] !== "?");
3855
+ const hasUnstaged = statusBefore.split("\n").some((l) => l.length > 1 && l[1] !== " " && l[1] !== "?");
3856
+ if (!hasStaged && !stageAll) {
3857
+ return "Nothing to commit: no staged changes. Use stage_all=true or specify files to stage first.";
3858
+ }
3859
+ if (stageAll && !hasStaged && !hasUnstaged) {
3860
+ return "Nothing to commit: working tree clean.";
3861
+ }
3862
+ const commitArgs = ["commit", "-m", message];
3863
+ if (stageAll) commitArgs.splice(1, 0, "-a");
3864
+ const commitOutput = runGit(commitArgs, cwd);
3865
+ const hash = runGit(["rev-parse", "--short", "HEAD"], cwd).trim();
3866
+ const summary = runGit(["log", "-1", "--pretty=format:%h %s"], cwd).trim();
3867
+ return `\u2713 Committed ${hash}
3868
+ ${summary}
3869
+
3870
+ ${commitOutput.trim()}`;
3871
+ }
3872
+ };
3873
+
3874
+ // src/tools/builtin/notebook-edit.ts
3875
+ import { readFileSync as readFileSync6, existsSync as existsSync11 } from "fs";
3876
+ import { writeFile } from "fs/promises";
3877
+ import { resolve as resolve4, extname as extname3 } from "path";
3878
+ var notebookEditTool = {
3879
+ definition: {
3880
+ name: "notebook_edit",
3881
+ description: `Edit a Jupyter notebook (.ipynb) by operating on individual cells. Supports replacing, inserting, or deleting cells. Use read_file first to see cell indices \u2014 they are 1-based and match the "Cell N" labels in the rendered output. Preserves notebook metadata (kernel, nbformat, etc.).`,
3882
+ parameters: {
3883
+ path: {
3884
+ type: "string",
3885
+ description: "Notebook file path (must end in .ipynb)",
3886
+ required: true
3887
+ },
3888
+ action: {
3889
+ type: "string",
3890
+ description: 'Operation: "replace" (edit existing cell), "insert" (add new cell), or "delete" (remove cell)',
3891
+ enum: ["replace", "insert", "delete"],
3892
+ required: true
3893
+ },
3894
+ cell_index: {
3895
+ type: "number",
3896
+ description: 'Cell index, 1-based. For "insert", the new cell is added BEFORE this index (use cells.length+1 to append at the end).',
3897
+ required: true
3898
+ },
3899
+ cell_type: {
3900
+ type: "string",
3901
+ description: 'Cell type for "insert" (code/markdown/raw). Defaults to "code" if omitted.',
3902
+ enum: ["code", "markdown", "raw"],
3903
+ required: false
3904
+ },
3905
+ content: {
3906
+ type: "string",
3907
+ description: 'New cell content for "replace" and "insert". Not used for "delete".',
3908
+ required: false
3909
+ }
3910
+ },
3911
+ dangerous: false
3912
+ },
3913
+ async execute(args) {
3914
+ const filePath = String(args["path"] ?? "");
3915
+ const action = String(args["action"] ?? "");
3916
+ const cellIndexRaw = Number(args["cell_index"]);
3917
+ const content = args["content"] != null ? String(args["content"]) : "";
3918
+ const cellType = args["cell_type"] ?? "code";
3919
+ if (!filePath) throw new ToolError("notebook_edit", "path is required");
3920
+ if (!["replace", "insert", "delete"].includes(action)) {
3921
+ throw new ToolError("notebook_edit", `Unknown action: ${action}. Use replace/insert/delete.`);
3922
+ }
3923
+ if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
3924
+ throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
3925
+ }
3926
+ const absPath = resolve4(filePath);
3927
+ if (extname3(absPath).toLowerCase() !== ".ipynb") {
3928
+ throw new ToolError("notebook_edit", "path must point to a .ipynb file");
3929
+ }
3930
+ if (!existsSync11(absPath)) {
3931
+ throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
3932
+ }
3933
+ const raw = readFileSync6(absPath, "utf-8");
3934
+ const nb = parseNotebook(raw);
3935
+ const cellIdx0 = cellIndexRaw - 1;
3936
+ undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
3937
+ fileCheckpoints.snapshot(absPath, ToolExecutor.currentMessageIndex);
3938
+ let summary = "";
3939
+ if (action === "replace") {
3940
+ if (cellIdx0 < 0 || cellIdx0 >= nb.cells.length) {
3941
+ throw new ToolError(
3942
+ "notebook_edit",
3943
+ `cell_index ${cellIndexRaw} out of range (notebook has ${nb.cells.length} cells)`
3944
+ );
3945
+ }
3946
+ if (content === "") {
3947
+ throw new ToolError("notebook_edit", "content is required for replace action");
3948
+ }
3949
+ const existing = nb.cells[cellIdx0];
3950
+ const oldContent = cellSourceToString(existing.source);
3951
+ existing.source = stringToCellSource(content);
3952
+ if (existing.cell_type === "code") {
3953
+ existing.outputs = [];
3954
+ existing.execution_count = null;
3955
+ }
3956
+ summary = `Replaced cell ${cellIndexRaw} [${existing.cell_type}]: ${oldContent.length} \u2192 ${content.length} chars`;
3957
+ } else if (action === "insert") {
3958
+ if (cellIdx0 < 0 || cellIdx0 > nb.cells.length) {
3959
+ throw new ToolError(
3960
+ "notebook_edit",
3961
+ `cell_index ${cellIndexRaw} out of range for insert (valid: 1-${nb.cells.length + 1})`
3962
+ );
3963
+ }
3964
+ const newCell = createCell(cellType, content);
3965
+ nb.cells.splice(cellIdx0, 0, newCell);
3966
+ summary = `Inserted new [${cellType}] cell at position ${cellIndexRaw} (now ${nb.cells.length} cells total)`;
3967
+ } else {
3968
+ if (cellIdx0 < 0 || cellIdx0 >= nb.cells.length) {
3969
+ throw new ToolError(
3970
+ "notebook_edit",
3971
+ `cell_index ${cellIndexRaw} out of range (notebook has ${nb.cells.length} cells)`
3972
+ );
3973
+ }
3974
+ const removed = nb.cells.splice(cellIdx0, 1)[0];
3975
+ summary = `Deleted cell ${cellIndexRaw} [${removed.cell_type}] (${nb.cells.length} cells remaining)`;
3976
+ }
3977
+ const serialized = serializeNotebook(nb);
3978
+ await writeFile(absPath, serialized, "utf-8");
3979
+ return `\u2713 ${summary}`;
3980
+ }
3981
+ };
3982
+
3380
3983
  // src/tools/registry.ts
3381
3984
  import { pathToFileURL } from "url";
3382
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
3383
- import { join as join5 } from "path";
3985
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
3986
+ import { join as join6 } from "path";
3384
3987
  var ToolRegistry = class {
3385
3988
  tools = /* @__PURE__ */ new Map();
3386
3989
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -3405,6 +4008,11 @@ var ToolRegistry = class {
3405
4008
  this.register(taskCreateTool);
3406
4009
  this.register(taskListTool);
3407
4010
  this.register(taskStopTool);
4011
+ this.register(gitStatusTool);
4012
+ this.register(gitDiffTool);
4013
+ this.register(gitLogTool);
4014
+ this.register(gitCommitTool);
4015
+ this.register(notebookEditTool);
3408
4016
  }
3409
4017
  register(tool) {
3410
4018
  this.tools.set(tool.definition.name, tool);
@@ -3456,7 +4064,7 @@ var ToolRegistry = class {
3456
4064
  * Returns the number of successfully loaded plugins.
3457
4065
  */
3458
4066
  async loadPlugins(pluginsDir, allowPlugins = false) {
3459
- if (!existsSync10(pluginsDir)) {
4067
+ if (!existsSync12(pluginsDir)) {
3460
4068
  try {
3461
4069
  mkdirSync4(pluginsDir, { recursive: true });
3462
4070
  } catch {
@@ -3482,12 +4090,12 @@ var ToolRegistry = class {
3482
4090
  process.stderr.write(
3483
4091
  `
3484
4092
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
3485
- ` + files.map((f) => ` + ${join5(pluginsDir, f)}`).join("\n") + "\n\n"
4093
+ ` + files.map((f) => ` + ${join6(pluginsDir, f)}`).join("\n") + "\n\n"
3486
4094
  );
3487
4095
  let loaded = 0;
3488
4096
  for (const file of files) {
3489
4097
  try {
3490
- const fileUrl = pathToFileURL(join5(pluginsDir, file)).href;
4098
+ const fileUrl = pathToFileURL(join6(pluginsDir, file)).href;
3491
4099
  const mod = await import(fileUrl);
3492
4100
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
3493
4101
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.25";
9
+ var VERSION = "0.4.28";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -7,7 +7,7 @@ import {
7
7
  ProviderNotFoundError,
8
8
  RateLimitError,
9
9
  schemaToJsonSchema
10
- } from "./chunk-5GZQLJAY.js";
10
+ } from "./chunk-EWJQOJSB.js";
11
11
  import {
12
12
  APP_NAME,
13
13
  CONFIG_DIR_NAME,
@@ -20,7 +20,7 @@ import {
20
20
  MCP_TOOL_PREFIX,
21
21
  PLUGINS_DIR_NAME,
22
22
  VERSION
23
- } from "./chunk-AHH5I2U6.js";
23
+ } from "./chunk-7SW4XRDJ.js";
24
24
 
25
25
  // src/config/config-manager.ts
26
26
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -387,7 +387,7 @@ ${content}`);
387
387
  }
388
388
  }
389
389
  async function runTaskMode(config, providers, configManager, topic) {
390
- const { TaskOrchestrator } = await import("./task-orchestrator-4N5UUA6L.js");
390
+ const { TaskOrchestrator } = await import("./task-orchestrator-TDBWMIH4.js");
391
391
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
392
392
  let interrupted = false;
393
393
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  saveDevState,
21
21
  sessionHasMeaningfulContent,
22
22
  setupProxy
23
- } from "./chunk-SS7BQZ5R.js";
23
+ } from "./chunk-SAJICC6G.js";
24
24
  import {
25
25
  ToolExecutor,
26
26
  ToolRegistry,
@@ -33,7 +33,7 @@ import {
33
33
  spawnAgentContext,
34
34
  theme,
35
35
  undoStack
36
- } from "./chunk-5GZQLJAY.js";
36
+ } from "./chunk-EWJQOJSB.js";
37
37
  import {
38
38
  fileCheckpoints
39
39
  } from "./chunk-4BKXL7SM.js";
@@ -57,7 +57,7 @@ import {
57
57
  SKILLS_DIR_NAME,
58
58
  VERSION,
59
59
  buildUserIdentityPrompt
60
- } from "./chunk-AHH5I2U6.js";
60
+ } from "./chunk-7SW4XRDJ.js";
61
61
 
62
62
  // src/index.ts
63
63
  import { program } from "commander";
@@ -2083,7 +2083,7 @@ ${hint}` : "")
2083
2083
  usage: "/test [command|filter]",
2084
2084
  async execute(args, ctx) {
2085
2085
  try {
2086
- const { executeTests } = await import("./run-tests-L3JNRB6X.js");
2086
+ const { executeTests } = await import("./run-tests-QRH2Z2OH.js");
2087
2087
  const argStr = args.join(" ").trim();
2088
2088
  let testArgs = {};
2089
2089
  if (argStr) {
@@ -3089,17 +3089,27 @@ var CustomCommandManager = class {
3089
3089
  // src/repl/notify.ts
3090
3090
  import { spawn } from "child_process";
3091
3091
  import { platform as platform2 } from "os";
3092
+ function safeSpawn(cmd, args) {
3093
+ let child;
3094
+ try {
3095
+ child = spawn(cmd, args, { detached: true, stdio: "ignore" });
3096
+ } catch {
3097
+ return;
3098
+ }
3099
+ child.on("error", () => {
3100
+ });
3101
+ child.unref();
3102
+ }
3092
3103
  function sendNotification(title, body) {
3093
3104
  const plat = platform2();
3094
3105
  try {
3095
3106
  if (plat === "darwin") {
3096
3107
  const safeTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3097
3108
  const safeBody = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3098
- spawn(
3099
- "osascript",
3100
- ["-e", `display notification "${safeBody}" with title "${safeTitle}"`],
3101
- { detached: true, stdio: "ignore" }
3102
- ).unref();
3109
+ safeSpawn("osascript", [
3110
+ "-e",
3111
+ `display notification "${safeBody}" with title "${safeTitle}"`
3112
+ ]);
3103
3113
  } else if (plat === "win32") {
3104
3114
  const safeTitle = title.replace(/`/g, "``").replace(/"/g, '`"');
3105
3115
  const safeBody = body.replace(/`/g, "``").replace(/"/g, '`"');
@@ -3114,13 +3124,16 @@ function sendNotification(title, body) {
3114
3124
  "Start-Sleep -Milliseconds 3500",
3115
3125
  "$n.Dispose()"
3116
3126
  ].join("; ");
3117
- spawn(
3118
- "powershell",
3119
- ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", script],
3120
- { detached: true, stdio: "ignore" }
3121
- ).unref();
3127
+ safeSpawn("powershell", [
3128
+ "-NoProfile",
3129
+ "-NonInteractive",
3130
+ "-WindowStyle",
3131
+ "Hidden",
3132
+ "-Command",
3133
+ script
3134
+ ]);
3122
3135
  } else {
3123
- spawn("notify-send", [title, body], { detached: true, stdio: "ignore" }).unref();
3136
+ safeSpawn("notify-send", [title, body]);
3124
3137
  }
3125
3138
  } catch {
3126
3139
  }
@@ -5365,7 +5378,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5365
5378
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5366
5379
  process.exit(1);
5367
5380
  }
5368
- const { startWebServer } = await import("./server-SZZXQZWY.js");
5381
+ const { startWebServer } = await import("./server-46ANSDN7.js");
5369
5382
  await startWebServer({ port, host: options.host });
5370
5383
  });
5371
5384
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5598,7 +5611,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5598
5611
  }),
5599
5612
  config.get("customProviders")
5600
5613
  );
5601
- const { startHub } = await import("./hub-JOYPSPR2.js");
5614
+ const { startHub } = await import("./hub-QJBT4RDT.js");
5602
5615
  await startHub(
5603
5616
  {
5604
5617
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-ETMUP3CY.js";
4
+ } from "./chunk-L7OCMF5F.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-AHH5I2U6.js";
5
+ } from "./chunk-7SW4XRDJ.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -15,7 +15,7 @@ import {
15
15
  hadPreviousWriteToolCalls,
16
16
  loadDevState,
17
17
  setupProxy
18
- } from "./chunk-SS7BQZ5R.js";
18
+ } from "./chunk-SAJICC6G.js";
19
19
  import {
20
20
  AuthManager
21
21
  } from "./chunk-BYNY5JPB.js";
@@ -33,7 +33,7 @@ import {
33
33
  spawnAgentContext,
34
34
  truncateOutput,
35
35
  undoStack
36
- } from "./chunk-5GZQLJAY.js";
36
+ } from "./chunk-EWJQOJSB.js";
37
37
  import "./chunk-4BKXL7SM.js";
38
38
  import {
39
39
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -52,7 +52,7 @@ import {
52
52
  SKILLS_DIR_NAME,
53
53
  VERSION,
54
54
  buildUserIdentityPrompt
55
- } from "./chunk-AHH5I2U6.js";
55
+ } from "./chunk-7SW4XRDJ.js";
56
56
 
57
57
  // src/web/server.ts
58
58
  import express from "express";
@@ -1606,7 +1606,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1606
1606
  case "test": {
1607
1607
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1608
1608
  try {
1609
- const { executeTests } = await import("./run-tests-L3JNRB6X.js");
1609
+ const { executeTests } = await import("./run-tests-QRH2Z2OH.js");
1610
1610
  const argStr = args.join(" ").trim();
1611
1611
  let testArgs = {};
1612
1612
  if (argStr) {
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-5GZQLJAY.js";
7
+ } from "./chunk-EWJQOJSB.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-AHH5I2U6.js";
11
+ } from "./chunk-7SW4XRDJ.js";
12
12
 
13
13
  // src/hub/task-orchestrator.ts
14
14
  import { createInterface } from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",