jinzd-ai-cli 0.4.267 → 0.4.269

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 (31) hide show
  1. package/dist/{batch-GY5MAN5F.js → batch-HUOYPVJQ.js} +2 -2
  2. package/dist/{chunk-THHPKZJ3.js → chunk-3TOMHXDW.js} +1 -1
  3. package/dist/{chunk-7HECI3U5.js → chunk-4HTJMLFY.js} +3 -1
  4. package/dist/{chunk-EWRZMIQE.js → chunk-5IUFUCIT.js} +1 -1
  5. package/dist/{chunk-WPN7LDLM.js → chunk-5PRWBJCI.js} +1 -1
  6. package/dist/{chunk-QN7WD2SM.js → chunk-6PROC2JO.js} +3 -3
  7. package/dist/{chunk-6C6FLDGH.js → chunk-AAERHPBN.js} +1 -1
  8. package/dist/{chunk-5AEOIETL.js → chunk-DJNGUPM3.js} +4 -4
  9. package/dist/{chunk-FI6IWUXW.js → chunk-KIHNZBEA.js} +1 -1
  10. package/dist/{chunk-ARYCWJN6.js → chunk-NVN3FHXI.js} +1 -1
  11. package/dist/{chunk-RVNL7MQG.js → chunk-QPPUYCRF.js} +1 -1
  12. package/dist/{chunk-JH343BDI.js → chunk-S3F5LOV3.js} +313 -2
  13. package/dist/{chunk-XLLUQEGO.js → chunk-V6KQ67VF.js} +1 -1
  14. package/dist/{chunk-5CU72V6P.js → chunk-XJAGGJVU.js} +3 -3
  15. package/dist/{ci-MUEQSNKU.js → ci-4QS2J7BG.js} +4 -4
  16. package/dist/{ci-format-2ZGGSOFK.js → ci-format-6L6M67WC.js} +2 -2
  17. package/dist/{constants-SZTJESDF.js → constants-IO5VJLEE.js} +1 -1
  18. package/dist/{doctor-cli-Z272364P.js → doctor-cli-ZU4E2DDL.js} +4 -4
  19. package/dist/electron-server.js +455 -58
  20. package/dist/{hub-MRLGKMRI.js → hub-AMDWXSKZ.js} +1 -1
  21. package/dist/index.js +117 -245
  22. package/dist/{persistent-memory-3FTOC7QT.js → persistent-memory-52SE5K44.js} +2 -2
  23. package/dist/{persistent-memory-HZASMDJQ.js → persistent-memory-H4DNDIOQ.js} +2 -2
  24. package/dist/{pr-VLLIALRK.js → pr-Z4O5H6I3.js} +4 -4
  25. package/dist/{run-tests-NTDZ6DDU.js → run-tests-FZSYHWYX.js} +2 -2
  26. package/dist/{run-tests-6MWE6D5S.js → run-tests-PYKKYMPA.js} +2 -2
  27. package/dist/{server-IALCWKSU.js → server-WTJSM2MT.js} +147 -41
  28. package/dist/{server-SP3AEHRQ.js → server-ZSTVYSYC.js} +5 -5
  29. package/dist/{task-orchestrator-Q6WFFI4J.js → task-orchestrator-PNLI3V36.js} +5 -5
  30. package/dist/{usage-DJDLHICZ.js → usage-GVFUVYUG.js} +2 -2
  31. package/package.json +1 -1
@@ -26,7 +26,9 @@ import {
26
26
  getGitContext,
27
27
  getGitRoot,
28
28
  isMemoryExpired,
29
+ isMemoryStale,
29
30
  listMemoryEntries,
31
+ listOrphanedProjectMemories,
30
32
  loadMemoryEntries,
31
33
  memoryStorePath,
32
34
  parseWritableScope,
@@ -34,14 +36,14 @@ import {
34
36
  touchMemoryReferences,
35
37
  updateMemoryApproval,
36
38
  updateMemoryEntry
37
- } from "./chunk-FI6IWUXW.js";
39
+ } from "./chunk-KIHNZBEA.js";
38
40
  import {
39
41
  redactJson,
40
42
  scanString
41
43
  } from "./chunk-YVTASHS5.js";
42
44
  import {
43
45
  runTestsTool
44
- } from "./chunk-ARYCWJN6.js";
46
+ } from "./chunk-NVN3FHXI.js";
45
47
  import {
46
48
  AGENTIC_BEHAVIOR_GUIDELINE,
47
49
  APP_NAME,
@@ -51,6 +53,7 @@ import {
51
53
  CONFIG_FILE_NAME,
52
54
  CONTEXT_FILE_CANDIDATES,
53
55
  CONTEXT_FILE_MAX_BYTES,
56
+ CONTEXT_PRESSURE_THRESHOLD,
54
57
  CUSTOM_COMMANDS_DIR_NAME,
55
58
  DEFAULT_MAX_TOKENS,
56
59
  DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
@@ -73,7 +76,7 @@ import {
73
76
  SUBAGENT_MAX_ROUNDS_LIMIT,
74
77
  VERSION,
75
78
  buildUserIdentityPrompt
76
- } from "./chunk-7HECI3U5.js";
79
+ } from "./chunk-4HTJMLFY.js";
77
80
  import {
78
81
  hasSemanticIndex,
79
82
  semanticSearch
@@ -5257,9 +5260,9 @@ var SessionManager = class {
5257
5260
  try {
5258
5261
  const data = JSON.parse(readFileSync4(filePath, "utf-8"));
5259
5262
  const messages = data.messages ?? [];
5260
- const matches = [];
5263
+ const matches2 = [];
5261
5264
  for (const msg of messages) {
5262
- if (matches.length >= 3) break;
5265
+ if (matches2.length >= 3) break;
5263
5266
  let text = "";
5264
5267
  if (typeof msg.content === "string") {
5265
5268
  text = msg.content;
@@ -5272,10 +5275,10 @@ var SessionManager = class {
5272
5275
  const start = Math.max(0, idx - 30);
5273
5276
  const end = Math.min(text.length, idx + query.length + 60);
5274
5277
  const snippet = (start > 0 ? "\u2026" : "") + text.slice(start, end).replace(/\n/g, " ") + (end < text.length ? "\u2026" : "");
5275
- matches.push({ role: msg.role, snippet });
5278
+ matches2.push({ role: msg.role, snippet });
5276
5279
  }
5277
5280
  }
5278
- if (matches.length > 0) {
5281
+ if (matches2.length > 0) {
5279
5282
  results.push({
5280
5283
  sessionMeta: {
5281
5284
  id: data.id,
@@ -5286,7 +5289,7 @@ var SessionManager = class {
5286
5289
  updated: safeDate(data.updated),
5287
5290
  title: data.title
5288
5291
  },
5289
- matches
5292
+ matches: matches2
5290
5293
  });
5291
5294
  }
5292
5295
  } catch (err) {
@@ -9394,20 +9397,20 @@ Results sorted by most recent modification time. Automatically skips node_module
9394
9397
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
9395
9398
  if (!existsSync16(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
9396
9399
  const regex = globToRegex(pattern);
9397
- const matches = [];
9398
- collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
9399
- if (matches.length === 0) {
9400
+ const matches2 = [];
9401
+ collectMatchingFiles(rootPath, rootPath, regex, matches2, maxResults);
9402
+ if (matches2.length === 0) {
9400
9403
  return `No files matched pattern: ${pattern}
9401
9404
  Searched in: ${rootPath}`;
9402
9405
  }
9403
- matches.sort((a, b) => b.mtime - a.mtime);
9406
+ matches2.sort((a, b) => b.mtime - a.mtime);
9404
9407
  const lines = [
9405
- `Found ${matches.length} file(s) matching: ${pattern}`,
9408
+ `Found ${matches2.length} file(s) matching: ${pattern}`,
9406
9409
  `Searched in: ${rootPath}`,
9407
9410
  "",
9408
- ...matches.map((m) => m.relPath)
9411
+ ...matches2.map((m) => m.relPath)
9409
9412
  ];
9410
- if (matches.length >= maxResults) {
9413
+ if (matches2.length >= maxResults) {
9411
9414
  lines.push("");
9412
9415
  lines.push(`(Results truncated at ${maxResults}. Use max_results or narrow your pattern.)`);
9413
9416
  }
@@ -10926,10 +10929,10 @@ function trustPlugin(configDir, name) {
10926
10929
  }
10927
10930
  function getInstalledPlugin(configDir, name) {
10928
10931
  const plugins = listInstalledPlugins(configDir);
10929
- const matches = plugins.filter((p) => p.name === name || p.name.startsWith(name));
10930
- if (matches.length === 0) throw new Error(`plugin not found: ${name}`);
10931
- if (matches.length > 1) throw new Error(`plugin name is ambiguous: ${name}`);
10932
- return matches[0];
10932
+ const matches2 = plugins.filter((p) => p.name === name || p.name.startsWith(name));
10933
+ if (matches2.length === 0) throw new Error(`plugin not found: ${name}`);
10934
+ if (matches2.length > 1) throw new Error(`plugin name is ambiguous: ${name}`);
10935
+ return matches2[0];
10933
10936
  }
10934
10937
  function listInstalledPlugins(configDir) {
10935
10938
  const root = pluginRoot(configDir);
@@ -11450,8 +11453,8 @@ function summarizeContent(content) {
11450
11453
  function extractEvidence(content, toolNames) {
11451
11454
  const evidence = /* @__PURE__ */ new Set();
11452
11455
  if (toolNames.length > 0) evidence.add(`Tools: ${toolNames.join(", ")}`);
11453
- const matches = content.match(/(?:[\w.-]+[\\/])+[\w.-]+\.(?:ts|tsx|js|jsx|json|md|css|html|py|go|rs|yml|yaml)/g) ?? [];
11454
- for (const match of matches.slice(0, 8)) evidence.add(match);
11456
+ const matches2 = content.match(/(?:[\w.-]+[\\/])+[\w.-]+\.(?:ts|tsx|js|jsx|json|md|css|html|py|go|rs|yml|yaml)/g) ?? [];
11457
+ for (const match of matches2.slice(0, 8)) evidence.add(match);
11455
11458
  return [...evidence].slice(0, 10);
11456
11459
  }
11457
11460
  function formatAgentResult(input) {
@@ -14161,6 +14164,112 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
14161
14164
  return committedSize < originalSize;
14162
14165
  }
14163
14166
 
14167
+ // src/core/context-pressure.ts
14168
+ function estimateConversationTokens(input) {
14169
+ let total = 0;
14170
+ if (input.systemPrompt) total += estimateTokens(input.systemPrompt);
14171
+ for (const msg of input.messages) {
14172
+ if (typeof msg.content === "string") {
14173
+ total += estimateTokens(msg.content);
14174
+ } else if (Array.isArray(msg.content)) {
14175
+ for (const part of msg.content) {
14176
+ if (part.type === "text" && part.text) total += estimateTokens(part.text);
14177
+ }
14178
+ }
14179
+ if (msg.toolCalls) {
14180
+ for (const tc of msg.toolCalls) {
14181
+ total += estimateTokens(JSON.stringify(tc.arguments));
14182
+ }
14183
+ }
14184
+ }
14185
+ return total;
14186
+ }
14187
+ function evaluateContextPressure(input) {
14188
+ if (input.contextWindow <= 0) return { shouldCompact: false, ratio: null, percent: null };
14189
+ const ratio = input.estimatedTokens / input.contextWindow;
14190
+ const threshold = input.threshold ?? CONTEXT_PRESSURE_THRESHOLD;
14191
+ return {
14192
+ shouldCompact: input.autoCompactEnabled && ratio >= threshold,
14193
+ ratio,
14194
+ percent: Math.round(ratio * 100)
14195
+ };
14196
+ }
14197
+ var AUTO_COMPACT_INSTRUCTION = "Preserve key context, compress old content, ensure seamless continuation";
14198
+
14199
+ // src/core/model-router.ts
14200
+ var TAG_REGEX = /(?:^|\s)#([a-zA-Z][\w-]{0,31})\b/g;
14201
+ function extractTags(message) {
14202
+ const tags = /* @__PURE__ */ new Set();
14203
+ let m;
14204
+ TAG_REGEX.lastIndex = 0;
14205
+ while ((m = TAG_REGEX.exec(message)) !== null) {
14206
+ tags.add(m[1].toLowerCase());
14207
+ }
14208
+ return tags;
14209
+ }
14210
+ function matches(message, matcher) {
14211
+ const trimmed = message.trim();
14212
+ const lower = trimmed.toLowerCase();
14213
+ if (matcher.tag) {
14214
+ const tags = extractTags(trimmed);
14215
+ if (!tags.has(matcher.tag.toLowerCase())) return false;
14216
+ }
14217
+ if (matcher.contains && matcher.contains.length > 0) {
14218
+ const hit = matcher.contains.some((kw) => lower.includes(kw.toLowerCase()));
14219
+ if (!hit) return false;
14220
+ }
14221
+ if (typeof matcher.maxLength === "number") {
14222
+ if (trimmed.length > matcher.maxLength) return false;
14223
+ }
14224
+ if (typeof matcher.minLength === "number") {
14225
+ if (trimmed.length < matcher.minLength) return false;
14226
+ }
14227
+ const hasAnyCondition = !!matcher.tag || matcher.contains && matcher.contains.length > 0 || typeof matcher.maxLength === "number" || typeof matcher.minLength === "number";
14228
+ return !!hasAnyCondition;
14229
+ }
14230
+ function pickModel(message, currentModel, config, availableModels = []) {
14231
+ if (!config.enabled || config.rules.length === 0) {
14232
+ return { model: currentModel, reason: "routing disabled", overridden: false };
14233
+ }
14234
+ const isAvailable = (m) => availableModels.length === 0 || availableModels.includes(m);
14235
+ for (let i = 0; i < config.rules.length; i++) {
14236
+ const rule = config.rules[i];
14237
+ if (!matches(message, rule.match)) continue;
14238
+ if (!isAvailable(rule.model)) continue;
14239
+ if (rule.model === currentModel) {
14240
+ return {
14241
+ model: currentModel,
14242
+ reason: `rule "${rule.name ?? `#${i}`}" matched (same as current)`,
14243
+ overridden: false,
14244
+ ruleIdx: i
14245
+ };
14246
+ }
14247
+ return {
14248
+ model: rule.model,
14249
+ reason: `rule "${rule.name ?? `#${i}`}" matched`,
14250
+ overridden: true,
14251
+ ruleIdx: i
14252
+ };
14253
+ }
14254
+ if (config.fallback && config.fallback !== currentModel && isAvailable(config.fallback)) {
14255
+ return {
14256
+ model: config.fallback,
14257
+ reason: "fallback",
14258
+ overridden: true
14259
+ };
14260
+ }
14261
+ return { model: currentModel, reason: "no rule matched", overridden: false };
14262
+ }
14263
+ function stripRoutingTags(message) {
14264
+ return message.replace(/(?:^|\s)#(fast|deep|default)\b/gi, " ").replace(/\s{2,}/g, " ").trim();
14265
+ }
14266
+ function decideModel(input) {
14267
+ if (!input.routingConfig || !input.routingConfig.enabled) {
14268
+ return { model: input.currentModel, reason: "routing disabled", overridden: false };
14269
+ }
14270
+ return pickModel(input.userInput, input.currentModel, input.routingConfig, input.availableModels);
14271
+ }
14272
+
14164
14273
  // src/core/context-files.ts
14165
14274
  import { existsSync as existsSync25, readFileSync as readFileSync20 } from "fs";
14166
14275
  import { join as join19, relative as relative4, resolve as resolve8 } from "path";
@@ -14511,6 +14620,84 @@ Output the Markdown content directly, do not wrap the entire file in a code bloc
14511
14620
  return parts.join("\n");
14512
14621
  }
14513
14622
 
14623
+ // src/core/memory-filters.ts
14624
+ function parseFlags(items) {
14625
+ const flags = {};
14626
+ const text = [];
14627
+ for (let i = 0; i < items.length; i++) {
14628
+ const item = items[i];
14629
+ if (item.startsWith("--")) {
14630
+ const key = item.slice(2);
14631
+ const next = items[i + 1];
14632
+ if (next && !next.startsWith("--")) {
14633
+ flags[key] = next;
14634
+ i++;
14635
+ } else {
14636
+ flags[key] = true;
14637
+ }
14638
+ } else {
14639
+ text.push(item);
14640
+ }
14641
+ }
14642
+ return { flags, text: text.join(" ").trim() };
14643
+ }
14644
+ var SCOPES = /* @__PURE__ */ new Set(["personal", "project", "session", "team"]);
14645
+ var BOOLEAN_FLAGS = ["pending", "orphaned"];
14646
+ function parseMemoryShowFilters(args) {
14647
+ const { flags, text } = parseFlags(args);
14648
+ const strayTerms = [];
14649
+ for (const key of BOOLEAN_FLAGS) {
14650
+ const v = flags[key];
14651
+ if (typeof v === "string") {
14652
+ strayTerms.push(v);
14653
+ flags[key] = true;
14654
+ }
14655
+ }
14656
+ const scopeRaw = flags["scope"];
14657
+ const freeText = [text, ...strayTerms].filter(Boolean).join(" ").trim();
14658
+ const searchRaw = typeof flags["search"] === "string" ? flags["search"] : freeText || void 0;
14659
+ const staleRaw = flags["stale"];
14660
+ let staleDays;
14661
+ let invalidStale = false;
14662
+ if (staleRaw !== void 0) {
14663
+ const n = typeof staleRaw === "string" ? Number(staleRaw) : NaN;
14664
+ if (!Number.isFinite(n) || n <= 0) invalidStale = true;
14665
+ else staleDays = n;
14666
+ }
14667
+ return {
14668
+ pending: flags["pending"] === true,
14669
+ orphaned: flags["orphaned"] === true,
14670
+ scope: typeof scopeRaw === "string" && SCOPES.has(scopeRaw) ? scopeRaw : void 0,
14671
+ search: searchRaw?.toLowerCase(),
14672
+ staleDays,
14673
+ invalidStale
14674
+ };
14675
+ }
14676
+ function filterMemoryEntries(entries, filters) {
14677
+ let out = entries;
14678
+ const applied = [];
14679
+ if (filters.orphaned) applied.push("orphaned");
14680
+ if (filters.pending) {
14681
+ out = out.filter((m) => !m.approved && !isMemoryExpired(m));
14682
+ applied.push("pending");
14683
+ }
14684
+ if (filters.scope) {
14685
+ out = out.filter((m) => m.scope === filters.scope);
14686
+ applied.push(`scope:${filters.scope}`);
14687
+ }
14688
+ if (filters.search) {
14689
+ const q = filters.search;
14690
+ out = out.filter((m) => m.content.toLowerCase().includes(q) || m.id.startsWith(q));
14691
+ applied.push(`search:"${q}"`);
14692
+ }
14693
+ if (filters.staleDays !== void 0) {
14694
+ const days = filters.staleDays;
14695
+ out = out.filter((m) => isMemoryStale(m, days));
14696
+ applied.push(`stale>${days}d`);
14697
+ }
14698
+ return { entries: out, applied };
14699
+ }
14700
+
14514
14701
  // src/hub/convergence.ts
14515
14702
  function convergenceThreshold(total) {
14516
14703
  if (total <= 0) return Infinity;
@@ -15287,6 +15474,12 @@ async function handlePlan(args, ctx) {
15287
15474
  } else if (sub === "execute") {
15288
15475
  ctx.planMode = false;
15289
15476
  ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
15477
+ } else if (sub === "status" || sub === "show") {
15478
+ ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
15479
+ return;
15480
+ } else if (sub !== void 0) {
15481
+ ctx.send({ type: "error", message: `Unknown /plan option "${sub}" \u2014 expected execute | exit | status` });
15482
+ return;
15290
15483
  } else {
15291
15484
  ctx.planMode = !ctx.planMode;
15292
15485
  ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
@@ -15824,7 +16017,7 @@ Cache: write=${cacheCreate} read=${cacheRead}` : "";
15824
16017
  Cost: ${formatCost(cost)}` : "";
15825
16018
  let memoryLine = "";
15826
16019
  try {
15827
- const { countPendingMemories } = await import("./persistent-memory-3FTOC7QT.js");
16020
+ const { countPendingMemories } = await import("./persistent-memory-52SE5K44.js");
15828
16021
  const pending = countPendingMemories(ctx.config.getConfigDir());
15829
16022
  if (pending > 0) memoryLine = `
15830
16023
  Memory: \u26A0 ${pending} pending approval \u2014 see the Memory panel or /memory`;
@@ -15928,14 +16121,14 @@ async function handleSession(args, ctx) {
15928
16121
  return;
15929
16122
  }
15930
16123
  const list2 = ctx.sessions.listSessions();
15931
- const matches = list2.filter((s) => s.messageCount === filterMessages);
15932
- if (matches.length === 0) {
16124
+ const matches2 = list2.filter((s) => s.messageCount === filterMessages);
16125
+ if (matches2.length === 0) {
15933
16126
  ctx.send({ type: "info", message: `No sessions with ${filterMessages} message(s) found.` });
15934
16127
  return;
15935
16128
  }
15936
16129
  let deleted = 0;
15937
16130
  let failed = 0;
15938
- for (const s of matches) {
16131
+ for (const s of matches2) {
15939
16132
  if (ctx.sessions.deleteSession(s.id)) {
15940
16133
  deleted++;
15941
16134
  } else {
@@ -16637,7 +16830,7 @@ async function handleTest(args, ctx) {
16637
16830
  const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
16638
16831
  testArgs = isCommand ? { command: argStr } : { filter: argStr };
16639
16832
  }
16640
- const runTests = ctx.runTests ?? (await import("./run-tests-6MWE6D5S.js")).executeTests;
16833
+ const runTests = ctx.runTests ?? (await import("./run-tests-PYKKYMPA.js")).executeTests;
16641
16834
  const report = await runTests(testArgs);
16642
16835
  ctx.send({ type: "info", message: report });
16643
16836
  } catch (err) {
@@ -16895,7 +17088,7 @@ async function handleMemory(args, ctx) {
16895
17088
  ctx.handleMemoryManage(sub, args[1], sub === "expire" ? args[2] : void 0);
16896
17089
  } else if (sub === "export") {
16897
17090
  if (args[1] === "md") {
16898
- const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-3FTOC7QT.js");
17091
+ const { exportMemoryEntries: exportMemoryEntries2 } = await import("./persistent-memory-52SE5K44.js");
16899
17092
  ctx.send({
16900
17093
  type: "export_data",
16901
17094
  format: "md",
@@ -16915,7 +17108,8 @@ async function handleMemory(args, ctx) {
16915
17108
  });
16916
17109
  }
16917
17110
  } else {
16918
- ctx.memoryShow();
17111
+ const isShow = sub === "show" || sub === "view" || sub === "list";
17112
+ ctx.memoryShow(isShow ? args.slice(1) : args);
16919
17113
  }
16920
17114
  }
16921
17115
  async function handleAddDir(args, ctx) {
@@ -16946,6 +17140,119 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
16946
17140
  It will be included in AI context for subsequent messages.` });
16947
17141
  }
16948
17142
 
17143
+ // src/core/profile-command.ts
17144
+ var PROFILE_FIELDS = ["name", "nickname", "role", "bio", "interests", "locale", "extra"];
17145
+ function stripSurroundingQuotes(value) {
17146
+ const pairs = [['"', '"'], ["'", "'"], ["\u201C", "\u201D"], ["\u2018", "\u2019"], ["\u300C", "\u300D"]];
17147
+ for (const [open, close] of pairs) {
17148
+ if (value.length >= 2 && value.startsWith(open) && value.endsWith(close)) {
17149
+ return value.slice(open.length, value.length - close.length).trim();
17150
+ }
17151
+ }
17152
+ return value;
17153
+ }
17154
+ function showRows(profile) {
17155
+ const rows = [];
17156
+ if (profile.name) rows.push(["Name", profile.name]);
17157
+ if (profile.nickname) rows.push(["Nickname", profile.nickname]);
17158
+ if (profile.role) rows.push(["Role", profile.role]);
17159
+ if (profile.bio) rows.push(["Bio", profile.bio]);
17160
+ if (profile.interests?.length) rows.push(["Interests", profile.interests.join(", ")]);
17161
+ if (profile.locale) rows.push(["Locale", profile.locale]);
17162
+ if (profile.extra) {
17163
+ rows.push(["Extra", profile.extra.length > 80 ? `${profile.extra.slice(0, 80)}\u2026` : profile.extra]);
17164
+ }
17165
+ return rows;
17166
+ }
17167
+ function applyProfileCommand(args, profile) {
17168
+ const sub = (args[0] ?? "show").toLowerCase();
17169
+ if (sub === "show" || sub === "view") {
17170
+ const rows = showRows(profile);
17171
+ if (rows.length === 0) {
17172
+ return {
17173
+ summary: "No profile configured yet.",
17174
+ rows: [],
17175
+ details: ["Set one with /profile set <field> <value> \u2014 it is injected into every conversation."],
17176
+ changed: false,
17177
+ isError: false
17178
+ };
17179
+ }
17180
+ return {
17181
+ summary: "\u{1F464} Your Profile",
17182
+ rows,
17183
+ details: ["This identity is injected into every AI conversation."],
17184
+ changed: false,
17185
+ isError: false
17186
+ };
17187
+ }
17188
+ if (sub === "clear") {
17189
+ return {
17190
+ profile: {},
17191
+ summary: "Profile cleared. The AI will no longer know who you are.",
17192
+ rows: [],
17193
+ details: [],
17194
+ changed: true,
17195
+ isError: false
17196
+ };
17197
+ }
17198
+ if (sub === "set") {
17199
+ const field = args[1];
17200
+ const value = args.slice(2).join(" ").trim();
17201
+ if (!field || !PROFILE_FIELDS.includes(field)) {
17202
+ return {
17203
+ summary: "Usage: /profile set <field> <value>",
17204
+ rows: [],
17205
+ details: [`Fields: ${PROFILE_FIELDS.join(", ")}`],
17206
+ changed: false,
17207
+ isError: true
17208
+ };
17209
+ }
17210
+ if (!value) {
17211
+ return {
17212
+ summary: `Usage: /profile set ${field} <value>`,
17213
+ rows: [],
17214
+ details: [],
17215
+ changed: false,
17216
+ isError: true
17217
+ };
17218
+ }
17219
+ const updated = { ...profile };
17220
+ if (field === "interests") {
17221
+ updated.interests = value.split(",").map((s) => stripSurroundingQuotes(s.trim())).filter(Boolean);
17222
+ } else {
17223
+ updated[field] = stripSurroundingQuotes(value);
17224
+ }
17225
+ const shown = field === "interests" ? (updated.interests ?? []).join(", ") : stripSurroundingQuotes(value);
17226
+ return {
17227
+ profile: updated,
17228
+ summary: `Profile updated: ${field} = ${shown}`,
17229
+ rows: [],
17230
+ details: [],
17231
+ changed: true,
17232
+ isError: false
17233
+ };
17234
+ }
17235
+ if (sub === "edit") {
17236
+ return {
17237
+ summary: "Interactive editing lives in the setup wizard.",
17238
+ rows: [],
17239
+ details: [
17240
+ "Run `aicli config` in a terminal, or use /profile set <field> <value> here.",
17241
+ `Fields: ${PROFILE_FIELDS.join(", ")}`
17242
+ ],
17243
+ changed: false,
17244
+ isError: false
17245
+ };
17246
+ }
17247
+ return {
17248
+ summary: `Unknown /profile option "${args[0]}" \u2014 expected show | set | clear | edit`,
17249
+ rows: [],
17250
+ details: [],
17251
+ changed: false,
17252
+ isError: true
17253
+ };
17254
+ }
17255
+
16949
17256
  // src/web/commands/security-commands.ts
16950
17257
  async function handleConfig(args, ctx) {
16951
17258
  const sub = args[0];
@@ -17007,6 +17314,18 @@ async function handleBug(_args, ctx) {
17007
17314
  ];
17008
17315
  ctx.send({ type: "info", message: lines.join("\n") });
17009
17316
  }
17317
+ async function handleProfile(args, ctx) {
17318
+ const outcome = applyProfileCommand(args, ctx.config.get("userProfile") ?? {});
17319
+ if (outcome.profile) {
17320
+ ctx.config.set("userProfile", outcome.profile);
17321
+ ctx.config.save();
17322
+ }
17323
+ const lines = [outcome.summary];
17324
+ for (const [label, value] of outcome.rows) lines.push(` ${label}: ${value}`);
17325
+ for (const line of outcome.details) lines.push(line);
17326
+ ctx.send({ type: outcome.isError ? "error" : "info", message: lines.join("\n") });
17327
+ if (outcome.changed) ctx.sendStatus();
17328
+ }
17010
17329
 
17011
17330
  // src/web/commands/agent-commands.ts
17012
17331
  async function handleAuto(args, ctx) {
@@ -17050,6 +17369,17 @@ async function handlePermissions(args, ctx) {
17050
17369
  }
17051
17370
  async function handleYolo(args, ctx) {
17052
17371
  const sub = args[0]?.toLowerCase();
17372
+ if (sub === "status") {
17373
+ ctx.send({
17374
+ type: "info",
17375
+ message: `YOLO mode: ${ctx.toolExecutor.sessionAutoApprove ? "ON (write tools auto-approved)" : "OFF"}`
17376
+ });
17377
+ return;
17378
+ }
17379
+ if (sub !== void 0 && sub !== "on" && sub !== "off") {
17380
+ ctx.send({ type: "error", message: `Unknown /yolo option "${sub}" \u2014 expected on | off | status` });
17381
+ return;
17382
+ }
17053
17383
  if (sub === "off") {
17054
17384
  ctx.toolExecutor.sessionAutoApprove = false;
17055
17385
  ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
@@ -17207,11 +17537,11 @@ async function handleSearch(args, ctx) {
17207
17537
  }
17208
17538
  const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
17209
17539
  for (const r of results) {
17210
- const { sessionMeta, matches } = r;
17540
+ const { sessionMeta, matches: matches2 } = r;
17211
17541
  const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
17212
17542
  lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
17213
17543
  if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
17214
- for (const m of matches) {
17544
+ for (const m of matches2) {
17215
17545
  const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
17216
17546
  lines.push(` ${icon} ${m.snippet}`);
17217
17547
  }
@@ -17261,14 +17591,17 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
17261
17591
  }
17262
17592
 
17263
17593
  // src/web/session-handler.ts
17264
- var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Set([
17265
- "copy",
17266
- "paste",
17267
- "exit",
17268
- "quit",
17269
- "snapshot",
17270
- "hooks",
17271
- "hook"
17594
+ var CLI_ONLY_WEB_COMMANDS = /* @__PURE__ */ new Map([
17595
+ ["copy", "It copies to the terminal clipboard; use your browser/OS copy instead."],
17596
+ ["paste", "It reads the terminal clipboard; paste directly into the composer instead."],
17597
+ ["exit", "Close the browser tab to end this Web session."],
17598
+ ["quit", "Close the browser tab to end this Web session."],
17599
+ ["snapshot", "Dev-state snapshots are written when the terminal REPL exits."],
17600
+ ["hooks", "Hook trust is granted per machine from the terminal."],
17601
+ ["hook", "Hook trust is granted per machine from the terminal."],
17602
+ // 以下两条并非「终端才有意义」,而是 Web 侧尚未实现——如实说明,别假装是设计。
17603
+ ["route", "Smart model routing is not applied to Web sessions yet (config.routing is REPL-only)."],
17604
+ ["security", "Redaction status and toggles are terminal-only for now; /status shows the current profile."]
17272
17605
  ]);
17273
17606
  var webCommandHandlers = {
17274
17607
  provider: handleProvider,
@@ -17304,6 +17637,7 @@ var webCommandHandlers = {
17304
17637
  config: handleConfig,
17305
17638
  doctor: handleDoctor,
17306
17639
  bug: handleBug,
17640
+ profile: handleProfile,
17307
17641
  auto: handleAuto,
17308
17642
  permissions: handlePermissions,
17309
17643
  yolo: handleYolo,
@@ -17338,17 +17672,18 @@ var SessionHandler = class {
17338
17672
  sessionAccruedCostUsd = 0;
17339
17673
  sessionCostIncomplete = false;
17340
17674
  /** Accumulate a TokenUsage (with optional cache fields) into sessionTokenUsage. */
17341
- addWebSessionUsage(u) {
17675
+ addWebSessionUsage(u, modelOverride) {
17676
+ const usageModel = modelOverride ?? this.currentModel;
17342
17677
  this.sessionTokenUsage.inputTokens += u.inputTokens;
17343
17678
  this.sessionTokenUsage.outputTokens += u.outputTokens;
17344
17679
  this.sessionTokenUsage.cacheCreationTokens += u.cacheCreationTokens ?? 0;
17345
17680
  this.sessionTokenUsage.cacheReadTokens += u.cacheReadTokens ?? 0;
17346
- const priced = this.costTracker.addCost(this.currentProvider, this.currentModel, u);
17681
+ const priced = this.costTracker.addCost(this.currentProvider, usageModel, u);
17347
17682
  if (priced === null) this.sessionCostIncomplete = true;
17348
17683
  else this.sessionAccruedCostUsd += priced;
17349
17684
  const totalTokens = u.inputTokens + u.outputTokens + (u.cacheCreationTokens ?? 0) + (u.cacheReadTokens ?? 0);
17350
- if (totalTokens > 0 && !getPricing(this.currentProvider, this.currentModel)) {
17351
- const key = `${this.currentProvider}/${this.currentModel}`;
17685
+ if (totalTokens > 0 && !getPricing(this.currentProvider, usageModel)) {
17686
+ const key = `${this.currentProvider}/${usageModel}`;
17352
17687
  if (!this.unknownPricingWarnings.has(key)) {
17353
17688
  this.unknownPricingWarnings.add(key);
17354
17689
  this.send({ type: "info", message: `Cost notice: pricing is unknown for ${key}. Tokens are tracked, but cost is excluded from totals. Run /cost or aicli usage to review.` });
@@ -17796,6 +18131,16 @@ var SessionHandler = class {
17796
18131
  } else {
17797
18132
  msgContent = content;
17798
18133
  }
18134
+ const routingDecision = this.computeRoutingDecision(typeof msgContent === "string" ? msgContent : content);
18135
+ if (typeof msgContent === "string") {
18136
+ msgContent = stripRoutingTags(msgContent);
18137
+ } else if (msgContent.length > 0 && msgContent[msgContent.length - 1]?.type === "text") {
18138
+ const last = msgContent[msgContent.length - 1];
18139
+ msgContent = [...msgContent.slice(0, -1), { type: "text", text: stripRoutingTags(last.text) }];
18140
+ }
18141
+ if (routingDecision.overridden) {
18142
+ this.send({ type: "info", message: `\u2192 Routed to ${routingDecision.model} (${routingDecision.reason})` });
18143
+ }
17799
18144
  const userMessage = { role: "user", content: msgContent, timestamp: /* @__PURE__ */ new Date() };
17800
18145
  session.addMessage(userMessage);
17801
18146
  const provider = this.providers.get(this.currentProvider);
@@ -17807,10 +18152,13 @@ var SessionHandler = class {
17807
18152
  const hasToolSupport = typeof provider.chatWithTools === "function";
17808
18153
  const { toolDefs, mcpBudgetNote } = hasToolSupport ? this.getFilteredToolDefs() : { toolDefs: [], mcpBudgetNote: null };
17809
18154
  if (hasToolSupport && toolDefs.length > 0) {
17810
- await this.handleChatWithTools(provider, session.messages, toolDefs, mcpBudgetNote);
18155
+ await this.handleChatWithTools(provider, session.messages, toolDefs, mcpBudgetNote, routingDecision.model);
17811
18156
  } else {
17812
- await this.handleChatSimple(provider, session.messages);
18157
+ await this.handleChatSimple(provider, session.messages, routingDecision.model);
17813
18158
  }
18159
+ await this.checkContextPressure();
18160
+ const budgetWarning = this.costTracker.checkBudget(this.config.get("monthlyBudget"));
18161
+ if (budgetWarning) this.send({ type: "info", message: `\u26A0 ${budgetWarning}` });
17814
18162
  } catch (err) {
17815
18163
  const parts = [];
17816
18164
  let current = err;
@@ -17829,8 +18177,9 @@ var SessionHandler = class {
17829
18177
  this.sendSessionList();
17830
18178
  }
17831
18179
  }
17832
- async handleChatSimple(provider, messages) {
18180
+ async handleChatSimple(provider, messages, modelOverride) {
17833
18181
  const session = this.sessions.current;
18182
+ const effectiveModel = modelOverride ?? this.currentModel;
17834
18183
  const modelParams = this.getModelParams();
17835
18184
  const ac = new AbortController();
17836
18185
  this.abortController = ac;
@@ -17838,7 +18187,7 @@ var SessionHandler = class {
17838
18187
  const { stable: simplStable, volatile: simplVolatile } = this.buildSystemPrompt();
17839
18188
  const stream = provider.chatStream({
17840
18189
  messages,
17841
- model: this.currentModel,
18190
+ model: effectiveModel,
17842
18191
  systemPrompt: simplStable,
17843
18192
  systemPromptVolatile: simplVolatile,
17844
18193
  stream: true,
@@ -17857,7 +18206,7 @@ var SessionHandler = class {
17857
18206
  if (chunk.done) {
17858
18207
  this.send({ type: "response_done", content: fullContent, usage: chunk.usage });
17859
18208
  if (chunk.usage) {
17860
- this.addWebSessionUsage(chunk.usage);
18209
+ this.addWebSessionUsage(chunk.usage, effectiveModel);
17861
18210
  session.addTokenUsage(chunk.usage);
17862
18211
  }
17863
18212
  if (chunk.reasoningContent) simpleReasoning = chunk.reasoningContent;
@@ -17882,8 +18231,9 @@ var SessionHandler = class {
17882
18231
  this.abortController = null;
17883
18232
  }
17884
18233
  }
17885
- async handleChatWithTools(provider, messages, toolDefs, mcpBudgetNote) {
18234
+ async handleChatWithTools(provider, messages, toolDefs, mcpBudgetNote, modelOverride) {
17886
18235
  const session = this.sessions.current;
18236
+ const effectiveModel = modelOverride ?? this.currentModel;
17887
18237
  const apiMessages = [...messages];
17888
18238
  const maxToolRounds = this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
17889
18239
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
@@ -17913,7 +18263,7 @@ ${mcpBudgetNote}` : "");
17913
18263
  callModel: async (_round, extraMessages) => {
17914
18264
  const chatRequest = {
17915
18265
  messages: apiMessages,
17916
- model: this.currentModel,
18266
+ model: effectiveModel,
17917
18267
  systemPrompt,
17918
18268
  systemPromptVolatile,
17919
18269
  stream: false,
@@ -17958,7 +18308,7 @@ Details: ${errMsg.split("\n")[0]}
17958
18308
  const summaryResult = await provider.chatWithTools(
17959
18309
  {
17960
18310
  messages: apiMessages,
17961
- model: this.currentModel,
18311
+ model: effectiveModel,
17962
18312
  systemPrompt,
17963
18313
  systemPromptVolatile,
17964
18314
  stream: false,
@@ -17974,7 +18324,7 @@ Details: ${errMsg.split("\n")[0]}
17974
18324
  executeTools: async (toolCalls) => {
17975
18325
  googleSearchContext.configManager = this.config;
17976
18326
  spawnAgentContext.provider = provider;
17977
- spawnAgentContext.model = this.currentModel;
18327
+ spawnAgentContext.model = effectiveModel;
17978
18328
  spawnAgentContext.systemPrompt = systemPromptVolatile ? `${systemPrompt}
17979
18329
 
17980
18330
  ---
@@ -18329,10 +18679,11 @@ ${summaryContent}`,
18329
18679
  }
18330
18680
  // ── Commands ─────────────────────────────────────────────────────
18331
18681
  async handleCommand(name, args) {
18332
- if (CLI_ONLY_WEB_COMMANDS.has(name)) {
18682
+ const cliOnlyReason = CLI_ONLY_WEB_COMMANDS.get(name);
18683
+ if (cliOnlyReason) {
18333
18684
  this.send({
18334
18685
  type: "error",
18335
- message: `/${name} is only available in the terminal REPL. Run \`aicli\` in a terminal and use /${name} there.`
18686
+ message: `/${name} is not available in the Web UI. ${cliOnlyReason}`
18336
18687
  });
18337
18688
  return;
18338
18689
  }
@@ -18343,6 +18694,43 @@ ${summaryContent}`,
18343
18694
  this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
18344
18695
  }
18345
18696
  }
18697
+ /**
18698
+ * 到达上下文压力阈值时自动压缩(与 REPL 同一判据,见 core/context-pressure.ts)。
18699
+ * 上下文窗口未知时不动——不基于猜测改用户的对话历史。
18700
+ */
18701
+ /**
18702
+ * 智能模型路由决策(审计附录 D-5)。候选限定在当前 provider 注册的模型内——
18703
+ * 跨 provider 换模型会破坏 API key 与对话连续性。
18704
+ */
18705
+ computeRoutingDecision(userInput) {
18706
+ let availableModels = [];
18707
+ try {
18708
+ availableModels = this.providers.get(this.currentProvider).info.models.map((m) => m.id);
18709
+ } catch {
18710
+ availableModels = [];
18711
+ }
18712
+ return decideModel({
18713
+ userInput,
18714
+ currentModel: this.currentModel,
18715
+ routingConfig: this.config.get("routing"),
18716
+ availableModels
18717
+ });
18718
+ }
18719
+ async checkContextPressure() {
18720
+ const contextWindow = this.getContextWindowSize();
18721
+ const session = this.sessions.current;
18722
+ const pressure = evaluateContextPressure({
18723
+ autoCompactEnabled: this.config.get("autoCompact") === true,
18724
+ estimatedTokens: contextWindow > 0 && session ? estimateConversationTokens({ systemPrompt: this.activeSystemPrompt, messages: session.messages }) : 0,
18725
+ contextWindow
18726
+ });
18727
+ if (!pressure.shouldCompact) return;
18728
+ this.send({
18729
+ type: "info",
18730
+ message: `\u26A0 Context pressure: ~${pressure.percent}% of the ${contextWindow.toLocaleString()}-token window used. Auto-compacting\u2026`
18731
+ });
18732
+ await this.compactSession(AUTO_COMPACT_INSTRUCTION);
18733
+ }
18346
18734
  async compactSession(instruction) {
18347
18735
  const session = this.sessions.current;
18348
18736
  if (!session || session.messages.length < 6) {
@@ -18490,18 +18878,27 @@ ${summaryContent}`,
18490
18878
  });
18491
18879
  }
18492
18880
  }
18493
- memoryShow() {
18881
+ memoryShow(args = []) {
18494
18882
  const configDir = this.config.getConfigDir();
18495
- const entries = listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
18496
- const content = entries.length === 0 ? "(empty \u2014 no persistent memory entries yet)" : entries.map((entry) => {
18883
+ const filters = parseMemoryShowFilters(args);
18884
+ if (filters.invalidStale) {
18885
+ this.send({ type: "error", message: "Usage: /memory show --stale <days> (positive number)" });
18886
+ return;
18887
+ }
18888
+ const source = filters.orphaned ? listOrphanedProjectMemories(configDir) : listMemoryEntries(configDir, { includeExpired: true, includeRejected: true });
18889
+ const { entries, applied } = filterMemoryEntries(source, filters);
18890
+ const filterNote = applied.length > 0 ? ` (filtered: ${applied.join(", ")})` : "";
18891
+ const content = entries.length === 0 ? applied.length > 0 ? `(no entries match [${applied.join(", ")}])` : "(empty \u2014 no persistent memory entries yet)" : `Governed persistent memory${filterNote}
18892
+
18893
+ ` + entries.map((entry) => {
18497
18894
  const status = isMemoryExpired(entry) ? "expired" : entry.approved ? "approved" : "pending";
18498
- const source = [
18895
+ const source2 = [
18499
18896
  entry.source ?? "unknown",
18500
18897
  entry.sourceSession ? `session ${entry.sourceSession.slice(0, 8)}` : "",
18501
18898
  entry.sourceProject ? `project ${entry.sourceProject}` : ""
18502
18899
  ].filter(Boolean).join(" \xB7 ");
18503
18900
  return `## ${entry.id.slice(0, 8)} \xB7 ${status} \xB7 scope:${entry.scope} \xB7 sensitivity:${entry.sensitivity}
18504
- Source: ${source}
18901
+ Source: ${source2}
18505
18902
  ${entry.content}`;
18506
18903
  }).join("\n\n");
18507
18904
  this.send({