jinzd-ai-cli 0.4.61 → 0.4.63

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-GA74LZ62.js";
13
+ } from "./chunk-MLEM56CR.js";
14
14
 
15
15
  // src/tools/builtin/bash.ts
16
16
  import { execSync } from "child_process";
@@ -1365,14 +1365,19 @@ var ToolExecutor = class {
1365
1365
  }
1366
1366
  }
1367
1367
  async executeAll(calls) {
1368
- const safeCalls = [];
1368
+ const safeParallel = [];
1369
+ const safeBash = [];
1369
1370
  const fileWriteCalls = [];
1370
1371
  const otherCalls = [];
1371
1372
  for (let i = 0; i < calls.length; i++) {
1372
1373
  const call = calls[i];
1373
1374
  const level = getDangerLevel(call.name, call.arguments);
1374
1375
  if (level === "safe") {
1375
- safeCalls.push({ idx: i, call });
1376
+ if (call.name === "bash") {
1377
+ safeBash.push({ idx: i, call });
1378
+ } else {
1379
+ safeParallel.push({ idx: i, call });
1380
+ }
1376
1381
  } else if (isFileWriteTool(call.name) && level === "write") {
1377
1382
  fileWriteCalls.push({ idx: i, call });
1378
1383
  } else {
@@ -1380,11 +1385,20 @@ var ToolExecutor = class {
1380
1385
  }
1381
1386
  }
1382
1387
  const results = new Array(calls.length);
1383
- await Promise.all(
1384
- safeCalls.map(async ({ idx, call }) => {
1388
+ const t0 = Date.now();
1389
+ const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(async ({ idx, call }) => {
1390
+ results[idx] = await this.execute(call);
1391
+ })) : Promise.resolve();
1392
+ const bashPhase = (async () => {
1393
+ for (const { idx, call } of safeBash) {
1385
1394
  results[idx] = await this.execute(call);
1386
- })
1387
- );
1395
+ }
1396
+ })();
1397
+ await Promise.all([parallelPhase, bashPhase]);
1398
+ if (safeParallel.length >= 2) {
1399
+ const elapsed = Date.now() - t0;
1400
+ console.log(theme.dim(` \u26A1 ${safeParallel.length} tools executed in parallel (${elapsed}ms)`));
1401
+ }
1388
1402
  if (fileWriteCalls.length === 1) {
1389
1403
  const { idx, call } = fileWriteCalls[0];
1390
1404
  results[idx] = await this.execute(call);
@@ -1418,31 +1432,41 @@ var ToolExecutor = class {
1418
1432
  if (this.sessionAutoApprove) {
1419
1433
  console.log(theme.warning(" \u26A1 All auto-approved (session /yolo mode)"));
1420
1434
  }
1421
- const results = [];
1435
+ const results = new Array(calls.length);
1436
+ const approvedIndices = [];
1422
1437
  for (let i = 0; i < calls.length; i++) {
1423
- const call = calls[i];
1424
1438
  const approved = decision === "all" || decision !== "none" && decision.has(i + 1);
1425
1439
  if (approved) {
1440
+ approvedIndices.push(i);
1441
+ } else {
1442
+ console.log(theme.dim(` [${i + 1}] `) + theme.dim("rejected"));
1443
+ results[i] = { callId: calls[i].id, content: `[User rejected] The user rejected this ${calls[i].name} operation. Do not retry without asking.`, isError: true };
1444
+ }
1445
+ }
1446
+ const t0 = Date.now();
1447
+ await Promise.all(
1448
+ approvedIndices.map(async (i) => {
1449
+ const call = calls[i];
1426
1450
  const tool = this.registry.get(call.name);
1427
1451
  if (!tool) {
1428
- results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
1429
- continue;
1452
+ results[i] = { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
1453
+ return;
1430
1454
  }
1431
1455
  try {
1432
1456
  const rawContent = await tool.execute(call.arguments);
1433
1457
  const content = truncateOutput(rawContent, call.name);
1434
1458
  const wasTruncated = content !== rawContent;
1435
1459
  this.printToolResult(call.name, rawContent, false, wasTruncated);
1436
- results.push({ callId: call.id, content, isError: false });
1460
+ results[i] = { callId: call.id, content, isError: false };
1437
1461
  } catch (err) {
1438
1462
  const message = err instanceof Error ? err.message : String(err);
1439
1463
  this.printToolResult(call.name, message, true, false);
1440
- results.push({ callId: call.id, content: message, isError: true });
1464
+ results[i] = { callId: call.id, content: message, isError: true };
1441
1465
  }
1442
- } else {
1443
- console.log(theme.dim(` [${i + 1}] `) + theme.dim("rejected"));
1444
- results.push({ callId: call.id, content: `[User rejected] The user rejected this ${call.name} operation. Do not retry without asking.`, isError: true });
1445
- }
1466
+ })
1467
+ );
1468
+ if (approvedIndices.length >= 2) {
1469
+ console.log(theme.dim(` \u26A1 ${approvedIndices.length} file writes executed in parallel (${Date.now() - t0}ms)`));
1446
1470
  }
1447
1471
  return results;
1448
1472
  }
@@ -4062,6 +4086,28 @@ var notebookEditTool = {
4062
4086
  }
4063
4087
  };
4064
4088
 
4089
+ // src/core/token-estimator.ts
4090
+ var CJK_REGEX = /[\u2E80-\u9FFF\uA000-\uA4FF\uAC00-\uD7FF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
4091
+ function estimateTokens(text) {
4092
+ if (!text) return 0;
4093
+ const cjkMatches = text.match(CJK_REGEX);
4094
+ const cjkCount = cjkMatches ? cjkMatches.length : 0;
4095
+ const nonCjkCount = text.length - cjkCount;
4096
+ const tokens = cjkCount * 1.5 + nonCjkCount * 0.25;
4097
+ return Math.ceil(tokens) + 4;
4098
+ }
4099
+ function estimateToolDefinitionTokens(def) {
4100
+ let charCount = def.name.length + def.description.length;
4101
+ for (const [key, param] of Object.entries(def.parameters)) {
4102
+ charCount += key.length + param.type.length + param.description.length;
4103
+ if (param.enum) {
4104
+ charCount += param.enum.join(" ").length;
4105
+ }
4106
+ }
4107
+ const structuralOverhead = 40 + Object.keys(def.parameters).length * 30;
4108
+ return Math.ceil((charCount + structuralOverhead) * 0.25) + 10;
4109
+ }
4110
+
4065
4111
  // src/tools/registry.ts
4066
4112
  import { pathToFileURL } from "url";
4067
4113
  import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
@@ -4110,6 +4156,74 @@ var ToolRegistry = class {
4110
4156
  getDefinitions() {
4111
4157
  return [...this.tools.values()].map((t) => t.definition);
4112
4158
  }
4159
+ /**
4160
+ * Return tool definitions within a token budget, trimming MCP tools if needed.
4161
+ *
4162
+ * Strategy: always include all builtin + plugin tools. If total tokens exceed
4163
+ * the budget, keep only MCP tools that were used in this session (by name),
4164
+ * trimming the rest. Returns { definitions, trimmedCount, systemNote }.
4165
+ *
4166
+ * @param tokenBudget Max tokens for tool definitions (typically 20% of context window)
4167
+ * @param usedToolNames Names of MCP tools already called this session (always kept)
4168
+ */
4169
+ getDefinitionsWithBudget(tokenBudget, usedToolNames) {
4170
+ const allDefs = this.getDefinitions();
4171
+ let totalTokens = 0;
4172
+ for (const def of allDefs) {
4173
+ totalTokens += estimateToolDefinitionTokens(def);
4174
+ }
4175
+ if (totalTokens <= tokenBudget) {
4176
+ return { definitions: allDefs, trimmedCount: 0, systemNote: null };
4177
+ }
4178
+ const builtinDefs = [];
4179
+ const mcpDefs = [];
4180
+ for (const def of allDefs) {
4181
+ if (this.mcpToolNames.has(def.name)) {
4182
+ mcpDefs.push(def);
4183
+ } else {
4184
+ builtinDefs.push(def);
4185
+ }
4186
+ }
4187
+ let budget = tokenBudget;
4188
+ for (const def of builtinDefs) {
4189
+ budget -= estimateToolDefinitionTokens(def);
4190
+ }
4191
+ const kept = [...builtinDefs];
4192
+ const used = usedToolNames ?? /* @__PURE__ */ new Set();
4193
+ const remaining = [];
4194
+ for (const def of mcpDefs) {
4195
+ if (used.has(def.name)) {
4196
+ const cost = estimateToolDefinitionTokens(def);
4197
+ budget -= cost;
4198
+ kept.push(def);
4199
+ } else {
4200
+ remaining.push(def);
4201
+ }
4202
+ }
4203
+ let trimmed = 0;
4204
+ for (const def of remaining) {
4205
+ const cost = estimateToolDefinitionTokens(def);
4206
+ if (budget >= cost) {
4207
+ budget -= cost;
4208
+ kept.push(def);
4209
+ } else {
4210
+ trimmed++;
4211
+ }
4212
+ }
4213
+ let systemNote = null;
4214
+ if (trimmed > 0) {
4215
+ const trimmedServers = /* @__PURE__ */ new Set();
4216
+ const keptNames = new Set(kept.map((d) => d.name));
4217
+ for (const def of mcpDefs) {
4218
+ if (!keptNames.has(def.name)) {
4219
+ const parts = def.name.split("__");
4220
+ if (parts.length >= 2) trimmedServers.add(parts[1]);
4221
+ }
4222
+ }
4223
+ 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.`;
4224
+ }
4225
+ return { definitions: kept, trimmedCount: trimmed, systemNote };
4226
+ }
4113
4227
  listAll() {
4114
4228
  return [...this.tools.values()];
4115
4229
  }
@@ -4229,5 +4343,6 @@ export {
4229
4343
  askUserContext,
4230
4344
  googleSearchContext,
4231
4345
  spawnAgentContext,
4346
+ estimateTokens,
4232
4347
  ToolRegistry
4233
4348
  };
@@ -8,7 +8,7 @@ import {
8
8
  RateLimitError,
9
9
  schemaToJsonSchema,
10
10
  truncateForPersist
11
- } from "./chunk-2ZCD5F4X.js";
11
+ } from "./chunk-E45EGVSY.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-GA74LZ62.js";
24
+ } from "./chunk-MLEM56CR.js";
25
25
 
26
26
  // src/config/config-manager.ts
27
27
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -182,6 +182,10 @@ var ConfigSchema = z.object({
182
182
  // 实际上限还会受模型 contextWindow 动态约束(取 contextWindow/4 作为下限)。
183
183
  // 设置为 0 或未配置时使用默认值;不建议设为小于 12_000 或大于模型 contextWindow/2。
184
184
  maxToolOutputChars: z.number().int().min(0).default(5e5),
185
+ // 月度成本预算(USD)。
186
+ // 设置后,每次 AI 回复后会跟踪成本,接近或超过预算时在 /status 和 /cost 中显示警告。
187
+ // 默认 0 = 不限制。例:50 表示每月最多花 $50。
188
+ monthlyBudget: z.number().min(0).default(0),
185
189
  // 插件加载开关(安全控制)
186
190
  // 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
187
191
  // 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
@@ -2462,14 +2466,25 @@ var Session = class _Session {
2462
2466
  /**
2463
2467
  * 上下文压缩:用摘要消息替换旧消息,保留最近 keepLast 条。
2464
2468
  *
2469
+ * Tool-history-aware: if the cut point lands inside a tool round
2470
+ * (assistant+toolCalls followed by tool results), expand to keep the
2471
+ * entire round intact. This prevents orphaned tool results.
2472
+ *
2465
2473
  * 压缩后消息结构:
2466
- * [summaryMsg(user), ackMsg(assistant), ...最近 keepLast 条原始消息]
2474
+ * [summaryMsg(user), ackMsg(assistant), ...最近 N 条原始消息]
2467
2475
  *
2468
2476
  * @returns 被删除的消息条数
2469
2477
  */
2470
2478
  compact(summaryMsg, ackMsg, keepLast) {
2471
- const preserved = this.messages.slice(-keepLast);
2472
- const removedCount = this.messages.length - preserved.length;
2479
+ let cutIndex = this.messages.length - keepLast;
2480
+ if (cutIndex <= 0) {
2481
+ return 0;
2482
+ }
2483
+ while (cutIndex > 0 && this.messages[cutIndex]?.role === "tool") {
2484
+ cutIndex--;
2485
+ }
2486
+ const preserved = this.messages.slice(cutIndex);
2487
+ const removedCount = cutIndex;
2473
2488
  this.messages = [summaryMsg, ackMsg, ...preserved];
2474
2489
  this.updated = /* @__PURE__ */ new Date();
2475
2490
  return removedCount;
@@ -3687,6 +3702,7 @@ function formatCost(amount) {
3687
3702
  }
3688
3703
 
3689
3704
  // src/session/tool-history.ts
3705
+ var SESSION_SIZE_LIMIT = 2 * 1024 * 1024;
3690
3706
  function persistToolRound(session, toolCalls, toolResults, opts) {
3691
3707
  session.addMessage({
3692
3708
  role: "assistant",
@@ -3757,16 +3773,51 @@ function rebuildExtraMessages(provider, toolHistory) {
3757
3773
  }
3758
3774
  return result;
3759
3775
  }
3760
-
3761
- // src/core/token-estimator.ts
3762
- var CJK_REGEX = /[\u2E80-\u9FFF\uA000-\uA4FF\uAC00-\uD7FF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
3763
- function estimateTokens(text) {
3764
- if (!text) return 0;
3765
- const cjkMatches = text.match(CJK_REGEX);
3766
- const cjkCount = cjkMatches ? cjkMatches.length : 0;
3767
- const nonCjkCount = text.length - cjkCount;
3768
- const tokens = cjkCount * 1.5 + nonCjkCount * 0.25;
3769
- return Math.ceil(tokens) + 4;
3776
+ function trimOldToolOutput(messages, keepRecentRounds = 10) {
3777
+ const roundStarts = [];
3778
+ for (let i = 0; i < messages.length; i++) {
3779
+ if (messages[i].role === "assistant" && messages[i].toolCalls?.length) {
3780
+ roundStarts.push(i);
3781
+ }
3782
+ }
3783
+ if (roundStarts.length <= keepRecentRounds) return 0;
3784
+ const cutoffRoundIdx = roundStarts.length - keepRecentRounds;
3785
+ let trimmed = 0;
3786
+ for (let r = 0; r < cutoffRoundIdx; r++) {
3787
+ const start = roundStarts[r];
3788
+ const end = r + 1 < roundStarts.length ? roundStarts[r + 1] : messages.length;
3789
+ const assistantMsg = messages[start];
3790
+ if (typeof assistantMsg.content === "string" && assistantMsg.content.length > 200) {
3791
+ assistantMsg.content = assistantMsg.content.slice(0, 100) + "\u2026 [trimmed for size]";
3792
+ trimmed++;
3793
+ }
3794
+ for (let j = start + 1; j < end; j++) {
3795
+ const msg = messages[j];
3796
+ if (msg.role === "tool") {
3797
+ const status = msg.isError ? "\u2717 error" : "\u2713 ok";
3798
+ const name = msg.toolName ?? "unknown";
3799
+ const currentContent = typeof msg.content === "string" ? msg.content : "";
3800
+ if (currentContent.length > 200) {
3801
+ msg.content = `[${name}: ${status}] (output trimmed for size \u2014 ${currentContent.length} chars)`;
3802
+ trimmed++;
3803
+ }
3804
+ }
3805
+ }
3806
+ }
3807
+ return trimmed;
3808
+ }
3809
+ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
3810
+ const json = JSON.stringify(session.toJSON());
3811
+ if (json.length <= sizeLimit) return false;
3812
+ let keepRecent = 10;
3813
+ while (keepRecent >= 2) {
3814
+ const trimmed = trimOldToolOutput(session.messages, keepRecent);
3815
+ if (trimmed === 0) break;
3816
+ const newSize = JSON.stringify(session.toJSON()).length;
3817
+ if (newSize <= sizeLimit) return true;
3818
+ keepRecent = Math.max(2, Math.floor(keepRecent / 2));
3819
+ }
3820
+ return true;
3770
3821
  }
3771
3822
 
3772
3823
  // src/repl/dev-state.ts
@@ -3882,7 +3933,7 @@ export {
3882
3933
  persistToolRound,
3883
3934
  extractToolHistory,
3884
3935
  rebuildExtraMessages,
3885
- estimateTokens,
3936
+ autoTrimSessionIfNeeded,
3886
3937
  SNAPSHOT_PROMPT,
3887
3938
  sessionHasMeaningfulContent,
3888
3939
  saveDevState,
@@ -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.61";
9
+ var VERSION = "0.4.63";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -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.61";
11
+ var VERSION = "0.4.63";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  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-W66JWWKF.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-LNJWNTSJ.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,12 +8,12 @@ import {
8
8
  SessionManager,
9
9
  SkillManager,
10
10
  TOOL_CALL_REMINDER,
11
+ autoTrimSessionIfNeeded,
11
12
  buildPhantomCorrectionMessage,
12
13
  buildWriteRoundReminder,
13
14
  clearDevState,
14
15
  computeCost,
15
16
  detectsHallucinatedFileOp,
16
- estimateTokens,
17
17
  extractToolHistory,
18
18
  extractWrittenFilePaths,
19
19
  findPhantomClaims,
@@ -31,11 +31,12 @@ import {
31
31
  saveDevState,
32
32
  sessionHasMeaningfulContent,
33
33
  setupProxy
34
- } from "./chunk-2WGVM2J2.js";
34
+ } from "./chunk-EYRPTNVI.js";
35
35
  import {
36
36
  ToolExecutor,
37
37
  ToolRegistry,
38
38
  askUserContext,
39
+ estimateTokens,
39
40
  googleSearchContext,
40
41
  initTheme,
41
42
  lastResponseStore,
@@ -45,7 +46,7 @@ import {
45
46
  spawnAgentContext,
46
47
  theme,
47
48
  undoStack
48
- } from "./chunk-2ZCD5F4X.js";
49
+ } from "./chunk-E45EGVSY.js";
49
50
  import {
50
51
  fileCheckpoints
51
52
  } from "./chunk-4BKXL7SM.js";
@@ -70,15 +71,15 @@ import {
70
71
  SKILLS_DIR_NAME,
71
72
  VERSION,
72
73
  buildUserIdentityPrompt
73
- } from "./chunk-GA74LZ62.js";
74
+ } from "./chunk-MLEM56CR.js";
74
75
 
75
76
  // src/index.ts
76
77
  import { program } from "commander";
77
78
 
78
79
  // src/repl/repl.ts
79
80
  import * as readline from "readline";
80
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
81
- import { join as join4, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
81
+ import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
82
+ import { join as join5, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
82
83
  import chalk4 from "chalk";
83
84
 
84
85
  // src/repl/renderer.ts
@@ -1842,7 +1843,7 @@ ${hint}` : "")
1842
1843
  {
1843
1844
  name: "cost",
1844
1845
  description: "Show session token usage, prompt-cache hits, and USD cost",
1845
- usage: "/cost [reset]",
1846
+ usage: "/cost [reset | history]",
1846
1847
  execute(args, ctx) {
1847
1848
  const sub = args[0]?.toLowerCase();
1848
1849
  if (sub === "reset") {
@@ -1850,6 +1851,28 @@ ${hint}` : "")
1850
1851
  ctx.renderer.printSuccess("Session token counters reset.");
1851
1852
  return;
1852
1853
  }
1854
+ if (sub === "history" || sub === "h") {
1855
+ const tracker = ctx.getCostTracker();
1856
+ const budget = ctx.config.get("monthlyBudget");
1857
+ console.log();
1858
+ console.log(theme.heading(" \u{1F4B0} Cross-Session Cost Dashboard"));
1859
+ console.log(theme.dim(" " + "\u2500".repeat(48)));
1860
+ const summary = tracker.formatSummary(budget);
1861
+ for (const line of summary.split("\n")) {
1862
+ console.log(theme.dim(" ") + chalk2.white(line));
1863
+ }
1864
+ console.log(theme.dim(" " + "\u2500".repeat(48)));
1865
+ const warning = tracker.checkBudget(budget);
1866
+ if (warning) {
1867
+ console.log(theme.warning(` ${warning}`));
1868
+ } else if (budget && budget > 0) {
1869
+ console.log(theme.success(" \u2713 Within monthly budget"));
1870
+ } else {
1871
+ console.log(theme.dim(' Tip: set "monthlyBudget" in config (e.g., 50 for $50/month)'));
1872
+ }
1873
+ console.log();
1874
+ return;
1875
+ }
1853
1876
  const session = ctx.sessions.current;
1854
1877
  const usage = session?.tokenUsage ?? {
1855
1878
  inputTokens: 0,
@@ -2165,7 +2188,7 @@ ${hint}` : "")
2165
2188
  usage: "/test [command|filter]",
2166
2189
  async execute(args, ctx) {
2167
2190
  try {
2168
- const { executeTests } = await import("./run-tests-CT4PRGGP.js");
2191
+ const { executeTests } = await import("./run-tests-KRGPFYPF.js");
2169
2192
  const argStr = args.join(" ").trim();
2170
2193
  let testArgs = {};
2171
2194
  if (argStr) {
@@ -3229,6 +3252,125 @@ var CustomCommandManager = class {
3229
3252
  }
3230
3253
  };
3231
3254
 
3255
+ // src/core/cost-tracker.ts
3256
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
3257
+ import { join as join4 } from "path";
3258
+ var CostTracker = class {
3259
+ filePath;
3260
+ records = [];
3261
+ dirty = false;
3262
+ constructor(configDir) {
3263
+ this.filePath = join4(configDir, "cost-history.json");
3264
+ this.load();
3265
+ }
3266
+ load() {
3267
+ try {
3268
+ if (existsSync4(this.filePath)) {
3269
+ const data = JSON.parse(readFileSync3(this.filePath, "utf-8"));
3270
+ if (data.version === 1 && Array.isArray(data.records)) {
3271
+ this.records = data.records;
3272
+ }
3273
+ }
3274
+ } catch {
3275
+ this.records = [];
3276
+ }
3277
+ }
3278
+ /** Save to disk (atomic write). */
3279
+ save() {
3280
+ if (!this.dirty) return;
3281
+ const data = { version: 1, records: this.records };
3282
+ const tmp = this.filePath + ".tmp";
3283
+ writeFileSync2(tmp, JSON.stringify(data, null, 2), "utf-8");
3284
+ renameSync(tmp, this.filePath);
3285
+ this.dirty = false;
3286
+ }
3287
+ /**
3288
+ * Record cost from a completed session/interaction.
3289
+ */
3290
+ addCost(provider, model, usage) {
3291
+ const cost = computeCost(provider, model, usage);
3292
+ if (cost === null || cost === 0) return;
3293
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3294
+ let record = this.records.find((r) => r.date === today);
3295
+ if (!record) {
3296
+ record = { date: today, cost: 0, sessions: 0, inputTokens: 0, outputTokens: 0 };
3297
+ this.records.push(record);
3298
+ }
3299
+ record.cost += cost;
3300
+ record.sessions += 1;
3301
+ record.inputTokens += usage.inputTokens;
3302
+ record.outputTokens += usage.outputTokens;
3303
+ this.dirty = true;
3304
+ if (this.records.length > 90) {
3305
+ this.records = this.records.slice(-90);
3306
+ }
3307
+ }
3308
+ /** Get total cost for a given month ("2026-04"). */
3309
+ getMonthlyCost(yearMonth) {
3310
+ const prefix = yearMonth ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
3311
+ return this.records.filter((r) => r.date.startsWith(prefix)).reduce((sum, r) => sum + r.cost, 0);
3312
+ }
3313
+ /** Get today's cost. */
3314
+ getTodayCost() {
3315
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3316
+ return this.records.find((r) => r.date === today)?.cost ?? 0;
3317
+ }
3318
+ /** Get total cost for last N days. */
3319
+ getRecentCost(days) {
3320
+ const cutoff = /* @__PURE__ */ new Date();
3321
+ cutoff.setDate(cutoff.getDate() - days);
3322
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
3323
+ return this.records.filter((r) => r.date >= cutoffStr).reduce((sum, r) => sum + r.cost, 0);
3324
+ }
3325
+ /** Get all records for display. */
3326
+ getRecords() {
3327
+ return [...this.records];
3328
+ }
3329
+ /**
3330
+ * Check if monthly cost exceeds budget, return warning message or null.
3331
+ */
3332
+ checkBudget(monthlyBudget) {
3333
+ if (!monthlyBudget || monthlyBudget <= 0) return null;
3334
+ const monthlyCost = this.getMonthlyCost();
3335
+ const ratio = monthlyCost / monthlyBudget;
3336
+ if (ratio >= 1) {
3337
+ return `\u{1F6A8} Monthly budget exceeded: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
3338
+ }
3339
+ if (ratio >= 0.8) {
3340
+ return `\u26A0 Monthly budget warning: ${formatCost(monthlyCost)} / ${formatCost(monthlyBudget)} (${Math.round(ratio * 100)}%)`;
3341
+ }
3342
+ return null;
3343
+ }
3344
+ /**
3345
+ * Format a cost summary for display.
3346
+ */
3347
+ formatSummary(monthlyBudget) {
3348
+ const today = this.getTodayCost();
3349
+ const monthly = this.getMonthlyCost();
3350
+ const yearMonth = (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
3351
+ const lines = [
3352
+ `Today: ${formatCost(today)}`,
3353
+ `This month: ${formatCost(monthly)} (${yearMonth})`
3354
+ ];
3355
+ if (monthlyBudget && monthlyBudget > 0) {
3356
+ const ratio = monthly / monthlyBudget;
3357
+ const bar = "\u2588".repeat(Math.min(20, Math.round(ratio * 20))) + "\u2591".repeat(Math.max(0, 20 - Math.round(ratio * 20)));
3358
+ lines.push(`Budget: ${formatCost(monthlyBudget)} [${bar}] ${Math.round(ratio * 100)}%`);
3359
+ }
3360
+ const last7 = [];
3361
+ for (let i = 6; i >= 0; i--) {
3362
+ const d = /* @__PURE__ */ new Date();
3363
+ d.setDate(d.getDate() - i);
3364
+ const dateStr = d.toISOString().slice(0, 10);
3365
+ const record = this.records.find((r) => r.date === dateStr);
3366
+ const dayLabel = dateStr.slice(5);
3367
+ last7.push(`${dayLabel}: ${record ? formatCost(record.cost) : "$0.00"}`);
3368
+ }
3369
+ lines.push(`Last 7 days: ${last7.join(" ")}`);
3370
+ return lines.join("\n");
3371
+ }
3372
+ };
3373
+
3232
3374
  // src/repl/notify.ts
3233
3375
  import { spawn } from "child_process";
3234
3376
  import { platform as platform2 } from "os";
@@ -3302,7 +3444,7 @@ function parseAtReferences(input2, cwd) {
3302
3444
  const absPath = resolve2(cwd, rawPath);
3303
3445
  const ext = extname2(rawPath).toLowerCase();
3304
3446
  const mime = IMAGE_MIME[ext];
3305
- if (!existsSync4(absPath)) {
3447
+ if (!existsSync5(absPath)) {
3306
3448
  refs.push({ path: rawPath, type: "notfound" });
3307
3449
  continue;
3308
3450
  }
@@ -3312,7 +3454,7 @@ function parseAtReferences(input2, cwd) {
3312
3454
  refs.push({ path: rawPath, type: "toolarge" });
3313
3455
  continue;
3314
3456
  }
3315
- const data = readFileSync3(absPath).toString("base64");
3457
+ const data = readFileSync4(absPath).toString("base64");
3316
3458
  imageParts.push({
3317
3459
  type: "image_url",
3318
3460
  image_url: { url: `data:${mime};base64,${data}` }
@@ -3320,7 +3462,7 @@ function parseAtReferences(input2, cwd) {
3320
3462
  refs.push({ path: rawPath, type: "image" });
3321
3463
  textBody = textBody.replace(match[0], "").trim();
3322
3464
  } else {
3323
- const content = readFileSync3(absPath, "utf-8");
3465
+ const content = readFileSync4(absPath, "utf-8");
3324
3466
  const inlined = `
3325
3467
 
3326
3468
  [File: ${rawPath}]
@@ -3389,6 +3531,7 @@ var Repl = class {
3389
3531
  if (options?.blockedTools) this.blockedTools = options.blockedTools;
3390
3532
  if (options?.resumeSessionId) this.resumeSessionId = options.resumeSessionId;
3391
3533
  if (options?.maxToolRoundsOverride !== void 0) this.maxToolRoundsOverride = options.maxToolRoundsOverride;
3534
+ this.costTracker = new CostTracker(this.config.getConfigDir());
3392
3535
  }
3393
3536
  rl;
3394
3537
  currentProvider;
@@ -3404,12 +3547,13 @@ var Repl = class {
3404
3547
  contextLayers = [];
3405
3548
  /** 本次会话累计 token 用量 */
3406
3549
  sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
3407
- /** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage. */
3550
+ /** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage + cost tracker. */
3408
3551
  addSessionUsage(u) {
3409
3552
  this.sessionTokenUsage.inputTokens += u.inputTokens;
3410
3553
  this.sessionTokenUsage.outputTokens += u.outputTokens;
3411
3554
  this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
3412
3555
  this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
3556
+ this.costTracker.addCost(this.currentProvider, this.currentModel, u);
3413
3557
  }
3414
3558
  /** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
3415
3559
  gitBranch = null;
@@ -3451,6 +3595,8 @@ var Repl = class {
3451
3595
  selecting = false;
3452
3596
  /** CLI --max-tool-rounds 覆盖值;未指定时从 config.maxToolRounds 读取 */
3453
3597
  maxToolRoundsOverride;
3598
+ /** 跨 session 成本追踪器 */
3599
+ costTracker;
3454
3600
  // ── /add-dir 目录上下文支持 ────────────────────────────────────────────────
3455
3601
  /**
3456
3602
  * 扫描目录内容,返回格式化字符串(含目录树 + 关键文件内容)。
@@ -3530,7 +3676,7 @@ var Repl = class {
3530
3676
  const filtered = entries.filter((e) => !SKIP_DIRS_SET.has(e));
3531
3677
  for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
3532
3678
  const name = filtered[i];
3533
- const fullPath = join4(dir, name);
3679
+ const fullPath = join5(dir, name);
3534
3680
  const isLast = i === filtered.length - 1;
3535
3681
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
3536
3682
  let isDir;
@@ -3564,7 +3710,7 @@ ${treeLines.join("\n")}`
3564
3710
  for (const name of entries) {
3565
3711
  if (totalChars >= MAX_TOTAL_CHARS) break;
3566
3712
  if (SKIP_DIRS_SET.has(name)) continue;
3567
- const fullPath = join4(dir, name);
3713
+ const fullPath = join5(dir, name);
3568
3714
  let st;
3569
3715
  try {
3570
3716
  st = statSync3(fullPath);
@@ -3579,7 +3725,7 @@ ${treeLines.join("\n")}`
3579
3725
  if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
3580
3726
  if (st.size > MAX_FILE_CHARS * 3) continue;
3581
3727
  try {
3582
- let content = readFileSync3(fullPath, "utf-8");
3728
+ let content = readFileSync4(fullPath, "utf-8");
3583
3729
  if (content.length > MAX_FILE_CHARS) {
3584
3730
  content = content.slice(0, MAX_FILE_CHARS) + `
3585
3731
  ... (truncated, ${content.length} chars total)`;
@@ -3609,7 +3755,7 @@ ${content}
3609
3755
  */
3610
3756
  addExtraContextDir(dirPath) {
3611
3757
  const absPath = resolve2(dirPath);
3612
- if (!existsSync4(absPath)) {
3758
+ if (!existsSync5(absPath)) {
3613
3759
  return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
3614
3760
  }
3615
3761
  let isDir;
@@ -3643,9 +3789,9 @@ ${content}
3643
3789
  */
3644
3790
  findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
3645
3791
  for (const candidate of candidates) {
3646
- const fullPath = join4(dir, candidate);
3647
- if (existsSync4(fullPath)) {
3648
- const content = readFileSync3(fullPath, "utf-8").trim();
3792
+ const fullPath = join5(dir, candidate);
3793
+ if (existsSync5(fullPath)) {
3794
+ const content = readFileSync4(fullPath, "utf-8").trim();
3649
3795
  if (content) return { filePath: fullPath, content };
3650
3796
  }
3651
3797
  }
@@ -3673,10 +3819,10 @@ ${content}
3673
3819
  const cwd = process.cwd();
3674
3820
  const gitRoot = getGitRoot(cwd);
3675
3821
  const projectRoot = gitRoot ?? cwd;
3676
- const mcpPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
3677
- if (!existsSync4(mcpPath)) return null;
3822
+ const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
3823
+ if (!existsSync5(mcpPath)) return null;
3678
3824
  try {
3679
- const raw = JSON.parse(readFileSync3(mcpPath, "utf-8"));
3825
+ const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
3680
3826
  const servers = raw?.mcpServers;
3681
3827
  if (!servers || typeof servers !== "object") {
3682
3828
  process.stderr.write(
@@ -3722,8 +3868,8 @@ ${content}
3722
3868
  );
3723
3869
  return { layers: [], mergedContent: "" };
3724
3870
  }
3725
- if (existsSync4(fullPath)) {
3726
- const content = readFileSync3(fullPath, "utf-8").trim();
3871
+ if (existsSync5(fullPath)) {
3872
+ const content = readFileSync4(fullPath, "utf-8").trim();
3727
3873
  if (content) {
3728
3874
  const layer = {
3729
3875
  level: "project",
@@ -3780,9 +3926,9 @@ ${content}
3780
3926
  * 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
3781
3927
  */
3782
3928
  loadMemoryContent() {
3783
- const memoryPath = join4(this.config.getConfigDir(), MEMORY_FILE_NAME);
3784
- if (!existsSync4(memoryPath)) return null;
3785
- let content = readFileSync3(memoryPath, "utf-8").trim();
3929
+ const memoryPath = join5(this.config.getConfigDir(), MEMORY_FILE_NAME);
3930
+ if (!existsSync5(memoryPath)) return null;
3931
+ let content = readFileSync4(memoryPath, "utf-8").trim();
3786
3932
  if (!content) return null;
3787
3933
  if (content.length > MEMORY_MAX_CHARS) {
3788
3934
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -3911,6 +4057,11 @@ ${skillContent}`);
3911
4057
  "4. Current status and tasks not yet completed",
3912
4058
  "5. Key details to remember (file paths, config values, error messages, variable names, etc.)",
3913
4059
  "",
4060
+ "If the conversation contains tool call rounds (assistant calling tools like read_file, write_file, bash, etc.), ",
4061
+ 'summarize each round as a single line: "- [tool_name] target \u2192 result/status". ',
4062
+ 'Do NOT include raw tool output. Group consecutive similar operations (e.g., "read 5 files in src/") ',
4063
+ "instead of listing each one individually.",
4064
+ "",
3914
4065
  "Requirements: Be well-organized so that subsequent conversation can seamlessly continue from this summary."
3915
4066
  ];
3916
4067
  if (instruction) summaryPromptLines.push("", `Additional requirement: ${instruction}`);
@@ -4088,6 +4239,16 @@ Session '${this.resumeSessionId}' not found.
4088
4239
  process.stdout.write(
4089
4240
  theme.dim(` \u{1F4C2} Resumed session: ${session.id.slice(0, 8)} `) + theme.dim(`(${session.messages.length} messages`) + (session.title ? theme.dim(`, "${session.title}"`) : "") + theme.dim(")\n")
4090
4241
  );
4242
+ const msgs = session.messages;
4243
+ if (msgs.length > 0) {
4244
+ const lastMsg = msgs[msgs.length - 1];
4245
+ const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
4246
+ if (isIncomplete) {
4247
+ process.stdout.write(
4248
+ theme.warning(" \u26A0 Session appears to have been interrupted mid-task (last message is tool output).\n") + theme.dim(" The AI will see the tool history and can continue where it left off.\n") + theme.dim(' Tip: type "continue where you left off" or describe what to do next.\n')
4249
+ );
4250
+ }
4251
+ }
4091
4252
  }
4092
4253
  if (layers.length > 0) {
4093
4254
  if (layers.length === 1) {
@@ -4128,14 +4289,14 @@ Session '${this.resumeSessionId}' not found.
4128
4289
  process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
4129
4290
  `));
4130
4291
  }
4131
- const skillsDir = join4(this.config.getConfigDir(), SKILLS_DIR_NAME);
4292
+ const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
4132
4293
  this.skillManager = new SkillManager(skillsDir);
4133
4294
  const skillCount = this.skillManager.loadSkills();
4134
4295
  if (skillCount > 0) {
4135
4296
  process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
4136
4297
  `));
4137
4298
  }
4138
- const commandsDir = join4(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
4299
+ const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
4139
4300
  this.customCommandManager = new CustomCommandManager(commandsDir);
4140
4301
  const customCmdCount = this.customCommandManager.loadCommands();
4141
4302
  if (customCmdCount > 0) {
@@ -4318,8 +4479,17 @@ Session '${this.resumeSessionId}' not found.
4318
4479
  await this.handleChatSimple(provider, session.messages);
4319
4480
  }
4320
4481
  if (this.config.get("session").autoSave) {
4482
+ if (autoTrimSessionIfNeeded(session)) {
4483
+ process.stderr.write(theme.dim(" [session] Trimmed old tool output to reduce file size\n"));
4484
+ }
4321
4485
  await this.sessions.save();
4322
4486
  }
4487
+ this.costTracker.save();
4488
+ const budgetWarning = this.costTracker.checkBudget(this.config.get("monthlyBudget"));
4489
+ if (budgetWarning) {
4490
+ process.stdout.write(theme.warning(` ${budgetWarning}
4491
+ `));
4492
+ }
4323
4493
  const elapsed = Date.now() - t0;
4324
4494
  const threshold = this.config.get("ui").notificationThreshold;
4325
4495
  if (threshold > 0 && elapsed >= threshold) {
@@ -4583,14 +4753,14 @@ Session '${this.resumeSessionId}' not found.
4583
4753
  const dir = normalized.includes("/") ? dirname3(normalized) : ".";
4584
4754
  const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
4585
4755
  const absDir = resolve2(process.cwd(), dir);
4586
- if (!existsSync4(absDir)) return [];
4756
+ if (!existsSync5(absDir)) return [];
4587
4757
  const entries = readdirSync3(absDir);
4588
4758
  const results = [];
4589
4759
  for (const entry of entries) {
4590
4760
  if (entry.startsWith(".")) continue;
4591
4761
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
4592
4762
  try {
4593
- const fullPath = join4(absDir, entry);
4763
+ const fullPath = join5(absDir, entry);
4594
4764
  const stat = statSync3(fullPath);
4595
4765
  const rel = dir === "." ? entry : `${dir}/${entry}`;
4596
4766
  results.push(stat.isDirectory() ? `${rel}/` : rel);
@@ -4862,6 +5032,8 @@ Session '${this.resumeSessionId}' not found.
4862
5032
  async handleChatWithTools(provider, messages) {
4863
5033
  const session = this.sessions.current;
4864
5034
  let toolDefs;
5035
+ let mcpBudgetNote = null;
5036
+ const usedMcpToolNames = /* @__PURE__ */ new Set();
4865
5037
  if (this.planMode) {
4866
5038
  toolDefs = this.toolRegistry.getDefinitions().filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name));
4867
5039
  } else {
@@ -4869,7 +5041,19 @@ Session '${this.resumeSessionId}' not found.
4869
5041
  if (skillFilter) {
4870
5042
  toolDefs = this.toolRegistry.getDefinitions().filter((t) => skillFilter.has(t.name));
4871
5043
  } else {
4872
- toolDefs = this.toolRegistry.getDefinitions();
5044
+ const contextWindow = this.getContextWindowSize();
5045
+ if (contextWindow > 0) {
5046
+ const toolBudget = Math.floor(contextWindow * 0.2);
5047
+ const { definitions, trimmedCount, systemNote } = this.toolRegistry.getDefinitionsWithBudget(toolBudget, usedMcpToolNames);
5048
+ toolDefs = definitions;
5049
+ mcpBudgetNote = systemNote;
5050
+ if (trimmedCount > 0) {
5051
+ process.stderr.write(theme.dim(` [MCP budget] ${trimmedCount} MCP tool(s) trimmed to fit ${toolBudget.toLocaleString()} token budget
5052
+ `));
5053
+ }
5054
+ } else {
5055
+ toolDefs = this.toolRegistry.getDefinitions();
5056
+ }
4873
5057
  }
4874
5058
  }
4875
5059
  if (this.allowedTools) {
@@ -4904,7 +5088,9 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
4904
5088
  - Do NOT read the same file more than once \u2014 use the content from previous reads.
4905
5089
  - Prioritize the most critical tasks first in case rounds run out.
4906
5090
  - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
4907
- const systemPrompt = baseSystemPrompt + roundBudgetHint;
5091
+ const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
5092
+
5093
+ ${mcpBudgetNote}` : "");
4908
5094
  const modelParams = this.getModelParams();
4909
5095
  const useStreaming = this.config.get("ui").streaming;
4910
5096
  const spinner = this.renderer.showSpinner("Thinking...");
@@ -5270,6 +5456,9 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
5270
5456
  const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
5271
5457
  const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults, reasoningContent);
5272
5458
  extraMessages.push(...newMsgs);
5459
+ for (const tc of result.toolCalls) {
5460
+ if (tc.name.startsWith("mcp__")) usedMcpToolNames.add(tc.name);
5461
+ }
5273
5462
  const streamedContent = "content" in result ? result.content : void 0;
5274
5463
  persistToolRound(session, result.toolCalls, toolResults, {
5275
5464
  assistantContent: streamedContent,
@@ -5606,6 +5795,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5606
5795
  listContextDirs: () => [...this.extraContextDirs],
5607
5796
  forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
5608
5797
  getToolExecutor: () => this.toolExecutor,
5798
+ getCostTracker: () => this.costTracker,
5609
5799
  exit: () => this.handleExit()
5610
5800
  };
5611
5801
  await cmd.execute(args, ctx);
@@ -5710,7 +5900,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5710
5900
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5711
5901
  process.exit(1);
5712
5902
  }
5713
- const { startWebServer } = await import("./server-YJWIPDF2.js");
5903
+ const { startWebServer } = await import("./server-BGMUORHT.js");
5714
5904
  await startWebServer({ port, host: options.host });
5715
5905
  });
5716
5906
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5943,7 +6133,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5943
6133
  }),
5944
6134
  config.get("customProviders")
5945
6135
  );
5946
- const { startHub } = await import("./hub-WA2DZCSQ.js");
6136
+ const { startHub } = await import("./hub-R6ID4F6J.js");
5947
6137
  await startHub(
5948
6138
  {
5949
6139
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-QUD2AVHH.js";
4
+ } from "./chunk-GUD733DE.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-GA74LZ62.js";
5
+ } from "./chunk-MLEM56CR.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -7,9 +7,9 @@ import {
7
7
  SessionManager,
8
8
  SkillManager,
9
9
  TOOL_CALL_REMINDER,
10
+ autoTrimSessionIfNeeded,
10
11
  computeCost,
11
12
  detectsHallucinatedFileOp,
12
- estimateTokens,
13
13
  extractToolHistory,
14
14
  formatCost,
15
15
  formatGitContextForPrompt,
@@ -21,7 +21,7 @@ import {
21
21
  persistToolRound,
22
22
  rebuildExtraMessages,
23
23
  setupProxy
24
- } from "./chunk-2WGVM2J2.js";
24
+ } from "./chunk-EYRPTNVI.js";
25
25
  import {
26
26
  AuthManager
27
27
  } from "./chunk-BYNY5JPB.js";
@@ -30,6 +30,7 @@ import {
30
30
  ToolRegistry,
31
31
  askUserContext,
32
32
  checkPermission,
33
+ estimateTokens,
33
34
  getDangerLevel,
34
35
  googleSearchContext,
35
36
  isFileWriteTool,
@@ -40,7 +41,7 @@ import {
40
41
  spawnAgentContext,
41
42
  truncateOutput,
42
43
  undoStack
43
- } from "./chunk-2ZCD5F4X.js";
44
+ } from "./chunk-E45EGVSY.js";
44
45
  import "./chunk-4BKXL7SM.js";
45
46
  import {
46
47
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -60,7 +61,7 @@ import {
60
61
  SKILLS_DIR_NAME,
61
62
  VERSION,
62
63
  buildUserIdentityPrompt
63
- } from "./chunk-GA74LZ62.js";
64
+ } from "./chunk-MLEM56CR.js";
64
65
 
65
66
  // src/web/server.ts
66
67
  import express from "express";
@@ -324,14 +325,19 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
324
325
  }
325
326
  }
326
327
  async executeAll(calls) {
327
- const safeCalls = [];
328
+ const safeParallel = [];
329
+ const safeBash = [];
328
330
  const fileWriteCalls = [];
329
331
  const otherCalls = [];
330
332
  for (let i = 0; i < calls.length; i++) {
331
333
  const call = calls[i];
332
334
  const level = getDangerLevel(call.name, call.arguments);
333
335
  if (level === "safe") {
334
- safeCalls.push({ idx: i, call });
336
+ if (call.name === "bash") {
337
+ safeBash.push({ idx: i, call });
338
+ } else {
339
+ safeParallel.push({ idx: i, call });
340
+ }
335
341
  } else if (isFileWriteTool(call.name) && level === "write") {
336
342
  fileWriteCalls.push({ idx: i, call });
337
343
  } else {
@@ -339,11 +345,15 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
339
345
  }
340
346
  }
341
347
  const results = new Array(calls.length);
342
- await Promise.all(
343
- safeCalls.map(async ({ idx, call }) => {
348
+ const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(async ({ idx, call }) => {
349
+ results[idx] = await this.execute(call);
350
+ })) : Promise.resolve();
351
+ const bashPhase = (async () => {
352
+ for (const { idx, call } of safeBash) {
344
353
  results[idx] = await this.execute(call);
345
- })
346
- );
354
+ }
355
+ })();
356
+ await Promise.all([parallelPhase, bashPhase]);
347
357
  if (fileWriteCalls.length === 1) {
348
358
  const { idx, call } = fileWriteCalls[0];
349
359
  results[idx] = await this.execute(call);
@@ -361,33 +371,39 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
361
371
  async executeBatchFileWrites(items) {
362
372
  const calls = items.map((i) => i.call);
363
373
  const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
364
- const results = [];
374
+ const results = new Array(calls.length);
375
+ const approvedIndices = [];
365
376
  for (let i = 0; i < calls.length; i++) {
366
- const call = calls[i];
367
377
  if (decision === "all" || decision instanceof Set && decision.has(i + 1)) {
378
+ approvedIndices.push(i);
379
+ } else {
380
+ results[i] = {
381
+ callId: calls[i].id,
382
+ content: `[User rejected] File write rejected by user. Do not retry without asking.`,
383
+ isError: true
384
+ };
385
+ }
386
+ }
387
+ await Promise.all(
388
+ approvedIndices.map(async (i) => {
389
+ const call = calls[i];
368
390
  const tool = this.registry.get(call.name);
369
391
  if (!tool) {
370
- results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
371
- continue;
392
+ results[i] = { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
393
+ return;
372
394
  }
373
395
  try {
374
396
  const rawContent = await tool.execute(call.arguments);
375
397
  const content = truncateOutput(rawContent, call.name);
376
398
  this.sendToolCallResult(call, rawContent, false);
377
- results.push({ callId: call.id, content, isError: false });
399
+ results[i] = { callId: call.id, content, isError: false };
378
400
  } catch (err) {
379
401
  const message = err instanceof Error ? err.message : String(err);
380
402
  this.sendToolCallResult(call, message, true);
381
- results.push({ callId: call.id, content: message, isError: true });
403
+ results[i] = { callId: call.id, content: message, isError: true };
382
404
  }
383
- } else {
384
- results.push({
385
- callId: call.id,
386
- content: `[User rejected] File write rejected by user. Do not retry without asking.`,
387
- isError: true
388
- });
389
- }
390
- }
405
+ })
406
+ );
391
407
  return results;
392
408
  }
393
409
  };
@@ -697,6 +713,7 @@ var SessionHandler = class _SessionHandler {
697
713
  /** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
698
714
  saveIfNeeded() {
699
715
  if (this.sessions.current && this.sessions.current.messages.length > 0) {
716
+ autoTrimSessionIfNeeded(this.sessions.current);
700
717
  const id = this.sessions.current.id;
701
718
  this.sessions.save();
702
719
  this.unsavedSessions.delete(id);
@@ -754,9 +771,9 @@ var SessionHandler = class _SessionHandler {
754
771
  return;
755
772
  }
756
773
  const hasToolSupport = typeof provider.chatWithTools === "function";
757
- const toolDefs = hasToolSupport ? this.getFilteredToolDefs() : [];
774
+ const { toolDefs, mcpBudgetNote } = hasToolSupport ? this.getFilteredToolDefs() : { toolDefs: [], mcpBudgetNote: null };
758
775
  if (hasToolSupport && toolDefs.length > 0) {
759
- await this.handleChatWithTools(provider, session.messages, toolDefs);
776
+ await this.handleChatWithTools(provider, session.messages, toolDefs, mcpBudgetNote);
760
777
  } else {
761
778
  await this.handleChatSimple(provider, session.messages);
762
779
  }
@@ -821,7 +838,7 @@ var SessionHandler = class _SessionHandler {
821
838
  this.abortController = null;
822
839
  }
823
840
  }
824
- async handleChatWithTools(provider, messages, toolDefs) {
841
+ async handleChatWithTools(provider, messages, toolDefs, mcpBudgetNote) {
825
842
  const session = this.sessions.current;
826
843
  const { baseMessages: cleanMessages, toolHistory } = extractToolHistory(messages);
827
844
  const apiMessages = [...cleanMessages];
@@ -839,7 +856,9 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
839
856
  - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
840
857
  - Prioritize the most critical tasks first in case rounds run out.
841
858
  - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
842
- const systemPrompt = baseSystemPrompt + roundBudgetHint;
859
+ const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
860
+
861
+ ${mcpBudgetNote}` : "");
843
862
  const modelParams = this.getModelParams();
844
863
  const roundUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
845
864
  const supportsStreamingTools = typeof provider.chatWithToolsStream === "function";
@@ -1323,6 +1342,17 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1323
1342
  this.sessions.loadSession(found.id);
1324
1343
  this.resetWebSessionUsage();
1325
1344
  this.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
1345
+ const loadedSession = this.sessions.current;
1346
+ if (loadedSession && loadedSession.messages.length > 0) {
1347
+ const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
1348
+ const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
1349
+ if (isIncomplete) {
1350
+ this.send({
1351
+ type: "info",
1352
+ message: '\u26A0 This session appears to have been interrupted mid-task. The AI will see the tool history and can continue where it left off. Type "continue where you left off" to resume.'
1353
+ });
1354
+ }
1355
+ }
1326
1356
  this.sendSessionMessages();
1327
1357
  this.sendStatus();
1328
1358
  this.sendSessionList();
@@ -1929,7 +1959,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1929
1959
  case "test": {
1930
1960
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1931
1961
  try {
1932
- const { executeTests } = await import("./run-tests-CT4PRGGP.js");
1962
+ const { executeTests } = await import("./run-tests-KRGPFYPF.js");
1933
1963
  const argStr = args.join(" ").trim();
1934
1964
  let testArgs = {};
1935
1965
  if (argStr) {
@@ -2517,16 +2547,26 @@ Add .md files to create commands.` });
2517
2547
  };
2518
2548
  }
2519
2549
  getFilteredToolDefs() {
2520
- let defs = this.toolRegistry.getDefinitions();
2521
2550
  if (this.planMode) {
2522
- defs = defs.filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name));
2523
- } else {
2524
- const skillFilter = this.skillManager?.getActiveToolFilter();
2525
- if (skillFilter) {
2526
- defs = defs.filter((t) => skillFilter.has(t.name));
2527
- }
2551
+ return {
2552
+ toolDefs: this.toolRegistry.getDefinitions().filter((t) => PLAN_MODE_READONLY_TOOLS.has(t.name)),
2553
+ mcpBudgetNote: null
2554
+ };
2555
+ }
2556
+ const skillFilter = this.skillManager?.getActiveToolFilter();
2557
+ if (skillFilter) {
2558
+ return {
2559
+ toolDefs: this.toolRegistry.getDefinitions().filter((t) => skillFilter.has(t.name)),
2560
+ mcpBudgetNote: null
2561
+ };
2562
+ }
2563
+ const contextWindow = this.getContextWindowSize();
2564
+ if (contextWindow > 0) {
2565
+ const toolBudget = Math.floor(contextWindow * 0.2);
2566
+ const { definitions, systemNote } = this.toolRegistry.getDefinitionsWithBudget(toolBudget);
2567
+ return { toolDefs: definitions, mcpBudgetNote: systemNote };
2528
2568
  }
2529
- return defs;
2569
+ return { toolDefs: this.toolRegistry.getDefinitions(), mcpBudgetNote: null };
2530
2570
  }
2531
2571
  /**
2532
2572
  * 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-2ZCD5F4X.js";
7
+ } from "./chunk-E45EGVSY.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-GA74LZ62.js";
11
+ } from "./chunk-MLEM56CR.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.61",
3
+ "version": "0.4.63",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",