jinzd-ai-cli 0.4.62 → 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-LLI6COMK.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
  }
@@ -8,7 +8,7 @@ import {
8
8
  RateLimitError,
9
9
  schemaToJsonSchema,
10
10
  truncateForPersist
11
- } from "./chunk-672OV76Z.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-LLI6COMK.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 权限在主进程中执行(可读写文件、访问网络、执行命令),
@@ -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.62";
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.62";
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-2ATTBG2S.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
@@ -31,7 +31,7 @@ import {
31
31
  saveDevState,
32
32
  sessionHasMeaningfulContent,
33
33
  setupProxy
34
- } from "./chunk-6OTF2ILP.js";
34
+ } from "./chunk-EYRPTNVI.js";
35
35
  import {
36
36
  ToolExecutor,
37
37
  ToolRegistry,
@@ -46,7 +46,7 @@ import {
46
46
  spawnAgentContext,
47
47
  theme,
48
48
  undoStack
49
- } from "./chunk-672OV76Z.js";
49
+ } from "./chunk-E45EGVSY.js";
50
50
  import {
51
51
  fileCheckpoints
52
52
  } from "./chunk-4BKXL7SM.js";
@@ -71,15 +71,15 @@ import {
71
71
  SKILLS_DIR_NAME,
72
72
  VERSION,
73
73
  buildUserIdentityPrompt
74
- } from "./chunk-LLI6COMK.js";
74
+ } from "./chunk-MLEM56CR.js";
75
75
 
76
76
  // src/index.ts
77
77
  import { program } from "commander";
78
78
 
79
79
  // src/repl/repl.ts
80
80
  import * as readline from "readline";
81
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
82
- 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";
83
83
  import chalk4 from "chalk";
84
84
 
85
85
  // src/repl/renderer.ts
@@ -1843,7 +1843,7 @@ ${hint}` : "")
1843
1843
  {
1844
1844
  name: "cost",
1845
1845
  description: "Show session token usage, prompt-cache hits, and USD cost",
1846
- usage: "/cost [reset]",
1846
+ usage: "/cost [reset | history]",
1847
1847
  execute(args, ctx) {
1848
1848
  const sub = args[0]?.toLowerCase();
1849
1849
  if (sub === "reset") {
@@ -1851,6 +1851,28 @@ ${hint}` : "")
1851
1851
  ctx.renderer.printSuccess("Session token counters reset.");
1852
1852
  return;
1853
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
+ }
1854
1876
  const session = ctx.sessions.current;
1855
1877
  const usage = session?.tokenUsage ?? {
1856
1878
  inputTokens: 0,
@@ -2166,7 +2188,7 @@ ${hint}` : "")
2166
2188
  usage: "/test [command|filter]",
2167
2189
  async execute(args, ctx) {
2168
2190
  try {
2169
- const { executeTests } = await import("./run-tests-ORVJAUJG.js");
2191
+ const { executeTests } = await import("./run-tests-KRGPFYPF.js");
2170
2192
  const argStr = args.join(" ").trim();
2171
2193
  let testArgs = {};
2172
2194
  if (argStr) {
@@ -3230,6 +3252,125 @@ var CustomCommandManager = class {
3230
3252
  }
3231
3253
  };
3232
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
+
3233
3374
  // src/repl/notify.ts
3234
3375
  import { spawn } from "child_process";
3235
3376
  import { platform as platform2 } from "os";
@@ -3303,7 +3444,7 @@ function parseAtReferences(input2, cwd) {
3303
3444
  const absPath = resolve2(cwd, rawPath);
3304
3445
  const ext = extname2(rawPath).toLowerCase();
3305
3446
  const mime = IMAGE_MIME[ext];
3306
- if (!existsSync4(absPath)) {
3447
+ if (!existsSync5(absPath)) {
3307
3448
  refs.push({ path: rawPath, type: "notfound" });
3308
3449
  continue;
3309
3450
  }
@@ -3313,7 +3454,7 @@ function parseAtReferences(input2, cwd) {
3313
3454
  refs.push({ path: rawPath, type: "toolarge" });
3314
3455
  continue;
3315
3456
  }
3316
- const data = readFileSync3(absPath).toString("base64");
3457
+ const data = readFileSync4(absPath).toString("base64");
3317
3458
  imageParts.push({
3318
3459
  type: "image_url",
3319
3460
  image_url: { url: `data:${mime};base64,${data}` }
@@ -3321,7 +3462,7 @@ function parseAtReferences(input2, cwd) {
3321
3462
  refs.push({ path: rawPath, type: "image" });
3322
3463
  textBody = textBody.replace(match[0], "").trim();
3323
3464
  } else {
3324
- const content = readFileSync3(absPath, "utf-8");
3465
+ const content = readFileSync4(absPath, "utf-8");
3325
3466
  const inlined = `
3326
3467
 
3327
3468
  [File: ${rawPath}]
@@ -3390,6 +3531,7 @@ var Repl = class {
3390
3531
  if (options?.blockedTools) this.blockedTools = options.blockedTools;
3391
3532
  if (options?.resumeSessionId) this.resumeSessionId = options.resumeSessionId;
3392
3533
  if (options?.maxToolRoundsOverride !== void 0) this.maxToolRoundsOverride = options.maxToolRoundsOverride;
3534
+ this.costTracker = new CostTracker(this.config.getConfigDir());
3393
3535
  }
3394
3536
  rl;
3395
3537
  currentProvider;
@@ -3405,12 +3547,13 @@ var Repl = class {
3405
3547
  contextLayers = [];
3406
3548
  /** 本次会话累计 token 用量 */
3407
3549
  sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
3408
- /** 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. */
3409
3551
  addSessionUsage(u) {
3410
3552
  this.sessionTokenUsage.inputTokens += u.inputTokens;
3411
3553
  this.sessionTokenUsage.outputTokens += u.outputTokens;
3412
3554
  this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
3413
3555
  this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
3556
+ this.costTracker.addCost(this.currentProvider, this.currentModel, u);
3414
3557
  }
3415
3558
  /** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
3416
3559
  gitBranch = null;
@@ -3452,6 +3595,8 @@ var Repl = class {
3452
3595
  selecting = false;
3453
3596
  /** CLI --max-tool-rounds 覆盖值;未指定时从 config.maxToolRounds 读取 */
3454
3597
  maxToolRoundsOverride;
3598
+ /** 跨 session 成本追踪器 */
3599
+ costTracker;
3455
3600
  // ── /add-dir 目录上下文支持 ────────────────────────────────────────────────
3456
3601
  /**
3457
3602
  * 扫描目录内容,返回格式化字符串(含目录树 + 关键文件内容)。
@@ -3531,7 +3676,7 @@ var Repl = class {
3531
3676
  const filtered = entries.filter((e) => !SKIP_DIRS_SET.has(e));
3532
3677
  for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
3533
3678
  const name = filtered[i];
3534
- const fullPath = join4(dir, name);
3679
+ const fullPath = join5(dir, name);
3535
3680
  const isLast = i === filtered.length - 1;
3536
3681
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
3537
3682
  let isDir;
@@ -3565,7 +3710,7 @@ ${treeLines.join("\n")}`
3565
3710
  for (const name of entries) {
3566
3711
  if (totalChars >= MAX_TOTAL_CHARS) break;
3567
3712
  if (SKIP_DIRS_SET.has(name)) continue;
3568
- const fullPath = join4(dir, name);
3713
+ const fullPath = join5(dir, name);
3569
3714
  let st;
3570
3715
  try {
3571
3716
  st = statSync3(fullPath);
@@ -3580,7 +3725,7 @@ ${treeLines.join("\n")}`
3580
3725
  if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
3581
3726
  if (st.size > MAX_FILE_CHARS * 3) continue;
3582
3727
  try {
3583
- let content = readFileSync3(fullPath, "utf-8");
3728
+ let content = readFileSync4(fullPath, "utf-8");
3584
3729
  if (content.length > MAX_FILE_CHARS) {
3585
3730
  content = content.slice(0, MAX_FILE_CHARS) + `
3586
3731
  ... (truncated, ${content.length} chars total)`;
@@ -3610,7 +3755,7 @@ ${content}
3610
3755
  */
3611
3756
  addExtraContextDir(dirPath) {
3612
3757
  const absPath = resolve2(dirPath);
3613
- if (!existsSync4(absPath)) {
3758
+ if (!existsSync5(absPath)) {
3614
3759
  return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
3615
3760
  }
3616
3761
  let isDir;
@@ -3644,9 +3789,9 @@ ${content}
3644
3789
  */
3645
3790
  findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
3646
3791
  for (const candidate of candidates) {
3647
- const fullPath = join4(dir, candidate);
3648
- if (existsSync4(fullPath)) {
3649
- 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();
3650
3795
  if (content) return { filePath: fullPath, content };
3651
3796
  }
3652
3797
  }
@@ -3674,10 +3819,10 @@ ${content}
3674
3819
  const cwd = process.cwd();
3675
3820
  const gitRoot = getGitRoot(cwd);
3676
3821
  const projectRoot = gitRoot ?? cwd;
3677
- const mcpPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
3678
- if (!existsSync4(mcpPath)) return null;
3822
+ const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
3823
+ if (!existsSync5(mcpPath)) return null;
3679
3824
  try {
3680
- const raw = JSON.parse(readFileSync3(mcpPath, "utf-8"));
3825
+ const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
3681
3826
  const servers = raw?.mcpServers;
3682
3827
  if (!servers || typeof servers !== "object") {
3683
3828
  process.stderr.write(
@@ -3723,8 +3868,8 @@ ${content}
3723
3868
  );
3724
3869
  return { layers: [], mergedContent: "" };
3725
3870
  }
3726
- if (existsSync4(fullPath)) {
3727
- const content = readFileSync3(fullPath, "utf-8").trim();
3871
+ if (existsSync5(fullPath)) {
3872
+ const content = readFileSync4(fullPath, "utf-8").trim();
3728
3873
  if (content) {
3729
3874
  const layer = {
3730
3875
  level: "project",
@@ -3781,9 +3926,9 @@ ${content}
3781
3926
  * 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
3782
3927
  */
3783
3928
  loadMemoryContent() {
3784
- const memoryPath = join4(this.config.getConfigDir(), MEMORY_FILE_NAME);
3785
- if (!existsSync4(memoryPath)) return null;
3786
- 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();
3787
3932
  if (!content) return null;
3788
3933
  if (content.length > MEMORY_MAX_CHARS) {
3789
3934
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -4094,6 +4239,16 @@ Session '${this.resumeSessionId}' not found.
4094
4239
  process.stdout.write(
4095
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")
4096
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
+ }
4097
4252
  }
4098
4253
  if (layers.length > 0) {
4099
4254
  if (layers.length === 1) {
@@ -4134,14 +4289,14 @@ Session '${this.resumeSessionId}' not found.
4134
4289
  process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
4135
4290
  `));
4136
4291
  }
4137
- const skillsDir = join4(this.config.getConfigDir(), SKILLS_DIR_NAME);
4292
+ const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
4138
4293
  this.skillManager = new SkillManager(skillsDir);
4139
4294
  const skillCount = this.skillManager.loadSkills();
4140
4295
  if (skillCount > 0) {
4141
4296
  process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
4142
4297
  `));
4143
4298
  }
4144
- const commandsDir = join4(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
4299
+ const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
4145
4300
  this.customCommandManager = new CustomCommandManager(commandsDir);
4146
4301
  const customCmdCount = this.customCommandManager.loadCommands();
4147
4302
  if (customCmdCount > 0) {
@@ -4329,6 +4484,12 @@ Session '${this.resumeSessionId}' not found.
4329
4484
  }
4330
4485
  await this.sessions.save();
4331
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
+ }
4332
4493
  const elapsed = Date.now() - t0;
4333
4494
  const threshold = this.config.get("ui").notificationThreshold;
4334
4495
  if (threshold > 0 && elapsed >= threshold) {
@@ -4592,14 +4753,14 @@ Session '${this.resumeSessionId}' not found.
4592
4753
  const dir = normalized.includes("/") ? dirname3(normalized) : ".";
4593
4754
  const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
4594
4755
  const absDir = resolve2(process.cwd(), dir);
4595
- if (!existsSync4(absDir)) return [];
4756
+ if (!existsSync5(absDir)) return [];
4596
4757
  const entries = readdirSync3(absDir);
4597
4758
  const results = [];
4598
4759
  for (const entry of entries) {
4599
4760
  if (entry.startsWith(".")) continue;
4600
4761
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
4601
4762
  try {
4602
- const fullPath = join4(absDir, entry);
4763
+ const fullPath = join5(absDir, entry);
4603
4764
  const stat = statSync3(fullPath);
4604
4765
  const rel = dir === "." ? entry : `${dir}/${entry}`;
4605
4766
  results.push(stat.isDirectory() ? `${rel}/` : rel);
@@ -5634,6 +5795,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5634
5795
  listContextDirs: () => [...this.extraContextDirs],
5635
5796
  forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
5636
5797
  getToolExecutor: () => this.toolExecutor,
5798
+ getCostTracker: () => this.costTracker,
5637
5799
  exit: () => this.handleExit()
5638
5800
  };
5639
5801
  await cmd.execute(args, ctx);
@@ -5738,7 +5900,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5738
5900
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5739
5901
  process.exit(1);
5740
5902
  }
5741
- const { startWebServer } = await import("./server-HBAOUOIC.js");
5903
+ const { startWebServer } = await import("./server-BGMUORHT.js");
5742
5904
  await startWebServer({ port, host: options.host });
5743
5905
  });
5744
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) => {
@@ -5971,7 +6133,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5971
6133
  }),
5972
6134
  config.get("customProviders")
5973
6135
  );
5974
- const { startHub } = await import("./hub-2XLQSZY2.js");
6136
+ const { startHub } = await import("./hub-R6ID4F6J.js");
5975
6137
  await startHub(
5976
6138
  {
5977
6139
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-YUUCUJHU.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-LLI6COMK.js";
5
+ } from "./chunk-MLEM56CR.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -21,7 +21,7 @@ import {
21
21
  persistToolRound,
22
22
  rebuildExtraMessages,
23
23
  setupProxy
24
- } from "./chunk-6OTF2ILP.js";
24
+ } from "./chunk-EYRPTNVI.js";
25
25
  import {
26
26
  AuthManager
27
27
  } from "./chunk-BYNY5JPB.js";
@@ -41,7 +41,7 @@ import {
41
41
  spawnAgentContext,
42
42
  truncateOutput,
43
43
  undoStack
44
- } from "./chunk-672OV76Z.js";
44
+ } from "./chunk-E45EGVSY.js";
45
45
  import "./chunk-4BKXL7SM.js";
46
46
  import {
47
47
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -61,7 +61,7 @@ import {
61
61
  SKILLS_DIR_NAME,
62
62
  VERSION,
63
63
  buildUserIdentityPrompt
64
- } from "./chunk-LLI6COMK.js";
64
+ } from "./chunk-MLEM56CR.js";
65
65
 
66
66
  // src/web/server.ts
67
67
  import express from "express";
@@ -325,14 +325,19 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
325
325
  }
326
326
  }
327
327
  async executeAll(calls) {
328
- const safeCalls = [];
328
+ const safeParallel = [];
329
+ const safeBash = [];
329
330
  const fileWriteCalls = [];
330
331
  const otherCalls = [];
331
332
  for (let i = 0; i < calls.length; i++) {
332
333
  const call = calls[i];
333
334
  const level = getDangerLevel(call.name, call.arguments);
334
335
  if (level === "safe") {
335
- 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
+ }
336
341
  } else if (isFileWriteTool(call.name) && level === "write") {
337
342
  fileWriteCalls.push({ idx: i, call });
338
343
  } else {
@@ -340,11 +345,15 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
340
345
  }
341
346
  }
342
347
  const results = new Array(calls.length);
343
- await Promise.all(
344
- 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) {
345
353
  results[idx] = await this.execute(call);
346
- })
347
- );
354
+ }
355
+ })();
356
+ await Promise.all([parallelPhase, bashPhase]);
348
357
  if (fileWriteCalls.length === 1) {
349
358
  const { idx, call } = fileWriteCalls[0];
350
359
  results[idx] = await this.execute(call);
@@ -362,33 +371,39 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
362
371
  async executeBatchFileWrites(items) {
363
372
  const calls = items.map((i) => i.call);
364
373
  const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
365
- const results = [];
374
+ const results = new Array(calls.length);
375
+ const approvedIndices = [];
366
376
  for (let i = 0; i < calls.length; i++) {
367
- const call = calls[i];
368
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];
369
390
  const tool = this.registry.get(call.name);
370
391
  if (!tool) {
371
- results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
372
- continue;
392
+ results[i] = { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
393
+ return;
373
394
  }
374
395
  try {
375
396
  const rawContent = await tool.execute(call.arguments);
376
397
  const content = truncateOutput(rawContent, call.name);
377
398
  this.sendToolCallResult(call, rawContent, false);
378
- results.push({ callId: call.id, content, isError: false });
399
+ results[i] = { callId: call.id, content, isError: false };
379
400
  } catch (err) {
380
401
  const message = err instanceof Error ? err.message : String(err);
381
402
  this.sendToolCallResult(call, message, true);
382
- results.push({ callId: call.id, content: message, isError: true });
403
+ results[i] = { callId: call.id, content: message, isError: true };
383
404
  }
384
- } else {
385
- results.push({
386
- callId: call.id,
387
- content: `[User rejected] File write rejected by user. Do not retry without asking.`,
388
- isError: true
389
- });
390
- }
391
- }
405
+ })
406
+ );
392
407
  return results;
393
408
  }
394
409
  };
@@ -1327,6 +1342,17 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1327
1342
  this.sessions.loadSession(found.id);
1328
1343
  this.resetWebSessionUsage();
1329
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
+ }
1330
1356
  this.sendSessionMessages();
1331
1357
  this.sendStatus();
1332
1358
  this.sendSessionList();
@@ -1933,7 +1959,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1933
1959
  case "test": {
1934
1960
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1935
1961
  try {
1936
- const { executeTests } = await import("./run-tests-ORVJAUJG.js");
1962
+ const { executeTests } = await import("./run-tests-KRGPFYPF.js");
1937
1963
  const argStr = args.join(" ").trim();
1938
1964
  let testArgs = {};
1939
1965
  if (argStr) {
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-672OV76Z.js";
7
+ } from "./chunk-E45EGVSY.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-LLI6COMK.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.62",
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",