jinzd-ai-cli 0.4.66 → 0.4.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.4.66";
11
+ var VERSION = "0.4.68";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -8,7 +8,7 @@ import {
8
8
  RateLimitError,
9
9
  schemaToJsonSchema,
10
10
  truncateForPersist
11
- } from "./chunk-X5RX2VGQ.js";
11
+ } from "./chunk-Q5QSCO5D.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-NIJZBQ6I.js";
24
+ } from "./chunk-3LCVJ4AF.js";
25
25
 
26
26
  // src/config/config-manager.ts
27
27
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -191,7 +191,27 @@ var ConfigSchema = z.object({
191
191
  // 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
192
192
  // 必须确认插件来源可信后,再设为 true 启用。
193
193
  // 可通过 /config 命令或直接编辑 ~/.aicli/config.json 开启。
194
- allowPlugins: z.boolean().default(false)
194
+ allowPlugins: z.boolean().default(false),
195
+ // 智能模型路由(v0.4.68+)
196
+ // 按用户每轮输入的内容/标签/长度动态选择模型,在同一 provider 内切换,
197
+ // 例:短问题走 haiku(省钱),planning 走 opus(质量)。
198
+ // enabled=false 时永远返回当前模型。rules 按顺序匹配,首个命中的规则生效。
199
+ // 每个 rule 的 match 必须至少有一个条件(tag/contains/maxLength/minLength)。
200
+ // 详见 src/core/model-router.ts。
201
+ routing: z.object({
202
+ enabled: z.boolean().default(false),
203
+ rules: z.array(z.object({
204
+ match: z.object({
205
+ contains: z.array(z.string()).optional(),
206
+ maxLength: z.number().int().positive().optional(),
207
+ minLength: z.number().int().positive().optional(),
208
+ tag: z.string().optional()
209
+ }),
210
+ model: z.string(),
211
+ name: z.string().optional()
212
+ })).default([]),
213
+ fallback: z.string().optional()
214
+ }).default({ enabled: false, rules: [] })
195
215
  });
196
216
 
197
217
  // src/config/config-manager.ts
@@ -10,7 +10,7 @@ import {
10
10
  SUBAGENT_DEFAULT_MAX_ROUNDS,
11
11
  SUBAGENT_MAX_ROUNDS_LIMIT,
12
12
  runTestsTool
13
- } from "./chunk-NIJZBQ6I.js";
13
+ } from "./chunk-3LCVJ4AF.js";
14
14
 
15
15
  // src/tools/builtin/bash.ts
16
16
  import { execSync } from "child_process";
@@ -793,6 +793,11 @@ import { dirname as dirname2 } from "path";
793
793
  import chalk3 from "chalk";
794
794
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
795
795
 
796
+ // src/core/readline-internal.ts
797
+ function rlInternal(rl) {
798
+ return rl;
799
+ }
800
+
796
801
  // src/tools/types.ts
797
802
  function isFileWriteTool(name) {
798
803
  return name === "write_file" || name === "edit_file" || name === "notebook_edit";
@@ -842,6 +847,51 @@ function schemaToJsonSchema(schema) {
842
847
  return result;
843
848
  }
844
849
 
850
+ // src/tools/executor-phases.ts
851
+ function groupCallsByPhase(calls) {
852
+ const safeParallel = [];
853
+ const safeBash = [];
854
+ const fileWriteCalls = [];
855
+ const otherCalls = [];
856
+ for (let i = 0; i < calls.length; i++) {
857
+ const call = calls[i];
858
+ const level = getDangerLevel(call.name, call.arguments);
859
+ if (level === "safe") {
860
+ if (call.name === "bash") {
861
+ safeBash.push({ idx: i, call });
862
+ } else {
863
+ safeParallel.push({ idx: i, call });
864
+ }
865
+ } else if (isFileWriteTool(call.name) && level === "write") {
866
+ fileWriteCalls.push({ idx: i, call });
867
+ } else {
868
+ otherCalls.push({ idx: i, call });
869
+ }
870
+ }
871
+ return { safeParallel, safeBash, fileWriteCalls, otherCalls };
872
+ }
873
+ async function runSafePhases(phase, execute, results, logTag = "executor") {
874
+ const t0 = Date.now();
875
+ const { safeParallel, safeBash } = phase;
876
+ const wrapExec = async (idx, call) => {
877
+ try {
878
+ results[idx] = await execute(call);
879
+ } catch (err) {
880
+ const msg = err instanceof Error ? err.message : String(err);
881
+ console.error(`[${logTag}] Unexpected error in tool "${call.name}":`, err);
882
+ results[idx] = { callId: call.id, content: `Tool execution failed: ${msg}`, isError: true };
883
+ }
884
+ };
885
+ const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(({ idx, call }) => wrapExec(idx, call))) : Promise.resolve();
886
+ const bashPhase = (async () => {
887
+ for (const { idx, call } of safeBash) {
888
+ await wrapExec(idx, call);
889
+ }
890
+ })();
891
+ await Promise.all([parallelPhase, bashPhase]);
892
+ return Date.now() - t0;
893
+ }
894
+
845
895
  // src/tools/diff-utils.ts
846
896
  import chalk from "chalk";
847
897
  function renderDiff(oldText, newText, opts = {}) {
@@ -1365,57 +1415,22 @@ var ToolExecutor = class {
1365
1415
  }
1366
1416
  }
1367
1417
  async executeAll(calls) {
1368
- const safeParallel = [];
1369
- const safeBash = [];
1370
- const fileWriteCalls = [];
1371
- const otherCalls = [];
1372
- for (let i = 0; i < calls.length; i++) {
1373
- const call = calls[i];
1374
- const level = getDangerLevel(call.name, call.arguments);
1375
- if (level === "safe") {
1376
- if (call.name === "bash") {
1377
- safeBash.push({ idx: i, call });
1378
- } else {
1379
- safeParallel.push({ idx: i, call });
1380
- }
1381
- } else if (isFileWriteTool(call.name) && level === "write") {
1382
- fileWriteCalls.push({ idx: i, call });
1383
- } else {
1384
- otherCalls.push({ idx: i, call });
1385
- }
1386
- }
1418
+ const phase = groupCallsByPhase(calls);
1387
1419
  const results = new Array(calls.length);
1388
- const t0 = Date.now();
1389
- const wrapExec = async (idx, call) => {
1390
- try {
1391
- results[idx] = await this.execute(call);
1392
- } catch (err) {
1393
- const msg = err instanceof Error ? err.message : String(err);
1394
- console.error(`[executor] Unexpected error in tool "${call.name}":`, err);
1395
- results[idx] = { callId: call.id, content: `Tool execution failed: ${msg}`, isError: true };
1396
- }
1397
- };
1398
- const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(({ idx, call }) => wrapExec(idx, call))) : Promise.resolve();
1399
- const bashPhase = (async () => {
1400
- for (const { idx, call } of safeBash) {
1401
- await wrapExec(idx, call);
1402
- }
1403
- })();
1404
- await Promise.all([parallelPhase, bashPhase]);
1405
- if (safeParallel.length >= 2) {
1406
- const elapsed = Date.now() - t0;
1407
- console.log(theme.dim(` \u26A1 ${safeParallel.length} tools executed in parallel (${elapsed}ms)`));
1408
- }
1409
- if (fileWriteCalls.length === 1) {
1410
- const { idx, call } = fileWriteCalls[0];
1420
+ const elapsed = await runSafePhases(phase, (c) => this.execute(c), results, "executor");
1421
+ if (phase.safeParallel.length >= 2) {
1422
+ console.log(theme.dim(` \u26A1 ${phase.safeParallel.length} tools executed in parallel (${elapsed}ms)`));
1423
+ }
1424
+ if (phase.fileWriteCalls.length === 1) {
1425
+ const { idx, call } = phase.fileWriteCalls[0];
1411
1426
  results[idx] = await this.execute(call);
1412
- } else if (fileWriteCalls.length >= 2) {
1413
- const batchResults = await this.executeBatchFileWrites(fileWriteCalls.map((f) => f.call));
1414
- for (let i = 0; i < fileWriteCalls.length; i++) {
1415
- results[fileWriteCalls[i].idx] = batchResults[i];
1427
+ } else if (phase.fileWriteCalls.length >= 2) {
1428
+ const batchResults = await this.executeBatchFileWrites(phase.fileWriteCalls.map((f) => f.call));
1429
+ for (let i = 0; i < phase.fileWriteCalls.length; i++) {
1430
+ results[phase.fileWriteCalls[i].idx] = batchResults[i];
1416
1431
  }
1417
1432
  }
1418
- for (const { idx, call } of otherCalls) {
1433
+ for (const { idx, call } of phase.otherCalls) {
1419
1434
  results[idx] = await this.execute(call);
1420
1435
  }
1421
1436
  return results;
@@ -1450,6 +1465,10 @@ var ToolExecutor = class {
1450
1465
  results[i] = { callId: calls[i].id, content: `[User rejected] The user rejected this ${calls[i].name} operation. Do not retry without asking.`, isError: true };
1451
1466
  }
1452
1467
  }
1468
+ if (approvedIndices.length >= 2) {
1469
+ const labels = approvedIndices.map((i) => `[${i + 1}]`).join(" ");
1470
+ console.log(theme.dim(` \u26A1 Writing ${labels} in parallel (results may interleave)`));
1471
+ }
1453
1472
  const t0 = Date.now();
1454
1473
  await Promise.all(
1455
1474
  approvedIndices.map(async (i) => {
@@ -1488,7 +1507,7 @@ var ToolExecutor = class {
1488
1507
  return Promise.resolve("none");
1489
1508
  }
1490
1509
  const rl = this.rl;
1491
- const rlAny = rl;
1510
+ const rlAny = rlInternal(rl);
1492
1511
  const savedOutput = rlAny.output;
1493
1512
  rlAny.output = process.stdout;
1494
1513
  rl.resume();
@@ -1668,7 +1687,7 @@ var ToolExecutor = class {
1668
1687
  return Promise.resolve(false);
1669
1688
  }
1670
1689
  const rl = this.rl;
1671
- const rlAny = rl;
1690
+ const rlAny = rlInternal(rl);
1672
1691
  const savedOutput = rlAny.output;
1673
1692
  rlAny.output = process.stdout;
1674
1693
  rl.resume();
@@ -2930,7 +2949,7 @@ var askUserTool = {
2930
2949
  }
2931
2950
  };
2932
2951
  function promptUser(rl, question) {
2933
- const rlAny = rl;
2952
+ const rlAny = rlInternal(rl);
2934
2953
  const savedOutput = rlAny.output;
2935
2954
  rlAny.output = process.stdout;
2936
2955
  rl.resume();
@@ -4332,13 +4351,15 @@ export {
4332
4351
  RateLimitError,
4333
4352
  ConfigError,
4334
4353
  ProviderNotFoundError,
4335
- isFileWriteTool,
4336
4354
  getDangerLevel,
4337
4355
  schemaToJsonSchema,
4338
4356
  initTheme,
4339
4357
  theme,
4340
4358
  undoStack,
4341
4359
  renderDiff,
4360
+ rlInternal,
4361
+ groupCallsByPhase,
4362
+ runSafePhases,
4342
4363
  runHook,
4343
4364
  checkPermission,
4344
4365
  setMaxOutputCap,
@@ -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.66";
9
+ var VERSION = "0.4.68";
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-KOCFBDPK.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-WDRXASIC.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-HOBLE365.js";
34
+ } from "./chunk-G5AISHJE.js";
35
35
  import {
36
36
  ToolExecutor,
37
37
  ToolRegistry,
@@ -41,12 +41,13 @@ import {
41
41
  initTheme,
42
42
  lastResponseStore,
43
43
  renderDiff,
44
+ rlInternal,
44
45
  setContextWindow,
45
46
  setMaxOutputCap,
46
47
  spawnAgentContext,
47
48
  theme,
48
49
  undoStack
49
- } from "./chunk-X5RX2VGQ.js";
50
+ } from "./chunk-Q5QSCO5D.js";
50
51
  import {
51
52
  fileCheckpoints
52
53
  } from "./chunk-4BKXL7SM.js";
@@ -71,7 +72,7 @@ import {
71
72
  SKILLS_DIR_NAME,
72
73
  VERSION,
73
74
  buildUserIdentityPrompt
74
- } from "./chunk-NIJZBQ6I.js";
75
+ } from "./chunk-3LCVJ4AF.js";
75
76
 
76
77
  // src/index.ts
77
78
  import { program } from "commander";
@@ -1074,6 +1075,79 @@ function createDefaultCommands() {
1074
1075
  );
1075
1076
  }
1076
1077
  },
1078
+ {
1079
+ name: "route",
1080
+ description: "Smart model routing \u2014 enable/disable or inspect routing rules",
1081
+ usage: "/route [on|off|show|test <message>]",
1082
+ async execute(args, ctx) {
1083
+ const sub = (args[0] ?? "show").toLowerCase();
1084
+ const routing = ctx.config.get("routing");
1085
+ if (sub === "on" || sub === "enable") {
1086
+ ctx.config.setByPath("routing.enabled", "true");
1087
+ ctx.renderer.printSuccess("Smart model routing enabled.");
1088
+ if (!routing || routing.rules.length === 0) {
1089
+ ctx.renderer.printInfo(
1090
+ 'No rules configured yet. Add rules under `routing.rules` in ~/.aicli/config.json.\nExample: { match: { tag: "fast" }, model: "claude-haiku-4-5" }'
1091
+ );
1092
+ }
1093
+ return;
1094
+ }
1095
+ if (sub === "off" || sub === "disable") {
1096
+ ctx.config.setByPath("routing.enabled", "false");
1097
+ ctx.renderer.printSuccess("Smart model routing disabled.");
1098
+ return;
1099
+ }
1100
+ if (sub === "test") {
1101
+ const msg = args.slice(1).join(" ").trim();
1102
+ if (!msg) {
1103
+ console.log(theme.warning(" Usage: /route test <message>"));
1104
+ return;
1105
+ }
1106
+ const decision = ctx.computeRoutingDecision(msg);
1107
+ const marker = decision.overridden ? theme.accent("\u2192 ROUTED") : theme.dim("(unchanged)");
1108
+ console.log();
1109
+ console.log(` Input: ${theme.dim(msg)}`);
1110
+ console.log(` Current: ${theme.info(ctx.getCurrentModel())}`);
1111
+ console.log(` Decision: ${theme.info(decision.model)} ${marker}`);
1112
+ console.log(` Reason: ${theme.dim(decision.reason)}`);
1113
+ if (typeof decision.ruleIdx === "number") {
1114
+ console.log(` Rule: #${decision.ruleIdx}`);
1115
+ }
1116
+ console.log();
1117
+ return;
1118
+ }
1119
+ if (sub === "show" || sub === "status") {
1120
+ console.log();
1121
+ console.log(` ${theme.heading("Smart Model Routing")}`);
1122
+ console.log(` Status: ${routing?.enabled ? theme.success("enabled") : theme.dim("disabled")}`);
1123
+ console.log(` Provider: ${theme.info(ctx.getCurrentProvider())}`);
1124
+ console.log(` Current: ${theme.info(ctx.getCurrentModel())}`);
1125
+ if (routing?.fallback) {
1126
+ console.log(` Fallback: ${theme.info(routing.fallback)}`);
1127
+ }
1128
+ console.log();
1129
+ if (!routing || routing.rules.length === 0) {
1130
+ console.log(` ${theme.dim("(no rules configured \u2014 edit ~/.aicli/config.json `routing.rules`)")}`);
1131
+ } else {
1132
+ console.log(` ${theme.heading("Rules")} ${theme.dim(`(evaluated top-to-bottom)`)}:`);
1133
+ routing.rules.forEach((r, i) => {
1134
+ const parts = [];
1135
+ if (r.match.tag) parts.push(`tag=#${r.match.tag}`);
1136
+ if (r.match.contains && r.match.contains.length > 0) parts.push(`contains=[${r.match.contains.slice(0, 3).join(", ")}${r.match.contains.length > 3 ? ", \u2026" : ""}]`);
1137
+ if (typeof r.match.maxLength === "number") parts.push(`maxLen=${r.match.maxLength}`);
1138
+ if (typeof r.match.minLength === "number") parts.push(`minLen=${r.match.minLength}`);
1139
+ const cond = parts.length > 0 ? parts.join(" & ") : theme.warning("(empty \u2014 never matches)");
1140
+ console.log(` ${theme.dim(`#${i}`)} ${r.name ? theme.accent(r.name) + " " : ""}${cond} ${theme.dim("\u2192")} ${theme.info(r.model)}`);
1141
+ });
1142
+ }
1143
+ console.log();
1144
+ console.log(` ${theme.dim("Commands: /route on | off | test <msg> | show")}`);
1145
+ console.log();
1146
+ return;
1147
+ }
1148
+ console.log(theme.warning(` Unknown subcommand: ${sub}. Usage: /route [on|off|show|test <message>]`));
1149
+ }
1150
+ },
1077
1151
  {
1078
1152
  name: "clear",
1079
1153
  description: "Clear conversation history",
@@ -1123,18 +1197,18 @@ function createDefaultCommands() {
1123
1197
  return;
1124
1198
  }
1125
1199
  const sessions = ctx.sessions.listSessions();
1126
- const matches = sessions.filter((s) => s.id.startsWith(id));
1127
- if (matches.length === 0) {
1200
+ const matches2 = sessions.filter((s) => s.id.startsWith(id));
1201
+ if (matches2.length === 0) {
1128
1202
  ctx.renderer.renderError(`Session '${id}' not found.`);
1129
1203
  return;
1130
1204
  }
1131
- if (matches.length > 1) {
1132
- console.log(theme.warning(` \u26A0 Ambiguous prefix '${id}' matches ${matches.length} sessions \u2014 loading most recent:`));
1133
- for (const m of matches.slice(0, 5)) {
1205
+ if (matches2.length > 1) {
1206
+ console.log(theme.warning(` \u26A0 Ambiguous prefix '${id}' matches ${matches2.length} sessions \u2014 loading most recent:`));
1207
+ for (const m of matches2.slice(0, 5)) {
1134
1208
  console.log(theme.dim(` ${m.id.slice(0, 12)} ${m.title ?? "(untitled)"}`));
1135
1209
  }
1136
1210
  }
1137
- const match = matches[0];
1211
+ const match = matches2[0];
1138
1212
  ctx.sessions.loadSession(match.id);
1139
1213
  ctx.setProvider(match.provider, match.model);
1140
1214
  ctx.resetSessionTokenUsage();
@@ -1372,13 +1446,13 @@ ${text}
1372
1446
  ${theme.heading(`Found ${results.length} session(s) containing "${query}"`)}
1373
1447
  `);
1374
1448
  for (const r of results) {
1375
- const { sessionMeta, matches } = r;
1449
+ const { sessionMeta, matches: matches2 } = r;
1376
1450
  const dateStr = sessionMeta.updated.toLocaleDateString();
1377
1451
  console.log(
1378
1452
  ` ${theme.accent(sessionMeta.id.slice(0, 8))}` + theme.dim(` [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`) + (sessionMeta.title ? `
1379
1453
  ${theme.dim(" " + sessionMeta.title)}` : "")
1380
1454
  );
1381
- for (const m of matches) {
1455
+ for (const m of matches2) {
1382
1456
  const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
1383
1457
  console.log(` ${icon} ${theme.warning(m.snippet)}`);
1384
1458
  }
@@ -2193,7 +2267,7 @@ ${hint}` : "")
2193
2267
  usage: "/test [command|filter]",
2194
2268
  async execute(args, ctx) {
2195
2269
  try {
2196
- const { executeTests } = await import("./run-tests-47TTKJTB.js");
2270
+ const { executeTests } = await import("./run-tests-WD53PYVA.js");
2197
2271
  const argStr = args.join(" ").trim();
2198
2272
  let testArgs = {};
2199
2273
  if (argStr) {
@@ -3273,7 +3347,7 @@ var CostTracker = class {
3273
3347
  if (existsSync4(this.filePath)) {
3274
3348
  const data = JSON.parse(readFileSync3(this.filePath, "utf-8"));
3275
3349
  if (data.version === 1 && Array.isArray(data.records)) {
3276
- this.records = data.records;
3350
+ this.records = [...data.records].sort((a, b) => a.date.localeCompare(b.date));
3277
3351
  }
3278
3352
  }
3279
3353
  } catch {
@@ -3393,6 +3467,74 @@ var CostTracker = class {
3393
3467
  }
3394
3468
  };
3395
3469
 
3470
+ // src/core/model-router.ts
3471
+ var TAG_REGEX = /(?:^|\s)#([a-zA-Z][\w-]{0,31})\b/g;
3472
+ function extractTags(message) {
3473
+ const tags = /* @__PURE__ */ new Set();
3474
+ let m;
3475
+ TAG_REGEX.lastIndex = 0;
3476
+ while ((m = TAG_REGEX.exec(message)) !== null) {
3477
+ tags.add(m[1].toLowerCase());
3478
+ }
3479
+ return tags;
3480
+ }
3481
+ function matches(message, matcher) {
3482
+ const trimmed = message.trim();
3483
+ const lower = trimmed.toLowerCase();
3484
+ if (matcher.tag) {
3485
+ const tags = extractTags(trimmed);
3486
+ if (!tags.has(matcher.tag.toLowerCase())) return false;
3487
+ }
3488
+ if (matcher.contains && matcher.contains.length > 0) {
3489
+ const hit = matcher.contains.some((kw) => lower.includes(kw.toLowerCase()));
3490
+ if (!hit) return false;
3491
+ }
3492
+ if (typeof matcher.maxLength === "number") {
3493
+ if (trimmed.length > matcher.maxLength) return false;
3494
+ }
3495
+ if (typeof matcher.minLength === "number") {
3496
+ if (trimmed.length < matcher.minLength) return false;
3497
+ }
3498
+ const hasAnyCondition = !!matcher.tag || matcher.contains && matcher.contains.length > 0 || typeof matcher.maxLength === "number" || typeof matcher.minLength === "number";
3499
+ return !!hasAnyCondition;
3500
+ }
3501
+ function pickModel(message, currentModel, config, availableModels = []) {
3502
+ if (!config.enabled || config.rules.length === 0) {
3503
+ return { model: currentModel, reason: "routing disabled", overridden: false };
3504
+ }
3505
+ const isAvailable = (m) => availableModels.length === 0 || availableModels.includes(m);
3506
+ for (let i = 0; i < config.rules.length; i++) {
3507
+ const rule = config.rules[i];
3508
+ if (!matches(message, rule.match)) continue;
3509
+ if (!isAvailable(rule.model)) continue;
3510
+ if (rule.model === currentModel) {
3511
+ return {
3512
+ model: currentModel,
3513
+ reason: `rule "${rule.name ?? `#${i}`}" matched (same as current)`,
3514
+ overridden: false,
3515
+ ruleIdx: i
3516
+ };
3517
+ }
3518
+ return {
3519
+ model: rule.model,
3520
+ reason: `rule "${rule.name ?? `#${i}`}" matched`,
3521
+ overridden: true,
3522
+ ruleIdx: i
3523
+ };
3524
+ }
3525
+ if (config.fallback && config.fallback !== currentModel && isAvailable(config.fallback)) {
3526
+ return {
3527
+ model: config.fallback,
3528
+ reason: "fallback",
3529
+ overridden: true
3530
+ };
3531
+ }
3532
+ return { model: currentModel, reason: "no rule matched", overridden: false };
3533
+ }
3534
+ function stripRoutingTags(message) {
3535
+ return message.replace(/(?:^|\s)#(fast|deep|default)\b/gi, " ").replace(/\s{2,}/g, " ").trim();
3536
+ }
3537
+
3396
3538
  // src/repl/notify.ts
3397
3539
  import { spawn } from "child_process";
3398
3540
  import { platform as platform2 } from "os";
@@ -3569,13 +3711,15 @@ var Repl = class {
3569
3711
  contextLayers = [];
3570
3712
  /** 本次会话累计 token 用量 */
3571
3713
  sessionTokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
3572
- /** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage + cost tracker. */
3573
- addSessionUsage(u) {
3714
+ /** Fold a single-request TokenUsage (with optional cache fields) into sessionTokenUsage + cost tracker.
3715
+ * modelOverride lets the smart router attribute cost to the actually-used model
3716
+ * when it differs from the UI-selected currentModel. */
3717
+ addSessionUsage(u, modelOverride) {
3574
3718
  this.sessionTokenUsage.inputTokens += u.inputTokens;
3575
3719
  this.sessionTokenUsage.outputTokens += u.outputTokens;
3576
3720
  this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
3577
3721
  this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
3578
- this.costTracker.addCost(this.currentProvider, this.currentModel, u);
3722
+ this.costTracker.addCost(this.currentProvider, modelOverride ?? this.currentModel, u);
3579
3723
  }
3580
3724
  /** 启动时检测到的 Git 分支(无 git 仓库时为 null) */
3581
3725
  gitBranch = null;
@@ -4388,7 +4532,7 @@ Session '${this.resumeSessionId}' not found.
4388
4532
  }
4389
4533
  processing = true;
4390
4534
  this.rl.pause();
4391
- const rlAny = this.rl;
4535
+ const rlAny = rlInternal(this.rl);
4392
4536
  const savedOutput = rlAny.output;
4393
4537
  rlAny.output = null;
4394
4538
  try {
@@ -4403,10 +4547,10 @@ Session '${this.resumeSessionId}' not found.
4403
4547
  processing = false;
4404
4548
  if (this.running) {
4405
4549
  rlAny.output = savedOutput;
4406
- const rlInternal = this.rl;
4407
- rlInternal.line = "";
4408
- rlInternal.cursor = 0;
4409
- rlInternal.paused = false;
4550
+ const rlInternal2 = this.rl;
4551
+ rlInternal2.line = "";
4552
+ rlInternal2.cursor = 0;
4553
+ rlInternal2.paused = false;
4410
4554
  process.stdin.resume();
4411
4555
  this.showPrompt();
4412
4556
  } else {
@@ -4467,7 +4611,16 @@ Session '${this.resumeSessionId}' not found.
4467
4611
  `));
4468
4612
  }
4469
4613
  }
4470
- const messageContent = parts.length > 0 ? parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts : userInput;
4614
+ const routingDecision = this.computeRoutingDecision(userInput);
4615
+ const cleanInput = stripRoutingTags(userInput);
4616
+ let effectiveParts = parts;
4617
+ if (cleanInput !== userInput && parts.length > 0 && parts[0].type === "text") {
4618
+ effectiveParts = [
4619
+ { type: "text", text: stripRoutingTags(parts[0].text ?? "") },
4620
+ ...parts.slice(1)
4621
+ ];
4622
+ }
4623
+ const messageContent = effectiveParts.length > 0 ? effectiveParts.length === 1 && effectiveParts[0].type === "text" ? effectiveParts[0].text : effectiveParts : cleanInput;
4471
4624
  if (hasImage) {
4472
4625
  const visionHint = this.getVisionModelHint();
4473
4626
  if (visionHint) {
@@ -4488,6 +4641,12 @@ Session '${this.resumeSessionId}' not found.
4488
4641
  timestamp: /* @__PURE__ */ new Date()
4489
4642
  });
4490
4643
  this.events.emit("message.before", { input: userInput });
4644
+ if (routingDecision.overridden) {
4645
+ process.stdout.write(
4646
+ theme.dim(` \u2192 Routed to ${routingDecision.model} (${routingDecision.reason})
4647
+ `)
4648
+ );
4649
+ }
4491
4650
  const t0 = Date.now();
4492
4651
  try {
4493
4652
  const provider = this.providers.get(this.currentProvider);
@@ -4495,10 +4654,11 @@ Session '${this.resumeSessionId}' not found.
4495
4654
  if (supportsTools) {
4496
4655
  await this.handleChatWithTools(
4497
4656
  provider,
4498
- session.messages
4657
+ session.messages,
4658
+ routingDecision.model
4499
4659
  );
4500
4660
  } else {
4501
- await this.handleChatSimple(provider, session.messages);
4661
+ await this.handleChatSimple(provider, session.messages, routingDecision.model);
4502
4662
  }
4503
4663
  if (this.config.get("session").autoSave) {
4504
4664
  if (autoTrimSessionIfNeeded(session)) {
@@ -4576,9 +4736,10 @@ Session '${this.resumeSessionId}' not found.
4576
4736
  */
4577
4737
  /** 运行时 thinking 模式覆盖:null=使用配置值,true/false=运行时覆盖 */
4578
4738
  runtimeThinking = null;
4579
- getModelParams() {
4739
+ getModelParams(modelOverride) {
4580
4740
  const allParams = this.config.get("modelParams");
4581
- const params = allParams[this.currentModel] ?? {};
4741
+ const modelId = modelOverride ?? this.currentModel;
4742
+ const params = allParams[modelId] ?? {};
4582
4743
  return {
4583
4744
  ...params,
4584
4745
  maxTokens: params.maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -4586,6 +4747,25 @@ Session '${this.resumeSessionId}' not found.
4586
4747
  thinkingBudget: params.thinkingBudget
4587
4748
  };
4588
4749
  }
4750
+ /**
4751
+ * Compute smart-routing decision for this user turn.
4752
+ * Only considers models available for the current provider (rule skipped otherwise).
4753
+ * When routing is disabled or no rule matches, returns the current model unchanged.
4754
+ */
4755
+ computeRoutingDecision(userInput) {
4756
+ const routingConfig = this.config.get("routing");
4757
+ if (!routingConfig || !routingConfig.enabled) {
4758
+ return { model: this.currentModel, reason: "routing disabled", overridden: false };
4759
+ }
4760
+ let availableModels = [];
4761
+ try {
4762
+ const provider = this.providers.get(this.currentProvider);
4763
+ availableModels = provider.info.models.map((m) => m.id);
4764
+ } catch {
4765
+ availableModels = [];
4766
+ }
4767
+ return pickModel(userInput, this.currentModel, routingConfig, availableModels);
4768
+ }
4589
4769
  // ─── Context 自动管理 ───────────────────────────────────────────────────
4590
4770
  /**
4591
4771
  * 估算文本的 token 数。
@@ -4638,12 +4818,15 @@ Session '${this.resumeSessionId}' not found.
4638
4818
  return total;
4639
4819
  }
4640
4820
  /**
4641
- * 获取当前模型的 context window 大小。
4821
+ * 获取指定模型的 context window 大小(默认当前模型)。
4822
+ * 智能路由可能在 handleChatWithTools 内把 effectiveModel 暂时切到别的模型,
4823
+ * 故此处接受可选的 modelOverride 以保持计算一致性。
4642
4824
  */
4643
- getContextWindowSize() {
4825
+ getContextWindowSize(modelOverride) {
4644
4826
  try {
4645
4827
  const provider = this.providers.get(this.currentProvider);
4646
- const modelInfo = provider.info.models.find((m) => m.id === this.currentModel);
4828
+ const modelId = modelOverride ?? this.currentModel;
4829
+ const modelInfo = provider.info.models.find((m) => m.id === modelId);
4647
4830
  return modelInfo?.contextWindow ?? 0;
4648
4831
  } catch {
4649
4832
  return 0;
@@ -4880,16 +5063,17 @@ Session '${this.resumeSessionId}' not found.
4880
5063
  }
4881
5064
  });
4882
5065
  }
4883
- async handleChatSimple(provider, messages) {
5066
+ async handleChatSimple(provider, messages, modelOverride) {
4884
5067
  const session = this.sessions.current;
4885
5068
  const useStreaming = this.config.get("ui").streaming;
4886
- const modelParams = this.getModelParams();
5069
+ const effectiveModel = modelOverride ?? this.currentModel;
5070
+ const modelParams = this.getModelParams(effectiveModel);
4887
5071
  if (useStreaming) {
4888
5072
  const ac = this.setupStreamInterrupt();
4889
5073
  try {
4890
5074
  const stream = provider.chatStream({
4891
5075
  messages,
4892
- model: this.currentModel,
5076
+ model: effectiveModel,
4893
5077
  systemPrompt: this.buildCurrentSystemPrompt(),
4894
5078
  stream: true,
4895
5079
  temperature: modelParams.temperature,
@@ -4909,7 +5093,7 @@ Session '${this.resumeSessionId}' not found.
4909
5093
  session.addMessage({ role: "assistant", content, timestamp: /* @__PURE__ */ new Date() });
4910
5094
  this.events.emit("message.after", { content });
4911
5095
  if (usage) {
4912
- this.addSessionUsage(usage);
5096
+ this.addSessionUsage(usage, effectiveModel);
4913
5097
  session.addTokenUsage(usage);
4914
5098
  if (showTokens && !tokensShown) {
4915
5099
  this.renderer.renderUsage(usage, this.sessionTokenUsage);
@@ -4923,7 +5107,7 @@ Session '${this.resumeSessionId}' not found.
4923
5107
  try {
4924
5108
  const response = await provider.chat({
4925
5109
  messages,
4926
- model: this.currentModel,
5110
+ model: effectiveModel,
4927
5111
  systemPrompt: this.buildCurrentSystemPrompt(),
4928
5112
  stream: false,
4929
5113
  temperature: modelParams.temperature,
@@ -4938,7 +5122,7 @@ Session '${this.resumeSessionId}' not found.
4938
5122
  session.addMessage({ role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() });
4939
5123
  this.events.emit("message.after", { content: response.content });
4940
5124
  if (response.usage) {
4941
- this.addSessionUsage(response.usage);
5125
+ this.addSessionUsage(response.usage, effectiveModel);
4942
5126
  session.addTokenUsage(response.usage);
4943
5127
  if (this.shouldShowTokens()) {
4944
5128
  this.renderer.renderUsage(response.usage, this.sessionTokenUsage);
@@ -5051,8 +5235,9 @@ Session '${this.resumeSessionId}' not found.
5051
5235
  rawContent
5052
5236
  };
5053
5237
  }
5054
- async handleChatWithTools(provider, messages) {
5238
+ async handleChatWithTools(provider, messages, modelOverride) {
5055
5239
  const session = this.sessions.current;
5240
+ const effectiveModel = modelOverride ?? this.currentModel;
5056
5241
  let toolDefs;
5057
5242
  let mcpBudgetNote = null;
5058
5243
  const usedMcpToolNames = /* @__PURE__ */ new Set();
@@ -5063,7 +5248,7 @@ Session '${this.resumeSessionId}' not found.
5063
5248
  if (skillFilter) {
5064
5249
  toolDefs = this.toolRegistry.getDefinitions().filter((t) => skillFilter.has(t.name));
5065
5250
  } else {
5066
- const contextWindow = this.getContextWindowSize();
5251
+ const contextWindow = this.getContextWindowSize(effectiveModel);
5067
5252
  if (contextWindow > 0) {
5068
5253
  const toolBudget = Math.floor(contextWindow * 0.2);
5069
5254
  const { definitions, trimmedCount, systemNote } = this.toolRegistry.getDefinitionsWithBudget(toolBudget, usedMcpToolNames);
@@ -5113,7 +5298,7 @@ You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan effi
5113
5298
  const systemPrompt = baseSystemPrompt + roundBudgetHint + (mcpBudgetNote ? `
5114
5299
 
5115
5300
  ${mcpBudgetNote}` : "");
5116
- const modelParams = this.getModelParams();
5301
+ const modelParams = this.getModelParams(effectiveModel);
5117
5302
  const useStreaming = this.config.get("ui").streaming;
5118
5303
  const spinner = this.renderer.showSpinner("Thinking...");
5119
5304
  const roundUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
@@ -5203,7 +5388,7 @@ ${mcpBudgetNote}` : "");
5203
5388
  )
5204
5389
  );
5205
5390
  if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
5206
- this.addSessionUsage(roundUsage);
5391
+ this.addSessionUsage(roundUsage, effectiveModel);
5207
5392
  session.addTokenUsage(roundUsage);
5208
5393
  if (this.shouldShowTokens()) {
5209
5394
  this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
@@ -5231,7 +5416,7 @@ ${mcpBudgetNote}` : "");
5231
5416
  let alreadyRendered = false;
5232
5417
  const chatRequest = {
5233
5418
  messages: apiMessages,
5234
- model: this.currentModel,
5419
+ model: effectiveModel,
5235
5420
  systemPrompt,
5236
5421
  stream: false,
5237
5422
  temperature: modelParams.temperature,
@@ -5333,7 +5518,7 @@ ${mcpBudgetNote}` : "");
5333
5518
  )
5334
5519
  );
5335
5520
  if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
5336
- this.addSessionUsage(roundUsage);
5521
+ this.addSessionUsage(roundUsage, effectiveModel);
5337
5522
  session.addTokenUsage(roundUsage);
5338
5523
  if (this.shouldShowTokens()) {
5339
5524
  this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
@@ -5368,7 +5553,7 @@ ${mcpBudgetNote}` : "");
5368
5553
  });
5369
5554
  this.events.emit("message.after", { content: finalContent });
5370
5555
  if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
5371
- this.addSessionUsage(roundUsage);
5556
+ this.addSessionUsage(roundUsage, effectiveModel);
5372
5557
  session.addTokenUsage(roundUsage);
5373
5558
  if (this.shouldShowTokens()) {
5374
5559
  this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
@@ -5386,7 +5571,7 @@ ${mcpBudgetNote}` : "");
5386
5571
  try {
5387
5572
  const genStream = provider.chatStream({
5388
5573
  messages: apiMessages,
5389
- model: this.currentModel,
5574
+ model: effectiveModel,
5390
5575
  systemPrompt,
5391
5576
  stream: true,
5392
5577
  temperature: modelParams.temperature,
@@ -5422,7 +5607,7 @@ ${mcpBudgetNote}` : "");
5422
5607
  const newMsgs2 = provider.buildToolResultMessages(result.toolCalls, syntheticResults, reasoningContent2);
5423
5608
  extraMessages.push(...newMsgs2);
5424
5609
  if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
5425
- this.addSessionUsage(roundUsage);
5610
+ this.addSessionUsage(roundUsage, effectiveModel);
5426
5611
  session.addTokenUsage(roundUsage);
5427
5612
  if (teeShowTokens && !teeTokShown) {
5428
5613
  this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
@@ -5437,14 +5622,14 @@ ${mcpBudgetNote}` : "");
5437
5622
  askUserContext.rl = this.rl;
5438
5623
  googleSearchContext.configManager = this.config;
5439
5624
  streamToFileContext.provider = provider;
5440
- streamToFileContext.model = this.currentModel;
5625
+ streamToFileContext.model = effectiveModel;
5441
5626
  streamToFileContext.systemPrompt = systemPrompt;
5442
5627
  streamToFileContext.messages = apiMessages;
5443
5628
  streamToFileContext.extraMessages = extraMessages;
5444
5629
  streamToFileContext.temperature = modelParams.temperature;
5445
5630
  streamToFileContext.timeout = modelParams.timeout;
5446
5631
  spawnAgentContext.provider = provider;
5447
- spawnAgentContext.model = this.currentModel;
5632
+ spawnAgentContext.model = effectiveModel;
5448
5633
  spawnAgentContext.systemPrompt = systemPrompt;
5449
5634
  spawnAgentContext.modelParams = modelParams;
5450
5635
  spawnAgentContext.configManager = this.config;
@@ -5559,7 +5744,7 @@ ${mcpBudgetNote}` : "");
5559
5744
  process.stdout.write(theme.dim(" (Press ") + theme.info("n") + theme.dim(" or ") + theme.info("Esc") + theme.dim(" to stop)\n"));
5560
5745
  this.teardownInterjectionListener();
5561
5746
  const pauseResponse = await new Promise((resolve3) => {
5562
- const rlWithOutput = this.rl;
5747
+ const rlWithOutput = rlInternal(this.rl);
5563
5748
  const savedOutput = rlWithOutput.output;
5564
5749
  rlWithOutput.output = process.stdout;
5565
5750
  this.rl.question(theme.warning(" \u25B8 "), (answer) => {
@@ -5604,7 +5789,7 @@ ${mcpBudgetNote}` : "");
5604
5789
  const summaryResult = await provider.chatWithTools(
5605
5790
  {
5606
5791
  messages: apiMessages,
5607
- model: this.currentModel,
5792
+ model: effectiveModel,
5608
5793
  systemPrompt,
5609
5794
  stream: false,
5610
5795
  temperature: modelParams.temperature,
@@ -5642,7 +5827,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5642
5827
  );
5643
5828
  }
5644
5829
  if (roundUsage.inputTokens > 0 || roundUsage.outputTokens > 0) {
5645
- this.addSessionUsage(roundUsage);
5830
+ this.addSessionUsage(roundUsage, effectiveModel);
5646
5831
  session.addTokenUsage(roundUsage);
5647
5832
  if (this.shouldShowTokens()) {
5648
5833
  this.renderer.renderUsage(roundUsage, this.sessionTokenUsage);
@@ -5702,9 +5887,9 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5702
5887
  select: (prompt, items, initialIndex) => {
5703
5888
  this.selecting = true;
5704
5889
  return selectFromList(prompt, items, initialIndex).finally(() => {
5705
- const rlInternal = this.rl;
5706
- rlInternal.line = "";
5707
- rlInternal.cursor = 0;
5890
+ const rlInternal2 = this.rl;
5891
+ rlInternal2.line = "";
5892
+ rlInternal2.cursor = 0;
5708
5893
  process.stdin.pause();
5709
5894
  setImmediate(() => {
5710
5895
  this.selecting = false;
@@ -5749,7 +5934,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5749
5934
  getGitBranch: () => this.gitBranch,
5750
5935
  getLastResponse: () => lastResponseStore.content,
5751
5936
  runSetupWizard: async () => {
5752
- const rlAny = this.rl;
5937
+ const rlAny = rlInternal(this.rl);
5753
5938
  rlAny.output = process.stdout;
5754
5939
  process.stdin.resume();
5755
5940
  try {
@@ -5818,6 +6003,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
5818
6003
  forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
5819
6004
  getToolExecutor: () => this.toolExecutor,
5820
6005
  getCostTracker: () => this.costTracker,
6006
+ computeRoutingDecision: (userInput) => this.computeRoutingDecision(userInput),
5821
6007
  exit: () => this.handleExit()
5822
6008
  };
5823
6009
  await cmd.execute(args, ctx);
@@ -5922,7 +6108,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5922
6108
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5923
6109
  process.exit(1);
5924
6110
  }
5925
- const { startWebServer } = await import("./server-A3SB52SS.js");
6111
+ const { startWebServer } = await import("./server-MDBQX5UZ.js");
5926
6112
  await startWebServer({ port, host: options.host });
5927
6113
  });
5928
6114
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -6155,7 +6341,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
6155
6341
  }),
6156
6342
  config.get("customProviders")
6157
6343
  );
6158
- const { startHub } = await import("./hub-JA7HH44N.js");
6344
+ const { startHub } = await import("./hub-4VPTOMBP.js");
6159
6345
  await startHub(
6160
6346
  {
6161
6347
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-UIFW7G4A.js";
4
+ } from "./chunk-VO5IZN2C.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-NIJZBQ6I.js";
5
+ } from "./chunk-3LCVJ4AF.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-HOBLE365.js";
24
+ } from "./chunk-G5AISHJE.js";
25
25
  import {
26
26
  AuthManager
27
27
  } from "./chunk-BYNY5JPB.js";
@@ -33,15 +33,16 @@ import {
33
33
  estimateTokens,
34
34
  getDangerLevel,
35
35
  googleSearchContext,
36
- isFileWriteTool,
36
+ groupCallsByPhase,
37
37
  renderDiff,
38
38
  runHook,
39
+ runSafePhases,
39
40
  setContextWindow,
40
41
  setMaxOutputCap,
41
42
  spawnAgentContext,
42
43
  truncateOutput,
43
44
  undoStack
44
- } from "./chunk-X5RX2VGQ.js";
45
+ } from "./chunk-Q5QSCO5D.js";
45
46
  import "./chunk-4BKXL7SM.js";
46
47
  import {
47
48
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -61,7 +62,7 @@ import {
61
62
  SKILLS_DIR_NAME,
62
63
  VERSION,
63
64
  buildUserIdentityPrompt
64
- } from "./chunk-NIJZBQ6I.js";
65
+ } from "./chunk-3LCVJ4AF.js";
65
66
 
66
67
  // src/web/server.ts
67
68
  import express from "express";
@@ -325,52 +326,19 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
325
326
  }
326
327
  }
327
328
  async executeAll(calls) {
328
- const safeParallel = [];
329
- const safeBash = [];
330
- const fileWriteCalls = [];
331
- const otherCalls = [];
332
- for (let i = 0; i < calls.length; i++) {
333
- const call = calls[i];
334
- const level = getDangerLevel(call.name, call.arguments);
335
- if (level === "safe") {
336
- if (call.name === "bash") {
337
- safeBash.push({ idx: i, call });
338
- } else {
339
- safeParallel.push({ idx: i, call });
340
- }
341
- } else if (isFileWriteTool(call.name) && level === "write") {
342
- fileWriteCalls.push({ idx: i, call });
343
- } else {
344
- otherCalls.push({ idx: i, call });
345
- }
346
- }
329
+ const phase = groupCallsByPhase(calls);
347
330
  const results = new Array(calls.length);
348
- const wrapExec = async (idx, call) => {
349
- try {
350
- results[idx] = await this.execute(call);
351
- } catch (err) {
352
- const msg = err instanceof Error ? err.message : String(err);
353
- console.error(`[tool-executor-web] Unexpected error in tool "${call.name}":`, err);
354
- results[idx] = { callId: call.id, content: `Tool execution failed: ${msg}`, isError: true };
355
- }
356
- };
357
- const parallelPhase = safeParallel.length > 0 ? Promise.all(safeParallel.map(({ idx, call }) => wrapExec(idx, call))) : Promise.resolve();
358
- const bashPhase = (async () => {
359
- for (const { idx, call } of safeBash) {
360
- await wrapExec(idx, call);
361
- }
362
- })();
363
- await Promise.all([parallelPhase, bashPhase]);
364
- if (fileWriteCalls.length === 1) {
365
- const { idx, call } = fileWriteCalls[0];
331
+ await runSafePhases(phase, (c) => this.execute(c), results, "tool-executor-web");
332
+ if (phase.fileWriteCalls.length === 1) {
333
+ const { idx, call } = phase.fileWriteCalls[0];
366
334
  results[idx] = await this.execute(call);
367
- } else if (fileWriteCalls.length >= 2) {
368
- const batchResult = await this.executeBatchFileWrites(fileWriteCalls);
369
- for (let i = 0; i < fileWriteCalls.length; i++) {
370
- results[fileWriteCalls[i].idx] = batchResult[i];
335
+ } else if (phase.fileWriteCalls.length >= 2) {
336
+ const batchResult = await this.executeBatchFileWrites(phase.fileWriteCalls);
337
+ for (let i = 0; i < phase.fileWriteCalls.length; i++) {
338
+ results[phase.fileWriteCalls[i].idx] = batchResult[i];
371
339
  }
372
340
  }
373
- for (const { idx, call } of otherCalls) {
341
+ for (const { idx, call } of phase.otherCalls) {
374
342
  results[idx] = await this.execute(call);
375
343
  }
376
344
  return results;
@@ -391,6 +359,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
391
359
  };
392
360
  }
393
361
  }
362
+ if (approvedIndices.length >= 2) {
363
+ const labels = approvedIndices.map((i) => `[${i + 1}]`).join(" ");
364
+ this.send({
365
+ type: "info",
366
+ message: `\u26A1 Writing ${labels} in parallel (results may interleave)`
367
+ });
368
+ }
394
369
  await Promise.all(
395
370
  approvedIndices.map(async (i) => {
396
371
  const call = calls[i];
@@ -1971,7 +1946,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1971
1946
  case "test": {
1972
1947
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1973
1948
  try {
1974
- const { executeTests } = await import("./run-tests-47TTKJTB.js");
1949
+ const { executeTests } = await import("./run-tests-WD53PYVA.js");
1975
1950
  const argStr = args.join(" ").trim();
1976
1951
  let testArgs = {};
1977
1952
  if (argStr) {
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-X5RX2VGQ.js";
7
+ } from "./chunk-Q5QSCO5D.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-NIJZBQ6I.js";
11
+ } from "./chunk-3LCVJ4AF.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.66",
3
+ "version": "0.4.68",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",