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
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  McpManager,
5
5
  SNAPSHOT_PROMPT,
6
6
  SkillManager,
7
+ applyPlanCommand,
7
8
  applyProfileCommand,
8
9
  applyThinkCommand,
9
10
  armDeferredSave,
@@ -14,21 +15,37 @@ import {
14
15
  costSourceNote,
15
16
  decideDeferredSave,
16
17
  decideModel,
18
+ describeRouting,
17
19
  estimateConversationTokens,
18
20
  evaluateContextPressure,
19
21
  filterMemoryEntries,
22
+ formatSessionSummaryLine,
20
23
  loadDevState,
24
+ parseBatchDeleteArgs,
25
+ parseCleanArgs,
21
26
  parseMemoryShowFilters,
27
+ parseModelCommand,
28
+ parseRouteCommand,
29
+ parseSessionListArgs,
22
30
  parseSimpleYaml,
23
31
  persistToolRound,
24
32
  readGitDiff,
33
+ resolveModelSwitch,
34
+ resolveProviderSwitch,
35
+ resolveSessionRef,
36
+ routeDecisionRows,
37
+ routingEmptyRulesNote,
25
38
  saveDevState,
26
39
  scanProject,
40
+ selectSessions,
41
+ selectStaleSessions,
27
42
  sessionHasMeaningfulContent,
43
+ sessionListJsonRow,
44
+ sessionListSummary,
28
45
  setupProxy,
29
46
  snapshotFile,
30
47
  stripRoutingTags
31
- } from "./chunk-HJVKCOAG.js";
48
+ } from "./chunk-7L4TEK7Q.js";
32
49
  import {
33
50
  CostTracker,
34
51
  computeCost,
@@ -58,7 +75,7 @@ import {
58
75
  setMaxOutputCap,
59
76
  spawnAgentContext,
60
77
  undoStack
61
- } from "./chunk-KIGKL3PZ.js";
78
+ } from "./chunk-FQJFGGKN.js";
62
79
  import {
63
80
  buildReviewPrompt,
64
81
  buildSecurityReviewPrompt
@@ -99,7 +116,7 @@ import {
99
116
  touchMemoryReferences,
100
117
  updateMemoryApproval,
101
118
  updateMemoryEntry
102
- } from "./chunk-7BO5H2QI.js";
119
+ } from "./chunk-6WMT7OQG.js";
103
120
  import "./chunk-4DEMGCK4.js";
104
121
  import "./chunk-UUSRWSSX.js";
105
122
  import "./chunk-CKH4KQ4E.js";
@@ -123,7 +140,7 @@ import {
123
140
  formatDoctorReport,
124
141
  loadContextFiles,
125
142
  writeCrashLog
126
- } from "./chunk-7IPMONKZ.js";
143
+ } from "./chunk-JLOQBYU4.js";
127
144
  import {
128
145
  describePlugin,
129
146
  getActivePluginAssets,
@@ -140,14 +157,14 @@ import {
140
157
  trustHook,
141
158
  trustPlugin,
142
159
  untrustHook
143
- } from "./chunk-UWQCTTP4.js";
160
+ } from "./chunk-IIXZ7IPN.js";
144
161
  import {
145
162
  ProviderRegistry,
146
163
  listBuiltInProviderMetadata
147
164
  } from "./chunk-UGY5SVNX.js";
148
165
  import {
149
166
  ConfigManager
150
- } from "./chunk-OCOGYIRY.js";
167
+ } from "./chunk-UD3BVNC5.js";
151
168
  import {
152
169
  CONTENT_ONLY_STREAM_REMINDER,
153
170
  TEE_FINAL_USER_NUDGE,
@@ -185,7 +202,7 @@ import {
185
202
  import {
186
203
  fileCheckpoints
187
204
  } from "./chunk-4BKXL7SM.js";
188
- import "./chunk-3VFOXEIL.js";
205
+ import "./chunk-NZPHEUJM.js";
189
206
  import {
190
207
  AGENTIC_BEHAVIOR_GUIDELINE,
191
208
  AUTHOR,
@@ -203,7 +220,7 @@ import {
203
220
  SKILLS_DIR_NAME,
204
221
  VERSION,
205
222
  buildUserIdentityPrompt
206
- } from "./chunk-ZP3RCZKM.js";
223
+ } from "./chunk-2BKRWTEQ.js";
207
224
 
208
225
  // src/index.ts
209
226
  import { program } from "commander";
@@ -722,7 +739,6 @@ import { platform } from "os";
722
739
 
723
740
  // src/repl/commands/constants.ts
724
741
  var SESSION_PAGE_SIZE = 20;
725
- var DEFAULT_SESSION_LIST_LIMIT = 50;
726
742
  var MAX_TITLE_WIDTH = 40;
727
743
 
728
744
  // src/repl/commands/utils.ts
@@ -801,73 +817,6 @@ function formatSessionDate(date) {
801
817
  if (diffDays < 7) return `${abs} (${diffDays} days ago)`;
802
818
  return abs;
803
819
  }
804
- function parseSessionListArgs(args) {
805
- let limit = DEFAULT_SESSION_LIST_LIMIT;
806
- let all = false;
807
- const filter = {};
808
- const warnings = [];
809
- let search = null;
810
- let days = null;
811
- let since = null;
812
- let sort = { field: "updated", order: "desc" };
813
- let json = false;
814
- let stale = null;
815
- let verbose = false;
816
- for (let i = 1; i < args.length; i++) {
817
- const a = args[i];
818
- if (a === "--all" || a === "-a") {
819
- all = true;
820
- } else if ((a === "--limit" || a === "-n") && i + 1 < args.length) {
821
- const n = Number(args[i + 1]);
822
- if (Number.isFinite(n) && n > 0) {
823
- limit = n;
824
- i++;
825
- }
826
- } else if (a === "--filter" && i + 1 < args.length) {
827
- const val = args[++i];
828
- const eq = val.indexOf("=");
829
- if (eq > 0) {
830
- const key = val.slice(0, eq);
831
- const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
832
- if (key === "provider") filter.provider = vals;
833
- else if (key === "model") filter.model = vals;
834
- else if (key === "messages") {
835
- const n = Number(vals[0]);
836
- if (Number.isFinite(n) && n >= 0) filter.messages = n;
837
- else warnings.push(`Invalid filter value for "messages": expected a non-negative integer, got "${vals[0]}"`);
838
- } else {
839
- warnings.push(`Unknown filter key: "${key}". Supported keys: provider, model, messages`);
840
- }
841
- } else {
842
- warnings.push(`Invalid --filter syntax: "${val}". Expected key=value`);
843
- }
844
- } else if ((a === "--search" || a === "-s") && i + 1 < args.length) {
845
- search = args[++i];
846
- } else if (a === "--days" && i + 1 < args.length) {
847
- const n = Number(args[++i]);
848
- if (Number.isFinite(n) && n > 0) days = n;
849
- } else if (a === "--since" && i + 1 < args.length) {
850
- since = args[++i];
851
- } else if (a === "--sort" && i + 1 < args.length) {
852
- const sv = args[++i];
853
- const colon = sv.indexOf(":");
854
- const field = colon > 0 ? sv.slice(0, colon) : sv;
855
- const order = colon > 0 ? sv.slice(colon + 1) : "desc";
856
- if (field === "updated" || field === "created" || field === "messages" || field === "size") {
857
- sort = { field, order: order === "asc" ? "asc" : "desc" };
858
- }
859
- } else if (a === "--json") {
860
- json = true;
861
- } else if (a === "--verbose" || a === "-v") {
862
- verbose = true;
863
- } else if (a === "--stale" && i + 1 < args.length) {
864
- const n = Number(args[++i]);
865
- if (Number.isFinite(n) && n > 0) stale = n;
866
- }
867
- }
868
- if (all) limit = -1;
869
- return { limit, all, filter, search, days, since, sort, json, stale, verbose, warnings };
870
- }
871
820
  function renderTranscriptMessage(msg, renderer) {
872
821
  if (msg.role === "user") {
873
822
  const text = getContentText(msg.content);
@@ -979,34 +928,41 @@ function createProviderCommands() {
979
928
  } else {
980
929
  targetId = args[0];
981
930
  }
982
- if (!ctx.providers.has(targetId)) {
983
- ctx.renderer.renderError(`Provider '${targetId}' is not configured. Run: aicli config`);
984
- return;
985
- }
986
- const targetProvider = ctx.providers.get(targetId);
987
- if (targetProvider.info.models.length === 0) {
988
- const models = await targetProvider.listModels();
989
- if (models.length === 0) {
990
- ctx.renderer.renderError(
991
- `Provider '${targetId}' has no available models. ` + (targetId === "ollama" ? "Make sure Ollama is running (`ollama serve`) and has models (`ollama pull <model>`)." : "Check provider configuration.")
992
- );
993
- return;
931
+ const configured = ctx.providers.has(targetId);
932
+ let availableModels = [];
933
+ let providerDefaultModel = "";
934
+ if (configured) {
935
+ const target = ctx.providers.get(targetId);
936
+ providerDefaultModel = target.info.defaultModel;
937
+ availableModels = target.info.models.map((m) => m.id);
938
+ if (availableModels.length === 0) {
939
+ availableModels = (await target.listModels()).map((m) => m.id);
994
940
  }
995
941
  }
996
- if (targetId === ctx.getCurrentProvider()) {
997
- ctx.renderer.printInfo(`Already using provider: ${targetId}`);
942
+ const session = ctx.sessions.current;
943
+ const outcome = resolveProviderSwitch({
944
+ targetId,
945
+ currentProvider: ctx.getCurrentProvider(),
946
+ configured,
947
+ availableModels,
948
+ configuredDefaultModel: ctx.config.get("defaultModels")[targetId],
949
+ providerDefaultModel,
950
+ messageCount: session?.messages.length ?? 0
951
+ });
952
+ if (outcome.status === "not-configured" || outcome.status === "no-models") {
953
+ ctx.renderer.renderError(outcome.message);
954
+ return;
955
+ }
956
+ if (outcome.status === "unchanged") {
957
+ ctx.renderer.printInfo(outcome.message);
998
958
  return;
999
959
  }
1000
- const session = ctx.sessions.current;
1001
- const msgCount = session?.messages.length ?? 0;
1002
- ctx.setProvider(targetId);
960
+ ctx.setProvider(outcome.providerId, outcome.model);
1003
961
  if (session) {
1004
- session.updateProvider(targetId, ctx.getCurrentModel());
962
+ session.updateProvider(outcome.providerId, outcome.model);
1005
963
  await ctx.sessions.save();
1006
964
  }
1007
- ctx.renderer.printSuccess(
1008
- `Switched to provider: ${targetId}` + (msgCount > 0 ? ` (conversation preserved: ${msgCount} messages)` : "")
1009
- );
965
+ ctx.renderer.printSuccess(outcome.message);
1010
966
  }
1011
967
  },
1012
968
  {
@@ -1014,29 +970,29 @@ function createProviderCommands() {
1014
970
  description: "Switch model, refresh live model list, or inspect model cache",
1015
971
  usage: "/model [name|refresh [provider]|cache [clear [provider]]]",
1016
972
  async execute(args, ctx) {
1017
- const sub = (args[0] ?? "").toLowerCase();
1018
- if (sub === "refresh") {
1019
- const providerId = args[1] ?? ctx.getCurrentProvider();
1020
- if (!ctx.providers.has(providerId)) {
1021
- ctx.renderer.printInfo(`Provider is not configured: ${providerId}`);
973
+ const intent = parseModelCommand(args, ctx.getCurrentProvider());
974
+ if (intent.kind === "refresh") {
975
+ if (!ctx.providers.has(intent.providerId)) {
976
+ ctx.renderer.printInfo(`Provider is not configured: ${intent.providerId}`);
1022
977
  return;
1023
978
  }
1024
- ctx.renderer.printInfo(`Refreshing models for ${providerId}...`);
979
+ ctx.renderer.printInfo(`Refreshing models for ${intent.providerId}...`);
1025
980
  try {
1026
- const models = await ctx.providers.refreshModels(providerId);
1027
- ctx.renderer.printSuccess(`Refreshed ${models.length} model(s) for ${providerId}.`);
981
+ const models = await ctx.providers.refreshModels(intent.providerId);
982
+ ctx.renderer.printSuccess(`Refreshed ${models.length} model(s) for ${intent.providerId}.`);
1028
983
  } catch (err) {
1029
984
  ctx.renderer.printInfo(`Model refresh failed: ${err instanceof Error ? err.message : String(err)}`);
1030
985
  }
1031
986
  return;
1032
987
  }
1033
- if (sub === "cache") {
1034
- if ((args[1] ?? "").toLowerCase() === "clear") {
1035
- const providerId = args[2];
1036
- ctx.providers.clearModelCache(providerId);
1037
- ctx.renderer.printSuccess(providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache.");
1038
- return;
1039
- }
988
+ if (intent.kind === "cache-clear") {
989
+ ctx.providers.clearModelCache(intent.providerId);
990
+ ctx.renderer.printSuccess(
991
+ intent.providerId ? `Cleared model cache for ${intent.providerId}.` : "Cleared model cache."
992
+ );
993
+ return;
994
+ }
995
+ if (intent.kind === "cache-show") {
1040
996
  const rows = ctx.providers.getModelCacheStatus();
1041
997
  if (rows.length === 0) {
1042
998
  ctx.renderer.printInfo("Model cache is empty. Use /model refresh to populate it.");
@@ -1048,9 +1004,11 @@ function createProviderCommands() {
1048
1004
  );
1049
1005
  return;
1050
1006
  }
1007
+ let knownModels = ctx.providers.has(ctx.getCurrentProvider()) ? ctx.providers.get(ctx.getCurrentProvider()).info.models.map((m) => m.id) : [];
1051
1008
  let targetModel;
1052
- if (!args[0]) {
1009
+ if (intent.kind === "select") {
1053
1010
  const models = await ctx.providers.listModels(ctx.getCurrentProvider());
1011
+ knownModels = models.map((m) => m.id);
1054
1012
  const currentModel = ctx.getCurrentModel();
1055
1013
  const items = models.map((m) => ({
1056
1014
  value: m.id,
@@ -1069,22 +1027,27 @@ function createProviderCommands() {
1069
1027
  if (!chosen) return;
1070
1028
  targetModel = chosen;
1071
1029
  } else {
1072
- targetModel = args[0];
1030
+ targetModel = intent.modelId;
1073
1031
  }
1074
- if (targetModel === ctx.getCurrentModel()) {
1075
- ctx.renderer.printInfo(`Already using model: ${targetModel}`);
1032
+ const session = ctx.sessions.current;
1033
+ const outcome = resolveModelSwitch({
1034
+ targetModel,
1035
+ currentModel: ctx.getCurrentModel(),
1036
+ provider: ctx.getCurrentProvider(),
1037
+ knownModels,
1038
+ messageCount: session?.messages.length ?? 0
1039
+ });
1040
+ if (outcome.status === "unchanged") {
1041
+ ctx.renderer.printInfo(outcome.message);
1076
1042
  return;
1077
1043
  }
1078
- const session = ctx.sessions.current;
1079
- const msgCount = session?.messages.length ?? 0;
1080
- ctx.setProvider(ctx.getCurrentProvider(), targetModel);
1044
+ ctx.setProvider(ctx.getCurrentProvider(), outcome.model);
1081
1045
  if (session) {
1082
- session.updateProvider(ctx.getCurrentProvider(), targetModel);
1046
+ session.updateProvider(ctx.getCurrentProvider(), outcome.model);
1083
1047
  await ctx.sessions.save();
1084
1048
  }
1085
- ctx.renderer.printSuccess(
1086
- `Switched to model: ${targetModel}` + (msgCount > 0 ? ` (conversation preserved: ${msgCount} messages)` : "")
1087
- );
1049
+ ctx.renderer.printSuccess(outcome.message);
1050
+ if (outcome.warning) console.log(theme.warning(` ${outcome.warning}`));
1088
1051
  }
1089
1052
  },
1090
1053
  {
@@ -1092,72 +1055,61 @@ function createProviderCommands() {
1092
1055
  description: "Smart model routing \u2014 enable/disable or inspect routing rules",
1093
1056
  usage: "/route [on|off|show|test <message>]",
1094
1057
  async execute(args, ctx) {
1095
- const sub = (args[0] ?? "show").toLowerCase();
1058
+ const intent = parseRouteCommand(args);
1096
1059
  const routing = ctx.config.get("routing");
1097
- if (sub === "on" || sub === "enable") {
1060
+ if (intent.kind === "enable") {
1098
1061
  ctx.config.setByPath("routing.enabled", "true");
1099
1062
  ctx.renderer.printSuccess("Smart model routing enabled.");
1100
- if (!routing || routing.rules.length === 0) {
1101
- ctx.renderer.printInfo(
1102
- 'No rules configured yet. Add rules under `routing.rules` in ~/.aicli/config.json.\nExample: { match: { tag: "fast" }, model: "claude-haiku-4-5" }'
1103
- );
1104
- }
1063
+ const note = routingEmptyRulesNote(routing);
1064
+ if (note) ctx.renderer.printInfo(note);
1105
1065
  return;
1106
1066
  }
1107
- if (sub === "off" || sub === "disable") {
1067
+ if (intent.kind === "disable") {
1108
1068
  ctx.config.setByPath("routing.enabled", "false");
1109
1069
  ctx.renderer.printSuccess("Smart model routing disabled.");
1110
1070
  return;
1111
1071
  }
1112
- if (sub === "test") {
1113
- const msg = args.slice(1).join(" ").trim();
1114
- if (!msg) {
1115
- console.log(theme.warning(" Usage: /route test <message>"));
1116
- return;
1117
- }
1118
- const decision = ctx.computeRoutingDecision(msg);
1119
- const marker = decision.overridden ? theme.accent("\u2192 ROUTED") : theme.dim("(unchanged)");
1120
- console.log();
1121
- console.log(` Input: ${theme.dim(msg)}`);
1122
- console.log(` Current: ${theme.info(ctx.getCurrentModel())}`);
1123
- console.log(` Decision: ${theme.info(decision.model)} ${marker}`);
1124
- console.log(` Reason: ${theme.dim(decision.reason)}`);
1125
- if (typeof decision.ruleIdx === "number") {
1126
- console.log(` Rule: #${decision.ruleIdx}`);
1127
- }
1128
- console.log();
1072
+ if (intent.kind === "usage" || intent.kind === "unknown") {
1073
+ console.log(theme.warning(` ${intent.message}`));
1129
1074
  return;
1130
1075
  }
1131
- if (sub === "show" || sub === "status") {
1132
- console.log();
1133
- console.log(` ${theme.heading("Smart Model Routing")}`);
1134
- console.log(` Status: ${routing?.enabled ? theme.success("enabled") : theme.dim("disabled")}`);
1135
- console.log(` Provider: ${theme.info(ctx.getCurrentProvider())}`);
1136
- console.log(` Current: ${theme.info(ctx.getCurrentModel())}`);
1137
- if (routing?.fallback) {
1138
- console.log(` Fallback: ${theme.info(routing.fallback)}`);
1139
- }
1076
+ if (intent.kind === "test") {
1077
+ const decision = ctx.computeRoutingDecision(intent.message);
1140
1078
  console.log();
1141
- if (!routing || routing.rules.length === 0) {
1142
- console.log(` ${theme.dim("(no rules configured \u2014 edit ~/.aicli/config.json `routing.rules`)")}`);
1143
- } else {
1144
- console.log(` ${theme.heading("Rules")} ${theme.dim(`(evaluated top-to-bottom)`)}:`);
1145
- routing.rules.forEach((r, i) => {
1146
- const parts = [];
1147
- if (r.match.tag) parts.push(`tag=#${r.match.tag}`);
1148
- 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" : ""}]`);
1149
- if (typeof r.match.maxLength === "number") parts.push(`maxLen=${r.match.maxLength}`);
1150
- if (typeof r.match.minLength === "number") parts.push(`minLen=${r.match.minLength}`);
1151
- const cond = parts.length > 0 ? parts.join(" & ") : theme.warning("(empty \u2014 never matches)");
1152
- console.log(` ${theme.dim(`#${i}`)} ${r.name ? theme.accent(r.name) + " " : ""}${cond} ${theme.dim("\u2192")} ${theme.info(r.model)}`);
1153
- });
1079
+ for (const row of routeDecisionRows(intent.message, ctx.getCurrentModel(), decision)) {
1080
+ const pad = `${row.label}:`.padEnd(10);
1081
+ const value = row.label === "Input" || row.label === "Reason" || row.label === "Rule" ? theme.dim(row.value) : theme.info(row.value);
1082
+ console.log(` ${pad}${value}`);
1154
1083
  }
1155
1084
  console.log();
1156
- console.log(` ${theme.dim("Commands: /route on | off | test <msg> | show")}`);
1157
- console.log();
1158
1085
  return;
1159
1086
  }
1160
- console.log(theme.warning(` Unknown subcommand: ${sub}. Usage: /route [on|off|show|test <message>]`));
1087
+ const view = describeRouting({
1088
+ routing,
1089
+ provider: ctx.getCurrentProvider(),
1090
+ currentModel: ctx.getCurrentModel()
1091
+ });
1092
+ console.log();
1093
+ console.log(` ${theme.heading("Smart Model Routing")}`);
1094
+ console.log(` Status: ${view.enabled ? theme.success("enabled") : theme.dim("disabled")}`);
1095
+ console.log(` Provider: ${theme.info(view.provider)}`);
1096
+ console.log(` Current: ${theme.info(view.currentModel)}`);
1097
+ if (view.fallback) console.log(` Fallback: ${theme.info(view.fallback)}`);
1098
+ console.log();
1099
+ if (view.noRulesNote) {
1100
+ console.log(` ${theme.dim(view.noRulesNote)}`);
1101
+ } else {
1102
+ console.log(` ${theme.heading("Rules")} ${theme.dim(`(evaluated top-to-bottom)`)}:`);
1103
+ for (const rule of view.rules) {
1104
+ const cond = rule.empty ? theme.warning(rule.condition) : rule.condition;
1105
+ console.log(
1106
+ ` ${theme.dim(`#${rule.index}`)} ${rule.name ? theme.accent(rule.name) + " " : ""}${cond} ${theme.dim("\u2192")} ${theme.info(rule.model)}`
1107
+ );
1108
+ }
1109
+ }
1110
+ console.log();
1111
+ console.log(` ${theme.dim(view.hint)}`);
1112
+ console.log();
1161
1113
  }
1162
1114
  },
1163
1115
  {
@@ -1191,56 +1143,33 @@ function createProviderCommands() {
1191
1143
  description: "Enter Plan Mode (read-only): AI can only read files and plan, not modify anything",
1192
1144
  usage: "/plan [execute|exit|status]",
1193
1145
  execute(args, ctx) {
1194
- const sub = args[0]?.toLowerCase();
1195
- if (!sub || sub === "enter") {
1196
- if (ctx.getPlanMode()) {
1197
- ctx.renderer.printInfo("Already in Plan Mode. Use /plan execute to start executing, or /plan exit to leave.");
1146
+ const outcome = applyPlanCommand(args[0], ctx.getPlanMode());
1147
+ if (outcome.changed) ctx.setPlanMode(outcome.planMode);
1148
+ switch (outcome.status) {
1149
+ case "entered":
1150
+ console.log();
1151
+ console.log(theme.warning(` ${outcome.summary}`));
1152
+ console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1153
+ for (const line of outcome.details) console.log(theme.dim(` ${line}`));
1154
+ console.log();
1198
1155
  return;
1199
- }
1200
- ctx.setPlanMode(true);
1201
- console.log();
1202
- console.log(theme.warning(" \u{1F4CB} Plan Mode activated"));
1203
- console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1204
- console.log(theme.dim(" AI can now ONLY use read-only tools:"));
1205
- console.log(theme.dim(" read_file \xB7 list_dir \xB7 grep_files \xB7 glob_files"));
1206
- console.log(theme.dim(" web_fetch \xB7 google_search \xB7 ask_user \xB7 write_todos"));
1207
- console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1208
- console.log(theme.dim(" Describe your task and the AI will analyze the codebase"));
1209
- console.log(theme.dim(" and create a detailed implementation plan."));
1210
- console.log(theme.dim(" When ready: /plan execute | To cancel: /plan exit"));
1211
- console.log();
1212
- return;
1213
- }
1214
- if (sub === "execute") {
1215
- if (!ctx.getPlanMode()) {
1216
- ctx.renderer.printInfo("Not in Plan Mode. Enter first with /plan");
1156
+ case "executing":
1157
+ ctx.renderer.printSuccess(outcome.summary);
1217
1158
  return;
1218
- }
1219
- ctx.setPlanMode(false);
1220
- ctx.renderer.printSuccess("Plan Mode ended \u2014 switching to execute mode. AI now has full tool access.");
1221
- return;
1222
- }
1223
- if (sub === "exit" || sub === "cancel") {
1224
- if (!ctx.getPlanMode()) {
1225
- ctx.renderer.printInfo("Not in Plan Mode.");
1159
+ case "already-active":
1160
+ case "not-active":
1161
+ case "cancelled":
1162
+ ctx.renderer.printInfo(outcome.summary);
1226
1163
  return;
1227
- }
1228
- ctx.setPlanMode(false);
1229
- ctx.renderer.printInfo("Plan Mode cancelled. Returning to normal mode.");
1230
- return;
1231
- }
1232
- if (sub === "status") {
1233
- const active = ctx.getPlanMode();
1234
- console.log();
1235
- console.log(` Plan Mode: ${active ? theme.warning("ACTIVE \u{1F4CB}") : theme.dim("inactive")}`);
1236
- if (active) {
1237
- console.log(theme.dim(" Tools restricted to read-only set."));
1238
- console.log(theme.dim(" Use /plan execute to start executing, or /plan exit to cancel."));
1239
- }
1240
- console.log();
1241
- return;
1164
+ case "status":
1165
+ console.log();
1166
+ console.log(` ${outcome.planMode ? theme.warning(outcome.summary) : theme.dim(outcome.summary)}`);
1167
+ for (const line of outcome.details) console.log(theme.dim(` ${line}`));
1168
+ console.log();
1169
+ return;
1170
+ default:
1171
+ ctx.renderer.renderError(outcome.summary);
1242
1172
  }
1243
- ctx.renderer.renderError("Usage: /plan [execute|exit|status]");
1244
1173
  }
1245
1174
  }
1246
1175
  ];
@@ -1409,7 +1338,7 @@ No tools match "${filter}".
1409
1338
  const { join: join6 } = await import("path");
1410
1339
  const { existsSync: existsSync6 } = await import("fs");
1411
1340
  const { getGitRoot: getGitRoot2 } = await import("./git-context-EXOEHQSF.js");
1412
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-N3CHKZJY.js");
1341
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-3VMMZJDS.js");
1413
1342
  const { approveProject, hashMcpFile } = await import("./project-trust-NKYHL3VZ.js");
1414
1343
  const cwd = process.cwd();
1415
1344
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -2329,7 +2258,7 @@ function createReviewCommands() {
2329
2258
  usage: "/test [command|filter]",
2330
2259
  async execute(args, ctx) {
2331
2260
  try {
2332
- const { executeTests } = await import("./run-tests-76M6AC3G.js");
2261
+ const { executeTests } = await import("./run-tests-EL54INOI.js");
2333
2262
  const argStr = args.join(" ").trim();
2334
2263
  let testArgs = {};
2335
2264
  if (argStr) {
@@ -3093,7 +3022,7 @@ function createSessionCommands() {
3093
3022
  {
3094
3023
  name: "session",
3095
3024
  description: "Session management",
3096
- usage: "/session new|list [--limit N|--all] [--filter provider=|model=|messages=N] [--search kw] [--days N|--stale N] [--sort f[:o]] [--json] [--verbose]|load <id>|show <id> [page]|page <n>|remove <id>|clean --stale N [--dry-run]",
3025
+ usage: "/session new|list [--limit N|--all] [--filter provider=|model=|messages=N] [--search kw] [--days N|--stale N] [--sort f[:o]] [--json] [--verbose]|load <id>|show <id> [page]|page <n>|rename <id> <title>|remove <id>|clean --stale N [--dry-run]",
3097
3026
  async execute(args, ctx) {
3098
3027
  const sub = args[0];
3099
3028
  if (sub === "new") {
@@ -3105,139 +3034,44 @@ function createSessionCommands() {
3105
3034
  ctx.renderer.printSuccess("New session started.");
3106
3035
  } else if (sub === "list") {
3107
3036
  const opts = parseSessionListArgs(args);
3108
- for (const w of opts.warnings) {
3109
- ctx.renderer.printInfo(`\u26A0 ${w}`);
3110
- }
3111
- const needFileSize = opts.verbose || opts.sort.field === "size";
3112
- let allSessions = ctx.sessions.listSessions({ includeFileSize: needFileSize });
3113
- if (allSessions.length === 0) {
3037
+ for (const w of opts.warnings) ctx.renderer.printInfo(`\u26A0 ${w}`);
3038
+ const all = ctx.sessions.listSessions({ includeFileSize: opts.verbose || opts.sort.field === "size" });
3039
+ if (all.length === 0) {
3114
3040
  ctx.renderer.printInfo("No saved sessions.");
3115
3041
  return;
3116
3042
  }
3117
- const hasFilters = opts.filter.provider || opts.filter.model || opts.filter.messages !== void 0;
3118
- const hasSearch = !!opts.search;
3119
- const hasTimeFilter = opts.days !== null || opts.since !== null;
3120
- const hasStale = opts.stale !== null;
3121
- if (hasFilters) {
3122
- const p = opts.filter.provider;
3123
- const m = opts.filter.model;
3124
- const msg = opts.filter.messages;
3125
- allSessions = allSessions.filter((s) => {
3126
- if (p && !p.includes(s.provider.toLowerCase())) return false;
3127
- if (m && !m.some((kw) => s.model.toLowerCase().includes(kw))) return false;
3128
- if (msg !== void 0 && s.messageCount !== msg) return false;
3129
- return true;
3130
- });
3131
- }
3132
- if (hasSearch) {
3133
- const q = opts.search.toLowerCase();
3134
- allSessions = allSessions.filter(
3135
- (s) => (s.title ?? "").toLowerCase().includes(q) || s.id.toLowerCase().includes(q)
3136
- );
3137
- }
3138
- if (hasTimeFilter) {
3139
- const now = Date.now();
3140
- let cutoff = 0;
3141
- if (opts.since) {
3142
- cutoff = new Date(opts.since).getTime();
3143
- } else if (opts.days !== null) {
3144
- cutoff = now - opts.days * 24 * 60 * 60 * 1e3;
3145
- }
3146
- if (!isNaN(cutoff)) {
3147
- allSessions = allSessions.filter((s) => s.updated.getTime() >= cutoff);
3148
- }
3149
- }
3150
- const staleCutoff = hasStale ? Date.now() - opts.stale * 24 * 60 * 60 * 1e3 : 0;
3151
- if (hasStale) {
3152
- allSessions = allSessions.filter((s) => s.updated.getTime() < staleCutoff);
3153
- }
3154
- const defaultSort = opts.sort.field === "updated" && opts.sort.order === "desc";
3155
- if (!defaultSort) {
3156
- const mul = opts.sort.order === "asc" ? 1 : -1;
3157
- allSessions = [...allSessions].sort((a, b) => {
3158
- switch (opts.sort.field) {
3159
- case "created":
3160
- return (a.created.getTime() - b.created.getTime()) * mul;
3161
- case "messages":
3162
- return (a.messageCount - b.messageCount) * mul;
3163
- case "size":
3164
- return ((a.fileSize ?? 0) - (b.fileSize ?? 0)) * mul;
3165
- default:
3166
- return (a.updated.getTime() - b.updated.getTime()) * mul;
3167
- }
3168
- });
3169
- }
3170
- const totalCount = allSessions.length;
3171
- if (totalCount === 0) {
3043
+ const view = selectSessions(all, opts);
3044
+ if (view.totalCount === 0) {
3172
3045
  ctx.renderer.printInfo("No sessions match the filter criteria.");
3173
3046
  return;
3174
3047
  }
3175
3048
  if (opts.json) {
3176
- const sliced2 = opts.limit > 0 ? allSessions.slice(0, opts.limit) : allSessions;
3177
- const payload = sliced2.map((s) => ({
3178
- id: s.id,
3179
- title: s.title ?? null,
3180
- provider: s.provider,
3181
- model: s.model,
3182
- messageCount: s.messageCount,
3183
- created: s.created.toISOString(),
3184
- updated: s.updated.toISOString(),
3185
- ...needFileSize ? { fileSize: s.fileSize } : {},
3186
- ...s.branchCount !== void 0 ? { branchCount: s.branchCount, activeBranch: s.activeBranch } : {}
3187
- }));
3188
- console.log(JSON.stringify(payload, null, 2));
3189
- if (opts.limit > 0 && totalCount > opts.limit) {
3049
+ console.log(JSON.stringify(view.shown.map((s) => sessionListJsonRow(s, view.needsFileSize)), null, 2));
3050
+ if (view.shown.length < view.totalCount) {
3190
3051
  console.error(
3191
3052
  `
3192
- (truncated: ${opts.limit} of ${totalCount} sessions shown; use --all for full list)`
3053
+ (truncated: ${view.shown.length} of ${view.totalCount} sessions shown; use --all for full list)`
3193
3054
  );
3194
3055
  }
3195
3056
  return;
3196
3057
  }
3197
- const sliced = opts.limit > 0 ? allSessions.slice(0, opts.limit) : allSessions;
3198
3058
  const headers = ["ID (short)", "Provider", "Model", "Messages"];
3199
- if (opts.verbose) {
3200
- headers.push("Size");
3201
- headers.push("Br");
3202
- }
3059
+ if (opts.verbose) headers.push("Size", "Br");
3203
3060
  headers.push("Date", "Title");
3204
- const rows = sliced.map((s) => {
3205
- const isStale = hasStale ? s.updated.getTime() < staleCutoff : false;
3061
+ const rows = view.shown.map((s) => {
3062
+ const isStale = view.staleCutoff !== null && s.updated.getTime() < view.staleCutoff;
3206
3063
  const dim = isStale ? theme.dim : (x) => String(x);
3207
- const row = [
3208
- dim(s.id.slice(0, 8)),
3209
- dim(s.provider),
3210
- dim(s.model),
3211
- s.messageCount
3212
- ];
3064
+ const row = [dim(s.id.slice(0, 8)), dim(s.provider), dim(s.model), s.messageCount];
3213
3065
  if (opts.verbose) {
3214
- row.push(
3215
- s.fileSize !== void 0 ? formatFileSize(s.fileSize) : "\u2014",
3216
- s.branchCount ?? 1
3217
- );
3066
+ row.push(s.fileSize !== void 0 ? formatFileSize(s.fileSize) : "\u2014", s.branchCount ?? 1);
3218
3067
  }
3219
- row.push(
3220
- formatSessionDate(s.updated),
3221
- dim(truncateTitle(s.title ?? "(untitled)", MAX_TITLE_WIDTH))
3222
- );
3068
+ row.push(formatSessionDate(s.updated), dim(truncateTitle(s.title ?? "(untitled)", MAX_TITLE_WIDTH)));
3223
3069
  return row;
3224
3070
  });
3225
3071
  console.log();
3226
3072
  ctx.renderer.printTable(headers, rows);
3227
- const totalMessages = allSessions.reduce((sum, s) => sum + s.messageCount, 0);
3228
- const shown = sliced.length;
3229
- const filterHints = [];
3230
- if (hasFilters) filterHints.push("filtered");
3231
- if (hasSearch) filterHints.push("searched");
3232
- if (hasTimeFilter) filterHints.push("time-limited");
3233
- if (hasStale) filterHints.push(`stale >${opts.stale}d`);
3234
- const filterTag = filterHints.length > 0 ? ` (${filterHints.join(", ")})` : "";
3235
- console.log(
3236
- theme.dim(
3237
- ` \u2500\u2500 ${shown} of ${totalCount} sessions${filterTag} \xB7 ${totalMessages} messages total`
3238
- )
3239
- );
3240
- if (shown < totalCount) {
3073
+ console.log(theme.dim(` \u2500\u2500 ${sessionListSummary(view)}`));
3074
+ if (view.shown.length < view.totalCount) {
3241
3075
  console.log(theme.dim(` Use --all to show all, or --limit N to change count.`));
3242
3076
  }
3243
3077
  } else if (sub === "load" || sub === "show") {
@@ -3246,19 +3080,18 @@ function createSessionCommands() {
3246
3080
  ctx.renderer.renderError(`Usage: /session ${sub} <id>${sub === "show" ? " [page]" : ""}`);
3247
3081
  return;
3248
3082
  }
3249
- const sessions = ctx.sessions.listSessions();
3250
- const matches = sessions.filter((s) => s.id.startsWith(id));
3251
- if (matches.length === 0) {
3252
- ctx.renderer.renderError(`Session '${id}' not found.`);
3083
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), id, "most-recent");
3084
+ if (ref.status !== "ok") {
3085
+ ctx.renderer.renderError(ref.message);
3253
3086
  return;
3254
3087
  }
3255
- if (matches.length > 1) {
3256
- console.log(theme.warning(` \u26A0 Ambiguous prefix '${id}' matches ${matches.length} sessions \u2014 loading most recent:`));
3257
- for (const m of matches.slice(0, 5)) {
3088
+ if (ref.message) {
3089
+ console.log(theme.warning(` ${ref.message}`));
3090
+ for (const m of ref.ambiguousWith.slice(0, 5)) {
3258
3091
  console.log(theme.dim(` ${m.id.slice(0, 12)} ${m.title ?? "(untitled)"}`));
3259
3092
  }
3260
3093
  }
3261
- const match = matches[0];
3094
+ const match = ref.match;
3262
3095
  ctx.sessions.loadSession(match.id);
3263
3096
  ctx.setProvider(match.provider, match.model);
3264
3097
  ctx.resetSessionTokenUsage();
@@ -3282,70 +3115,54 @@ function createSessionCommands() {
3282
3115
  return;
3283
3116
  }
3284
3117
  renderTranscriptPage(cur.messages, ctx.renderer, n);
3118
+ } else if (sub === "rename") {
3119
+ const id = args[1];
3120
+ const newTitle = args.slice(2).join(" ").trim();
3121
+ if (!id || !newTitle) {
3122
+ ctx.renderer.renderError("Usage: /session rename <id> <new title>");
3123
+ return;
3124
+ }
3125
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), id, "strict");
3126
+ if (ref.status !== "ok") {
3127
+ ctx.renderer.renderError(ref.message);
3128
+ if (ref.status === "ambiguous") {
3129
+ for (const m of ref.matches.slice(0, 10)) console.log(theme.dim(` ${formatSessionSummaryLine(m)}`));
3130
+ }
3131
+ return;
3132
+ }
3133
+ const priorId = ctx.sessions.current?.id ?? null;
3134
+ const isCurrent = priorId === ref.match.id;
3135
+ const session = isCurrent ? ctx.sessions.current : ctx.sessions.loadSession(ref.match.id);
3136
+ session.title = newTitle;
3137
+ await ctx.sessions.save();
3138
+ if (!isCurrent && priorId) {
3139
+ ctx.sessions.loadSession(priorId);
3140
+ }
3141
+ ctx.renderer.printSuccess(`Renamed session ${ref.match.id.slice(0, 8)}: "${newTitle}"`);
3285
3142
  } else if (sub === "remove" || sub === "delete") {
3286
3143
  if (args[1] === "--filter") {
3287
- let filterMessages;
3288
- let dryRun = false;
3289
- for (let i = 1; i < args.length; i++) {
3290
- if (args[i] === "--filter" && i + 1 < args.length) {
3291
- const val = args[++i];
3292
- const eq = val.indexOf("=");
3293
- if (eq > 0) {
3294
- const key = val.slice(0, eq);
3295
- const vals = val.slice(eq + 1).split(",").map((s) => s.trim().toLowerCase());
3296
- if (key === "messages") {
3297
- const n = Number(vals[0]);
3298
- if (Number.isFinite(n) && n >= 0) filterMessages = n;
3299
- else {
3300
- ctx.renderer.renderError(`Invalid filter value for "messages": expected a non-negative integer, got "${vals[0]}"`);
3301
- return;
3302
- }
3303
- } else {
3304
- ctx.renderer.renderError(`Unsupported filter key for remove: "${key}". Only "messages" is supported.`);
3305
- return;
3306
- }
3307
- } else {
3308
- ctx.renderer.renderError(`Invalid --filter syntax: "${val}". Expected key=value (e.g., messages=0)`);
3309
- return;
3310
- }
3311
- } else if (args[i] === "--dry-run") {
3312
- dryRun = true;
3313
- }
3314
- }
3315
- if (filterMessages === void 0) {
3316
- ctx.renderer.renderError("Usage: /session remove --filter messages=N [--dry-run]");
3144
+ const opts = parseBatchDeleteArgs(args);
3145
+ if (opts.error) {
3146
+ ctx.renderer.renderError(opts.error);
3317
3147
  return;
3318
3148
  }
3319
- const allSessions = ctx.sessions.listSessions();
3320
- const matches2 = allSessions.filter((s) => s.messageCount === filterMessages);
3321
- if (matches2.length === 0) {
3322
- ctx.renderer.printInfo(`No sessions with ${filterMessages} message(s) found.`);
3149
+ const matches = ctx.sessions.listSessions().filter((s) => s.messageCount === opts.messages);
3150
+ if (matches.length === 0) {
3151
+ ctx.renderer.printInfo(`No sessions with ${opts.messages} message(s) found.`);
3323
3152
  return;
3324
3153
  }
3325
3154
  console.log();
3326
- console.log(
3327
- theme.warning(` Found ${matches2.length} session(s) with ${filterMessages} message(s):`)
3328
- );
3329
- for (const s of matches2.slice(0, 20)) {
3330
- console.log(
3331
- theme.dim(
3332
- ` ${s.id.slice(0, 12)} ${s.provider}/${s.model} ${String(s.messageCount).padStart(4)} msgs ${s.updated.toISOString().slice(0, 10)} ${truncateTitle(s.title ?? "(untitled)", 60)}`
3333
- )
3334
- );
3335
- }
3336
- if (matches2.length > 20) {
3337
- console.log(theme.dim(` ... and ${matches2.length - 20} more`));
3338
- }
3339
- if (dryRun) {
3340
- console.log(
3341
- theme.info(`
3342
- --dry-run: no sessions were deleted. Remove --dry-run to actually delete.`)
3343
- );
3155
+ console.log(theme.warning(` Found ${matches.length} session(s) with ${opts.messages} message(s):`));
3156
+ for (const s of matches.slice(0, 20)) console.log(theme.dim(` ${formatSessionSummaryLine(s)}`));
3157
+ if (matches.length > 20) console.log(theme.dim(` ... and ${matches.length - 20} more`));
3158
+ if (opts.dryRun) {
3159
+ console.log(theme.info(`
3160
+ --dry-run: no sessions were deleted. Remove --dry-run to actually delete.`));
3344
3161
  return;
3345
3162
  }
3346
3163
  console.log();
3347
3164
  const choice2 = await ctx.select(
3348
- `Delete all ${matches2.length} matching sessions?`,
3165
+ `Delete all ${matches.length} matching sessions?`,
3349
3166
  [
3350
3167
  { label: "No \u2014 keep them", value: "no" },
3351
3168
  { label: "Yes \u2014 delete permanently", value: "yes", hint: "unlinks all matching history files" }
@@ -3357,17 +3174,12 @@ function createSessionCommands() {
3357
3174
  }
3358
3175
  let deleted = 0;
3359
3176
  let failed = 0;
3360
- for (const s of matches2) {
3361
- if (ctx.sessions.deleteSession(s.id)) {
3362
- deleted++;
3363
- } else {
3364
- failed++;
3365
- }
3177
+ for (const s of matches) {
3178
+ if (ctx.sessions.deleteSession(s.id)) deleted++;
3179
+ else failed++;
3366
3180
  }
3367
3181
  const currentStillExists = ctx.sessions.current !== null;
3368
- ctx.renderer.printSuccess(
3369
- `Removed: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`
3370
- );
3182
+ ctx.renderer.printSuccess(`Removed: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`);
3371
3183
  if (!currentStillExists) {
3372
3184
  ctx.sessions.createSession(ctx.getCurrentProvider(), ctx.getCurrentModel());
3373
3185
  ctx.resetSessionTokenUsage();
@@ -3381,20 +3193,15 @@ function createSessionCommands() {
3381
3193
  ctx.renderer.renderError("Usage: /session remove <id> | /session remove --filter messages=N [--dry-run]");
3382
3194
  return;
3383
3195
  }
3384
- const sessions = ctx.sessions.listSessions();
3385
- const matches = sessions.filter((s) => s.id.startsWith(id));
3386
- if (matches.length === 0) {
3387
- ctx.renderer.renderError(`Session '${id}' not found.`);
3388
- return;
3389
- }
3390
- if (matches.length > 1) {
3391
- ctx.renderer.renderError(`Ambiguous prefix '${id}' matches ${matches.length} sessions \u2014 be more specific:`);
3392
- for (const m of matches.slice(0, 10)) {
3393
- console.log(theme.dim(` ${m.id.slice(0, 12)} ${String(m.messageCount).padStart(4)} msgs ${m.updated.toLocaleDateString()} ${m.title ?? "(untitled)"}`));
3196
+ const ref = resolveSessionRef(ctx.sessions.listSessions(), id, "strict");
3197
+ if (ref.status !== "ok") {
3198
+ ctx.renderer.renderError(ref.message);
3199
+ if (ref.status === "ambiguous") {
3200
+ for (const m of ref.matches.slice(0, 10)) console.log(theme.dim(` ${formatSessionSummaryLine(m)}`));
3394
3201
  }
3395
3202
  return;
3396
3203
  }
3397
- const match = matches[0];
3204
+ const match = ref.match;
3398
3205
  const isCurrent = ctx.sessions.current?.id === match.id;
3399
3206
  console.log();
3400
3207
  console.log(theme.warning(" About to permanently delete session (no undo):"));
@@ -3421,44 +3228,19 @@ function createSessionCommands() {
3421
3228
  ctx.renderer.printInfo("Current session was deleted \u2014 started a fresh session.");
3422
3229
  }
3423
3230
  } else if (sub === "clean") {
3424
- let staleDays = 90;
3425
- let dryRun = false;
3426
- for (let i = 1; i < args.length; i++) {
3427
- if (args[i] === "--stale" && i + 1 < args.length) {
3428
- const n = Number(args[++i]);
3429
- if (Number.isFinite(n) && n > 0) staleDays = n;
3430
- } else if (args[i] === "--dry-run") {
3431
- dryRun = true;
3432
- }
3433
- }
3434
- const cutoff = Date.now() - staleDays * 24 * 60 * 60 * 1e3;
3435
- const allSessions = ctx.sessions.listSessions();
3436
- const stale = allSessions.filter((s) => s.updated.getTime() < cutoff);
3231
+ const opts = parseCleanArgs(args);
3232
+ const stale = selectStaleSessions(ctx.sessions.listSessions(), opts.staleDays);
3437
3233
  if (stale.length === 0) {
3438
- ctx.renderer.printInfo(`No sessions older than ${staleDays} days found.`);
3234
+ ctx.renderer.printInfo(`No sessions older than ${opts.staleDays} days found.`);
3439
3235
  return;
3440
3236
  }
3441
3237
  console.log();
3442
- console.log(
3443
- theme.warning(
3444
- ` Found ${stale.length} session(s) not updated in ${staleDays}+ days:`
3445
- )
3446
- );
3447
- for (const s of stale.slice(0, 20)) {
3448
- console.log(
3449
- theme.dim(
3450
- ` ${s.id.slice(0, 12)} ${String(s.messageCount).padStart(4)} msgs ${s.updated.toISOString().slice(0, 10)} ${truncateTitle(s.title ?? "(untitled)", 60)}`
3451
- )
3452
- );
3453
- }
3454
- if (stale.length > 20) {
3455
- console.log(theme.dim(` ... and ${stale.length - 20} more`));
3456
- }
3457
- if (dryRun) {
3458
- console.log(
3459
- theme.info(`
3460
- --dry-run: no sessions were deleted. Remove --dry-run to actually delete.`)
3461
- );
3238
+ console.log(theme.warning(` Found ${stale.length} session(s) not updated in ${opts.staleDays}+ days:`));
3239
+ for (const s of stale.slice(0, 20)) console.log(theme.dim(` ${formatSessionSummaryLine(s)}`));
3240
+ if (stale.length > 20) console.log(theme.dim(` ... and ${stale.length - 20} more`));
3241
+ if (opts.dryRun) {
3242
+ console.log(theme.info(`
3243
+ --dry-run: no sessions were deleted. Remove --dry-run to actually delete.`));
3462
3244
  return;
3463
3245
  }
3464
3246
  console.log();
@@ -3476,17 +3258,12 @@ function createSessionCommands() {
3476
3258
  let deleted = 0;
3477
3259
  let failed = 0;
3478
3260
  for (const s of stale) {
3479
- if (ctx.sessions.deleteSession(s.id)) {
3480
- deleted++;
3481
- } else {
3482
- failed++;
3483
- }
3261
+ if (ctx.sessions.deleteSession(s.id)) deleted++;
3262
+ else failed++;
3484
3263
  }
3485
- ctx.renderer.printSuccess(
3486
- `Cleaned up: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`
3487
- );
3264
+ ctx.renderer.printSuccess(`Cleaned up: ${deleted} deleted${failed > 0 ? `, ${failed} failed` : ""}.`);
3488
3265
  } else {
3489
- ctx.renderer.renderError("Usage: /session new|list|load <id>|show <id> [page]|page <n>|remove <id>|remove --filter messages=N [--dry-run]|clean --stale N [--dry-run]");
3266
+ ctx.renderer.renderError("Usage: /session new|list|load <id>|show <id> [page]|page <n>|rename <id> <title>|remove <id>|remove --filter messages=N [--dry-run]|clean --stale N [--dry-run]");
3490
3267
  }
3491
3268
  }
3492
3269
  },
@@ -7559,7 +7336,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7559
7336
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7560
7337
  process.exit(1);
7561
7338
  }
7562
- const { startWebServer } = await import("./server-C523CIGN.js");
7339
+ const { startWebServer } = await import("./server-WUSJWC3A.js");
7563
7340
  await startWebServer({ port, host: options.host });
7564
7341
  });
7565
7342
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -7726,11 +7503,11 @@ program.command("sessions").description("List recent conversation sessions").opt
7726
7503
  console.log(footer + "\n");
7727
7504
  });
7728
7505
  program.command("usage").description("Show token + cost usage grouped by provider/model (cross-session)").option("--days <n>", "Only the last N days (inclusive of today)").option("--month <ym>", "Only a specific month, format YYYY-MM (e.g. 2026-06)").option("--json", "Output as JSON (for scripting)").action(async (options) => {
7729
- const { runUsageCli } = await import("./usage-EFHXXMYX.js");
7506
+ const { runUsageCli } = await import("./usage-CW6BT2UV.js");
7730
7507
  await runUsageCli(options);
7731
7508
  });
7732
7509
  program.command("doctor").description("Health check: API keys, config, MCP, recent crashes, tool usage, disk usage").option("--json", "Output as JSON (for scripting)").option("--reset-stats", "Reset accumulated tool usage statistics").action(async (options) => {
7733
- const { runDoctorCli } = await import("./doctor-cli-Z3TGHTSX.js");
7510
+ const { runDoctorCli } = await import("./doctor-cli-WVMEWMYE.js");
7734
7511
  const argv = process.argv.slice(2);
7735
7512
  await runDoctorCli({
7736
7513
  json: !!options.json || argv.includes("--json"),
@@ -7739,7 +7516,7 @@ program.command("doctor").description("Health check: API keys, config, MCP, rece
7739
7516
  });
7740
7517
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
7741
7518
  try {
7742
- const batch = await import("./batch-2I5TPZA2.js");
7519
+ const batch = await import("./batch-6QZSWCBL.js");
7743
7520
  switch (action) {
7744
7521
  case "submit":
7745
7522
  if (!arg) {
@@ -7782,7 +7559,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
7782
7559
  }
7783
7560
  });
7784
7561
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
7785
- const { startMcpServer } = await import("./server-R7D5RR36.js");
7562
+ const { startMcpServer } = await import("./server-6C7GITHY.js");
7786
7563
  await startMcpServer({
7787
7564
  allowDestructive: !!options.allowDestructive,
7788
7565
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7795,7 +7572,7 @@ program.command("pr <action> [target]").description("PR review helpers: review |
7795
7572
  console.error(`Unknown pr action: ${action}. Use review | security-review | summarize.`);
7796
7573
  process.exit(1);
7797
7574
  }
7798
- const { runPrCli } = await import("./pr-ZUGSWQ5R.js");
7575
+ const { runPrCli } = await import("./pr-GFB5JC26.js");
7799
7576
  const result = await runPrCli({
7800
7577
  action,
7801
7578
  target,
@@ -7815,8 +7592,8 @@ program.command("pr <action> [target]").description("PR review helpers: review |
7815
7592
  process.exit(result.exitCode);
7816
7593
  });
7817
7594
  program.command("ci").description("Headless PR review (code + security) \u2014 reads git/gh diff, optionally posts to PR. Designed for GitHub Actions.").option("--pr <num>", "PR number or URL; diff fetched via gh pr diff <value>", (v) => /^#?\d+$/.test(v) ? parseInt(v.replace(/^#/, ""), 10) : v).option("--base <ref>", "Base ref for git diff <ref>...HEAD (ignored when --pr set)").option("--post", "Post review as a PR comment (requires gh CLI + GH_TOKEN, needs --pr)").option("--no-update", "Always create a new comment instead of updating the previous aicli review").option("--skip-code", "Skip the code review section").option("--skip-security", "Skip the security review section").option("--detailed", "Use the detailed code-review prompt").option("--max-diff <n>", "Max diff chars sent to the model (default 30000)", (v) => parseInt(v, 10)).option("--provider <id>", "Override provider (default: config.defaultProvider)").option("--model <id>", "Override model").option("--dry-run", "Print result to stdout instead of posting (overrides --post)").option("--json", "Output a structured JSON report").option("--markdown", "Output Markdown report (default)").option("--sarif", "Output SARIF 2.1.0 for code scanning artifacts").option("--fail-on-security-high", "Fail when the security review contains HIGH/CRITICAL findings (default)").option("--no-fail-on-security-high", "Do not fail on security HIGH/CRITICAL findings").option("--fail-on-test-failure", "Fail when the report mentions test failures (default)").option("--no-fail-on-test-failure", "Do not fail on test failure findings").option("--fail-on-lint-failure", "Fail when the report mentions lint/typecheck failures (default)").option("--no-fail-on-lint-failure", "Do not fail on lint/typecheck failure findings").action(async (options) => {
7818
- const { runCi } = await import("./ci-JFC45LIS.js");
7819
- const { formatCiResult } = await import("./ci-format-P3LIZHUR.js");
7595
+ const { runCi } = await import("./ci-F4ESOYI5.js");
7596
+ const { formatCiResult } = await import("./ci-format-AIRW3TVQ.js");
7820
7597
  const formats = [options.json, options.markdown, options.sarif].filter(Boolean).length;
7821
7598
  if (formats > 1) {
7822
7599
  console.error("Choose only one output format: --json, --markdown, or --sarif.");
@@ -7974,7 +7751,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
7974
7751
  config.get("customProviders"),
7975
7752
  config.getConfigDir()
7976
7753
  );
7977
- const { startHub } = await import("./hub-NEWIFMQ3.js");
7754
+ const { startHub } = await import("./hub-JJCCCU3O.js");
7978
7755
  await startHub(
7979
7756
  {
7980
7757
  topic: topic ?? "",