jinzd-ai-cli 0.4.276 → 0.4.278

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.
Files changed (39) hide show
  1. package/dist/{batch-F7SKIBI3.js → batch-7Y4NAA6N.js} +2 -2
  2. package/dist/{chunk-VF7DMSKF.js → chunk-2BYQP6JX.js} +1 -1
  3. package/dist/{chunk-GLSR2NR3.js → chunk-4DEMGCK4.js} +4 -3
  4. package/dist/{chunk-MXPUOF5K.js → chunk-4Z6GAYB5.js} +4 -4
  5. package/dist/{chunk-NXAHXWPF.js → chunk-H7QC5VMQ.js} +1 -1
  6. package/dist/{chunk-MCKUGWOY.js → chunk-JJCNNQTU.js} +183 -3
  7. package/dist/{chunk-AOISQVXS.js → chunk-L3CPM472.js} +1 -1
  8. package/dist/{chunk-ATD2Y3PB.js → chunk-LOXEYWC3.js} +1 -1
  9. package/dist/{chunk-WYDP2WEQ.js → chunk-LTZFUYBT.js} +4 -4
  10. package/dist/{chunk-4NH4Q7QK.js → chunk-LXXQTWT2.js} +1 -1
  11. package/dist/{chunk-QCG6UU2S.js → chunk-PPIISCFN.js} +4 -3
  12. package/dist/{chunk-KWSGSSWN.js → chunk-RFSKPCYC.js} +1 -1
  13. package/dist/{chunk-OMBKLWLJ.js → chunk-SB4XONS4.js} +1 -1
  14. package/dist/{chunk-UNL7NR55.js → chunk-SJCOE4C7.js} +1 -1
  15. package/dist/{chunk-4KTK2OYU.js → chunk-UGY5SVNX.js} +46 -20
  16. package/dist/{chunk-NXNXWM6H.js → chunk-WOXIKQRI.js} +16 -7
  17. package/dist/{chunk-FLDYAXJR.js → chunk-X3722RFA.js} +1 -1
  18. package/dist/{ci-VURFRMWQ.js → ci-WDF2CYR4.js} +5 -5
  19. package/dist/{ci-format-AFJJIURF.js → ci-format-7B5VAZGA.js} +2 -2
  20. package/dist/{constants-WO7ERENL.js → constants-DC3TNLTL.js} +1 -1
  21. package/dist/{doctor-cli-BGGILTHE.js → doctor-cli-CDDG5XOY.js} +5 -5
  22. package/dist/electron-server.js +378 -78
  23. package/dist/{hub-Z73RLNJI.js → hub-TO7ODOIA.js} +1 -1
  24. package/dist/index.js +175 -188
  25. package/dist/{indexer-M2LJGTY2.js → indexer-RBEKMFT4.js} +1 -1
  26. package/dist/{indexer-VZETNE4H.js → indexer-TNDWGFRI.js} +1 -1
  27. package/dist/{persistent-memory-5QMARQJF.js → persistent-memory-BU63CSGK.js} +2 -2
  28. package/dist/{persistent-memory-CI544RLT.js → persistent-memory-JCENBMCJ.js} +2 -2
  29. package/dist/{pr-24REDMGV.js → pr-GVSZYX6I.js} +5 -5
  30. package/dist/{run-tests-72VNZXNT.js → run-tests-2Q5MJHXB.js} +2 -2
  31. package/dist/{run-tests-JJ3KOY5E.js → run-tests-R4IQS6IF.js} +2 -2
  32. package/dist/{server-JMXC2N3F.js → server-KAURBKFI.js} +167 -62
  33. package/dist/{server-QJHCJDA7.js → server-WXLE53HG.js} +6 -6
  34. package/dist/{task-orchestrator-DJN36HP5.js → task-orchestrator-RJ6FXYY5.js} +8 -8
  35. package/dist/{usage-HXJVDETY.js → usage-V3R7DA7M.js} +2 -2
  36. package/dist/web/client/app.js +4 -1
  37. package/dist/web/client/app.js.br +0 -0
  38. package/dist/web/client/app.js.gz +0 -0
  39. package/package.json +1 -1
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-SKET65WZ.js";
14
14
  import {
15
15
  indexProject
16
- } from "./chunk-QCG6UU2S.js";
16
+ } from "./chunk-PPIISCFN.js";
17
17
  import {
18
18
  addMemoryEntry,
19
19
  backupMemoryStore,
@@ -40,14 +40,14 @@ import {
40
40
  touchMemoryReferences,
41
41
  updateMemoryApproval,
42
42
  updateMemoryEntry
43
- } from "./chunk-4NH4Q7QK.js";
43
+ } from "./chunk-LXXQTWT2.js";
44
44
  import {
45
45
  redactJson,
46
46
  scanString
47
47
  } from "./chunk-FSC6KEWU.js";
48
48
  import {
49
49
  runTestsTool
50
- } from "./chunk-ATD2Y3PB.js";
50
+ } from "./chunk-LOXEYWC3.js";
51
51
  import {
52
52
  AGENTIC_BEHAVIOR_GUIDELINE,
53
53
  APP_NAME,
@@ -80,7 +80,7 @@ import {
80
80
  SUBAGENT_MAX_ROUNDS_LIMIT,
81
81
  VERSION,
82
82
  buildUserIdentityPrompt
83
- } from "./chunk-UNL7NR55.js";
83
+ } from "./chunk-SJCOE4C7.js";
84
84
  import {
85
85
  hasSemanticIndex,
86
86
  semanticSearch
@@ -1155,7 +1155,8 @@ var BaseProvider = class {
1155
1155
  return this.reliableContentOnlyTee;
1156
1156
  }
1157
1157
  /**
1158
- * 该 provider 是否支持 `reasoning_effort` 档位(low/high/max)。目前只有 DeepSeek V4。
1158
+ * 该 provider 是否支持 `reasoning_effort` 档位(low/high/max)。目前是 DeepSeek V4
1159
+ * 与智谱 GLM-5 系列(v0.4.276 接入 GLM-5.3 时补上——它关不掉思考,档位是唯一的成本闸门)。
1159
1160
  *
1160
1161
  * 2026-08-14 审计 P2-03:提示符标签此前无条件渲染成 `[THINK:high]`,于是 Claude
1161
1162
  * (用 thinkingBudget 计 token 预算)和 GLM(根本没有 effort 概念)也显示一个
@@ -2136,6 +2137,16 @@ Node.js does not automatically use system proxies. Try one of the following:
2136
2137
  // src/providers/openai-compatible.ts
2137
2138
  import OpenAI from "openai";
2138
2139
 
2140
+ // src/providers/openai-extensions.ts
2141
+ function toSdkParams(params) {
2142
+ return params;
2143
+ }
2144
+ function readReasoningContent(message) {
2145
+ if (!message || typeof message !== "object") return void 0;
2146
+ const value = message.reasoning_content;
2147
+ return typeof value === "string" ? value : void 0;
2148
+ }
2149
+
2139
2150
  // src/tools/hallucination.ts
2140
2151
  var HALLUCINATION_PATTERNS = [
2141
2152
  /文件路径[::]\s*`?[^\s`]+\.\w{1,5}/,
@@ -3377,7 +3388,7 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3377
3388
  continue;
3378
3389
  }
3379
3390
  if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
3380
- const assistantMsg = {
3391
+ msgs.push({
3381
3392
  role: "assistant",
3382
3393
  content: typeof m.content === "string" && m.content ? m.content : null,
3383
3394
  tool_calls: m.toolCalls.map((tc) => ({
@@ -3386,15 +3397,22 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3386
3397
  function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
3387
3398
  })),
3388
3399
  reasoning_content: m.reasoningContent ?? ""
3389
- };
3390
- msgs.push(assistantMsg);
3400
+ });
3391
3401
  continue;
3392
3402
  }
3393
- const base = { role: m.role, content: m.content };
3394
3403
  if (m.role === "assistant") {
3395
- base.reasoning_content = m.reasoningContent ?? "";
3404
+ msgs.push({
3405
+ role: "assistant",
3406
+ content: typeof m.content === "string" ? m.content : "",
3407
+ reasoning_content: m.reasoningContent ?? ""
3408
+ });
3409
+ continue;
3410
+ }
3411
+ if (m.role === "system") {
3412
+ msgs.push({ role: "system", content: typeof m.content === "string" ? m.content : "" });
3413
+ continue;
3396
3414
  }
3397
- msgs.push(base);
3415
+ msgs.push({ role: "user", content: m.content });
3398
3416
  }
3399
3417
  const systemContent = [request.systemPrompt, request.systemPromptVolatile].filter(Boolean).join("\n\n---\n\n");
3400
3418
  if (systemContent) {
@@ -3423,12 +3441,13 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3423
3441
  */
3424
3442
  async chat(request) {
3425
3443
  try {
3426
- const response = await this.client.chat.completions.create({
3444
+ const params = {
3427
3445
  model: request.model,
3428
3446
  messages: this.buildMessages(request),
3429
3447
  ...this.buildChatRequestParams(request),
3430
3448
  stream: false
3431
- }, {
3449
+ };
3450
+ const response = await this.client.chat.completions.create(toSdkParams(params), {
3432
3451
  timeout: request.timeout ?? this.defaultTimeout,
3433
3452
  signal: request.signal
3434
3453
  });
@@ -3436,7 +3455,7 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3436
3455
  if (!firstChoice) {
3437
3456
  return { content: "", model: response.model, usage: void 0 };
3438
3457
  }
3439
- const reasoningContent = firstChoice.message.reasoning_content;
3458
+ const reasoningContent = readReasoningContent(firstChoice.message);
3440
3459
  return {
3441
3460
  content: firstChoice.message.content ?? "",
3442
3461
  model: response.model,
@@ -3449,14 +3468,15 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3449
3468
  }
3450
3469
  async *chatStream(request) {
3451
3470
  try {
3452
- const stream = await this.client.chat.completions.create({
3471
+ const params = {
3453
3472
  model: request.model,
3454
3473
  messages: this.buildMessages(request),
3455
3474
  ...this.buildChatRequestParams(request),
3456
3475
  stream: true,
3457
3476
  // 请求末尾 usage chunk,供 token 统计使用
3458
3477
  stream_options: { include_usage: true }
3459
- }, {
3478
+ };
3479
+ const stream = await this.client.chat.completions.create(toSdkParams(params), {
3460
3480
  timeout: request.timeout ?? this.defaultTimeout,
3461
3481
  signal: request.signal
3462
3482
  });
@@ -3526,14 +3546,15 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3526
3546
  const baseMessages = this.buildMessages(request);
3527
3547
  const extraMessages = request._extraMessages ?? [];
3528
3548
  const allMessages = [...baseMessages, ...extraMessages];
3529
- const response = await this.client.chat.completions.create({
3549
+ const params = {
3530
3550
  model: request.model,
3531
3551
  messages: allMessages,
3532
3552
  tools: openaiTools,
3533
3553
  tool_choice: "auto",
3534
3554
  ...this.buildChatRequestParams(request),
3535
3555
  stream: false
3536
- }, {
3556
+ };
3557
+ const response = await this.client.chat.completions.create(toSdkParams(params), {
3537
3558
  timeout: request.timeout ?? this.defaultTimeout,
3538
3559
  // 非流式路径同样必须收 signal,否则 Ctrl+C 期间整个请求不可中断(见 chat() 上方注释)
3539
3560
  signal: request.signal
@@ -3546,7 +3567,7 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3546
3567
  const finishReason = firstChoice.finish_reason;
3547
3568
  const usage = toUsage(response.usage);
3548
3569
  const hasToolCalls = !!(message.tool_calls && message.tool_calls.length > 0);
3549
- const reasoningContent = message.reasoning_content;
3570
+ const reasoningContent = readReasoningContent(message);
3550
3571
  if (message.tool_calls && message.tool_calls.length > 0) {
3551
3572
  const toolCalls = message.tool_calls.map((tc) => {
3552
3573
  const parsed = parseToolCallArguments(
@@ -3625,7 +3646,7 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3625
3646
  const extraMessages = request._extraMessages ?? [];
3626
3647
  const allMessages = [...baseMessages, ...extraMessages];
3627
3648
  try {
3628
- const stream = await this.client.chat.completions.create({
3649
+ const params = {
3629
3650
  model: request.model,
3630
3651
  messages: allMessages,
3631
3652
  tools: openaiTools,
@@ -3633,7 +3654,8 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3633
3654
  ...this.buildChatRequestParams(request),
3634
3655
  stream: true,
3635
3656
  stream_options: { include_usage: true }
3636
- }, {
3657
+ };
3658
+ const stream = await this.client.chat.completions.create(toSdkParams(params), {
3637
3659
  timeout: request.timeout ?? this.defaultTimeout,
3638
3660
  signal: request.signal
3639
3661
  });
@@ -3751,9 +3773,11 @@ var OpenAICompatibleProvider = class extends BaseProvider {
3751
3773
  name: tc.name,
3752
3774
  arguments: JSON.stringify(tc.arguments)
3753
3775
  }
3754
- }))
3776
+ })),
3777
+ // DeepSeek V4 thinking 模式严格校验:assistant 消息必须包含 reasoning_content
3778
+ // 字段(即使为空字符串也必须存在,否则 API 返回 400)
3779
+ reasoning_content: reasoningContent ?? ""
3755
3780
  };
3756
- assistantMsg.reasoning_content = reasoningContent ?? "";
3757
3781
  const resultMsgs = results.map((r) => ({
3758
3782
  role: "tool",
3759
3783
  tool_call_id: r.callId,
@@ -3966,7 +3990,7 @@ var ZhipuProvider = class extends OpenAICompatibleProvider {
3966
3990
  buildChatRequestParams(request) {
3967
3991
  const params = super.buildChatRequestParams(request);
3968
3992
  if (request.thinkingEffort && REASONING_EFFORT_MODELS.test(request.model)) {
3969
- params["reasoning_effort"] = request.thinkingEffort;
3993
+ params.reasoning_effort = request.thinkingEffort;
3970
3994
  }
3971
3995
  return params;
3972
3996
  }
@@ -8525,7 +8549,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
8525
8549
  const mode = appendMode ? "appended" : "written";
8526
8550
  void (async () => {
8527
8551
  try {
8528
- const { updateFile } = await import("./indexer-M2LJGTY2.js");
8552
+ const { updateFile } = await import("./indexer-RBEKMFT4.js");
8529
8553
  await updateFile(process.cwd(), filePath);
8530
8554
  } catch {
8531
8555
  }
@@ -10733,6 +10757,13 @@ function formatResults2(query, data, _requested) {
10733
10757
  return header + "\n" + results.join("\n\n");
10734
10758
  }
10735
10759
 
10760
+ // src/core/tool-capable-provider.ts
10761
+ function isToolCapableProvider(provider) {
10762
+ if (!provider) return false;
10763
+ const candidate = provider;
10764
+ return typeof candidate.chatWithTools === "function" && typeof candidate.buildToolResultMessages === "function";
10765
+ }
10766
+
10736
10767
  // src/agents/agent-config.ts
10737
10768
  import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
10738
10769
  import { join as join13 } from "path";
@@ -11544,7 +11575,8 @@ async function runSubAgent(task, maxRounds, agentIndex, ctx, agent) {
11544
11575
  if (!ctx.provider) {
11545
11576
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
11546
11577
  }
11547
- const providerFromConfig = agent.provider && ctx.providers?.has(agent.provider) ? ctx.providers.get(agent.provider) : void 0;
11578
+ const configured = agent.provider && ctx.providers?.has(agent.provider) ? ctx.providers.get(agent.provider) : void 0;
11579
+ const providerFromConfig = isToolCapableProvider(configured) ? configured : void 0;
11548
11580
  const provider = providerFromConfig ?? ctx.provider;
11549
11581
  const model = agent.model ?? (providerFromConfig?.info?.defaultModel ?? ctx.model);
11550
11582
  const run = startAgentRun({
@@ -15511,48 +15543,259 @@ function applyThinkCommand(sub, state2) {
15511
15543
  return stay(`Unknown /think option "${sub}" \u2014 expected on | off | low | high | max | status`);
15512
15544
  }
15513
15545
 
15546
+ // src/core/provider-command.ts
15547
+ function preservedSuffix(messageCount) {
15548
+ return messageCount > 0 ? ` (conversation preserved: ${messageCount} messages)` : "";
15549
+ }
15550
+ function resolveProviderSwitch(input) {
15551
+ const id = input.targetId;
15552
+ if (!input.configured) {
15553
+ return {
15554
+ status: "not-configured",
15555
+ message: `Provider '${id}' is not configured. Run: aicli config`
15556
+ };
15557
+ }
15558
+ if (input.availableModels.length === 0) {
15559
+ return {
15560
+ status: "no-models",
15561
+ message: `Provider '${id}' has no available models. ` + (id === "ollama" ? "Make sure Ollama is running (`ollama serve`) and has models (`ollama pull <model>`)." : "Check provider configuration.")
15562
+ };
15563
+ }
15564
+ if (id === input.currentProvider) {
15565
+ return { status: "unchanged", message: `Already using provider: ${id}` };
15566
+ }
15567
+ const model = input.configuredDefaultModel || input.providerDefaultModel;
15568
+ return {
15569
+ status: "switched",
15570
+ providerId: id,
15571
+ model,
15572
+ message: `Switched to provider: ${id} (${model})` + preservedSuffix(input.messageCount)
15573
+ };
15574
+ }
15575
+ function parseModelCommand(args, currentProvider) {
15576
+ const raw = args[0];
15577
+ if (!raw) return { kind: "select" };
15578
+ const sub = raw.toLowerCase();
15579
+ if (sub === "refresh") return { kind: "refresh", providerId: args[1] ?? currentProvider };
15580
+ if (sub === "cache") {
15581
+ if ((args[1] ?? "").toLowerCase() === "clear") return { kind: "cache-clear", providerId: args[2] };
15582
+ return { kind: "cache-show" };
15583
+ }
15584
+ return { kind: "set", modelId: raw };
15585
+ }
15586
+ function resolveModelSwitch(input) {
15587
+ if (input.targetModel === input.currentModel) {
15588
+ return { status: "unchanged", message: `Already using model: ${input.targetModel}` };
15589
+ }
15590
+ const known = input.knownModels.includes(input.targetModel);
15591
+ return {
15592
+ status: "switched",
15593
+ model: input.targetModel,
15594
+ message: `Switched to model: ${input.targetModel}` + preservedSuffix(input.messageCount),
15595
+ warning: known ? void 0 : `'${input.targetModel}' is not in the known model list for ${input.provider}. If the provider rejects it, check the spelling or run /model refresh ${input.provider}.`
15596
+ };
15597
+ }
15598
+ var PLAN_MODE_TOOL_HINT = [
15599
+ "read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files",
15600
+ "web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos"
15601
+ ];
15602
+ function applyPlanCommand(sub, active) {
15603
+ const key = (sub ?? "enter").toLowerCase() === "toggle" ? active ? "exit" : "enter" : (sub ?? "enter").toLowerCase();
15604
+ if (key === "enter") {
15605
+ if (active) {
15606
+ return {
15607
+ planMode: true,
15608
+ changed: false,
15609
+ status: "already-active",
15610
+ summary: "Already in Plan Mode. Use /plan execute to start executing, or /plan exit to leave.",
15611
+ details: []
15612
+ };
15613
+ }
15614
+ return {
15615
+ planMode: true,
15616
+ changed: true,
15617
+ status: "entered",
15618
+ summary: "\u{1F4CB} Plan Mode activated",
15619
+ details: [
15620
+ "AI can now ONLY use read-only tools:",
15621
+ ...PLAN_MODE_TOOL_HINT,
15622
+ "Describe your task and the AI will analyze the codebase and create a detailed implementation plan.",
15623
+ "When ready: /plan execute | To cancel: /plan exit"
15624
+ ]
15625
+ };
15626
+ }
15627
+ if (key === "execute") {
15628
+ if (!active) {
15629
+ return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode. Enter first with /plan", details: [] };
15630
+ }
15631
+ return {
15632
+ planMode: false,
15633
+ changed: true,
15634
+ status: "executing",
15635
+ summary: "Plan Mode ended \u2014 switching to execute mode. AI now has full tool access.",
15636
+ details: []
15637
+ };
15638
+ }
15639
+ if (key === "exit" || key === "cancel") {
15640
+ if (!active) {
15641
+ return { planMode: false, changed: false, status: "not-active", summary: "Not in Plan Mode.", details: [] };
15642
+ }
15643
+ return { planMode: false, changed: true, status: "cancelled", summary: "Plan Mode cancelled. Returning to normal mode.", details: [] };
15644
+ }
15645
+ if (key === "status" || key === "show") {
15646
+ return {
15647
+ planMode: active,
15648
+ changed: false,
15649
+ status: "status",
15650
+ summary: `Plan Mode: ${active ? "ACTIVE \u{1F4CB}" : "inactive"}`,
15651
+ details: active ? ["Tools restricted to read-only set.", "Use /plan execute to start executing, or /plan exit to cancel."] : []
15652
+ };
15653
+ }
15654
+ return {
15655
+ planMode: active,
15656
+ changed: false,
15657
+ status: "unknown",
15658
+ summary: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status`,
15659
+ details: []
15660
+ };
15661
+ }
15662
+ function parseRouteCommand(args) {
15663
+ const sub = (args[0] ?? "show").toLowerCase();
15664
+ if (sub === "on" || sub === "enable") return { kind: "enable" };
15665
+ if (sub === "off" || sub === "disable") return { kind: "disable" };
15666
+ if (sub === "test") {
15667
+ const message = args.slice(1).join(" ").trim();
15668
+ return message ? { kind: "test", message } : { kind: "usage", message: "Usage: /route test <message>" };
15669
+ }
15670
+ if (sub === "show" || sub === "status") return { kind: "show" };
15671
+ return { kind: "unknown", message: `Unknown subcommand: ${sub}. Usage: /route [on|off|show|test <message>]` };
15672
+ }
15673
+ function routingEmptyRulesNote(routing) {
15674
+ if (routing && routing.rules.length > 0) return null;
15675
+ return 'No rules configured yet. Add rules under `routing.rules` in ~/.aicli/config.json.\nExample: { match: { tag: "fast" }, model: "claude-haiku-4-5" }';
15676
+ }
15677
+ function describeRoutingRule(rule, index) {
15678
+ const parts = [];
15679
+ if (rule.match.tag) parts.push(`tag=#${rule.match.tag}`);
15680
+ if (rule.match.contains && rule.match.contains.length > 0) {
15681
+ const shown = rule.match.contains.slice(0, 3).join(", ");
15682
+ parts.push(`contains=[${shown}${rule.match.contains.length > 3 ? ", \u2026" : ""}]`);
15683
+ }
15684
+ if (typeof rule.match.maxLength === "number") parts.push(`maxLen=${rule.match.maxLength}`);
15685
+ if (typeof rule.match.minLength === "number") parts.push(`minLen=${rule.match.minLength}`);
15686
+ const empty = parts.length === 0;
15687
+ return {
15688
+ index,
15689
+ name: rule.name,
15690
+ condition: empty ? "(empty \u2014 never matches)" : parts.join(" & "),
15691
+ model: rule.model,
15692
+ empty
15693
+ };
15694
+ }
15695
+ function describeRouting(input) {
15696
+ const rules = input.routing?.rules ?? [];
15697
+ return {
15698
+ enabled: input.routing?.enabled === true,
15699
+ provider: input.provider,
15700
+ currentModel: input.currentModel,
15701
+ fallback: input.routing?.fallback,
15702
+ rules: rules.map((r, i) => describeRoutingRule(r, i)),
15703
+ noRulesNote: rules.length === 0 ? "(no rules configured \u2014 edit ~/.aicli/config.json `routing.rules`)" : null,
15704
+ hint: "Commands: /route on | off | test <msg> | show"
15705
+ };
15706
+ }
15707
+ function routeDecisionRows(message, currentModel, decision) {
15708
+ const rows = [
15709
+ { label: "Input", value: message },
15710
+ { label: "Current", value: currentModel },
15711
+ { label: "Decision", value: `${decision.model} ${decision.overridden ? "\u2192 ROUTED" : "(unchanged)"}` },
15712
+ { label: "Reason", value: decision.reason }
15713
+ ];
15714
+ if (typeof decision.ruleIdx === "number") rows.push({ label: "Rule", value: `#${decision.ruleIdx}` });
15715
+ return rows;
15716
+ }
15717
+
15514
15718
  // src/web/commands/provider-commands.ts
15719
+ function syncSessionProvider(ctx) {
15720
+ const session = ctx.sessions.current;
15721
+ if (!session) return;
15722
+ session.updateProvider(ctx.currentProvider, ctx.currentModel);
15723
+ ctx.saveIfNeeded();
15724
+ }
15515
15725
  async function handleProvider(args, ctx) {
15516
15726
  const id = args[0];
15517
15727
  if (!id) {
15518
15728
  ctx.send({ type: "error", message: "Usage: /provider <id>" });
15519
15729
  return;
15520
15730
  }
15521
- const p = ctx.providers.get(id);
15522
- if (!p) {
15523
- ctx.send({ type: "error", message: `Provider "${id}" not available` });
15731
+ const configured = ctx.providers.has(id);
15732
+ let availableModels = [];
15733
+ let providerDefaultModel = "";
15734
+ if (configured) {
15735
+ const target = ctx.providers.get(id);
15736
+ providerDefaultModel = target.info.defaultModel;
15737
+ availableModels = target.info.models.map((m) => m.id);
15738
+ if (availableModels.length === 0) {
15739
+ try {
15740
+ availableModels = (await target.listModels()).map((m) => m.id);
15741
+ } catch {
15742
+ availableModels = [];
15743
+ }
15744
+ }
15745
+ }
15746
+ const outcome = resolveProviderSwitch({
15747
+ targetId: id,
15748
+ currentProvider: ctx.currentProvider,
15749
+ configured,
15750
+ availableModels,
15751
+ configuredDefaultModel: ctx.config.get("defaultModels")[id],
15752
+ providerDefaultModel,
15753
+ messageCount: ctx.sessions.current?.messages.length ?? 0
15754
+ });
15755
+ if (outcome.status === "not-configured" || outcome.status === "no-models") {
15756
+ ctx.send({ type: "error", message: outcome.message });
15757
+ return;
15758
+ }
15759
+ if (outcome.status === "unchanged") {
15760
+ ctx.send({ type: "info", message: outcome.message });
15524
15761
  return;
15525
15762
  }
15526
- ctx.currentProvider = id;
15527
- ctx.currentModel = p.info.defaultModel;
15763
+ ctx.currentProvider = outcome.providerId;
15764
+ ctx.currentModel = outcome.model;
15528
15765
  ctx.updateContextWindow();
15529
- ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
15766
+ syncSessionProvider(ctx);
15767
+ ctx.send({ type: "info", message: outcome.message });
15530
15768
  ctx.sendStatus();
15531
15769
  }
15532
15770
  async function handleModel(args, ctx) {
15533
- const sub = args[0];
15534
- if (!sub) {
15771
+ const intent = parseModelCommand(args, ctx.currentProvider);
15772
+ if (intent.kind === "select") {
15535
15773
  ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
15536
15774
  return;
15537
15775
  }
15538
- if (sub === "refresh") {
15539
- const providerId = args[1] ?? ctx.currentProvider;
15776
+ if (intent.kind === "refresh") {
15777
+ if (!ctx.providers.has(intent.providerId)) {
15778
+ ctx.send({ type: "error", message: `Provider is not configured: ${intent.providerId}` });
15779
+ return;
15780
+ }
15540
15781
  try {
15541
- const models2 = await ctx.providers.refreshModels(providerId);
15542
- ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
15782
+ const models2 = await ctx.providers.refreshModels(intent.providerId);
15783
+ ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${intent.providerId}.` });
15543
15784
  ctx.sendStatus();
15544
15785
  } catch (err) {
15545
15786
  ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
15546
15787
  }
15547
15788
  return;
15548
15789
  }
15549
- if (sub === "cache") {
15550
- if ((args[1] ?? "").toLowerCase() === "clear") {
15551
- const providerId = args[2];
15552
- ctx.providers.clearModelCache(providerId);
15553
- ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
15554
- return;
15555
- }
15790
+ if (intent.kind === "cache-clear") {
15791
+ ctx.providers.clearModelCache(intent.providerId);
15792
+ ctx.send({
15793
+ type: "info",
15794
+ message: intent.providerId ? `Cleared model cache for ${intent.providerId}.` : "Cleared model cache."
15795
+ });
15796
+ return;
15797
+ }
15798
+ if (intent.kind === "cache-show") {
15556
15799
  const rows = ctx.providers.getModelCacheStatus();
15557
15800
  ctx.send({
15558
15801
  type: "info",
@@ -15560,16 +15803,25 @@ async function handleModel(args, ctx) {
15560
15803
  });
15561
15804
  return;
15562
15805
  }
15563
- const modelId = sub;
15564
15806
  const models = await ctx.providers.listModels(ctx.currentProvider);
15565
- const found = models.find((m) => m.id === modelId);
15566
- if (!found) {
15567
- ctx.send({ type: "error", message: `Model "${modelId}" not found` });
15807
+ const outcome = resolveModelSwitch({
15808
+ targetModel: intent.modelId,
15809
+ currentModel: ctx.currentModel,
15810
+ provider: ctx.currentProvider,
15811
+ knownModels: models.map((m) => m.id),
15812
+ messageCount: ctx.sessions.current?.messages.length ?? 0
15813
+ });
15814
+ if (outcome.status === "unchanged") {
15815
+ ctx.send({ type: "info", message: outcome.message });
15568
15816
  return;
15569
15817
  }
15570
- ctx.currentModel = modelId;
15818
+ ctx.currentModel = outcome.model;
15571
15819
  ctx.updateContextWindow();
15572
- ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
15820
+ syncSessionProvider(ctx);
15821
+ ctx.send({
15822
+ type: "info",
15823
+ message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
15824
+ });
15573
15825
  ctx.sendStatus();
15574
15826
  }
15575
15827
  async function handleThink(args, ctx) {
@@ -15579,7 +15831,7 @@ async function handleThink(args, ctx) {
15579
15831
  runtimeEffort: ctx.runtimeThinkingEffort,
15580
15832
  configuredThinking: modelParams?.thinking,
15581
15833
  configuredEffort: modelParams?.thinkingEffort,
15582
- supportsEffort: ctx.providers.get(ctx.currentProvider)?.supportsReasoningEffort ?? false,
15834
+ supportsEffort: ctx.providers.has(ctx.currentProvider) ? ctx.providers.get(ctx.currentProvider).supportsReasoningEffort : false,
15583
15835
  provider: ctx.currentProvider
15584
15836
  });
15585
15837
  if (outcome.changed) {
@@ -15593,24 +15845,59 @@ async function handleThink(args, ctx) {
15593
15845
  if (outcome.changed) ctx.sendStatus();
15594
15846
  }
15595
15847
  async function handlePlan(args, ctx) {
15596
- const sub = args[0];
15597
- if (sub === "exit" || sub === "cancel") {
15598
- ctx.planMode = false;
15599
- ctx.send({ type: "info", message: "Plan mode OFF." });
15600
- } else if (sub === "execute") {
15601
- ctx.planMode = false;
15602
- ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
15603
- } else if (sub === "status" || sub === "show") {
15604
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
15848
+ const outcome = applyPlanCommand(args[0], ctx.planMode);
15849
+ if (outcome.changed) ctx.planMode = outcome.planMode;
15850
+ ctx.send({
15851
+ type: outcome.status === "unknown" ? "error" : "info",
15852
+ message: [outcome.summary, ...outcome.details].join(" \u2014 ")
15853
+ });
15854
+ if (outcome.changed) ctx.sendStatus();
15855
+ }
15856
+ async function handleRoute(args, ctx) {
15857
+ const intent = parseRouteCommand(args);
15858
+ const routing = ctx.config.get("routing");
15859
+ if (intent.kind === "enable" || intent.kind === "disable") {
15860
+ const on = intent.kind === "enable";
15861
+ ctx.config.setByPath("routing.enabled", on ? "true" : "false");
15862
+ const note = on ? routingEmptyRulesNote(routing) : null;
15863
+ ctx.send({
15864
+ type: "info",
15865
+ message: `Smart model routing ${on ? "enabled" : "disabled"}.` + (note ? `
15866
+ ${note}` : "")
15867
+ });
15868
+ ctx.sendStatus();
15869
+ return;
15870
+ }
15871
+ if (intent.kind === "usage" || intent.kind === "unknown") {
15872
+ ctx.send({ type: "error", message: intent.message });
15605
15873
  return;
15606
- } else if (sub !== void 0) {
15607
- ctx.send({ type: "error", message: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status` });
15874
+ }
15875
+ if (intent.kind === "test") {
15876
+ const decision = ctx.computeRoutingDecision(intent.message);
15877
+ ctx.send({
15878
+ type: "info",
15879
+ message: routeDecisionRows(intent.message, ctx.currentModel, decision).map((row) => `${row.label}: ${row.value}`).join("\n")
15880
+ });
15608
15881
  return;
15882
+ }
15883
+ const view = describeRouting({ routing, provider: ctx.currentProvider, currentModel: ctx.currentModel });
15884
+ const lines = [
15885
+ "Smart Model Routing",
15886
+ `Status: ${view.enabled ? "enabled" : "disabled"}`,
15887
+ `Provider: ${view.provider}`,
15888
+ `Current: ${view.currentModel}`
15889
+ ];
15890
+ if (view.fallback) lines.push(`Fallback: ${view.fallback}`);
15891
+ if (view.noRulesNote) {
15892
+ lines.push(view.noRulesNote);
15609
15893
  } else {
15610
- ctx.planMode = !ctx.planMode;
15611
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
15894
+ lines.push("Rules (evaluated top-to-bottom):");
15895
+ for (const rule of view.rules) {
15896
+ lines.push(` #${rule.index} ${rule.name ? `${rule.name} ` : ""}${rule.condition} \u2192 ${rule.model}`);
15897
+ }
15612
15898
  }
15613
- ctx.sendStatus();
15899
+ lines.push(view.hint);
15900
+ ctx.send({ type: "info", message: lines.join("\n") });
15614
15901
  }
15615
15902
 
15616
15903
  // src/core/session-cost.ts
@@ -16143,7 +16430,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
16143
16430
  Cost: ${formatCost(cost)}` : "";
16144
16431
  let memoryLine = "";
16145
16432
  try {
16146
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-CI544RLT.js");
16433
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-JCENBMCJ.js");
16147
16434
  const pending = countPendingMemories2(ctx.config.getConfigDir());
16148
16435
  if (pending > 0) memoryLine = `
16149
16436
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -16956,7 +17243,7 @@ async function handleTest(args, ctx) {
16956
17243
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
16957
17244
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
16958
17245
  }
16959
- const runTests = ctx.runTests ?? (await import("./run-tests-72VNZXNT.js")).executeTests;
17246
+ const runTests = ctx.runTests ?? (await import("./run-tests-2Q5MJHXB.js")).executeTests;
16960
17247
  const report = await runTests(testArgs);
16961
17248
  ctx.send({ type: "info", message: report });
16962
17249
  } catch (err) {
@@ -17134,7 +17421,7 @@ async function handleIndex(args, ctx) {
17134
17421
  const sub = (args[0] ?? "status").toLowerCase();
17135
17422
  const root = process.cwd();
17136
17423
  const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
17137
- const { indexProject: indexProject2 } = await import("./indexer-M2LJGTY2.js");
17424
+ const { indexProject: indexProject2 } = await import("./indexer-RBEKMFT4.js");
17138
17425
  const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
17139
17426
  if (sub === "status") {
17140
17427
  const idx = loadIndex2(root);
@@ -17292,7 +17579,7 @@ async function handleMemory(args, ctx) {
17292
17579
  ctx.handleMemoryManage(sub, args[1], sub === "expire" ? args[2] : void 0);
17293
17580
  } else if (sub === "export") {
17294
17581
  if (args[1] === "md") {
17295
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-CI544RLT.js");
17582
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-JCENBMCJ.js");
17296
17583
  ctx.send({
17297
17584
  type: "export_data",
17298
17585
  format: "md",
@@ -17734,7 +18021,8 @@ async function handleHelp(_args, ctx) {
17734
18021
  " /clear \u2014 Clear conversation & start new session",
17735
18022
  " /compact [hint] \u2014 Compress conversation history",
17736
18023
  " /think [on|off] \u2014 Toggle extended thinking mode",
17737
- " /plan [enter|exit] \u2014 Toggle read-only planning mode",
18024
+ " /plan [execute|exit|status] \u2014 Read-only planning mode (bare /plan enters it)",
18025
+ " /route [on|off|show|test <msg>] \u2014 Smart model routing",
17738
18026
  " /session new|list|load|delete <id> \u2014 Session management",
17739
18027
  " /system [prompt|clear] \u2014 Set/view/reset system prompt",
17740
18028
  " /context [status|reload] \u2014 Show/reload context layers",
@@ -17871,8 +18159,9 @@ var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Map([
17871
18159
  ["snapshot", "Dev-state snapshots are written when the terminal REPL exits."],
17872
18160
  ["hooks", "Hook trust is granted per machine from the terminal."],
17873
18161
  ["hook", "Hook trust is granted per machine from the terminal."],
17874
- // 以下两条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
17875
- ["route", "Smart model routing is not applied to Web sessions yet (config.routing is REPL-only)."],
18162
+ // 以下一条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
18163
+ // (`route` 曾在这里,理由写的是「Web 尚未应用智能路由」;v0.4.269 让 routing 在 Web
18164
+ // 上真正生效之后那句话就不再成立,命令已在 Web 实现,见 commands/provider-commands.ts。)
17876
18165
  ["security", "Redaction status and toggles are terminal-only for now; /status shows the current profile."]
17877
18166
  ]);
17878
18167
  var webCommandHandlers = {
@@ -17880,6 +18169,7 @@ var webCommandHandlers = {
17880
18169
  model: handleModel,
17881
18170
  think: handleThink,
17882
18171
  plan: handlePlan,
18172
+ route: handleRoute,
17883
18173
  clear: handleClear,
17884
18174
  compact: handleCompact,
17885
18175
  status: handleStatus,
@@ -18052,9 +18342,19 @@ var SessionHandler = class {
18052
18342
  }
18053
18343
  runLifecycleHooks(hooks, "SessionStart", { source: "web" }, { configDir: this.config.getConfigDir() });
18054
18344
  this.sendStatus();
18055
- askUserContext.rl = null;
18345
+ askUserContext.rl = void 0;
18056
18346
  askUserContext.prompting = false;
18057
18347
  }
18348
+ /**
18349
+ * P3-03:本方法与 `WebCommandContext` 列出的其余 36 个成员一并由 private 放开为
18350
+ * public——它们本就经由 ctx 门面暴露给每一个 web 命令 handler,`private` 只是名义上的。
18351
+ *
18352
+ * `web-command-context.ts` 的文件注释明明白白写着「`ctx: WebCommandContext = this`
18353
+ * 这个赋值保证 handler 只能碰到这些成员」,而 `handleCommand` 实际写的是
18354
+ * `this as unknown as WebCommandContext`——那道保证被这一次 cast 整个绕过了,
18355
+ * 唯一拦着真赋值的就是这些名义上的 private。去掉 cast 之后,SessionHandler 与门面
18356
+ * 接口之间的任何漂移(少一个成员、签名改了)都会当场编译失败,而不是等到运行时。
18357
+ */
18058
18358
  send(msg) {
18059
18359
  if (this.ws.readyState === this.ws.OPEN) {
18060
18360
  this.ws.send(JSON.stringify(msg));
@@ -18423,10 +18723,10 @@ var SessionHandler = class {
18423
18723
  this.processing = false;
18424
18724
  return;
18425
18725
  }
18426
- const hasToolSupport = typeof provider.chatWithTools === "function";
18427
- const { toolDefs, mcpBudgetNote } = hasToolSupport ? this.getFilteredToolDefs() : { toolDefs: [], mcpBudgetNote: null };
18428
- if (hasToolSupport && toolDefs.length > 0) {
18429
- await this.handleChatWithTools(provider, session.messages, toolDefs, mcpBudgetNote, routingDecision.model);
18726
+ const toolCapable = isToolCapableProvider(provider) ? provider : null;
18727
+ const { toolDefs, mcpBudgetNote } = toolCapable ? this.getFilteredToolDefs() : { toolDefs: [], mcpBudgetNote: null };
18728
+ if (toolCapable && toolDefs.length > 0) {
18729
+ await this.handleChatWithTools(toolCapable, session.messages, toolDefs, mcpBudgetNote, routingDecision.model);
18430
18730
  } else {
18431
18731
  await this.handleChatSimple(provider, session.messages, routingDecision.model);
18432
18732
  }