@yorz/cli 0.6.5 → 0.7.0

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 (67) hide show
  1. package/dist/cli/index.js +665 -39
  2. package/dist/gui/assets/{abnfDiagram-VRR7QNED-Dysui3FU.js → abnfDiagram-VRR7QNED-pQ0yp01L.js} +1 -1
  3. package/dist/gui/assets/{arc-Dva0_Fbz.js → arc-k5zxnVZT.js} +1 -1
  4. package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-CSusN0wM.js → architectureDiagram-ZJ3FMSHR-DX-yKkN9.js} +1 -1
  5. package/dist/gui/assets/{blockDiagram-677ZJIJ3-Boy63uL4.js → blockDiagram-677ZJIJ3-CMoP3PmZ.js} +1 -1
  6. package/dist/gui/assets/{c4Diagram-LMCZKHZV-DREOCesJ.js → c4Diagram-LMCZKHZV-CFAjMeqQ.js} +1 -1
  7. package/dist/gui/assets/channel-sLqZREwh.js +1 -0
  8. package/dist/gui/assets/{chunk-2Q5K7J3B-CQGkW5Le.js → chunk-2Q5K7J3B-CSYRtuuw.js} +1 -1
  9. package/dist/gui/assets/{chunk-32BRIVSS-ByKgaOB0.js → chunk-32BRIVSS-BRohWK3g.js} +1 -1
  10. package/dist/gui/assets/{chunk-5VM5RSS4-ouTiIpFg.js → chunk-5VM5RSS4-BANnogAt.js} +1 -1
  11. package/dist/gui/assets/{chunk-EX3LRPZG-BBKKhIQU.js → chunk-EX3LRPZG-BcVKPAuP.js} +1 -1
  12. package/dist/gui/assets/{chunk-JWPE2WC7-i8qqnqZU.js → chunk-JWPE2WC7-D4KqJLpk.js} +1 -1
  13. package/dist/gui/assets/{chunk-MOJQB5TN-BmLvCJEW.js → chunk-MOJQB5TN-CDFJAykP.js} +1 -1
  14. package/dist/gui/assets/{chunk-RYQCIY6F-DgIPwUtq.js → chunk-RYQCIY6F-BJN0o293.js} +1 -1
  15. package/dist/gui/assets/{chunk-V7JOEXUC-CHFlZVs7.js → chunk-V7JOEXUC-B3m1VhnM.js} +1 -1
  16. package/dist/gui/assets/{chunk-VR4S4FIN-CF4gb-1V.js → chunk-VR4S4FIN-BLYOZzRK.js} +1 -1
  17. package/dist/gui/assets/{chunk-XXDRQBXY-6Ajr-TJy.js → chunk-XXDRQBXY-s69CKrwP.js} +1 -1
  18. package/dist/gui/assets/classDiagram-OUVF2IWQ-DpZPCbfk.js +1 -0
  19. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DpZPCbfk.js +1 -0
  20. package/dist/gui/assets/{cose-bilkent-JH36ORCC-Bq1NoHo9.js → cose-bilkent-JH36ORCC-BrNJSX1S.js} +1 -1
  21. package/dist/gui/assets/{cynefin-VYW2F7L2-BLZ43yIC.js → cynefin-VYW2F7L2-i8qxCu_0.js} +1 -1
  22. package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DOG8J1W-.js → cynefinDiagram-TSTJHNR4-dpueKRs1.js} +1 -1
  23. package/dist/gui/assets/{dagre-VKFMJZFB-6s-zIAym.js → dagre-VKFMJZFB-fqwQB8CS.js} +1 -1
  24. package/dist/gui/assets/{diagram-FQU43EPY-MdHGL1Ep.js → diagram-FQU43EPY-B99SBDqa.js} +1 -1
  25. package/dist/gui/assets/{diagram-G47NLZAW-vNqROz2G.js → diagram-G47NLZAW-DF4Oz_Q6.js} +1 -1
  26. package/dist/gui/assets/{diagram-NH7WQ7WH-mBHJAt54.js → diagram-NH7WQ7WH-BIaXJ_6B.js} +1 -1
  27. package/dist/gui/assets/{diagram-OA4YK3LP-Duzu45NT.js → diagram-OA4YK3LP-CJ4_-IYz.js} +1 -1
  28. package/dist/gui/assets/{diagram-WEI45ONY-B9KIE7jm.js → diagram-WEI45ONY-LvUrzfuG.js} +1 -1
  29. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-D-VxYOb1.js → ebnfDiagram-CCIWWBDH-De5G_4vB.js} +1 -1
  30. package/dist/gui/assets/{erDiagram-Q63AITRT-bGX1YWnp.js → erDiagram-Q63AITRT-rYLQVUAr.js} +1 -1
  31. package/dist/gui/assets/{flowDiagram-23GEKE2U-BFbLxzKU.js → flowDiagram-23GEKE2U-CwDsoVsz.js} +1 -1
  32. package/dist/gui/assets/{ganttDiagram-NO4QXBWP-CFlioSP9.js → ganttDiagram-NO4QXBWP-Bm1KWOcG.js} +1 -1
  33. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-9m210Kgm.js → gitGraphDiagram-IHSO6WYX-s3jsEQWl.js} +1 -1
  34. package/dist/gui/assets/index-Cft6LQWD.css +1 -0
  35. package/dist/gui/assets/index-eECshTCa.js +36 -0
  36. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-D35ODxoC.js → infoDiagram-FWYZ7A6U-xmq3a-E3.js} +1 -1
  37. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-B-mZqJl4.js → ishikawaDiagram-FXEZZL3T-CFPUA4LV.js} +1 -1
  38. package/dist/gui/assets/{journeyDiagram-5HDEW3XC-CmFLCu-y.js → journeyDiagram-5HDEW3XC-BOiMo2WC.js} +1 -1
  39. package/dist/gui/assets/{kanban-definition-HUTT4EX6-C-risdsz.js → kanban-definition-HUTT4EX6-o_oz2FbJ.js} +1 -1
  40. package/dist/gui/assets/{linear-D5TY5eoS.js → linear-D1YtFTY4.js} +1 -1
  41. package/dist/gui/assets/{mermaid.core-wGjSg1h-.js → mermaid.core-DQyDucFo.js} +4 -4
  42. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-BSdCxt4H.js → mindmap-definition-LN4V7U3C-CX9Et0Qr.js} +1 -1
  43. package/dist/gui/assets/{pegDiagram-2B236MQR-DrexK9bW.js → pegDiagram-2B236MQR-DKoIr3v7.js} +1 -1
  44. package/dist/gui/assets/{pieDiagram-ENE6RG2P-ay6UKmgt.js → pieDiagram-ENE6RG2P-BSJigzmR.js} +1 -1
  45. package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-DgoOnLZO.js → quadrantDiagram-ABIIQ3AL-Bpmh7Mn0.js} +1 -1
  46. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-Dsg3x2_p.js → railroadDiagram-RFXS5EU6-DoaeONQd.js} +1 -1
  47. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-BtbCKvwF.js → requirementDiagram-TGXJPOKE-B7GlOm4X.js} +1 -1
  48. package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-TabZvNT3.js → sankeyDiagram-HTMAVEWB-B7IwnQiq.js} +1 -1
  49. package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C53WsGFo.js → sequenceDiagram-DBY2YBRQ-Nwq2uhP9.js} +1 -1
  50. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-D_JRhNnb.js → sizeCapture-X5ZJPWSS-CCdTHNj_.js} +1 -1
  51. package/dist/gui/assets/{stateDiagram-2N3HPSRC-D-_V4W7N.js → stateDiagram-2N3HPSRC-CqxRfIAb.js} +1 -1
  52. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CWQWNOHt.js +1 -0
  53. package/dist/gui/assets/{swimlanes-5IMT3BWC-DgyOlILO.js → swimlanes-5IMT3BWC-Bn-R9jtG.js} +2 -2
  54. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-D7kdlDFk.js +8 -0
  55. package/dist/gui/assets/{timeline-definition-FHXFAJF6-7M8Rj4NA.js → timeline-definition-FHXFAJF6-BKXgnjEG.js} +1 -1
  56. package/dist/gui/assets/{vennDiagram-L72KCM5P-DVyooCr2.js → vennDiagram-L72KCM5P-Czw5UU3t.js} +1 -1
  57. package/dist/gui/assets/{wardleyDiagram-EHGQE667-CYry07bZ.js → wardleyDiagram-EHGQE667-BJS3BME3.js} +1 -1
  58. package/dist/gui/assets/{xychartDiagram-FW5EYKEG-Dvx8ojch.js → xychartDiagram-FW5EYKEG-DBt-6lUr.js} +1 -1
  59. package/dist/gui/index.html +7 -12
  60. package/package.json +1 -1
  61. package/dist/gui/assets/channel-BAEHbRlk.js +0 -1
  62. package/dist/gui/assets/classDiagram-OUVF2IWQ-2TSTdFv6.js +0 -1
  63. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-2TSTdFv6.js +0 -1
  64. package/dist/gui/assets/index-DDGLj_Fo.js +0 -36
  65. package/dist/gui/assets/index-Dwx7eFhj.css +0 -1
  66. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-COKZr5A4.js +0 -1
  67. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BaU_Y0XV.js +0 -8
package/dist/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir, platform } from "node:os";
3
3
  import { Command, Option } from "commander";
4
- import { stat, mkdir, readFile, writeFile, rename, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
4
+ import { stat, mkdir, writeFile, rename, readFile, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
5
5
  import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, sep, relative, normalize } from "node:path";
6
6
  import { createHash, randomUUID, randomBytes } from "node:crypto";
7
7
  import { existsSync, openSync, closeSync, statSync, readSync, mkdirSync, watch, createReadStream } from "node:fs";
@@ -23,7 +23,7 @@ import { createOpencode } from "@opencode-ai/sdk";
23
23
  import { createInterface as createInterface$1 } from "node:readline/promises";
24
24
  import MarkdownIt from "markdown-it";
25
25
  const name = "@yorz/cli";
26
- const version = "0.6.5";
26
+ const version = "0.7.0";
27
27
  const type = "module";
28
28
  const description = "";
29
29
  const bin = { "yorz": "./dist/cli/index.js" };
@@ -62,6 +62,11 @@ const DEFAULT_NOTIFICATIONS = {
62
62
  const DEFAULT_POWER = {
63
63
  inhibitWhenRunning: "system-default"
64
64
  };
65
+ const DEFAULT_APPEARANCE = {
66
+ themeMode: "system",
67
+ themeName: "terminal",
68
+ language: "zh-CN"
69
+ };
65
70
  const SHORTCUT_ACTION_IDS = [
66
71
  "newSpec",
67
72
  "toggleSpecDetailFullscreen",
@@ -74,6 +79,9 @@ function resolveGlobalConfigDir(env = process.env) {
74
79
  return join(base, "yorz");
75
80
  }
76
81
  function resolveGlobalConfigPath(env = process.env) {
82
+ return join(resolveGlobalConfigDir(env), "config.json");
83
+ }
84
+ function resolveLegacyGlobalProjectsPath(env = process.env) {
77
85
  return join(resolveGlobalConfigDir(env), "projects.json");
78
86
  }
79
87
  function resolveGlobalSkillsDir(env = process.env) {
@@ -84,7 +92,14 @@ function resolveSkillEntry(skillName, env = process.env) {
84
92
  }
85
93
  async function loadGlobalConfig(filePath) {
86
94
  const fp = filePath ?? resolveGlobalConfigPath();
87
- if (!existsSync(fp)) return defaultGlobalConfig();
95
+ if (!existsSync(fp)) {
96
+ const legacy = filePath ? basename$1(fp) === "config.json" ? join(dirname(fp), "projects.json") : "" : resolveLegacyGlobalProjectsPath();
97
+ if (legacy && existsSync(legacy)) return loadGlobalConfigFromPath(legacy);
98
+ return defaultGlobalConfig();
99
+ }
100
+ return loadGlobalConfigFromPath(fp);
101
+ }
102
+ async function loadGlobalConfigFromPath(fp) {
88
103
  let raw;
89
104
  try {
90
105
  raw = await readFile(fp, "utf8");
@@ -134,7 +149,9 @@ function normalizeConfig$1(value) {
134
149
  agent: normalizeAgent$1(obj.agent),
135
150
  notifications: normalizeNotifications(obj.notifications),
136
151
  shortcuts: normalizeShortcuts(obj.shortcuts),
137
- power: normalizePower(obj.power)
152
+ power: normalizePower(obj.power),
153
+ appearance: normalizeAppearance(obj.appearance),
154
+ customInstructions: normalizeCustomInstructions(obj.customInstructions)
138
155
  };
139
156
  }
140
157
  function defaultGlobalConfig() {
@@ -151,7 +168,9 @@ function defaultGlobalConfig() {
151
168
  shortcuts: {},
152
169
  power: {
153
170
  inhibitWhenRunning: DEFAULT_POWER.inhibitWhenRunning
154
- }
171
+ },
172
+ appearance: { ...DEFAULT_APPEARANCE },
173
+ customInstructions: []
155
174
  };
156
175
  }
157
176
  function normalizeAgent$1(value) {
@@ -202,6 +221,51 @@ function normalizePower(value) {
202
221
  }
203
222
  return { inhibitWhenRunning: DEFAULT_POWER.inhibitWhenRunning };
204
223
  }
224
+ function normalizeAppearance(value) {
225
+ if (!value || typeof value !== "object") return { ...DEFAULT_APPEARANCE };
226
+ const obj = value;
227
+ const themeMode = obj.themeMode;
228
+ const themeName = obj.themeName;
229
+ const language = obj.language;
230
+ return {
231
+ themeMode: isThemeMode(themeMode) ? themeMode : DEFAULT_APPEARANCE.themeMode,
232
+ themeName: isThemeName(themeName) ? themeName : DEFAULT_APPEARANCE.themeName,
233
+ language: isLanguage(language) ? language : DEFAULT_APPEARANCE.language
234
+ };
235
+ }
236
+ function normalizeCustomInstructions(value) {
237
+ if (!Array.isArray(value)) return [];
238
+ const out = [];
239
+ const seen = /* @__PURE__ */ new Set();
240
+ for (const item of value) {
241
+ if (!item || typeof item !== "object") continue;
242
+ const obj = item;
243
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
244
+ const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
245
+ if (!id || !name2 || seen.has(id) || !/^[\w-]+$/.test(name2)) continue;
246
+ seen.add(id);
247
+ out.push({
248
+ id,
249
+ name: name2,
250
+ description: typeof obj.description === "string" ? obj.description : "",
251
+ // Fall back to the pre-rename `systemPrompt` so existing configs keep
252
+ // working; the next save rewrites them under the new key.
253
+ hiddenPrompt: typeof obj.hiddenPrompt === "string" ? obj.hiddenPrompt : typeof obj.systemPrompt === "string" ? obj.systemPrompt : "",
254
+ prefill: typeof obj.prefill === "string" ? obj.prefill : "",
255
+ createdAt: typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0
256
+ });
257
+ }
258
+ return out;
259
+ }
260
+ function isThemeMode(value) {
261
+ return value === "system" || value === "light" || value === "dark";
262
+ }
263
+ function isThemeName(value) {
264
+ return value === "terminal" || value === "graphite" || value === "paper";
265
+ }
266
+ function isLanguage(value) {
267
+ return value === "zh-CN" || value === "en";
268
+ }
205
269
  function normalizeShortcutBinding(value) {
206
270
  const parts = value.split("+").map((part) => part.trim()).filter(Boolean);
207
271
  let key = "";
@@ -1540,6 +1604,25 @@ function parseAnnotateBody(body) {
1540
1604
  if (note.length > 2e3) return { error: "note too long (max 2000)" };
1541
1605
  return { sectionPath, quote, note };
1542
1606
  }
1607
+ const HIDDEN_PROMPT_OPEN = "<!-- yorz:hidden -->";
1608
+ const HIDDEN_PROMPT_CLOSE = "<!-- /yorz:hidden -->";
1609
+ const SLASH_NAME_RE$1 = /^\/([\w-]+)(?:\s|$)/;
1610
+ function matchCustomInstruction(prompt, instructions) {
1611
+ const matched = SLASH_NAME_RE$1.exec(prompt.trim());
1612
+ if (!matched) return null;
1613
+ const name2 = matched[1];
1614
+ return instructions.find((item) => item.name === name2) ?? null;
1615
+ }
1616
+ function wrapHiddenPrompt(hidden, original) {
1617
+ const body = hidden.trim();
1618
+ if (!body) return original;
1619
+ return [HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE, original].join("\n");
1620
+ }
1621
+ function appendHiddenPrompt(original, hidden) {
1622
+ const body = hidden.trim();
1623
+ if (!body) return original;
1624
+ return [original, "", HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE].join("\n");
1625
+ }
1543
1626
  const CHAT_DEBUG_DIR_REL = ".yorz/tmp/debug";
1544
1627
  const CHAT_DEBUG_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1545
1628
  const CHAT_DEBUG_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
@@ -1547,18 +1630,18 @@ function isYorzDebugCommand(prompt) {
1547
1630
  return /^\/yorz-debug(?:\s|$)/.test(prompt.trim());
1548
1631
  }
1549
1632
  function buildChatDebugPrompt(prompt, now = /* @__PURE__ */ new Date()) {
1550
- const body = prompt.trim().replace(/^\/yorz-debug(?:\s+|$)/, "").trim();
1633
+ const original = prompt.trim();
1634
+ const body = original.replace(/^\/yorz-debug(?:\s+|$)/, "").trim();
1551
1635
  const debugPath = `${CHAT_DEBUG_DIR_REL}/debug-${formatDebugTimestamp(now)}.md`;
1552
- const bug = body || "请先根据对话上下文确认待调试问题;若上下文不足,请向用户补齐复现信息。";
1553
- return [
1636
+ const guide = [
1554
1637
  `${skillRef("yorz-debug")},然后进入 Debug 模式。`,
1555
1638
  `本次是普通 chat 独立触发,没有 spec_dir。Debug 活文档必须写入临时文件 \`${debugPath}\`。`,
1556
1639
  `写入前请确保目录 \`${CHAT_DEBUG_DIR_REL}/\` 存在;该目录属于临时目录,会由 YorZ 定时清理。`,
1557
1640
  `如果该文件不存在则创建;本文件只承载本次 chat debug 记录,不需要追加复用其他文件。`,
1558
1641
  "",
1559
- "待调试问题:",
1560
- bug
1642
+ body ? "待调试问题见下方用户输入(忽略其中的 `/yorz-debug` 指令前缀):" : "请先根据对话上下文确认待调试问题;若上下文不足,请向用户补齐复现信息。"
1561
1643
  ].join("\n");
1644
+ return wrapHiddenPrompt(guide, original);
1562
1645
  }
1563
1646
  async function cleanupExpiredChatDebugFiles(projectPath, opts = {}) {
1564
1647
  const root = join(projectPath, CHAT_DEBUG_DIR_REL);
@@ -1607,6 +1690,55 @@ function formatDebugTimestamp(date) {
1607
1690
  pad(date.getSeconds())
1608
1691
  ].join("");
1609
1692
  }
1693
+ const SLASH_NAME_RE = /^\/([\w-]+)(?:\s|$)/;
1694
+ const SPEC_COMMAND_RE = /^\/yorz-spec(?:\s|$)/;
1695
+ function isSlashCommand(prompt) {
1696
+ return SLASH_NAME_RE.test(prompt.trim());
1697
+ }
1698
+ function buildChatSpecPrompt(prompt, specsDirRelative) {
1699
+ const original = prompt.trim();
1700
+ const body = original.replace(SPEC_COMMAND_RE, "").trim();
1701
+ const guide = [
1702
+ `${skillRef("yorz-spec")},然后按其自动模式判定推进。`,
1703
+ `本次是普通 chat 独立触发,未指定 spec_path:若当前会话上下文中已出现过 spec 文档,继续推进该 spec;否则按 skill 的「新建 spec」流程创建后立即推进 plan 阶段。`,
1704
+ `spec 目录为 \`${specsDirRelative}/\`。`,
1705
+ "",
1706
+ body ? "本次需求见下方用户输入(忽略其中的 `/yorz-spec` 指令前缀):" : "请先根据对话上下文确认本次要处理的 spec 或需求;若上下文不足,请向用户补齐后再推进。"
1707
+ ].join("\n");
1708
+ return wrapHiddenPrompt(guide, original);
1709
+ }
1710
+ function buildUnknownCommandPrompt(prompt, hit) {
1711
+ const original = prompt.trim();
1712
+ const lines = [
1713
+ "下方用户输入以 `/` 开头,这是 YorZ 输入框的指令语法,**不是**你自身的 slash command,请勿按命令解析、也不要回复 `Unknown command`。"
1714
+ ];
1715
+ if (hit) {
1716
+ lines.push(
1717
+ `\`/${hit.name}\` 是用户在 YorZ 中配置的自定义指令,但未配置隐藏提示词${hit.description ? `;其说明为:${hit.description}` : ""}。请结合该指令名与下方正文理解用户意图。`
1718
+ );
1719
+ } else {
1720
+ lines.push("该指令未在 YorZ 中配置,请忽略指令前缀,按普通文本理解下方正文。");
1721
+ }
1722
+ return wrapHiddenPrompt(lines.join("\n"), original);
1723
+ }
1724
+ function resolveChatPrompt(prompt, instructions, opts = {}) {
1725
+ const original = prompt.trim();
1726
+ if (!isSlashCommand(original)) return { prompt, builtin: null };
1727
+ if (isYorzDebugCommand(original)) {
1728
+ return { prompt: buildChatDebugPrompt(original, opts.now), builtin: "yorz-debug" };
1729
+ }
1730
+ if (SPEC_COMMAND_RE.test(original)) {
1731
+ return {
1732
+ prompt: buildChatSpecPrompt(original, opts.specsDirRelative ?? ".yorz/specs"),
1733
+ builtin: "yorz-spec"
1734
+ };
1735
+ }
1736
+ const hit = matchCustomInstruction(original, instructions);
1737
+ if (hit?.hiddenPrompt.trim()) {
1738
+ return { prompt: wrapHiddenPrompt(hit.hiddenPrompt, original), builtin: null };
1739
+ }
1740
+ return { prompt: buildUnknownCommandPrompt(original, hit), builtin: null };
1741
+ }
1610
1742
  const KINDS = ["claude", "codex", "opencode"];
1611
1743
  const DRAFT_ID_RE = /^[a-zA-Z0-9-]{1,64}$/;
1612
1744
  function buildChatPrompt(prompt, draftId, attachments) {
@@ -1616,13 +1748,14 @@ function buildChatPrompt(prompt, draftId, attachments) {
1616
1748
  const rel = `${dir}/${a.storedName}`;
1617
1749
  return a.kind === "image" ? `- ![${a.name}](${rel})` : `- [${a.name}](${rel})`;
1618
1750
  });
1619
- return [
1751
+ return appendHiddenPrompt(
1620
1752
  prompt,
1621
- "",
1622
- "---",
1623
- `本次消息附带 ${attachments.length} 个附件,已保存在临时目录 \`${dir}/\`(请按需用文件工具直接读取,无需迁移):`,
1624
- ...lines
1625
- ].join("\n");
1753
+ [
1754
+ "---",
1755
+ `本次消息附带 ${attachments.length} 个附件,已保存在临时目录 \`${dir}/\`(请按需用文件工具直接读取,无需迁移):`,
1756
+ ...lines
1757
+ ].join("\n")
1758
+ );
1626
1759
  }
1627
1760
  function createSessionsRoutes(resolveProject) {
1628
1761
  const app = new Hono();
@@ -1638,6 +1771,11 @@ function createSessionsRoutes(resolveProject) {
1638
1771
  if (p instanceof Response) return p;
1639
1772
  return c.json(await p.sessions.listSessions());
1640
1773
  });
1774
+ app.get("/projects/:projectId/agent-usage", async (c) => {
1775
+ const p = await need(c);
1776
+ if (p instanceof Response) return p;
1777
+ return c.json(await p.sessions.getUsageStatus());
1778
+ });
1641
1779
  app.post("/projects/:projectId/sessions", async (c) => {
1642
1780
  const p = await need(c);
1643
1781
  if (p instanceof Response) return p;
@@ -1689,7 +1827,11 @@ function createSessionsRoutes(resolveProject) {
1689
1827
  }
1690
1828
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
1691
1829
  if (!prompt) return c.json({ error: "prompt required" }, 400);
1692
- let finalPrompt = isYorzDebugCommand(prompt) ? buildChatDebugPrompt(prompt) : prompt;
1830
+ const instructions = isSlashCommand(prompt) ? await loadGlobalConfig().then((cfg) => cfg.customInstructions).catch(() => []) : [];
1831
+ const resolved = resolveChatPrompt(prompt, instructions, {
1832
+ specsDirRelative: p.specsDirRelative
1833
+ });
1834
+ let finalPrompt = resolved.prompt;
1693
1835
  if (body.draftId !== void 0) {
1694
1836
  if (typeof body.draftId !== "string" || !DRAFT_ID_RE.test(body.draftId)) {
1695
1837
  return c.json({ error: "draftId has invalid format" }, 400);
@@ -1700,9 +1842,9 @@ function createSessionsRoutes(resolveProject) {
1700
1842
  finalPrompt = buildChatPrompt(finalPrompt, draftId, metas);
1701
1843
  }
1702
1844
  }
1703
- if (isYorzDebugCommand(prompt)) void cleanupExpiredChatDebugFiles(p.path).catch(() => {
1845
+ if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
1704
1846
  });
1705
- const handle = await p.sessions.send(c.req.param("sid"), finalPrompt);
1847
+ const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt);
1706
1848
  return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
1707
1849
  });
1708
1850
  app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
@@ -1713,13 +1855,13 @@ function createSessionsRoutes(resolveProject) {
1713
1855
  });
1714
1856
  return app;
1715
1857
  }
1716
- const execFileP$1 = promisify(execFile);
1858
+ const execFileP$2 = promisify(execFile);
1717
1859
  function withHiddenWindowsConsole(options, platform2 = process.platform) {
1718
1860
  if (platform2 !== "win32") return options;
1719
1861
  return { ...options, windowsHide: true };
1720
1862
  }
1721
1863
  async function execFileWithoutWindow(file, args, options) {
1722
- const result = await execFileP$1(file, args, withHiddenWindowsConsole(options));
1864
+ const result = await execFileP$2(file, args, withHiddenWindowsConsole(options));
1723
1865
  return { stdout: result.stdout, stderr: result.stderr };
1724
1866
  }
1725
1867
  function spawnWithoutWindow(command, args, options) {
@@ -2802,7 +2944,9 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
2802
2944
  agent: cfg.agent,
2803
2945
  notifications: cfg.notifications,
2804
2946
  shortcuts: cfg.shortcuts,
2805
- power: cfg.power
2947
+ power: cfg.power,
2948
+ appearance: cfg.appearance,
2949
+ customInstructions: cfg.customInstructions
2806
2950
  });
2807
2951
  });
2808
2952
  app.put("/global-config", async (c) => {
@@ -2819,6 +2963,8 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
2819
2963
  cfg.notifications = parsed.notifications;
2820
2964
  cfg.shortcuts = parsed.shortcuts;
2821
2965
  cfg.power = parsed.power;
2966
+ cfg.appearance = parsed.appearance;
2967
+ cfg.customInstructions = parsed.customInstructions;
2822
2968
  await saveGlobalConfig(cfg, globalConfigPath);
2823
2969
  await powerController.refresh();
2824
2970
  return c.json({
@@ -2827,7 +2973,9 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
2827
2973
  agent: cfg.agent,
2828
2974
  notifications: cfg.notifications,
2829
2975
  shortcuts: cfg.shortcuts,
2830
- power: cfg.power
2976
+ power: cfg.power,
2977
+ appearance: cfg.appearance,
2978
+ customInstructions: cfg.customInstructions
2831
2979
  }
2832
2980
  });
2833
2981
  });
@@ -2887,13 +3035,73 @@ function parseBody(value) {
2887
3035
  error: "power.inhibitWhenRunning must be system-default | prevent-display-sleep | keep-system-awake"
2888
3036
  };
2889
3037
  }
3038
+ const appearance = parseAppearance(obj.appearance ?? DEFAULT_APPEARANCE);
3039
+ if ("error" in appearance) return appearance;
3040
+ const customInstructions = parseCustomInstructions(obj.customInstructions);
3041
+ if ("error" in customInstructions) return customInstructions;
2890
3042
  return {
2891
3043
  agent: { defaultKind },
2892
3044
  notifications: { sessionEnd: { banner: s.banner, sound: s.sound } },
2893
3045
  shortcuts: normalizeShortcuts(shortcutObj),
2894
- power: { inhibitWhenRunning }
3046
+ power: { inhibitWhenRunning },
3047
+ appearance,
3048
+ customInstructions
2895
3049
  };
2896
3050
  }
3051
+ function parseAppearance(value) {
3052
+ if (!value || typeof value !== "object") return { error: "appearance must be an object" };
3053
+ const obj = value;
3054
+ const themeMode = obj.themeMode;
3055
+ if (!isThemeMode(themeMode))
3056
+ return { error: "appearance.themeMode must be system | light | dark" };
3057
+ const themeName = obj.themeName;
3058
+ if (!isThemeName(themeName)) {
3059
+ return { error: "appearance.themeName must be terminal | graphite | paper" };
3060
+ }
3061
+ const language = obj.language;
3062
+ if (!isLanguage(language)) return { error: "appearance.language must be zh-CN | en" };
3063
+ return { themeMode, themeName, language };
3064
+ }
3065
+ function parseCustomInstructions(value) {
3066
+ if (value === void 0) return [];
3067
+ if (!Array.isArray(value)) return { error: "customInstructions must be an array" };
3068
+ const out = [];
3069
+ const seen = /* @__PURE__ */ new Set();
3070
+ for (const [index, item] of value.entries()) {
3071
+ if (!item || typeof item !== "object") {
3072
+ return { error: `customInstructions.${index} must be an object` };
3073
+ }
3074
+ const obj = item;
3075
+ const id = typeof obj.id === "string" ? obj.id.trim() : "";
3076
+ const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
3077
+ if (!id) return { error: `customInstructions.${index}.id required` };
3078
+ if (!name2 || !/^[\w-]+$/.test(name2)) {
3079
+ return {
3080
+ error: `customInstructions.${index}.name must use letters, numbers, underscores, or hyphens`
3081
+ };
3082
+ }
3083
+ if (seen.has(id)) return { error: `duplicate custom instruction id: ${id}` };
3084
+ seen.add(id);
3085
+ const description2 = obj.description;
3086
+ const hiddenPrompt = obj.hiddenPrompt ?? obj.systemPrompt;
3087
+ const prefill = obj.prefill;
3088
+ const createdAt = obj.createdAt;
3089
+ if (typeof description2 !== "string") {
3090
+ return { error: `customInstructions.${index}.description must be a string` };
3091
+ }
3092
+ if (typeof hiddenPrompt !== "string") {
3093
+ return { error: `customInstructions.${index}.hiddenPrompt must be a string` };
3094
+ }
3095
+ if (typeof prefill !== "string") {
3096
+ return { error: `customInstructions.${index}.prefill must be a string` };
3097
+ }
3098
+ if (typeof createdAt !== "number" || createdAt <= 0) {
3099
+ return { error: `customInstructions.${index}.createdAt must be a positive number` };
3100
+ }
3101
+ out.push({ id, name: name2, description: description2, hiddenPrompt, prefill, createdAt });
3102
+ }
3103
+ return out;
3104
+ }
2897
3105
  function createSpecDraftsRoutes(resolveProject) {
2898
3106
  const app = new Hono();
2899
3107
  const need = async (c) => {
@@ -4104,7 +4312,7 @@ class RegistryEventBus {
4104
4312
  this.scheduleEmit();
4105
4313
  });
4106
4314
  } catch (err) {
4107
- registryLog().warn("projects.json watch failed", { dir, message: err.message });
4315
+ registryLog().warn("global config watch failed", { dir, message: err.message });
4108
4316
  }
4109
4317
  }
4110
4318
  stop() {
@@ -5305,6 +5513,37 @@ function toolResultText(content) {
5305
5513
  }
5306
5514
  return content == null ? "" : JSON.stringify(content);
5307
5515
  }
5516
+ const USAGE_WINDOWS = [
5517
+ ["five_hour", "5-hour"],
5518
+ ["seven_day", "7-day"],
5519
+ ["seven_day_sonnet", "Sonnet 7-day"],
5520
+ ["seven_day_opus", "Opus 7-day"],
5521
+ ["seven_day_oauth_apps", "OAuth apps 7-day"]
5522
+ ];
5523
+ function usageWindows(usage) {
5524
+ const out = [];
5525
+ const limits = usage.rate_limits;
5526
+ if (!limits) return out;
5527
+ for (const [key, label] of USAGE_WINDOWS) {
5528
+ const win = limits[key];
5529
+ if (!win) continue;
5530
+ out.push({
5531
+ key,
5532
+ label,
5533
+ utilization: typeof win.utilization === "number" ? win.utilization : null,
5534
+ resetsAt: typeof win.resets_at === "string" ? win.resets_at : null
5535
+ });
5536
+ }
5537
+ for (const win of limits.model_scoped ?? []) {
5538
+ out.push({
5539
+ key: `model:${win.display_name}`,
5540
+ label: win.display_name,
5541
+ utilization: typeof win.utilization === "number" ? win.utilization : null,
5542
+ resetsAt: typeof win.resets_at === "string" ? win.resets_at : null
5543
+ });
5544
+ }
5545
+ return out;
5546
+ }
5308
5547
  class ClaudeSession {
5309
5548
  constructor(id, cwd, isNew) {
5310
5549
  this.id = id;
@@ -5414,10 +5653,40 @@ class ClaudeAdapter {
5414
5653
  return out;
5415
5654
  }
5416
5655
  capabilities() {
5417
- return { listSessions: true, getMessages: true };
5656
+ return { listSessions: true, getMessages: true, usageStatus: true };
5657
+ }
5658
+ async getUsageStatus() {
5659
+ const options = {
5660
+ cwd: this.cwd,
5661
+ permissionMode: "bypassPermissions",
5662
+ allowDangerouslySkipPermissions: true
5663
+ };
5664
+ const q = query({ prompt: "", options });
5665
+ try {
5666
+ const usage = await q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET();
5667
+ q.close();
5668
+ return {
5669
+ kind: this.kind,
5670
+ status: usage.rate_limits_available ? "available" : "unavailable",
5671
+ checkedAt: Date.now(),
5672
+ subscriptionType: usage.subscription_type,
5673
+ rateLimitsAvailable: usage.rate_limits_available,
5674
+ windows: usageWindows(usage),
5675
+ message: usage.rate_limits_available ? void 0 : "claude usage rate limits are unavailable for this auth/provider"
5676
+ };
5677
+ } catch (err) {
5678
+ q.close();
5679
+ return {
5680
+ kind: this.kind,
5681
+ status: "error",
5682
+ checkedAt: Date.now(),
5683
+ message: err instanceof Error ? err.message : String(err)
5684
+ };
5685
+ }
5418
5686
  }
5419
5687
  }
5420
5688
  const DEFAULT_CODEX_STORAGE_ROOT = join(homedir(), ".codex");
5689
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
5421
5690
  const TITLE_MAX_LENGTH$1 = 64;
5422
5691
  function codexThreadOptions(cwd) {
5423
5692
  return {
@@ -5474,6 +5743,107 @@ function parseCodexSessionIndex(text) {
5474
5743
  }
5475
5744
  return out;
5476
5745
  }
5746
+ function asRecord$1(value) {
5747
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5748
+ }
5749
+ function numberFrom$1(value) {
5750
+ if (typeof value === "number" && Number.isFinite(value)) return value;
5751
+ if (typeof value === "string" && value.trim()) {
5752
+ const n = Number(value);
5753
+ if (Number.isFinite(n)) return n;
5754
+ }
5755
+ return null;
5756
+ }
5757
+ function resetFrom$1(value) {
5758
+ if (typeof value === "string" && value.trim()) {
5759
+ const n2 = Number(value);
5760
+ if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
5761
+ const d = new Date(value);
5762
+ return Number.isFinite(d.getTime()) ? d.toISOString() : null;
5763
+ }
5764
+ const n = numberFrom$1(value);
5765
+ return n == null ? null : new Date(n * 1e3).toISOString();
5766
+ }
5767
+ function codexWindow(key, label, value) {
5768
+ const rec = asRecord$1(value);
5769
+ if (!rec) return null;
5770
+ const utilization = numberFrom$1(rec.used_percent) ?? numberFrom$1(rec.usedPercent) ?? numberFrom$1(rec.utilization) ?? numberFrom$1(rec.percent_used);
5771
+ if (utilization == null) return null;
5772
+ return {
5773
+ key,
5774
+ label,
5775
+ utilization,
5776
+ resetsAt: resetFrom$1(rec.resets_at ?? rec.reset_at ?? rec.resetsAt ?? rec.resetAt)
5777
+ };
5778
+ }
5779
+ function parseCodexUsageResponse(raw) {
5780
+ const root = asRecord$1(raw);
5781
+ if (!root) return null;
5782
+ const rateLimit = asRecord$1(root.rate_limit);
5783
+ const rateLimits = asRecord$1(root.rate_limits);
5784
+ const windows = [];
5785
+ const primary = codexWindow(
5786
+ "primary",
5787
+ "Primary",
5788
+ rateLimit?.primary_window ?? rateLimits?.primary ?? root.primary
5789
+ );
5790
+ if (primary) windows.push(primary);
5791
+ const secondary = codexWindow(
5792
+ "secondary",
5793
+ "Secondary",
5794
+ rateLimit?.secondary_window ?? rateLimits?.secondary ?? root.secondary
5795
+ );
5796
+ if (secondary) windows.push(secondary);
5797
+ const additional = root.additional_rate_limits ?? root.additionalRateLimits ?? rateLimit?.additional;
5798
+ if (Array.isArray(additional)) {
5799
+ for (const [idx, item] of additional.entries()) {
5800
+ const rec = asRecord$1(item);
5801
+ const label = String(rec?.label ?? rec?.name ?? rec?.limit_name ?? `Additional ${idx + 1}`);
5802
+ const win = codexWindow(`additional:${idx}`, label, item);
5803
+ if (win) windows.push(win);
5804
+ }
5805
+ }
5806
+ if (windows.length === 0) return null;
5807
+ return {
5808
+ kind: "codex",
5809
+ status: "available",
5810
+ checkedAt: Date.now(),
5811
+ source: "private-api",
5812
+ subscriptionType: typeof root.plan_type === "string" ? root.plan_type : null,
5813
+ rateLimitsAvailable: true,
5814
+ windows
5815
+ };
5816
+ }
5817
+ function parseCodexTokenCountSnapshot(text) {
5818
+ let latestTs = 0;
5819
+ let latest = null;
5820
+ for (const line of text.split("\n")) {
5821
+ const trimmed = line.trim();
5822
+ if (!trimmed) continue;
5823
+ let obj;
5824
+ try {
5825
+ obj = JSON.parse(trimmed);
5826
+ } catch {
5827
+ continue;
5828
+ }
5829
+ const rec = asRecord$1(obj);
5830
+ const payload = asRecord$1(rec?.payload);
5831
+ if (payload?.type !== "token_count") continue;
5832
+ const parsed = parseCodexUsageResponse({ rate_limits: payload.rate_limits });
5833
+ if (!parsed) continue;
5834
+ const ts = typeof rec?.timestamp === "string" ? Date.parse(rec.timestamp) : 0;
5835
+ if (ts >= latestTs) {
5836
+ latestTs = ts;
5837
+ latest = {
5838
+ ...parsed,
5839
+ checkedAt: Number.isFinite(ts) && ts > 0 ? ts : Date.now(),
5840
+ source: "local-snapshot",
5841
+ message: "codex local token_count snapshot"
5842
+ };
5843
+ }
5844
+ }
5845
+ return latest;
5846
+ }
5477
5847
  class CodexSession {
5478
5848
  constructor(thread) {
5479
5849
  this.thread = thread;
@@ -5597,7 +5967,52 @@ class CodexAdapter {
5597
5967
  return out;
5598
5968
  }
5599
5969
  capabilities() {
5600
- return { listSessions: true, getMessages: true };
5970
+ return { listSessions: true, getMessages: true, usageStatus: true };
5971
+ }
5972
+ async getUsageStatus() {
5973
+ const live = await this.readPrivateUsage().catch(() => null);
5974
+ if (live) return live;
5975
+ const snapshot = await this.readLatestUsageSnapshot().catch(() => null);
5976
+ if (snapshot) return snapshot;
5977
+ return {
5978
+ kind: this.kind,
5979
+ status: "unavailable",
5980
+ checkedAt: Date.now(),
5981
+ message: "codex usage is unavailable: private API failed and no local token_count snapshot found"
5982
+ };
5983
+ }
5984
+ async readPrivateUsage() {
5985
+ const auth = await this.readAuth();
5986
+ if (!auth) return null;
5987
+ const res = await fetch(CODEX_USAGE_URL, {
5988
+ headers: {
5989
+ authorization: `Bearer ${auth.accessToken}`,
5990
+ "chatgpt-account-id": auth.accountId
5991
+ }
5992
+ });
5993
+ if (!res.ok) return null;
5994
+ return parseCodexUsageResponse(await res.json());
5995
+ }
5996
+ async readAuth() {
5997
+ let raw;
5998
+ try {
5999
+ raw = JSON.parse(await readFile(join(this.storageRoot, "auth.json"), "utf8"));
6000
+ } catch {
6001
+ return null;
6002
+ }
6003
+ const tokens = asRecord$1(asRecord$1(raw)?.tokens);
6004
+ const accessToken = typeof tokens?.access_token === "string" ? tokens.access_token : "";
6005
+ const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : "";
6006
+ return accessToken && accountId ? { accessToken, accountId } : null;
6007
+ }
6008
+ async readLatestUsageSnapshot() {
6009
+ let latest = null;
6010
+ for (const file of await this.walkRollouts()) {
6011
+ const parsed = parseCodexTokenCountSnapshot(await readFile(file, "utf8"));
6012
+ if (!parsed) continue;
6013
+ if (!latest || parsed.checkedAt >= latest.checkedAt) latest = parsed;
6014
+ }
6015
+ return latest;
5601
6016
  }
5602
6017
  async walkRollouts() {
5603
6018
  const out = [];
@@ -5684,6 +6099,137 @@ class CodexAdapter {
5684
6099
  return null;
5685
6100
  }
5686
6101
  }
6102
+ const execFileP$1 = promisify(execFile);
6103
+ const OPENCODE_QUOTA_INSTALL = "npx @slkiser/opencode-quota@latest init";
6104
+ const OPENCODE_QUOTA_PLUGIN_BIN = join(
6105
+ homedir(),
6106
+ ".cache",
6107
+ "opencode",
6108
+ "packages",
6109
+ "@slkiser",
6110
+ "opencode-quota@latest",
6111
+ "node_modules",
6112
+ "@slkiser",
6113
+ "opencode-quota",
6114
+ "dist",
6115
+ "bin",
6116
+ "opencode-quota.js"
6117
+ );
6118
+ function asRecord(value) {
6119
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
6120
+ }
6121
+ function numberFrom(value) {
6122
+ if (typeof value === "number" && Number.isFinite(value)) return value;
6123
+ if (typeof value === "string" && value.trim()) {
6124
+ const n = Number(value);
6125
+ if (Number.isFinite(n)) return n;
6126
+ }
6127
+ return null;
6128
+ }
6129
+ function resetFrom(value) {
6130
+ if (typeof value === "string" && value.trim()) {
6131
+ const n2 = Number(value);
6132
+ if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
6133
+ const d = new Date(value);
6134
+ return Number.isFinite(d.getTime()) ? d.toISOString() : null;
6135
+ }
6136
+ const n = numberFrom(value);
6137
+ return n == null ? null : new Date(n * 1e3).toISOString();
6138
+ }
6139
+ function usageWindowFromRecord(rec, idx) {
6140
+ const label = String(rec.label ?? rec.name ?? rec.title ?? rec.provider ?? `Window ${idx + 1}`);
6141
+ const used = numberFrom(rec.usedPercent) ?? numberFrom(rec.used_percent) ?? numberFrom(rec.percent) ?? numberFrom(rec.utilization);
6142
+ const remaining = numberFrom(rec.remainingPercent) ?? numberFrom(rec.remaining_percent) ?? numberFrom(rec.percentRemaining);
6143
+ const utilization = used ?? (remaining == null ? null : 100 - remaining);
6144
+ if (utilization == null) return null;
6145
+ return {
6146
+ key: String(rec.key ?? rec.id ?? label),
6147
+ label,
6148
+ utilization,
6149
+ resetsAt: resetFrom(rec.resetsAt ?? rec.resets_at ?? rec.resetAt ?? rec.reset_at ?? rec.reset)
6150
+ };
6151
+ }
6152
+ function collectOpencodeQuotaWindows(value, out, seen) {
6153
+ if (!value || seen.has(value)) return;
6154
+ seen.add(value);
6155
+ if (Array.isArray(value)) {
6156
+ for (const item of value) collectOpencodeQuotaWindows(item, out, seen);
6157
+ return;
6158
+ }
6159
+ const rec = asRecord(value);
6160
+ if (!rec) return;
6161
+ const win = usageWindowFromRecord(rec, out.length);
6162
+ if (win) out.push(win);
6163
+ for (const key of ["windows", "quotas", "items", "limits", "entries"]) {
6164
+ if (key in rec) collectOpencodeQuotaWindows(rec[key], out, seen);
6165
+ }
6166
+ for (const key of ["providers", "data"]) {
6167
+ const child = asRecord(rec[key]);
6168
+ if (child) {
6169
+ for (const value2 of Object.values(child)) collectOpencodeQuotaWindows(value2, out, seen);
6170
+ } else if (key in rec) {
6171
+ collectOpencodeQuotaWindows(rec[key], out, seen);
6172
+ }
6173
+ }
6174
+ }
6175
+ function parseOpencodeQuotaOutput(text) {
6176
+ let raw;
6177
+ try {
6178
+ raw = JSON.parse(text);
6179
+ } catch {
6180
+ return parseOpencodeQuotaText(text);
6181
+ }
6182
+ const windows = [];
6183
+ collectOpencodeQuotaWindows(raw, windows, /* @__PURE__ */ new Set());
6184
+ if (windows.length === 0) return null;
6185
+ return {
6186
+ kind: "opencode",
6187
+ status: "available",
6188
+ checkedAt: Date.now(),
6189
+ source: "external-cli",
6190
+ rateLimitsAvailable: true,
6191
+ windows
6192
+ };
6193
+ }
6194
+ function parseOpencodeQuotaText(text) {
6195
+ const windows = [];
6196
+ let provider = "";
6197
+ for (const line of text.split("\n")) {
6198
+ const providerMatch = line.match(/^\s*→\s*\[(.+?)]/);
6199
+ if (providerMatch) {
6200
+ provider = providerMatch[1].trim();
6201
+ continue;
6202
+ }
6203
+ const quotaMatch = line.match(/^\s*(.+?quota)\s+.*?(\d+(?:\.\d+)?)%\s+left/i);
6204
+ if (!quotaMatch) continue;
6205
+ const label = [provider, quotaMatch[1].trim()].filter(Boolean).join(": ");
6206
+ const remaining = Number(quotaMatch[2]);
6207
+ windows.push({
6208
+ key: label || `quota:${windows.length}`,
6209
+ label: label || `Quota ${windows.length + 1}`,
6210
+ utilization: Math.max(0, Math.min(100, 100 - remaining)),
6211
+ resetsAt: null
6212
+ });
6213
+ }
6214
+ if (windows.length === 0) return null;
6215
+ return {
6216
+ kind: "opencode",
6217
+ status: "available",
6218
+ checkedAt: Date.now(),
6219
+ source: "external-cli",
6220
+ rateLimitsAvailable: true,
6221
+ windows
6222
+ };
6223
+ }
6224
+ async function opencodeQuotaCommands() {
6225
+ const commands = [{ cmd: "opencode-quota", args: ["show", "--json"] }];
6226
+ try {
6227
+ await access(OPENCODE_QUOTA_PLUGIN_BIN);
6228
+ commands.push({ cmd: process.execPath, args: [OPENCODE_QUOTA_PLUGIN_BIN, "show", "--json"] });
6229
+ } catch {
6230
+ }
6231
+ return commands;
6232
+ }
5687
6233
  function textParts(parts) {
5688
6234
  const out = [];
5689
6235
  for (const p of parts) {
@@ -5713,7 +6259,9 @@ class OpenCodeSession {
5713
6259
  if (this.aborted) return;
5714
6260
  const data = res.data;
5715
6261
  if (!data) {
5716
- const errMsg = res.error && typeof res.error === "object" && "data" in res.error ? String(res.error.data?.message ?? "prompt failed") : "prompt failed";
6262
+ const errMsg = res.error && typeof res.error === "object" && "data" in res.error ? String(
6263
+ res.error.data?.message ?? "prompt failed"
6264
+ ) : "prompt failed";
5717
6265
  yield { type: "error", message: errMsg };
5718
6266
  return;
5719
6267
  }
@@ -5795,7 +6343,51 @@ class OpenCodeAdapter {
5795
6343
  return out;
5796
6344
  }
5797
6345
  capabilities() {
5798
- return { listSessions: true, getMessages: true };
6346
+ return { listSessions: true, getMessages: true, usageStatus: true };
6347
+ }
6348
+ async getUsageStatus() {
6349
+ let lastError;
6350
+ for (const command of await opencodeQuotaCommands()) {
6351
+ try {
6352
+ const { stdout } = await execFileP$1(command.cmd, command.args, {
6353
+ cwd: this.cwd,
6354
+ timeout: 1e4,
6355
+ maxBuffer: 1024 * 1024
6356
+ });
6357
+ const parsed = parseOpencodeQuotaOutput(stdout);
6358
+ if (parsed) return parsed;
6359
+ return {
6360
+ kind: this.kind,
6361
+ status: "error",
6362
+ checkedAt: Date.now(),
6363
+ source: "external-cli",
6364
+ message: "opencode-quota returned no parseable quota windows"
6365
+ };
6366
+ } catch (err2) {
6367
+ lastError = err2;
6368
+ if (err2.code === "ENOENT") continue;
6369
+ break;
6370
+ }
6371
+ }
6372
+ const err = lastError;
6373
+ const code = err?.code;
6374
+ if (code === "ENOENT") {
6375
+ return {
6376
+ kind: this.kind,
6377
+ status: "unavailable",
6378
+ checkedAt: Date.now(),
6379
+ source: "external-cli",
6380
+ installCommand: OPENCODE_QUOTA_INSTALL,
6381
+ message: "opencode-quota is not installed"
6382
+ };
6383
+ }
6384
+ return {
6385
+ kind: this.kind,
6386
+ status: "error",
6387
+ checkedAt: Date.now(),
6388
+ source: "external-cli",
6389
+ message: err instanceof Error ? err.message : String(err)
6390
+ };
5799
6391
  }
5800
6392
  async dispose() {
5801
6393
  if (!this.booting) return;
@@ -5937,14 +6529,17 @@ class SessionManager {
5937
6529
  const indexed2 = byId.get(info.id);
5938
6530
  if (!indexed2) {
5939
6531
  byId.set(info.id, info);
5940
- } else if (isOpaqueTitle(indexed2.title, indexed2.id) && !isOpaqueTitle(info.title, info.id)) {
5941
- byId.set(info.id, {
5942
- ...indexed2,
5943
- title: info.title,
5944
- createdAt: Math.min(indexed2.createdAt, info.createdAt),
5945
- updatedAt: Math.max(indexed2.updatedAt, info.updatedAt)
5946
- });
6532
+ continue;
6533
+ }
6534
+ const merged = {
6535
+ ...indexed2,
6536
+ createdAt: Math.min(indexed2.createdAt, info.createdAt),
6537
+ updatedAt: Math.max(indexed2.updatedAt, info.updatedAt)
6538
+ };
6539
+ if (isOpaqueTitle(indexed2.title, indexed2.id) && !isOpaqueTitle(info.title, info.id)) {
6540
+ merged.title = info.title;
5947
6541
  }
6542
+ byId.set(info.id, merged);
5948
6543
  }
5949
6544
  } catch {
5950
6545
  }
@@ -5952,8 +6547,11 @@ class SessionManager {
5952
6547
  const alive = [...byId.values()].filter(
5953
6548
  (s) => nativeIds.has(s.id) || s.createdAt !== s.updatedAt || this.running.has(s.id)
5954
6549
  );
6550
+ const activityRank = (s) => this.running.has(s.id) ? 1 : 0;
5955
6551
  const out = [];
5956
- for (const s of alive.sort((a, b) => b.updatedAt - a.updatedAt)) {
6552
+ for (const s of alive.sort(
6553
+ (a, b) => activityRank(b) - activityRank(a) || b.updatedAt - a.updatedAt
6554
+ )) {
5957
6555
  out.push({ ...s, running: this.running.has(s.id) });
5958
6556
  if (out.length >= SESSION_LIST_LIMIT) break;
5959
6557
  }
@@ -6001,6 +6599,27 @@ class SessionManager {
6001
6599
  if (!adapter.capabilities().getMessages) return [];
6002
6600
  return adapter.getMessages(sid);
6003
6601
  }
6602
+ async getUsageStatus() {
6603
+ const adapter = this.adapters.get(this.defaultKind);
6604
+ if (!adapter.capabilities().usageStatus || !adapter.getUsageStatus) {
6605
+ return {
6606
+ kind: this.defaultKind,
6607
+ status: "unavailable",
6608
+ checkedAt: Date.now(),
6609
+ message: `${this.defaultKind} adapter does not support usage status queries`
6610
+ };
6611
+ }
6612
+ try {
6613
+ return await adapter.getUsageStatus();
6614
+ } catch (err) {
6615
+ return {
6616
+ kind: this.defaultKind,
6617
+ status: "error",
6618
+ checkedAt: Date.now(),
6619
+ message: err instanceof Error ? err.message : String(err)
6620
+ };
6621
+ }
6622
+ }
6004
6623
  async ensureLive(sid) {
6005
6624
  const existing = this.live.get(sid);
6006
6625
  if (existing) return existing;
@@ -6012,12 +6631,19 @@ class SessionManager {
6012
6631
  if (this.disposing) session.abort();
6013
6632
  return ls;
6014
6633
  }
6015
- async send(sid, prompt) {
6634
+ /**
6635
+ * `titleSource` lets callers derive the session title from the user's own
6636
+ * text while sending an expanded prompt to the Agent. Without it, an injected
6637
+ * hidden prompt would become the title.
6638
+ */
6639
+ async send(sid, prompt, titleSource) {
6016
6640
  const runId = randomUUID();
6017
6641
  const emitter = this.emitterFor(sid);
6018
6642
  let currentSid = sid;
6019
6643
  const startedAt = Date.now();
6020
- await this.maybeUpdateTitleFromPrompt(currentSid, prompt).catch(() => {
6644
+ await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
6645
+ });
6646
+ await this.store.touch(sid).catch(() => {
6021
6647
  });
6022
6648
  this.setRunning(sid, true);
6023
6649
  agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });