jinzd-ai-cli 0.4.68 → 0.4.70

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 {
8
8
  RateLimitError,
9
9
  schemaToJsonSchema,
10
10
  truncateForPersist
11
- } from "./chunk-Q5QSCO5D.js";
11
+ } from "./chunk-IVTWWDWZ.js";
12
12
  import {
13
13
  APP_NAME,
14
14
  CONFIG_DIR_NAME,
@@ -21,7 +21,7 @@ import {
21
21
  MCP_TOOL_PREFIX,
22
22
  PLUGINS_DIR_NAME,
23
23
  VERSION
24
- } from "./chunk-3LCVJ4AF.js";
24
+ } from "./chunk-ND3O5NQU.js";
25
25
 
26
26
  // src/config/config-manager.ts
27
27
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -417,21 +417,30 @@ var ClaudeProvider = class extends BaseProvider {
417
417
  }
418
418
  /**
419
419
  * Build a cacheable system prompt payload.
420
- * When the prompt is long enough to be worth caching, return an array with a
421
- * single text block carrying `cache_control: { type: 'ephemeral' }`. This caches
422
- * system + memory + context files across every request in an agentic loop.
423
- * Short prompts pass through as a plain string (no caching overhead).
420
+ *
421
+ * stable — session-invariant content (guidelines, memory, context). Marked with
422
+ * cache_control: ephemeral when long enough, so Claude reuses the KV cache
423
+ * across every round in an agentic loop AND across turns within a session.
424
+ * volatile — per-request content (date/time, working directory). Appended as a second
425
+ * text block WITHOUT cache_control so it never busts the stable cache.
426
+ *
427
+ * Short prompts (below the Anthropic minimum) pass through as a plain string.
424
428
  */
425
- buildSystemParam(systemPrompt) {
426
- if (!systemPrompt) return void 0;
427
- if (systemPrompt.length < CACHE_MIN_SYSTEM_CHARS) return systemPrompt;
428
- return [
429
- {
430
- type: "text",
431
- text: systemPrompt,
432
- cache_control: { type: "ephemeral" }
433
- }
429
+ buildSystemParam(stable, volatile) {
430
+ const stableText = stable ?? "";
431
+ const volatileText = volatile ?? "";
432
+ if (!stableText && !volatileText) return void 0;
433
+ if (stableText.length < CACHE_MIN_SYSTEM_CHARS) {
434
+ const combined = [stableText, volatileText].filter(Boolean).join("\n\n---\n\n");
435
+ return combined || void 0;
436
+ }
437
+ const blocks = [
438
+ { type: "text", text: stableText, cache_control: { type: "ephemeral" } }
434
439
  ];
440
+ if (volatileText) {
441
+ blocks.push({ type: "text", text: volatileText });
442
+ }
443
+ return blocks;
435
444
  }
436
445
  /**
437
446
  * Mark the last tool definition with `cache_control: ephemeral` so the entire
@@ -511,7 +520,7 @@ var ClaudeProvider = class extends BaseProvider {
511
520
  const response = await this.client.messages.create({
512
521
  model: request.model,
513
522
  messages,
514
- system: this.buildSystemParam(request.systemPrompt),
523
+ system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
515
524
  max_tokens: request.maxTokens ?? 8192,
516
525
  temperature,
517
526
  thinking
@@ -536,7 +545,7 @@ var ClaudeProvider = class extends BaseProvider {
536
545
  const stream = this.client.messages.stream({
537
546
  model: request.model,
538
547
  messages,
539
- system: this.buildSystemParam(request.systemPrompt),
548
+ system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
540
549
  max_tokens: request.maxTokens ?? 8192,
541
550
  temperature,
542
551
  thinking
@@ -593,7 +602,7 @@ var ClaudeProvider = class extends BaseProvider {
593
602
  model: request.model,
594
603
  messages: allMessages,
595
604
  tools: anthropicTools,
596
- system: this.buildSystemParam(request.systemPrompt),
605
+ system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
597
606
  max_tokens: request.maxTokens ?? 8192,
598
607
  temperature,
599
608
  thinking
@@ -649,7 +658,7 @@ var ClaudeProvider = class extends BaseProvider {
649
658
  model: request.model,
650
659
  messages: allMessages,
651
660
  tools: anthropicTools,
652
- system: this.buildSystemParam(request.systemPrompt),
661
+ system: this.buildSystemParam(request.systemPrompt, request.systemPromptVolatile),
653
662
  max_tokens: request.maxTokens ?? 8192,
654
663
  temperature,
655
664
  thinking
@@ -909,7 +918,7 @@ var GeminiProvider = class extends BaseProvider {
909
918
  }
910
919
  const genModel = this.client.getGenerativeModel({
911
920
  model: request.model,
912
- systemInstruction: request.systemPrompt
921
+ systemInstruction: [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n") || void 0
913
922
  }, this.reqOpts);
914
923
  const history = this.toGeminiHistory(request.messages.slice(0, -1));
915
924
  const lastMsgParts = this.contentToGeminiParts(
@@ -935,7 +944,7 @@ var GeminiProvider = class extends BaseProvider {
935
944
  }
936
945
  const genModel = this.client.getGenerativeModel({
937
946
  model: request.model,
938
- systemInstruction: request.systemPrompt
947
+ systemInstruction: [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n") || void 0
939
948
  }, this.reqOpts);
940
949
  const history = this.toGeminiHistory(request.messages.slice(0, -1));
941
950
  const lastMsgParts = this.contentToGeminiParts(
@@ -1003,7 +1012,7 @@ var GeminiProvider = class extends BaseProvider {
1003
1012
  }
1004
1013
  const genModel = this.client.getGenerativeModel({
1005
1014
  model: request.model,
1006
- systemInstruction: request.systemPrompt
1015
+ systemInstruction: [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n") || void 0
1007
1016
  }, this.reqOpts);
1008
1017
  const chat = genModel.startChat({ history: fullHistory, tools: geminiTools });
1009
1018
  const result = await chat.sendMessage(msgToSend);
@@ -1127,8 +1136,9 @@ var OpenAICompatibleProvider = class extends BaseProvider {
1127
1136
  /** 将 systemPrompt + messages 合并为 OpenAI messages 数组(system 消息放首位)。 */
1128
1137
  buildMessages(request) {
1129
1138
  const msgs = this.normalizeMessages(request.messages);
1130
- if (request.systemPrompt) {
1131
- return [{ role: "system", content: request.systemPrompt }, ...msgs];
1139
+ const systemContent = [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n");
1140
+ if (systemContent) {
1141
+ return [{ role: "system", content: systemContent }, ...msgs];
1132
1142
  }
1133
1143
  return msgs;
1134
1144
  }
@@ -10,7 +10,7 @@ import {
10
10
  SUBAGENT_DEFAULT_MAX_ROUNDS,
11
11
  SUBAGENT_MAX_ROUNDS_LIMIT,
12
12
  runTestsTool
13
- } from "./chunk-3LCVJ4AF.js";
13
+ } from "./chunk-ND3O5NQU.js";
14
14
 
15
15
  // src/tools/builtin/bash.ts
16
16
  import { execSync } from "child_process";
@@ -1839,16 +1839,123 @@ function findWhitespaceTolerant(fileLines, searchLines) {
1839
1839
  }
1840
1840
  return { matchStart, matchCount };
1841
1841
  }
1842
+ function applyReplace(content, oldStr, newStr, options = {}) {
1843
+ if (oldStr === "") {
1844
+ return { ok: false, error: "old_str cannot be empty" };
1845
+ }
1846
+ if (options.ignoreWhitespace) {
1847
+ const fileLines = content.split("\n");
1848
+ const searchLines = oldStr.split("\n");
1849
+ const { matchStart, matchCount } = findWhitespaceTolerant(fileLines, searchLines);
1850
+ if (matchStart === -1) {
1851
+ const similar = findSimilarLines(content, oldStr);
1852
+ const hint = similar.length > 0 ? `
1853
+ Similar lines found (did you mean?):
1854
+ ${similar.join("\n")}` : "";
1855
+ return {
1856
+ ok: false,
1857
+ error: `old_str not found in file (even with whitespace ignored). File has ${fileLines.length} lines.${hint}`
1858
+ };
1859
+ }
1860
+ if (matchCount > 1) {
1861
+ return {
1862
+ ok: false,
1863
+ error: "old_str matches multiple locations with whitespace-tolerant matching. Please include more surrounding context to make it unique."
1864
+ };
1865
+ }
1866
+ const before = fileLines.slice(0, matchStart);
1867
+ const after = fileLines.slice(matchStart + searchLines.length);
1868
+ const updated2 = [...before, newStr, ...after].join("\n");
1869
+ return {
1870
+ ok: true,
1871
+ content: updated2,
1872
+ info: { mode: "ignore_whitespace", replacedCount: 1, lineNumber: matchStart + 1 }
1873
+ };
1874
+ }
1875
+ if (options.replaceAll) {
1876
+ const occurrences = content.split(oldStr).length - 1;
1877
+ if (occurrences === 0) {
1878
+ const similar = findSimilarLines(content, oldStr);
1879
+ const hint = similar.length > 0 ? `
1880
+ Similar lines found (did you mean?):
1881
+ ${similar.join("\n")}` : "";
1882
+ return { ok: false, error: `old_str not found in file.${hint}` };
1883
+ }
1884
+ const updated2 = content.split(oldStr).join(newStr);
1885
+ return {
1886
+ ok: true,
1887
+ content: updated2,
1888
+ info: { mode: "replace_all", replacedCount: occurrences }
1889
+ };
1890
+ }
1891
+ const firstIndex = content.indexOf(oldStr);
1892
+ if (firstIndex === -1) {
1893
+ const lines = content.split("\n");
1894
+ const similar = findSimilarLines(content, oldStr);
1895
+ const hint = similar.length > 0 ? `
1896
+ Similar lines found (did you mean?):
1897
+ ${similar.join("\n")}` : "";
1898
+ return {
1899
+ ok: false,
1900
+ error: `old_str not found in file. File has ${lines.length} lines.${hint}
1901
+ Tip: try ignore_whitespace: true if indentation differs.`
1902
+ };
1903
+ }
1904
+ const secondIndex = content.indexOf(oldStr, firstIndex + 1);
1905
+ if (secondIndex !== -1) {
1906
+ return {
1907
+ ok: false,
1908
+ error: `old_str appears multiple times in file (at positions ${firstIndex} and ${secondIndex}). Please include more surrounding context to make it unique, or set replace_all: true.`
1909
+ };
1910
+ }
1911
+ const updated = content.slice(0, firstIndex) + newStr + content.slice(firstIndex + oldStr.length);
1912
+ const linesBefore = content.slice(0, firstIndex).split("\n").length;
1913
+ return {
1914
+ ok: true,
1915
+ content: updated,
1916
+ info: { mode: "exact", replacedCount: 1, lineNumber: linesBefore }
1917
+ };
1918
+ }
1919
+ function parseEditsArg(raw) {
1920
+ if (!Array.isArray(raw)) {
1921
+ throw new ToolError("edit_file", "edits must be an array of { old_str, new_str } objects");
1922
+ }
1923
+ if (raw.length === 0) {
1924
+ throw new ToolError("edit_file", "edits array is empty \u2014 provide at least one edit");
1925
+ }
1926
+ if (raw.length > 200) {
1927
+ throw new ToolError("edit_file", `edits array too large (${raw.length}) \u2014 max 200 per call`);
1928
+ }
1929
+ return raw.map((e, i) => {
1930
+ if (e == null || typeof e !== "object") {
1931
+ throw new ToolError("edit_file", `edits[${i}] must be an object`);
1932
+ }
1933
+ const rec = e;
1934
+ if (typeof rec.old_str !== "string") {
1935
+ throw new ToolError("edit_file", `edits[${i}].old_str must be a string`);
1936
+ }
1937
+ if (rec.new_str !== void 0 && typeof rec.new_str !== "string") {
1938
+ throw new ToolError("edit_file", `edits[${i}].new_str must be a string`);
1939
+ }
1940
+ return {
1941
+ old_str: rec.old_str,
1942
+ new_str: String(rec.new_str ?? ""),
1943
+ ignore_whitespace: Boolean(rec.ignore_whitespace),
1944
+ replace_all: Boolean(rec.replace_all)
1945
+ };
1946
+ });
1947
+ }
1842
1948
  var editFileTool = {
1843
1949
  definition: {
1844
1950
  name: "edit_file",
1845
- description: `Precisely edit file contents. Supports three modes:
1846
- 1. String replace (most common): Provide old_str and new_str to replace an exact match. old_str must appear exactly once in the file (unless replace_all is true).
1847
- 2. Line insert: Provide insert_after_line (1-based line number) and insert_content to insert after that line.
1848
- 3. Line delete: Provide delete_from_line and delete_to_line (inclusive) to delete that range.
1849
- Optional ignore_whitespace: true to match ignoring indentation differences.
1850
- Optional replace_all: true to replace ALL occurrences of old_str in the file at once (saves tool rounds when renaming variables/functions).
1851
- Note: Path can be absolute or relative to the current working directory.`,
1951
+ description: `Precisely edit file contents. Four modes:
1952
+ 1. String replace (most common): Provide old_str and new_str to replace an exact match. old_str must appear exactly once (unless replace_all is true).
1953
+ 2. Line insert: Provide insert_after_line (1-based) and insert_content.
1954
+ 3. Line delete: Provide delete_from_line and delete_to_line (inclusive).
1955
+ 4. Batch edits: Provide edits=[{old_str, new_str, ignore_whitespace?, replace_all?}, ...] to apply MULTIPLE edits in ONE call \u2014 saves tool rounds/tokens when refactoring a file. Edits are applied sequentially in-memory; by default any failure rolls back ALL edits (set stop_on_error=false to apply successful ones and report failures).
1956
+ Optional ignore_whitespace: true ignores indentation differences during matching.
1957
+ Optional replace_all: true replaces ALL occurrences of old_str.
1958
+ Note: Path can be absolute or relative to cwd.`,
1852
1959
  parameters: {
1853
1960
  path: {
1854
1961
  type: "string",
@@ -1857,22 +1964,22 @@ Note: Path can be absolute or relative to the current working directory.`,
1857
1964
  },
1858
1965
  old_str: {
1859
1966
  type: "string",
1860
- description: "[Replace mode] Original string to replace, must appear exactly once (include enough context for uniqueness)",
1967
+ description: "[Single-replace mode] Original string to replace, must appear exactly once (include enough context for uniqueness)",
1861
1968
  required: false
1862
1969
  },
1863
1970
  new_str: {
1864
1971
  type: "string",
1865
- description: "[Replace mode] New replacement string, can be empty to delete old_str",
1972
+ description: "[Single-replace mode] New replacement string, can be empty to delete old_str",
1866
1973
  required: false
1867
1974
  },
1868
1975
  ignore_whitespace: {
1869
1976
  type: "boolean",
1870
- description: "[Replace mode] Whether to ignore leading/trailing whitespace per line when matching, defaults to false",
1977
+ description: "[Single-replace mode] Whether to ignore leading/trailing whitespace per line when matching, defaults to false",
1871
1978
  required: false
1872
1979
  },
1873
1980
  replace_all: {
1874
1981
  type: "boolean",
1875
- description: "[Replace mode] Replace ALL occurrences of old_str instead of requiring unique match. Useful for renaming variables/functions across the file in one call.",
1982
+ description: "[Single-replace mode] Replace ALL occurrences of old_str instead of requiring unique match. Useful for renaming variables/functions across the file in one call.",
1876
1983
  required: false
1877
1984
  },
1878
1985
  insert_after_line: {
@@ -1895,6 +2002,16 @@ Note: Path can be absolute or relative to the current working directory.`,
1895
2002
  description: "[Delete mode] Delete up to and including this line (1-based)",
1896
2003
  required: false
1897
2004
  },
2005
+ edits: {
2006
+ type: "array",
2007
+ description: "[Batch mode] Array of {old_str, new_str, ignore_whitespace?, replace_all?} objects. Each applied sequentially to the in-memory content. Max 200 per call.",
2008
+ required: false
2009
+ },
2010
+ stop_on_error: {
2011
+ type: "boolean",
2012
+ description: "[Batch mode] If true (default), any failing edit rolls back the whole batch and writes nothing. If false, successful edits are written and failed ones are reported.",
2013
+ required: false
2014
+ },
1898
2015
  encoding: {
1899
2016
  type: "string",
1900
2017
  description: "File encoding, defaults to utf-8",
@@ -1910,83 +2027,84 @@ Note: Path can be absolute or relative to the current working directory.`,
1910
2027
  if (!filePath) throw new ToolError("edit_file", "path is required");
1911
2028
  if (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
1912
2029
  const original = readFileSync4(filePath, encoding);
2030
+ if (args["edits"] !== void 0) {
2031
+ const edits = parseEditsArg(args["edits"]);
2032
+ const stopOnError = args["stop_on_error"] !== false;
2033
+ let working = original;
2034
+ const reports = [];
2035
+ let appliedCount = 0;
2036
+ for (let i = 0; i < edits.length; i++) {
2037
+ const edit = edits[i];
2038
+ const res = applyReplace(working, edit.old_str, edit.new_str, {
2039
+ ignoreWhitespace: edit.ignore_whitespace,
2040
+ replaceAll: edit.replace_all
2041
+ });
2042
+ if (res.ok) {
2043
+ working = res.content;
2044
+ appliedCount++;
2045
+ reports.push({
2046
+ index: i,
2047
+ ok: true,
2048
+ summary: `${res.info?.mode} at ${res.info?.lineNumber != null ? `line ${res.info.lineNumber}` : `${res.info?.replacedCount} occurrence(s)`}: ${truncatePreview(edit.old_str)} \u2192 ${truncatePreview(edit.new_str)}`
2049
+ });
2050
+ } else {
2051
+ reports.push({ index: i, ok: false, error: res.error });
2052
+ if (stopOnError) break;
2053
+ }
2054
+ }
2055
+ const anyFailed = reports.some((r) => !r.ok);
2056
+ const writeChanges = appliedCount > 0 && !(anyFailed && stopOnError);
2057
+ if (writeChanges) {
2058
+ undoStack.push(filePath, `edit_file (batch ${appliedCount}/${edits.length}): ${filePath}`);
2059
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
2060
+ writeFileSync3(filePath, working, encoding);
2061
+ }
2062
+ const lines = [];
2063
+ if (anyFailed && stopOnError) {
2064
+ lines.push(`ERROR: Batch edit aborted \u2014 ${appliedCount}/${edits.length} applied, then edit #${appliedCount} failed. No changes written (stop_on_error=true).`);
2065
+ } else if (anyFailed) {
2066
+ lines.push(`Partial success: ${appliedCount}/${edits.length} edits applied to ${filePath} (stop_on_error=false).`);
2067
+ } else {
2068
+ lines.push(`Successfully applied ${appliedCount}/${edits.length} edit(s) to ${filePath}.`);
2069
+ }
2070
+ lines.push("");
2071
+ for (const r of reports) {
2072
+ if (r.ok) {
2073
+ lines.push(` \u2713 #${r.index + 1}: ${r.summary}`);
2074
+ } else {
2075
+ lines.push(` \u2717 #${r.index + 1}: ${r.error}`);
2076
+ }
2077
+ }
2078
+ return lines.join("\n");
2079
+ }
1913
2080
  if (args["old_str"] !== void 0) {
1914
2081
  const oldStr = String(args["old_str"]);
1915
2082
  const newStr = String(args["new_str"] ?? "");
1916
2083
  const ignoreWs = Boolean(args["ignore_whitespace"]);
1917
2084
  const replaceAll = Boolean(args["replace_all"]);
1918
2085
  if (oldStr === "") throw new ToolError("edit_file", "old_str cannot be empty");
1919
- if (ignoreWs) {
1920
- const fileLines = original.split("\n");
1921
- const searchLines = oldStr.split("\n");
1922
- const { matchStart, matchCount } = findWhitespaceTolerant(fileLines, searchLines);
1923
- if (matchStart === -1) {
1924
- const similar = findSimilarLines(original, oldStr);
1925
- const hint = similar.length > 0 ? `
1926
- Similar lines found (did you mean?):
1927
- ${similar.join("\n")}` : "";
1928
- return `ERROR: old_str not found in file (even with whitespace ignored).
1929
- File has ${fileLines.length} lines.${hint}
2086
+ const res = applyReplace(original, oldStr, newStr, {
2087
+ ignoreWhitespace: ignoreWs,
2088
+ replaceAll
2089
+ });
2090
+ if (!res.ok) {
2091
+ return `ERROR: ${res.error}
1930
2092
  Please read the file first and use exact text.`;
1931
- }
1932
- if (matchCount > 1) {
1933
- return `ERROR: old_str matches multiple locations with whitespace-tolerant matching. Please include more surrounding context to make it unique.`;
1934
- }
1935
- undoStack.push(filePath, `edit_file (ws-replace): ${filePath}`);
1936
- fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
1937
- const before = fileLines.slice(0, matchStart);
1938
- const after = fileLines.slice(matchStart + searchLines.length);
1939
- const updated2 = [...before, newStr, ...after].join("\n");
1940
- writeFileSync3(filePath, updated2, encoding);
1941
- return `Successfully edited ${filePath} (whitespace-tolerant match)
1942
- Location: around line ${matchStart + 1}
1943
- Replaced: ${searchLines.length} line(s) \u2192 ${newStr.split("\n").length} line(s)
1944
- Old: ${truncatePreview(oldStr)}
1945
- New: ${truncatePreview(newStr)}`;
1946
2093
  }
1947
- if (replaceAll) {
1948
- const occurrences = original.split(oldStr).length - 1;
1949
- if (occurrences === 0) {
1950
- const similar = findSimilarLines(original, oldStr);
1951
- const hint = similar.length > 0 ? `
1952
- Similar lines found (did you mean?):
1953
- ${similar.join("\n")}` : "";
1954
- return `ERROR: old_str not found in file.${hint}
1955
- Please read the file first and use exact text.`;
1956
- }
1957
- undoStack.push(filePath, `edit_file (replace_all): ${filePath}`);
1958
- fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
1959
- const updated2 = original.split(oldStr).join(newStr);
1960
- writeFileSync3(filePath, updated2, encoding);
1961
- return `Successfully edited ${filePath} (replace_all)
1962
- Replaced: ${occurrences} occurrence(s) of ${truncatePreview(oldStr)}
2094
+ const modeLabel = res.info?.mode === "ignore_whitespace" ? " (whitespace-tolerant match)" : res.info?.mode === "replace_all" ? " (replace_all)" : "";
2095
+ const label = res.info?.mode === "ignore_whitespace" ? "edit_file (ws-replace)" : res.info?.mode === "replace_all" ? "edit_file (replace_all)" : "edit_file (replace)";
2096
+ undoStack.push(filePath, `${label}: ${filePath}`);
2097
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
2098
+ writeFileSync3(filePath, res.content, encoding);
2099
+ if (res.info?.mode === "replace_all") {
2100
+ return `Successfully edited ${filePath}${modeLabel}
2101
+ Replaced: ${res.info.replacedCount} occurrence(s) of ${truncatePreview(oldStr)}
1963
2102
  With: ${truncatePreview(newStr)}`;
1964
2103
  }
1965
- const firstIndex = original.indexOf(oldStr);
1966
- if (firstIndex === -1) {
1967
- const lines = original.split("\n");
1968
- const similar = findSimilarLines(original, oldStr);
1969
- const hint = similar.length > 0 ? `
1970
- Similar lines found (did you mean?):
1971
- ${similar.join("\n")}` : "";
1972
- return `ERROR: old_str not found in file.
1973
- File has ${lines.length} lines.${hint}
1974
- Please read the file first and use exact text including whitespace/indentation.
1975
- Tip: You can also try ignore_whitespace: true to match ignoring indentation differences.`;
1976
- }
1977
- const secondIndex = original.indexOf(oldStr, firstIndex + 1);
1978
- if (secondIndex !== -1) {
1979
- return `ERROR: old_str appears multiple times in file (at least at positions ${firstIndex} and ${secondIndex}). Please include more surrounding context to make it unique.`;
1980
- }
1981
- undoStack.push(filePath, `edit_file (replace): ${filePath}`);
1982
- fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
1983
- const updated = original.slice(0, firstIndex) + newStr + original.slice(firstIndex + oldStr.length);
1984
- writeFileSync3(filePath, updated, encoding);
1985
2104
  const oldLines = oldStr.split("\n").length;
1986
2105
  const newLines = newStr.split("\n").length;
1987
- const linesBefore = original.slice(0, firstIndex).split("\n").length;
1988
- return `Successfully edited ${filePath}
1989
- Location: around line ${linesBefore}
2106
+ return `Successfully edited ${filePath}${modeLabel}
2107
+ Location: around line ${res.info?.lineNumber ?? "?"}
1990
2108
  Replaced: ${oldLines} line(s) \u2192 ${newLines} line(s)
1991
2109
  Old: ${truncatePreview(oldStr)}
1992
2110
  New: ${truncatePreview(newStr)}`;
@@ -2022,7 +2140,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
2022
2140
  }
2023
2141
  throw new ToolError(
2024
2142
  "edit_file",
2025
- "No operation specified. Provide either: (old_str + new_str) for replace, (insert_after_line + insert_content) for insert, or (delete_from_line + delete_to_line) for delete."
2143
+ "No operation specified. Provide one of: (old_str + new_str), (insert_after_line + insert_content), (delete_from_line + delete_to_line), or edits=[...]."
2026
2144
  );
2027
2145
  }
2028
2146
  };
@@ -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.68";
11
+ var VERSION = "0.4.70";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -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.68";
9
+ var VERSION = "0.4.70";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -385,7 +385,7 @@ ${content}`);
385
385
  }
386
386
  }
387
387
  async function runTaskMode(config, providers, configManager, topic) {
388
- const { TaskOrchestrator } = await import("./task-orchestrator-WDRXASIC.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-EB2XPY5S.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  saveDevState,
32
32
  sessionHasMeaningfulContent,
33
33
  setupProxy
34
- } from "./chunk-G5AISHJE.js";
34
+ } from "./chunk-3YT2DUUT.js";
35
35
  import {
36
36
  ToolExecutor,
37
37
  ToolRegistry,
@@ -47,7 +47,7 @@ import {
47
47
  spawnAgentContext,
48
48
  theme,
49
49
  undoStack
50
- } from "./chunk-Q5QSCO5D.js";
50
+ } from "./chunk-IVTWWDWZ.js";
51
51
  import {
52
52
  fileCheckpoints
53
53
  } from "./chunk-4BKXL7SM.js";
@@ -72,7 +72,7 @@ import {
72
72
  SKILLS_DIR_NAME,
73
73
  VERSION,
74
74
  buildUserIdentityPrompt
75
- } from "./chunk-3LCVJ4AF.js";
75
+ } from "./chunk-ND3O5NQU.js";
76
76
 
77
77
  // src/index.ts
78
78
  import { program } from "commander";
@@ -2267,7 +2267,7 @@ ${hint}` : "")
2267
2267
  usage: "/test [command|filter]",
2268
2268
  async execute(args, ctx) {
2269
2269
  try {
2270
- const { executeTests } = await import("./run-tests-WD53PYVA.js");
2270
+ const { executeTests } = await import("./run-tests-NPRCZYN3.js");
2271
2271
  const argStr = args.join(" ").trim();
2272
2272
  let testArgs = {};
2273
2273
  if (argStr) {
@@ -4120,17 +4120,28 @@ ${projectContext}`);
4120
4120
  if (gitContextStr) parts.push(gitContextStr);
4121
4121
  return parts.length > 0 ? parts.join("\n\n---\n\n") : void 0;
4122
4122
  }
4123
- /** 每次请求时动态生成含当前日期时间的 system prompt。 */
4123
+ /**
4124
+ * Build the system prompt split into stable (cacheable) and volatile (non-cacheable) parts.
4125
+ *
4126
+ * stable — identity, guidelines, memory, dev state, project context, skills, plan mode.
4127
+ * Content is session-invariant → Claude caches it across every agentic round
4128
+ * AND across turns, so only the first request of a session pays full input cost.
4129
+ * volatile — current date/time + working directory. Appended as a separate uncached block
4130
+ * so it never invalidates the stable cache.
4131
+ *
4132
+ * ⚠️ 注意:不使用 toLocaleDateString/toLocaleTimeString,
4133
+ * pkg 打包的精简 ICU 不支持 locale 参数,会回退到错误格式。
4134
+ */
4124
4135
  buildCurrentSystemPrompt() {
4125
4136
  const now = /* @__PURE__ */ new Date();
4126
4137
  const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
4127
4138
  const pad = (n) => String(n).padStart(2, "0");
4128
4139
  const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${WEEKDAYS[now.getDay()]}`;
4129
4140
  const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
4130
- const dateTimeInfo = `Current date and time: ${dateStr} ${timeStr}`;
4131
4141
  const osName = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
4132
4142
  const shellInfo = process.platform === "win32" ? "The bash tool uses PowerShell (not cmd). Use PowerShell command syntax. Do not use Unix commands (e.g. grep, head, find, wc, date +format, etc.), use PowerShell equivalents instead (e.g. Select-String, Select-Object -First, Get-ChildItem, Measure-Object, Get-Date -Format, etc.)." : `The bash tool uses ${process.env.SHELL || "/bin/bash"}.`;
4133
- const envInfo = `OS: ${osName}
4143
+ const volatile = `Current date and time: ${dateStr} ${timeStr}
4144
+ OS: ${osName}
4134
4145
  ${shellInfo}
4135
4146
  Working directory: ${process.cwd()}`;
4136
4147
  const effectiveMaxRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
@@ -4142,24 +4153,20 @@ You have a maximum of ${effectiveMaxRounds} tool call rounds per conversation tu
4142
4153
  - On Windows, use PowerShell cmdlets (Invoke-RestMethod, Get-ChildItem) instead of Unix commands (curl, grep, find).
4143
4154
  - If starting a long-running server process via bash, use background execution \u2014 do not block the tool round.
4144
4155
  - Prioritize the most critical tasks first in case you run out of rounds.`;
4145
- const parts = [dateTimeInfo + "\n" + envInfo];
4156
+ const stableParts = [];
4146
4157
  const userProfile = this.config.get("userProfile");
4147
4158
  if (userProfile) {
4148
4159
  const identityPrompt = buildUserIdentityPrompt(userProfile);
4149
- if (identityPrompt) {
4150
- parts.push(identityPrompt);
4151
- }
4160
+ if (identityPrompt) stableParts.push(identityPrompt);
4152
4161
  }
4153
- parts.push(AGENTIC_BEHAVIOR_GUIDELINE + budgetInfo);
4162
+ stableParts.push(AGENTIC_BEHAVIOR_GUIDELINE + budgetInfo);
4154
4163
  const memory = this.loadMemoryContent();
4155
- if (memory) {
4156
- parts.push(`# Persistent Memory
4164
+ if (memory) stableParts.push(`# Persistent Memory
4157
4165
 
4158
4166
  ${memory.content}`);
4159
- }
4160
4167
  const devState = loadDevState();
4161
4168
  if (devState) {
4162
- parts.push(
4169
+ stableParts.push(
4163
4170
  `# Development State Handoff
4164
4171
 
4165
4172
  The following is a development state snapshot generated by the previous AI model. Please seamlessly continue the user's work based on this.
@@ -4168,25 +4175,21 @@ Important: If the snapshot mentions key reference files (e.g. templates, format
4168
4175
  ` + devState
4169
4176
  );
4170
4177
  }
4171
- if (this.activeSystemPrompt) {
4172
- parts.push(this.activeSystemPrompt);
4173
- }
4178
+ if (this.activeSystemPrompt) stableParts.push(this.activeSystemPrompt);
4174
4179
  for (const { dir, content } of this.extraContextDirs) {
4175
- parts.push(`# Added Directory Context: ${dir}
4180
+ stableParts.push(`# Added Directory Context: ${dir}
4176
4181
 
4177
4182
  ${content}`);
4178
4183
  }
4179
4184
  const skillContent = this.skillManager?.getActivePromptContent();
4180
4185
  if (skillContent) {
4181
4186
  const skillName = this.skillManager.getActive().meta.name;
4182
- parts.push(`# Active Skill: ${skillName}
4187
+ stableParts.push(`# Active Skill: ${skillName}
4183
4188
 
4184
4189
  ${skillContent}`);
4185
4190
  }
4186
- if (this.planMode) {
4187
- parts.push(PLAN_MODE_SYSTEM_ADDON);
4188
- }
4189
- return parts.join("\n\n---\n\n");
4191
+ if (this.planMode) stableParts.push(PLAN_MODE_SYSTEM_ADDON);
4192
+ return { stable: stableParts.join("\n\n---\n\n"), volatile };
4190
4193
  }
4191
4194
  /**
4192
4195
  * 上下文压缩:调用当前 provider 生成对话摘要,替换旧消息,保留最近 COMPACT_KEEP_LAST 条。
@@ -4237,10 +4240,12 @@ ${skillContent}`);
4237
4240
  ...session.messages,
4238
4241
  { role: "user", content: summaryPromptText, timestamp: /* @__PURE__ */ new Date() }
4239
4242
  ];
4243
+ const { stable: syStable, volatile: syVolatile } = this.buildCurrentSystemPrompt();
4240
4244
  const response = await provider.chat({
4241
4245
  messages: summaryMessages,
4242
4246
  model: this.currentModel,
4243
- systemPrompt: this.buildCurrentSystemPrompt(),
4247
+ systemPrompt: syStable,
4248
+ systemPromptVolatile: syVolatile,
4244
4249
  stream: false,
4245
4250
  temperature: 0.3,
4246
4251
  maxTokens: Math.min(modelParams.maxTokens ?? DEFAULT_MAX_TOKENS, 4096),
@@ -4302,10 +4307,12 @@ ${response.content.trim()}
4302
4307
  timestamp: /* @__PURE__ */ new Date()
4303
4308
  }
4304
4309
  ];
4310
+ const { stable: snStable, volatile: snVolatile } = this.buildCurrentSystemPrompt();
4305
4311
  const response = await provider.chat({
4306
4312
  messages: snapshotMessages,
4307
4313
  model: this.currentModel,
4308
- systemPrompt: this.buildCurrentSystemPrompt(),
4314
+ systemPrompt: snStable,
4315
+ systemPromptVolatile: snVolatile,
4309
4316
  stream: false,
4310
4317
  temperature: 0.3,
4311
4318
  maxTokens: Math.min(modelParams.maxTokens ?? 4096, 4096),
@@ -4779,7 +4786,8 @@ Session '${this.resumeSessionId}' not found.
4779
4786
  */
4780
4787
  estimateConversationTokens() {
4781
4788
  let total = 0;
4782
- const sysPrompt = this.buildCurrentSystemPrompt();
4789
+ const { stable: estStable, volatile: estVolatile } = this.buildCurrentSystemPrompt();
4790
+ const sysPrompt = [estStable, estVolatile].filter(Boolean).join("\n\n---\n\n");
4783
4791
  if (sysPrompt) total += this.estimateTokens(sysPrompt);
4784
4792
  const session = this.sessions.current;
4785
4793
  if (session) {
@@ -5068,13 +5076,15 @@ Session '${this.resumeSessionId}' not found.
5068
5076
  const useStreaming = this.config.get("ui").streaming;
5069
5077
  const effectiveModel = modelOverride ?? this.currentModel;
5070
5078
  const modelParams = this.getModelParams(effectiveModel);
5079
+ const { stable: chatStable, volatile: chatVolatile } = this.buildCurrentSystemPrompt();
5071
5080
  if (useStreaming) {
5072
5081
  const ac = this.setupStreamInterrupt();
5073
5082
  try {
5074
5083
  const stream = provider.chatStream({
5075
5084
  messages,
5076
5085
  model: effectiveModel,
5077
- systemPrompt: this.buildCurrentSystemPrompt(),
5086
+ systemPrompt: chatStable,
5087
+ systemPromptVolatile: chatVolatile,
5078
5088
  stream: true,
5079
5089
  temperature: modelParams.temperature,
5080
5090
  maxTokens: modelParams.maxTokens,
@@ -5108,7 +5118,8 @@ Session '${this.resumeSessionId}' not found.
5108
5118
  const response = await provider.chat({
5109
5119
  messages,
5110
5120
  model: effectiveModel,
5111
- systemPrompt: this.buildCurrentSystemPrompt(),
5121
+ systemPrompt: chatStable,
5122
+ systemPromptVolatile: chatVolatile,
5112
5123
  stream: false,
5113
5124
  temperature: modelParams.temperature,
5114
5125
  maxTokens: modelParams.maxTokens,
@@ -5275,7 +5286,7 @@ Session '${this.resumeSessionId}' not found.
5275
5286
  const maxToolRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
5276
5287
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
5277
5288
  const autoPauseInterval = typeof autoPauseIntervalRaw === "number" ? autoPauseIntervalRaw : DEFAULT_AUTO_PAUSE_INTERVAL;
5278
- const baseSystemPrompt = (this.buildCurrentSystemPrompt() ?? "") + TOOL_CALL_REMINDER;
5289
+ const { stable: toolStable, volatile: toolVolatile } = this.buildCurrentSystemPrompt();
5279
5290
  const pauseHint = autoPauseInterval > 0 ? `
5280
5291
  - Every ${autoPauseInterval} rounds the user will be asked whether to continue \u2014 use this as a natural checkpoint to report progress.` : "";
5281
5292
  const roundBudgetHint = this.planMode ? `
@@ -5295,9 +5306,10 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
5295
5306
  - Do NOT read the same file more than once \u2014 use the content from previous reads.
5296
5307
  - Prioritize the most critical tasks first in case rounds run out.
5297
5308
  - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
5298
- const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
5309
+ const systemPrompt = toolStable + TOOL_CALL_REMINDER + roundBudgetHint + (mcpBudgetNote ? `
5299
5310
 
5300
5311
  ${mcpBudgetNote}` : "");
5312
+ const systemPromptVolatile = toolVolatile;
5301
5313
  const modelParams = this.getModelParams(effectiveModel);
5302
5314
  const useStreaming = this.config.get("ui").streaming;
5303
5315
  const spinner = this.renderer.showSpinner("Thinking...");
@@ -5418,6 +5430,7 @@ ${mcpBudgetNote}` : "");
5418
5430
  messages: apiMessages,
5419
5431
  model: effectiveModel,
5420
5432
  systemPrompt,
5433
+ systemPromptVolatile,
5421
5434
  stream: false,
5422
5435
  temperature: modelParams.temperature,
5423
5436
  maxTokens: modelParams.maxTokens,
@@ -5573,6 +5586,7 @@ ${mcpBudgetNote}` : "");
5573
5586
  messages: apiMessages,
5574
5587
  model: effectiveModel,
5575
5588
  systemPrompt,
5589
+ systemPromptVolatile,
5576
5590
  stream: true,
5577
5591
  temperature: modelParams.temperature,
5578
5592
  maxTokens: modelParams.maxTokens,
@@ -5623,14 +5637,22 @@ ${mcpBudgetNote}` : "");
5623
5637
  googleSearchContext.configManager = this.config;
5624
5638
  streamToFileContext.provider = provider;
5625
5639
  streamToFileContext.model = effectiveModel;
5626
- streamToFileContext.systemPrompt = systemPrompt;
5640
+ streamToFileContext.systemPrompt = systemPromptVolatile ? `${systemPrompt}
5641
+
5642
+ ---
5643
+
5644
+ ${systemPromptVolatile}` : systemPrompt;
5627
5645
  streamToFileContext.messages = apiMessages;
5628
5646
  streamToFileContext.extraMessages = extraMessages;
5629
5647
  streamToFileContext.temperature = modelParams.temperature;
5630
5648
  streamToFileContext.timeout = modelParams.timeout;
5631
5649
  spawnAgentContext.provider = provider;
5632
5650
  spawnAgentContext.model = effectiveModel;
5633
- spawnAgentContext.systemPrompt = systemPrompt;
5651
+ spawnAgentContext.systemPrompt = systemPromptVolatile ? `${systemPrompt}
5652
+
5653
+ ---
5654
+
5655
+ ${systemPromptVolatile}` : systemPrompt;
5634
5656
  spawnAgentContext.modelParams = modelParams;
5635
5657
  spawnAgentContext.configManager = this.config;
5636
5658
  ToolExecutor.currentMessageIndex = session.messages.length;
@@ -5791,6 +5813,7 @@ ${mcpBudgetNote}` : "");
5791
5813
  messages: apiMessages,
5792
5814
  model: effectiveModel,
5793
5815
  systemPrompt,
5816
+ systemPromptVolatile,
5794
5817
  stream: false,
5795
5818
  temperature: modelParams.temperature,
5796
5819
  maxTokens: modelParams.maxTokens,
@@ -5852,10 +5875,12 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5852
5875
  try {
5853
5876
  const provider = this.providers.get(this.currentProvider);
5854
5877
  const modelParams = this.getModelParams();
5878
+ const { stable: ccStable, volatile: ccVolatile } = this.buildCurrentSystemPrompt();
5855
5879
  const response = await provider.chat({
5856
5880
  messages: [{ role: "user", content: prompt, timestamp: /* @__PURE__ */ new Date() }],
5857
5881
  model: this.currentModel,
5858
- systemPrompt: this.buildCurrentSystemPrompt(),
5882
+ systemPrompt: ccStable,
5883
+ systemPromptVolatile: ccVolatile,
5859
5884
  stream: false,
5860
5885
  temperature: 0.3,
5861
5886
  maxTokens: modelParams.maxTokens ?? 4096,
@@ -5910,7 +5935,11 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5910
5935
  getCurrentProvider: () => this.currentProvider,
5911
5936
  getCurrentModel: () => this.currentModel,
5912
5937
  getContextLayers: () => [...this.contextLayers],
5913
- getActiveSystemPrompt: () => this.buildCurrentSystemPrompt(),
5938
+ getActiveSystemPrompt: () => {
5939
+ const { stable, volatile } = this.buildCurrentSystemPrompt();
5940
+ const combined = [stable, volatile].filter(Boolean).join("\n\n---\n\n");
5941
+ return combined || void 0;
5942
+ },
5914
5943
  reloadContext: () => {
5915
5944
  const { layers, mergedContent } = this.loadHierarchicalContext();
5916
5945
  this.contextLayers = layers;
@@ -5963,10 +5992,12 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5963
5992
  chatOnce: async (prompt, options) => {
5964
5993
  const provider = this.providers.get(this.currentProvider);
5965
5994
  const modelParams = this.getModelParams();
5995
+ const { stable: coStable, volatile: coVolatile } = this.buildCurrentSystemPrompt();
5966
5996
  const response = await provider.chat({
5967
5997
  messages: [{ role: "user", content: prompt, timestamp: /* @__PURE__ */ new Date() }],
5968
5998
  model: this.currentModel,
5969
- systemPrompt: this.buildCurrentSystemPrompt(),
5999
+ systemPrompt: coStable,
6000
+ systemPromptVolatile: coVolatile,
5970
6001
  stream: false,
5971
6002
  temperature: options?.temperature ?? 0.3,
5972
6003
  maxTokens: options?.maxTokens ?? modelParams.maxTokens ?? 4096,
@@ -6108,7 +6139,7 @@ program.command("web").description("Start Web UI server with browser-based chat
6108
6139
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
6109
6140
  process.exit(1);
6110
6141
  }
6111
- const { startWebServer } = await import("./server-MDBQX5UZ.js");
6142
+ const { startWebServer } = await import("./server-WYL3OD5N.js");
6112
6143
  await startWebServer({ port, host: options.host });
6113
6144
  });
6114
6145
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -6341,7 +6372,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
6341
6372
  }),
6342
6373
  config.get("customProviders")
6343
6374
  );
6344
- const { startHub } = await import("./hub-4VPTOMBP.js");
6375
+ const { startHub } = await import("./hub-BGO4X72R.js");
6345
6376
  await startHub(
6346
6377
  {
6347
6378
  topic: topic ?? "",
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-3LCVJ4AF.js";
5
+ } from "./chunk-ND3O5NQU.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-VO5IZN2C.js";
4
+ } from "./chunk-YJKPARSH.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -21,7 +21,7 @@ import {
21
21
  persistToolRound,
22
22
  rebuildExtraMessages,
23
23
  setupProxy
24
- } from "./chunk-G5AISHJE.js";
24
+ } from "./chunk-3YT2DUUT.js";
25
25
  import {
26
26
  AuthManager
27
27
  } from "./chunk-BYNY5JPB.js";
@@ -42,7 +42,7 @@ import {
42
42
  spawnAgentContext,
43
43
  truncateOutput,
44
44
  undoStack
45
- } from "./chunk-Q5QSCO5D.js";
45
+ } from "./chunk-IVTWWDWZ.js";
46
46
  import "./chunk-4BKXL7SM.js";
47
47
  import {
48
48
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -62,7 +62,7 @@ import {
62
62
  SKILLS_DIR_NAME,
63
63
  VERSION,
64
64
  buildUserIdentityPrompt
65
- } from "./chunk-3LCVJ4AF.js";
65
+ } from "./chunk-ND3O5NQU.js";
66
66
 
67
67
  // src/web/server.ts
68
68
  import express from "express";
@@ -393,7 +393,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
393
393
  // src/core/system-prompt-builder.ts
394
394
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
395
395
  import { join } from "path";
396
- function buildSystemPrompt(ctx) {
396
+ function buildSystemPromptParts(ctx) {
397
397
  const now = /* @__PURE__ */ new Date();
398
398
  const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
399
399
  const pad = (n) => String(n).padStart(2, "0");
@@ -402,28 +402,25 @@ function buildSystemPrompt(ctx) {
402
402
  const dateTimeInfo = `Current date and time: ${dateStr} ${timeStr}`;
403
403
  const osName = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
404
404
  const shellInfo = process.platform === "win32" ? "The bash tool uses PowerShell (not cmd). Use PowerShell command syntax. Do not use Unix commands (e.g. grep, head, find, wc, date +format, etc.), use PowerShell equivalents instead (e.g. Select-String, Select-Object -First, Get-ChildItem, Measure-Object, Get-Date -Format, etc.)." : `The bash tool uses ${process.env.SHELL || "/bin/bash"}.`;
405
- const envInfo = `OS: ${osName}
405
+ const volatile = `${dateTimeInfo}
406
+ OS: ${osName}
406
407
  ${shellInfo}
407
408
  Working directory: ${process.cwd()}`;
408
- const parts = [dateTimeInfo + "\n" + envInfo];
409
+ const stableParts = [];
409
410
  if (ctx.userProfile) {
410
411
  const identityPrompt = buildUserIdentityPrompt(ctx.userProfile);
411
- if (identityPrompt) {
412
- parts.push(identityPrompt);
413
- }
412
+ if (identityPrompt) stableParts.push(identityPrompt);
414
413
  }
415
- parts.push(AGENTIC_BEHAVIOR_GUIDELINE);
414
+ stableParts.push(AGENTIC_BEHAVIOR_GUIDELINE);
416
415
  if (ctx.configDir) {
417
416
  const memory = loadMemoryContent(ctx.configDir);
418
- if (memory) {
419
- parts.push(`# Persistent Memory
417
+ if (memory) stableParts.push(`# Persistent Memory
420
418
 
421
419
  ${memory.content}`);
422
- }
423
420
  }
424
421
  const devState = loadDevState();
425
422
  if (devState) {
426
- parts.push(
423
+ stableParts.push(
427
424
  `# Development State Handoff
428
425
 
429
426
  The following is a development state snapshot generated by the previous AI model. Please seamlessly continue the user's work based on this.
@@ -432,25 +429,21 @@ Important: If the snapshot mentions key reference files (e.g. templates, format
432
429
  ` + devState
433
430
  );
434
431
  }
435
- if (ctx.activeSystemPrompt) {
436
- parts.push(ctx.activeSystemPrompt);
437
- }
432
+ if (ctx.activeSystemPrompt) stableParts.push(ctx.activeSystemPrompt);
438
433
  if (ctx.extraContextDirs) {
439
434
  for (const { dir, content } of ctx.extraContextDirs) {
440
- parts.push(`# Added Directory Context: ${dir}
435
+ stableParts.push(`# Added Directory Context: ${dir}
441
436
 
442
437
  ${content}`);
443
438
  }
444
439
  }
445
440
  if (ctx.activeSkill) {
446
- parts.push(`# Active Skill: ${ctx.activeSkill.name}
441
+ stableParts.push(`# Active Skill: ${ctx.activeSkill.name}
447
442
 
448
443
  ${ctx.activeSkill.content}`);
449
444
  }
450
- if (ctx.planMode) {
451
- parts.push(PLAN_MODE_SYSTEM_ADDON);
452
- }
453
- return parts.join("\n\n---\n\n");
445
+ if (ctx.planMode) stableParts.push(PLAN_MODE_SYSTEM_ADDON);
446
+ return { stable: stableParts.join("\n\n---\n\n"), volatile };
454
447
  }
455
448
  function loadMemoryContent(configDir) {
456
449
  const memoryPath = join(configDir, MEMORY_FILE_NAME);
@@ -785,10 +778,12 @@ var SessionHandler = class _SessionHandler {
785
778
  const ac = new AbortController();
786
779
  this.abortController = ac;
787
780
  try {
781
+ const { stable: simplStable, volatile: simplVolatile } = this.buildSystemPrompt();
788
782
  const stream = provider.chatStream({
789
783
  messages,
790
784
  model: this.currentModel,
791
- systemPrompt: this.buildSystemPrompt(),
785
+ systemPrompt: simplStable,
786
+ systemPromptVolatile: simplVolatile,
792
787
  stream: true,
793
788
  temperature: modelParams.temperature,
794
789
  maxTokens: modelParams.maxTokens,
@@ -830,7 +825,7 @@ var SessionHandler = class _SessionHandler {
830
825
  const maxToolRounds = this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
831
826
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
832
827
  const autoPauseInterval = typeof autoPauseIntervalRaw === "number" ? autoPauseIntervalRaw : 50;
833
- const baseSystemPrompt = (this.buildSystemPrompt() ?? "") + TOOL_CALL_REMINDER;
828
+ const { stable: toolStable, volatile: toolVolatile } = this.buildSystemPrompt();
834
829
  const pauseHint = autoPauseInterval > 0 ? `
835
830
  - Every ${autoPauseInterval} rounds the user will be asked whether to continue \u2014 use this as a natural checkpoint to report progress.` : "";
836
831
  const roundBudgetHint = `
@@ -840,9 +835,10 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
840
835
  - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
841
836
  - Prioritize the most critical tasks first in case rounds run out.
842
837
  - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
843
- const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
838
+ const systemPrompt = toolStable + TOOL_CALL_REMINDER + roundBudgetHint + (mcpBudgetNote ? `
844
839
 
845
840
  ${mcpBudgetNote}` : "");
841
+ const systemPromptVolatile = toolVolatile;
846
842
  const modelParams = this.getModelParams();
847
843
  const roundUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
848
844
  const supportsStreamingTools = typeof provider.chatWithToolsStream === "function";
@@ -929,6 +925,7 @@ Too much tool output accumulated this turn. Your work so far is preserved.
929
925
  messages: apiMessages,
930
926
  model: this.currentModel,
931
927
  systemPrompt,
928
+ systemPromptVolatile,
932
929
  stream: false,
933
930
  temperature: modelParams.temperature,
934
931
  maxTokens: modelParams.maxTokens,
@@ -1028,7 +1025,11 @@ Details: ${errMsg.split("\n")[0]}
1028
1025
  googleSearchContext.configManager = this.config;
1029
1026
  spawnAgentContext.provider = provider;
1030
1027
  spawnAgentContext.model = this.currentModel;
1031
- spawnAgentContext.systemPrompt = systemPrompt;
1028
+ spawnAgentContext.systemPrompt = systemPromptVolatile ? `${systemPrompt}
1029
+
1030
+ ---
1031
+
1032
+ ${systemPromptVolatile}` : systemPrompt;
1032
1033
  spawnAgentContext.modelParams = modelParams;
1033
1034
  spawnAgentContext.configManager = this.config;
1034
1035
  ToolExecutor.currentMessageIndex = this.sessions.current?.messages.length ?? 0;
@@ -1106,6 +1107,7 @@ Details: ${errMsg.split("\n")[0]}
1106
1107
  messages: apiMessages,
1107
1108
  model: this.currentModel,
1108
1109
  systemPrompt,
1110
+ systemPromptVolatile,
1109
1111
  stream: false,
1110
1112
  temperature: modelParams.temperature,
1111
1113
  maxTokens: modelParams.maxTokens,
@@ -1946,7 +1948,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1946
1948
  case "test": {
1947
1949
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1948
1950
  try {
1949
- const { executeTests } = await import("./run-tests-WD53PYVA.js");
1951
+ const { executeTests } = await import("./run-tests-NPRCZYN3.js");
1950
1952
  const argStr = args.join(" ").trim();
1951
1953
  let testArgs = {};
1952
1954
  if (argStr) {
@@ -2500,13 +2502,14 @@ Add .md files to create commands.` });
2500
2502
  buildSystemPrompt() {
2501
2503
  const skillContent = this.skillManager?.getActivePromptContent();
2502
2504
  const activeSkill = skillContent && this.skillManager?.getActive() ? { name: this.skillManager.getActive().meta.name, content: skillContent } : void 0;
2503
- let prompt = buildSystemPrompt({
2505
+ const { stable: baseStable, volatile } = buildSystemPromptParts({
2504
2506
  activeSystemPrompt: this.activeSystemPrompt,
2505
2507
  activeSkill,
2506
2508
  planMode: this.planMode,
2507
2509
  configDir: this.config.getConfigDir(),
2508
2510
  userProfile: this.config.get("userProfile")
2509
2511
  });
2512
+ let stable = baseStable;
2510
2513
  if (this.addedDirs.size > 0) {
2511
2514
  const MAX_DIR_CONTEXT = 4e4;
2512
2515
  let dirContext = "\n\n--- Added Directory Context ---\n";
@@ -2519,9 +2522,9 @@ Add .md files to create commands.` });
2519
2522
  dirContext += this.scanDirTree(dir, 2, 40) + "\n";
2520
2523
  totalLen = dirContext.length;
2521
2524
  }
2522
- prompt += dirContext;
2525
+ stable += dirContext;
2523
2526
  }
2524
- return prompt;
2527
+ return { stable, volatile };
2525
2528
  }
2526
2529
  getModelParams() {
2527
2530
  const allParams = this.config.get("modelParams");
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-Q5QSCO5D.js";
7
+ } from "./chunk-IVTWWDWZ.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-3LCVJ4AF.js";
11
+ } from "./chunk-ND3O5NQU.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.68",
3
+ "version": "0.4.70",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",