jinzd-ai-cli 0.4.60 → 0.4.62

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.
@@ -10,7 +10,7 @@ import {
10
10
  SUBAGENT_DEFAULT_MAX_ROUNDS,
11
11
  SUBAGENT_MAX_ROUNDS_LIMIT,
12
12
  runTestsTool
13
- } from "./chunk-2DWWB4KD.js";
13
+ } from "./chunk-LLI6COMK.js";
14
14
 
15
15
  // src/tools/builtin/bash.ts
16
16
  import { execSync } from "child_process";
@@ -4062,6 +4062,28 @@ var notebookEditTool = {
4062
4062
  }
4063
4063
  };
4064
4064
 
4065
+ // src/core/token-estimator.ts
4066
+ var CJK_REGEX = /[\u2E80-\u9FFF\uA000-\uA4FF\uAC00-\uD7FF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
4067
+ function estimateTokens(text) {
4068
+ if (!text) return 0;
4069
+ const cjkMatches = text.match(CJK_REGEX);
4070
+ const cjkCount = cjkMatches ? cjkMatches.length : 0;
4071
+ const nonCjkCount = text.length - cjkCount;
4072
+ const tokens = cjkCount * 1.5 + nonCjkCount * 0.25;
4073
+ return Math.ceil(tokens) + 4;
4074
+ }
4075
+ function estimateToolDefinitionTokens(def) {
4076
+ let charCount = def.name.length + def.description.length;
4077
+ for (const [key, param] of Object.entries(def.parameters)) {
4078
+ charCount += key.length + param.type.length + param.description.length;
4079
+ if (param.enum) {
4080
+ charCount += param.enum.join(" ").length;
4081
+ }
4082
+ }
4083
+ const structuralOverhead = 40 + Object.keys(def.parameters).length * 30;
4084
+ return Math.ceil((charCount + structuralOverhead) * 0.25) + 10;
4085
+ }
4086
+
4065
4087
  // src/tools/registry.ts
4066
4088
  import { pathToFileURL } from "url";
4067
4089
  import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
@@ -4110,6 +4132,74 @@ var ToolRegistry = class {
4110
4132
  getDefinitions() {
4111
4133
  return [...this.tools.values()].map((t) => t.definition);
4112
4134
  }
4135
+ /**
4136
+ * Return tool definitions within a token budget, trimming MCP tools if needed.
4137
+ *
4138
+ * Strategy: always include all builtin + plugin tools. If total tokens exceed
4139
+ * the budget, keep only MCP tools that were used in this session (by name),
4140
+ * trimming the rest. Returns { definitions, trimmedCount, systemNote }.
4141
+ *
4142
+ * @param tokenBudget Max tokens for tool definitions (typically 20% of context window)
4143
+ * @param usedToolNames Names of MCP tools already called this session (always kept)
4144
+ */
4145
+ getDefinitionsWithBudget(tokenBudget, usedToolNames) {
4146
+ const allDefs = this.getDefinitions();
4147
+ let totalTokens = 0;
4148
+ for (const def of allDefs) {
4149
+ totalTokens += estimateToolDefinitionTokens(def);
4150
+ }
4151
+ if (totalTokens <= tokenBudget) {
4152
+ return { definitions: allDefs, trimmedCount: 0, systemNote: null };
4153
+ }
4154
+ const builtinDefs = [];
4155
+ const mcpDefs = [];
4156
+ for (const def of allDefs) {
4157
+ if (this.mcpToolNames.has(def.name)) {
4158
+ mcpDefs.push(def);
4159
+ } else {
4160
+ builtinDefs.push(def);
4161
+ }
4162
+ }
4163
+ let budget = tokenBudget;
4164
+ for (const def of builtinDefs) {
4165
+ budget -= estimateToolDefinitionTokens(def);
4166
+ }
4167
+ const kept = [...builtinDefs];
4168
+ const used = usedToolNames ?? /* @__PURE__ */ new Set();
4169
+ const remaining = [];
4170
+ for (const def of mcpDefs) {
4171
+ if (used.has(def.name)) {
4172
+ const cost = estimateToolDefinitionTokens(def);
4173
+ budget -= cost;
4174
+ kept.push(def);
4175
+ } else {
4176
+ remaining.push(def);
4177
+ }
4178
+ }
4179
+ let trimmed = 0;
4180
+ for (const def of remaining) {
4181
+ const cost = estimateToolDefinitionTokens(def);
4182
+ if (budget >= cost) {
4183
+ budget -= cost;
4184
+ kept.push(def);
4185
+ } else {
4186
+ trimmed++;
4187
+ }
4188
+ }
4189
+ let systemNote = null;
4190
+ if (trimmed > 0) {
4191
+ const trimmedServers = /* @__PURE__ */ new Set();
4192
+ const keptNames = new Set(kept.map((d) => d.name));
4193
+ for (const def of mcpDefs) {
4194
+ if (!keptNames.has(def.name)) {
4195
+ const parts = def.name.split("__");
4196
+ if (parts.length >= 2) trimmedServers.add(parts[1]);
4197
+ }
4198
+ }
4199
+ systemNote = `[MCP Tool Budget] ${trimmed} MCP tool(s) from server(s) [${[...trimmedServers].join(", ")}] were excluded to fit the context window. If you need a specific MCP tool that isn't listed, tell the user to ask for it explicitly.`;
4200
+ }
4201
+ return { definitions: kept, trimmedCount: trimmed, systemNote };
4202
+ }
4113
4203
  listAll() {
4114
4204
  return [...this.tools.values()];
4115
4205
  }
@@ -4229,5 +4319,6 @@ export {
4229
4319
  askUserContext,
4230
4320
  googleSearchContext,
4231
4321
  spawnAgentContext,
4322
+ estimateTokens,
4232
4323
  ToolRegistry
4233
4324
  };
@@ -8,7 +8,7 @@ import {
8
8
  RateLimitError,
9
9
  schemaToJsonSchema,
10
10
  truncateForPersist
11
- } from "./chunk-C32FFHMY.js";
11
+ } from "./chunk-672OV76Z.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-2DWWB4KD.js";
24
+ } from "./chunk-LLI6COMK.js";
25
25
 
26
26
  // src/config/config-manager.ts
27
27
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -2462,14 +2462,25 @@ var Session = class _Session {
2462
2462
  /**
2463
2463
  * 上下文压缩:用摘要消息替换旧消息,保留最近 keepLast 条。
2464
2464
  *
2465
+ * Tool-history-aware: if the cut point lands inside a tool round
2466
+ * (assistant+toolCalls followed by tool results), expand to keep the
2467
+ * entire round intact. This prevents orphaned tool results.
2468
+ *
2465
2469
  * 压缩后消息结构:
2466
- * [summaryMsg(user), ackMsg(assistant), ...最近 keepLast 条原始消息]
2470
+ * [summaryMsg(user), ackMsg(assistant), ...最近 N 条原始消息]
2467
2471
  *
2468
2472
  * @returns 被删除的消息条数
2469
2473
  */
2470
2474
  compact(summaryMsg, ackMsg, keepLast) {
2471
- const preserved = this.messages.slice(-keepLast);
2472
- const removedCount = this.messages.length - preserved.length;
2475
+ let cutIndex = this.messages.length - keepLast;
2476
+ if (cutIndex <= 0) {
2477
+ return 0;
2478
+ }
2479
+ while (cutIndex > 0 && this.messages[cutIndex]?.role === "tool") {
2480
+ cutIndex--;
2481
+ }
2482
+ const preserved = this.messages.slice(cutIndex);
2483
+ const removedCount = cutIndex;
2473
2484
  this.messages = [summaryMsg, ackMsg, ...preserved];
2474
2485
  this.updated = /* @__PURE__ */ new Date();
2475
2486
  return removedCount;
@@ -3687,6 +3698,7 @@ function formatCost(amount) {
3687
3698
  }
3688
3699
 
3689
3700
  // src/session/tool-history.ts
3701
+ var SESSION_SIZE_LIMIT = 2 * 1024 * 1024;
3690
3702
  function persistToolRound(session, toolCalls, toolResults, opts) {
3691
3703
  session.addMessage({
3692
3704
  role: "assistant",
@@ -3757,6 +3769,52 @@ function rebuildExtraMessages(provider, toolHistory) {
3757
3769
  }
3758
3770
  return result;
3759
3771
  }
3772
+ function trimOldToolOutput(messages, keepRecentRounds = 10) {
3773
+ const roundStarts = [];
3774
+ for (let i = 0; i < messages.length; i++) {
3775
+ if (messages[i].role === "assistant" && messages[i].toolCalls?.length) {
3776
+ roundStarts.push(i);
3777
+ }
3778
+ }
3779
+ if (roundStarts.length <= keepRecentRounds) return 0;
3780
+ const cutoffRoundIdx = roundStarts.length - keepRecentRounds;
3781
+ let trimmed = 0;
3782
+ for (let r = 0; r < cutoffRoundIdx; r++) {
3783
+ const start = roundStarts[r];
3784
+ const end = r + 1 < roundStarts.length ? roundStarts[r + 1] : messages.length;
3785
+ const assistantMsg = messages[start];
3786
+ if (typeof assistantMsg.content === "string" && assistantMsg.content.length > 200) {
3787
+ assistantMsg.content = assistantMsg.content.slice(0, 100) + "\u2026 [trimmed for size]";
3788
+ trimmed++;
3789
+ }
3790
+ for (let j = start + 1; j < end; j++) {
3791
+ const msg = messages[j];
3792
+ if (msg.role === "tool") {
3793
+ const status = msg.isError ? "\u2717 error" : "\u2713 ok";
3794
+ const name = msg.toolName ?? "unknown";
3795
+ const currentContent = typeof msg.content === "string" ? msg.content : "";
3796
+ if (currentContent.length > 200) {
3797
+ msg.content = `[${name}: ${status}] (output trimmed for size \u2014 ${currentContent.length} chars)`;
3798
+ trimmed++;
3799
+ }
3800
+ }
3801
+ }
3802
+ }
3803
+ return trimmed;
3804
+ }
3805
+ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
3806
+ const json = JSON.stringify(session.toJSON());
3807
+ if (json.length <= sizeLimit) return false;
3808
+ let keepRecent = 10;
3809
+ while (keepRecent >= 2) {
3810
+ const trimmed = trimOldToolOutput(session.messages, keepRecent);
3811
+ if (trimmed === 0) break;
3812
+ const newSize = JSON.stringify(session.toJSON()).length;
3813
+ if (newSize <= sizeLimit) return true;
3814
+ keepRecent = Math.max(2, Math.floor(keepRecent / 2));
3815
+ }
3816
+ return true;
3817
+ }
3760
3818
 
3761
3819
  // src/repl/dev-state.ts
3762
3820
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync4 } from "fs";
@@ -3871,6 +3929,7 @@ export {
3871
3929
  persistToolRound,
3872
3930
  extractToolHistory,
3873
3931
  rebuildExtraMessages,
3932
+ autoTrimSessionIfNeeded,
3874
3933
  SNAPSHOT_PROMPT,
3875
3934
  sessionHasMeaningfulContent,
3876
3935
  saveDevState,
@@ -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.60";
11
+ var VERSION = "0.4.62";
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.60";
9
+ var VERSION = "0.4.62";
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-Z4IK3UEA.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-2ATTBG2S.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  SessionManager,
9
9
  SkillManager,
10
10
  TOOL_CALL_REMINDER,
11
+ autoTrimSessionIfNeeded,
11
12
  buildPhantomCorrectionMessage,
12
13
  buildWriteRoundReminder,
13
14
  clearDevState,
@@ -30,11 +31,12 @@ import {
30
31
  saveDevState,
31
32
  sessionHasMeaningfulContent,
32
33
  setupProxy
33
- } from "./chunk-3YVHYAXK.js";
34
+ } from "./chunk-6OTF2ILP.js";
34
35
  import {
35
36
  ToolExecutor,
36
37
  ToolRegistry,
37
38
  askUserContext,
39
+ estimateTokens,
38
40
  googleSearchContext,
39
41
  initTheme,
40
42
  lastResponseStore,
@@ -44,7 +46,7 @@ import {
44
46
  spawnAgentContext,
45
47
  theme,
46
48
  undoStack
47
- } from "./chunk-C32FFHMY.js";
49
+ } from "./chunk-672OV76Z.js";
48
50
  import {
49
51
  fileCheckpoints
50
52
  } from "./chunk-4BKXL7SM.js";
@@ -69,7 +71,7 @@ import {
69
71
  SKILLS_DIR_NAME,
70
72
  VERSION,
71
73
  buildUserIdentityPrompt
72
- } from "./chunk-2DWWB4KD.js";
74
+ } from "./chunk-LLI6COMK.js";
73
75
 
74
76
  // src/index.ts
75
77
  import { program } from "commander";
@@ -2164,7 +2166,7 @@ ${hint}` : "")
2164
2166
  usage: "/test [command|filter]",
2165
2167
  async execute(args, ctx) {
2166
2168
  try {
2167
- const { executeTests } = await import("./run-tests-QGJHXL5Z.js");
2169
+ const { executeTests } = await import("./run-tests-ORVJAUJG.js");
2168
2170
  const argStr = args.join(" ").trim();
2169
2171
  let testArgs = {};
2170
2172
  if (argStr) {
@@ -3910,6 +3912,11 @@ ${skillContent}`);
3910
3912
  "4. Current status and tasks not yet completed",
3911
3913
  "5. Key details to remember (file paths, config values, error messages, variable names, etc.)",
3912
3914
  "",
3915
+ "If the conversation contains tool call rounds (assistant calling tools like read_file, write_file, bash, etc.), ",
3916
+ 'summarize each round as a single line: "- [tool_name] target \u2192 result/status". ',
3917
+ 'Do NOT include raw tool output. Group consecutive similar operations (e.g., "read 5 files in src/") ',
3918
+ "instead of listing each one individually.",
3919
+ "",
3913
3920
  "Requirements: Be well-organized so that subsequent conversation can seamlessly continue from this summary."
3914
3921
  ];
3915
3922
  if (instruction) summaryPromptLines.push("", `Additional requirement: ${instruction}`);
@@ -4317,6 +4324,9 @@ Session '${this.resumeSessionId}' not found.
4317
4324
  await this.handleChatSimple(provider, session.messages);
4318
4325
  }
4319
4326
  if (this.config.get("session").autoSave) {
4327
+ if (autoTrimSessionIfNeeded(session)) {
4328
+ process.stderr.write(theme.dim(" [session] Trimmed old tool output to reduce file size\n"));
4329
+ }
4320
4330
  await this.sessions.save();
4321
4331
  }
4322
4332
  const elapsed = Date.now() - t0;
@@ -4399,7 +4409,7 @@ Session '${this.resumeSessionId}' not found.
4399
4409
  * 混合 CJK / ASCII 文本平均约 2.5 字符 = 1 token(与 renderer.ts 中的估算公式一致)。
4400
4410
  */
4401
4411
  estimateTokens(text) {
4402
- return Math.ceil(text.length / 2.5);
4412
+ return estimateTokens(text);
4403
4413
  }
4404
4414
  /**
4405
4415
  * 估算当前对话的总 token 消耗(system prompt + 所有 session messages)。
@@ -4418,6 +4428,11 @@ Session '${this.resumeSessionId}' not found.
4418
4428
  if (part.type === "text" && part.text) total += this.estimateTokens(part.text);
4419
4429
  }
4420
4430
  }
4431
+ if (msg.toolCalls) {
4432
+ for (const tc of msg.toolCalls) {
4433
+ total += this.estimateTokens(JSON.stringify(tc.arguments));
4434
+ }
4435
+ }
4421
4436
  }
4422
4437
  }
4423
4438
  return total;
@@ -4856,6 +4871,8 @@ Session '${this.resumeSessionId}' not found.
4856
4871
  async handleChatWithTools(provider, messages) {
4857
4872
  const session = this.sessions.current;
4858
4873
  let toolDefs;
4874
+ let mcpBudgetNote = null;
4875
+ const usedMcpToolNames = /* @__PURE__ */ new Set();
4859
4876
  if (this.planMode) {
4860
4877
  toolDefs = this.toolRegistry.getDefinitions().filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name));
4861
4878
  } else {
@@ -4863,7 +4880,19 @@ Session '${this.resumeSessionId}' not found.
4863
4880
  if (skillFilter) {
4864
4881
  toolDefs = this.toolRegistry.getDefinitions().filter((t) => skillFilter.has(t.name));
4865
4882
  } else {
4866
- toolDefs = this.toolRegistry.getDefinitions();
4883
+ const contextWindow = this.getContextWindowSize();
4884
+ if (contextWindow > 0) {
4885
+ const toolBudget = Math.floor(contextWindow * 0.2);
4886
+ const { definitions, trimmedCount, systemNote } = this.toolRegistry.getDefinitionsWithBudget(toolBudget, usedMcpToolNames);
4887
+ toolDefs = definitions;
4888
+ mcpBudgetNote = systemNote;
4889
+ if (trimmedCount > 0) {
4890
+ process.stderr.write(theme.dim(` [MCP budget] ${trimmedCount} MCP tool(s) trimmed to fit ${toolBudget.toLocaleString()} token budget
4891
+ `));
4892
+ }
4893
+ } else {
4894
+ toolDefs = this.toolRegistry.getDefinitions();
4895
+ }
4867
4896
  }
4868
4897
  }
4869
4898
  if (this.allowedTools) {
@@ -4898,7 +4927,9 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
4898
4927
  - Do NOT read the same file more than once \u2014 use the content from previous reads.
4899
4928
  - Prioritize the most critical tasks first in case rounds run out.
4900
4929
  - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
4901
- const systemPrompt = baseSystemPrompt + roundBudgetHint;
4930
+ const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
4931
+
4932
+ ${mcpBudgetNote}` : "");
4902
4933
  const modelParams = this.getModelParams();
4903
4934
  const useStreaming = this.config.get("ui").streaming;
4904
4935
  const spinner = this.renderer.showSpinner("Thinking...");
@@ -5264,6 +5295,9 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
5264
5295
  const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
5265
5296
  const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults, reasoningContent);
5266
5297
  extraMessages.push(...newMsgs);
5298
+ for (const tc of result.toolCalls) {
5299
+ if (tc.name.startsWith("mcp__")) usedMcpToolNames.add(tc.name);
5300
+ }
5267
5301
  const streamedContent = "content" in result ? result.content : void 0;
5268
5302
  persistToolRound(session, result.toolCalls, toolResults, {
5269
5303
  assistantContent: streamedContent,
@@ -5704,7 +5738,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5704
5738
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5705
5739
  process.exit(1);
5706
5740
  }
5707
- const { startWebServer } = await import("./server-L2XJYXMB.js");
5741
+ const { startWebServer } = await import("./server-HBAOUOIC.js");
5708
5742
  await startWebServer({ port, host: options.host });
5709
5743
  });
5710
5744
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5937,7 +5971,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5937
5971
  }),
5938
5972
  config.get("customProviders")
5939
5973
  );
5940
- const { startHub } = await import("./hub-JTMNY7JR.js");
5974
+ const { startHub } = await import("./hub-2XLQSZY2.js");
5941
5975
  await startHub(
5942
5976
  {
5943
5977
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-X4GL6D5L.js";
4
+ } from "./chunk-YUUCUJHU.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-2DWWB4KD.js";
5
+ } from "./chunk-LLI6COMK.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -7,6 +7,7 @@ import {
7
7
  SessionManager,
8
8
  SkillManager,
9
9
  TOOL_CALL_REMINDER,
10
+ autoTrimSessionIfNeeded,
10
11
  computeCost,
11
12
  detectsHallucinatedFileOp,
12
13
  extractToolHistory,
@@ -20,7 +21,7 @@ import {
20
21
  persistToolRound,
21
22
  rebuildExtraMessages,
22
23
  setupProxy
23
- } from "./chunk-3YVHYAXK.js";
24
+ } from "./chunk-6OTF2ILP.js";
24
25
  import {
25
26
  AuthManager
26
27
  } from "./chunk-BYNY5JPB.js";
@@ -29,6 +30,7 @@ import {
29
30
  ToolRegistry,
30
31
  askUserContext,
31
32
  checkPermission,
33
+ estimateTokens,
32
34
  getDangerLevel,
33
35
  googleSearchContext,
34
36
  isFileWriteTool,
@@ -39,7 +41,7 @@ import {
39
41
  spawnAgentContext,
40
42
  truncateOutput,
41
43
  undoStack
42
- } from "./chunk-C32FFHMY.js";
44
+ } from "./chunk-672OV76Z.js";
43
45
  import "./chunk-4BKXL7SM.js";
44
46
  import {
45
47
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -59,7 +61,7 @@ import {
59
61
  SKILLS_DIR_NAME,
60
62
  VERSION,
61
63
  buildUserIdentityPrompt
62
- } from "./chunk-2DWWB4KD.js";
64
+ } from "./chunk-LLI6COMK.js";
63
65
 
64
66
  // src/web/server.ts
65
67
  import express from "express";
@@ -657,7 +659,7 @@ var SessionHandler = class _SessionHandler {
657
659
  }
658
660
  /** 粗略估算文本 token 数(2.5 chars/token)*/
659
661
  estTokens(text) {
660
- return Math.ceil(text.length / 2.5);
662
+ return estimateTokens(text);
661
663
  }
662
664
  /**
663
665
  * 估算当前 agentic 请求总 token 数(session messages + extraMessages + system prompt)。
@@ -678,6 +680,11 @@ var SessionHandler = class _SessionHandler {
678
680
  }
679
681
  }
680
682
  }
683
+ if (msg.toolCalls) {
684
+ for (const tc of msg.toolCalls) {
685
+ total += this.estTokens(JSON.stringify(tc.arguments));
686
+ }
687
+ }
681
688
  }
682
689
  }
683
690
  if (extraMessages.length > 0) {
@@ -691,6 +698,7 @@ var SessionHandler = class _SessionHandler {
691
698
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
692
699
  saveIfNeeded() {
693
700
  if (this.sessions.current && this.sessions.current.messages.length > 0) {
701
+ autoTrimSessionIfNeeded(this.sessions.current);
694
702
  const id = this.sessions.current.id;
695
703
  this.sessions.save();
696
704
  this.unsavedSessions.delete(id);
@@ -748,9 +756,9 @@ var SessionHandler = class _SessionHandler {
748
756
  return;
749
757
  }
750
758
  const hasToolSupport = typeof provider.chatWithTools === "function";
751
- const toolDefs = hasToolSupport ? this.getFilteredToolDefs() : [];
759
+ const { toolDefs, mcpBudgetNote } = hasToolSupport ? this.getFilteredToolDefs() : { toolDefs: [], mcpBudgetNote: null };
752
760
  if (hasToolSupport && toolDefs.length > 0) {
753
- await this.handleChatWithTools(provider, session.messages, toolDefs);
761
+ await this.handleChatWithTools(provider, session.messages, toolDefs, mcpBudgetNote);
754
762
  } else {
755
763
  await this.handleChatSimple(provider, session.messages);
756
764
  }
@@ -815,7 +823,7 @@ var SessionHandler = class _SessionHandler {
815
823
  this.abortController = null;
816
824
  }
817
825
  }
818
- async handleChatWithTools(provider, messages, toolDefs) {
826
+ async handleChatWithTools(provider, messages, toolDefs, mcpBudgetNote) {
819
827
  const session = this.sessions.current;
820
828
  const { baseMessages: cleanMessages, toolHistory } = extractToolHistory(messages);
821
829
  const apiMessages = [...cleanMessages];
@@ -833,7 +841,9 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
833
841
  - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
834
842
  - Prioritize the most critical tasks first in case rounds run out.
835
843
  - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
836
- const systemPrompt = baseSystemPrompt + roundBudgetHint;
844
+ const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
845
+
846
+ ${mcpBudgetNote}` : "");
837
847
  const modelParams = this.getModelParams();
838
848
  const roundUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
839
849
  const supportsStreamingTools = typeof provider.chatWithToolsStream === "function";
@@ -1923,7 +1933,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1923
1933
  case "test": {
1924
1934
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1925
1935
  try {
1926
- const { executeTests } = await import("./run-tests-QGJHXL5Z.js");
1936
+ const { executeTests } = await import("./run-tests-ORVJAUJG.js");
1927
1937
  const argStr = args.join(" ").trim();
1928
1938
  let testArgs = {};
1929
1939
  if (argStr) {
@@ -2440,11 +2450,18 @@ Add .md files to create commands.` });
2440
2450
  sendSessionMessages() {
2441
2451
  const session = this.sessions.current;
2442
2452
  if (!session) return;
2443
- const messages = session.messages.map((m) => ({
2444
- role: m.role,
2445
- content: getContentText(m.content),
2446
- timestamp: m.timestamp?.toISOString()
2447
- }));
2453
+ const messages = session.messages.map((m) => {
2454
+ const out = {
2455
+ role: m.role,
2456
+ content: getContentText(m.content),
2457
+ timestamp: m.timestamp?.toISOString()
2458
+ };
2459
+ if (m.toolCalls) out.toolCalls = m.toolCalls;
2460
+ if (m.toolCallId) out.toolCallId = m.toolCallId;
2461
+ if (m.toolName) out.toolName = m.toolName;
2462
+ if (m.isError !== void 0) out.isError = m.isError;
2463
+ return out;
2464
+ });
2448
2465
  this.send({
2449
2466
  type: "session_messages",
2450
2467
  sessionId: session.id,
@@ -2504,16 +2521,26 @@ Add .md files to create commands.` });
2504
2521
  };
2505
2522
  }
2506
2523
  getFilteredToolDefs() {
2507
- let defs = this.toolRegistry.getDefinitions();
2508
2524
  if (this.planMode) {
2509
- defs = defs.filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name));
2510
- } else {
2511
- const skillFilter = this.skillManager?.getActiveToolFilter();
2512
- if (skillFilter) {
2513
- defs = defs.filter((t) => skillFilter.has(t.name));
2514
- }
2525
+ return {
2526
+ toolDefs: this.toolRegistry.getDefinitions().filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name)),
2527
+ mcpBudgetNote: null
2528
+ };
2529
+ }
2530
+ const skillFilter = this.skillManager?.getActiveToolFilter();
2531
+ if (skillFilter) {
2532
+ return {
2533
+ toolDefs: this.toolRegistry.getDefinitions().filter((t) => skillFilter.has(t.name)),
2534
+ mcpBudgetNote: null
2535
+ };
2536
+ }
2537
+ const contextWindow = this.getContextWindowSize();
2538
+ if (contextWindow > 0) {
2539
+ const toolBudget = Math.floor(contextWindow * 0.2);
2540
+ const { definitions, systemNote } = this.toolRegistry.getDefinitionsWithBudget(toolBudget);
2541
+ return { toolDefs: definitions, mcpBudgetNote: systemNote };
2515
2542
  }
2516
- return defs;
2543
+ return { toolDefs: this.toolRegistry.getDefinitions(), mcpBudgetNote: null };
2517
2544
  }
2518
2545
  /**
2519
2546
  * Find first matching context file in a directory.
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-C32FFHMY.js";
7
+ } from "./chunk-672OV76Z.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-2DWWB4KD.js";
11
+ } from "./chunk-LLI6COMK.js";
12
12
 
13
13
  // src/hub/task-orchestrator.ts
14
14
  import { createInterface } from "readline";
@@ -717,6 +717,49 @@ function escapeHtml(str) {
717
717
  return div.innerHTML;
718
718
  }
719
719
 
720
+ /**
721
+ * Create a static tool-call card for session history view.
722
+ * Groups an assistant tool-call message with its subsequent tool results.
723
+ * Unlike live tool cards, these have no timer — just the final state.
724
+ *
725
+ * @param {Object} assistantMsg - The assistant message with toolCalls array
726
+ * @param {Object[]} resultMsgs - Subsequent tool-result messages
727
+ */
728
+ function createHistoryToolCards(assistantMsg, resultMsgs) {
729
+ const toolCalls = assistantMsg.toolCalls || [];
730
+ for (let i = 0; i < toolCalls.length; i++) {
731
+ const tc = toolCalls[i];
732
+ const rm = resultMsgs[i]; // may be undefined if results are missing
733
+ const isError = rm ? rm.isError : false;
734
+ const resultContent = rm ? rm.content : '';
735
+
736
+ const el = document.createElement('details');
737
+ el.className = 'tool-card tool-border-safe my-1';
738
+
739
+ const statusIcon = rm ? (isError ? '✗' : '✓') : '?';
740
+ const statusClass = rm ? (isError ? 'text-error' : 'text-success') : 'opacity-50';
741
+ const badgeClass = isError ? 'badge-error' : 'badge-info';
742
+ const levelIcon = isError ? '⚠' : '⚙';
743
+
744
+ const argsHtml = tc.arguments ? formatToolArgs(tc.arguments) : '';
745
+ const truncResult = resultContent.length > 500
746
+ ? resultContent.slice(0, 500) + '...'
747
+ : resultContent;
748
+
749
+ el.innerHTML = `
750
+ <summary class="flex items-center gap-2 w-full cursor-pointer select-none py-1">
751
+ <span class="badge ${badgeClass} badge-sm gap-1">${levelIcon} ${escapeHtml(tc.name)}</span>
752
+ <span class="tool-result-badge text-xs ml-auto ${statusClass}">${statusIcon}</span>
753
+ </summary>
754
+ <div class="tool-details-body pt-1">
755
+ ${argsHtml ? `<div class="tool-args w-full">${argsHtml}</div>` : ''}
756
+ ${rm ? `<div class="tool-result-content mt-2 pt-2 border-t border-base-content/10 w-full ${isError ? 'text-error' : 'text-success'}">${statusIcon} ${escapeHtml(truncResult)}</div>` : ''}
757
+ </div>
758
+ `;
759
+ messagesEl.appendChild(el);
760
+ }
761
+ }
762
+
720
763
  function scrollToBottom() {
721
764
  requestAnimationFrame(() => {
722
765
  chatArea.scrollTop = chatArea.scrollHeight;
@@ -1074,6 +1117,56 @@ function updateBatchBar() {
1074
1117
  * write it into the tab's `messagesHtml` cache; the live DOM is left
1075
1118
  * untouched so the active tab's content never flashes wrong data.
1076
1119
  */
1120
+
1121
+ /**
1122
+ * Render a messages array into the live DOM (messagesEl).
1123
+ * Handles user, assistant (text), assistant (toolCalls), and tool result messages.
1124
+ * Groups consecutive assistant+toolCalls with subsequent tool results into cards.
1125
+ */
1126
+ function renderMessagesArray(messages) {
1127
+ let i = 0;
1128
+ while (i < messages.length) {
1129
+ const m = messages[i];
1130
+ if (m.role === 'user') {
1131
+ addUserMessage(m.content);
1132
+ i++;
1133
+ } else if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
1134
+ // Assistant message with tool calls — collect subsequent tool results
1135
+ const resultMsgs = [];
1136
+ let j = i + 1;
1137
+ while (j < messages.length && messages[j].role === 'tool') {
1138
+ resultMsgs.push(messages[j]);
1139
+ j++;
1140
+ }
1141
+ // If the assistant also had text content, render it first
1142
+ if (m.content && m.content.trim()) {
1143
+ const el = createAssistantMessage();
1144
+ renderMarkdown(el, m.content);
1145
+ }
1146
+ createHistoryToolCards(m, resultMsgs);
1147
+ i = j;
1148
+ } else if (m.role === 'tool') {
1149
+ // Orphan tool result (no preceding assistant+toolCalls) — render as info card
1150
+ const statusIcon = m.isError ? '✗' : '✓';
1151
+ const statusClass = m.isError ? 'text-error' : 'text-success';
1152
+ const el = document.createElement('div');
1153
+ el.className = `tool-card tool-border-safe my-1 p-2 ${statusClass}`;
1154
+ const toolLabel = m.toolName ? escapeHtml(m.toolName) : 'tool';
1155
+ const truncContent = m.content && m.content.length > 300
1156
+ ? m.content.slice(0, 300) + '...' : (m.content || '');
1157
+ el.innerHTML = `<span class="badge badge-info badge-sm">${toolLabel}</span> ${statusIcon} ${escapeHtml(truncContent)}`;
1158
+ messagesEl.appendChild(el);
1159
+ i++;
1160
+ } else if (m.role === 'assistant') {
1161
+ const el = createAssistantMessage();
1162
+ renderMarkdown(el, m.content);
1163
+ i++;
1164
+ } else {
1165
+ // system or unknown — skip
1166
+ i++;
1167
+ }
1168
+ }
1169
+ }
1077
1170
  function renderSessionMessages(msg) {
1078
1171
  // Back-compat: if called with a bare array (legacy), treat as active-tab apply
1079
1172
  const messages = Array.isArray(msg) ? msg : msg.messages;
@@ -1096,14 +1189,7 @@ function renderSessionMessages(msg) {
1096
1189
  // Active tab: paint directly into the live DOM (preserves any in-flight
1097
1190
  // streaming helpers that rely on messagesEl)
1098
1191
  messagesEl.innerHTML = '';
1099
- for (const m of messages) {
1100
- if (m.role === 'user') {
1101
- addUserMessage(m.content);
1102
- } else if (m.role === 'assistant') {
1103
- const el = createAssistantMessage();
1104
- renderMarkdown(el, m.content);
1105
- }
1106
- }
1192
+ renderMessagesArray(messages);
1107
1193
  scrollToBottom();
1108
1194
  // Snapshot into cache so subsequent tab-snapshots see the latest content
1109
1195
  sessionTabs[targetIdx].messagesHtml = messagesEl.innerHTML;
@@ -1136,14 +1222,7 @@ function buildMessagesHtmlOffDom(messages) {
1136
1222
  currentAssistantContent = '';
1137
1223
  currentThinkingEl = null;
1138
1224
  currentThinkingContent = '';
1139
- for (const m of messages) {
1140
- if (m.role === 'user') {
1141
- addUserMessage(m.content);
1142
- } else if (m.role === 'assistant') {
1143
- const el = createAssistantMessage();
1144
- renderMarkdown(el, m.content);
1145
- }
1146
- }
1225
+ renderMessagesArray(messages);
1147
1226
  return messagesEl.innerHTML;
1148
1227
  } finally {
1149
1228
  messagesEl.innerHTML = savedHtml;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.60",
3
+ "version": "0.4.62",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",