@yorz/cli 0.7.0 → 0.8.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 +1082 -276
- package/dist/gui/assets/{abnfDiagram-VRR7QNED-pQ0yp01L.js → abnfDiagram-VRR7QNED-D1kFOejy.js} +1 -1
- package/dist/gui/assets/{arc-k5zxnVZT.js → arc--bTdICho.js} +1 -1
- package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-DX-yKkN9.js → architectureDiagram-ZJ3FMSHR-C3DMgcAW.js} +1 -1
- package/dist/gui/assets/{blockDiagram-677ZJIJ3-CMoP3PmZ.js → blockDiagram-677ZJIJ3-BCtvFOy7.js} +1 -1
- package/dist/gui/assets/{c4Diagram-LMCZKHZV-CFAjMeqQ.js → c4Diagram-LMCZKHZV-CDw5fv3L.js} +1 -1
- package/dist/gui/assets/channel-Zv7U9soI.js +1 -0
- package/dist/gui/assets/{chunk-2Q5K7J3B-CSYRtuuw.js → chunk-2Q5K7J3B-DbJTSeCo.js} +1 -1
- package/dist/gui/assets/{chunk-32BRIVSS-BRohWK3g.js → chunk-32BRIVSS-BcF_P8AP.js} +1 -1
- package/dist/gui/assets/{chunk-5VM5RSS4-BANnogAt.js → chunk-5VM5RSS4-CYp4dWLx.js} +1 -1
- package/dist/gui/assets/{chunk-EX3LRPZG-BcVKPAuP.js → chunk-EX3LRPZG-BDzejC8h.js} +1 -1
- package/dist/gui/assets/{chunk-JWPE2WC7-D4KqJLpk.js → chunk-JWPE2WC7-Fnenn1Qc.js} +1 -1
- package/dist/gui/assets/{chunk-MOJQB5TN-CDFJAykP.js → chunk-MOJQB5TN-Cx7nt_sO.js} +1 -1
- package/dist/gui/assets/{chunk-RYQCIY6F-BJN0o293.js → chunk-RYQCIY6F-MQPZLynl.js} +1 -1
- package/dist/gui/assets/{chunk-V7JOEXUC-B3m1VhnM.js → chunk-V7JOEXUC-F5RsZN0n.js} +1 -1
- package/dist/gui/assets/{chunk-VR4S4FIN-BLYOZzRK.js → chunk-VR4S4FIN-CoOdAASP.js} +1 -1
- package/dist/gui/assets/{chunk-XXDRQBXY-s69CKrwP.js → chunk-XXDRQBXY-Dp3kfiet.js} +1 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-DRHl_SaL.js +1 -0
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DRHl_SaL.js +1 -0
- package/dist/gui/assets/{cose-bilkent-JH36ORCC-BrNJSX1S.js → cose-bilkent-JH36ORCC-PfqHyC7G.js} +1 -1
- package/dist/gui/assets/{cynefin-VYW2F7L2-i8qxCu_0.js → cynefin-VYW2F7L2-DerSwhBp.js} +1 -1
- package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-dpueKRs1.js → cynefinDiagram-TSTJHNR4-BwakZjCU.js} +1 -1
- package/dist/gui/assets/{dagre-VKFMJZFB-fqwQB8CS.js → dagre-VKFMJZFB-Bqgor74s.js} +1 -1
- package/dist/gui/assets/{diagram-FQU43EPY-B99SBDqa.js → diagram-FQU43EPY-DgyWxpcU.js} +1 -1
- package/dist/gui/assets/{diagram-G47NLZAW-DF4Oz_Q6.js → diagram-G47NLZAW-DwvD_Up0.js} +1 -1
- package/dist/gui/assets/{diagram-NH7WQ7WH-BIaXJ_6B.js → diagram-NH7WQ7WH-DSimddOu.js} +1 -1
- package/dist/gui/assets/{diagram-OA4YK3LP-CJ4_-IYz.js → diagram-OA4YK3LP-CrBUd7XP.js} +1 -1
- package/dist/gui/assets/{diagram-WEI45ONY-LvUrzfuG.js → diagram-WEI45ONY-BWgcRZqn.js} +1 -1
- package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-De5G_4vB.js → ebnfDiagram-CCIWWBDH-g9dEaUVe.js} +1 -1
- package/dist/gui/assets/{erDiagram-Q63AITRT-rYLQVUAr.js → erDiagram-Q63AITRT-DhYVc7YD.js} +1 -1
- package/dist/gui/assets/{flowDiagram-23GEKE2U-CwDsoVsz.js → flowDiagram-23GEKE2U-COeJThES.js} +1 -1
- package/dist/gui/assets/{ganttDiagram-NO4QXBWP-Bm1KWOcG.js → ganttDiagram-NO4QXBWP-DJxQzqsI.js} +1 -1
- package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-s3jsEQWl.js → gitGraphDiagram-IHSO6WYX-CC_xIkVG.js} +1 -1
- package/dist/gui/assets/{index-Cft6LQWD.css → index-BzxGbxnX.css} +1 -1
- package/dist/gui/assets/index-tquwDQwL.js +39 -0
- package/dist/gui/assets/{infoDiagram-FWYZ7A6U-xmq3a-E3.js → infoDiagram-FWYZ7A6U-C6FK6dRz.js} +1 -1
- package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-CFPUA4LV.js → ishikawaDiagram-FXEZZL3T-D7b3GjN0.js} +1 -1
- package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BOiMo2WC.js → journeyDiagram-5HDEW3XC-Ct0_MZXS.js} +1 -1
- package/dist/gui/assets/{kanban-definition-HUTT4EX6-o_oz2FbJ.js → kanban-definition-HUTT4EX6-M4nszR7h.js} +1 -1
- package/dist/gui/assets/{linear-D1YtFTY4.js → linear-CGUJayt8.js} +1 -1
- package/dist/gui/assets/{mermaid.core-DQyDucFo.js → mermaid.core-DpiXCPQI.js} +4 -4
- package/dist/gui/assets/{mindmap-definition-LN4V7U3C-CX9Et0Qr.js → mindmap-definition-LN4V7U3C-CBbqzgM2.js} +1 -1
- package/dist/gui/assets/{pegDiagram-2B236MQR-DKoIr3v7.js → pegDiagram-2B236MQR-BW9OSYgR.js} +1 -1
- package/dist/gui/assets/{pieDiagram-ENE6RG2P-BSJigzmR.js → pieDiagram-ENE6RG2P-CT3MdE-x.js} +1 -1
- package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-Bpmh7Mn0.js → quadrantDiagram-ABIIQ3AL-BAqDSslO.js} +1 -1
- package/dist/gui/assets/{railroadDiagram-RFXS5EU6-DoaeONQd.js → railroadDiagram-RFXS5EU6-BjxuOun7.js} +1 -1
- package/dist/gui/assets/{requirementDiagram-TGXJPOKE-B7GlOm4X.js → requirementDiagram-TGXJPOKE-bqwQ3Xy2.js} +1 -1
- package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-B7IwnQiq.js → sankeyDiagram-HTMAVEWB-D5UFHu3G.js} +1 -1
- package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-Nwq2uhP9.js → sequenceDiagram-DBY2YBRQ-BimXCn08.js} +1 -1
- package/dist/gui/assets/{sizeCapture-X5ZJPWSS-CCdTHNj_.js → sizeCapture-X5ZJPWSS-3iod82ny.js} +1 -1
- package/dist/gui/assets/{stateDiagram-2N3HPSRC-CqxRfIAb.js → stateDiagram-2N3HPSRC-BCkRUWaU.js} +1 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-7yy6idF2.js +1 -0
- package/dist/gui/assets/{swimlanes-5IMT3BWC-Bn-R9jtG.js → swimlanes-5IMT3BWC-D7XMYBp1.js} +2 -2
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-DGa9rk3s.js +8 -0
- package/dist/gui/assets/{timeline-definition-FHXFAJF6-BKXgnjEG.js → timeline-definition-FHXFAJF6-D9U9I24L.js} +1 -1
- package/dist/gui/assets/{vennDiagram-L72KCM5P-Czw5UU3t.js → vennDiagram-L72KCM5P-DKs7jWNI.js} +1 -1
- package/dist/gui/assets/{wardleyDiagram-EHGQE667-BJS3BME3.js → wardleyDiagram-EHGQE667-CLvHSSW6.js} +1 -1
- package/dist/gui/assets/{xychartDiagram-FW5EYKEG-DBt-6lUr.js → xychartDiagram-FW5EYKEG-V0U7x5OK.js} +1 -1
- package/dist/gui/index.html +2 -2
- package/package.json +2 -1
- package/dist/gui/assets/channel-sLqZREwh.js +0 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-DpZPCbfk.js +0 -1
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DpZPCbfk.js +0 -1
- package/dist/gui/assets/index-eECshTCa.js +0 -36
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CWQWNOHt.js +0 -1
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-D7kdlDFk.js +0 -8
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { homedir, platform } from "node:os";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
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,
|
|
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.8.0";
|
|
27
27
|
const type = "module";
|
|
28
28
|
const description = "";
|
|
29
29
|
const bin = { "yorz": "./dist/cli/index.js" };
|
|
@@ -35,7 +35,7 @@ const repository = { "type": "git", "url": "git+https://github.com/hughfenghen/Y
|
|
|
35
35
|
const homepage = "https://github.com/hughfenghen/YorZ#readme";
|
|
36
36
|
const publishConfig = { "access": "public" };
|
|
37
37
|
const scripts = { "build": "pnpm run build:cli && pnpm run build:gui", "build:cli": "vite build", "build:gui": "vite build --config vite.gui.config.ts", "dev:cli": "pnpm run build:cli && node dist/cli/index.js serve --port 7424 --foreground", "dev:gui": "vite --config vite.gui.config.ts", "typecheck": "tsc -b", "test": "vitest run", "test:agent": "node scripts/test-agent.mjs", "test:e2e": "pnpm run build && playwright test", "format": 'prettier --write "src/**/*.{ts,tsx,md,css}"', "prepublishOnly": "pnpm run build" };
|
|
38
|
-
const dependencies = { "@anthropic-ai/claude-agent-sdk": "^0.3.207", "@hono/node-server": "^2.0.4", "@openai/codex-sdk": "^0.144.1", "@opencode-ai/sdk": "^1.17.18", "chokidar": "^5.0.0", "commander": "^12.1.0", "gray-matter": "^4.0.3", "hono": "^4.12.25", "jsdom": "^25.0.1", "markdown-it": "^14.2.0", "mermaid": "^11.0.0", "morphdom": "^2.7.8", "timeago.js": "^4.0.2", "trash": "^10.1.1" };
|
|
38
|
+
const dependencies = { "@anthropic-ai/claude-agent-sdk": "^0.3.207", "@hono/node-server": "^2.0.4", "@openai/codex-sdk": "^0.144.1", "@opencode-ai/sdk": "^1.17.18", "chokidar": "^5.0.0", "commander": "^12.1.0", "gray-matter": "^4.0.3", "hono": "^4.12.25", "jsdom": "^25.0.1", "markdown-it": "^14.2.0", "mermaid": "^11.0.0", "morphdom": "^2.7.8", "parse-diff": "^0.12.0", "timeago.js": "^4.0.2", "trash": "^10.1.1" };
|
|
39
39
|
const devDependencies = { "@fontsource/jetbrains-mono": "^5.3.0", "@kobalte/core": "^0.13.12", "@playwright/test": "^1.61.0", "@solidjs/router": "^0.16.1", "@types/jsdom": "^21.1.7", "@types/markdown-it": "^14.1.2", "@types/node": "^22.10.0", "autoprefixer": "^10.5.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.1.0", "highlight.js": "^11.11.1", "i18next": "^26.3.6", "i18next-browser-languagedetector": "^8.2.1", "lucide-solid": "^1.24.0", "markdown-it-task-lists": "^2.1.1", "postcss": "^8.5.16", "prettier": "^3.4.0", "solid-js": "^1.9.13", "tailwind-merge": "^3.6.0", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7", "typescript": "^5.7.0", "vite": "^6.0.0", "vite-plugin-solid": "^2.11.12", "vitest": "^2.1.0" };
|
|
40
40
|
const pkg = {
|
|
41
41
|
name,
|
|
@@ -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 = {
|
|
@@ -233,30 +320,6 @@ function normalizeAppearance(value) {
|
|
|
233
320
|
language: isLanguage(language) ? language : DEFAULT_APPEARANCE.language
|
|
234
321
|
};
|
|
235
322
|
}
|
|
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
323
|
function isThemeMode(value) {
|
|
261
324
|
return value === "system" || value === "light" || value === "dark";
|
|
262
325
|
}
|
|
@@ -1136,6 +1199,202 @@ function skillEntryPath(skillName, env = process.env) {
|
|
|
1136
1199
|
function skillRef(skillName, env = process.env) {
|
|
1137
1200
|
return `请先完整阅读并严格遵循 ${skillEntryPath(skillName, env)}(YorZ 内置 ${skillName} skill 规则)`;
|
|
1138
1201
|
}
|
|
1202
|
+
const METRICS_DIR_NAME = "metrics";
|
|
1203
|
+
const TELEMETRY_FILE_NAME = "telemetry.jsonl";
|
|
1204
|
+
const PROJECT_META_FILE_NAME = "project.json";
|
|
1205
|
+
const DEFAULT_TELEMETRY_MAX_BYTES = 5 * 1024 * 1024;
|
|
1206
|
+
const DEFAULT_TELEMETRY_MAX_ARCHIVES = 2;
|
|
1207
|
+
function resolveMetricsDir(env = process.env) {
|
|
1208
|
+
return join(resolveGlobalConfigDir(env), METRICS_DIR_NAME);
|
|
1209
|
+
}
|
|
1210
|
+
function resolveProjectMetricsDir(projectRoot, env = process.env) {
|
|
1211
|
+
return join(resolveMetricsDir(env), generateProjectId(resolve(projectRoot)));
|
|
1212
|
+
}
|
|
1213
|
+
function findProjectRoot(startDir) {
|
|
1214
|
+
let dir = resolve(startDir);
|
|
1215
|
+
for (; ; ) {
|
|
1216
|
+
if (existsSync(join(dir, ".yorz"))) return dir;
|
|
1217
|
+
const parent = dirname(dir);
|
|
1218
|
+
if (parent === dir) return null;
|
|
1219
|
+
dir = parent;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function normalizeUsage(kind, raw) {
|
|
1223
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1224
|
+
const obj = raw;
|
|
1225
|
+
const snapshot = kind === "codex" ? fromCodex(obj) : kind === "opencode" ? fromOpenCode(obj) : fromClaude(obj);
|
|
1226
|
+
return isEmpty(snapshot) ? void 0 : snapshot;
|
|
1227
|
+
}
|
|
1228
|
+
function fromClaude(u) {
|
|
1229
|
+
return compact({
|
|
1230
|
+
inputTokens: num$2(u.input_tokens),
|
|
1231
|
+
outputTokens: num$2(u.output_tokens),
|
|
1232
|
+
cacheReadTokens: num$2(u.cache_read_input_tokens),
|
|
1233
|
+
cacheCreateTokens: num$2(u.cache_creation_input_tokens)
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
function fromCodex(u) {
|
|
1237
|
+
const total = num$2(u.input_tokens);
|
|
1238
|
+
const cached = num$2(u.cached_input_tokens);
|
|
1239
|
+
const uncached = total !== void 0 && cached !== void 0 ? Math.max(total - cached, 0) : total;
|
|
1240
|
+
return compact({
|
|
1241
|
+
inputTokens: uncached,
|
|
1242
|
+
cacheReadTokens: cached,
|
|
1243
|
+
outputTokens: num$2(u.output_tokens),
|
|
1244
|
+
reasoningTokens: num$2(u.reasoning_output_tokens)
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
function fromOpenCode(u) {
|
|
1248
|
+
const cache = u.cache && typeof u.cache === "object" ? u.cache : {};
|
|
1249
|
+
return compact({
|
|
1250
|
+
inputTokens: num$2(u.input),
|
|
1251
|
+
outputTokens: num$2(u.output),
|
|
1252
|
+
reasoningTokens: num$2(u.reasoning),
|
|
1253
|
+
cacheReadTokens: num$2(cache.read),
|
|
1254
|
+
cacheCreateTokens: num$2(cache.write)
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
function num$2(value) {
|
|
1258
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1259
|
+
}
|
|
1260
|
+
function compact(snapshot) {
|
|
1261
|
+
const out = {};
|
|
1262
|
+
for (const [key, value] of Object.entries(snapshot)) {
|
|
1263
|
+
if (value !== void 0) out[key] = value;
|
|
1264
|
+
}
|
|
1265
|
+
return out;
|
|
1266
|
+
}
|
|
1267
|
+
function isEmpty(snapshot) {
|
|
1268
|
+
return Object.keys(snapshot).length === 0;
|
|
1269
|
+
}
|
|
1270
|
+
const TELEMETRY_SCHEMA_VERSION = 1;
|
|
1271
|
+
function isTelemetryEnabled(env = process.env) {
|
|
1272
|
+
const raw = env.YORZ_TELEMETRY?.trim().toLowerCase();
|
|
1273
|
+
if (!raw) return true;
|
|
1274
|
+
return !["off", "0", "false", "no", "disabled"].includes(raw);
|
|
1275
|
+
}
|
|
1276
|
+
class TelemetryRecorder {
|
|
1277
|
+
constructor(projectRoot, projectId, dir, enabled) {
|
|
1278
|
+
this.projectRoot = projectRoot;
|
|
1279
|
+
this.projectId = projectId;
|
|
1280
|
+
this.dir = dir;
|
|
1281
|
+
this.enabled = enabled;
|
|
1282
|
+
this.sink = enabled ? new RotatingFileSink(
|
|
1283
|
+
dir,
|
|
1284
|
+
TELEMETRY_FILE_NAME,
|
|
1285
|
+
DEFAULT_TELEMETRY_MAX_BYTES,
|
|
1286
|
+
DEFAULT_TELEMETRY_MAX_ARCHIVES
|
|
1287
|
+
) : null;
|
|
1288
|
+
}
|
|
1289
|
+
sink;
|
|
1290
|
+
metaWritten = false;
|
|
1291
|
+
get filePath() {
|
|
1292
|
+
return join(this.dir, TELEMETRY_FILE_NAME);
|
|
1293
|
+
}
|
|
1294
|
+
/** Queue one event. Returns immediately; never throws. */
|
|
1295
|
+
record(event, payload = {}) {
|
|
1296
|
+
if (!this.sink) return;
|
|
1297
|
+
let line;
|
|
1298
|
+
try {
|
|
1299
|
+
const envelope = {
|
|
1300
|
+
v: TELEMETRY_SCHEMA_VERSION,
|
|
1301
|
+
ts: nowStamp(),
|
|
1302
|
+
event,
|
|
1303
|
+
projectId: this.projectId,
|
|
1304
|
+
...payload
|
|
1305
|
+
};
|
|
1306
|
+
line = `${JSON.stringify(envelope)}
|
|
1307
|
+
`;
|
|
1308
|
+
} catch {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
this.sink.write(line);
|
|
1312
|
+
this.ensureMeta();
|
|
1313
|
+
}
|
|
1314
|
+
/** Resolve once every queued line has hit the disk (tests / shutdown). */
|
|
1315
|
+
async flush() {
|
|
1316
|
+
await this.sink?.flush();
|
|
1317
|
+
}
|
|
1318
|
+
/** Write the id → absolute path sidecar once per process. Best-effort. */
|
|
1319
|
+
ensureMeta() {
|
|
1320
|
+
if (this.metaWritten || !this.sink) return;
|
|
1321
|
+
this.metaWritten = true;
|
|
1322
|
+
const meta = {
|
|
1323
|
+
id: this.projectId,
|
|
1324
|
+
path: this.projectRoot,
|
|
1325
|
+
firstSeenAt: nowStamp()
|
|
1326
|
+
};
|
|
1327
|
+
void (async () => {
|
|
1328
|
+
try {
|
|
1329
|
+
await mkdir(this.dir, { recursive: true });
|
|
1330
|
+
await writeFile(join(this.dir, PROJECT_META_FILE_NAME), `${JSON.stringify(meta, null, 2)}
|
|
1331
|
+
`);
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
})();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const recorders = /* @__PURE__ */ new Map();
|
|
1338
|
+
function getTelemetry(projectRoot, env = process.env) {
|
|
1339
|
+
const root = resolve(projectRoot);
|
|
1340
|
+
const cached = recorders.get(root);
|
|
1341
|
+
if (cached) return cached;
|
|
1342
|
+
const recorder = new TelemetryRecorder(
|
|
1343
|
+
root,
|
|
1344
|
+
generateProjectId(root),
|
|
1345
|
+
resolveProjectMetricsDir(root, env),
|
|
1346
|
+
isTelemetryEnabled(env)
|
|
1347
|
+
);
|
|
1348
|
+
recorders.set(root, recorder);
|
|
1349
|
+
return recorder;
|
|
1350
|
+
}
|
|
1351
|
+
function nowStamp(date = /* @__PURE__ */ new Date()) {
|
|
1352
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1353
|
+
const ymd = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
1354
|
+
return `${ymd} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1355
|
+
}
|
|
1356
|
+
const TASK_LINE = /^- \[( |x|X)\]/gm;
|
|
1357
|
+
function snapshotSpec(detail) {
|
|
1358
|
+
if (!detail) return null;
|
|
1359
|
+
let tasksTotal = 0;
|
|
1360
|
+
let tasksDone = 0;
|
|
1361
|
+
for (const match of detail.body.matchAll(TASK_LINE)) {
|
|
1362
|
+
tasksTotal += 1;
|
|
1363
|
+
if (match[1] !== " ") tasksDone += 1;
|
|
1364
|
+
}
|
|
1365
|
+
return {
|
|
1366
|
+
stage: detail.frontmatter.stage,
|
|
1367
|
+
tasksTotal,
|
|
1368
|
+
tasksDone,
|
|
1369
|
+
specBytes: Buffer.byteLength(detail.body, "utf8")
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function trackSpecStage(opts) {
|
|
1373
|
+
const { projectRoot, store, specId, handle, before, trigger } = opts;
|
|
1374
|
+
const beforeSnapshot = snapshotSpec(before);
|
|
1375
|
+
handle.onDone(() => {
|
|
1376
|
+
void (async () => {
|
|
1377
|
+
let afterSnapshot = null;
|
|
1378
|
+
try {
|
|
1379
|
+
afterSnapshot = snapshotSpec(await store.read(specId));
|
|
1380
|
+
} catch {
|
|
1381
|
+
}
|
|
1382
|
+
const payload = {
|
|
1383
|
+
specId,
|
|
1384
|
+
trigger,
|
|
1385
|
+
traceId: handle.runId,
|
|
1386
|
+
sessionId: handle.sessionId,
|
|
1387
|
+
stageBefore: beforeSnapshot?.stage,
|
|
1388
|
+
stageAfter: afterSnapshot?.stage,
|
|
1389
|
+
tasksTotal: afterSnapshot?.tasksTotal,
|
|
1390
|
+
tasksDone: afterSnapshot?.tasksDone,
|
|
1391
|
+
tasksDoneBefore: beforeSnapshot?.tasksDone,
|
|
1392
|
+
specBytes: afterSnapshot?.specBytes
|
|
1393
|
+
};
|
|
1394
|
+
getTelemetry(projectRoot).record("spec.stage", payload);
|
|
1395
|
+
})();
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1139
1398
|
function createSpecsRoutes(resolveProject) {
|
|
1140
1399
|
const app = new Hono();
|
|
1141
1400
|
const need = async (c) => {
|
|
@@ -1166,7 +1425,7 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1166
1425
|
const beforeIds = new Set((await p.store.list()).map((s) => s.id));
|
|
1167
1426
|
const prompt = buildDraftPrompt(input.type, input.requirement, input.draftId);
|
|
1168
1427
|
const { sessionId } = await p.sessions.createSession();
|
|
1169
|
-
const handle = await p.sessions.send(sessionId, prompt);
|
|
1428
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, { trigger: "new-spec" });
|
|
1170
1429
|
handle.onDone((finalSessionId) => {
|
|
1171
1430
|
void bindDraftSessionToCreatedSpec(p, beforeIds, finalSessionId);
|
|
1172
1431
|
});
|
|
@@ -1302,7 +1561,18 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1302
1561
|
"resume",
|
|
1303
1562
|
await buildDebugRuntimeContext(p)
|
|
1304
1563
|
) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
|
|
1305
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
1564
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
1565
|
+
trigger: "append",
|
|
1566
|
+
specId
|
|
1567
|
+
});
|
|
1568
|
+
trackSpecStage({
|
|
1569
|
+
projectRoot: p.path,
|
|
1570
|
+
store: p.store,
|
|
1571
|
+
specId,
|
|
1572
|
+
handle,
|
|
1573
|
+
before: detail,
|
|
1574
|
+
trigger: "append"
|
|
1575
|
+
});
|
|
1306
1576
|
return c.json({ ok: true, runId: handle.runId, sessionId });
|
|
1307
1577
|
}
|
|
1308
1578
|
return c.json({ ok: true });
|
|
@@ -1316,7 +1586,18 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1316
1586
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
1317
1587
|
const debugActive = await readDebugMdStatus(join(p.specsDir, specId)) === "debugging";
|
|
1318
1588
|
const prompt = debugActive ? buildDebugPrompt(p.specsDirRelative, specId, "resume", await buildDebugRuntimeContext(p)) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
|
|
1319
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
1589
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
1590
|
+
trigger: "run",
|
|
1591
|
+
specId
|
|
1592
|
+
});
|
|
1593
|
+
trackSpecStage({
|
|
1594
|
+
projectRoot: p.path,
|
|
1595
|
+
store: p.store,
|
|
1596
|
+
specId,
|
|
1597
|
+
handle,
|
|
1598
|
+
before: detail,
|
|
1599
|
+
trigger: "run"
|
|
1600
|
+
});
|
|
1320
1601
|
return c.json({ runId: handle.runId, sessionId });
|
|
1321
1602
|
});
|
|
1322
1603
|
app.post("/projects/:projectId/specs/:id/explain", async (c) => {
|
|
@@ -1347,7 +1628,10 @@ ${text}
|
|
|
1347
1628
|
"""
|
|
1348
1629
|
`;
|
|
1349
1630
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
1350
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
1631
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
1632
|
+
trigger: "explain",
|
|
1633
|
+
specId
|
|
1634
|
+
});
|
|
1351
1635
|
return c.json({ runId: handle.runId, sessionId });
|
|
1352
1636
|
});
|
|
1353
1637
|
app.delete("/projects/:projectId/specs/:id", async (c) => {
|
|
@@ -1604,25 +1888,6 @@ function parseAnnotateBody(body) {
|
|
|
1604
1888
|
if (note.length > 2e3) return { error: "note too long (max 2000)" };
|
|
1605
1889
|
return { sectionPath, quote, note };
|
|
1606
1890
|
}
|
|
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
|
-
}
|
|
1626
1891
|
const CHAT_DEBUG_DIR_REL = ".yorz/tmp/debug";
|
|
1627
1892
|
const CHAT_DEBUG_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1628
1893
|
const CHAT_DEBUG_CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -1690,6 +1955,113 @@ function formatDebugTimestamp(date) {
|
|
|
1690
1955
|
pad(date.getSeconds())
|
|
1691
1956
|
].join("");
|
|
1692
1957
|
}
|
|
1958
|
+
const CURRENT_VERSION = 1;
|
|
1959
|
+
const DEFAULT_SPECS_DIR = ".yorz/specs";
|
|
1960
|
+
function defaultProjectConfig() {
|
|
1961
|
+
return {
|
|
1962
|
+
version: CURRENT_VERSION,
|
|
1963
|
+
agent: { kind: "inherit" },
|
|
1964
|
+
specsDir: DEFAULT_SPECS_DIR,
|
|
1965
|
+
commands: [],
|
|
1966
|
+
customInstructions: []
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
function configPath(cwd) {
|
|
1970
|
+
return join(cwd, ".yorz", "config.json");
|
|
1971
|
+
}
|
|
1972
|
+
async function loadProjectConfig(cwd) {
|
|
1973
|
+
const fp = configPath(cwd);
|
|
1974
|
+
if (!existsSync(fp)) return defaultProjectConfig();
|
|
1975
|
+
let raw;
|
|
1976
|
+
try {
|
|
1977
|
+
raw = await readFile(fp, "utf8");
|
|
1978
|
+
} catch {
|
|
1979
|
+
return defaultProjectConfig();
|
|
1980
|
+
}
|
|
1981
|
+
if (!raw.trim()) return defaultProjectConfig();
|
|
1982
|
+
let parsed;
|
|
1983
|
+
try {
|
|
1984
|
+
parsed = JSON.parse(raw);
|
|
1985
|
+
} catch {
|
|
1986
|
+
return defaultProjectConfig();
|
|
1987
|
+
}
|
|
1988
|
+
return normalizeConfig(parsed);
|
|
1989
|
+
}
|
|
1990
|
+
async function saveProjectConfig(cwd, cfg) {
|
|
1991
|
+
const fp = configPath(cwd);
|
|
1992
|
+
await mkdir(dirname(fp), { recursive: true });
|
|
1993
|
+
const normalized = normalizeConfig(cfg);
|
|
1994
|
+
const body = `${JSON.stringify(normalized, null, 2)}
|
|
1995
|
+
`;
|
|
1996
|
+
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
1997
|
+
await writeFile(tmp, body, "utf8");
|
|
1998
|
+
await rename(tmp, fp);
|
|
1999
|
+
}
|
|
2000
|
+
function resolveSpecsDir(cwd, cfg) {
|
|
2001
|
+
const root = resolve(cwd);
|
|
2002
|
+
const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
|
|
2003
|
+
const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
|
|
2004
|
+
const rel = relative(root, abs);
|
|
2005
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
2006
|
+
throw new Error(`specsDir must stay within the project root: ${raw}`);
|
|
2007
|
+
}
|
|
2008
|
+
return abs;
|
|
2009
|
+
}
|
|
2010
|
+
async function ensureSpecsDirExists(absSpecsDir) {
|
|
2011
|
+
await mkdir(absSpecsDir, { recursive: true });
|
|
2012
|
+
}
|
|
2013
|
+
function normalizeConfig(value) {
|
|
2014
|
+
if (!value || typeof value !== "object") return defaultProjectConfig();
|
|
2015
|
+
const obj = value;
|
|
2016
|
+
const agent = normalizeAgent(obj.agent);
|
|
2017
|
+
const specsDir = normalizeSpecsDir(obj.specsDir);
|
|
2018
|
+
const commands = normalizeCommands(obj.commands);
|
|
2019
|
+
const customInstructions = normalizeCustomInstructions(obj.customInstructions);
|
|
2020
|
+
return { version: CURRENT_VERSION, agent, specsDir, commands, customInstructions };
|
|
2021
|
+
}
|
|
2022
|
+
function normalizeCommands(value) {
|
|
2023
|
+
if (!Array.isArray(value)) return [];
|
|
2024
|
+
const out = [];
|
|
2025
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2026
|
+
for (const item of value) {
|
|
2027
|
+
if (!item || typeof item !== "object") continue;
|
|
2028
|
+
const obj = item;
|
|
2029
|
+
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
2030
|
+
const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
|
|
2031
|
+
const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
|
|
2032
|
+
if (!id || !name2 || !cli || seen.has(id)) continue;
|
|
2033
|
+
seen.add(id);
|
|
2034
|
+
const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
|
|
2035
|
+
out.push({ id, name: name2, cli, createdAt });
|
|
2036
|
+
}
|
|
2037
|
+
return out;
|
|
2038
|
+
}
|
|
2039
|
+
function normalizeAgent(value) {
|
|
2040
|
+
if (typeof value === "string") {
|
|
2041
|
+
if (value === "opencode") return { kind: "opencode" };
|
|
2042
|
+
if (value === "codex") return { kind: "codex" };
|
|
2043
|
+
return { kind: "claude" };
|
|
2044
|
+
}
|
|
2045
|
+
if (!value || typeof value !== "object") return { kind: "claude" };
|
|
2046
|
+
const obj = value;
|
|
2047
|
+
const kind = obj.kind;
|
|
2048
|
+
if (kind === "inherit") return { kind: "inherit" };
|
|
2049
|
+
if (kind === "opencode") return { kind: "opencode" };
|
|
2050
|
+
if (kind === "codex") return { kind: "codex" };
|
|
2051
|
+
if (kind === "custom") {
|
|
2052
|
+
const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
|
|
2053
|
+
if (!cmd) return { kind: "claude" };
|
|
2054
|
+
const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
|
|
2055
|
+
return { kind: "custom", cmd, args };
|
|
2056
|
+
}
|
|
2057
|
+
return { kind: "claude" };
|
|
2058
|
+
}
|
|
2059
|
+
function normalizeSpecsDir(value) {
|
|
2060
|
+
if (typeof value !== "string") return DEFAULT_SPECS_DIR;
|
|
2061
|
+
const trimmed = value.trim();
|
|
2062
|
+
if (!trimmed) return DEFAULT_SPECS_DIR;
|
|
2063
|
+
return trimmed;
|
|
2064
|
+
}
|
|
1693
2065
|
const SLASH_NAME_RE = /^\/([\w-]+)(?:\s|$)/;
|
|
1694
2066
|
const SPEC_COMMAND_RE = /^\/yorz-spec(?:\s|$)/;
|
|
1695
2067
|
function isSlashCommand(prompt) {
|
|
@@ -1739,6 +2111,13 @@ function resolveChatPrompt(prompt, instructions, opts = {}) {
|
|
|
1739
2111
|
}
|
|
1740
2112
|
return { prompt: buildUnknownCommandPrompt(original, hit), builtin: null };
|
|
1741
2113
|
}
|
|
2114
|
+
async function loadCustomInstructions(projectPath) {
|
|
2115
|
+
const [project, global] = await Promise.all([
|
|
2116
|
+
loadProjectConfig(projectPath).then((cfg) => cfg.customInstructions).catch(() => []),
|
|
2117
|
+
loadGlobalConfig().then((cfg) => cfg.customInstructions).catch(() => [])
|
|
2118
|
+
]);
|
|
2119
|
+
return mergeCustomInstructions(project, global);
|
|
2120
|
+
}
|
|
1742
2121
|
const KINDS = ["claude", "codex", "opencode"];
|
|
1743
2122
|
const DRAFT_ID_RE = /^[a-zA-Z0-9-]{1,64}$/;
|
|
1744
2123
|
function buildChatPrompt(prompt, draftId, attachments) {
|
|
@@ -1827,7 +2206,7 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1827
2206
|
}
|
|
1828
2207
|
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
1829
2208
|
if (!prompt) return c.json({ error: "prompt required" }, 400);
|
|
1830
|
-
const instructions = isSlashCommand(prompt) ? await
|
|
2209
|
+
const instructions = isSlashCommand(prompt) ? await loadCustomInstructions(p.path) : [];
|
|
1831
2210
|
const resolved = resolveChatPrompt(prompt, instructions, {
|
|
1832
2211
|
specsDirRelative: p.specsDirRelative
|
|
1833
2212
|
});
|
|
@@ -1844,7 +2223,9 @@ function createSessionsRoutes(resolveProject) {
|
|
|
1844
2223
|
}
|
|
1845
2224
|
if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
|
|
1846
2225
|
});
|
|
1847
|
-
const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt
|
|
2226
|
+
const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt, {
|
|
2227
|
+
trigger: "chat"
|
|
2228
|
+
});
|
|
1848
2229
|
return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
|
|
1849
2230
|
});
|
|
1850
2231
|
app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
|
|
@@ -1924,16 +2305,27 @@ function assertSafeRelativePath(cwd, p) {
|
|
|
1924
2305
|
}
|
|
1925
2306
|
return p;
|
|
1926
2307
|
}
|
|
2308
|
+
function recordGitOp(cwd, args, startedAt, ok, code) {
|
|
2309
|
+
getTelemetry(cwd).record("git.op", {
|
|
2310
|
+
op: args[0] ?? "unknown",
|
|
2311
|
+
ok,
|
|
2312
|
+
exitCode: code,
|
|
2313
|
+
durMs: Date.now() - startedAt
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
1927
2316
|
async function runGit(cwd, args) {
|
|
2317
|
+
const startedAt = Date.now();
|
|
1928
2318
|
try {
|
|
1929
2319
|
const result = await execFileWithoutWindow("git", args, {
|
|
1930
2320
|
cwd,
|
|
1931
2321
|
encoding: "utf8",
|
|
1932
2322
|
maxBuffer: 16 * 1024 * 1024
|
|
1933
2323
|
});
|
|
2324
|
+
recordGitOp(cwd, args, startedAt, true, 0);
|
|
1934
2325
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
1935
2326
|
} catch (err) {
|
|
1936
2327
|
const e = err;
|
|
2328
|
+
recordGitOp(cwd, args, startedAt, false, typeof e.code === "number" ? e.code : void 0);
|
|
1937
2329
|
if (e.code === "ENOENT") {
|
|
1938
2330
|
throw new GitError("git_missing", "git not found on PATH");
|
|
1939
2331
|
}
|
|
@@ -1944,19 +2336,23 @@ async function runGitChecked(cwd, args) {
|
|
|
1944
2336
|
return runGit(cwd, args);
|
|
1945
2337
|
}
|
|
1946
2338
|
async function runGitRaw(cwd, args) {
|
|
2339
|
+
const startedAt = Date.now();
|
|
1947
2340
|
try {
|
|
1948
2341
|
const result = await execFileWithoutWindow("git", args, {
|
|
1949
2342
|
cwd,
|
|
1950
2343
|
encoding: "utf8",
|
|
1951
2344
|
maxBuffer: 16 * 1024 * 1024
|
|
1952
2345
|
});
|
|
2346
|
+
recordGitOp(cwd, args, startedAt, true, 0);
|
|
1953
2347
|
return { stdout: result.stdout, stderr: result.stderr, code: 0 };
|
|
1954
2348
|
} catch (err) {
|
|
1955
2349
|
const e = err;
|
|
1956
2350
|
if (e.code === "ENOENT") {
|
|
2351
|
+
recordGitOp(cwd, args, startedAt, false);
|
|
1957
2352
|
throw new GitError("git_missing", "git not found on PATH");
|
|
1958
2353
|
}
|
|
1959
2354
|
const code = typeof e.code === "number" ? e.code : 1;
|
|
2355
|
+
recordGitOp(cwd, args, startedAt, false, code);
|
|
1960
2356
|
return { stdout: e.stdout ?? "", stderr: e.stderr ?? e.message ?? "", code };
|
|
1961
2357
|
}
|
|
1962
2358
|
}
|
|
@@ -2018,7 +2414,7 @@ async function commit(cwd, opts) {
|
|
|
2018
2414
|
const extraPaths = renamed.map((r) => r.renamedFrom);
|
|
2019
2415
|
const allPaths = [...opts.paths, ...extraPaths];
|
|
2020
2416
|
await runGit(cwd, ["add", "--", ...allPaths]);
|
|
2021
|
-
await runGit(cwd, ["commit", "-m", message
|
|
2417
|
+
await runGit(cwd, ["commit", "-m", message]);
|
|
2022
2418
|
const { stdout } = await runGit(cwd, ["rev-parse", "HEAD"]);
|
|
2023
2419
|
return { commit: stdout.trim() };
|
|
2024
2420
|
}
|
|
@@ -2040,6 +2436,114 @@ async function discard(cwd, opts) {
|
|
|
2040
2436
|
await runGit(cwd, ["clean", "-fd", "--", r.path]);
|
|
2041
2437
|
}
|
|
2042
2438
|
}
|
|
2439
|
+
const MAX_PATCH_BYTES = 512 * 1024;
|
|
2440
|
+
const MAX_PATCH_LINES = 3e3;
|
|
2441
|
+
function truncatePatch(patch) {
|
|
2442
|
+
let text = patch;
|
|
2443
|
+
let truncated = false;
|
|
2444
|
+
if (Buffer.byteLength(text, "utf8") > MAX_PATCH_BYTES) {
|
|
2445
|
+
text = text.slice(0, MAX_PATCH_BYTES);
|
|
2446
|
+
truncated = true;
|
|
2447
|
+
}
|
|
2448
|
+
const lines = text.split("\n");
|
|
2449
|
+
if (lines.length > MAX_PATCH_LINES) {
|
|
2450
|
+
text = lines.slice(0, MAX_PATCH_LINES).join("\n");
|
|
2451
|
+
truncated = true;
|
|
2452
|
+
}
|
|
2453
|
+
return { patch: text, truncated };
|
|
2454
|
+
}
|
|
2455
|
+
async function untrackedDiff(cwd, path) {
|
|
2456
|
+
let buf;
|
|
2457
|
+
try {
|
|
2458
|
+
buf = await readFile(resolve(cwd, path));
|
|
2459
|
+
} catch {
|
|
2460
|
+
return { path, patch: "", binary: false, truncated: false };
|
|
2461
|
+
}
|
|
2462
|
+
if (buf.subarray(0, 8e3).includes(0)) {
|
|
2463
|
+
return { path, patch: "", binary: true, truncated: false };
|
|
2464
|
+
}
|
|
2465
|
+
const text = buf.toString("utf8");
|
|
2466
|
+
const lines = text.length === 0 ? [] : text.replace(/\n$/, "").split("\n");
|
|
2467
|
+
const body = lines.map((l) => `+${l}`).join("\n");
|
|
2468
|
+
const header = `--- /dev/null
|
|
2469
|
+
+++ b/${path}
|
|
2470
|
+
@@ -0,0 +1,${lines.length} @@
|
|
2471
|
+
`;
|
|
2472
|
+
const { patch, truncated } = truncatePatch(lines.length ? `${header}${body}` : header);
|
|
2473
|
+
return { path, patch, binary: false, truncated };
|
|
2474
|
+
}
|
|
2475
|
+
async function fileDiff(cwd, path) {
|
|
2476
|
+
assertSafeRelativePath(cwd, path);
|
|
2477
|
+
const change = (await listChanges(cwd)).find((c) => c.path === path);
|
|
2478
|
+
if (change?.status === "??") return untrackedDiff(cwd, path);
|
|
2479
|
+
const pathArgs = change?.renamedFrom ? [change.renamedFrom, path] : [path];
|
|
2480
|
+
const res = await runGitRaw(cwd, ["diff", "HEAD", "--", ...pathArgs]);
|
|
2481
|
+
if (res.code !== 0 && !res.stdout) {
|
|
2482
|
+
throw new GitError("git_failed", res.stderr.trim() || "git diff failed", res.stderr);
|
|
2483
|
+
}
|
|
2484
|
+
const raw = res.stdout;
|
|
2485
|
+
if (!raw.includes("@@") && /^Binary files .* differ$/m.test(raw)) {
|
|
2486
|
+
return { path, patch: "", binary: true, truncated: false };
|
|
2487
|
+
}
|
|
2488
|
+
const { patch, truncated } = truncatePatch(raw);
|
|
2489
|
+
return { path, patch, binary: false, truncated };
|
|
2490
|
+
}
|
|
2491
|
+
async function currentBranch(cwd) {
|
|
2492
|
+
const { stdout } = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2493
|
+
const branch = stdout.trim();
|
|
2494
|
+
if (!branch || branch === "HEAD") {
|
|
2495
|
+
throw new GitError("detached_head", "cannot operate from a detached HEAD");
|
|
2496
|
+
}
|
|
2497
|
+
return branch;
|
|
2498
|
+
}
|
|
2499
|
+
async function listBranches(cwd) {
|
|
2500
|
+
const [current, branchesResult] = await Promise.all([
|
|
2501
|
+
currentBranch(cwd),
|
|
2502
|
+
runGit(cwd, ["branch", "--format=%(refname:short)"])
|
|
2503
|
+
]);
|
|
2504
|
+
const branches = branchesResult.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2505
|
+
return { current, branches };
|
|
2506
|
+
}
|
|
2507
|
+
async function checkoutBranch(cwd, branch) {
|
|
2508
|
+
const target = branch?.trim() ?? "";
|
|
2509
|
+
if (!target) throw new GitError("invalid_branch", "branch must not be empty");
|
|
2510
|
+
const state = await listBranches(cwd);
|
|
2511
|
+
if (!state.branches.includes(target)) {
|
|
2512
|
+
throw new GitError("invalid_branch", `unknown local branch: ${target}`);
|
|
2513
|
+
}
|
|
2514
|
+
if (state.current === target) return { current: state.current };
|
|
2515
|
+
const res = await runGitRaw(cwd, ["checkout", target]);
|
|
2516
|
+
if (res.code !== 0) {
|
|
2517
|
+
throw new GitError("checkout_failed", res.stderr.trim() || "git checkout failed", res.stderr);
|
|
2518
|
+
}
|
|
2519
|
+
return { current: await currentBranch(cwd) };
|
|
2520
|
+
}
|
|
2521
|
+
async function push(cwd) {
|
|
2522
|
+
const branch = await currentBranch(cwd);
|
|
2523
|
+
const upstream = await runGitRaw(cwd, [
|
|
2524
|
+
"rev-parse",
|
|
2525
|
+
"--abbrev-ref",
|
|
2526
|
+
"--symbolic-full-name",
|
|
2527
|
+
"@{u}"
|
|
2528
|
+
]);
|
|
2529
|
+
const hasUpstream = upstream.code === 0 && upstream.stdout.trim().length > 0;
|
|
2530
|
+
const args = hasUpstream ? ["push"] : ["push", "--set-upstream", "origin", branch];
|
|
2531
|
+
const res = await runGitRaw(cwd, args);
|
|
2532
|
+
if (res.code !== 0) {
|
|
2533
|
+
throw new GitError("push_failed", res.stderr.trim() || "git push failed", res.stderr);
|
|
2534
|
+
}
|
|
2535
|
+
return { branch, createdUpstream: !hasUpstream };
|
|
2536
|
+
}
|
|
2537
|
+
async function pull(cwd) {
|
|
2538
|
+
const branch = await currentBranch(cwd);
|
|
2539
|
+
const before = (await runGit(cwd, ["rev-parse", "HEAD"])).stdout.trim();
|
|
2540
|
+
const res = await runGitRaw(cwd, ["pull", "--ff-only"]);
|
|
2541
|
+
if (res.code !== 0) {
|
|
2542
|
+
throw new GitError("pull_failed", res.stderr.trim() || "git pull failed", res.stderr);
|
|
2543
|
+
}
|
|
2544
|
+
const after = (await runGit(cwd, ["rev-parse", "HEAD"])).stdout.trim();
|
|
2545
|
+
return { branch, updated: before !== after };
|
|
2546
|
+
}
|
|
2043
2547
|
async function stash(cwd, opts) {
|
|
2044
2548
|
if (!Array.isArray(opts.paths) || opts.paths.length === 0) {
|
|
2045
2549
|
throw new GitError("no_paths", "stash requires at least one path");
|
|
@@ -2080,7 +2584,18 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2080
2584
|
const specRel = `${p.specsDirRelative}/${specId}/spec.md`;
|
|
2081
2585
|
const prompt = buildGitOpsPrompt(action, specId, specRel);
|
|
2082
2586
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
2083
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
2587
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
2588
|
+
trigger: "git-ops",
|
|
2589
|
+
specId
|
|
2590
|
+
});
|
|
2591
|
+
trackSpecStage({
|
|
2592
|
+
projectRoot: p.path,
|
|
2593
|
+
store: p.store,
|
|
2594
|
+
specId,
|
|
2595
|
+
handle,
|
|
2596
|
+
before: detail,
|
|
2597
|
+
trigger: "git-ops"
|
|
2598
|
+
});
|
|
2084
2599
|
return c.json({ runId: handle.runId, sessionId });
|
|
2085
2600
|
});
|
|
2086
2601
|
app.get("/projects/:projectId/specs/:id/debug", async (c) => {
|
|
@@ -2098,68 +2613,6 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2098
2613
|
return c.json({ exists: false, text: "" });
|
|
2099
2614
|
}
|
|
2100
2615
|
});
|
|
2101
|
-
app.get("/projects/:projectId/specs/:id/changes", async (c) => {
|
|
2102
|
-
const p = await need(c);
|
|
2103
|
-
if (p instanceof Response) return p;
|
|
2104
|
-
const specId = c.req.param("id");
|
|
2105
|
-
const detail = await p.store.read(specId);
|
|
2106
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2107
|
-
try {
|
|
2108
|
-
const changes = await listChanges(p.path);
|
|
2109
|
-
return c.json({ changes });
|
|
2110
|
-
} catch (err) {
|
|
2111
|
-
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2112
|
-
throw err;
|
|
2113
|
-
}
|
|
2114
|
-
});
|
|
2115
|
-
app.post("/projects/:projectId/specs/:id/commit", async (c) => {
|
|
2116
|
-
const p = await need(c);
|
|
2117
|
-
if (p instanceof Response) return p;
|
|
2118
|
-
const specId = c.req.param("id");
|
|
2119
|
-
const detail = await p.store.read(specId);
|
|
2120
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2121
|
-
let body;
|
|
2122
|
-
try {
|
|
2123
|
-
body = await c.req.json();
|
|
2124
|
-
} catch {
|
|
2125
|
-
return c.json({ error: "invalid JSON body" }, 400);
|
|
2126
|
-
}
|
|
2127
|
-
const obj = body && typeof body === "object" ? body : {};
|
|
2128
|
-
const message = typeof obj.message === "string" ? obj.message.trim() : "";
|
|
2129
|
-
const paths = Array.isArray(obj.paths) ? obj.paths.filter((v) => typeof v === "string") : [];
|
|
2130
|
-
if (!message) return c.json({ error: "message must not be empty" }, 400);
|
|
2131
|
-
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2132
|
-
try {
|
|
2133
|
-
const result = await commit(p.path, { message, paths });
|
|
2134
|
-
return c.json(result);
|
|
2135
|
-
} catch (err) {
|
|
2136
|
-
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2137
|
-
throw err;
|
|
2138
|
-
}
|
|
2139
|
-
});
|
|
2140
|
-
app.post("/projects/:projectId/specs/:id/discard", async (c) => {
|
|
2141
|
-
const p = await need(c);
|
|
2142
|
-
if (p instanceof Response) return p;
|
|
2143
|
-
const specId = c.req.param("id");
|
|
2144
|
-
const detail = await p.store.read(specId);
|
|
2145
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2146
|
-
let body;
|
|
2147
|
-
try {
|
|
2148
|
-
body = await c.req.json();
|
|
2149
|
-
} catch {
|
|
2150
|
-
return c.json({ error: "invalid JSON body" }, 400);
|
|
2151
|
-
}
|
|
2152
|
-
const obj = body && typeof body === "object" ? body : {};
|
|
2153
|
-
const paths = Array.isArray(obj.paths) ? obj.paths.filter((v) => typeof v === "string") : [];
|
|
2154
|
-
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2155
|
-
try {
|
|
2156
|
-
await discard(p.path, { paths });
|
|
2157
|
-
return c.json({ ok: true });
|
|
2158
|
-
} catch (err) {
|
|
2159
|
-
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2160
|
-
throw err;
|
|
2161
|
-
}
|
|
2162
|
-
});
|
|
2163
2616
|
app.post("/projects/:projectId/specs/:id/stash", async (c) => {
|
|
2164
2617
|
const p = await need(c);
|
|
2165
2618
|
if (p instanceof Response) return p;
|
|
@@ -2204,6 +2657,126 @@ function buildGitOpsPrompt(action, specId, specRel) {
|
|
|
2204
2657
|
`;
|
|
2205
2658
|
}
|
|
2206
2659
|
}
|
|
2660
|
+
function createGitRoutes(resolveProject) {
|
|
2661
|
+
const app = new Hono();
|
|
2662
|
+
const need = async (c) => {
|
|
2663
|
+
const id = c.req.param("projectId") ?? "";
|
|
2664
|
+
if (!id) return c.json({ error: "projectId required" }, 400);
|
|
2665
|
+
const project = await resolveProject(id);
|
|
2666
|
+
if (!project) return c.json({ error: "project not found" }, 404);
|
|
2667
|
+
return project;
|
|
2668
|
+
};
|
|
2669
|
+
const readBody = async (c) => {
|
|
2670
|
+
try {
|
|
2671
|
+
const body = await c.req.json();
|
|
2672
|
+
return body && typeof body === "object" ? body : {};
|
|
2673
|
+
} catch {
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
const toPaths = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
2678
|
+
app.get("/projects/:projectId/git/changes", async (c) => {
|
|
2679
|
+
const p = await need(c);
|
|
2680
|
+
if (p instanceof Response) return p;
|
|
2681
|
+
try {
|
|
2682
|
+
return c.json({ changes: await listChanges(p.path) });
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2685
|
+
throw err;
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
app.get("/projects/:projectId/git/diff", async (c) => {
|
|
2689
|
+
const p = await need(c);
|
|
2690
|
+
if (p instanceof Response) return p;
|
|
2691
|
+
const path = c.req.query("path") ?? "";
|
|
2692
|
+
if (!path) return c.json({ error: "path required" }, 400);
|
|
2693
|
+
try {
|
|
2694
|
+
return c.json(await fileDiff(p.path, path));
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2697
|
+
throw err;
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
app.get("/projects/:projectId/git/branches", async (c) => {
|
|
2701
|
+
const p = await need(c);
|
|
2702
|
+
if (p instanceof Response) return p;
|
|
2703
|
+
try {
|
|
2704
|
+
return c.json(await listBranches(p.path));
|
|
2705
|
+
} catch (err) {
|
|
2706
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2707
|
+
throw err;
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
app.post("/projects/:projectId/git/checkout", async (c) => {
|
|
2711
|
+
const p = await need(c);
|
|
2712
|
+
if (p instanceof Response) return p;
|
|
2713
|
+
const body = await readBody(c);
|
|
2714
|
+
if (!body) return c.json({ error: "invalid JSON body" }, 400);
|
|
2715
|
+
const branch = typeof body.branch === "string" ? body.branch.trim() : "";
|
|
2716
|
+
if (!branch) return c.json({ error: "branch must not be empty" }, 400);
|
|
2717
|
+
try {
|
|
2718
|
+
const result = await checkoutBranch(p.path, branch);
|
|
2719
|
+
return c.json({ ok: true, ...result });
|
|
2720
|
+
} catch (err) {
|
|
2721
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2722
|
+
throw err;
|
|
2723
|
+
}
|
|
2724
|
+
});
|
|
2725
|
+
app.post("/projects/:projectId/git/commit", async (c) => {
|
|
2726
|
+
const p = await need(c);
|
|
2727
|
+
if (p instanceof Response) return p;
|
|
2728
|
+
const body = await readBody(c);
|
|
2729
|
+
if (!body) return c.json({ error: "invalid JSON body" }, 400);
|
|
2730
|
+
const message = typeof body.message === "string" ? body.message.trim() : "";
|
|
2731
|
+
const paths = toPaths(body.paths);
|
|
2732
|
+
if (!message) return c.json({ error: "message must not be empty" }, 400);
|
|
2733
|
+
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2734
|
+
try {
|
|
2735
|
+
return c.json(await commit(p.path, { message, paths }));
|
|
2736
|
+
} catch (err) {
|
|
2737
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2738
|
+
throw err;
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
app.post("/projects/:projectId/git/discard", async (c) => {
|
|
2742
|
+
const p = await need(c);
|
|
2743
|
+
if (p instanceof Response) return p;
|
|
2744
|
+
const body = await readBody(c);
|
|
2745
|
+
if (!body) return c.json({ error: "invalid JSON body" }, 400);
|
|
2746
|
+
const paths = toPaths(body.paths);
|
|
2747
|
+
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2748
|
+
try {
|
|
2749
|
+
await discard(p.path, { paths });
|
|
2750
|
+
return c.json({ ok: true });
|
|
2751
|
+
} catch (err) {
|
|
2752
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2753
|
+
throw err;
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
app.post("/projects/:projectId/git/push", async (c) => {
|
|
2757
|
+
const p = await need(c);
|
|
2758
|
+
if (p instanceof Response) return p;
|
|
2759
|
+
try {
|
|
2760
|
+
const result = await push(p.path);
|
|
2761
|
+
return c.json({ ok: true, ...result });
|
|
2762
|
+
} catch (err) {
|
|
2763
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2764
|
+
throw err;
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
app.post("/projects/:projectId/git/pull", async (c) => {
|
|
2768
|
+
const p = await need(c);
|
|
2769
|
+
if (p instanceof Response) return p;
|
|
2770
|
+
try {
|
|
2771
|
+
const result = await pull(p.path);
|
|
2772
|
+
return c.json({ ok: true, ...result });
|
|
2773
|
+
} catch (err) {
|
|
2774
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2775
|
+
throw err;
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
return app;
|
|
2779
|
+
}
|
|
2207
2780
|
const sseLog = () => getLogger().child("sse");
|
|
2208
2781
|
const HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2209
2782
|
function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
@@ -2395,9 +2968,8 @@ class EventsHub {
|
|
|
2395
2968
|
if (rest === "commands") return this.attachCommandRuns(s, topic, project);
|
|
2396
2969
|
const cm = /^command:(.+)$/.exec(rest);
|
|
2397
2970
|
if (cm) return this.attachCommandRun(s, topic, project, cm[1]);
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
sm = /^spec:([^:]+)$/.exec(rest);
|
|
2971
|
+
if (rest === "changes") return this.attachProjectChanges(s, topic, project);
|
|
2972
|
+
let sm = /^spec:([^:]+)$/.exec(rest);
|
|
2401
2973
|
if (sm) return this.attachSpec(s, topic, project, sm[1]);
|
|
2402
2974
|
sm = /^session:(.+)$/.exec(rest);
|
|
2403
2975
|
if (sm) return this.attachSession(s, topic, project, sm[1]);
|
|
@@ -2435,9 +3007,12 @@ class EventsHub {
|
|
|
2435
3007
|
this.emit(s, topic, "updated", { type: kind, mtime });
|
|
2436
3008
|
});
|
|
2437
3009
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
3010
|
+
/**
|
|
3011
|
+
* Git changes are repository-wide: the watcher is keyed on the project path,
|
|
3012
|
+
* so this topic carries no spec id and both the standalone Git page and the
|
|
3013
|
+
* spec Review page share one subscription.
|
|
3014
|
+
*/
|
|
3015
|
+
attachProjectChanges(s, topic, project) {
|
|
2441
3016
|
this.emit(s, topic, "ready", {});
|
|
2442
3017
|
const { current, unsubscribe } = subscribeGitChanges(project.path, (changes) => {
|
|
2443
3018
|
this.emit(s, topic, "changes-updated", { changes });
|
|
@@ -2593,111 +3168,6 @@ function createProjectRoutes(registry, worktreeManager) {
|
|
|
2593
3168
|
});
|
|
2594
3169
|
return app;
|
|
2595
3170
|
}
|
|
2596
|
-
const CURRENT_VERSION = 1;
|
|
2597
|
-
const DEFAULT_SPECS_DIR = ".yorz/specs";
|
|
2598
|
-
function defaultProjectConfig() {
|
|
2599
|
-
return {
|
|
2600
|
-
version: CURRENT_VERSION,
|
|
2601
|
-
agent: { kind: "inherit" },
|
|
2602
|
-
specsDir: DEFAULT_SPECS_DIR,
|
|
2603
|
-
commands: []
|
|
2604
|
-
};
|
|
2605
|
-
}
|
|
2606
|
-
function configPath(cwd) {
|
|
2607
|
-
return join(cwd, ".yorz", "config.json");
|
|
2608
|
-
}
|
|
2609
|
-
async function loadProjectConfig(cwd) {
|
|
2610
|
-
const fp = configPath(cwd);
|
|
2611
|
-
if (!existsSync(fp)) return defaultProjectConfig();
|
|
2612
|
-
let raw;
|
|
2613
|
-
try {
|
|
2614
|
-
raw = await readFile(fp, "utf8");
|
|
2615
|
-
} catch {
|
|
2616
|
-
return defaultProjectConfig();
|
|
2617
|
-
}
|
|
2618
|
-
if (!raw.trim()) return defaultProjectConfig();
|
|
2619
|
-
let parsed;
|
|
2620
|
-
try {
|
|
2621
|
-
parsed = JSON.parse(raw);
|
|
2622
|
-
} catch {
|
|
2623
|
-
return defaultProjectConfig();
|
|
2624
|
-
}
|
|
2625
|
-
return normalizeConfig(parsed);
|
|
2626
|
-
}
|
|
2627
|
-
async function saveProjectConfig(cwd, cfg) {
|
|
2628
|
-
const fp = configPath(cwd);
|
|
2629
|
-
await mkdir(dirname(fp), { recursive: true });
|
|
2630
|
-
const normalized = normalizeConfig(cfg);
|
|
2631
|
-
const body = `${JSON.stringify(normalized, null, 2)}
|
|
2632
|
-
`;
|
|
2633
|
-
const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
2634
|
-
await writeFile(tmp, body, "utf8");
|
|
2635
|
-
await rename(tmp, fp);
|
|
2636
|
-
}
|
|
2637
|
-
function resolveSpecsDir(cwd, cfg) {
|
|
2638
|
-
const root = resolve(cwd);
|
|
2639
|
-
const raw = (cfg.specsDir || DEFAULT_SPECS_DIR).trim() || DEFAULT_SPECS_DIR;
|
|
2640
|
-
const abs = isAbsolute(raw) ? resolve(raw) : resolve(root, raw);
|
|
2641
|
-
const rel = relative(root, abs);
|
|
2642
|
-
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
2643
|
-
throw new Error(`specsDir must stay within the project root: ${raw}`);
|
|
2644
|
-
}
|
|
2645
|
-
return abs;
|
|
2646
|
-
}
|
|
2647
|
-
async function ensureSpecsDirExists(absSpecsDir) {
|
|
2648
|
-
await mkdir(absSpecsDir, { recursive: true });
|
|
2649
|
-
}
|
|
2650
|
-
function normalizeConfig(value) {
|
|
2651
|
-
if (!value || typeof value !== "object") return defaultProjectConfig();
|
|
2652
|
-
const obj = value;
|
|
2653
|
-
const agent = normalizeAgent(obj.agent);
|
|
2654
|
-
const specsDir = normalizeSpecsDir(obj.specsDir);
|
|
2655
|
-
const commands = normalizeCommands(obj.commands);
|
|
2656
|
-
return { version: CURRENT_VERSION, agent, specsDir, commands };
|
|
2657
|
-
}
|
|
2658
|
-
function normalizeCommands(value) {
|
|
2659
|
-
if (!Array.isArray(value)) return [];
|
|
2660
|
-
const out = [];
|
|
2661
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2662
|
-
for (const item of value) {
|
|
2663
|
-
if (!item || typeof item !== "object") continue;
|
|
2664
|
-
const obj = item;
|
|
2665
|
-
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
2666
|
-
const name2 = typeof obj.name === "string" ? obj.name.trim() : "";
|
|
2667
|
-
const cli = typeof obj.cli === "string" ? obj.cli.trim() : "";
|
|
2668
|
-
if (!id || !name2 || !cli || seen.has(id)) continue;
|
|
2669
|
-
seen.add(id);
|
|
2670
|
-
const createdAt = typeof obj.createdAt === "number" && obj.createdAt > 0 ? obj.createdAt : 0;
|
|
2671
|
-
out.push({ id, name: name2, cli, createdAt });
|
|
2672
|
-
}
|
|
2673
|
-
return out;
|
|
2674
|
-
}
|
|
2675
|
-
function normalizeAgent(value) {
|
|
2676
|
-
if (typeof value === "string") {
|
|
2677
|
-
if (value === "opencode") return { kind: "opencode" };
|
|
2678
|
-
if (value === "codex") return { kind: "codex" };
|
|
2679
|
-
return { kind: "claude" };
|
|
2680
|
-
}
|
|
2681
|
-
if (!value || typeof value !== "object") return { kind: "claude" };
|
|
2682
|
-
const obj = value;
|
|
2683
|
-
const kind = obj.kind;
|
|
2684
|
-
if (kind === "inherit") return { kind: "inherit" };
|
|
2685
|
-
if (kind === "opencode") return { kind: "opencode" };
|
|
2686
|
-
if (kind === "codex") return { kind: "codex" };
|
|
2687
|
-
if (kind === "custom") {
|
|
2688
|
-
const cmd = typeof obj.cmd === "string" ? obj.cmd.trim() : "";
|
|
2689
|
-
if (!cmd) return { kind: "claude" };
|
|
2690
|
-
const args = Array.isArray(obj.args) ? obj.args.filter((a) => typeof a === "string") : [];
|
|
2691
|
-
return { kind: "custom", cmd, args };
|
|
2692
|
-
}
|
|
2693
|
-
return { kind: "claude" };
|
|
2694
|
-
}
|
|
2695
|
-
function normalizeSpecsDir(value) {
|
|
2696
|
-
if (typeof value !== "string") return DEFAULT_SPECS_DIR;
|
|
2697
|
-
const trimmed = value.trim();
|
|
2698
|
-
if (!trimmed) return DEFAULT_SPECS_DIR;
|
|
2699
|
-
return trimmed;
|
|
2700
|
-
}
|
|
2701
3171
|
function createProjectConfigRoutes(registry) {
|
|
2702
3172
|
const app = new Hono();
|
|
2703
3173
|
app.get("/projects/:projectId/config", async (c) => {
|
|
@@ -2724,7 +3194,8 @@ function createProjectConfigRoutes(registry) {
|
|
|
2724
3194
|
version: 1,
|
|
2725
3195
|
agent: parsed.agent,
|
|
2726
3196
|
specsDir: parsed.specsDir,
|
|
2727
|
-
commands: current.commands
|
|
3197
|
+
commands: current.commands,
|
|
3198
|
+
customInstructions: current.customInstructions
|
|
2728
3199
|
};
|
|
2729
3200
|
let absSpecsDir;
|
|
2730
3201
|
try {
|
|
@@ -2745,6 +3216,34 @@ function createProjectConfigRoutes(registry) {
|
|
|
2745
3216
|
await registry.reload(id);
|
|
2746
3217
|
return c.json({ ok: true, config: cfg });
|
|
2747
3218
|
});
|
|
3219
|
+
app.get("/projects/:projectId/custom-instructions", async (c) => {
|
|
3220
|
+
const id = c.req.param("projectId");
|
|
3221
|
+
const entry = await registry.findEntry(id);
|
|
3222
|
+
if (!entry) return c.json({ error: "project not found" }, 404);
|
|
3223
|
+
const cfg = await loadProjectConfig(entry.path);
|
|
3224
|
+
return c.json({ customInstructions: cfg.customInstructions });
|
|
3225
|
+
});
|
|
3226
|
+
app.put("/projects/:projectId/custom-instructions", async (c) => {
|
|
3227
|
+
const id = c.req.param("projectId");
|
|
3228
|
+
const entry = await registry.findEntry(id);
|
|
3229
|
+
if (!entry) return c.json({ error: "project not found" }, 404);
|
|
3230
|
+
let raw;
|
|
3231
|
+
try {
|
|
3232
|
+
raw = await c.req.json();
|
|
3233
|
+
} catch {
|
|
3234
|
+
return c.json({ error: "invalid JSON body" }, 400);
|
|
3235
|
+
}
|
|
3236
|
+
if (!raw || typeof raw !== "object") return c.json({ error: "body must be an object" }, 400);
|
|
3237
|
+
const parsed = parseCustomInstructions(raw.customInstructions);
|
|
3238
|
+
if ("error" in parsed) return c.json({ error: parsed.error }, 400);
|
|
3239
|
+
const current = await loadProjectConfig(entry.path);
|
|
3240
|
+
try {
|
|
3241
|
+
await saveProjectConfig(entry.path, { ...current, customInstructions: parsed });
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
return c.json({ error: err.message }, 500);
|
|
3244
|
+
}
|
|
3245
|
+
return c.json({ ok: true, customInstructions: parsed });
|
|
3246
|
+
});
|
|
2748
3247
|
return app;
|
|
2749
3248
|
}
|
|
2750
3249
|
function parseBody$1(value) {
|
|
@@ -3062,46 +3561,6 @@ function parseAppearance(value) {
|
|
|
3062
3561
|
if (!isLanguage(language)) return { error: "appearance.language must be zh-CN | en" };
|
|
3063
3562
|
return { themeMode, themeName, language };
|
|
3064
3563
|
}
|
|
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
|
-
}
|
|
3105
3564
|
function createSpecDraftsRoutes(resolveProject) {
|
|
3106
3565
|
const app = new Hono();
|
|
3107
3566
|
const need = async (c) => {
|
|
@@ -3944,6 +4403,15 @@ class CommandManager {
|
|
|
3944
4403
|
this.notifyRuns();
|
|
3945
4404
|
this.notifyRun(next);
|
|
3946
4405
|
log$3().info("command ended", { runId, status, exitCode, signal });
|
|
4406
|
+
getTelemetry(this.projectPath).record("cmd.exec", {
|
|
4407
|
+
commandId: next.commandId,
|
|
4408
|
+
name: next.name,
|
|
4409
|
+
runId,
|
|
4410
|
+
status,
|
|
4411
|
+
exitCode: exitCode ?? void 0,
|
|
4412
|
+
signal: signal ?? void 0,
|
|
4413
|
+
durMs: (next.endedAt ?? Date.now()) - next.startedAt
|
|
4414
|
+
});
|
|
3947
4415
|
}
|
|
3948
4416
|
notifyRuns() {
|
|
3949
4417
|
if (this.runsSubscribers.size === 0) return;
|
|
@@ -4766,7 +5234,7 @@ function parseCommitLines(stdout) {
|
|
|
4766
5234
|
function renderConflictSpec(args) {
|
|
4767
5235
|
const { branch, report, now } = args;
|
|
4768
5236
|
const today = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
|
|
4769
|
-
const
|
|
5237
|
+
const nowStamp2 = `${today} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
|
|
4770
5238
|
const fileLines = report.files.length ? report.files.map((f) => `- \`${f}\``).join("\n") : "- (无 — 合并失败但未列出冲突文件,请人工核查)";
|
|
4771
5239
|
const commitBlocks = report.files.length ? report.files.map((f) => {
|
|
4772
5240
|
const commits = report.commitsByFile[f] ?? [];
|
|
@@ -4836,7 +5304,7 @@ _暂无_
|
|
|
4836
5304
|
"---",
|
|
4837
5305
|
"stage: plan",
|
|
4838
5306
|
"last_action: 由 worktree 合并冲突自动生成",
|
|
4839
|
-
`updated_at: '${
|
|
5307
|
+
`updated_at: '${nowStamp2}'`,
|
|
4840
5308
|
`summary: 解决 ${branch} 合并到主项目时产生的 ${report.files.length} 个冲突文件`,
|
|
4841
5309
|
"---",
|
|
4842
5310
|
""
|
|
@@ -4892,7 +5360,9 @@ function createApp(opts) {
|
|
|
4892
5360
|
const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
|
|
4893
5361
|
void main.sessions.send(
|
|
4894
5362
|
sessionId,
|
|
4895
|
-
`${skillRef("yorz-spec")},然后处理 spec:${main.specsDirRelative}/${specId}/spec.md
|
|
5363
|
+
`${skillRef("yorz-spec")},然后处理 spec:${main.specsDirRelative}/${specId}/spec.md`,
|
|
5364
|
+
void 0,
|
|
5365
|
+
{ trigger: "conflict", specId }
|
|
4896
5366
|
);
|
|
4897
5367
|
}
|
|
4898
5368
|
});
|
|
@@ -4905,6 +5375,7 @@ function createApp(opts) {
|
|
|
4905
5375
|
api.route("/", createSpecsRoutes(resolveProject));
|
|
4906
5376
|
api.route("/", createSessionsRoutes(resolveProject));
|
|
4907
5377
|
api.route("/", createSpecReviewRoutes(resolveProject));
|
|
5378
|
+
api.route("/", createGitRoutes(resolveProject));
|
|
4908
5379
|
api.route("/", createSpecDraftsRoutes(resolveProject));
|
|
4909
5380
|
api.route("/", createWorktreeRoutes(opts.registry, worktreeManager));
|
|
4910
5381
|
api.route("/", createProjectFilesRoutes(resolveProject));
|
|
@@ -5378,6 +5849,7 @@ const watcherLog = () => getLogger().child("watcher");
|
|
|
5378
5849
|
const UNLINK_SETTLE_MS = 80;
|
|
5379
5850
|
class SpecWatcher {
|
|
5380
5851
|
root;
|
|
5852
|
+
cwd;
|
|
5381
5853
|
unlinkSettleMs;
|
|
5382
5854
|
detailListeners = /* @__PURE__ */ new Map();
|
|
5383
5855
|
listListeners = /* @__PURE__ */ new Set();
|
|
@@ -5388,6 +5860,7 @@ class SpecWatcher {
|
|
|
5388
5860
|
watcher = null;
|
|
5389
5861
|
constructor(opts) {
|
|
5390
5862
|
this.root = opts.specsDir ?? join(opts.cwd, ".yorz", "specs");
|
|
5863
|
+
this.cwd = opts.cwd;
|
|
5391
5864
|
this.unlinkSettleMs = opts.unlinkSettleMs ?? UNLINK_SETTLE_MS;
|
|
5392
5865
|
}
|
|
5393
5866
|
async start() {
|
|
@@ -5481,6 +5954,7 @@ class SpecWatcher {
|
|
|
5481
5954
|
this.pendingUnlinks.set(id, timer);
|
|
5482
5955
|
}
|
|
5483
5956
|
emit(id, evt, mtimeMs) {
|
|
5957
|
+
getTelemetry(this.cwd).record("spec.change", { specId: id, kind: evt, mtimeMs });
|
|
5484
5958
|
for (const cb of this.detailListeners.get(id) ?? []) cb(evt, mtimeMs);
|
|
5485
5959
|
for (const cb of this.listListeners) cb();
|
|
5486
5960
|
}
|
|
@@ -5544,6 +6018,29 @@ function usageWindows(usage) {
|
|
|
5544
6018
|
}
|
|
5545
6019
|
return out;
|
|
5546
6020
|
}
|
|
6021
|
+
function num$1(value) {
|
|
6022
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
6023
|
+
}
|
|
6024
|
+
function isRecord(value) {
|
|
6025
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6026
|
+
}
|
|
6027
|
+
function withCost(usage, costUsd) {
|
|
6028
|
+
if (costUsd === void 0) return usage;
|
|
6029
|
+
return { ...usage ?? {}, costUsd };
|
|
6030
|
+
}
|
|
6031
|
+
function primaryModel(modelUsage) {
|
|
6032
|
+
if (!isRecord(modelUsage)) return void 0;
|
|
6033
|
+
let best;
|
|
6034
|
+
let bestOutput = -1;
|
|
6035
|
+
for (const [model, raw] of Object.entries(modelUsage)) {
|
|
6036
|
+
const output = isRecord(raw) ? num$1(raw.outputTokens) ?? 0 : 0;
|
|
6037
|
+
if (output > bestOutput) {
|
|
6038
|
+
bestOutput = output;
|
|
6039
|
+
best = model;
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
return best;
|
|
6043
|
+
}
|
|
5547
6044
|
class ClaudeSession {
|
|
5548
6045
|
constructor(id, cwd, isNew) {
|
|
5549
6046
|
this.id = id;
|
|
@@ -5570,6 +6067,7 @@ class ClaudeSession {
|
|
|
5570
6067
|
try {
|
|
5571
6068
|
const q = query({ prompt, options });
|
|
5572
6069
|
let usage;
|
|
6070
|
+
let metrics;
|
|
5573
6071
|
let completed = false;
|
|
5574
6072
|
for await (const msg of q) {
|
|
5575
6073
|
const m = msg;
|
|
@@ -5594,12 +6092,32 @@ class ClaudeSession {
|
|
|
5594
6092
|
}
|
|
5595
6093
|
} else if (m.type === "result") {
|
|
5596
6094
|
usage = m.usage;
|
|
6095
|
+
metrics = {
|
|
6096
|
+
usage: withCost(normalizeUsage("claude", m.usage), num$1(m.total_cost_usd)),
|
|
6097
|
+
modelUsage: isRecord(m.modelUsage) ? m.modelUsage : void 0,
|
|
6098
|
+
numTurns: num$1(m.num_turns),
|
|
6099
|
+
stopReason: typeof m.stop_reason === "string" ? m.stop_reason : void 0,
|
|
6100
|
+
model: primaryModel(m.modelUsage),
|
|
6101
|
+
durationMs: num$1(m.duration_ms),
|
|
6102
|
+
apiDurationMs: num$1(m.duration_api_ms)
|
|
6103
|
+
};
|
|
6104
|
+
} else if (m.type === "system" && m.subtype === "compact_boundary") {
|
|
6105
|
+
const meta = m.compact_metadata ?? {};
|
|
6106
|
+
yield {
|
|
6107
|
+
type: "compact",
|
|
6108
|
+
metrics: {
|
|
6109
|
+
trigger: meta.trigger === "manual" || meta.trigger === "auto" ? meta.trigger : void 0,
|
|
6110
|
+
preTokens: num$1(meta.pre_tokens),
|
|
6111
|
+
postTokens: num$1(meta.post_tokens),
|
|
6112
|
+
durationMs: num$1(meta.duration_ms)
|
|
6113
|
+
}
|
|
6114
|
+
};
|
|
5597
6115
|
} else if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
|
|
5598
6116
|
completed = true;
|
|
5599
|
-
yield { type: "turn-completed", usage };
|
|
6117
|
+
yield { type: "turn-completed", usage, metrics };
|
|
5600
6118
|
}
|
|
5601
6119
|
}
|
|
5602
|
-
if (!completed) yield { type: "turn-completed", usage };
|
|
6120
|
+
if (!completed) yield { type: "turn-completed", usage, metrics };
|
|
5603
6121
|
} catch (err) {
|
|
5604
6122
|
if (ctrl.signal.aborted) return;
|
|
5605
6123
|
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -5882,7 +6400,11 @@ class CodexSession {
|
|
|
5882
6400
|
yield { type: "error", message: item.message };
|
|
5883
6401
|
}
|
|
5884
6402
|
} else if (ev.type === "turn.completed") {
|
|
5885
|
-
yield {
|
|
6403
|
+
yield {
|
|
6404
|
+
type: "turn-completed",
|
|
6405
|
+
usage: ev.usage,
|
|
6406
|
+
metrics: { usage: normalizeUsage("codex", ev.usage) }
|
|
6407
|
+
};
|
|
5886
6408
|
} else if (ev.type === "turn.failed") {
|
|
5887
6409
|
yield { type: "error", message: ev.error.message };
|
|
5888
6410
|
} else if (ev.type === "error") {
|
|
@@ -6268,7 +6790,14 @@ class OpenCodeSession {
|
|
|
6268
6790
|
for (const part of textParts(data.parts)) {
|
|
6269
6791
|
if (part.type === "text") yield { type: "text", delta: part.text };
|
|
6270
6792
|
}
|
|
6271
|
-
yield {
|
|
6793
|
+
yield {
|
|
6794
|
+
type: "turn-completed",
|
|
6795
|
+
usage: data.info.tokens,
|
|
6796
|
+
metrics: {
|
|
6797
|
+
usage: normalizeUsage("opencode", data.info.tokens),
|
|
6798
|
+
model: typeof data.info.modelID === "string" ? data.info.modelID : void 0
|
|
6799
|
+
}
|
|
6800
|
+
};
|
|
6272
6801
|
} catch (err) {
|
|
6273
6802
|
if (this.aborted) return;
|
|
6274
6803
|
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -6636,9 +7165,11 @@ class SessionManager {
|
|
|
6636
7165
|
* text while sending an expanded prompt to the Agent. Without it, an injected
|
|
6637
7166
|
* hidden prompt would become the title.
|
|
6638
7167
|
*/
|
|
6639
|
-
async send(sid, prompt, titleSource) {
|
|
7168
|
+
async send(sid, prompt, titleSource, meta = {}) {
|
|
6640
7169
|
const runId = randomUUID();
|
|
6641
7170
|
const emitter = this.emitterFor(sid);
|
|
7171
|
+
const telemetry = getTelemetry(this.cwd);
|
|
7172
|
+
const attribution = { specId: meta.specId, trigger: meta.trigger };
|
|
6642
7173
|
let currentSid = sid;
|
|
6643
7174
|
const startedAt = Date.now();
|
|
6644
7175
|
await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
|
|
@@ -6647,18 +7178,57 @@ class SessionManager {
|
|
|
6647
7178
|
});
|
|
6648
7179
|
this.setRunning(sid, true);
|
|
6649
7180
|
agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
|
|
7181
|
+
telemetry.record("agent.dispatch", {
|
|
7182
|
+
...attribution,
|
|
7183
|
+
phase: "start",
|
|
7184
|
+
sessionId: sid,
|
|
7185
|
+
traceId: runId,
|
|
7186
|
+
promptLength: prompt.length
|
|
7187
|
+
});
|
|
7188
|
+
let agentKind;
|
|
7189
|
+
let dispatchError;
|
|
6650
7190
|
const task = (async () => {
|
|
6651
7191
|
try {
|
|
6652
7192
|
const ls = await this.ensureLive(sid);
|
|
7193
|
+
agentKind = ls.kind;
|
|
6653
7194
|
for await (const ev of ls.session.send(prompt)) {
|
|
6654
7195
|
if (ev.type === "session-started" && ev.sessionId !== currentSid) {
|
|
6655
7196
|
const oldSid = currentSid;
|
|
6656
7197
|
currentSid = ev.sessionId;
|
|
6657
7198
|
await this.reconcile(oldSid, ev.sessionId);
|
|
7199
|
+
} else if (ev.type === "turn-completed") {
|
|
7200
|
+
const m = ev.metrics ?? {};
|
|
7201
|
+
telemetry.record("agent.turn", {
|
|
7202
|
+
...attribution,
|
|
7203
|
+
sessionId: currentSid,
|
|
7204
|
+
traceId: runId,
|
|
7205
|
+
agentKind,
|
|
7206
|
+
usage: m.usage,
|
|
7207
|
+
modelUsage: m.modelUsage,
|
|
7208
|
+
numTurns: m.numTurns,
|
|
7209
|
+
stopReason: m.stopReason,
|
|
7210
|
+
model: m.model,
|
|
7211
|
+
durMs: m.durationMs,
|
|
7212
|
+
apiDurMs: m.apiDurationMs
|
|
7213
|
+
});
|
|
7214
|
+
} else if (ev.type === "compact") {
|
|
7215
|
+
telemetry.record("agent.compact", {
|
|
7216
|
+
...attribution,
|
|
7217
|
+
sessionId: currentSid,
|
|
7218
|
+
traceId: runId,
|
|
7219
|
+
agentKind,
|
|
7220
|
+
// Named apart from the dispatch `trigger` so both survive on the
|
|
7221
|
+
// same line: which action started the run, and what compacted it.
|
|
7222
|
+
compactTrigger: ev.metrics.trigger,
|
|
7223
|
+
preTokens: ev.metrics.preTokens,
|
|
7224
|
+
postTokens: ev.metrics.postTokens,
|
|
7225
|
+
durMs: ev.metrics.durationMs
|
|
7226
|
+
});
|
|
6658
7227
|
}
|
|
6659
7228
|
emitter.emit("event", ev);
|
|
6660
7229
|
}
|
|
6661
7230
|
} catch (err) {
|
|
7231
|
+
dispatchError = err instanceof Error ? err.message : String(err);
|
|
6662
7232
|
agentLog().error("dispatch failed", {
|
|
6663
7233
|
sessionId: currentSid,
|
|
6664
7234
|
runId,
|
|
@@ -6676,6 +7246,17 @@ class SessionManager {
|
|
|
6676
7246
|
runId,
|
|
6677
7247
|
durationMs: Date.now() - startedAt
|
|
6678
7248
|
});
|
|
7249
|
+
telemetry.record("agent.dispatch", {
|
|
7250
|
+
...attribution,
|
|
7251
|
+
phase: "end",
|
|
7252
|
+
sessionId: currentSid,
|
|
7253
|
+
traceId: runId,
|
|
7254
|
+
agentKind,
|
|
7255
|
+
promptLength: prompt.length,
|
|
7256
|
+
durMs: Date.now() - startedAt,
|
|
7257
|
+
ok: dispatchError === void 0,
|
|
7258
|
+
errorMessage: dispatchError
|
|
7259
|
+
});
|
|
6679
7260
|
await this.store.touch(currentSid).catch(() => {
|
|
6680
7261
|
});
|
|
6681
7262
|
this.setRunning(currentSid, false);
|
|
@@ -8867,6 +9448,7 @@ async function lintFile(path, opts = {}) {
|
|
|
8867
9448
|
}
|
|
8868
9449
|
[...new Set([...SPEC_RULES, ...REVIEW_RULES].map((r) => r.id))];
|
|
8869
9450
|
async function runLint(opts) {
|
|
9451
|
+
const startedAt = Date.now();
|
|
8870
9452
|
const cwd = resolve(opts.cwd);
|
|
8871
9453
|
let targets = [];
|
|
8872
9454
|
if (opts.all) {
|
|
@@ -8936,8 +9518,220 @@ async function runLint(opts) {
|
|
|
8936
9518
|
Total: ${errorCount} error(s), ${warnCount} warn(s) across ${reports.length} file(s).
|
|
8937
9519
|
`);
|
|
8938
9520
|
}
|
|
9521
|
+
await recordLintRun(cwd, {
|
|
9522
|
+
fileCount: targets.length,
|
|
9523
|
+
errorCount,
|
|
9524
|
+
warnCount,
|
|
9525
|
+
exitCode,
|
|
9526
|
+
durMs: Date.now() - startedAt
|
|
9527
|
+
});
|
|
8939
9528
|
return { reports, errorCount, warnCount, exitCode };
|
|
8940
9529
|
}
|
|
9530
|
+
async function recordLintRun(cwd, payload) {
|
|
9531
|
+
const root = findProjectRoot(cwd);
|
|
9532
|
+
if (!root) return;
|
|
9533
|
+
const telemetry = getTelemetry(root);
|
|
9534
|
+
telemetry.record("lint.run", payload);
|
|
9535
|
+
await telemetry.flush();
|
|
9536
|
+
}
|
|
9537
|
+
const UNATTRIBUTED = "(no spec)";
|
|
9538
|
+
async function runMetrics(opts) {
|
|
9539
|
+
const dirs = await resolveTargetDirs(opts);
|
|
9540
|
+
if (dirs.length === 0) {
|
|
9541
|
+
process.stderr.write(
|
|
9542
|
+
"no telemetry found — run a dispatch first, or pass --project <id|path> / --all\n"
|
|
9543
|
+
);
|
|
9544
|
+
return { exitCode: 1, summary: emptySummary() };
|
|
9545
|
+
}
|
|
9546
|
+
const summary = emptySummary();
|
|
9547
|
+
for (const dir of dirs) {
|
|
9548
|
+
summary.projects.push(await projectLabel(dir));
|
|
9549
|
+
for (const file of await telemetryFiles(dir)) {
|
|
9550
|
+
summary.files.push(file);
|
|
9551
|
+
await consumeFile(file, opts.since, summary);
|
|
9552
|
+
}
|
|
9553
|
+
}
|
|
9554
|
+
summary.specs.sort((a, b) => b.costUsd - a.costUsd || b.turns - a.turns);
|
|
9555
|
+
if (opts.format === "json") {
|
|
9556
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}
|
|
9557
|
+
`);
|
|
9558
|
+
} else {
|
|
9559
|
+
process.stdout.write(renderText(summary));
|
|
9560
|
+
}
|
|
9561
|
+
return { exitCode: 0, summary };
|
|
9562
|
+
}
|
|
9563
|
+
async function resolveTargetDirs(opts) {
|
|
9564
|
+
const metricsDir = resolveMetricsDir();
|
|
9565
|
+
if (opts.all) {
|
|
9566
|
+
if (!existsSync(metricsDir)) return [];
|
|
9567
|
+
const entries = await readdir(metricsDir, { withFileTypes: true });
|
|
9568
|
+
return entries.filter((e) => e.isDirectory()).map((e) => join(metricsDir, e.name));
|
|
9569
|
+
}
|
|
9570
|
+
if (opts.project) {
|
|
9571
|
+
const dir2 = isAbsolute(opts.project) ? resolveProjectMetricsDir(opts.project) : join(metricsDir, opts.project);
|
|
9572
|
+
return existsSync(dir2) ? [dir2] : [];
|
|
9573
|
+
}
|
|
9574
|
+
const root = findProjectRoot(opts.cwd);
|
|
9575
|
+
if (!root) return [];
|
|
9576
|
+
const dir = resolveProjectMetricsDir(root);
|
|
9577
|
+
return existsSync(dir) ? [dir] : [];
|
|
9578
|
+
}
|
|
9579
|
+
async function telemetryFiles(dir) {
|
|
9580
|
+
if (!existsSync(dir)) return [];
|
|
9581
|
+
const entries = await readdir(dir);
|
|
9582
|
+
const archives = entries.filter((name2) => name2.startsWith(`${TELEMETRY_FILE_NAME}.`)).sort((a, b) => Number(b.split(".").pop()) - Number(a.split(".").pop())).map((name2) => join(dir, name2));
|
|
9583
|
+
const current = join(dir, TELEMETRY_FILE_NAME);
|
|
9584
|
+
return existsSync(current) ? [...archives, current] : archives;
|
|
9585
|
+
}
|
|
9586
|
+
async function projectLabel(dir) {
|
|
9587
|
+
try {
|
|
9588
|
+
const raw = await readFile(join(dir, PROJECT_META_FILE_NAME), "utf8");
|
|
9589
|
+
const meta = JSON.parse(raw);
|
|
9590
|
+
return meta.path ? `${meta.id ?? "?"} (${meta.path})` : meta.id ?? dir;
|
|
9591
|
+
} catch {
|
|
9592
|
+
return dir;
|
|
9593
|
+
}
|
|
9594
|
+
}
|
|
9595
|
+
async function consumeFile(file, since, out) {
|
|
9596
|
+
let raw;
|
|
9597
|
+
try {
|
|
9598
|
+
raw = await readFile(file, "utf8");
|
|
9599
|
+
} catch {
|
|
9600
|
+
return;
|
|
9601
|
+
}
|
|
9602
|
+
for (const line of raw.split("\n")) {
|
|
9603
|
+
if (!line.trim()) continue;
|
|
9604
|
+
let ev;
|
|
9605
|
+
try {
|
|
9606
|
+
ev = JSON.parse(line);
|
|
9607
|
+
} catch {
|
|
9608
|
+
out.skipped += 1;
|
|
9609
|
+
continue;
|
|
9610
|
+
}
|
|
9611
|
+
const ts = typeof ev.ts === "string" ? ev.ts : "";
|
|
9612
|
+
if (since && ts.slice(0, 10) < since) continue;
|
|
9613
|
+
out.lines += 1;
|
|
9614
|
+
const event = typeof ev.event === "string" ? ev.event : "unknown";
|
|
9615
|
+
out.eventCounts[event] = (out.eventCounts[event] ?? 0) + 1;
|
|
9616
|
+
applyEvent(ev, event, out);
|
|
9617
|
+
}
|
|
9618
|
+
}
|
|
9619
|
+
function applyEvent(ev, event, out) {
|
|
9620
|
+
if (!event.startsWith("agent.")) return;
|
|
9621
|
+
const bucket = bucketFor(out, typeof ev.specId === "string" ? ev.specId : UNATTRIBUTED);
|
|
9622
|
+
if (event === "agent.dispatch") {
|
|
9623
|
+
if (ev.phase !== "end") return;
|
|
9624
|
+
bucket.dispatches += 1;
|
|
9625
|
+
out.totals.dispatches += 1;
|
|
9626
|
+
if (ev.ok === false) {
|
|
9627
|
+
bucket.failures += 1;
|
|
9628
|
+
out.totals.failures += 1;
|
|
9629
|
+
}
|
|
9630
|
+
return;
|
|
9631
|
+
}
|
|
9632
|
+
if (event === "agent.compact") {
|
|
9633
|
+
bucket.compactions += 1;
|
|
9634
|
+
out.totals.compactions += 1;
|
|
9635
|
+
return;
|
|
9636
|
+
}
|
|
9637
|
+
if (event !== "agent.turn") return;
|
|
9638
|
+
bucket.turns += 1;
|
|
9639
|
+
out.totals.turns += 1;
|
|
9640
|
+
const durMs = num(ev.durMs);
|
|
9641
|
+
bucket.agentDurMs += durMs;
|
|
9642
|
+
out.totals.agentDurMs += durMs;
|
|
9643
|
+
const usage = ev.usage ?? {};
|
|
9644
|
+
for (const key of [
|
|
9645
|
+
"inputTokens",
|
|
9646
|
+
"cacheReadTokens",
|
|
9647
|
+
"cacheCreateTokens",
|
|
9648
|
+
"outputTokens"
|
|
9649
|
+
]) {
|
|
9650
|
+
bucket[key] += num(usage[key]);
|
|
9651
|
+
out.totals[key] += num(usage[key]);
|
|
9652
|
+
}
|
|
9653
|
+
bucket.costUsd += num(usage.costUsd);
|
|
9654
|
+
out.totals.costUsd += num(usage.costUsd);
|
|
9655
|
+
}
|
|
9656
|
+
function bucketFor(out, specId) {
|
|
9657
|
+
let bucket = out.specs.find((s) => s.specId === specId);
|
|
9658
|
+
if (!bucket) {
|
|
9659
|
+
bucket = {
|
|
9660
|
+
specId,
|
|
9661
|
+
dispatches: 0,
|
|
9662
|
+
turns: 0,
|
|
9663
|
+
compactions: 0,
|
|
9664
|
+
inputTokens: 0,
|
|
9665
|
+
cacheReadTokens: 0,
|
|
9666
|
+
cacheCreateTokens: 0,
|
|
9667
|
+
outputTokens: 0,
|
|
9668
|
+
costUsd: 0,
|
|
9669
|
+
agentDurMs: 0,
|
|
9670
|
+
failures: 0,
|
|
9671
|
+
stages: []
|
|
9672
|
+
};
|
|
9673
|
+
out.specs.push(bucket);
|
|
9674
|
+
}
|
|
9675
|
+
return bucket;
|
|
9676
|
+
}
|
|
9677
|
+
function num(value) {
|
|
9678
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
9679
|
+
}
|
|
9680
|
+
function emptySummary() {
|
|
9681
|
+
return {
|
|
9682
|
+
projects: [],
|
|
9683
|
+
files: [],
|
|
9684
|
+
lines: 0,
|
|
9685
|
+
skipped: 0,
|
|
9686
|
+
eventCounts: {},
|
|
9687
|
+
totals: {
|
|
9688
|
+
dispatches: 0,
|
|
9689
|
+
turns: 0,
|
|
9690
|
+
compactions: 0,
|
|
9691
|
+
inputTokens: 0,
|
|
9692
|
+
cacheReadTokens: 0,
|
|
9693
|
+
cacheCreateTokens: 0,
|
|
9694
|
+
outputTokens: 0,
|
|
9695
|
+
costUsd: 0,
|
|
9696
|
+
agentDurMs: 0,
|
|
9697
|
+
failures: 0
|
|
9698
|
+
},
|
|
9699
|
+
specs: []
|
|
9700
|
+
};
|
|
9701
|
+
}
|
|
9702
|
+
const SPEC_ROW_LIMIT = 20;
|
|
9703
|
+
function renderText(s) {
|
|
9704
|
+
const lines = [];
|
|
9705
|
+
lines.push(`projects: ${s.projects.join(", ") || "(none)"}`);
|
|
9706
|
+
lines.push(`events: ${s.lines}${s.skipped ? ` (${s.skipped} unparseable line(s))` : ""}`);
|
|
9707
|
+
const kinds = Object.entries(s.eventCounts).sort((a, b) => b[1] - a[1]);
|
|
9708
|
+
if (kinds.length) {
|
|
9709
|
+
lines.push(`by kind: ${kinds.map(([k, n]) => `${k}=${n}`).join(" ")}`);
|
|
9710
|
+
}
|
|
9711
|
+
const t = s.totals;
|
|
9712
|
+
lines.push("");
|
|
9713
|
+
lines.push(
|
|
9714
|
+
`total: ${t.dispatches} dispatch(es), ${t.turns} turn(s), ${t.failures} failure(s), ${t.compactions} compaction(s)`
|
|
9715
|
+
);
|
|
9716
|
+
lines.push(
|
|
9717
|
+
`tokens: in=${t.inputTokens} cacheRead=${t.cacheReadTokens} cacheWrite=${t.cacheCreateTokens} out=${t.outputTokens}`
|
|
9718
|
+
);
|
|
9719
|
+
lines.push(`cost: $${t.costUsd.toFixed(4)} agent time: ${(t.agentDurMs / 1e3).toFixed(1)}s`);
|
|
9720
|
+
if (s.specs.length) {
|
|
9721
|
+
lines.push("");
|
|
9722
|
+
lines.push("per spec (by cost):");
|
|
9723
|
+
for (const spec of s.specs.slice(0, SPEC_ROW_LIMIT)) {
|
|
9724
|
+
lines.push(
|
|
9725
|
+
` ${spec.specId.padEnd(40)} $${spec.costUsd.toFixed(4).padStart(9)} ${String(spec.dispatches).padStart(3)} disp ${String(spec.turns).padStart(3)} turn in=${spec.inputTokens} cacheRead=${spec.cacheReadTokens} out=${spec.outputTokens}`
|
|
9726
|
+
);
|
|
9727
|
+
}
|
|
9728
|
+
if (s.specs.length > SPEC_ROW_LIMIT) {
|
|
9729
|
+
lines.push(` … ${s.specs.length - SPEC_ROW_LIMIT} more spec(s) not shown (use --format json)`);
|
|
9730
|
+
}
|
|
9731
|
+
}
|
|
9732
|
+
return `${lines.join("\n")}
|
|
9733
|
+
`;
|
|
9734
|
+
}
|
|
8941
9735
|
const program = new Command();
|
|
8942
9736
|
program.name("yorz").description("YorZ CLI — manage the yorz-spec skill.").version(pkg.version);
|
|
8943
9737
|
const uninstallCmd = program.command("uninstall").description("Remove YorZ artifacts (skills, ...).");
|
|
@@ -9001,6 +9795,18 @@ program.command("lint [paths...]").description("Lint spec.md / review.md files f
|
|
|
9001
9795
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9002
9796
|
}
|
|
9003
9797
|
);
|
|
9798
|
+
program.command("metrics").description("Summarize collected telemetry (token / cost / duration per spec).").option("--format <format>", "output format: text | json", "text").option("--project <idOrPath>", "project id or absolute path (default: project of --cwd)").option("--all", "aggregate across every project that has telemetry", false).option("--since <date>", "only events on or after this local date (YYYY-MM-DD)").option("--cwd <path>", "working directory (default: process.cwd())").action(
|
|
9799
|
+
async (opts) => {
|
|
9800
|
+
const result = await runMetrics({
|
|
9801
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
9802
|
+
format: opts.format === "json" ? "json" : "text",
|
|
9803
|
+
project: opts.project,
|
|
9804
|
+
all: opts.all === true,
|
|
9805
|
+
since: opts.since
|
|
9806
|
+
});
|
|
9807
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9808
|
+
}
|
|
9809
|
+
);
|
|
9004
9810
|
const serveCmd = program.command("serve").description("Start the YorZ Service (HTTP + SSE + static GUI; multi-project).").option("-p, --port <port>", "port to listen on", "7423").option(
|
|
9005
9811
|
"--host <address>",
|
|
9006
9812
|
"bind address (default: 127.0.0.1; the service runs shell commands, so exposing it to the network is opt-in)"
|