@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.
- package/dist/cli/index.js +665 -39
- package/dist/gui/assets/{abnfDiagram-VRR7QNED-Dysui3FU.js → abnfDiagram-VRR7QNED-pQ0yp01L.js} +1 -1
- package/dist/gui/assets/{arc-Dva0_Fbz.js → arc-k5zxnVZT.js} +1 -1
- package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-CSusN0wM.js → architectureDiagram-ZJ3FMSHR-DX-yKkN9.js} +1 -1
- package/dist/gui/assets/{blockDiagram-677ZJIJ3-Boy63uL4.js → blockDiagram-677ZJIJ3-CMoP3PmZ.js} +1 -1
- package/dist/gui/assets/{c4Diagram-LMCZKHZV-DREOCesJ.js → c4Diagram-LMCZKHZV-CFAjMeqQ.js} +1 -1
- package/dist/gui/assets/channel-sLqZREwh.js +1 -0
- package/dist/gui/assets/{chunk-2Q5K7J3B-CQGkW5Le.js → chunk-2Q5K7J3B-CSYRtuuw.js} +1 -1
- package/dist/gui/assets/{chunk-32BRIVSS-ByKgaOB0.js → chunk-32BRIVSS-BRohWK3g.js} +1 -1
- package/dist/gui/assets/{chunk-5VM5RSS4-ouTiIpFg.js → chunk-5VM5RSS4-BANnogAt.js} +1 -1
- package/dist/gui/assets/{chunk-EX3LRPZG-BBKKhIQU.js → chunk-EX3LRPZG-BcVKPAuP.js} +1 -1
- package/dist/gui/assets/{chunk-JWPE2WC7-i8qqnqZU.js → chunk-JWPE2WC7-D4KqJLpk.js} +1 -1
- package/dist/gui/assets/{chunk-MOJQB5TN-BmLvCJEW.js → chunk-MOJQB5TN-CDFJAykP.js} +1 -1
- package/dist/gui/assets/{chunk-RYQCIY6F-DgIPwUtq.js → chunk-RYQCIY6F-BJN0o293.js} +1 -1
- package/dist/gui/assets/{chunk-V7JOEXUC-CHFlZVs7.js → chunk-V7JOEXUC-B3m1VhnM.js} +1 -1
- package/dist/gui/assets/{chunk-VR4S4FIN-CF4gb-1V.js → chunk-VR4S4FIN-BLYOZzRK.js} +1 -1
- package/dist/gui/assets/{chunk-XXDRQBXY-6Ajr-TJy.js → chunk-XXDRQBXY-s69CKrwP.js} +1 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-DpZPCbfk.js +1 -0
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DpZPCbfk.js +1 -0
- package/dist/gui/assets/{cose-bilkent-JH36ORCC-Bq1NoHo9.js → cose-bilkent-JH36ORCC-BrNJSX1S.js} +1 -1
- package/dist/gui/assets/{cynefin-VYW2F7L2-BLZ43yIC.js → cynefin-VYW2F7L2-i8qxCu_0.js} +1 -1
- package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DOG8J1W-.js → cynefinDiagram-TSTJHNR4-dpueKRs1.js} +1 -1
- package/dist/gui/assets/{dagre-VKFMJZFB-6s-zIAym.js → dagre-VKFMJZFB-fqwQB8CS.js} +1 -1
- package/dist/gui/assets/{diagram-FQU43EPY-MdHGL1Ep.js → diagram-FQU43EPY-B99SBDqa.js} +1 -1
- package/dist/gui/assets/{diagram-G47NLZAW-vNqROz2G.js → diagram-G47NLZAW-DF4Oz_Q6.js} +1 -1
- package/dist/gui/assets/{diagram-NH7WQ7WH-mBHJAt54.js → diagram-NH7WQ7WH-BIaXJ_6B.js} +1 -1
- package/dist/gui/assets/{diagram-OA4YK3LP-Duzu45NT.js → diagram-OA4YK3LP-CJ4_-IYz.js} +1 -1
- package/dist/gui/assets/{diagram-WEI45ONY-B9KIE7jm.js → diagram-WEI45ONY-LvUrzfuG.js} +1 -1
- package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-D-VxYOb1.js → ebnfDiagram-CCIWWBDH-De5G_4vB.js} +1 -1
- package/dist/gui/assets/{erDiagram-Q63AITRT-bGX1YWnp.js → erDiagram-Q63AITRT-rYLQVUAr.js} +1 -1
- package/dist/gui/assets/{flowDiagram-23GEKE2U-BFbLxzKU.js → flowDiagram-23GEKE2U-CwDsoVsz.js} +1 -1
- package/dist/gui/assets/{ganttDiagram-NO4QXBWP-CFlioSP9.js → ganttDiagram-NO4QXBWP-Bm1KWOcG.js} +1 -1
- package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-9m210Kgm.js → gitGraphDiagram-IHSO6WYX-s3jsEQWl.js} +1 -1
- package/dist/gui/assets/index-Cft6LQWD.css +1 -0
- package/dist/gui/assets/index-eECshTCa.js +36 -0
- package/dist/gui/assets/{infoDiagram-FWYZ7A6U-D35ODxoC.js → infoDiagram-FWYZ7A6U-xmq3a-E3.js} +1 -1
- package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-B-mZqJl4.js → ishikawaDiagram-FXEZZL3T-CFPUA4LV.js} +1 -1
- package/dist/gui/assets/{journeyDiagram-5HDEW3XC-CmFLCu-y.js → journeyDiagram-5HDEW3XC-BOiMo2WC.js} +1 -1
- package/dist/gui/assets/{kanban-definition-HUTT4EX6-C-risdsz.js → kanban-definition-HUTT4EX6-o_oz2FbJ.js} +1 -1
- package/dist/gui/assets/{linear-D5TY5eoS.js → linear-D1YtFTY4.js} +1 -1
- package/dist/gui/assets/{mermaid.core-wGjSg1h-.js → mermaid.core-DQyDucFo.js} +4 -4
- package/dist/gui/assets/{mindmap-definition-LN4V7U3C-BSdCxt4H.js → mindmap-definition-LN4V7U3C-CX9Et0Qr.js} +1 -1
- package/dist/gui/assets/{pegDiagram-2B236MQR-DrexK9bW.js → pegDiagram-2B236MQR-DKoIr3v7.js} +1 -1
- package/dist/gui/assets/{pieDiagram-ENE6RG2P-ay6UKmgt.js → pieDiagram-ENE6RG2P-BSJigzmR.js} +1 -1
- package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-DgoOnLZO.js → quadrantDiagram-ABIIQ3AL-Bpmh7Mn0.js} +1 -1
- package/dist/gui/assets/{railroadDiagram-RFXS5EU6-Dsg3x2_p.js → railroadDiagram-RFXS5EU6-DoaeONQd.js} +1 -1
- package/dist/gui/assets/{requirementDiagram-TGXJPOKE-BtbCKvwF.js → requirementDiagram-TGXJPOKE-B7GlOm4X.js} +1 -1
- package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-TabZvNT3.js → sankeyDiagram-HTMAVEWB-B7IwnQiq.js} +1 -1
- package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C53WsGFo.js → sequenceDiagram-DBY2YBRQ-Nwq2uhP9.js} +1 -1
- package/dist/gui/assets/{sizeCapture-X5ZJPWSS-D_JRhNnb.js → sizeCapture-X5ZJPWSS-CCdTHNj_.js} +1 -1
- package/dist/gui/assets/{stateDiagram-2N3HPSRC-D-_V4W7N.js → stateDiagram-2N3HPSRC-CqxRfIAb.js} +1 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CWQWNOHt.js +1 -0
- package/dist/gui/assets/{swimlanes-5IMT3BWC-DgyOlILO.js → swimlanes-5IMT3BWC-Bn-R9jtG.js} +2 -2
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-D7kdlDFk.js +8 -0
- package/dist/gui/assets/{timeline-definition-FHXFAJF6-7M8Rj4NA.js → timeline-definition-FHXFAJF6-BKXgnjEG.js} +1 -1
- package/dist/gui/assets/{vennDiagram-L72KCM5P-DVyooCr2.js → vennDiagram-L72KCM5P-Czw5UU3t.js} +1 -1
- package/dist/gui/assets/{wardleyDiagram-EHGQE667-CYry07bZ.js → wardleyDiagram-EHGQE667-BJS3BME3.js} +1 -1
- package/dist/gui/assets/{xychartDiagram-FW5EYKEG-Dvx8ojch.js → xychartDiagram-FW5EYKEG-DBt-6lUr.js} +1 -1
- package/dist/gui/index.html +7 -12
- package/package.json +1 -1
- package/dist/gui/assets/channel-BAEHbRlk.js +0 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-2TSTdFv6.js +0 -1
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-2TSTdFv6.js +0 -1
- package/dist/gui/assets/index-DDGLj_Fo.js +0 -36
- package/dist/gui/assets/index-Dwx7eFhj.css +0 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-COKZr5A4.js +0 -1
- 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,
|
|
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.
|
|
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))
|
|
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
|
|
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
|
|
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})`;
|
|
1618
1750
|
});
|
|
1619
|
-
return
|
|
1751
|
+
return appendHiddenPrompt(
|
|
1620
1752
|
prompt,
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
-
|
|
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 (
|
|
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$
|
|
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$
|
|
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("
|
|
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(
|
|
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
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
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(
|
|
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
|
-
|
|
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 });
|