@yorz/cli 0.7.1 → 0.8.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 +982 -83
- package/dist/gui/assets/{abnfDiagram-VRR7QNED-Bfi5VmJM.js → abnfDiagram-VRR7QNED-D-y0QKuD.js} +1 -1
- package/dist/gui/assets/{arc-DW35HiMx.js → arc-B49ieWDx.js} +1 -1
- package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-D6ol2HjT.js → architectureDiagram-ZJ3FMSHR-CcOou6CY.js} +1 -1
- package/dist/gui/assets/{blockDiagram-677ZJIJ3-DimsChzs.js → blockDiagram-677ZJIJ3-DY8EHGpO.js} +1 -1
- package/dist/gui/assets/{c4Diagram-LMCZKHZV-aQtOPEhO.js → c4Diagram-LMCZKHZV-CV8mRhow.js} +1 -1
- package/dist/gui/assets/channel-BakWbuVQ.js +1 -0
- package/dist/gui/assets/{chunk-2Q5K7J3B-Bg7XMADz.js → chunk-2Q5K7J3B-BjplUf2p.js} +1 -1
- package/dist/gui/assets/{chunk-32BRIVSS-CJvj41Qg.js → chunk-32BRIVSS-Dua0UKp_.js} +1 -1
- package/dist/gui/assets/{chunk-5VM5RSS4-B6iQkE6V.js → chunk-5VM5RSS4-Br-SORjV.js} +1 -1
- package/dist/gui/assets/{chunk-EX3LRPZG-Bime3fML.js → chunk-EX3LRPZG-Dgb6iahE.js} +1 -1
- package/dist/gui/assets/{chunk-JWPE2WC7-B2d6pOPL.js → chunk-JWPE2WC7-BkOQaZRq.js} +1 -1
- package/dist/gui/assets/{chunk-MOJQB5TN-C2BtAAqD.js → chunk-MOJQB5TN-D1qUO9Uh.js} +1 -1
- package/dist/gui/assets/{chunk-RYQCIY6F-DvOYjxsw.js → chunk-RYQCIY6F-CwiUC3Pd.js} +1 -1
- package/dist/gui/assets/{chunk-V7JOEXUC-HRF7SSaJ.js → chunk-V7JOEXUC-O0VQnEjo.js} +1 -1
- package/dist/gui/assets/{chunk-VR4S4FIN-oMVpMTS0.js → chunk-VR4S4FIN-C-iULcHx.js} +1 -1
- package/dist/gui/assets/{chunk-XXDRQBXY-Gx6kiEHQ.js → chunk-XXDRQBXY-buihollE.js} +1 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-DgfCpbC5.js +1 -0
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DgfCpbC5.js +1 -0
- package/dist/gui/assets/{cose-bilkent-JH36ORCC-MoHCpl37.js → cose-bilkent-JH36ORCC-DJT3GZ0Y.js} +1 -1
- package/dist/gui/assets/{cynefin-VYW2F7L2-2tnFSmNB.js → cynefin-VYW2F7L2-CUB4SFmD.js} +1 -1
- package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-Cp7uDUjl.js → cynefinDiagram-TSTJHNR4-Dt0_fxzu.js} +1 -1
- package/dist/gui/assets/{dagre-VKFMJZFB-CTNuVp99.js → dagre-VKFMJZFB-BU2VgIkF.js} +1 -1
- package/dist/gui/assets/{diagram-FQU43EPY-Z9nTO-2q.js → diagram-FQU43EPY-CpRyd1t3.js} +1 -1
- package/dist/gui/assets/{diagram-G47NLZAW-D1H3xA3x.js → diagram-G47NLZAW-CcJkOrvN.js} +1 -1
- package/dist/gui/assets/{diagram-NH7WQ7WH-BGpM31nf.js → diagram-NH7WQ7WH-BcWa9fg4.js} +1 -1
- package/dist/gui/assets/{diagram-OA4YK3LP-Bhn6yNbV.js → diagram-OA4YK3LP-Clu9LYE6.js} +1 -1
- package/dist/gui/assets/{diagram-WEI45ONY-BDJ6eaig.js → diagram-WEI45ONY-BKt73qmr.js} +1 -1
- package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-C8UHOTF4.js → ebnfDiagram-CCIWWBDH-ycI3xDTZ.js} +1 -1
- package/dist/gui/assets/{erDiagram-Q63AITRT-BpoLARsO.js → erDiagram-Q63AITRT-CJf0jIvL.js} +1 -1
- package/dist/gui/assets/{flowDiagram-23GEKE2U-DSODAemd.js → flowDiagram-23GEKE2U-8X7ZYojT.js} +1 -1
- package/dist/gui/assets/{ganttDiagram-NO4QXBWP-Bv5JA_dU.js → ganttDiagram-NO4QXBWP-DsGJIvEc.js} +1 -1
- package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-CW7ObzYy.js → gitGraphDiagram-IHSO6WYX-Dg-PnhMR.js} +1 -1
- package/dist/gui/assets/index-DGSN0uvJ.css +1 -0
- package/dist/gui/assets/index-DIKCFt79.js +39 -0
- package/dist/gui/assets/{infoDiagram-FWYZ7A6U-uTM_03X9.js → infoDiagram-FWYZ7A6U-CX-PGRjw.js} +1 -1
- package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-BlbCXOf8.js → ishikawaDiagram-FXEZZL3T-BGq-1WGF.js} +1 -1
- package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BwtfR_HU.js → journeyDiagram-5HDEW3XC-CX7J6QLx.js} +1 -1
- package/dist/gui/assets/{kanban-definition-HUTT4EX6-BXwvYv3u.js → kanban-definition-HUTT4EX6-XcUXgRy6.js} +1 -1
- package/dist/gui/assets/{linear-C5c8PyN7.js → linear-D-w3YyOR.js} +1 -1
- package/dist/gui/assets/{mermaid.core-Dbw2b69i.js → mermaid.core-BjB4Bulu.js} +4 -4
- package/dist/gui/assets/{mindmap-definition-LN4V7U3C-9P8fXZBY.js → mindmap-definition-LN4V7U3C-D2kDYOXu.js} +1 -1
- package/dist/gui/assets/{pegDiagram-2B236MQR-81kUPbkr.js → pegDiagram-2B236MQR-C79JhnNB.js} +1 -1
- package/dist/gui/assets/{pieDiagram-ENE6RG2P-BI7iwgtu.js → pieDiagram-ENE6RG2P-CbLYFwp4.js} +1 -1
- package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-CSxA_Epi.js → quadrantDiagram-ABIIQ3AL-BXNa6Xix.js} +1 -1
- package/dist/gui/assets/{railroadDiagram-RFXS5EU6-D4DnN2WS.js → railroadDiagram-RFXS5EU6-CG8ojk-9.js} +1 -1
- package/dist/gui/assets/{requirementDiagram-TGXJPOKE-DjNONyxQ.js → requirementDiagram-TGXJPOKE-Ddn0mejD.js} +1 -1
- package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-Di4S5cUR.js → sankeyDiagram-HTMAVEWB-DXVSB34u.js} +1 -1
- package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-CLVwUzez.js → sequenceDiagram-DBY2YBRQ-B00FxwKQ.js} +1 -1
- package/dist/gui/assets/{sizeCapture-X5ZJPWSS-C5y0Ry_b.js → sizeCapture-X5ZJPWSS-BUiEsqdO.js} +1 -1
- package/dist/gui/assets/{stateDiagram-2N3HPSRC-IdHsn6JH.js → stateDiagram-2N3HPSRC-DflPz4MT.js} +1 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-DQohEYjS.js +1 -0
- package/dist/gui/assets/{swimlanes-5IMT3BWC-bDCHEODt.js → swimlanes-5IMT3BWC-yL7Pms6k.js} +2 -2
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-CUZb2EKw.js +8 -0
- package/dist/gui/assets/{timeline-definition-FHXFAJF6-B6f6vq0E.js → timeline-definition-FHXFAJF6--V8N1aYE.js} +1 -1
- package/dist/gui/assets/{vennDiagram-L72KCM5P-AjYH8jaU.js → vennDiagram-L72KCM5P-CjlyPLPm.js} +1 -1
- package/dist/gui/assets/{wardleyDiagram-EHGQE667-NV3g14vB.js → wardleyDiagram-EHGQE667-C99qPu9U.js} +1 -1
- package/dist/gui/assets/{xychartDiagram-FW5EYKEG-D4yDWaI8.js → xychartDiagram-FW5EYKEG-DeUNfALY.js} +1 -1
- package/dist/gui/index.html +24 -8
- package/package.json +2 -1
- package/dist/gui/assets/channel-CYD2fo3z.js +0 -1
- package/dist/gui/assets/classDiagram-OUVF2IWQ-CFzbN5IF.js +0 -1
- package/dist/gui/assets/classDiagram-v2-EOCWNBFH-CFzbN5IF.js +0 -1
- package/dist/gui/assets/index-CAnHTFea.js +0 -38
- package/dist/gui/assets/index-Cft6LQWD.css +0 -1
- package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CJBs-brH.js +0 -1
- package/dist/gui/assets/swimlanesDiagram-G3AALYLV-BpY4RNvM.js +0 -8
package/dist/cli/index.js
CHANGED
|
@@ -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.1";
|
|
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,
|
|
@@ -373,10 +373,19 @@ function normalizeWorktree(value) {
|
|
|
373
373
|
if (cleanSlug) meta.cleanSlug = cleanSlug;
|
|
374
374
|
return meta;
|
|
375
375
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
const WORKTREE_DIR_SUFFIX = ".wt";
|
|
377
|
+
const MAX_WORKTREE_NESTING = 4;
|
|
378
|
+
function resolveWorktreeMainPath(absPath) {
|
|
379
|
+
const parent = dirname(absPath);
|
|
380
|
+
const parentName = basename$1(parent);
|
|
381
|
+
if (!parentName.endsWith(WORKTREE_DIR_SUFFIX) || parentName === WORKTREE_DIR_SUFFIX) return null;
|
|
382
|
+
return join(dirname(parent), parentName.slice(0, -WORKTREE_DIR_SUFFIX.length));
|
|
383
|
+
}
|
|
384
|
+
function generateProjectId(absPath, depth = 0) {
|
|
379
385
|
const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
|
|
386
|
+
const mainPath = depth < MAX_WORKTREE_NESTING ? resolveWorktreeMainPath(absPath) : null;
|
|
387
|
+
if (mainPath) return `${generateProjectId(mainPath, depth + 1)}_wt-${hash}`;
|
|
388
|
+
const slug = slugify(basename$1(absPath)) || "proj";
|
|
380
389
|
return `${slug}-${hash}`;
|
|
381
390
|
}
|
|
382
391
|
function slugify(text) {
|
|
@@ -1199,6 +1208,336 @@ function skillEntryPath(skillName, env = process.env) {
|
|
|
1199
1208
|
function skillRef(skillName, env = process.env) {
|
|
1200
1209
|
return `请先完整阅读并严格遵循 ${skillEntryPath(skillName, env)}(YorZ 内置 ${skillName} skill 规则)`;
|
|
1201
1210
|
}
|
|
1211
|
+
const METRICS_DIR_NAME = "metrics";
|
|
1212
|
+
const TELEMETRY_FILE_NAME = "telemetry.jsonl";
|
|
1213
|
+
const PROJECTS_INDEX_FILE_NAME = "projects.json";
|
|
1214
|
+
const TELEMETRY_RETENTION_DAYS = 365;
|
|
1215
|
+
const TELEMETRY_RETENTION_MS = TELEMETRY_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
1216
|
+
const TELEMETRY_MAX_BYTES = Number.POSITIVE_INFINITY;
|
|
1217
|
+
const TELEMETRY_MAX_ARCHIVES = 0;
|
|
1218
|
+
function resolveMetricsDir(env = process.env) {
|
|
1219
|
+
return join(resolveGlobalConfigDir(env), METRICS_DIR_NAME);
|
|
1220
|
+
}
|
|
1221
|
+
function resolveTelemetryFile(env = process.env) {
|
|
1222
|
+
return join(resolveMetricsDir(env), TELEMETRY_FILE_NAME);
|
|
1223
|
+
}
|
|
1224
|
+
function resolveProjectsIndexFile(env = process.env) {
|
|
1225
|
+
return join(resolveMetricsDir(env), PROJECTS_INDEX_FILE_NAME);
|
|
1226
|
+
}
|
|
1227
|
+
function findProjectRoot(startDir) {
|
|
1228
|
+
let dir = resolve(startDir);
|
|
1229
|
+
for (; ; ) {
|
|
1230
|
+
if (existsSync(join(dir, ".yorz"))) return dir;
|
|
1231
|
+
const parent = dirname(dir);
|
|
1232
|
+
if (parent === dir) return null;
|
|
1233
|
+
dir = parent;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function normalizeUsage(kind, raw) {
|
|
1237
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1238
|
+
const obj = raw;
|
|
1239
|
+
const snapshot = kind === "codex" ? fromCodex(obj) : kind === "opencode" ? fromOpenCode(obj) : fromClaude(obj);
|
|
1240
|
+
return isEmpty(snapshot) ? void 0 : snapshot;
|
|
1241
|
+
}
|
|
1242
|
+
function fromClaude(u) {
|
|
1243
|
+
return compact({
|
|
1244
|
+
inputTokens: num$2(u.input_tokens),
|
|
1245
|
+
outputTokens: num$2(u.output_tokens),
|
|
1246
|
+
cacheReadTokens: num$2(u.cache_read_input_tokens),
|
|
1247
|
+
cacheCreateTokens: num$2(u.cache_creation_input_tokens)
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
function fromCodex(u) {
|
|
1251
|
+
const total = num$2(u.input_tokens);
|
|
1252
|
+
const cached = num$2(u.cached_input_tokens);
|
|
1253
|
+
const uncached = total !== void 0 && cached !== void 0 ? Math.max(total - cached, 0) : total;
|
|
1254
|
+
return compact({
|
|
1255
|
+
inputTokens: uncached,
|
|
1256
|
+
cacheReadTokens: cached,
|
|
1257
|
+
outputTokens: num$2(u.output_tokens),
|
|
1258
|
+
reasoningTokens: num$2(u.reasoning_output_tokens)
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
function fromOpenCode(u) {
|
|
1262
|
+
const cache = u.cache && typeof u.cache === "object" ? u.cache : {};
|
|
1263
|
+
return compact({
|
|
1264
|
+
inputTokens: num$2(u.input),
|
|
1265
|
+
outputTokens: num$2(u.output),
|
|
1266
|
+
reasoningTokens: num$2(u.reasoning),
|
|
1267
|
+
cacheReadTokens: num$2(cache.read),
|
|
1268
|
+
cacheCreateTokens: num$2(cache.write)
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
function num$2(value) {
|
|
1272
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1273
|
+
}
|
|
1274
|
+
function compact(snapshot) {
|
|
1275
|
+
const out = {};
|
|
1276
|
+
for (const [key, value] of Object.entries(snapshot)) {
|
|
1277
|
+
if (value !== void 0) out[key] = value;
|
|
1278
|
+
}
|
|
1279
|
+
return out;
|
|
1280
|
+
}
|
|
1281
|
+
function isEmpty(snapshot) {
|
|
1282
|
+
return Object.keys(snapshot).length === 0;
|
|
1283
|
+
}
|
|
1284
|
+
const TELEMETRY_SCHEMA_VERSION = 2;
|
|
1285
|
+
function isTelemetryEnabled(env = process.env) {
|
|
1286
|
+
const raw = env.YORZ_TELEMETRY?.trim().toLowerCase();
|
|
1287
|
+
if (!raw) return true;
|
|
1288
|
+
return !["off", "0", "false", "no", "disabled"].includes(raw);
|
|
1289
|
+
}
|
|
1290
|
+
const sinks = /* @__PURE__ */ new Map();
|
|
1291
|
+
const indexQueues = /* @__PURE__ */ new Map();
|
|
1292
|
+
function enqueueIndexUpdate(file, task) {
|
|
1293
|
+
const next = (indexQueues.get(file) ?? Promise.resolve()).then(task).catch(() => {
|
|
1294
|
+
});
|
|
1295
|
+
indexQueues.set(file, next);
|
|
1296
|
+
}
|
|
1297
|
+
function getSink(dir) {
|
|
1298
|
+
let sink = sinks.get(dir);
|
|
1299
|
+
if (!sink) {
|
|
1300
|
+
sink = new RotatingFileSink(dir, TELEMETRY_FILE_NAME, TELEMETRY_MAX_BYTES, TELEMETRY_MAX_ARCHIVES);
|
|
1301
|
+
sinks.set(dir, sink);
|
|
1302
|
+
}
|
|
1303
|
+
return sink;
|
|
1304
|
+
}
|
|
1305
|
+
class TelemetryRecorder {
|
|
1306
|
+
constructor(projectRoot, projectId, dir, enabled, indexFile) {
|
|
1307
|
+
this.projectRoot = projectRoot;
|
|
1308
|
+
this.projectId = projectId;
|
|
1309
|
+
this.dir = dir;
|
|
1310
|
+
this.enabled = enabled;
|
|
1311
|
+
this.indexFile = indexFile;
|
|
1312
|
+
this.sink = enabled ? getSink(dir) : null;
|
|
1313
|
+
}
|
|
1314
|
+
sink;
|
|
1315
|
+
indexWritten = false;
|
|
1316
|
+
get filePath() {
|
|
1317
|
+
return join(this.dir, TELEMETRY_FILE_NAME);
|
|
1318
|
+
}
|
|
1319
|
+
/** Queue one event. Returns immediately; never throws. */
|
|
1320
|
+
record(event, payload = {}) {
|
|
1321
|
+
if (!this.sink) return;
|
|
1322
|
+
let line;
|
|
1323
|
+
try {
|
|
1324
|
+
const envelope = {
|
|
1325
|
+
v: TELEMETRY_SCHEMA_VERSION,
|
|
1326
|
+
ts: Date.now(),
|
|
1327
|
+
event,
|
|
1328
|
+
projectId: this.projectId,
|
|
1329
|
+
...payload
|
|
1330
|
+
};
|
|
1331
|
+
line = `${JSON.stringify(envelope)}
|
|
1332
|
+
`;
|
|
1333
|
+
} catch {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
this.sink.write(line);
|
|
1337
|
+
this.ensureIndexed();
|
|
1338
|
+
}
|
|
1339
|
+
/** Resolve once every queued line has hit the disk (tests / shutdown). */
|
|
1340
|
+
async flush() {
|
|
1341
|
+
await this.sink?.flush();
|
|
1342
|
+
await indexQueues.get(this.indexFile);
|
|
1343
|
+
}
|
|
1344
|
+
/** Merge this project into `projects.json` once per process. Best-effort. */
|
|
1345
|
+
ensureIndexed() {
|
|
1346
|
+
if (this.indexWritten || !this.sink) return;
|
|
1347
|
+
this.indexWritten = true;
|
|
1348
|
+
const { projectId, projectRoot, indexFile } = this;
|
|
1349
|
+
enqueueIndexUpdate(indexFile, async () => {
|
|
1350
|
+
let index = {};
|
|
1351
|
+
try {
|
|
1352
|
+
index = JSON.parse(await readFile(indexFile, "utf8"));
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
1355
|
+
const existing = index[projectId];
|
|
1356
|
+
if (existing?.path === projectRoot) return;
|
|
1357
|
+
index[projectId] = {
|
|
1358
|
+
path: projectRoot,
|
|
1359
|
+
firstSeenAt: existing?.firstSeenAt ?? Date.now()
|
|
1360
|
+
};
|
|
1361
|
+
await writeFile(indexFile, `${JSON.stringify(index, null, 2)}
|
|
1362
|
+
`);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const recorders = /* @__PURE__ */ new Map();
|
|
1367
|
+
function getTelemetry(projectRoot, env = process.env) {
|
|
1368
|
+
const root = resolve(projectRoot);
|
|
1369
|
+
const cached = recorders.get(root);
|
|
1370
|
+
if (cached) return cached;
|
|
1371
|
+
const recorder = new TelemetryRecorder(
|
|
1372
|
+
root,
|
|
1373
|
+
generateProjectId(root),
|
|
1374
|
+
resolveMetricsDir(env),
|
|
1375
|
+
isTelemetryEnabled(env),
|
|
1376
|
+
resolveProjectsIndexFile(env)
|
|
1377
|
+
);
|
|
1378
|
+
recorders.set(root, recorder);
|
|
1379
|
+
return recorder;
|
|
1380
|
+
}
|
|
1381
|
+
const TASK_LINE = /^- \[( |x|X)\]/gm;
|
|
1382
|
+
function snapshotSpec(detail) {
|
|
1383
|
+
if (!detail) return null;
|
|
1384
|
+
let tasksTotal = 0;
|
|
1385
|
+
let tasksDone = 0;
|
|
1386
|
+
for (const match of detail.body.matchAll(TASK_LINE)) {
|
|
1387
|
+
tasksTotal += 1;
|
|
1388
|
+
if (match[1] !== " ") tasksDone += 1;
|
|
1389
|
+
}
|
|
1390
|
+
return {
|
|
1391
|
+
stage: detail.frontmatter.stage,
|
|
1392
|
+
tasksTotal,
|
|
1393
|
+
tasksDone,
|
|
1394
|
+
specBytes: Buffer.byteLength(detail.body, "utf8")
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function trackSpecStage(opts) {
|
|
1398
|
+
const { projectRoot, store, specId, handle, before, trigger } = opts;
|
|
1399
|
+
const beforeSnapshot = snapshotSpec(before);
|
|
1400
|
+
handle.onDone(() => {
|
|
1401
|
+
void (async () => {
|
|
1402
|
+
let afterSnapshot = null;
|
|
1403
|
+
try {
|
|
1404
|
+
afterSnapshot = snapshotSpec(await store.read(specId));
|
|
1405
|
+
} catch {
|
|
1406
|
+
}
|
|
1407
|
+
const payload = {
|
|
1408
|
+
specId,
|
|
1409
|
+
trigger,
|
|
1410
|
+
traceId: handle.runId,
|
|
1411
|
+
sessionId: handle.sessionId,
|
|
1412
|
+
stageBefore: beforeSnapshot?.stage,
|
|
1413
|
+
stageAfter: afterSnapshot?.stage,
|
|
1414
|
+
tasksTotal: afterSnapshot?.tasksTotal,
|
|
1415
|
+
tasksDone: afterSnapshot?.tasksDone,
|
|
1416
|
+
tasksDoneBefore: beforeSnapshot?.tasksDone,
|
|
1417
|
+
specBytes: afterSnapshot?.specBytes
|
|
1418
|
+
};
|
|
1419
|
+
getTelemetry(projectRoot).record("spec.stage", payload);
|
|
1420
|
+
})();
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
const RETIRED_EVENTS = /* @__PURE__ */ new Set(["git.op", "spec.change"]);
|
|
1424
|
+
async function initTelemetryStore(opts = {}) {
|
|
1425
|
+
const env = opts.env ?? process.env;
|
|
1426
|
+
const now = opts.now ?? Date.now();
|
|
1427
|
+
const cutoff = now - (opts.retentionMs ?? TELEMETRY_RETENTION_MS);
|
|
1428
|
+
const stats = { migrated: 0, kept: 0, dropped: 0, legacyDirs: 0 };
|
|
1429
|
+
const dir = resolveMetricsDir(env);
|
|
1430
|
+
if (!existsSync(dir)) return stats;
|
|
1431
|
+
const rows = [];
|
|
1432
|
+
collect(await readLines(resolveTelemetryFile(env)), cutoff, rows, stats);
|
|
1433
|
+
const migratedBefore = rows.length;
|
|
1434
|
+
await migrateLegacyDirs(dir, env, cutoff, rows, stats);
|
|
1435
|
+
stats.migrated = rows.length - migratedBefore;
|
|
1436
|
+
rows.sort((a, b) => a.ts - b.ts);
|
|
1437
|
+
stats.kept = rows.length;
|
|
1438
|
+
if (rows.length === 0 && stats.dropped === 0) return stats;
|
|
1439
|
+
try {
|
|
1440
|
+
await writeAtomic(resolveTelemetryFile(env), rows.map((r) => `${JSON.stringify(r)}
|
|
1441
|
+
`).join(""));
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
return stats;
|
|
1445
|
+
}
|
|
1446
|
+
async function migrateLegacyDirs(metricsDir, env, cutoff, rows, stats) {
|
|
1447
|
+
let entries;
|
|
1448
|
+
try {
|
|
1449
|
+
entries = (await readdir(metricsDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1450
|
+
} catch {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (entries.length === 0) return;
|
|
1454
|
+
const index = await readIndex(env);
|
|
1455
|
+
for (const projectId of entries) {
|
|
1456
|
+
const legacyDir = join(metricsDir, projectId);
|
|
1457
|
+
for (const file of await legacyFiles(legacyDir)) {
|
|
1458
|
+
collect(await readLines(file), cutoff, rows, stats, projectId);
|
|
1459
|
+
}
|
|
1460
|
+
const meta = await readLegacyMeta(legacyDir);
|
|
1461
|
+
if (meta && !index[projectId]) {
|
|
1462
|
+
index[projectId] = { path: meta.path, firstSeenAt: meta.firstSeenAt };
|
|
1463
|
+
}
|
|
1464
|
+
try {
|
|
1465
|
+
await rm(legacyDir, { recursive: true, force: true });
|
|
1466
|
+
stats.legacyDirs += 1;
|
|
1467
|
+
} catch {
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
await writeIndex(env, index);
|
|
1471
|
+
}
|
|
1472
|
+
async function legacyFiles(dir) {
|
|
1473
|
+
try {
|
|
1474
|
+
return (await readdir(dir)).filter((name2) => name2 === TELEMETRY_FILE_NAME || name2.startsWith(`${TELEMETRY_FILE_NAME}.`)).map((name2) => join(dir, name2));
|
|
1475
|
+
} catch {
|
|
1476
|
+
return [];
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async function readLegacyMeta(dir) {
|
|
1480
|
+
try {
|
|
1481
|
+
const raw = JSON.parse(await readFile(join(dir, "project.json"), "utf8"));
|
|
1482
|
+
if (!raw.path) return null;
|
|
1483
|
+
return { path: raw.path, firstSeenAt: parseTs(raw.firstSeenAt) ?? 0 };
|
|
1484
|
+
} catch {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function collect(lines, cutoff, out, stats, fallbackProjectId) {
|
|
1489
|
+
for (const line of lines) {
|
|
1490
|
+
let ev;
|
|
1491
|
+
try {
|
|
1492
|
+
ev = JSON.parse(line);
|
|
1493
|
+
} catch {
|
|
1494
|
+
stats.dropped += 1;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const ts = parseTs(ev.ts);
|
|
1498
|
+
const event = typeof ev.event === "string" ? ev.event : "";
|
|
1499
|
+
const projectId = typeof ev.projectId === "string" ? ev.projectId : fallbackProjectId;
|
|
1500
|
+
if (ts === null || ts < cutoff || !event || !projectId || RETIRED_EVENTS.has(event)) {
|
|
1501
|
+
stats.dropped += 1;
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
out.push({ ...ev, v: TELEMETRY_SCHEMA_VERSION, ts, event, projectId });
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
function parseTs(value) {
|
|
1508
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
1509
|
+
if (typeof value !== "string") return null;
|
|
1510
|
+
const ms = Date.parse(value.includes("T") ? value : value.replace(" ", "T"));
|
|
1511
|
+
return Number.isNaN(ms) ? null : ms;
|
|
1512
|
+
}
|
|
1513
|
+
async function readLines(file) {
|
|
1514
|
+
try {
|
|
1515
|
+
return (await readFile(file, "utf8")).split("\n").filter((l) => l.trim());
|
|
1516
|
+
} catch {
|
|
1517
|
+
return [];
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function readIndex(env) {
|
|
1521
|
+
try {
|
|
1522
|
+
return JSON.parse(await readFile(resolveProjectsIndexFile(env), "utf8"));
|
|
1523
|
+
} catch {
|
|
1524
|
+
return {};
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function writeIndex(env, index) {
|
|
1528
|
+
if (Object.keys(index).length === 0) return;
|
|
1529
|
+
try {
|
|
1530
|
+
await writeFile(resolveProjectsIndexFile(env), `${JSON.stringify(index, null, 2)}
|
|
1531
|
+
`);
|
|
1532
|
+
} catch {
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async function writeAtomic(file, content) {
|
|
1536
|
+
const tmp = `${file}.tmp`;
|
|
1537
|
+
await mkdir(join(file, ".."), { recursive: true });
|
|
1538
|
+
await writeFile(tmp, content, "utf8");
|
|
1539
|
+
await rename(tmp, file);
|
|
1540
|
+
}
|
|
1202
1541
|
function createSpecsRoutes(resolveProject) {
|
|
1203
1542
|
const app = new Hono();
|
|
1204
1543
|
const need = async (c) => {
|
|
@@ -1229,7 +1568,7 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1229
1568
|
const beforeIds = new Set((await p.store.list()).map((s) => s.id));
|
|
1230
1569
|
const prompt = buildDraftPrompt(input.type, input.requirement, input.draftId);
|
|
1231
1570
|
const { sessionId } = await p.sessions.createSession();
|
|
1232
|
-
const handle = await p.sessions.send(sessionId, prompt);
|
|
1571
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, { trigger: "new-spec" });
|
|
1233
1572
|
handle.onDone((finalSessionId) => {
|
|
1234
1573
|
void bindDraftSessionToCreatedSpec(p, beforeIds, finalSessionId);
|
|
1235
1574
|
});
|
|
@@ -1365,7 +1704,18 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1365
1704
|
"resume",
|
|
1366
1705
|
await buildDebugRuntimeContext(p)
|
|
1367
1706
|
) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
|
|
1368
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
1707
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
1708
|
+
trigger: "append",
|
|
1709
|
+
specId
|
|
1710
|
+
});
|
|
1711
|
+
trackSpecStage({
|
|
1712
|
+
projectRoot: p.path,
|
|
1713
|
+
store: p.store,
|
|
1714
|
+
specId,
|
|
1715
|
+
handle,
|
|
1716
|
+
before: detail,
|
|
1717
|
+
trigger: "append"
|
|
1718
|
+
});
|
|
1369
1719
|
return c.json({ ok: true, runId: handle.runId, sessionId });
|
|
1370
1720
|
}
|
|
1371
1721
|
return c.json({ ok: true });
|
|
@@ -1379,7 +1729,18 @@ function createSpecsRoutes(resolveProject) {
|
|
|
1379
1729
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
1380
1730
|
const debugActive = await readDebugMdStatus(join(p.specsDir, specId)) === "debugging";
|
|
1381
1731
|
const prompt = debugActive ? buildDebugPrompt(p.specsDirRelative, specId, "resume", await buildDebugRuntimeContext(p)) : `${skillRef("yorz-spec")},然后处理 spec:${p.specsDirRelative}/${specId}/spec.md`;
|
|
1382
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
1732
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
1733
|
+
trigger: "run",
|
|
1734
|
+
specId
|
|
1735
|
+
});
|
|
1736
|
+
trackSpecStage({
|
|
1737
|
+
projectRoot: p.path,
|
|
1738
|
+
store: p.store,
|
|
1739
|
+
specId,
|
|
1740
|
+
handle,
|
|
1741
|
+
before: detail,
|
|
1742
|
+
trigger: "run"
|
|
1743
|
+
});
|
|
1383
1744
|
return c.json({ runId: handle.runId, sessionId });
|
|
1384
1745
|
});
|
|
1385
1746
|
app.post("/projects/:projectId/specs/:id/explain", async (c) => {
|
|
@@ -1410,7 +1771,10 @@ ${text}
|
|
|
1410
1771
|
"""
|
|
1411
1772
|
`;
|
|
1412
1773
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
1413
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
1774
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
1775
|
+
trigger: "explain",
|
|
1776
|
+
specId
|
|
1777
|
+
});
|
|
1414
1778
|
return c.json({ runId: handle.runId, sessionId });
|
|
1415
1779
|
});
|
|
1416
1780
|
app.delete("/projects/:projectId/specs/:id", async (c) => {
|
|
@@ -2002,7 +2366,9 @@ function createSessionsRoutes(resolveProject) {
|
|
|
2002
2366
|
}
|
|
2003
2367
|
if (resolved.builtin === "yorz-debug") void cleanupExpiredChatDebugFiles(p.path).catch(() => {
|
|
2004
2368
|
});
|
|
2005
|
-
const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt
|
|
2369
|
+
const handle = await p.sessions.send(c.req.param("sid"), finalPrompt, prompt, {
|
|
2370
|
+
trigger: "chat"
|
|
2371
|
+
});
|
|
2006
2372
|
return c.json({ runId: handle.runId, sessionId: handle.sessionId }, 202);
|
|
2007
2373
|
});
|
|
2008
2374
|
app.post("/projects/:projectId/sessions/:sid/abort", async (c) => {
|
|
@@ -2198,6 +2564,119 @@ async function discard(cwd, opts) {
|
|
|
2198
2564
|
await runGit(cwd, ["clean", "-fd", "--", r.path]);
|
|
2199
2565
|
}
|
|
2200
2566
|
}
|
|
2567
|
+
const MAX_PATCH_BYTES = 512 * 1024;
|
|
2568
|
+
const MAX_PATCH_LINES = 3e3;
|
|
2569
|
+
function truncatePatch(patch) {
|
|
2570
|
+
let text = patch;
|
|
2571
|
+
let truncated = false;
|
|
2572
|
+
if (Buffer.byteLength(text, "utf8") > MAX_PATCH_BYTES) {
|
|
2573
|
+
text = text.slice(0, MAX_PATCH_BYTES);
|
|
2574
|
+
truncated = true;
|
|
2575
|
+
}
|
|
2576
|
+
const lines = text.split("\n");
|
|
2577
|
+
if (lines.length > MAX_PATCH_LINES) {
|
|
2578
|
+
text = lines.slice(0, MAX_PATCH_LINES).join("\n");
|
|
2579
|
+
truncated = true;
|
|
2580
|
+
}
|
|
2581
|
+
return { patch: text, truncated };
|
|
2582
|
+
}
|
|
2583
|
+
async function untrackedDiff(cwd, path) {
|
|
2584
|
+
let buf;
|
|
2585
|
+
try {
|
|
2586
|
+
buf = await readFile(resolve(cwd, path));
|
|
2587
|
+
} catch {
|
|
2588
|
+
return { path, patch: "", binary: false, truncated: false };
|
|
2589
|
+
}
|
|
2590
|
+
if (buf.subarray(0, 8e3).includes(0)) {
|
|
2591
|
+
return { path, patch: "", binary: true, truncated: false };
|
|
2592
|
+
}
|
|
2593
|
+
const text = buf.toString("utf8");
|
|
2594
|
+
const lines = text.length === 0 ? [] : text.replace(/\n$/, "").split("\n");
|
|
2595
|
+
const body = lines.map((l) => `+${l}`).join("\n");
|
|
2596
|
+
const header = `--- /dev/null
|
|
2597
|
+
+++ b/${path}
|
|
2598
|
+
@@ -0,0 +1,${lines.length} @@
|
|
2599
|
+
`;
|
|
2600
|
+
const { patch, truncated } = truncatePatch(lines.length ? `${header}${body}` : header);
|
|
2601
|
+
return { path, patch, binary: false, truncated };
|
|
2602
|
+
}
|
|
2603
|
+
async function fileDiff(cwd, path) {
|
|
2604
|
+
assertSafeRelativePath(cwd, path);
|
|
2605
|
+
const change = (await listChanges(cwd)).find((c) => c.path === path);
|
|
2606
|
+
if (change?.status === "??") return untrackedDiff(cwd, path);
|
|
2607
|
+
const pathArgs = change?.renamedFrom ? [change.renamedFrom, path] : [path];
|
|
2608
|
+
const res = await runGitRaw(cwd, ["diff", "HEAD", "--", ...pathArgs]);
|
|
2609
|
+
if (res.code !== 0 && !res.stdout) {
|
|
2610
|
+
throw new GitError("git_failed", res.stderr.trim() || "git diff failed", res.stderr);
|
|
2611
|
+
}
|
|
2612
|
+
const raw = res.stdout;
|
|
2613
|
+
if (!raw.includes("@@") && /^Binary files .* differ$/m.test(raw)) {
|
|
2614
|
+
return { path, patch: "", binary: true, truncated: false };
|
|
2615
|
+
}
|
|
2616
|
+
const { patch, truncated } = truncatePatch(raw);
|
|
2617
|
+
return { path, patch, binary: false, truncated };
|
|
2618
|
+
}
|
|
2619
|
+
async function currentBranch(cwd) {
|
|
2620
|
+
const { stdout } = await runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2621
|
+
const branch = stdout.trim();
|
|
2622
|
+
if (!branch || branch === "HEAD") {
|
|
2623
|
+
throw new GitError("detached_head", "cannot operate from a detached HEAD");
|
|
2624
|
+
}
|
|
2625
|
+
return branch;
|
|
2626
|
+
}
|
|
2627
|
+
async function listBranches(cwd) {
|
|
2628
|
+
const [current, branchesResult] = await Promise.all([
|
|
2629
|
+
currentBranch(cwd),
|
|
2630
|
+
runGit(cwd, [
|
|
2631
|
+
"for-each-ref",
|
|
2632
|
+
"--sort=-committerdate",
|
|
2633
|
+
"--format=%(refname:short)",
|
|
2634
|
+
"refs/heads"
|
|
2635
|
+
])
|
|
2636
|
+
]);
|
|
2637
|
+
const branches = branchesResult.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2638
|
+
return { current, branches };
|
|
2639
|
+
}
|
|
2640
|
+
async function checkoutBranch(cwd, branch) {
|
|
2641
|
+
const target = branch?.trim() ?? "";
|
|
2642
|
+
if (!target) throw new GitError("invalid_branch", "branch must not be empty");
|
|
2643
|
+
const state = await listBranches(cwd);
|
|
2644
|
+
if (!state.branches.includes(target)) {
|
|
2645
|
+
throw new GitError("invalid_branch", `unknown local branch: ${target}`);
|
|
2646
|
+
}
|
|
2647
|
+
if (state.current === target) return { current: state.current };
|
|
2648
|
+
const res = await runGitRaw(cwd, ["checkout", target]);
|
|
2649
|
+
if (res.code !== 0) {
|
|
2650
|
+
throw new GitError("checkout_failed", res.stderr.trim() || "git checkout failed", res.stderr);
|
|
2651
|
+
}
|
|
2652
|
+
return { current: await currentBranch(cwd) };
|
|
2653
|
+
}
|
|
2654
|
+
async function push(cwd) {
|
|
2655
|
+
const branch = await currentBranch(cwd);
|
|
2656
|
+
const upstream = await runGitRaw(cwd, [
|
|
2657
|
+
"rev-parse",
|
|
2658
|
+
"--abbrev-ref",
|
|
2659
|
+
"--symbolic-full-name",
|
|
2660
|
+
"@{u}"
|
|
2661
|
+
]);
|
|
2662
|
+
const hasUpstream = upstream.code === 0 && upstream.stdout.trim().length > 0;
|
|
2663
|
+
const args = hasUpstream ? ["push"] : ["push", "--set-upstream", "origin", branch];
|
|
2664
|
+
const res = await runGitRaw(cwd, args);
|
|
2665
|
+
if (res.code !== 0) {
|
|
2666
|
+
throw new GitError("push_failed", res.stderr.trim() || "git push failed", res.stderr);
|
|
2667
|
+
}
|
|
2668
|
+
return { branch, createdUpstream: !hasUpstream };
|
|
2669
|
+
}
|
|
2670
|
+
async function pull(cwd) {
|
|
2671
|
+
const branch = await currentBranch(cwd);
|
|
2672
|
+
const before = (await runGit(cwd, ["rev-parse", "HEAD"])).stdout.trim();
|
|
2673
|
+
const res = await runGitRaw(cwd, ["pull", "--ff-only"]);
|
|
2674
|
+
if (res.code !== 0) {
|
|
2675
|
+
throw new GitError("pull_failed", res.stderr.trim() || "git pull failed", res.stderr);
|
|
2676
|
+
}
|
|
2677
|
+
const after = (await runGit(cwd, ["rev-parse", "HEAD"])).stdout.trim();
|
|
2678
|
+
return { branch, updated: before !== after };
|
|
2679
|
+
}
|
|
2201
2680
|
async function stash(cwd, opts) {
|
|
2202
2681
|
if (!Array.isArray(opts.paths) || opts.paths.length === 0) {
|
|
2203
2682
|
throw new GitError("no_paths", "stash requires at least one path");
|
|
@@ -2238,7 +2717,18 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2238
2717
|
const specRel = `${p.specsDirRelative}/${specId}/spec.md`;
|
|
2239
2718
|
const prompt = buildGitOpsPrompt(action, specId, specRel);
|
|
2240
2719
|
const { sessionId } = await p.sessions.ensureSessionForSpec(specId);
|
|
2241
|
-
const handle = await p.sessions.send(sessionId, prompt
|
|
2720
|
+
const handle = await p.sessions.send(sessionId, prompt, void 0, {
|
|
2721
|
+
trigger: "git-ops",
|
|
2722
|
+
specId
|
|
2723
|
+
});
|
|
2724
|
+
trackSpecStage({
|
|
2725
|
+
projectRoot: p.path,
|
|
2726
|
+
store: p.store,
|
|
2727
|
+
specId,
|
|
2728
|
+
handle,
|
|
2729
|
+
before: detail,
|
|
2730
|
+
trigger: "git-ops"
|
|
2731
|
+
});
|
|
2242
2732
|
return c.json({ runId: handle.runId, sessionId });
|
|
2243
2733
|
});
|
|
2244
2734
|
app.get("/projects/:projectId/specs/:id/debug", async (c) => {
|
|
@@ -2256,21 +2746,7 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2256
2746
|
return c.json({ exists: false, text: "" });
|
|
2257
2747
|
}
|
|
2258
2748
|
});
|
|
2259
|
-
app.
|
|
2260
|
-
const p = await need(c);
|
|
2261
|
-
if (p instanceof Response) return p;
|
|
2262
|
-
const specId = c.req.param("id");
|
|
2263
|
-
const detail = await p.store.read(specId);
|
|
2264
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2265
|
-
try {
|
|
2266
|
-
const changes = await listChanges(p.path);
|
|
2267
|
-
return c.json({ changes });
|
|
2268
|
-
} catch (err) {
|
|
2269
|
-
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2270
|
-
throw err;
|
|
2271
|
-
}
|
|
2272
|
-
});
|
|
2273
|
-
app.post("/projects/:projectId/specs/:id/commit", async (c) => {
|
|
2749
|
+
app.post("/projects/:projectId/specs/:id/stash", async (c) => {
|
|
2274
2750
|
const p = await need(c);
|
|
2275
2751
|
if (p instanceof Response) return p;
|
|
2276
2752
|
const specId = c.req.param("id");
|
|
@@ -2285,30 +2761,122 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2285
2761
|
const obj = body && typeof body === "object" ? body : {};
|
|
2286
2762
|
const message = typeof obj.message === "string" ? obj.message.trim() : "";
|
|
2287
2763
|
const paths = Array.isArray(obj.paths) ? obj.paths.filter((v) => typeof v === "string") : [];
|
|
2288
|
-
if (!message) return c.json({ error: "message must not be empty" }, 400);
|
|
2289
2764
|
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2290
2765
|
try {
|
|
2291
|
-
|
|
2292
|
-
return c.json(
|
|
2766
|
+
await stash(p.path, { message: message || `yorz:${specId}`, paths });
|
|
2767
|
+
return c.json({ ok: true });
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2770
|
+
throw err;
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
return app;
|
|
2774
|
+
}
|
|
2775
|
+
function buildGitOpsPrompt(action, specId, specRel) {
|
|
2776
|
+
const base = `${skillRef("yorz-git-ops")},然后执行 git 操作:
|
|
2777
|
+
- spec 文档:${specRel}
|
|
2778
|
+
- 请依据 spec 文档与 git status/diff 自行判断本次 spec 关联的变更文件
|
|
2779
|
+
- spec-id:${specId}
|
|
2780
|
+
`;
|
|
2781
|
+
switch (action) {
|
|
2782
|
+
case "commit":
|
|
2783
|
+
return base + `- 动作:git-commit。请基于 spec 文档与 git status/diff,由你自主判断本次 spec 相关的变更文件,执行 \`git add\` + \`git commit\`;commit message 由你生成,不带 scope;禁止 \`git push\` 与 \`git reset --hard\`。
|
|
2784
|
+
`;
|
|
2785
|
+
case "discard":
|
|
2786
|
+
return base + `- 动作:git-discard。请使用 \`git restore --staged --worktree -- <paths>\` 与 \`git clean -fd -- <paths>\` 丢弃 spec 相关的未提交变更;对 untracked 新文件应先在终端输出列表再处理;不要预先 stash 备份;禁止 \`git reset --hard\` 与 \`git push\`。
|
|
2787
|
+
`;
|
|
2788
|
+
case "stash":
|
|
2789
|
+
return base + `- 动作:git-stash。请使用 \`git stash push -m "yorz:${specId}" -- <paths>\` 暂存 spec 相关变更文件;禁止 \`git push\`。
|
|
2790
|
+
`;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function createGitRoutes(resolveProject) {
|
|
2794
|
+
const app = new Hono();
|
|
2795
|
+
const need = async (c) => {
|
|
2796
|
+
const id = c.req.param("projectId") ?? "";
|
|
2797
|
+
if (!id) return c.json({ error: "projectId required" }, 400);
|
|
2798
|
+
const project = await resolveProject(id);
|
|
2799
|
+
if (!project) return c.json({ error: "project not found" }, 404);
|
|
2800
|
+
return project;
|
|
2801
|
+
};
|
|
2802
|
+
const readBody = async (c) => {
|
|
2803
|
+
try {
|
|
2804
|
+
const body = await c.req.json();
|
|
2805
|
+
return body && typeof body === "object" ? body : {};
|
|
2806
|
+
} catch {
|
|
2807
|
+
return null;
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
const toPaths = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
2811
|
+
app.get("/projects/:projectId/git/changes", async (c) => {
|
|
2812
|
+
const p = await need(c);
|
|
2813
|
+
if (p instanceof Response) return p;
|
|
2814
|
+
try {
|
|
2815
|
+
return c.json({ changes: await listChanges(p.path) });
|
|
2293
2816
|
} catch (err) {
|
|
2294
2817
|
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2295
2818
|
throw err;
|
|
2296
2819
|
}
|
|
2297
2820
|
});
|
|
2298
|
-
app.
|
|
2821
|
+
app.get("/projects/:projectId/git/diff", async (c) => {
|
|
2299
2822
|
const p = await need(c);
|
|
2300
2823
|
if (p instanceof Response) return p;
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2304
|
-
let body;
|
|
2824
|
+
const path = c.req.query("path") ?? "";
|
|
2825
|
+
if (!path) return c.json({ error: "path required" }, 400);
|
|
2305
2826
|
try {
|
|
2306
|
-
|
|
2307
|
-
} catch {
|
|
2308
|
-
return c.json({ error:
|
|
2827
|
+
return c.json(await fileDiff(p.path, path));
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2830
|
+
throw err;
|
|
2309
2831
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2832
|
+
});
|
|
2833
|
+
app.get("/projects/:projectId/git/branches", async (c) => {
|
|
2834
|
+
const p = await need(c);
|
|
2835
|
+
if (p instanceof Response) return p;
|
|
2836
|
+
try {
|
|
2837
|
+
return c.json(await listBranches(p.path));
|
|
2838
|
+
} catch (err) {
|
|
2839
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2840
|
+
throw err;
|
|
2841
|
+
}
|
|
2842
|
+
});
|
|
2843
|
+
app.post("/projects/:projectId/git/checkout", async (c) => {
|
|
2844
|
+
const p = await need(c);
|
|
2845
|
+
if (p instanceof Response) return p;
|
|
2846
|
+
const body = await readBody(c);
|
|
2847
|
+
if (!body) return c.json({ error: "invalid JSON body" }, 400);
|
|
2848
|
+
const branch = typeof body.branch === "string" ? body.branch.trim() : "";
|
|
2849
|
+
if (!branch) return c.json({ error: "branch must not be empty" }, 400);
|
|
2850
|
+
try {
|
|
2851
|
+
const result = await checkoutBranch(p.path, branch);
|
|
2852
|
+
return c.json({ ok: true, ...result });
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2855
|
+
throw err;
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
app.post("/projects/:projectId/git/commit", async (c) => {
|
|
2859
|
+
const p = await need(c);
|
|
2860
|
+
if (p instanceof Response) return p;
|
|
2861
|
+
const body = await readBody(c);
|
|
2862
|
+
if (!body) return c.json({ error: "invalid JSON body" }, 400);
|
|
2863
|
+
const message = typeof body.message === "string" ? body.message.trim() : "";
|
|
2864
|
+
const paths = toPaths(body.paths);
|
|
2865
|
+
if (!message) return c.json({ error: "message must not be empty" }, 400);
|
|
2866
|
+
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2867
|
+
try {
|
|
2868
|
+
return c.json(await commit(p.path, { message, paths }));
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2871
|
+
throw err;
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
app.post("/projects/:projectId/git/discard", async (c) => {
|
|
2875
|
+
const p = await need(c);
|
|
2876
|
+
if (p instanceof Response) return p;
|
|
2877
|
+
const body = await readBody(c);
|
|
2878
|
+
if (!body) return c.json({ error: "invalid JSON body" }, 400);
|
|
2879
|
+
const paths = toPaths(body.paths);
|
|
2312
2880
|
if (paths.length === 0) return c.json({ error: "paths must not be empty" }, 400);
|
|
2313
2881
|
try {
|
|
2314
2882
|
await discard(p.path, { paths });
|
|
@@ -2318,25 +2886,23 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2318
2886
|
throw err;
|
|
2319
2887
|
}
|
|
2320
2888
|
});
|
|
2321
|
-
app.post("/projects/:projectId/
|
|
2889
|
+
app.post("/projects/:projectId/git/push", async (c) => {
|
|
2322
2890
|
const p = await need(c);
|
|
2323
2891
|
if (p instanceof Response) return p;
|
|
2324
|
-
const specId = c.req.param("id");
|
|
2325
|
-
const detail = await p.store.read(specId);
|
|
2326
|
-
if (!detail) return c.json({ error: "spec not found" }, 404);
|
|
2327
|
-
let body;
|
|
2328
2892
|
try {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2893
|
+
const result = await push(p.path);
|
|
2894
|
+
return c.json({ ok: true, ...result });
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2897
|
+
throw err;
|
|
2332
2898
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
const
|
|
2336
|
-
if (
|
|
2899
|
+
});
|
|
2900
|
+
app.post("/projects/:projectId/git/pull", async (c) => {
|
|
2901
|
+
const p = await need(c);
|
|
2902
|
+
if (p instanceof Response) return p;
|
|
2337
2903
|
try {
|
|
2338
|
-
await
|
|
2339
|
-
return c.json({ ok: true });
|
|
2904
|
+
const result = await pull(p.path);
|
|
2905
|
+
return c.json({ ok: true, ...result });
|
|
2340
2906
|
} catch (err) {
|
|
2341
2907
|
if (err instanceof GitError) return c.json({ error: err.message }, 400);
|
|
2342
2908
|
throw err;
|
|
@@ -2344,24 +2910,6 @@ function createSpecReviewRoutes(resolveProject) {
|
|
|
2344
2910
|
});
|
|
2345
2911
|
return app;
|
|
2346
2912
|
}
|
|
2347
|
-
function buildGitOpsPrompt(action, specId, specRel) {
|
|
2348
|
-
const base = `${skillRef("yorz-git-ops")},然后执行 git 操作:
|
|
2349
|
-
- spec 文档:${specRel}
|
|
2350
|
-
- 请依据 spec 文档与 git status/diff 自行判断本次 spec 关联的变更文件
|
|
2351
|
-
- spec-id:${specId}
|
|
2352
|
-
`;
|
|
2353
|
-
switch (action) {
|
|
2354
|
-
case "commit":
|
|
2355
|
-
return base + `- 动作:git-commit。请基于 spec 文档与 git status/diff,由你自主判断本次 spec 相关的变更文件,执行 \`git add\` + \`git commit\`;commit message 由你生成,不带 scope;禁止 \`git push\` 与 \`git reset --hard\`。
|
|
2356
|
-
`;
|
|
2357
|
-
case "discard":
|
|
2358
|
-
return base + `- 动作:git-discard。请使用 \`git restore --staged --worktree -- <paths>\` 与 \`git clean -fd -- <paths>\` 丢弃 spec 相关的未提交变更;对 untracked 新文件应先在终端输出列表再处理;不要预先 stash 备份;禁止 \`git reset --hard\` 与 \`git push\`。
|
|
2359
|
-
`;
|
|
2360
|
-
case "stash":
|
|
2361
|
-
return base + `- 动作:git-stash。请使用 \`git stash push -m "yorz:${specId}" -- <paths>\` 暂存 spec 相关变更文件;禁止 \`git push\`。
|
|
2362
|
-
`;
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
2913
|
const sseLog = () => getLogger().child("sse");
|
|
2366
2914
|
const HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2367
2915
|
function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
|
|
@@ -2553,9 +3101,8 @@ class EventsHub {
|
|
|
2553
3101
|
if (rest === "commands") return this.attachCommandRuns(s, topic, project);
|
|
2554
3102
|
const cm = /^command:(.+)$/.exec(rest);
|
|
2555
3103
|
if (cm) return this.attachCommandRun(s, topic, project, cm[1]);
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
sm = /^spec:([^:]+)$/.exec(rest);
|
|
3104
|
+
if (rest === "changes") return this.attachProjectChanges(s, topic, project);
|
|
3105
|
+
let sm = /^spec:([^:]+)$/.exec(rest);
|
|
2559
3106
|
if (sm) return this.attachSpec(s, topic, project, sm[1]);
|
|
2560
3107
|
sm = /^session:(.+)$/.exec(rest);
|
|
2561
3108
|
if (sm) return this.attachSession(s, topic, project, sm[1]);
|
|
@@ -2593,9 +3140,12 @@ class EventsHub {
|
|
|
2593
3140
|
this.emit(s, topic, "updated", { type: kind, mtime });
|
|
2594
3141
|
});
|
|
2595
3142
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
3143
|
+
/**
|
|
3144
|
+
* Git changes are repository-wide: the watcher is keyed on the project path,
|
|
3145
|
+
* so this topic carries no spec id and both the standalone Git page and the
|
|
3146
|
+
* spec Review page share one subscription.
|
|
3147
|
+
*/
|
|
3148
|
+
attachProjectChanges(s, topic, project) {
|
|
2599
3149
|
this.emit(s, topic, "ready", {});
|
|
2600
3150
|
const { current, unsubscribe } = subscribeGitChanges(project.path, (changes) => {
|
|
2601
3151
|
this.emit(s, topic, "changes-updated", { changes });
|
|
@@ -3986,6 +4536,15 @@ class CommandManager {
|
|
|
3986
4536
|
this.notifyRuns();
|
|
3987
4537
|
this.notifyRun(next);
|
|
3988
4538
|
log$3().info("command ended", { runId, status, exitCode, signal });
|
|
4539
|
+
getTelemetry(this.projectPath).record("cmd.exec", {
|
|
4540
|
+
commandId: next.commandId,
|
|
4541
|
+
name: next.name,
|
|
4542
|
+
runId,
|
|
4543
|
+
status,
|
|
4544
|
+
exitCode: exitCode ?? void 0,
|
|
4545
|
+
signal: signal ?? void 0,
|
|
4546
|
+
durMs: (next.endedAt ?? Date.now()) - next.startedAt
|
|
4547
|
+
});
|
|
3989
4548
|
}
|
|
3990
4549
|
notifyRuns() {
|
|
3991
4550
|
if (this.runsSubscribers.size === 0) return;
|
|
@@ -4934,7 +5493,9 @@ function createApp(opts) {
|
|
|
4934
5493
|
const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
|
|
4935
5494
|
void main.sessions.send(
|
|
4936
5495
|
sessionId,
|
|
4937
|
-
`${skillRef("yorz-spec")},然后处理 spec:${main.specsDirRelative}/${specId}/spec.md
|
|
5496
|
+
`${skillRef("yorz-spec")},然后处理 spec:${main.specsDirRelative}/${specId}/spec.md`,
|
|
5497
|
+
void 0,
|
|
5498
|
+
{ trigger: "conflict", specId }
|
|
4938
5499
|
);
|
|
4939
5500
|
}
|
|
4940
5501
|
});
|
|
@@ -4947,6 +5508,7 @@ function createApp(opts) {
|
|
|
4947
5508
|
api.route("/", createSpecsRoutes(resolveProject));
|
|
4948
5509
|
api.route("/", createSessionsRoutes(resolveProject));
|
|
4949
5510
|
api.route("/", createSpecReviewRoutes(resolveProject));
|
|
5511
|
+
api.route("/", createGitRoutes(resolveProject));
|
|
4950
5512
|
api.route("/", createSpecDraftsRoutes(resolveProject));
|
|
4951
5513
|
api.route("/", createWorktreeRoutes(opts.registry, worktreeManager));
|
|
4952
5514
|
api.route("/", createProjectFilesRoutes(resolveProject));
|
|
@@ -5586,6 +6148,29 @@ function usageWindows(usage) {
|
|
|
5586
6148
|
}
|
|
5587
6149
|
return out;
|
|
5588
6150
|
}
|
|
6151
|
+
function num$1(value) {
|
|
6152
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
6153
|
+
}
|
|
6154
|
+
function isRecord(value) {
|
|
6155
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6156
|
+
}
|
|
6157
|
+
function withCost(usage, costUsd) {
|
|
6158
|
+
if (costUsd === void 0) return usage;
|
|
6159
|
+
return { ...usage ?? {}, costUsd };
|
|
6160
|
+
}
|
|
6161
|
+
function primaryModel(modelUsage) {
|
|
6162
|
+
if (!isRecord(modelUsage)) return void 0;
|
|
6163
|
+
let best;
|
|
6164
|
+
let bestOutput = -1;
|
|
6165
|
+
for (const [model, raw] of Object.entries(modelUsage)) {
|
|
6166
|
+
const output = isRecord(raw) ? num$1(raw.outputTokens) ?? 0 : 0;
|
|
6167
|
+
if (output > bestOutput) {
|
|
6168
|
+
bestOutput = output;
|
|
6169
|
+
best = model;
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
return best;
|
|
6173
|
+
}
|
|
5589
6174
|
class ClaudeSession {
|
|
5590
6175
|
constructor(id, cwd, isNew) {
|
|
5591
6176
|
this.id = id;
|
|
@@ -5612,6 +6197,7 @@ class ClaudeSession {
|
|
|
5612
6197
|
try {
|
|
5613
6198
|
const q = query({ prompt, options });
|
|
5614
6199
|
let usage;
|
|
6200
|
+
let metrics;
|
|
5615
6201
|
let completed = false;
|
|
5616
6202
|
for await (const msg of q) {
|
|
5617
6203
|
const m = msg;
|
|
@@ -5636,12 +6222,32 @@ class ClaudeSession {
|
|
|
5636
6222
|
}
|
|
5637
6223
|
} else if (m.type === "result") {
|
|
5638
6224
|
usage = m.usage;
|
|
6225
|
+
metrics = {
|
|
6226
|
+
usage: withCost(normalizeUsage("claude", m.usage), num$1(m.total_cost_usd)),
|
|
6227
|
+
modelUsage: isRecord(m.modelUsage) ? m.modelUsage : void 0,
|
|
6228
|
+
numTurns: num$1(m.num_turns),
|
|
6229
|
+
stopReason: typeof m.stop_reason === "string" ? m.stop_reason : void 0,
|
|
6230
|
+
model: primaryModel(m.modelUsage),
|
|
6231
|
+
durationMs: num$1(m.duration_ms),
|
|
6232
|
+
apiDurationMs: num$1(m.duration_api_ms)
|
|
6233
|
+
};
|
|
6234
|
+
} else if (m.type === "system" && m.subtype === "compact_boundary") {
|
|
6235
|
+
const meta = m.compact_metadata ?? {};
|
|
6236
|
+
yield {
|
|
6237
|
+
type: "compact",
|
|
6238
|
+
metrics: {
|
|
6239
|
+
trigger: meta.trigger === "manual" || meta.trigger === "auto" ? meta.trigger : void 0,
|
|
6240
|
+
preTokens: num$1(meta.pre_tokens),
|
|
6241
|
+
postTokens: num$1(meta.post_tokens),
|
|
6242
|
+
durationMs: num$1(meta.duration_ms)
|
|
6243
|
+
}
|
|
6244
|
+
};
|
|
5639
6245
|
} else if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
|
|
5640
6246
|
completed = true;
|
|
5641
|
-
yield { type: "turn-completed", usage };
|
|
6247
|
+
yield { type: "turn-completed", usage, metrics };
|
|
5642
6248
|
}
|
|
5643
6249
|
}
|
|
5644
|
-
if (!completed) yield { type: "turn-completed", usage };
|
|
6250
|
+
if (!completed) yield { type: "turn-completed", usage, metrics };
|
|
5645
6251
|
} catch (err) {
|
|
5646
6252
|
if (ctrl.signal.aborted) return;
|
|
5647
6253
|
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -5924,7 +6530,11 @@ class CodexSession {
|
|
|
5924
6530
|
yield { type: "error", message: item.message };
|
|
5925
6531
|
}
|
|
5926
6532
|
} else if (ev.type === "turn.completed") {
|
|
5927
|
-
yield {
|
|
6533
|
+
yield {
|
|
6534
|
+
type: "turn-completed",
|
|
6535
|
+
usage: ev.usage,
|
|
6536
|
+
metrics: { usage: normalizeUsage("codex", ev.usage) }
|
|
6537
|
+
};
|
|
5928
6538
|
} else if (ev.type === "turn.failed") {
|
|
5929
6539
|
yield { type: "error", message: ev.error.message };
|
|
5930
6540
|
} else if (ev.type === "error") {
|
|
@@ -6310,7 +6920,14 @@ class OpenCodeSession {
|
|
|
6310
6920
|
for (const part of textParts(data.parts)) {
|
|
6311
6921
|
if (part.type === "text") yield { type: "text", delta: part.text };
|
|
6312
6922
|
}
|
|
6313
|
-
yield {
|
|
6923
|
+
yield {
|
|
6924
|
+
type: "turn-completed",
|
|
6925
|
+
usage: data.info.tokens,
|
|
6926
|
+
metrics: {
|
|
6927
|
+
usage: normalizeUsage("opencode", data.info.tokens),
|
|
6928
|
+
model: typeof data.info.modelID === "string" ? data.info.modelID : void 0
|
|
6929
|
+
}
|
|
6930
|
+
};
|
|
6314
6931
|
} catch (err) {
|
|
6315
6932
|
if (this.aborted) return;
|
|
6316
6933
|
yield { type: "error", message: err instanceof Error ? err.message : String(err) };
|
|
@@ -6678,9 +7295,11 @@ class SessionManager {
|
|
|
6678
7295
|
* text while sending an expanded prompt to the Agent. Without it, an injected
|
|
6679
7296
|
* hidden prompt would become the title.
|
|
6680
7297
|
*/
|
|
6681
|
-
async send(sid, prompt, titleSource) {
|
|
7298
|
+
async send(sid, prompt, titleSource, meta = {}) {
|
|
6682
7299
|
const runId = randomUUID();
|
|
6683
7300
|
const emitter = this.emitterFor(sid);
|
|
7301
|
+
const telemetry = getTelemetry(this.cwd);
|
|
7302
|
+
const attribution = { specId: meta.specId, trigger: meta.trigger };
|
|
6684
7303
|
let currentSid = sid;
|
|
6685
7304
|
const startedAt = Date.now();
|
|
6686
7305
|
await this.maybeUpdateTitleFromPrompt(currentSid, titleSource ?? prompt).catch(() => {
|
|
@@ -6689,18 +7308,57 @@ class SessionManager {
|
|
|
6689
7308
|
});
|
|
6690
7309
|
this.setRunning(sid, true);
|
|
6691
7310
|
agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
|
|
7311
|
+
telemetry.record("agent.dispatch", {
|
|
7312
|
+
...attribution,
|
|
7313
|
+
phase: "start",
|
|
7314
|
+
sessionId: sid,
|
|
7315
|
+
traceId: runId,
|
|
7316
|
+
promptLength: prompt.length
|
|
7317
|
+
});
|
|
7318
|
+
let agentKind;
|
|
7319
|
+
let dispatchError;
|
|
6692
7320
|
const task = (async () => {
|
|
6693
7321
|
try {
|
|
6694
7322
|
const ls = await this.ensureLive(sid);
|
|
7323
|
+
agentKind = ls.kind;
|
|
6695
7324
|
for await (const ev of ls.session.send(prompt)) {
|
|
6696
7325
|
if (ev.type === "session-started" && ev.sessionId !== currentSid) {
|
|
6697
7326
|
const oldSid = currentSid;
|
|
6698
7327
|
currentSid = ev.sessionId;
|
|
6699
7328
|
await this.reconcile(oldSid, ev.sessionId);
|
|
7329
|
+
} else if (ev.type === "turn-completed") {
|
|
7330
|
+
const m = ev.metrics ?? {};
|
|
7331
|
+
telemetry.record("agent.turn", {
|
|
7332
|
+
...attribution,
|
|
7333
|
+
sessionId: currentSid,
|
|
7334
|
+
traceId: runId,
|
|
7335
|
+
agentKind,
|
|
7336
|
+
usage: m.usage,
|
|
7337
|
+
modelUsage: m.modelUsage,
|
|
7338
|
+
numTurns: m.numTurns,
|
|
7339
|
+
stopReason: m.stopReason,
|
|
7340
|
+
model: m.model,
|
|
7341
|
+
durMs: m.durationMs,
|
|
7342
|
+
apiDurMs: m.apiDurationMs
|
|
7343
|
+
});
|
|
7344
|
+
} else if (ev.type === "compact") {
|
|
7345
|
+
telemetry.record("agent.compact", {
|
|
7346
|
+
...attribution,
|
|
7347
|
+
sessionId: currentSid,
|
|
7348
|
+
traceId: runId,
|
|
7349
|
+
agentKind,
|
|
7350
|
+
// Named apart from the dispatch `trigger` so both survive on the
|
|
7351
|
+
// same line: which action started the run, and what compacted it.
|
|
7352
|
+
compactTrigger: ev.metrics.trigger,
|
|
7353
|
+
preTokens: ev.metrics.preTokens,
|
|
7354
|
+
postTokens: ev.metrics.postTokens,
|
|
7355
|
+
durMs: ev.metrics.durationMs
|
|
7356
|
+
});
|
|
6700
7357
|
}
|
|
6701
7358
|
emitter.emit("event", ev);
|
|
6702
7359
|
}
|
|
6703
7360
|
} catch (err) {
|
|
7361
|
+
dispatchError = err instanceof Error ? err.message : String(err);
|
|
6704
7362
|
agentLog().error("dispatch failed", {
|
|
6705
7363
|
sessionId: currentSid,
|
|
6706
7364
|
runId,
|
|
@@ -6718,6 +7376,17 @@ class SessionManager {
|
|
|
6718
7376
|
runId,
|
|
6719
7377
|
durationMs: Date.now() - startedAt
|
|
6720
7378
|
});
|
|
7379
|
+
telemetry.record("agent.dispatch", {
|
|
7380
|
+
...attribution,
|
|
7381
|
+
phase: "end",
|
|
7382
|
+
sessionId: currentSid,
|
|
7383
|
+
traceId: runId,
|
|
7384
|
+
agentKind,
|
|
7385
|
+
promptLength: prompt.length,
|
|
7386
|
+
durMs: Date.now() - startedAt,
|
|
7387
|
+
ok: dispatchError === void 0,
|
|
7388
|
+
errorMessage: dispatchError
|
|
7389
|
+
});
|
|
6721
7390
|
await this.store.touch(currentSid).catch(() => {
|
|
6722
7391
|
});
|
|
6723
7392
|
this.setRunning(currentSid, false);
|
|
@@ -7271,6 +7940,12 @@ async function start(opts = {}) {
|
|
|
7271
7940
|
} catch {
|
|
7272
7941
|
}
|
|
7273
7942
|
}
|
|
7943
|
+
try {
|
|
7944
|
+
const stats = await initTelemetryStore();
|
|
7945
|
+
log$1().info("telemetry store ready", { ...stats });
|
|
7946
|
+
} catch (err) {
|
|
7947
|
+
log$1().warn("telemetry housekeeping failed", { err });
|
|
7948
|
+
}
|
|
7274
7949
|
const projects = await registry.list();
|
|
7275
7950
|
const systemNotifications = new SystemNotificationCenter();
|
|
7276
7951
|
systemNotifications.startVersionChecks();
|
|
@@ -8909,6 +9584,7 @@ async function lintFile(path, opts = {}) {
|
|
|
8909
9584
|
}
|
|
8910
9585
|
[...new Set([...SPEC_RULES, ...REVIEW_RULES].map((r) => r.id))];
|
|
8911
9586
|
async function runLint(opts) {
|
|
9587
|
+
const startedAt = Date.now();
|
|
8912
9588
|
const cwd = resolve(opts.cwd);
|
|
8913
9589
|
let targets = [];
|
|
8914
9590
|
if (opts.all) {
|
|
@@ -8978,8 +9654,219 @@ async function runLint(opts) {
|
|
|
8978
9654
|
Total: ${errorCount} error(s), ${warnCount} warn(s) across ${reports.length} file(s).
|
|
8979
9655
|
`);
|
|
8980
9656
|
}
|
|
9657
|
+
await recordLintRun(cwd, {
|
|
9658
|
+
fileCount: targets.length,
|
|
9659
|
+
errorCount,
|
|
9660
|
+
warnCount,
|
|
9661
|
+
exitCode,
|
|
9662
|
+
durMs: Date.now() - startedAt
|
|
9663
|
+
});
|
|
8981
9664
|
return { reports, errorCount, warnCount, exitCode };
|
|
8982
9665
|
}
|
|
9666
|
+
async function recordLintRun(cwd, payload) {
|
|
9667
|
+
const root = findProjectRoot(cwd);
|
|
9668
|
+
if (!root) return;
|
|
9669
|
+
const telemetry = getTelemetry(root);
|
|
9670
|
+
telemetry.record("lint.run", payload);
|
|
9671
|
+
await telemetry.flush();
|
|
9672
|
+
}
|
|
9673
|
+
const UNATTRIBUTED = "(no spec)";
|
|
9674
|
+
async function runMetrics(opts) {
|
|
9675
|
+
const file = resolveTelemetryFile();
|
|
9676
|
+
if (!existsSync(file)) {
|
|
9677
|
+
process.stderr.write("no telemetry found — run a dispatch first\n");
|
|
9678
|
+
return { exitCode: 1, summary: emptySummary() };
|
|
9679
|
+
}
|
|
9680
|
+
const filter = resolveProjectFilter(opts);
|
|
9681
|
+
const summary = emptySummary();
|
|
9682
|
+
summary.files.push(file);
|
|
9683
|
+
await consumeFile(file, opts, filter, summary);
|
|
9684
|
+
if (summary.lines === 0) {
|
|
9685
|
+
process.stderr.write(
|
|
9686
|
+
filter ? `no telemetry for project ${filter} — pass --project <id|path> / --all
|
|
9687
|
+
` : "no telemetry matched the given filters\n"
|
|
9688
|
+
);
|
|
9689
|
+
return { exitCode: 1, summary };
|
|
9690
|
+
}
|
|
9691
|
+
summary.projects = await projectLabels(summary.projects);
|
|
9692
|
+
summary.specs.sort((a, b) => b.costUsd - a.costUsd || b.turns - a.turns);
|
|
9693
|
+
if (opts.format === "json") {
|
|
9694
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}
|
|
9695
|
+
`);
|
|
9696
|
+
} else {
|
|
9697
|
+
process.stdout.write(renderText(summary));
|
|
9698
|
+
}
|
|
9699
|
+
return { exitCode: 0, summary };
|
|
9700
|
+
}
|
|
9701
|
+
function resolveProjectFilter(opts) {
|
|
9702
|
+
if (opts.all) return null;
|
|
9703
|
+
if (opts.project) {
|
|
9704
|
+
return isAbsolute(opts.project) ? generateProjectId(resolve(opts.project)) : opts.project;
|
|
9705
|
+
}
|
|
9706
|
+
const root = findProjectRoot(opts.cwd);
|
|
9707
|
+
return root ? generateProjectId(root) : null;
|
|
9708
|
+
}
|
|
9709
|
+
async function projectLabels(ids) {
|
|
9710
|
+
let index = {};
|
|
9711
|
+
try {
|
|
9712
|
+
index = JSON.parse(await readFile(resolveProjectsIndexFile(), "utf8"));
|
|
9713
|
+
} catch {
|
|
9714
|
+
}
|
|
9715
|
+
return ids.map((id) => index[id]?.path ? `${id} (${index[id].path})` : id);
|
|
9716
|
+
}
|
|
9717
|
+
function sinceMs(since) {
|
|
9718
|
+
if (!since) return null;
|
|
9719
|
+
const ms = Date.parse(`${since}T00:00:00`);
|
|
9720
|
+
return Number.isNaN(ms) ? null : ms;
|
|
9721
|
+
}
|
|
9722
|
+
async function consumeFile(file, opts, filter, out) {
|
|
9723
|
+
let raw;
|
|
9724
|
+
try {
|
|
9725
|
+
raw = await readFile(file, "utf8");
|
|
9726
|
+
} catch {
|
|
9727
|
+
return;
|
|
9728
|
+
}
|
|
9729
|
+
const from = sinceMs(opts.since);
|
|
9730
|
+
const seenProjects = /* @__PURE__ */ new Set();
|
|
9731
|
+
for (const line of raw.split("\n")) {
|
|
9732
|
+
if (!line.trim()) continue;
|
|
9733
|
+
let ev;
|
|
9734
|
+
try {
|
|
9735
|
+
ev = JSON.parse(line);
|
|
9736
|
+
} catch {
|
|
9737
|
+
out.skipped += 1;
|
|
9738
|
+
continue;
|
|
9739
|
+
}
|
|
9740
|
+
const projectId = typeof ev.projectId === "string" ? ev.projectId : "";
|
|
9741
|
+
if (filter && projectId !== filter) continue;
|
|
9742
|
+
const ts = parseTs(ev.ts);
|
|
9743
|
+
if (from !== null && (ts === null || ts < from)) continue;
|
|
9744
|
+
if (projectId && !seenProjects.has(projectId)) {
|
|
9745
|
+
seenProjects.add(projectId);
|
|
9746
|
+
out.projects.push(projectId);
|
|
9747
|
+
}
|
|
9748
|
+
out.lines += 1;
|
|
9749
|
+
const event = typeof ev.event === "string" ? ev.event : "unknown";
|
|
9750
|
+
out.eventCounts[event] = (out.eventCounts[event] ?? 0) + 1;
|
|
9751
|
+
applyEvent(ev, event, out);
|
|
9752
|
+
}
|
|
9753
|
+
}
|
|
9754
|
+
function applyEvent(ev, event, out) {
|
|
9755
|
+
if (!event.startsWith("agent.")) return;
|
|
9756
|
+
const bucket = bucketFor(out, typeof ev.specId === "string" ? ev.specId : UNATTRIBUTED);
|
|
9757
|
+
if (event === "agent.dispatch") {
|
|
9758
|
+
if (ev.phase !== "end") return;
|
|
9759
|
+
bucket.dispatches += 1;
|
|
9760
|
+
out.totals.dispatches += 1;
|
|
9761
|
+
if (ev.ok === false) {
|
|
9762
|
+
bucket.failures += 1;
|
|
9763
|
+
out.totals.failures += 1;
|
|
9764
|
+
}
|
|
9765
|
+
return;
|
|
9766
|
+
}
|
|
9767
|
+
if (event === "agent.compact") {
|
|
9768
|
+
bucket.compactions += 1;
|
|
9769
|
+
out.totals.compactions += 1;
|
|
9770
|
+
return;
|
|
9771
|
+
}
|
|
9772
|
+
if (event !== "agent.turn") return;
|
|
9773
|
+
bucket.turns += 1;
|
|
9774
|
+
out.totals.turns += 1;
|
|
9775
|
+
const durMs = num(ev.durMs);
|
|
9776
|
+
bucket.agentDurMs += durMs;
|
|
9777
|
+
out.totals.agentDurMs += durMs;
|
|
9778
|
+
const usage = ev.usage ?? {};
|
|
9779
|
+
for (const key of [
|
|
9780
|
+
"inputTokens",
|
|
9781
|
+
"cacheReadTokens",
|
|
9782
|
+
"cacheCreateTokens",
|
|
9783
|
+
"outputTokens"
|
|
9784
|
+
]) {
|
|
9785
|
+
bucket[key] += num(usage[key]);
|
|
9786
|
+
out.totals[key] += num(usage[key]);
|
|
9787
|
+
}
|
|
9788
|
+
bucket.costUsd += num(usage.costUsd);
|
|
9789
|
+
out.totals.costUsd += num(usage.costUsd);
|
|
9790
|
+
}
|
|
9791
|
+
function bucketFor(out, specId) {
|
|
9792
|
+
let bucket = out.specs.find((s) => s.specId === specId);
|
|
9793
|
+
if (!bucket) {
|
|
9794
|
+
bucket = {
|
|
9795
|
+
specId,
|
|
9796
|
+
dispatches: 0,
|
|
9797
|
+
turns: 0,
|
|
9798
|
+
compactions: 0,
|
|
9799
|
+
inputTokens: 0,
|
|
9800
|
+
cacheReadTokens: 0,
|
|
9801
|
+
cacheCreateTokens: 0,
|
|
9802
|
+
outputTokens: 0,
|
|
9803
|
+
costUsd: 0,
|
|
9804
|
+
agentDurMs: 0,
|
|
9805
|
+
failures: 0,
|
|
9806
|
+
stages: []
|
|
9807
|
+
};
|
|
9808
|
+
out.specs.push(bucket);
|
|
9809
|
+
}
|
|
9810
|
+
return bucket;
|
|
9811
|
+
}
|
|
9812
|
+
function num(value) {
|
|
9813
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
9814
|
+
}
|
|
9815
|
+
function emptySummary() {
|
|
9816
|
+
return {
|
|
9817
|
+
projects: [],
|
|
9818
|
+
files: [],
|
|
9819
|
+
lines: 0,
|
|
9820
|
+
skipped: 0,
|
|
9821
|
+
eventCounts: {},
|
|
9822
|
+
totals: {
|
|
9823
|
+
dispatches: 0,
|
|
9824
|
+
turns: 0,
|
|
9825
|
+
compactions: 0,
|
|
9826
|
+
inputTokens: 0,
|
|
9827
|
+
cacheReadTokens: 0,
|
|
9828
|
+
cacheCreateTokens: 0,
|
|
9829
|
+
outputTokens: 0,
|
|
9830
|
+
costUsd: 0,
|
|
9831
|
+
agentDurMs: 0,
|
|
9832
|
+
failures: 0
|
|
9833
|
+
},
|
|
9834
|
+
specs: []
|
|
9835
|
+
};
|
|
9836
|
+
}
|
|
9837
|
+
const SPEC_ROW_LIMIT = 20;
|
|
9838
|
+
function renderText(s) {
|
|
9839
|
+
const lines = [];
|
|
9840
|
+
lines.push(`projects: ${s.projects.join(", ") || "(none)"}`);
|
|
9841
|
+
lines.push(`events: ${s.lines}${s.skipped ? ` (${s.skipped} unparseable line(s))` : ""}`);
|
|
9842
|
+
const kinds = Object.entries(s.eventCounts).sort((a, b) => b[1] - a[1]);
|
|
9843
|
+
if (kinds.length) {
|
|
9844
|
+
lines.push(`by kind: ${kinds.map(([k, n]) => `${k}=${n}`).join(" ")}`);
|
|
9845
|
+
}
|
|
9846
|
+
const t = s.totals;
|
|
9847
|
+
lines.push("");
|
|
9848
|
+
lines.push(
|
|
9849
|
+
`total: ${t.dispatches} dispatch(es), ${t.turns} turn(s), ${t.failures} failure(s), ${t.compactions} compaction(s)`
|
|
9850
|
+
);
|
|
9851
|
+
lines.push(
|
|
9852
|
+
`tokens: in=${t.inputTokens} cacheRead=${t.cacheReadTokens} cacheWrite=${t.cacheCreateTokens} out=${t.outputTokens}`
|
|
9853
|
+
);
|
|
9854
|
+
lines.push(`cost: $${t.costUsd.toFixed(4)} agent time: ${(t.agentDurMs / 1e3).toFixed(1)}s`);
|
|
9855
|
+
if (s.specs.length) {
|
|
9856
|
+
lines.push("");
|
|
9857
|
+
lines.push("per spec (by cost):");
|
|
9858
|
+
for (const spec of s.specs.slice(0, SPEC_ROW_LIMIT)) {
|
|
9859
|
+
lines.push(
|
|
9860
|
+
` ${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}`
|
|
9861
|
+
);
|
|
9862
|
+
}
|
|
9863
|
+
if (s.specs.length > SPEC_ROW_LIMIT) {
|
|
9864
|
+
lines.push(` … ${s.specs.length - SPEC_ROW_LIMIT} more spec(s) not shown (use --format json)`);
|
|
9865
|
+
}
|
|
9866
|
+
}
|
|
9867
|
+
return `${lines.join("\n")}
|
|
9868
|
+
`;
|
|
9869
|
+
}
|
|
8983
9870
|
const program = new Command();
|
|
8984
9871
|
program.name("yorz").description("YorZ CLI — manage the yorz-spec skill.").version(pkg.version);
|
|
8985
9872
|
const uninstallCmd = program.command("uninstall").description("Remove YorZ artifacts (skills, ...).");
|
|
@@ -9043,6 +9930,18 @@ program.command("lint [paths...]").description("Lint spec.md / review.md files f
|
|
|
9043
9930
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9044
9931
|
}
|
|
9045
9932
|
);
|
|
9933
|
+
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(
|
|
9934
|
+
async (opts) => {
|
|
9935
|
+
const result = await runMetrics({
|
|
9936
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
9937
|
+
format: opts.format === "json" ? "json" : "text",
|
|
9938
|
+
project: opts.project,
|
|
9939
|
+
all: opts.all === true,
|
|
9940
|
+
since: opts.since
|
|
9941
|
+
});
|
|
9942
|
+
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
9943
|
+
}
|
|
9944
|
+
);
|
|
9046
9945
|
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(
|
|
9047
9946
|
"--host <address>",
|
|
9048
9947
|
"bind address (default: 127.0.0.1; the service runs shell commands, so exposing it to the network is opt-in)"
|