@yorz/cli 0.6.5 → 0.7.1
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 +815 -147
- package/dist/gui/assets/{abnfDiagram-VRR7QNED-Dysui3FU.js → abnfDiagram-VRR7QNED-Bfi5VmJM.js} +1 -1
- package/dist/gui/assets/{arc-Dva0_Fbz.js → arc-DW35HiMx.js} +1 -1
- package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-CSusN0wM.js → architectureDiagram-ZJ3FMSHR-D6ol2HjT.js} +1 -1
- package/dist/gui/assets/{blockDiagram-677ZJIJ3-Boy63uL4.js → blockDiagram-677ZJIJ3-DimsChzs.js} +1 -1
- package/dist/gui/assets/{c4Diagram-LMCZKHZV-DREOCesJ.js → c4Diagram-LMCZKHZV-aQtOPEhO.js} +1 -1
- package/dist/gui/assets/channel-CYD2fo3z.js +1 -0
- package/dist/gui/assets/{chunk-2Q5K7J3B-CQGkW5Le.js → chunk-2Q5K7J3B-Bg7XMADz.js} +1 -1
- package/dist/gui/assets/{chunk-32BRIVSS-ByKgaOB0.js → chunk-32BRIVSS-CJvj41Qg.js} +1 -1
- package/dist/gui/assets/{chunk-5VM5RSS4-ouTiIpFg.js → chunk-5VM5RSS4-B6iQkE6V.js} +1 -1
- package/dist/gui/assets/{chunk-EX3LRPZG-BBKKhIQU.js → chunk-EX3LRPZG-Bime3fML.js} +1 -1
- package/dist/gui/assets/{chunk-JWPE2WC7-i8qqnqZU.js → chunk-JWPE2WC7-B2d6pOPL.js} +1 -1
- package/dist/gui/assets/{chunk-MOJQB5TN-BmLvCJEW.js → chunk-MOJQB5TN-C2BtAAqD.js} +1 -1
- package/dist/gui/assets/{chunk-RYQCIY6F-DgIPwUtq.js → chunk-RYQCIY6F-DvOYjxsw.js} +1 -1
- package/dist/gui/assets/{chunk-V7JOEXUC-CHFlZVs7.js → chunk-V7JOEXUC-HRF7SSaJ.js} +1 -1
- package/dist/gui/assets/{chunk-VR4S4FIN-CF4gb-1V.js → chunk-VR4S4FIN-oMVpMTS0.js} +1 -1
- package/dist/gui/assets/{chunk-XXDRQBXY-6Ajr-TJy.js → chunk-XXDRQBXY-Gx6kiEHQ.js} +1 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-CFzbN5IF.js +1 -0
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-CFzbN5IF.js +1 -0
- package/dist/gui/assets/{cose-bilkent-JH36ORCC-Bq1NoHo9.js → cose-bilkent-JH36ORCC-MoHCpl37.js} +1 -1
- package/dist/gui/assets/{cynefin-VYW2F7L2-BLZ43yIC.js → cynefin-VYW2F7L2-2tnFSmNB.js} +1 -1
- package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DOG8J1W-.js → cynefinDiagram-TSTJHNR4-Cp7uDUjl.js} +1 -1
- package/dist/gui/assets/{dagre-VKFMJZFB-6s-zIAym.js → dagre-VKFMJZFB-CTNuVp99.js} +1 -1
- package/dist/gui/assets/{diagram-FQU43EPY-MdHGL1Ep.js → diagram-FQU43EPY-Z9nTO-2q.js} +1 -1
- package/dist/gui/assets/{diagram-G47NLZAW-vNqROz2G.js → diagram-G47NLZAW-D1H3xA3x.js} +1 -1
- package/dist/gui/assets/{diagram-NH7WQ7WH-mBHJAt54.js → diagram-NH7WQ7WH-BGpM31nf.js} +1 -1
- package/dist/gui/assets/{diagram-OA4YK3LP-Duzu45NT.js → diagram-OA4YK3LP-Bhn6yNbV.js} +1 -1
- package/dist/gui/assets/{diagram-WEI45ONY-B9KIE7jm.js → diagram-WEI45ONY-BDJ6eaig.js} +1 -1
- package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-D-VxYOb1.js → ebnfDiagram-CCIWWBDH-C8UHOTF4.js} +1 -1
- package/dist/gui/assets/{erDiagram-Q63AITRT-bGX1YWnp.js → erDiagram-Q63AITRT-BpoLARsO.js} +1 -1
- package/dist/gui/assets/{flowDiagram-23GEKE2U-BFbLxzKU.js → flowDiagram-23GEKE2U-DSODAemd.js} +1 -1
- package/dist/gui/assets/{ganttDiagram-NO4QXBWP-CFlioSP9.js → ganttDiagram-NO4QXBWP-Bv5JA_dU.js} +1 -1
- package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-9m210Kgm.js → gitGraphDiagram-IHSO6WYX-CW7ObzYy.js} +1 -1
- package/dist/gui/assets/index-CAnHTFea.js +38 -0
- package/dist/gui/assets/index-Cft6LQWD.css +1 -0
- package/dist/gui/assets/{infoDiagram-FWYZ7A6U-D35ODxoC.js → infoDiagram-FWYZ7A6U-uTM_03X9.js} +1 -1
- package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-B-mZqJl4.js → ishikawaDiagram-FXEZZL3T-BlbCXOf8.js} +1 -1
- package/dist/gui/assets/{journeyDiagram-5HDEW3XC-CmFLCu-y.js → journeyDiagram-5HDEW3XC-BwtfR_HU.js} +1 -1
- package/dist/gui/assets/{kanban-definition-HUTT4EX6-C-risdsz.js → kanban-definition-HUTT4EX6-BXwvYv3u.js} +1 -1
- package/dist/gui/assets/{linear-D5TY5eoS.js → linear-C5c8PyN7.js} +1 -1
- package/dist/gui/assets/{mermaid.core-wGjSg1h-.js → mermaid.core-Dbw2b69i.js} +4 -4
- package/dist/gui/assets/{mindmap-definition-LN4V7U3C-BSdCxt4H.js → mindmap-definition-LN4V7U3C-9P8fXZBY.js} +1 -1
- package/dist/gui/assets/{pegDiagram-2B236MQR-DrexK9bW.js → pegDiagram-2B236MQR-81kUPbkr.js} +1 -1
- package/dist/gui/assets/{pieDiagram-ENE6RG2P-ay6UKmgt.js → pieDiagram-ENE6RG2P-BI7iwgtu.js} +1 -1
- package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-DgoOnLZO.js → quadrantDiagram-ABIIQ3AL-CSxA_Epi.js} +1 -1
- package/dist/gui/assets/{railroadDiagram-RFXS5EU6-Dsg3x2_p.js → railroadDiagram-RFXS5EU6-D4DnN2WS.js} +1 -1
- package/dist/gui/assets/{requirementDiagram-TGXJPOKE-BtbCKvwF.js → requirementDiagram-TGXJPOKE-DjNONyxQ.js} +1 -1
- package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-TabZvNT3.js → sankeyDiagram-HTMAVEWB-Di4S5cUR.js} +1 -1
- package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C53WsGFo.js → sequenceDiagram-DBY2YBRQ-CLVwUzez.js} +1 -1
- package/dist/gui/assets/{sizeCapture-X5ZJPWSS-D_JRhNnb.js → sizeCapture-X5ZJPWSS-C5y0Ry_b.js} +1 -1
- package/dist/gui/assets/{stateDiagram-2N3HPSRC-D-_V4W7N.js → stateDiagram-2N3HPSRC-IdHsn6JH.js} +1 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CJBs-brH.js +1 -0
- package/dist/gui/assets/{swimlanes-5IMT3BWC-DgyOlILO.js → swimlanes-5IMT3BWC-bDCHEODt.js} +2 -2
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BpY4RNvM.js +8 -0
- package/dist/gui/assets/{timeline-definition-FHXFAJF6-7M8Rj4NA.js → timeline-definition-FHXFAJF6-B6f6vq0E.js} +1 -1
- package/dist/gui/assets/{vennDiagram-L72KCM5P-DVyooCr2.js → vennDiagram-L72KCM5P-AjYH8jaU.js} +1 -1
- package/dist/gui/assets/{wardleyDiagram-EHGQE667-CYry07bZ.js → wardleyDiagram-EHGQE667-NV3g14vB.js} +1 -1
- package/dist/gui/assets/{xychartDiagram-FW5EYKEG-Dvx8ojch.js → xychartDiagram-FW5EYKEG-D4yDWaI8.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,8 +1,8 @@
|
|
|
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,
|
|
5
|
-
import { join, isAbsolute, resolve, basename as basename$1, dirname, extname,
|
|
4
|
+
import { stat, mkdir, writeFile, rename, readFile, rm, access, readdir, appendFile, copyFile } from "node:fs/promises";
|
|
5
|
+
import { join, isAbsolute, resolve, basename as basename$1, dirname, extname, relative, sep, 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";
|
|
8
8
|
import { spawn, execFile, execFileSync } from "node:child_process";
|
|
@@ -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.1";
|
|
27
27
|
const type = "module";
|
|
28
28
|
const description = "";
|
|
29
29
|
const bin = { "yorz": "./dist/cli/index.js" };
|
|
@@ -54,6 +54,93 @@ const pkg = {
|
|
|
54
54
|
dependencies,
|
|
55
55
|
devDependencies
|
|
56
56
|
};
|
|
57
|
+
const HIDDEN_PROMPT_OPEN = "<!-- yorz:hidden -->";
|
|
58
|
+
const HIDDEN_PROMPT_CLOSE = "<!-- /yorz:hidden -->";
|
|
59
|
+
const SLASH_NAME_RE$1 = /^\/([\w-]+)(?:\s|$)/;
|
|
60
|
+
function matchCustomInstruction(prompt, instructions) {
|
|
61
|
+
const matched = SLASH_NAME_RE$1.exec(prompt.trim());
|
|
62
|
+
if (!matched) return null;
|
|
63
|
+
const name2 = matched[1];
|
|
64
|
+
return instructions.find((item) => item.name === name2) ?? null;
|
|
65
|
+
}
|
|
66
|
+
function wrapHiddenPrompt(hidden, original) {
|
|
67
|
+
const body = hidden.trim();
|
|
68
|
+
if (!body) return original;
|
|
69
|
+
return [HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE, original].join("\n");
|
|
70
|
+
}
|
|
71
|
+
function appendHiddenPrompt(original, hidden) {
|
|
72
|
+
const body = hidden.trim();
|
|
73
|
+
if (!body) return original;
|
|
74
|
+
return [original, "", HIDDEN_PROMPT_OPEN, body, HIDDEN_PROMPT_CLOSE].join("\n");
|
|
75
|
+
}
|
|
76
|
+
function normalizeCustomInstructions(value) {
|
|
77
|
+
if (!Array.isArray(value)) return [];
|
|
78
|
+
const out = [];
|
|
79
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const item of value) {
|
|
81
|
+
if (!item || typeof item !== "object") continue;
|
|
82
|
+
const obj = item;
|
|
83
|
+
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
84
|
+
const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
|
|
85
|
+
if (!id || !name2 || seen.has(id) || !/^[\w-]+$/.test(name2)) continue;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
out.push({
|
|
88
|
+
id,
|
|
89
|
+
name: name2,
|
|
90
|
+
description: typeof obj.description === "string" ? obj.description : "",
|
|
91
|
+
// Fall back to the pre-rename `systemPrompt` so existing configs keep
|
|
92
|
+
// working; the next save rewrites them under the new key.
|
|
93
|
+
hiddenPrompt: typeof obj.hiddenPrompt === "string" ? obj.hiddenPrompt : typeof obj.systemPrompt === "string" ? obj.systemPrompt : "",
|
|
94
|
+
prefill: typeof obj.prefill === "string" ? obj.prefill : "",
|
|
95
|
+
createdAt: typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function parseCustomInstructions(value) {
|
|
101
|
+
if (value === void 0) return [];
|
|
102
|
+
if (!Array.isArray(value)) return { error: "customInstructions must be an array" };
|
|
103
|
+
const out = [];
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
for (const [index, item] of value.entries()) {
|
|
106
|
+
if (!item || typeof item !== "object") {
|
|
107
|
+
return { error: `customInstructions.${index} must be an object` };
|
|
108
|
+
}
|
|
109
|
+
const obj = item;
|
|
110
|
+
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
111
|
+
const name2 = typeof obj.name === "string" ? obj.name.trim().replace(/^\/+/, "") : "";
|
|
112
|
+
if (!id) return { error: `customInstructions.${index}.id required` };
|
|
113
|
+
if (!name2 || !/^[\w-]+$/.test(name2)) {
|
|
114
|
+
return {
|
|
115
|
+
error: `customInstructions.${index}.name must use letters, numbers, underscores, or hyphens`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (seen.has(id)) return { error: `duplicate custom instruction id: ${id}` };
|
|
119
|
+
seen.add(id);
|
|
120
|
+
const description2 = obj.description;
|
|
121
|
+
const hiddenPrompt = obj.hiddenPrompt ?? obj.systemPrompt;
|
|
122
|
+
const prefill = obj.prefill;
|
|
123
|
+
const createdAt = obj.createdAt;
|
|
124
|
+
if (typeof description2 !== "string") {
|
|
125
|
+
return { error: `customInstructions.${index}.description must be a string` };
|
|
126
|
+
}
|
|
127
|
+
if (typeof hiddenPrompt !== "string") {
|
|
128
|
+
return { error: `customInstructions.${index}.hiddenPrompt must be a string` };
|
|
129
|
+
}
|
|
130
|
+
if (typeof prefill !== "string") {
|
|
131
|
+
return { error: `customInstructions.${index}.prefill must be a string` };
|
|
132
|
+
}
|
|
133
|
+
if (typeof createdAt !== "number" || createdAt <= 0) {
|
|
134
|
+
return { error: `customInstructions.${index}.createdAt must be a positive number` };
|
|
135
|
+
}
|
|
136
|
+
out.push({ id, name: name2, description: description2, hiddenPrompt, prefill, createdAt });
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function mergeCustomInstructions(project, global) {
|
|
141
|
+
const names = new Set(project.map((item) => item.name));
|
|
142
|
+
return [...project, ...global.filter((item) => !names.has(item.name))];
|
|
143
|
+
}
|
|
57
144
|
const CURRENT_VERSION$1 = 1;
|
|
58
145
|
const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
|
|
59
146
|
const DEFAULT_NOTIFICATIONS = {
|
|
@@ -62,6 +149,11 @@ const DEFAULT_NOTIFICATIONS = {
|
|
|
62
149
|
const DEFAULT_POWER = {
|
|
63
150
|
inhibitWhenRunning: "system-default"
|
|
64
151
|
};
|
|
152
|
+
const DEFAULT_APPEARANCE = {
|
|
153
|
+
themeMode: "system",
|
|
154
|
+
themeName: "terminal",
|
|
155
|
+
language: "zh-CN"
|
|
156
|
+
};
|
|
65
157
|
const SHORTCUT_ACTION_IDS = [
|
|
66
158
|
"newSpec",
|
|
67
159
|
"toggleSpecDetailFullscreen",
|
|
@@ -74,6 +166,9 @@ function resolveGlobalConfigDir(env = process.env) {
|
|
|
74
166
|
return join(base, "yorz");
|
|
75
167
|
}
|
|
76
168
|
function resolveGlobalConfigPath(env = process.env) {
|
|
169
|
+
return join(resolveGlobalConfigDir(env), "config.json");
|
|
170
|
+
}
|
|
171
|
+
function resolveLegacyGlobalProjectsPath(env = process.env) {
|
|
77
172
|
return join(resolveGlobalConfigDir(env), "projects.json");
|
|
78
173
|
}
|
|
79
174
|
function resolveGlobalSkillsDir(env = process.env) {
|
|
@@ -84,7 +179,14 @@ function resolveSkillEntry(skillName, env = process.env) {
|
|
|
84
179
|
}
|
|
85
180
|
async function loadGlobalConfig(filePath) {
|
|
86
181
|
const fp = filePath ?? resolveGlobalConfigPath();
|
|
87
|
-
if (!existsSync(fp))
|
|
182
|
+
if (!existsSync(fp)) {
|
|
183
|
+
const legacy = filePath ? basename$1(fp) === "config.json" ? join(dirname(fp), "projects.json") : "" : resolveLegacyGlobalProjectsPath();
|
|
184
|
+
if (legacy && existsSync(legacy)) return loadGlobalConfigFromPath(legacy);
|
|
185
|
+
return defaultGlobalConfig();
|
|
186
|
+
}
|
|
187
|
+
return loadGlobalConfigFromPath(fp);
|
|
188
|
+
}
|
|
189
|
+
async function loadGlobalConfigFromPath(fp) {
|
|
88
190
|
let raw;
|
|
89
191
|
try {
|
|
90
192
|
raw = await readFile(fp, "utf8");
|
|
@@ -134,7 +236,9 @@ function normalizeConfig$1(value) {
|
|
|
134
236
|
agent: normalizeAgent$1(obj.agent),
|
|
135
237
|
notifications: normalizeNotifications(obj.notifications),
|
|
136
238
|
shortcuts: normalizeShortcuts(obj.shortcuts),
|
|
137
|
-
power: normalizePower(obj.power)
|
|
239
|
+
power: normalizePower(obj.power),
|
|
240
|
+
appearance: normalizeAppearance(obj.appearance),
|
|
241
|
+
customInstructions: normalizeCustomInstructions(obj.customInstructions)
|
|
138
242
|
};
|
|
139
243
|
}
|
|
140
244
|
function defaultGlobalConfig() {
|
|
@@ -151,7 +255,9 @@ function defaultGlobalConfig() {
|
|
|
151
255
|
shortcuts: {},
|
|
152
256
|
power: {
|
|
153
257
|
inhibitWhenRunning: DEFAULT_POWER.inhibitWhenRunning
|
|
154
|
-
}
|
|
258
|
+
},
|
|
259
|
+
appearance: { ...DEFAULT_APPEARANCE },
|
|
260
|
+
customInstructions: []
|
|
155
261
|
};
|
|
156
262
|
}
|
|
157
263
|
function normalizeAgent$1(value) {
|
|
@@ -202,6 +308,27 @@ function normalizePower(value) {
|
|
|
202
308
|
}
|
|
203
309
|
return { inhibitWhenRunning: DEFAULT_POWER.inhibitWhenRunning };
|
|
204
310
|
}
|
|
311
|
+
function normalizeAppearance(value) {
|
|
312
|
+
if (!value || typeof value !== "object") return { ...DEFAULT_APPEARANCE };
|
|
313
|
+
const obj = value;
|
|
314
|
+
const themeMode = obj.themeMode;
|
|
315
|
+
const themeName = obj.themeName;
|
|
316
|
+
const language = obj.language;
|
|
317
|
+
return {
|
|
318
|
+
themeMode: isThemeMode(themeMode) ? themeMode : DEFAULT_APPEARANCE.themeMode,
|
|
319
|
+
themeName: isThemeName(themeName) ? themeName : DEFAULT_APPEARANCE.themeName,
|
|
320
|
+
language: isLanguage(language) ? language : DEFAULT_APPEARANCE.language
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function isThemeMode(value) {
|
|
324
|
+
return value === "system" || value === "light" || value === "dark";
|
|
325
|
+
}
|
|
326
|
+
function isThemeName(value) {
|
|
327
|
+
return value === "terminal" || value === "graphite" || value === "paper";
|
|
328
|
+
}
|
|
329
|
+
function isLanguage(value) {
|
|
330
|
+
return value === "zh-CN" || value === "en";
|
|
331
|
+
}
|
|
205
332
|
function normalizeShortcutBinding(value) {
|
|
206
333
|
const parts = value.split("+").map((part) => part.trim()).filter(Boolean);
|
|
207
334
|
let key = "";
|
|
@@ -1547,18 +1674,18 @@ function isYorzDebugCommand(prompt) {
|
|
|
1547
1674
|
return /^\/yorz-debug(?:\s|$)/.test(prompt.trim());
|
|
1548
1675
|
}
|
|
1549
1676
|
function buildChatDebugPrompt(prompt, now = /* @__PURE__ */ new Date()) {
|
|
1550
|
-
const
|
|
1677
|
+
const original = prompt.trim();
|
|
1678
|
+
const body = original.replace(/^\/yorz-debug(?:\s+|$)/, "").trim();
|
|
1551
1679
|
const debugPath = `${CHAT_DEBUG_DIR_REL}/debug-${formatDebugTimestamp(now)}.md`;
|
|
1552
|
-
const
|
|
1553
|
-
return [
|
|
1680
|
+
const guide = [
|
|
1554
1681
|
`${skillRef("yorz-debug")},然后进入 Debug 模式。`,
|
|
1555
1682
|
`本次是普通 chat 独立触发,没有 spec_dir。Debug 活文档必须写入临时文件 \`${debugPath}\`。`,
|
|
1556
1683
|
`写入前请确保目录 \`${CHAT_DEBUG_DIR_REL}/\` 存在;该目录属于临时目录,会由 YorZ 定时清理。`,
|
|
1557
1684
|
`如果该文件不存在则创建;本文件只承载本次 chat debug 记录,不需要追加复用其他文件。`,
|
|
1558
1685
|
"",
|
|
1559
|
-
"
|
|
1560
|
-
bug
|
|
1686
|
+
body ? "待调试问题见下方用户输入(忽略其中的 `/yorz-debug` 指令前缀):" : "请先根据对话上下文确认待调试问题;若上下文不足,请向用户补齐复现信息。"
|
|
1561
1687
|
].join("\n");
|
|
1688
|
+
return wrapHiddenPrompt(guide, original);
|
|
1562
1689
|
}
|
|
1563
1690
|
async function cleanupExpiredChatDebugFiles(projectPath, opts = {}) {
|
|
1564
1691
|
const root = join(projectPath, CHAT_DEBUG_DIR_REL);
|
|
@@ -1607,6 +1734,169 @@ function formatDebugTimestamp(date) {
|
|
|
1607
1734
|
pad(date.getSeconds())
|
|
1608
1735
|
].join("");
|
|
1609
1736
|
}
|
|
1737
|
+
const CURRENT_VERSION = 1;
|
|
1738
|
+
const DEFAULT_SPECS_DIR = ".yorz/specs";
|
|
1739
|
+
function defaultProjectConfig() {
|
|
1740
|
+
return {
|
|
1741
|
+
version: CURRENT_VERSION,
|
|
1742
|
+
agent: { kind: "inherit" },
|
|
1743
|
+
specsDir: DEFAULT_SPECS_DIR,
|
|
1744
|
+
commands: [],
|
|
1745
|
+
customInstructions: []
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function configPath(cwd) {
|
|
1749
|
+
return join(cwd, ".yorz", "config.json");
|
|
1750
|
+
}
|
|
1751
|
+
async function loadProjectConfig(cwd) {
|
|
1752
|
+
const fp = configPath(cwd);
|
|
1753
|
+
if (!existsSync(fp)) return defaultProjectConfig();
|
|
1754
|
+
let raw;
|
|
1755
|
+
try {
|
|
1756
|
+
raw = await readFile(fp, "utf8");
|
|
1757
|
+
} catch {
|
|
1758
|
+
return defaultProjectConfig();
|
|
1759
|
+
}
|
|
1760
|
+
if (!raw.trim()) return defaultProjectConfig();
|
|
1761
|
+
let parsed;
|
|
1762
|
+
try {
|
|
1763
|
+
parsed = JSON.parse(raw);
|
|
1764
|
+
} catch {
|
|
1765
|
+
return defaultProjectConfig();
|
|
1766
|
+
}
|
|
1767
|
+
return normalizeConfig(parsed);
|
|
1768
|
+
}
|
|
1769
|
+
async function saveProjectConfig(cwd, cfg) {
|
|
1770
|
+
const fp = configPath(cwd);
|
|
1771
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
1772
|
+
const normalized = normalizeConfig(cfg);
|
|
1773
|
+
const body = `${JSON.stringify(normalized, null, 2)}
|
|
1774
|
+
`;
|
|
1775
|
+
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
1776
|
+
await writeFile(tmp, body, "utf8");
|
|
1777
|
+
await rename(tmp, fp);
|
|
1778
|
+
}
|
|
1779
|
+
function resolveSpecsDir(cwd, cfg) {
|
|
1780
|
+
const root = resolve(cwd);
|
|
1781
|
+
const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
|
|
1782
|
+
const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
|
|
1783
|
+
const rel = relative(root, abs);
|
|
1784
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
1785
|
+
throw new Error(`specsDir must stay within the project root: ${raw}`);
|
|
1786
|
+
}
|
|
1787
|
+
return abs;
|
|
1788
|
+
}
|
|
1789
|
+
async function ensureSpecsDirExists(absSpecsDir) {
|
|
1790
|
+
await mkdir(absSpecsDir, { recursive: true });
|
|
1791
|
+
}
|
|
1792
|
+
function normalizeConfig(value) {
|
|
1793
|
+
if (!value || typeof value !== "object") return defaultProjectConfig();
|
|
1794
|
+
const obj = value;
|
|
1795
|
+
const agent = normalizeAgent(obj.agent);
|
|
1796
|
+
const specsDir = normalizeSpecsDir(obj.specsDir);
|
|
1797
|
+
const commands = normalizeCommands(obj.commands);
|
|
1798
|
+
const customInstructions = normalizeCustomInstructions(obj.customInstructions);
|
|
1799
|
+
return { version: CURRENT_VERSION, agent, specsDir, commands, customInstructions };
|
|
1800
|
+
}
|
|
1801
|
+
function normalizeCommands(value) {
|
|
1802
|
+
if (!Array.isArray(value)) return [];
|
|
1803
|
+
const out = [];
|
|
1804
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1805
|
+
for (const item of value) {
|
|
1806
|
+
if (!item || typeof item !== "object") continue;
|
|
1807
|
+
const obj = item;
|
|
1808
|
+
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
1809
|
+
const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
|
|
1810
|
+
const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
|
|
1811
|
+
if (!id || !name2 || !cli || seen.has(id)) continue;
|
|
1812
|
+
seen.add(id);
|
|
1813
|
+
const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
|
|
1814
|
+
out.push({ id, name: name2, cli, createdAt });
|
|
1815
|
+
}
|
|
1816
|
+
return out;
|
|
1817
|
+
}
|
|
1818
|
+
function normalizeAgent(value) {
|
|
1819
|
+
if (typeof value === "string") {
|
|
1820
|
+
if (value === "opencode") return { kind: "opencode" };
|
|
1821
|
+
if (value === "codex") return { kind: "codex" };
|
|
1822
|
+
return { kind: "claude" };
|
|
1823
|
+
}
|
|
1824
|
+
if (!value || typeof value !== "object") return { kind: "claude" };
|
|
1825
|
+
const obj = value;
|
|
1826
|
+
const kind = obj.kind;
|
|
1827
|
+
if (kind === "inherit") return { kind: "inherit" };
|
|
1828
|
+
if (kind === "opencode") return { kind: "opencode" };
|
|
1829
|
+
if (kind === "codex") return { kind: "codex" };
|
|
1830
|
+
if (kind === "custom") {
|
|
1831
|
+
const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
|
|
1832
|
+
if (!cmd) return { kind: "claude" };
|
|
1833
|
+
const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
|
|
1834
|
+
return { kind: "custom", cmd, args };
|
|
1835
|
+
}
|
|
1836
|
+
return { kind: "claude" };
|
|
1837
|
+
}
|
|
1838
|
+
function normalizeSpecsDir(value) {
|
|
1839
|
+
if (typeof value !== "string") return DEFAULT_SPECS_DIR;
|
|
1840
|
+
const trimmed = value.trim();
|
|
1841
|
+
if (!trimmed) return DEFAULT_SPECS_DIR;
|
|
1842
|
+
return trimmed;
|
|
1843
|
+
}
|
|
1844
|
+
const SLASH_NAME_RE = /^\/([\w-]+)(?:\s|$)/;
|
|
1845
|
+
const SPEC_COMMAND_RE = /^\/yorz-spec(?:\s|$)/;
|
|
1846
|
+
function isSlashCommand(prompt) {
|
|
1847
|
+
return SLASH_NAME_RE.test(prompt.trim());
|
|
1848
|
+
}
|
|
1849
|
+
function buildChatSpecPrompt(prompt, specsDirRelative) {
|
|
1850
|
+
const original = prompt.trim();
|
|
1851
|
+
const body = original.replace(SPEC_COMMAND_RE, "").trim();
|
|
1852
|
+
const guide = [
|
|
1853
|
+
`${skillRef("yorz-spec")},然后按其自动模式判定推进。`,
|
|
1854
|
+
`本次是普通 chat 独立触发,未指定 spec_path:若当前会话上下文中已出现过 spec 文档,继续推进该 spec;否则按 skill 的「新建 spec」流程创建后立即推进 plan 阶段。`,
|
|
1855
|
+
`spec 目录为 \`${specsDirRelative}/\`。`,
|
|
1856
|
+
"",
|
|
1857
|
+
body ? "本次需求见下方用户输入(忽略其中的 `/yorz-spec` 指令前缀):" : "请先根据对话上下文确认本次要处理的 spec 或需求;若上下文不足,请向用户补齐后再推进。"
|
|
1858
|
+
].join("\n");
|
|
1859
|
+
return wrapHiddenPrompt(guide, original);
|
|
1860
|
+
}
|
|
1861
|
+
function buildUnknownCommandPrompt(prompt, hit) {
|
|
1862
|
+
const original = prompt.trim();
|
|
1863
|
+
const lines = [
|
|
1864
|
+
"下方用户输入以 `/` 开头,这是 YorZ 输入框的指令语法,**不是**你自身的 slash command,请勿按命令解析、也不要回复 `Unknown command`。"
|
|
1865
|
+
];
|
|
1866
|
+
if (hit) {
|
|
1867
|
+
lines.push(
|
|
1868
|
+
`\`/${hit.name}\` 是用户在 YorZ 中配置的自定义指令,但未配置隐藏提示词${hit.description ? `;其说明为:${hit.description}` : ""}。请结合该指令名与下方正文理解用户意图。`
|
|
1869
|
+
);
|
|
1870
|
+
} else {
|
|
1871
|
+
lines.push("该指令未在 YorZ 中配置,请忽略指令前缀,按普通文本理解下方正文。");
|
|
1872
|
+
}
|
|
1873
|
+
return wrapHiddenPrompt(lines.join("\n"), original);
|
|
1874
|
+
}
|
|
1875
|
+
function resolveChatPrompt(prompt, instructions, opts = {}) {
|
|
1876
|
+
const original = prompt.trim();
|
|
1877
|
+
if (!isSlashCommand(original)) return { prompt, builtin: null };
|
|
1878
|
+
if (isYorzDebugCommand(original)) {
|
|
1879
|
+
return { prompt: buildChatDebugPrompt(original, opts.now), builtin: "yorz-debug" };
|
|
1880
|
+
}
|
|
1881
|
+
if (SPEC_COMMAND_RE.test(original)) {
|
|
1882
|
+
return {
|
|
1883
|
+
prompt: buildChatSpecPrompt(original, opts.specsDirRelative ?? ".yorz/specs"),
|
|
1884
|
+
builtin: "yorz-spec"
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
const hit = matchCustomInstruction(original, instructions);
|
|
1888
|
+
if (hit?.hiddenPrompt.trim()) {
|
|
1889
|
+
return { prompt: wrapHiddenPrompt(hit.hiddenPrompt, original), builtin: null };
|
|
1890
|
+
}
|
|
1891
|
+
return { prompt: buildUnknownCommandPrompt(original, hit), builtin: null };
|
|
1892
|
+
}
|
|
1893
|
+
async function loadCustomInstructions(projectPath) {
|
|
1894
|
+
const [project, global] = await Promise.all([
|
|
1895
|
+
loadProjectConfig(projectPath).then((cfg) => cfg.customInstructions).catch(() => []),
|
|
1896
|
+
loadGlobalConfig().then((cfg) => cfg.customInstructions).catch(() => [])
|
|
1897
|
+
]);
|
|
1898
|
+
return mergeCustomInstructions(project, global);
|
|
1899
|
+
}
|
|
1610
1900
|
const KINDS = ["claude", "codex", "opencode"];
|
|
1611
1901
|
const DRAFT_ID_RE = /^[a-zA-Z0-9-]{1,64}$/;
|
|
1612
1902
|
function buildChatPrompt(prompt, draftId, attachments) {
|
|
@@ -1616,13 +1906,14 @@ function buildChatPrompt(prompt, draftId, attachments) {
|
|
|
1616
1906
|
const rel = `${dir}/${a.storedName}`;
|
|
1617
1907
|
return a.kind === "image" ? `- ` : `- [${a.name}](${rel})`;
|
|
1618
1908
|
});
|
|
1619
|
-
return
|
|
1909
|
+
return appendHiddenPrompt(
|
|
1620
1910
|
prompt,
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1911
|
+
[
|
|
1912
|
+
"---",
|
|
1913
|
+
`本次消息附带 ${attachments.length} 个附件,已保存在临时目录 \`${dir}/\`(请按需用文件工具直接读取,无需迁移):`,
|
|
1914
|
+
...lines
|
|
1915
|
+
].join("\n")
|
|
1916
|
+
);
|
|
1626
1917
|
}
|
|
1627
1918
|
function createSessionsRoutes(resolveProject) {
|
|
1628
1919
|
const app = new Hono();
|
|
@@ -1638,6 +1929,11 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1638
1929
|
if (p instanceof Response) return p;
|
|
1639
1930
|
return c.json(await p.sessions.listSessions());
|
|
1640
1931
|
});
|
|
1932
|
+
app.get("/projects/:projectId/agent-usage", async (c) => {
|
|
1933
|
+
const p = await need(c);
|
|
1934
|
+
if (p instanceof Response) return p;
|
|
1935
|
+
return c.json(await p.sessions.getUsageStatus());
|
|
1936
|
+
});
|
|
1641
1937
|
app.post("/projects/:projectId/sessions", async (c) => {
|
|
1642
1938
|
const p = await need(c);
|
|
1643
1939
|
if (p instanceof Response) return p;
|
|
@@ -1689,7 +1985,11 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1689
1985
|
}
|
|
1690
1986
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1691
1987
|
if (!prompt) return c.json({ error: "prompt required" }, 400);
|
|
1692
|
-
|
|
1988
|
+
const instructions = isSlashCommand(prompt) ? await loadCustomInstructions(p.path) : [];
|
|
1989
|
+
const resolved = resolveChatPrompt(prompt, instructions, {
|
|
1990
|
+
specsDirRelative: p.specsDirRelative
|
|
1991
|
+
});
|
|
1992
|
+
let finalPrompt = resolved.prompt;
|
|
1693
1993
|
if (body.draftId !== void 0) {
|
|
1694
1994
|
if (typeof body.draftId !== "string" || !DRAFT_ID_RE.test(body.draftId)) {
|
|
1695
1995
|
return c.json({ error: "draftId has invalid format" }, 400);
|
|
@@ -1700,9 +2000,9 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1700
2000
|
finalPrompt = buildChatPrompt(finalPrompt, draftId, metas);
|
|
1701
2001
|
}
|
|
1702
2002
|
}
|
|
1703
|
-
if (
|
|
2003
|
+
if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
|
|
1704
2004
|
});
|
|
1705
|
-
const handle = await p.sessions.send(c.req.param("sid"), finalPrompt);
|
|
2005
|
+
const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt);
|
|
1706
2006
|
return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
|
|
1707
2007
|
});
|
|
1708
2008
|
app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
|
|
@@ -1713,13 +2013,13 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1713
2013
|
});
|
|
1714
2014
|
return app;
|
|
1715
2015
|
}
|
|
1716
|
-
const execFileP$
|
|
2016
|
+
const execFileP$2 = promisify(execFile);
|
|
1717
2017
|
function withHiddenWindowsConsole(options, platform2 = process.platform) {
|
|
1718
2018
|
if (platform2 !== "win32") return options;
|
|
1719
2019
|
return { ...options, windowsHide: true };
|
|
1720
2020
|
}
|
|
1721
2021
|
async function execFileWithoutWindow(file, args, options) {
|
|
1722
|
-
const result = await execFileP$
|
|
2022
|
+
const result = await execFileP$2(file, args, withHiddenWindowsConsole(options));
|
|
1723
2023
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
1724
2024
|
}
|
|
1725
2025
|
function spawnWithoutWindow(command, args, options) {
|
|
@@ -1876,7 +2176,7 @@ async function commit(cwd, opts) {
|
|
|
1876
2176
|
const extraPaths = renamed.map((r) => r.renamedFrom);
|
|
1877
2177
|
const allPaths = [...opts.paths, ...extraPaths];
|
|
1878
2178
|
await runGit(cwd, ["add", "--", ...allPaths]);
|
|
1879
|
-
await runGit(cwd, ["commit", "-m", message
|
|
2179
|
+
await runGit(cwd, ["commit", "-m", message]);
|
|
1880
2180
|
const { stdout } = await runGit(cwd, ["rev-parse", "HEAD"]);
|
|
1881
2181
|
return { commit: stdout.trim() };
|
|
1882
2182
|
}
|
|
@@ -2451,111 +2751,6 @@ function createProjectRoutes(registry, worktreeManager) {
|
|
|
2451
2751
|
});
|
|
2452
2752
|
return app;
|
|
2453
2753
|
}
|
|
2454
|
-
const CURRENT_VERSION = 1;
|
|
2455
|
-
const DEFAULT_SPECS_DIR = ".yorz/specs";
|
|
2456
|
-
function defaultProjectConfig() {
|
|
2457
|
-
return {
|
|
2458
|
-
version: CURRENT_VERSION,
|
|
2459
|
-
agent: { kind: "inherit" },
|
|
2460
|
-
specsDir: DEFAULT_SPECS_DIR,
|
|
2461
|
-
commands: []
|
|
2462
|
-
};
|
|
2463
|
-
}
|
|
2464
|
-
function configPath(cwd) {
|
|
2465
|
-
return join(cwd, ".yorz", "config.json");
|
|
2466
|
-
}
|
|
2467
|
-
async function loadProjectConfig(cwd) {
|
|
2468
|
-
const fp = configPath(cwd);
|
|
2469
|
-
if (!existsSync(fp)) return defaultProjectConfig();
|
|
2470
|
-
let raw;
|
|
2471
|
-
try {
|
|
2472
|
-
raw = await readFile(fp, "utf8");
|
|
2473
|
-
} catch {
|
|
2474
|
-
return defaultProjectConfig();
|
|
2475
|
-
}
|
|
2476
|
-
if (!raw.trim()) return defaultProjectConfig();
|
|
2477
|
-
let parsed;
|
|
2478
|
-
try {
|
|
2479
|
-
parsed = JSON.parse(raw);
|
|
2480
|
-
} catch {
|
|
2481
|
-
return defaultProjectConfig();
|
|
2482
|
-
}
|
|
2483
|
-
return normalizeConfig(parsed);
|
|
2484
|
-
}
|
|
2485
|
-
async function saveProjectConfig(cwd, cfg) {
|
|
2486
|
-
const fp = configPath(cwd);
|
|
2487
|
-
await mkdir(dirname(fp), { recursive: true });
|
|
2488
|
-
const normalized = normalizeConfig(cfg);
|
|
2489
|
-
const body = `${JSON.stringify(normalized, null, 2)}
|
|
2490
|
-
`;
|
|
2491
|
-
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
2492
|
-
await writeFile(tmp, body, "utf8");
|
|
2493
|
-
await rename(tmp, fp);
|
|
2494
|
-
}
|
|
2495
|
-
function resolveSpecsDir(cwd, cfg) {
|
|
2496
|
-
const root = resolve(cwd);
|
|
2497
|
-
const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
|
|
2498
|
-
const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
|
|
2499
|
-
const rel = relative(root, abs);
|
|
2500
|
-
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
2501
|
-
throw new Error(`specsDir must stay within the project root: ${raw}`);
|
|
2502
|
-
}
|
|
2503
|
-
return abs;
|
|
2504
|
-
}
|
|
2505
|
-
async function ensureSpecsDirExists(absSpecsDir) {
|
|
2506
|
-
await mkdir(absSpecsDir, { recursive: true });
|
|
2507
|
-
}
|
|
2508
|
-
function normalizeConfig(value) {
|
|
2509
|
-
if (!value || typeof value !== "object") return defaultProjectConfig();
|
|
2510
|
-
const obj = value;
|
|
2511
|
-
const agent = normalizeAgent(obj.agent);
|
|
2512
|
-
const specsDir = normalizeSpecsDir(obj.specsDir);
|
|
2513
|
-
const commands = normalizeCommands(obj.commands);
|
|
2514
|
-
return { version: CURRENT_VERSION, agent, specsDir, commands };
|
|
2515
|
-
}
|
|
2516
|
-
function normalizeCommands(value) {
|
|
2517
|
-
if (!Array.isArray(value)) return [];
|
|
2518
|
-
const out = [];
|
|
2519
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2520
|
-
for (const item of value) {
|
|
2521
|
-
if (!item || typeof item !== "object") continue;
|
|
2522
|
-
const obj = item;
|
|
2523
|
-
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
2524
|
-
const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
|
|
2525
|
-
const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
|
|
2526
|
-
if (!id || !name2 || !cli || seen.has(id)) continue;
|
|
2527
|
-
seen.add(id);
|
|
2528
|
-
const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
|
|
2529
|
-
out.push({ id, name: name2, cli, createdAt });
|
|
2530
|
-
}
|
|
2531
|
-
return out;
|
|
2532
|
-
}
|
|
2533
|
-
function normalizeAgent(value) {
|
|
2534
|
-
if (typeof value === "string") {
|
|
2535
|
-
if (value === "opencode") return { kind: "opencode" };
|
|
2536
|
-
if (value === "codex") return { kind: "codex" };
|
|
2537
|
-
return { kind: "claude" };
|
|
2538
|
-
}
|
|
2539
|
-
if (!value || typeof value !== "object") return { kind: "claude" };
|
|
2540
|
-
const obj = value;
|
|
2541
|
-
const kind = obj.kind;
|
|
2542
|
-
if (kind === "inherit") return { kind: "inherit" };
|
|
2543
|
-
if (kind === "opencode") return { kind: "opencode" };
|
|
2544
|
-
if (kind === "codex") return { kind: "codex" };
|
|
2545
|
-
if (kind === "custom") {
|
|
2546
|
-
const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
|
|
2547
|
-
if (!cmd) return { kind: "claude" };
|
|
2548
|
-
const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
|
|
2549
|
-
return { kind: "custom", cmd, args };
|
|
2550
|
-
}
|
|
2551
|
-
return { kind: "claude" };
|
|
2552
|
-
}
|
|
2553
|
-
function normalizeSpecsDir(value) {
|
|
2554
|
-
if (typeof value !== "string") return DEFAULT_SPECS_DIR;
|
|
2555
|
-
const trimmed = value.trim();
|
|
2556
|
-
if (!trimmed) return DEFAULT_SPECS_DIR;
|
|
2557
|
-
return trimmed;
|
|
2558
|
-
}
|
|
2559
2754
|
function createProjectConfigRoutes(registry) {
|
|
2560
2755
|
const app = new Hono();
|
|
2561
2756
|
app.get("/projects/:projectId/config", async (c) => {
|
|
@@ -2582,7 +2777,8 @@ function createProjectConfigRoutes(registry) {
|
|
|
2582
2777
|
version: 1,
|
|
2583
2778
|
agent: parsed.agent,
|
|
2584
2779
|
specsDir: parsed.specsDir,
|
|
2585
|
-
commands: current.commands
|
|
2780
|
+
commands: current.commands,
|
|
2781
|
+
customInstructions: current.customInstructions
|
|
2586
2782
|
};
|
|
2587
2783
|
let absSpecsDir;
|
|
2588
2784
|
try {
|
|
@@ -2603,6 +2799,34 @@ function createProjectConfigRoutes(registry) {
|
|
|
2603
2799
|
await registry.reload(id);
|
|
2604
2800
|
return c.json({ ok: true, config: cfg });
|
|
2605
2801
|
});
|
|
2802
|
+
app.get("/projects/:projectId/custom-instructions", async (c) => {
|
|
2803
|
+
const id = c.req.param("projectId");
|
|
2804
|
+
const entry = await registry.findEntry(id);
|
|
2805
|
+
if (!entry) return c.json({ error: "project not found" }, 404);
|
|
2806
|
+
const cfg = await loadProjectConfig(entry.path);
|
|
2807
|
+
return c.json({ customInstructions: cfg.customInstructions });
|
|
2808
|
+
});
|
|
2809
|
+
app.put("/projects/:projectId/custom-instructions", async (c) => {
|
|
2810
|
+
const id = c.req.param("projectId");
|
|
2811
|
+
const entry = await registry.findEntry(id);
|
|
2812
|
+
if (!entry) return c.json({ error: "project not found" }, 404);
|
|
2813
|
+
let raw;
|
|
2814
|
+
try {
|
|
2815
|
+
raw = await c.req.json();
|
|
2816
|
+
} catch {
|
|
2817
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
2818
|
+
}
|
|
2819
|
+
if (!raw || typeof raw !== "object") return c.json({ error: "body must be an object" }, 400);
|
|
2820
|
+
const parsed = parseCustomInstructions(raw.customInstructions);
|
|
2821
|
+
if ("error" in parsed) return c.json({ error: parsed.error }, 400);
|
|
2822
|
+
const current = await loadProjectConfig(entry.path);
|
|
2823
|
+
try {
|
|
2824
|
+
await saveProjectConfig(entry.path, { ...current, customInstructions: parsed });
|
|
2825
|
+
} catch (err) {
|
|
2826
|
+
return c.json({ error: err.message }, 500);
|
|
2827
|
+
}
|
|
2828
|
+
return c.json({ ok: true, customInstructions: parsed });
|
|
2829
|
+
});
|
|
2606
2830
|
return app;
|
|
2607
2831
|
}
|
|
2608
2832
|
function parseBody$1(value) {
|
|
@@ -2802,7 +3026,9 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
|
|
|
2802
3026
|
agent: cfg.agent,
|
|
2803
3027
|
notifications: cfg.notifications,
|
|
2804
3028
|
shortcuts: cfg.shortcuts,
|
|
2805
|
-
power: cfg.power
|
|
3029
|
+
power: cfg.power,
|
|
3030
|
+
appearance: cfg.appearance,
|
|
3031
|
+
customInstructions: cfg.customInstructions
|
|
2806
3032
|
});
|
|
2807
3033
|
});
|
|
2808
3034
|
app.put("/global-config", async (c) => {
|
|
@@ -2819,6 +3045,8 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
|
|
|
2819
3045
|
cfg.notifications = parsed.notifications;
|
|
2820
3046
|
cfg.shortcuts = parsed.shortcuts;
|
|
2821
3047
|
cfg.power = parsed.power;
|
|
3048
|
+
cfg.appearance = parsed.appearance;
|
|
3049
|
+
cfg.customInstructions = parsed.customInstructions;
|
|
2822
3050
|
await saveGlobalConfig(cfg, globalConfigPath);
|
|
2823
3051
|
await powerController.refresh();
|
|
2824
3052
|
return c.json({
|
|
@@ -2827,7 +3055,9 @@ function createGlobalConfigRoutes(globalConfigPath, powerController = getPowerIn
|
|
|
2827
3055
|
agent: cfg.agent,
|
|
2828
3056
|
notifications: cfg.notifications,
|
|
2829
3057
|
shortcuts: cfg.shortcuts,
|
|
2830
|
-
power: cfg.power
|
|
3058
|
+
power: cfg.power,
|
|
3059
|
+
appearance: cfg.appearance,
|
|
3060
|
+
customInstructions: cfg.customInstructions
|
|
2831
3061
|
}
|
|
2832
3062
|
});
|
|
2833
3063
|
});
|
|
@@ -2887,13 +3117,33 @@ function parseBody(value) {
|
|
|
2887
3117
|
error: "power.inhibitWhenRunning must be system-default | prevent-display-sleep | keep-system-awake"
|
|
2888
3118
|
};
|
|
2889
3119
|
}
|
|
3120
|
+
const appearance = parseAppearance(obj.appearance ?? DEFAULT_APPEARANCE);
|
|
3121
|
+
if ("error" in appearance) return appearance;
|
|
3122
|
+
const customInstructions = parseCustomInstructions(obj.customInstructions);
|
|
3123
|
+
if ("error" in customInstructions) return customInstructions;
|
|
2890
3124
|
return {
|
|
2891
3125
|
agent: { defaultKind },
|
|
2892
3126
|
notifications: { sessionEnd: { banner: s.banner, sound: s.sound } },
|
|
2893
3127
|
shortcuts: normalizeShortcuts(shortcutObj),
|
|
2894
|
-
power: { inhibitWhenRunning }
|
|
3128
|
+
power: { inhibitWhenRunning },
|
|
3129
|
+
appearance,
|
|
3130
|
+
customInstructions
|
|
2895
3131
|
};
|
|
2896
3132
|
}
|
|
3133
|
+
function parseAppearance(value) {
|
|
3134
|
+
if (!value || typeof value !== "object") return { error: "appearance must be an object" };
|
|
3135
|
+
const obj = value;
|
|
3136
|
+
const themeMode = obj.themeMode;
|
|
3137
|
+
if (!isThemeMode(themeMode))
|
|
3138
|
+
return { error: "appearance.themeMode must be system | light | dark" };
|
|
3139
|
+
const themeName = obj.themeName;
|
|
3140
|
+
if (!isThemeName(themeName)) {
|
|
3141
|
+
return { error: "appearance.themeName must be terminal | graphite | paper" };
|
|
3142
|
+
}
|
|
3143
|
+
const language = obj.language;
|
|
3144
|
+
if (!isLanguage(language)) return { error: "appearance.language must be zh-CN | en" };
|
|
3145
|
+
return { themeMode, themeName, language };
|
|
3146
|
+
}
|
|
2897
3147
|
function createSpecDraftsRoutes(resolveProject) {
|
|
2898
3148
|
const app = new Hono();
|
|
2899
3149
|
const need = async (c) => {
|
|
@@ -4104,7 +4354,7 @@ class RegistryEventBus {
|
|
|
4104
4354
|
this.scheduleEmit();
|
|
4105
4355
|
});
|
|
4106
4356
|
} catch (err) {
|
|
4107
|
-
registryLog().warn("
|
|
4357
|
+
registryLog().warn("global config watch failed", { dir, message: err.message });
|
|
4108
4358
|
}
|
|
4109
4359
|
}
|
|
4110
4360
|
stop() {
|
|
@@ -5305,6 +5555,37 @@ function toolResultText(content) {
|
|
|
5305
5555
|
}
|
|
5306
5556
|
return content == null ? "" : JSON.stringify(content);
|
|
5307
5557
|
}
|
|
5558
|
+
const USAGE_WINDOWS = [
|
|
5559
|
+
["five_hour", "5-hour"],
|
|
5560
|
+
["seven_day", "7-day"],
|
|
5561
|
+
["seven_day_sonnet", "Sonnet 7-day"],
|
|
5562
|
+
["seven_day_opus", "Opus 7-day"],
|
|
5563
|
+
["seven_day_oauth_apps", "OAuth apps 7-day"]
|
|
5564
|
+
];
|
|
5565
|
+
function usageWindows(usage) {
|
|
5566
|
+
const out = [];
|
|
5567
|
+
const limits = usage.rate_limits;
|
|
5568
|
+
if (!limits) return out;
|
|
5569
|
+
for (const [key, label] of USAGE_WINDOWS) {
|
|
5570
|
+
const win = limits[key];
|
|
5571
|
+
if (!win) continue;
|
|
5572
|
+
out.push({
|
|
5573
|
+
key,
|
|
5574
|
+
label,
|
|
5575
|
+
utilization: typeof win.utilization === "number" ? win.utilization : null,
|
|
5576
|
+
resetsAt: typeof win.resets_at === "string" ? win.resets_at : null
|
|
5577
|
+
});
|
|
5578
|
+
}
|
|
5579
|
+
for (const win of limits.model_scoped ?? []) {
|
|
5580
|
+
out.push({
|
|
5581
|
+
key: `model:${win.display_name}`,
|
|
5582
|
+
label: win.display_name,
|
|
5583
|
+
utilization: typeof win.utilization === "number" ? win.utilization : null,
|
|
5584
|
+
resetsAt: typeof win.resets_at === "string" ? win.resets_at : null
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
return out;
|
|
5588
|
+
}
|
|
5308
5589
|
class ClaudeSession {
|
|
5309
5590
|
constructor(id, cwd, isNew) {
|
|
5310
5591
|
this.id = id;
|
|
@@ -5414,10 +5695,40 @@ class ClaudeAdapter {
|
|
|
5414
5695
|
return out;
|
|
5415
5696
|
}
|
|
5416
5697
|
capabilities() {
|
|
5417
|
-
return { listSessions: true, getMessages: true };
|
|
5698
|
+
return { listSessions: true, getMessages: true, usageStatus: true };
|
|
5699
|
+
}
|
|
5700
|
+
async getUsageStatus() {
|
|
5701
|
+
const options = {
|
|
5702
|
+
cwd: this.cwd,
|
|
5703
|
+
permissionMode: "bypassPermissions",
|
|
5704
|
+
allowDangerouslySkipPermissions: true
|
|
5705
|
+
};
|
|
5706
|
+
const q = query({ prompt: "", options });
|
|
5707
|
+
try {
|
|
5708
|
+
const usage = await q.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET();
|
|
5709
|
+
q.close();
|
|
5710
|
+
return {
|
|
5711
|
+
kind: this.kind,
|
|
5712
|
+
status: usage.rate_limits_available ? "available" : "unavailable",
|
|
5713
|
+
checkedAt: Date.now(),
|
|
5714
|
+
subscriptionType: usage.subscription_type,
|
|
5715
|
+
rateLimitsAvailable: usage.rate_limits_available,
|
|
5716
|
+
windows: usageWindows(usage),
|
|
5717
|
+
message: usage.rate_limits_available ? void 0 : "claude usage rate limits are unavailable for this auth/provider"
|
|
5718
|
+
};
|
|
5719
|
+
} catch (err) {
|
|
5720
|
+
q.close();
|
|
5721
|
+
return {
|
|
5722
|
+
kind: this.kind,
|
|
5723
|
+
status: "error",
|
|
5724
|
+
checkedAt: Date.now(),
|
|
5725
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5418
5728
|
}
|
|
5419
5729
|
}
|
|
5420
5730
|
const DEFAULT_CODEX_STORAGE_ROOT = join(homedir(), ".codex");
|
|
5731
|
+
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
|
|
5421
5732
|
const TITLE_MAX_LENGTH$1 = 64;
|
|
5422
5733
|
function codexThreadOptions(cwd) {
|
|
5423
5734
|
return {
|
|
@@ -5474,6 +5785,107 @@ function parseCodexSessionIndex(text) {
|
|
|
5474
5785
|
}
|
|
5475
5786
|
return out;
|
|
5476
5787
|
}
|
|
5788
|
+
function asRecord$1(value) {
|
|
5789
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5790
|
+
}
|
|
5791
|
+
function numberFrom$1(value) {
|
|
5792
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
5793
|
+
if (typeof value === "string" && value.trim()) {
|
|
5794
|
+
const n = Number(value);
|
|
5795
|
+
if (Number.isFinite(n)) return n;
|
|
5796
|
+
}
|
|
5797
|
+
return null;
|
|
5798
|
+
}
|
|
5799
|
+
function resetFrom$1(value) {
|
|
5800
|
+
if (typeof value === "string" && value.trim()) {
|
|
5801
|
+
const n2 = Number(value);
|
|
5802
|
+
if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
|
|
5803
|
+
const d = new Date(value);
|
|
5804
|
+
return Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
5805
|
+
}
|
|
5806
|
+
const n = numberFrom$1(value);
|
|
5807
|
+
return n == null ? null : new Date(n * 1e3).toISOString();
|
|
5808
|
+
}
|
|
5809
|
+
function codexWindow(key, label, value) {
|
|
5810
|
+
const rec = asRecord$1(value);
|
|
5811
|
+
if (!rec) return null;
|
|
5812
|
+
const utilization = numberFrom$1(rec.used_percent) ?? numberFrom$1(rec.usedPercent) ?? numberFrom$1(rec.utilization) ?? numberFrom$1(rec.percent_used);
|
|
5813
|
+
if (utilization == null) return null;
|
|
5814
|
+
return {
|
|
5815
|
+
key,
|
|
5816
|
+
label,
|
|
5817
|
+
utilization,
|
|
5818
|
+
resetsAt: resetFrom$1(rec.resets_at ?? rec.reset_at ?? rec.resetsAt ?? rec.resetAt)
|
|
5819
|
+
};
|
|
5820
|
+
}
|
|
5821
|
+
function parseCodexUsageResponse(raw) {
|
|
5822
|
+
const root = asRecord$1(raw);
|
|
5823
|
+
if (!root) return null;
|
|
5824
|
+
const rateLimit = asRecord$1(root.rate_limit);
|
|
5825
|
+
const rateLimits = asRecord$1(root.rate_limits);
|
|
5826
|
+
const windows = [];
|
|
5827
|
+
const primary = codexWindow(
|
|
5828
|
+
"primary",
|
|
5829
|
+
"Primary",
|
|
5830
|
+
rateLimit?.primary_window ?? rateLimits?.primary ?? root.primary
|
|
5831
|
+
);
|
|
5832
|
+
if (primary) windows.push(primary);
|
|
5833
|
+
const secondary = codexWindow(
|
|
5834
|
+
"secondary",
|
|
5835
|
+
"Secondary",
|
|
5836
|
+
rateLimit?.secondary_window ?? rateLimits?.secondary ?? root.secondary
|
|
5837
|
+
);
|
|
5838
|
+
if (secondary) windows.push(secondary);
|
|
5839
|
+
const additional = root.additional_rate_limits ?? root.additionalRateLimits ?? rateLimit?.additional;
|
|
5840
|
+
if (Array.isArray(additional)) {
|
|
5841
|
+
for (const [idx, item] of additional.entries()) {
|
|
5842
|
+
const rec = asRecord$1(item);
|
|
5843
|
+
const label = String(rec?.label ?? rec?.name ?? rec?.limit_name ?? `Additional ${idx + 1}`);
|
|
5844
|
+
const win = codexWindow(`additional:${idx}`, label, item);
|
|
5845
|
+
if (win) windows.push(win);
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
if (windows.length === 0) return null;
|
|
5849
|
+
return {
|
|
5850
|
+
kind: "codex",
|
|
5851
|
+
status: "available",
|
|
5852
|
+
checkedAt: Date.now(),
|
|
5853
|
+
source: "private-api",
|
|
5854
|
+
subscriptionType: typeof root.plan_type === "string" ? root.plan_type : null,
|
|
5855
|
+
rateLimitsAvailable: true,
|
|
5856
|
+
windows
|
|
5857
|
+
};
|
|
5858
|
+
}
|
|
5859
|
+
function parseCodexTokenCountSnapshot(text) {
|
|
5860
|
+
let latestTs = 0;
|
|
5861
|
+
let latest = null;
|
|
5862
|
+
for (const line of text.split("\n")) {
|
|
5863
|
+
const trimmed = line.trim();
|
|
5864
|
+
if (!trimmed) continue;
|
|
5865
|
+
let obj;
|
|
5866
|
+
try {
|
|
5867
|
+
obj = JSON.parse(trimmed);
|
|
5868
|
+
} catch {
|
|
5869
|
+
continue;
|
|
5870
|
+
}
|
|
5871
|
+
const rec = asRecord$1(obj);
|
|
5872
|
+
const payload = asRecord$1(rec?.payload);
|
|
5873
|
+
if (payload?.type !== "token_count") continue;
|
|
5874
|
+
const parsed = parseCodexUsageResponse({ rate_limits: payload.rate_limits });
|
|
5875
|
+
if (!parsed) continue;
|
|
5876
|
+
const ts = typeof rec?.timestamp === "string" ? Date.parse(rec.timestamp) : 0;
|
|
5877
|
+
if (ts >= latestTs) {
|
|
5878
|
+
latestTs = ts;
|
|
5879
|
+
latest = {
|
|
5880
|
+
...parsed,
|
|
5881
|
+
checkedAt: Number.isFinite(ts) && ts > 0 ? ts : Date.now(),
|
|
5882
|
+
source: "local-snapshot",
|
|
5883
|
+
message: "codex local token_count snapshot"
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
return latest;
|
|
5888
|
+
}
|
|
5477
5889
|
class CodexSession {
|
|
5478
5890
|
constructor(thread) {
|
|
5479
5891
|
this.thread = thread;
|
|
@@ -5597,7 +6009,52 @@ class CodexAdapter {
|
|
|
5597
6009
|
return out;
|
|
5598
6010
|
}
|
|
5599
6011
|
capabilities() {
|
|
5600
|
-
return { listSessions: true, getMessages: true };
|
|
6012
|
+
return { listSessions: true, getMessages: true, usageStatus: true };
|
|
6013
|
+
}
|
|
6014
|
+
async getUsageStatus() {
|
|
6015
|
+
const live = await this.readPrivateUsage().catch(() => null);
|
|
6016
|
+
if (live) return live;
|
|
6017
|
+
const snapshot = await this.readLatestUsageSnapshot().catch(() => null);
|
|
6018
|
+
if (snapshot) return snapshot;
|
|
6019
|
+
return {
|
|
6020
|
+
kind: this.kind,
|
|
6021
|
+
status: "unavailable",
|
|
6022
|
+
checkedAt: Date.now(),
|
|
6023
|
+
message: "codex usage is unavailable: private API failed and no local token_count snapshot found"
|
|
6024
|
+
};
|
|
6025
|
+
}
|
|
6026
|
+
async readPrivateUsage() {
|
|
6027
|
+
const auth = await this.readAuth();
|
|
6028
|
+
if (!auth) return null;
|
|
6029
|
+
const res = await fetch(CODEX_USAGE_URL, {
|
|
6030
|
+
headers: {
|
|
6031
|
+
authorization: `Bearer ${auth.accessToken}`,
|
|
6032
|
+
"chatgpt-account-id": auth.accountId
|
|
6033
|
+
}
|
|
6034
|
+
});
|
|
6035
|
+
if (!res.ok) return null;
|
|
6036
|
+
return parseCodexUsageResponse(await res.json());
|
|
6037
|
+
}
|
|
6038
|
+
async readAuth() {
|
|
6039
|
+
let raw;
|
|
6040
|
+
try {
|
|
6041
|
+
raw = JSON.parse(await readFile(join(this.storageRoot, "auth.json"), "utf8"));
|
|
6042
|
+
} catch {
|
|
6043
|
+
return null;
|
|
6044
|
+
}
|
|
6045
|
+
const tokens = asRecord$1(asRecord$1(raw)?.tokens);
|
|
6046
|
+
const accessToken = typeof tokens?.access_token === "string" ? tokens.access_token : "";
|
|
6047
|
+
const accountId = typeof tokens?.account_id === "string" ? tokens.account_id : "";
|
|
6048
|
+
return accessToken && accountId ? { accessToken, accountId } : null;
|
|
6049
|
+
}
|
|
6050
|
+
async readLatestUsageSnapshot() {
|
|
6051
|
+
let latest = null;
|
|
6052
|
+
for (const file of await this.walkRollouts()) {
|
|
6053
|
+
const parsed = parseCodexTokenCountSnapshot(await readFile(file, "utf8"));
|
|
6054
|
+
if (!parsed) continue;
|
|
6055
|
+
if (!latest || parsed.checkedAt >= latest.checkedAt) latest = parsed;
|
|
6056
|
+
}
|
|
6057
|
+
return latest;
|
|
5601
6058
|
}
|
|
5602
6059
|
async walkRollouts() {
|
|
5603
6060
|
const out = [];
|
|
@@ -5684,6 +6141,137 @@ class CodexAdapter {
|
|
|
5684
6141
|
return null;
|
|
5685
6142
|
}
|
|
5686
6143
|
}
|
|
6144
|
+
const execFileP$1 = promisify(execFile);
|
|
6145
|
+
const OPENCODE_QUOTA_INSTALL = "npx @slkiser/opencode-quota@latest init";
|
|
6146
|
+
const OPENCODE_QUOTA_PLUGIN_BIN = join(
|
|
6147
|
+
homedir(),
|
|
6148
|
+
".cache",
|
|
6149
|
+
"opencode",
|
|
6150
|
+
"packages",
|
|
6151
|
+
"@slkiser",
|
|
6152
|
+
"opencode-quota@latest",
|
|
6153
|
+
"node_modules",
|
|
6154
|
+
"@slkiser",
|
|
6155
|
+
"opencode-quota",
|
|
6156
|
+
"dist",
|
|
6157
|
+
"bin",
|
|
6158
|
+
"opencode-quota.js"
|
|
6159
|
+
);
|
|
6160
|
+
function asRecord(value) {
|
|
6161
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6162
|
+
}
|
|
6163
|
+
function numberFrom(value) {
|
|
6164
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
6165
|
+
if (typeof value === "string" && value.trim()) {
|
|
6166
|
+
const n = Number(value);
|
|
6167
|
+
if (Number.isFinite(n)) return n;
|
|
6168
|
+
}
|
|
6169
|
+
return null;
|
|
6170
|
+
}
|
|
6171
|
+
function resetFrom(value) {
|
|
6172
|
+
if (typeof value === "string" && value.trim()) {
|
|
6173
|
+
const n2 = Number(value);
|
|
6174
|
+
if (Number.isFinite(n2)) return new Date(n2 * 1e3).toISOString();
|
|
6175
|
+
const d = new Date(value);
|
|
6176
|
+
return Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
6177
|
+
}
|
|
6178
|
+
const n = numberFrom(value);
|
|
6179
|
+
return n == null ? null : new Date(n * 1e3).toISOString();
|
|
6180
|
+
}
|
|
6181
|
+
function usageWindowFromRecord(rec, idx) {
|
|
6182
|
+
const label = String(rec.label ?? rec.name ?? rec.title ?? rec.provider ?? `Window ${idx + 1}`);
|
|
6183
|
+
const used = numberFrom(rec.usedPercent) ?? numberFrom(rec.used_percent) ?? numberFrom(rec.percent) ?? numberFrom(rec.utilization);
|
|
6184
|
+
const remaining = numberFrom(rec.remainingPercent) ?? numberFrom(rec.remaining_percent) ?? numberFrom(rec.percentRemaining);
|
|
6185
|
+
const utilization = used ?? (remaining == null ? null : 100 - remaining);
|
|
6186
|
+
if (utilization == null) return null;
|
|
6187
|
+
return {
|
|
6188
|
+
key: String(rec.key ?? rec.id ?? label),
|
|
6189
|
+
label,
|
|
6190
|
+
utilization,
|
|
6191
|
+
resetsAt: resetFrom(rec.resetsAt ?? rec.resets_at ?? rec.resetAt ?? rec.reset_at ?? rec.reset)
|
|
6192
|
+
};
|
|
6193
|
+
}
|
|
6194
|
+
function collectOpencodeQuotaWindows(value, out, seen) {
|
|
6195
|
+
if (!value || seen.has(value)) return;
|
|
6196
|
+
seen.add(value);
|
|
6197
|
+
if (Array.isArray(value)) {
|
|
6198
|
+
for (const item of value) collectOpencodeQuotaWindows(item, out, seen);
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
const rec = asRecord(value);
|
|
6202
|
+
if (!rec) return;
|
|
6203
|
+
const win = usageWindowFromRecord(rec, out.length);
|
|
6204
|
+
if (win) out.push(win);
|
|
6205
|
+
for (const key of ["windows", "quotas", "items", "limits", "entries"]) {
|
|
6206
|
+
if (key in rec) collectOpencodeQuotaWindows(rec[key], out, seen);
|
|
6207
|
+
}
|
|
6208
|
+
for (const key of ["providers", "data"]) {
|
|
6209
|
+
const child = asRecord(rec[key]);
|
|
6210
|
+
if (child) {
|
|
6211
|
+
for (const value2 of Object.values(child)) collectOpencodeQuotaWindows(value2, out, seen);
|
|
6212
|
+
} else if (key in rec) {
|
|
6213
|
+
collectOpencodeQuotaWindows(rec[key], out, seen);
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
function parseOpencodeQuotaOutput(text) {
|
|
6218
|
+
let raw;
|
|
6219
|
+
try {
|
|
6220
|
+
raw = JSON.parse(text);
|
|
6221
|
+
} catch {
|
|
6222
|
+
return parseOpencodeQuotaText(text);
|
|
6223
|
+
}
|
|
6224
|
+
const windows = [];
|
|
6225
|
+
collectOpencodeQuotaWindows(raw, windows, /* @__PURE__ */ new Set());
|
|
6226
|
+
if (windows.length === 0) return null;
|
|
6227
|
+
return {
|
|
6228
|
+
kind: "opencode",
|
|
6229
|
+
status: "available",
|
|
6230
|
+
checkedAt: Date.now(),
|
|
6231
|
+
source: "external-cli",
|
|
6232
|
+
rateLimitsAvailable: true,
|
|
6233
|
+
windows
|
|
6234
|
+
};
|
|
6235
|
+
}
|
|
6236
|
+
function parseOpencodeQuotaText(text) {
|
|
6237
|
+
const windows = [];
|
|
6238
|
+
let provider = "";
|
|
6239
|
+
for (const line of text.split("\n")) {
|
|
6240
|
+
const providerMatch = line.match(/^\s*→\s*\[(.+?)]/);
|
|
6241
|
+
if (providerMatch) {
|
|
6242
|
+
provider = providerMatch[1].trim();
|
|
6243
|
+
continue;
|
|
6244
|
+
}
|
|
6245
|
+
const quotaMatch = line.match(/^\s*(.+?quota)\s+.*?(\d+(?:\.\d+)?)%\s+left/i);
|
|
6246
|
+
if (!quotaMatch) continue;
|
|
6247
|
+
const label = [provider, quotaMatch[1].trim()].filter(Boolean).join(": ");
|
|
6248
|
+
const remaining = Number(quotaMatch[2]);
|
|
6249
|
+
windows.push({
|
|
6250
|
+
key: label || `quota:${windows.length}`,
|
|
6251
|
+
label: label || `Quota ${windows.length + 1}`,
|
|
6252
|
+
utilization: Math.max(0, Math.min(100, 100 - remaining)),
|
|
6253
|
+
resetsAt: null
|
|
6254
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
if (windows.length === 0) return null;
|
|
6257
|
+
return {
|
|
6258
|
+
kind: "opencode",
|
|
6259
|
+
status: "available",
|
|
6260
|
+
checkedAt: Date.now(),
|
|
6261
|
+
source: "external-cli",
|
|
6262
|
+
rateLimitsAvailable: true,
|
|
6263
|
+
windows
|
|
6264
|
+
};
|
|
6265
|
+
}
|
|
6266
|
+
async function opencodeQuotaCommands() {
|
|
6267
|
+
const commands = [{ cmd: "opencode-quota", args: ["show", "--json"] }];
|
|
6268
|
+
try {
|
|
6269
|
+
await access(OPENCODE_QUOTA_PLUGIN_BIN);
|
|
6270
|
+
commands.push({ cmd: process.execPath, args: [OPENCODE_QUOTA_PLUGIN_BIN, "show", "--json"] });
|
|
6271
|
+
} catch {
|
|
6272
|
+
}
|
|
6273
|
+
return commands;
|
|
6274
|
+
}
|
|
5687
6275
|
function textParts(parts) {
|
|
5688
6276
|
const out = [];
|
|
5689
6277
|
for (const p of parts) {
|
|
@@ -5713,7 +6301,9 @@ class OpenCodeSession {
|
|
|
5713
6301
|
if (this.aborted) return;
|
|
5714
6302
|
const data = res.data;
|
|
5715
6303
|
if (!data) {
|
|
5716
|
-
const errMsg = res.error && typeof res.error === "object" && "data" in res.error ? String(
|
|
6304
|
+
const errMsg = res.error && typeof res.error === "object" && "data" in res.error ? String(
|
|
6305
|
+
res.error.data?.message ?? "prompt failed"
|
|
6306
|
+
) : "prompt failed";
|
|
5717
6307
|
yield { type: "error", message: errMsg };
|
|
5718
6308
|
return;
|
|
5719
6309
|
}
|
|
@@ -5795,7 +6385,51 @@ class OpenCodeAdapter {
|
|
|
5795
6385
|
return out;
|
|
5796
6386
|
}
|
|
5797
6387
|
capabilities() {
|
|
5798
|
-
return { listSessions: true, getMessages: true };
|
|
6388
|
+
return { listSessions: true, getMessages: true, usageStatus: true };
|
|
6389
|
+
}
|
|
6390
|
+
async getUsageStatus() {
|
|
6391
|
+
let lastError;
|
|
6392
|
+
for (const command of await opencodeQuotaCommands()) {
|
|
6393
|
+
try {
|
|
6394
|
+
const { stdout } = await execFileP$1(command.cmd, command.args, {
|
|
6395
|
+
cwd: this.cwd,
|
|
6396
|
+
timeout: 1e4,
|
|
6397
|
+
maxBuffer: 1024 * 1024
|
|
6398
|
+
});
|
|
6399
|
+
const parsed = parseOpencodeQuotaOutput(stdout);
|
|
6400
|
+
if (parsed) return parsed;
|
|
6401
|
+
return {
|
|
6402
|
+
kind: this.kind,
|
|
6403
|
+
status: "error",
|
|
6404
|
+
checkedAt: Date.now(),
|
|
6405
|
+
source: "external-cli",
|
|
6406
|
+
message: "opencode-quota returned no parseable quota windows"
|
|
6407
|
+
};
|
|
6408
|
+
} catch (err2) {
|
|
6409
|
+
lastError = err2;
|
|
6410
|
+
if (err2.code === "ENOENT") continue;
|
|
6411
|
+
break;
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
const err = lastError;
|
|
6415
|
+
const code = err?.code;
|
|
6416
|
+
if (code === "ENOENT") {
|
|
6417
|
+
return {
|
|
6418
|
+
kind: this.kind,
|
|
6419
|
+
status: "unavailable",
|
|
6420
|
+
checkedAt: Date.now(),
|
|
6421
|
+
source: "external-cli",
|
|
6422
|
+
installCommand: OPENCODE_QUOTA_INSTALL,
|
|
6423
|
+
message: "opencode-quota is not installed"
|
|
6424
|
+
};
|
|
6425
|
+
}
|
|
6426
|
+
return {
|
|
6427
|
+
kind: this.kind,
|
|
6428
|
+
status: "error",
|
|
6429
|
+
checkedAt: Date.now(),
|
|
6430
|
+
source: "external-cli",
|
|
6431
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6432
|
+
};
|
|
5799
6433
|
}
|
|
5800
6434
|
async dispose() {
|
|
5801
6435
|
if (!this.booting) return;
|
|
@@ -5937,14 +6571,17 @@ class SessionManager {
|
|
|
5937
6571
|
const indexed2 = byId.get(info.id);
|
|
5938
6572
|
if (!indexed2) {
|
|
5939
6573
|
byId.set(info.id, info);
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
6574
|
+
continue;
|
|
6575
|
+
}
|
|
6576
|
+
const merged = {
|
|
6577
|
+
...indexed2,
|
|
6578
|
+
createdAt: Math.min(indexed2.createdAt, info.createdAt),
|
|
6579
|
+
updatedAt: Math.max(indexed2.updatedAt, info.updatedAt)
|
|
6580
|
+
};
|
|
6581
|
+
if (isOpaqueTitle(indexed2.title, indexed2.id) && !isOpaqueTitle(info.title, info.id)) {
|
|
6582
|
+
merged.title = info.title;
|
|
5947
6583
|
}
|
|
6584
|
+
byId.set(info.id, merged);
|
|
5948
6585
|
}
|
|
5949
6586
|
} catch {
|
|
5950
6587
|
}
|
|
@@ -5952,8 +6589,11 @@ class SessionManager {
|
|
|
5952
6589
|
const alive = [...byId.values()].filter(
|
|
5953
6590
|
(s) => nativeIds.has(s.id) || s.createdAt !== s.updatedAt || this.running.has(s.id)
|
|
5954
6591
|
);
|
|
6592
|
+
const activityRank = (s) => this.running.has(s.id) ? 1 : 0;
|
|
5955
6593
|
const out = [];
|
|
5956
|
-
for (const s of alive.sort(
|
|
6594
|
+
for (const s of alive.sort(
|
|
6595
|
+
(a, b) => activityRank(b) - activityRank(a) || b.updatedAt - a.updatedAt
|
|
6596
|
+
)) {
|
|
5957
6597
|
out.push({ ...s, running: this.running.has(s.id) });
|
|
5958
6598
|
if (out.length >= SESSION_LIST_LIMIT) break;
|
|
5959
6599
|
}
|
|
@@ -6001,6 +6641,27 @@ class SessionManager {
|
|
|
6001
6641
|
if (!adapter.capabilities().getMessages) return [];
|
|
6002
6642
|
return adapter.getMessages(sid);
|
|
6003
6643
|
}
|
|
6644
|
+
async getUsageStatus() {
|
|
6645
|
+
const adapter = this.adapters.get(this.defaultKind);
|
|
6646
|
+
if (!adapter.capabilities().usageStatus || !adapter.getUsageStatus) {
|
|
6647
|
+
return {
|
|
6648
|
+
kind: this.defaultKind,
|
|
6649
|
+
status: "unavailable",
|
|
6650
|
+
checkedAt: Date.now(),
|
|
6651
|
+
message: `${this.defaultKind} adapter does not support usage status queries`
|
|
6652
|
+
};
|
|
6653
|
+
}
|
|
6654
|
+
try {
|
|
6655
|
+
return await adapter.getUsageStatus();
|
|
6656
|
+
} catch (err) {
|
|
6657
|
+
return {
|
|
6658
|
+
kind: this.defaultKind,
|
|
6659
|
+
status: "error",
|
|
6660
|
+
checkedAt: Date.now(),
|
|
6661
|
+
message: err instanceof Error ? err.message : String(err)
|
|
6662
|
+
};
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6004
6665
|
async ensureLive(sid) {
|
|
6005
6666
|
const existing = this.live.get(sid);
|
|
6006
6667
|
if (existing) return existing;
|
|
@@ -6012,12 +6673,19 @@ class SessionManager {
|
|
|
6012
6673
|
if (this.disposing) session.abort();
|
|
6013
6674
|
return ls;
|
|
6014
6675
|
}
|
|
6015
|
-
|
|
6676
|
+
/**
|
|
6677
|
+
* `titleSource` lets callers derive the session title from the user's own
|
|
6678
|
+
* text while sending an expanded prompt to the Agent. Without it, an injected
|
|
6679
|
+
* hidden prompt would become the title.
|
|
6680
|
+
*/
|
|
6681
|
+
async send(sid, prompt, titleSource) {
|
|
6016
6682
|
const runId = randomUUID();
|
|
6017
6683
|
const emitter = this.emitterFor(sid);
|
|
6018
6684
|
let currentSid = sid;
|
|
6019
6685
|
const startedAt = Date.now();
|
|
6020
|
-
await this.maybeUpdateTitleFromPrompt(currentSid, prompt).catch(() => {
|
|
6686
|
+
await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
|
|
6687
|
+
});
|
|
6688
|
+
await this.store.touch(sid).catch(() => {
|
|
6021
6689
|
});
|
|
6022
6690
|
this.setRunning(sid, true);
|
|
6023
6691
|
agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
|