jinzd-ai-cli 0.4.277 → 0.4.279

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 (34) hide show
  1. package/dist/{batch-2I5TPZA2.js → batch-6QZSWCBL.js} +2 -2
  2. package/dist/{chunk-ZP3RCZKM.js → chunk-2BKRWTEQ.js} +1 -1
  3. package/dist/{chunk-Z752LDYF.js → chunk-5XHEXRMN.js} +1 -1
  4. package/dist/{chunk-7BO5H2QI.js → chunk-6WMT7OQG.js} +1 -1
  5. package/dist/{chunk-HJVKCOAG.js → chunk-7L4TEK7Q.js} +419 -3
  6. package/dist/{chunk-7YYUITED.js → chunk-CMNXAJLH.js} +1 -1
  7. package/dist/{chunk-KIGKL3PZ.js → chunk-FQJFGGKN.js} +4 -4
  8. package/dist/{chunk-UWQCTTP4.js → chunk-IIXZ7IPN.js} +1 -1
  9. package/dist/{chunk-7IPMONKZ.js → chunk-JLOQBYU4.js} +3 -3
  10. package/dist/{chunk-I3PYD2YN.js → chunk-JS6VAPWM.js} +3 -3
  11. package/dist/{chunk-3VFOXEIL.js → chunk-NZPHEUJM.js} +1 -1
  12. package/dist/{chunk-HCF442WL.js → chunk-RJY3HJS4.js} +1 -1
  13. package/dist/{chunk-BYNNKJUC.js → chunk-SIVM2YQQ.js} +1 -1
  14. package/dist/{chunk-OCOGYIRY.js → chunk-UD3BVNC5.js} +1 -1
  15. package/dist/{ci-JFC45LIS.js → ci-F4ESOYI5.js} +4 -4
  16. package/dist/{ci-format-P3LIZHUR.js → ci-format-AIRW3TVQ.js} +2 -2
  17. package/dist/{constants-N3CHKZJY.js → constants-3VMMZJDS.js} +1 -1
  18. package/dist/{doctor-cli-Z3TGHTSX.js → doctor-cli-WVMEWMYE.js} +4 -4
  19. package/dist/electron-server.js +699 -101
  20. package/dist/{hub-NEWIFMQ3.js → hub-JJCCCU3O.js} +1 -1
  21. package/dist/index.js +243 -466
  22. package/dist/{persistent-memory-6Q5FDFJ7.js → persistent-memory-KSZKM4JG.js} +2 -2
  23. package/dist/{persistent-memory-ZFR5GI7D.js → persistent-memory-QZN4Q3W4.js} +2 -2
  24. package/dist/{pr-ZUGSWQ5R.js → pr-GFB5JC26.js} +4 -4
  25. package/dist/{run-tests-76M6AC3G.js → run-tests-EL54INOI.js} +2 -2
  26. package/dist/{run-tests-USV7IJ7X.js → run-tests-HMHHIG7Y.js} +2 -2
  27. package/dist/{server-R7D5RR36.js → server-6C7GITHY.js} +5 -5
  28. package/dist/{server-C523CIGN.js → server-WUSJWC3A.js} +324 -106
  29. package/dist/{task-orchestrator-G2MQDNGQ.js → task-orchestrator-C7RUUA2Q.js} +5 -5
  30. package/dist/{usage-EFHXXMYX.js → usage-CW6BT2UV.js} +2 -2
  31. package/dist/web/client/app.js +18 -3
  32. package/dist/web/client/app.js.br +0 -0
  33. package/dist/web/client/app.js.gz +0 -0
  34. package/package.json +1 -1
@@ -16,6 +16,7 @@ import {
16
16
  AUTO_COMPACT_INSTRUCTION,
17
17
  McpManager,
18
18
  SkillManager,
19
+ applyPlanCommand,
19
20
  applyProfileCommand,
20
21
  applyThinkCommand,
21
22
  armDeferredSave,
@@ -25,19 +26,36 @@ import {
25
26
  costSourceNote,
26
27
  decideDeferredSave,
27
28
  decideModel,
29
+ describeRouting,
28
30
  estimateConversationTokens,
29
31
  evaluateContextPressure,
30
32
  filterMemoryEntries,
33
+ formatSessionSummaryLine,
31
34
  loadDevState,
35
+ parseBatchDeleteArgs,
36
+ parseCleanArgs,
32
37
  parseMemoryShowFilters,
38
+ parseModelCommand,
39
+ parseRouteCommand,
40
+ parseSessionListArgs,
33
41
  persistToolRound,
34
42
  readGitDiff,
43
+ resolveModelSwitch,
44
+ resolveProviderSwitch,
45
+ resolveSessionRef,
46
+ routeDecisionRows,
47
+ routingEmptyRulesNote,
35
48
  scanDirTree,
36
49
  scanProject,
50
+ selectSessions,
51
+ selectStaleSessions,
52
+ sessionListJsonRow,
53
+ sessionListNeedsFileSize,
54
+ sessionListSummary,
37
55
  setupProxy,
38
56
  snapshotFile,
39
57
  stripRoutingTags
40
- } from "./chunk-HJVKCOAG.js";
58
+ } from "./chunk-7L4TEK7Q.js";
41
59
  import {
42
60
  CNY_PER_USD,
43
61
  CostTracker,
@@ -79,7 +97,7 @@ import {
79
97
  spawnAgentContext,
80
98
  truncateOutput,
81
99
  undoStack
82
- } from "./chunk-KIGKL3PZ.js";
100
+ } from "./chunk-FQJFGGKN.js";
83
101
  import {
84
102
  buildReviewPrompt,
85
103
  buildSecurityReviewPrompt
@@ -106,7 +124,7 @@ import {
106
124
  restoreMemoryEntries,
107
125
  updateMemoryApproval,
108
126
  updateMemoryEntry
109
- } from "./chunk-7BO5H2QI.js";
127
+ } from "./chunk-6WMT7OQG.js";
110
128
  import "./chunk-4DEMGCK4.js";
111
129
  import "./chunk-UUSRWSSX.js";
112
130
  import "./chunk-CKH4KQ4E.js";
@@ -125,7 +143,7 @@ import {
125
143
  buildDoctorReport,
126
144
  formatDoctorReport,
127
145
  loadContextFiles
128
- } from "./chunk-7IPMONKZ.js";
146
+ } from "./chunk-JLOQBYU4.js";
129
147
  import {
130
148
  describePlugin,
131
149
  getActivePluginAssets,
@@ -140,13 +158,13 @@ import {
140
158
  runTool,
141
159
  setPluginEnabled,
142
160
  trustPlugin
143
- } from "./chunk-UWQCTTP4.js";
161
+ } from "./chunk-IIXZ7IPN.js";
144
162
  import {
145
163
  ProviderRegistry
146
164
  } from "./chunk-UGY5SVNX.js";
147
165
  import {
148
166
  ConfigManager
149
- } from "./chunk-OCOGYIRY.js";
167
+ } from "./chunk-UD3BVNC5.js";
150
168
  import {
151
169
  CONTENT_ONLY_STREAM_REMINDER,
152
170
  TEE_FINAL_USER_NUDGE,
@@ -176,7 +194,7 @@ import {
176
194
  maskConfigSecrets
177
195
  } from "./chunk-XNFIYS7G.js";
178
196
  import "./chunk-4BKXL7SM.js";
179
- import "./chunk-3VFOXEIL.js";
197
+ import "./chunk-NZPHEUJM.js";
180
198
  import {
181
199
  AGENTIC_BEHAVIOR_GUIDELINE,
182
200
  AUTHOR,
@@ -192,7 +210,7 @@ import {
192
210
  SKILLS_DIR_NAME,
193
211
  VERSION,
194
212
  buildUserIdentityPrompt
195
- } from "./chunk-ZP3RCZKM.js";
213
+ } from "./chunk-2BKRWTEQ.js";
196
214
 
197
215
  // src/web/server.ts
198
216
  import express from "express";
@@ -721,47 +739,86 @@ import { mkdirSync as mkdirSync2, createWriteStream as createWriteStream2 } from
721
739
  import { dirname as dirname2 } from "path";
722
740
 
723
741
  // src/web/commands/provider-commands.ts
742
+ function syncSessionProvider(ctx) {
743
+ const session = ctx.sessions.current;
744
+ if (!session) return;
745
+ session.updateProvider(ctx.currentProvider, ctx.currentModel);
746
+ ctx.saveIfNeeded();
747
+ }
724
748
  async function handleProvider(args, ctx) {
725
749
  const id = args[0];
726
750
  if (!id) {
727
751
  ctx.send({ type: "error", message: "Usage: /provider <id>" });
728
752
  return;
729
753
  }
730
- const p = ctx.providers.get(id);
731
- if (!p) {
732
- ctx.send({ type: "error", message: `Provider "${id}" not available` });
754
+ const configured = ctx.providers.has(id);
755
+ let availableModels = [];
756
+ let providerDefaultModel = "";
757
+ if (configured) {
758
+ const target = ctx.providers.get(id);
759
+ providerDefaultModel = target.info.defaultModel;
760
+ availableModels = target.info.models.map((m) => m.id);
761
+ if (availableModels.length === 0) {
762
+ try {
763
+ availableModels = (await target.listModels()).map((m) => m.id);
764
+ } catch {
765
+ availableModels = [];
766
+ }
767
+ }
768
+ }
769
+ const outcome = resolveProviderSwitch({
770
+ targetId: id,
771
+ currentProvider: ctx.currentProvider,
772
+ configured,
773
+ availableModels,
774
+ configuredDefaultModel: ctx.config.get("defaultModels")[id],
775
+ providerDefaultModel,
776
+ messageCount: ctx.sessions.current?.messages.length ?? 0
777
+ });
778
+ if (outcome.status === "not-configured" || outcome.status === "no-models") {
779
+ ctx.send({ type: "error", message: outcome.message });
780
+ return;
781
+ }
782
+ if (outcome.status === "unchanged") {
783
+ ctx.send({ type: "info", message: outcome.message });
733
784
  return;
734
785
  }
735
- ctx.currentProvider = id;
736
- ctx.currentModel = p.info.defaultModel;
786
+ ctx.currentProvider = outcome.providerId;
787
+ ctx.currentModel = outcome.model;
737
788
  ctx.updateContextWindow();
738
- ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
789
+ syncSessionProvider(ctx);
790
+ ctx.send({ type: "info", message: outcome.message });
739
791
  ctx.sendStatus();
740
792
  }
741
793
  async function handleModel(args, ctx) {
742
- const sub = args[0];
743
- if (!sub) {
794
+ const intent = parseModelCommand(args, ctx.currentProvider);
795
+ if (intent.kind === "select") {
744
796
  ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
745
797
  return;
746
798
  }
747
- if (sub === "refresh") {
748
- const providerId = args[1] ?? ctx.currentProvider;
799
+ if (intent.kind === "refresh") {
800
+ if (!ctx.providers.has(intent.providerId)) {
801
+ ctx.send({ type: "error", message: `Provider is not configured: ${intent.providerId}` });
802
+ return;
803
+ }
749
804
  try {
750
- const models2 = await ctx.providers.refreshModels(providerId);
751
- ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
805
+ const models2 = await ctx.providers.refreshModels(intent.providerId);
806
+ ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${intent.providerId}.` });
752
807
  ctx.sendStatus();
753
808
  } catch (err) {
754
809
  ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
755
810
  }
756
811
  return;
757
812
  }
758
- if (sub === "cache") {
759
- if ((args[1] ?? "").toLowerCase() === "clear") {
760
- const providerId = args[2];
761
- ctx.providers.clearModelCache(providerId);
762
- ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
763
- return;
764
- }
813
+ if (intent.kind === "cache-clear") {
814
+ ctx.providers.clearModelCache(intent.providerId);
815
+ ctx.send({
816
+ type: "info",
817
+ message: intent.providerId ? `Cleared model cache for ${intent.providerId}.` : "Cleared model cache."
818
+ });
819
+ return;
820
+ }
821
+ if (intent.kind === "cache-show") {
765
822
  const rows = ctx.providers.getModelCacheStatus();
766
823
  ctx.send({
767
824
  type: "info",
@@ -769,16 +826,25 @@ async function handleModel(args, ctx) {
769
826
  });
770
827
  return;
771
828
  }
772
- const modelId = sub;
773
829
  const models = await ctx.providers.listModels(ctx.currentProvider);
774
- const found = models.find((m) => m.id === modelId);
775
- if (!found) {
776
- ctx.send({ type: "error", message: `Model "${modelId}" not found` });
830
+ const outcome = resolveModelSwitch({
831
+ targetModel: intent.modelId,
832
+ currentModel: ctx.currentModel,
833
+ provider: ctx.currentProvider,
834
+ knownModels: models.map((m) => m.id),
835
+ messageCount: ctx.sessions.current?.messages.length ?? 0
836
+ });
837
+ if (outcome.status === "unchanged") {
838
+ ctx.send({ type: "info", message: outcome.message });
777
839
  return;
778
840
  }
779
- ctx.currentModel = modelId;
841
+ ctx.currentModel = outcome.model;
780
842
  ctx.updateContextWindow();
781
- ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
843
+ syncSessionProvider(ctx);
844
+ ctx.send({
845
+ type: "info",
846
+ message: outcome.warning ? `${outcome.message} \u2014 ${outcome.warning}` : outcome.message
847
+ });
782
848
  ctx.sendStatus();
783
849
  }
784
850
  async function handleThink(args, ctx) {
@@ -788,7 +854,7 @@ async function handleThink(args, ctx) {
788
854
  runtimeEffort: ctx.runtimeThinkingEffort,
789
855
  configuredThinking: modelParams?.thinking,
790
856
  configuredEffort: modelParams?.thinkingEffort,
791
- supportsEffort: ctx.providers.get(ctx.currentProvider)?.supportsReasoningEffort ?? false,
857
+ supportsEffort: ctx.providers.has(ctx.currentProvider) ? ctx.providers.get(ctx.currentProvider).supportsReasoningEffort : false,
792
858
  provider: ctx.currentProvider
793
859
  });
794
860
  if (outcome.changed) {
@@ -802,24 +868,59 @@ async function handleThink(args, ctx) {
802
868
  if (outcome.changed) ctx.sendStatus();
803
869
  }
804
870
  async function handlePlan(args, ctx) {
805
- const sub = args[0];
806
- if (sub === "exit" || sub === "cancel") {
807
- ctx.planMode = false;
808
- ctx.send({ type: "info", message: "Plan mode OFF." });
809
- } else if (sub === "execute") {
810
- ctx.planMode = false;
811
- ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
812
- } else if (sub === "status" || sub === "show") {
813
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
871
+ const outcome = applyPlanCommand(args[0], ctx.planMode);
872
+ if (outcome.changed) ctx.planMode = outcome.planMode;
873
+ ctx.send({
874
+ type: outcome.status === "unknown" ? "error" : "info",
875
+ message: [outcome.summary, ...outcome.details].join(" \u2014 ")
876
+ });
877
+ if (outcome.changed) ctx.sendStatus();
878
+ }
879
+ async function handleRoute(args, ctx) {
880
+ const intent = parseRouteCommand(args);
881
+ const routing = ctx.config.get("routing");
882
+ if (intent.kind === "enable" || intent.kind === "disable") {
883
+ const on = intent.kind === "enable";
884
+ ctx.config.setByPath("routing.enabled", on ? "true" : "false");
885
+ const note = on ? routingEmptyRulesNote(routing) : null;
886
+ ctx.send({
887
+ type: "info",
888
+ message: `Smart model routing ${on ? "enabled" : "disabled"}.` + (note ? `
889
+ ${note}` : "")
890
+ });
891
+ ctx.sendStatus();
814
892
  return;
815
- } else if (sub !== void 0) {
816
- ctx.send({ type: "error", message: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status` });
893
+ }
894
+ if (intent.kind === "usage" || intent.kind === "unknown") {
895
+ ctx.send({ type: "error", message: intent.message });
817
896
  return;
897
+ }
898
+ if (intent.kind === "test") {
899
+ const decision = ctx.computeRoutingDecision(intent.message);
900
+ ctx.send({
901
+ type: "info",
902
+ message: routeDecisionRows(intent.message, ctx.currentModel, decision).map((row) => `${row.label}: ${row.value}`).join("\n")
903
+ });
904
+ return;
905
+ }
906
+ const view = describeRouting({ routing, provider: ctx.currentProvider, currentModel: ctx.currentModel });
907
+ const lines = [
908
+ "Smart Model Routing",
909
+ `Status: ${view.enabled ? "enabled" : "disabled"}`,
910
+ `Provider: ${view.provider}`,
911
+ `Current: ${view.currentModel}`
912
+ ];
913
+ if (view.fallback) lines.push(`Fallback: ${view.fallback}`);
914
+ if (view.noRulesNote) {
915
+ lines.push(view.noRulesNote);
818
916
  } else {
819
- ctx.planMode = !ctx.planMode;
820
- ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
917
+ lines.push("Rules (evaluated top-to-bottom):");
918
+ for (const rule of view.rules) {
919
+ lines.push(` #${rule.index} ${rule.name ? `${rule.name} ` : ""}${rule.condition} \u2192 ${rule.model}`);
920
+ }
821
921
  }
822
- ctx.sendStatus();
922
+ lines.push(view.hint);
923
+ ctx.send({ type: "info", message: lines.join("\n") });
823
924
  }
824
925
 
825
926
  // src/web/commands/_shared.ts
@@ -863,7 +964,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
863
964
  Cost: ${formatCost(cost)}` : "";
864
965
  let memoryLine = "";
865
966
  try {
866
- const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-6Q5FDFJ7.js");
967
+ const { countPendingMemories: countPendingMemories2 } = await import("./persistent-memory-KSZKM4JG.js");
867
968
  const pending = countPendingMemories2(ctx.config.getConfigDir());
868
969
  if (pending > 0) memoryLine = `
869
970
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -906,11 +1007,17 @@ async function handleSession(args, ctx) {
906
1007
  ctx.sendSessionList();
907
1008
  return;
908
1009
  }
909
- const list = ctx.sessions.listSessions();
910
- const found = list.find((s) => s.id.startsWith(targetId));
911
- if (found) {
1010
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), targetId, "most-recent");
1011
+ if (ref.status === "ok") {
1012
+ const found = ref.match;
912
1013
  ctx.sessions.loadSession(found.id);
913
1014
  ctx.resetWebSessionUsage();
1015
+ if (ref.message) {
1016
+ ctx.send({
1017
+ type: "info",
1018
+ message: [ref.message, ...ref.ambiguousWith.slice(0, 5).map((m) => ` ${formatSessionSummaryLine(m)}`)].join("\n")
1019
+ });
1020
+ }
914
1021
  ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
915
1022
  const loadedSession = ctx.sessions.current;
916
1023
  if (loadedSession && loadedSession.messages.length > 0) {
@@ -940,52 +1047,81 @@ async function handleSession(args, ctx) {
940
1047
  return;
941
1048
  }
942
1049
  if (sub === "list") {
943
- ctx.sendSessionList();
1050
+ if (args.length <= 1) {
1051
+ ctx.sendSessionList();
1052
+ return;
1053
+ }
1054
+ const opts = parseSessionListArgs(args);
1055
+ const all = ctx.sessions.listSessions({ includeFileSize: sessionListNeedsFileSize(opts) });
1056
+ if (all.length === 0) {
1057
+ ctx.send({ type: "info", message: "No saved sessions." });
1058
+ return;
1059
+ }
1060
+ const view = selectSessions(all, opts);
1061
+ if (view.totalCount === 0) {
1062
+ ctx.send({
1063
+ type: "info",
1064
+ message: [...opts.warnings.map((w) => `\u26A0 ${w}`), "No sessions match the filter criteria."].join("\n")
1065
+ });
1066
+ return;
1067
+ }
1068
+ if (opts.json) {
1069
+ ctx.send({
1070
+ type: "info",
1071
+ message: JSON.stringify(view.shown.map((s) => sessionListJsonRow(s, view.needsFileSize)), null, 2)
1072
+ });
1073
+ return;
1074
+ }
1075
+ const lines = [
1076
+ ...opts.warnings.map((w) => `\u26A0 ${w}`),
1077
+ ...view.shown.map((s) => formatSessionSummaryLine(s)),
1078
+ `\u2500\u2500 ${sessionListSummary(view)}`
1079
+ ];
1080
+ if (view.shown.length < view.totalCount) lines.push("Use --all to show all, or --limit N to change count.");
1081
+ ctx.send({ type: "info", message: lines.join("\n") });
944
1082
  return;
945
1083
  }
946
1084
  if (sub === "delete" && args[1]) {
947
1085
  const targetId = args[1];
948
1086
  if (targetId === "--filter") {
949
- let filterMessages;
950
- for (let i = 1; i < args.length; i++) {
951
- if (args[i] === "--filter" && i + 1 < args.length) {
952
- const val = args[i + 1];
953
- i++;
954
- const eq = val.indexOf("=");
955
- if (eq > 0) {
956
- const key = val.slice(0, eq);
957
- const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
958
- if (key === "messages") {
959
- const n = Number(vals[0]);
960
- if (Number.isFinite(n) && n >= 0) filterMessages = n;
961
- }
962
- }
963
- }
964
- }
965
- if (filterMessages === void 0) {
966
- ctx.send({ type: "error", message: "Usage: /session delete --filter messages=N" });
1087
+ const opts = parseBatchDeleteArgs(args);
1088
+ if (opts.error) {
1089
+ ctx.send({ type: "error", message: opts.error });
967
1090
  return;
968
1091
  }
969
- const list2 = ctx.sessions.listSessions();
970
- const matches = list2.filter((s) => s.messageCount === filterMessages);
1092
+ const matches = ctx.sessions.listSessions().filter((s) => s.messageCount === opts.messages);
971
1093
  if (matches.length === 0) {
972
- ctx.send({ type: "info", message: `No sessions with ${filterMessages} message(s) found.` });
1094
+ ctx.send({ type: "info", message: `No sessions with ${opts.messages} message(s) found.` });
1095
+ return;
1096
+ }
1097
+ const preview = [
1098
+ `Found ${matches.length} session(s) with ${opts.messages} message(s):`,
1099
+ ...matches.slice(0, 20).map((s) => ` ${formatSessionSummaryLine(s)}`),
1100
+ ...matches.length > 20 ? [` ... and ${matches.length - 20} more`] : []
1101
+ ];
1102
+ if (opts.dryRun) {
1103
+ ctx.send({
1104
+ type: "info",
1105
+ message: [...preview, "--dry-run: no sessions were deleted. Remove --dry-run to actually delete."].join("\n")
1106
+ });
973
1107
  return;
974
1108
  }
975
1109
  let deleted = 0;
976
1110
  let failed = 0;
977
1111
  for (const s of matches) {
978
- if (ctx.sessions.deleteSession(s.id)) {
979
- deleted++;
980
- } else {
981
- failed++;
982
- }
1112
+ if (ctx.sessions.deleteSession(s.id)) deleted++;
1113
+ else failed++;
983
1114
  }
984
1115
  if (!ctx.sessions.current) {
985
- ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
1116
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
1117
+ ctx.unsavedSessions.set(created.id, created);
986
1118
  ctx.resetWebSessionUsage();
987
1119
  }
988
- ctx.send({ type: "info", message: `Deleted ${deleted} session(s) with ${filterMessages} message(s)${failed > 0 ? `, ${failed} failed` : ""}.` });
1120
+ ctx.send({
1121
+ type: "info",
1122
+ message: [...preview, `Deleted ${deleted} session(s)${failed > 0 ? `, ${failed} failed` : ""}.`].join("\n")
1123
+ });
1124
+ ctx.sendStatus();
989
1125
  ctx.sendSessionList();
990
1126
  return;
991
1127
  }
@@ -994,20 +1130,79 @@ async function handleSession(args, ctx) {
994
1130
  ctx.unsavedSessions.delete(cachedKey);
995
1131
  if (ctx.sessions.current?.id === cachedKey) {
996
1132
  ctx.sessions.clearCurrent();
1133
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
1134
+ ctx.unsavedSessions.set(created.id, created);
1135
+ ctx.resetWebSessionUsage();
1136
+ ctx.sendStatus();
997
1137
  }
998
1138
  ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
999
1139
  ctx.sendSessionList();
1000
1140
  return;
1001
1141
  }
1002
- const list = ctx.sessions.listSessions();
1003
- const found = list.find((s) => s.id.startsWith(targetId));
1004
- if (found) {
1005
- ctx.sessions.deleteSession(found.id);
1006
- ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
1007
- ctx.sendSessionList();
1008
- } else {
1142
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), targetId, "strict");
1143
+ if (ref.status === "not-found") {
1009
1144
  ctx.send({ type: "error", message: `Session not found: ${targetId}` });
1145
+ return;
1146
+ }
1147
+ if (ref.status === "ambiguous") {
1148
+ ctx.send({
1149
+ type: "error",
1150
+ message: [ref.message, ...ref.matches.slice(0, 10).map((m) => ` ${formatSessionSummaryLine(m)}`)].join("\n")
1151
+ });
1152
+ return;
1153
+ }
1154
+ const wasCurrent = ctx.sessions.current?.id === ref.match.id;
1155
+ if (!ctx.sessions.deleteSession(ref.match.id)) {
1156
+ ctx.send({ type: "error", message: "Delete failed \u2014 session file not found on disk." });
1157
+ return;
1158
+ }
1159
+ ctx.send({ type: "info", message: `Deleted session: ${ref.match.id.slice(0, 8)}` });
1160
+ if (wasCurrent) {
1161
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
1162
+ ctx.unsavedSessions.set(created.id, created);
1163
+ ctx.resetWebSessionUsage();
1164
+ ctx.send({ type: "info", message: "Current session was deleted \u2014 started a fresh session." });
1165
+ ctx.sendStatus();
1166
+ }
1167
+ ctx.sendSessionList();
1168
+ return;
1169
+ }
1170
+ if (sub === "clean") {
1171
+ const opts = parseCleanArgs(args);
1172
+ const stale = selectStaleSessions(ctx.sessions.listSessions(), opts.staleDays);
1173
+ if (stale.length === 0) {
1174
+ ctx.send({ type: "info", message: `No sessions older than ${opts.staleDays} days found.` });
1175
+ return;
1010
1176
  }
1177
+ const preview = [
1178
+ `Found ${stale.length} session(s) not updated in ${opts.staleDays}+ days:`,
1179
+ ...stale.slice(0, 20).map((s) => ` ${formatSessionSummaryLine(s)}`),
1180
+ ...stale.length > 20 ? [` ... and ${stale.length - 20} more`] : []
1181
+ ];
1182
+ if (opts.dryRun) {
1183
+ ctx.send({
1184
+ type: "info",
1185
+ message: [...preview, "--dry-run: no sessions were deleted. Remove --dry-run to actually delete."].join("\n")
1186
+ });
1187
+ return;
1188
+ }
1189
+ let deleted = 0;
1190
+ let failed = 0;
1191
+ for (const s of stale) {
1192
+ if (ctx.sessions.deleteSession(s.id)) deleted++;
1193
+ else failed++;
1194
+ }
1195
+ if (!ctx.sessions.current) {
1196
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
1197
+ ctx.unsavedSessions.set(created.id, created);
1198
+ ctx.resetWebSessionUsage();
1199
+ ctx.sendStatus();
1200
+ }
1201
+ ctx.send({
1202
+ type: "info",
1203
+ message: [...preview, `Cleaned up: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`].join("\n")
1204
+ });
1205
+ ctx.sendSessionList();
1011
1206
  return;
1012
1207
  }
1013
1208
  if (sub === "batch-delete") {
@@ -1031,6 +1226,12 @@ async function handleSession(args, ctx) {
1031
1226
  deleted++;
1032
1227
  }
1033
1228
  }
1229
+ if (!ctx.sessions.current) {
1230
+ const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
1231
+ ctx.unsavedSessions.set(created.id, created);
1232
+ ctx.resetWebSessionUsage();
1233
+ ctx.sendStatus();
1234
+ }
1034
1235
  ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
1035
1236
  ctx.sendSessionList();
1036
1237
  return;
@@ -1044,26 +1245,40 @@ async function handleSession(args, ctx) {
1044
1245
  }
1045
1246
  const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
1046
1247
  if (cachedKey) {
1047
- const session = ctx.unsavedSessions.get(cachedKey);
1048
- session.title = newTitle;
1248
+ const session2 = ctx.unsavedSessions.get(cachedKey);
1249
+ session2.title = newTitle;
1049
1250
  ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
1050
1251
  ctx.sendSessionList();
1051
1252
  return;
1052
1253
  }
1053
- const list = ctx.sessions.listSessions();
1054
- const found = list.find((s) => s.id.startsWith(targetId));
1055
- if (found) {
1056
- const session = ctx.sessions.loadSession(found.id);
1057
- session.title = newTitle;
1058
- ctx.sessions.save();
1059
- ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
1060
- ctx.sendSessionList();
1061
- } else {
1254
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), targetId, "strict");
1255
+ if (ref.status === "not-found") {
1062
1256
  ctx.send({ type: "error", message: `Session not found: ${targetId}` });
1257
+ return;
1063
1258
  }
1259
+ if (ref.status === "ambiguous") {
1260
+ ctx.send({
1261
+ type: "error",
1262
+ message: [ref.message, ...ref.matches.slice(0, 10).map((m) => ` ${formatSessionSummaryLine(m)}`)].join("\n")
1263
+ });
1264
+ return;
1265
+ }
1266
+ const priorId = ctx.sessions.current?.id ?? null;
1267
+ const isCurrent = priorId === ref.match.id;
1268
+ const session = isCurrent ? ctx.sessions.current : ctx.sessions.loadSession(ref.match.id);
1269
+ session.title = newTitle;
1270
+ ctx.sessions.save();
1271
+ if (!isCurrent && priorId) {
1272
+ ctx.sessions.loadSession(priorId);
1273
+ }
1274
+ ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
1275
+ ctx.sendSessionList();
1064
1276
  return;
1065
1277
  }
1066
- ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | delete --filter messages=N | rename <id> <title>" });
1278
+ ctx.send({
1279
+ type: "info",
1280
+ message: "Usage: /session new | list [--filter k=v] [--search kw] [--limit N|--all] [--sort f[:o]] [--json] | load <id> | delete <id> | delete --filter messages=N [--dry-run] | clean --stale N [--dry-run] | rename <id> <title>"
1281
+ });
1067
1282
  }
1068
1283
  async function handleExport(args, ctx) {
1069
1284
  const format = args[0] === "json" ? "json" : "md";
@@ -1676,7 +1891,7 @@ async function handleTest(args, ctx) {
1676
1891
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
1677
1892
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
1678
1893
  }
1679
- const runTests = ctx.runTests ?? (await import("./run-tests-76M6AC3G.js")).executeTests;
1894
+ const runTests = ctx.runTests ?? (await import("./run-tests-EL54INOI.js")).executeTests;
1680
1895
  const report = await runTests(testArgs);
1681
1896
  ctx.send({ type: "info", message: report });
1682
1897
  } catch (err) {
@@ -2012,7 +2227,7 @@ async function handleMemory(args, ctx) {
2012
2227
  ctx.handleMemoryManage(sub, args[1], sub === "expire" ? args[2] : void 0);
2013
2228
  } else if (sub === "export") {
2014
2229
  if (args[1] === "md") {
2015
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-6Q5FDFJ7.js");
2230
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-KSZKM4JG.js");
2016
2231
  ctx.send({
2017
2232
  type: "export_data",
2018
2233
  format: "md",
@@ -2266,8 +2481,9 @@ async function handleHelp(_args, ctx) {
2266
2481
  " /clear \u2014 Clear conversation & start new session",
2267
2482
  " /compact [hint] \u2014 Compress conversation history",
2268
2483
  " /think [on|off] \u2014 Toggle extended thinking mode",
2269
- " /plan [enter|exit] \u2014 Toggle read-only planning mode",
2270
- " /session new|list|load|delete <id> \u2014 Session management",
2484
+ " /plan [execute|exit|status] \u2014 Read-only planning mode (bare /plan enters it)",
2485
+ " /route [on|off|show|test <msg>] \u2014 Smart model routing",
2486
+ " /session new|list [filters]|load <id>|delete <id>|delete --filter messages=N [--dry-run]|clean --stale N [--dry-run]|rename <id> <title> \u2014 Session management",
2271
2487
  " /system [prompt|clear] \u2014 Set/view/reset system prompt",
2272
2488
  " /context [status|reload] \u2014 Show/reload context layers",
2273
2489
  " /status \u2014 Show session info & token usage",
@@ -2403,8 +2619,9 @@ var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Map([
2403
2619
  ["snapshot", "Dev-state snapshots are written when the terminal REPL exits."],
2404
2620
  ["hooks", "Hook trust is granted per machine from the terminal."],
2405
2621
  ["hook", "Hook trust is granted per machine from the terminal."],
2406
- // 以下两条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
2407
- ["route", "Smart model routing is not applied to Web sessions yet (config.routing is REPL-only)."],
2622
+ // 以下一条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
2623
+ // (`route` 曾在这里,理由写的是「Web 尚未应用智能路由」;v0.4.269 让 routing 在 Web
2624
+ // 上真正生效之后那句话就不再成立,命令已在 Web 实现,见 commands/provider-commands.ts。)
2408
2625
  ["security", "Redaction status and toggles are terminal-only for now; /status shows the current profile."]
2409
2626
  ]);
2410
2627
  var webCommandHandlers = {
@@ -2412,6 +2629,7 @@ var webCommandHandlers = {
2412
2629
  model: handleModel,
2413
2630
  think: handleThink,
2414
2631
  plan: handlePlan,
2632
+ route: handleRoute,
2415
2633
  clear: handleClear,
2416
2634
  compact: handleCompact,
2417
2635
  status: handleStatus,