@curdx/flow 7.1.5 → 7.1.7
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/CHANGELOG.md +87 -0
- package/README.md +14 -0
- package/dist/analyze-FX2PCSL6.mjs +1956 -0
- package/dist/check-TJPGCG3Z.mjs +222 -0
- package/dist/index.mjs +112 -107
- package/package.json +2 -2
- package/dist/analyze-4DE3HVCA.mjs +0 -794
|
@@ -1,794 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/analyze/index.ts
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, statSync as statSync2, writeFileSync } from "fs";
|
|
5
|
-
import { homedir } from "os";
|
|
6
|
-
import path3 from "path";
|
|
7
|
-
|
|
8
|
-
// src/analyze/filter.ts
|
|
9
|
-
function parseSince(since) {
|
|
10
|
-
if (!since) return void 0;
|
|
11
|
-
const m = /^(\d+)d$/.exec(since);
|
|
12
|
-
if (m) {
|
|
13
|
-
const days = Number(m[1]);
|
|
14
|
-
return new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
15
|
-
}
|
|
16
|
-
const d = new Date(since);
|
|
17
|
-
if (!Number.isNaN(d.getTime())) return d;
|
|
18
|
-
return void 0;
|
|
19
|
-
}
|
|
20
|
-
function filterEvents(events, opts) {
|
|
21
|
-
const sinceDate = parseSince(opts.since);
|
|
22
|
-
const seen = /* @__PURE__ */ new Set();
|
|
23
|
-
const out = [];
|
|
24
|
-
for (const ev of events) {
|
|
25
|
-
if (sinceDate && ev.ts) {
|
|
26
|
-
const evDate = new Date(ev.ts);
|
|
27
|
-
if (!Number.isNaN(evDate.getTime()) && evDate < sinceDate) continue;
|
|
28
|
-
}
|
|
29
|
-
const dedupeKey = computeDedupeKey(ev);
|
|
30
|
-
if (dedupeKey !== void 0) {
|
|
31
|
-
if (seen.has(dedupeKey)) continue;
|
|
32
|
-
seen.add(dedupeKey);
|
|
33
|
-
}
|
|
34
|
-
void opts.project;
|
|
35
|
-
out.push(ev);
|
|
36
|
-
}
|
|
37
|
-
out.sort((a, b) => a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0);
|
|
38
|
-
if (typeof opts.limit === "number" && opts.limit > 0) {
|
|
39
|
-
return out.slice(0, opts.limit);
|
|
40
|
-
}
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
|
-
function computeDedupeKey(ev) {
|
|
44
|
-
if (ev.uuid && ev.requestId) return `${ev.uuid}|${ev.requestId}`;
|
|
45
|
-
if (ev.uuid) return ev.uuid;
|
|
46
|
-
return void 0;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// src/analyze/parser.ts
|
|
50
|
-
import { createReadStream, readFileSync, statSync } from "fs";
|
|
51
|
-
import readline from "readline";
|
|
52
|
-
import path from "path";
|
|
53
|
-
import { fileURLToPath } from "url";
|
|
54
|
-
var BUILTIN_KIND_MAP = {
|
|
55
|
-
hook_success: "hook_invocation",
|
|
56
|
-
attachment: "hook_invocation",
|
|
57
|
-
// overridden by attachment.type when present
|
|
58
|
-
tool_use: "tool_call",
|
|
59
|
-
assistant: "assistant_turn",
|
|
60
|
-
user: "user_turn"
|
|
61
|
-
};
|
|
62
|
-
var BUILTIN_SCHEMA_MAP = {
|
|
63
|
-
hook_success: {
|
|
64
|
-
action: "hook_invocation",
|
|
65
|
-
fields: ["hookName", "hookEvent", "exitCode", "durationMs", "stderr"],
|
|
66
|
-
stderrMaxBytes: 500
|
|
67
|
-
},
|
|
68
|
-
tool_use: {
|
|
69
|
-
action: "tool_call",
|
|
70
|
-
fields: ["name", "input.subagent_type"],
|
|
71
|
-
filter: { name: ["Agent", "Task"] }
|
|
72
|
-
},
|
|
73
|
-
assistant: {
|
|
74
|
-
action: "assistant_turn",
|
|
75
|
-
fields: ["attributionPlugin", "attributionSkill"]
|
|
76
|
-
},
|
|
77
|
-
user: {
|
|
78
|
-
action: "user_turn",
|
|
79
|
-
fields: ["content"],
|
|
80
|
-
extractCommandName: true
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
function loadSchemaMap(schemaPath) {
|
|
84
|
-
const candidates = [];
|
|
85
|
-
if (schemaPath) candidates.push(schemaPath);
|
|
86
|
-
try {
|
|
87
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
88
|
-
candidates.push(
|
|
89
|
-
path.resolve(here, "..", "..", "plugins", "curdx-flow", "schemas", "transcript-events.json"),
|
|
90
|
-
path.resolve(here, "..", "plugins", "curdx-flow", "schemas", "transcript-events.json")
|
|
91
|
-
);
|
|
92
|
-
} catch {
|
|
93
|
-
}
|
|
94
|
-
candidates.push(
|
|
95
|
-
path.resolve(process.cwd(), "plugins", "curdx-flow", "schemas", "transcript-events.json")
|
|
96
|
-
);
|
|
97
|
-
for (const candidate of candidates) {
|
|
98
|
-
try {
|
|
99
|
-
const raw = readFileSync(candidate, "utf8");
|
|
100
|
-
const parsed = JSON.parse(raw);
|
|
101
|
-
if (parsed && typeof parsed === "object" && parsed.events && typeof parsed.events === "object") {
|
|
102
|
-
const out = {};
|
|
103
|
-
for (const [type, def] of Object.entries(parsed.events)) {
|
|
104
|
-
if (!def || typeof def !== "object") continue;
|
|
105
|
-
const d = def;
|
|
106
|
-
if (typeof d.action !== "string") continue;
|
|
107
|
-
out[type] = {
|
|
108
|
-
action: d.action,
|
|
109
|
-
fields: Array.isArray(d.fields) ? d.fields.filter((f) => typeof f === "string") : [],
|
|
110
|
-
...d.filter && typeof d.filter === "object" ? { filter: d.filter } : {},
|
|
111
|
-
...typeof d.extractCommandName === "boolean" ? { extractCommandName: d.extractCommandName } : {},
|
|
112
|
-
...typeof d.stderrMaxBytes === "number" ? { stderrMaxBytes: d.stderrMaxBytes } : {}
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
if (Object.keys(out).length > 0) return out;
|
|
116
|
-
}
|
|
117
|
-
process.stderr.write(`[analyze] schema map at ${candidate} malformed, trying next probe
|
|
118
|
-
`);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
const code = err.code;
|
|
121
|
-
if (code === "ENOENT") continue;
|
|
122
|
-
process.stderr.write(`[analyze] schema map probe failed at ${candidate}: ${err.message}
|
|
123
|
-
`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
process.stderr.write("[analyze] schema map not found, using builtin fallback\n");
|
|
127
|
-
return BUILTIN_SCHEMA_MAP;
|
|
128
|
-
}
|
|
129
|
-
var ACTION_TO_KIND = {
|
|
130
|
-
hook_invocation: "hook_invocation",
|
|
131
|
-
tool_call: "tool_call",
|
|
132
|
-
assistant_turn: "assistant_turn",
|
|
133
|
-
user_turn: "user_turn"
|
|
134
|
-
};
|
|
135
|
-
function classify(raw, schemaMap) {
|
|
136
|
-
const top = typeof raw.type === "string" ? raw.type : void 0;
|
|
137
|
-
if (!top) return void 0;
|
|
138
|
-
let effectiveType = top;
|
|
139
|
-
if (top === "attachment" && raw.attachment && typeof raw.attachment === "object") {
|
|
140
|
-
const att = raw.attachment;
|
|
141
|
-
if (typeof att.type === "string") effectiveType = att.type;
|
|
142
|
-
}
|
|
143
|
-
if (schemaMap && schemaMap[effectiveType]) {
|
|
144
|
-
const action = schemaMap[effectiveType]?.action;
|
|
145
|
-
if (action) {
|
|
146
|
-
const kind = ACTION_TO_KIND[action];
|
|
147
|
-
return kind ?? "unknown";
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return BUILTIN_KIND_MAP[effectiveType];
|
|
151
|
-
}
|
|
152
|
-
function pickString(raw, key) {
|
|
153
|
-
const v = raw[key];
|
|
154
|
-
return typeof v === "string" ? v : void 0;
|
|
155
|
-
}
|
|
156
|
-
async function* parseTranscript(path4, startOffset, schemaMap, counters) {
|
|
157
|
-
const localCounters = counters ?? { unknown_type: 0, parse_error: 0, processed: 0 };
|
|
158
|
-
const stream = createReadStream(path4, { encoding: "utf8", start: startOffset });
|
|
159
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
160
|
-
for await (const line of rl) {
|
|
161
|
-
if (!line) continue;
|
|
162
|
-
let raw;
|
|
163
|
-
try {
|
|
164
|
-
raw = JSON.parse(line);
|
|
165
|
-
} catch {
|
|
166
|
-
localCounters.parse_error += 1;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const kind = classify(raw, schemaMap);
|
|
170
|
-
if (!kind) {
|
|
171
|
-
localCounters.unknown_type += 1;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
localCounters.processed += 1;
|
|
175
|
-
const ts = pickString(raw, "timestamp") ?? pickString(raw, "ts") ?? "";
|
|
176
|
-
const uuid = pickString(raw, "uuid");
|
|
177
|
-
const requestId = pickString(raw, "requestId");
|
|
178
|
-
const cwd = pickString(raw, "cwd");
|
|
179
|
-
yield {
|
|
180
|
-
kind,
|
|
181
|
-
ts,
|
|
182
|
-
...uuid !== void 0 ? { uuid } : {},
|
|
183
|
-
...requestId !== void 0 ? { requestId } : {},
|
|
184
|
-
...cwd !== void 0 ? { cwd } : {},
|
|
185
|
-
payload: raw
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function getStateForPath(path4) {
|
|
190
|
-
const st = statSync(path4);
|
|
191
|
-
return {
|
|
192
|
-
byteOffset: st.size,
|
|
193
|
-
lastModifiedMs: st.mtimeMs,
|
|
194
|
-
sizeBytes: st.size
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function shouldRotate(prev, current) {
|
|
198
|
-
if (!prev) return false;
|
|
199
|
-
if (current.sizeBytes < prev.sizeBytes) return true;
|
|
200
|
-
if (current.lastModifiedMs < prev.lastModifiedMs) return true;
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// src/analyze/redact.ts
|
|
205
|
-
import { createHash } from "crypto";
|
|
206
|
-
import path2 from "path";
|
|
207
|
-
var PATH_FIELDS = /* @__PURE__ */ new Set(["cwd", "path", "file", "transcript_path", "projectPath"]);
|
|
208
|
-
var COMMAND_NAME_RE = /<command-name>([^<]+)<\/command-name>/g;
|
|
209
|
-
function hashProject(absPath) {
|
|
210
|
-
return createHash("sha256").update(absPath).digest("hex").slice(0, 8);
|
|
211
|
-
}
|
|
212
|
-
function redactPath(p) {
|
|
213
|
-
if (!p) return p;
|
|
214
|
-
const base = path2.basename(p) || p;
|
|
215
|
-
return `${base}@${hashProject(p)}`;
|
|
216
|
-
}
|
|
217
|
-
function extractCommandHints(text) {
|
|
218
|
-
const hints = [];
|
|
219
|
-
let m;
|
|
220
|
-
COMMAND_NAME_RE.lastIndex = 0;
|
|
221
|
-
while (m = COMMAND_NAME_RE.exec(text)) {
|
|
222
|
-
if (m[1]) hints.push(m[1].trim());
|
|
223
|
-
}
|
|
224
|
-
return hints;
|
|
225
|
-
}
|
|
226
|
-
function redactEvent(ev, opts = {}) {
|
|
227
|
-
const topType = ev.payload.type;
|
|
228
|
-
if (topType === "file-history-snapshot") return null;
|
|
229
|
-
const att = ev.payload.attachment;
|
|
230
|
-
if (att && typeof att === "object" && att.type === "file-history-snapshot") {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
if (opts.includePrompts) return ev;
|
|
234
|
-
const next = { ...ev, payload: { ...ev.payload } };
|
|
235
|
-
if (typeof next.cwd === "string") next.cwd = redactPath(next.cwd);
|
|
236
|
-
for (const key of PATH_FIELDS) {
|
|
237
|
-
const v = next.payload[key];
|
|
238
|
-
if (typeof v === "string") next.payload[key] = redactPath(v);
|
|
239
|
-
}
|
|
240
|
-
if (next.kind === "user_turn") {
|
|
241
|
-
let totalLength = 0;
|
|
242
|
-
const hints = [];
|
|
243
|
-
const message = next.payload.message;
|
|
244
|
-
if (message && typeof message === "object") {
|
|
245
|
-
const content = message.content;
|
|
246
|
-
if (typeof content === "string") {
|
|
247
|
-
totalLength += Buffer.byteLength(content, "utf8");
|
|
248
|
-
hints.push(...extractCommandHints(content));
|
|
249
|
-
} else if (Array.isArray(content)) {
|
|
250
|
-
for (const part of content) {
|
|
251
|
-
if (part && typeof part === "object") {
|
|
252
|
-
const t = part.text;
|
|
253
|
-
if (typeof t === "string") {
|
|
254
|
-
totalLength += Buffer.byteLength(t, "utf8");
|
|
255
|
-
hints.push(...extractCommandHints(t));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const synthesized = hints.map((h) => ({ type: "text", text: `<command-name>${h}</command-name>` }));
|
|
261
|
-
next.payload.message = { ...message, content: synthesized };
|
|
262
|
-
}
|
|
263
|
-
if (typeof next.payload.content === "string") {
|
|
264
|
-
totalLength += Buffer.byteLength(next.payload.content, "utf8");
|
|
265
|
-
hints.push(...extractCommandHints(next.payload.content));
|
|
266
|
-
delete next.payload.content;
|
|
267
|
-
}
|
|
268
|
-
next.payload.redacted = {
|
|
269
|
-
length: totalLength,
|
|
270
|
-
commandHints: Array.from(new Set(hints))
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
return next;
|
|
274
|
-
}
|
|
275
|
-
function redactReportFields(report, opts = {}) {
|
|
276
|
-
if (opts.includePrompts) return report;
|
|
277
|
-
return walk(report);
|
|
278
|
-
}
|
|
279
|
-
function walk(value) {
|
|
280
|
-
if (Array.isArray(value)) return value.map(walk);
|
|
281
|
-
if (value && typeof value === "object") {
|
|
282
|
-
const out = {};
|
|
283
|
-
for (const [k, v] of Object.entries(value)) {
|
|
284
|
-
if (typeof v === "string" && PATH_FIELDS.has(k)) {
|
|
285
|
-
out[k] = redactPath(v);
|
|
286
|
-
} else {
|
|
287
|
-
out[k] = walk(v);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
return value;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// src/analyze/report.ts
|
|
296
|
-
var DEFAULT_LIMIT = 10;
|
|
297
|
-
var STDERR_TRUNC = 200;
|
|
298
|
-
var FUZZY_TS_WINDOW_MS = 2e3;
|
|
299
|
-
var MIN_SAMPLES_FOR_PCT = 5;
|
|
300
|
-
var COMMAND_NAME_RE2 = /<command-name>([^<]+)<\/command-name>/;
|
|
301
|
-
function escapeCell(s) {
|
|
302
|
-
return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
303
|
-
}
|
|
304
|
-
function truncate(s, max = STDERR_TRUNC) {
|
|
305
|
-
if (!s) return "";
|
|
306
|
-
return s.length <= max ? s : s.slice(0, max);
|
|
307
|
-
}
|
|
308
|
-
function rollupHookFailures(events, errors, limit) {
|
|
309
|
-
const buckets = [];
|
|
310
|
-
for (const ev of events) {
|
|
311
|
-
if (ev.kind !== "hook_invocation") continue;
|
|
312
|
-
const att = ev.payload.attachment;
|
|
313
|
-
if (!att || typeof att !== "object") continue;
|
|
314
|
-
const a = att;
|
|
315
|
-
if (a.type !== "hook_success") continue;
|
|
316
|
-
const hookName = typeof a.hookName === "string" ? a.hookName : void 0;
|
|
317
|
-
const exitCode = typeof a.exitCode === "number" ? a.exitCode : void 0;
|
|
318
|
-
if (!hookName || exitCode === void 0 || exitCode === 0) continue;
|
|
319
|
-
const stderr = truncate(typeof a.stderr === "string" ? a.stderr : "");
|
|
320
|
-
const tsMs = ev.ts ? Date.parse(ev.ts) : NaN;
|
|
321
|
-
buckets.push({
|
|
322
|
-
hook: hookName,
|
|
323
|
-
ts: Number.isFinite(tsMs) ? tsMs : 0,
|
|
324
|
-
...ev.cwd ? { cwd: ev.cwd } : {},
|
|
325
|
-
stderr,
|
|
326
|
-
source: "jsonl"
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
for (const e of errors) {
|
|
330
|
-
if (!e.hook) continue;
|
|
331
|
-
const tsMs = e.ts ? Date.parse(e.ts) : NaN;
|
|
332
|
-
const cwd = e.cwd;
|
|
333
|
-
const tsResolved = Number.isFinite(tsMs) ? tsMs : 0;
|
|
334
|
-
const dup = buckets.find(
|
|
335
|
-
(b) => b.hook === e.hook && Math.abs(b.ts - tsResolved) <= FUZZY_TS_WINDOW_MS && (b.cwd ?? "") === (cwd ?? "")
|
|
336
|
-
);
|
|
337
|
-
if (dup) {
|
|
338
|
-
dup.source = "merged";
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
buckets.push({
|
|
342
|
-
hook: e.hook,
|
|
343
|
-
ts: tsResolved,
|
|
344
|
-
...cwd ? { cwd } : {},
|
|
345
|
-
stderr: truncate(e.msg ?? ""),
|
|
346
|
-
source: "errors.jsonl"
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
const counts = /* @__PURE__ */ new Map();
|
|
350
|
-
for (const b of buckets) {
|
|
351
|
-
const prev = counts.get(b.hook);
|
|
352
|
-
if (prev) {
|
|
353
|
-
prev.count += 1;
|
|
354
|
-
if (b.stderr) prev.lastStderr = b.stderr;
|
|
355
|
-
if (prev.source !== b.source) prev.source = "merged";
|
|
356
|
-
} else {
|
|
357
|
-
counts.set(b.hook, { count: 1, lastStderr: b.stderr, source: b.source });
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
const rows = Array.from(counts.entries()).map(([hook, v]) => ({ hook, count: v.count, lastStderr: v.lastStderr, source: v.source })).sort((a, b) => b.count - a.count);
|
|
361
|
-
return rows.slice(0, limit);
|
|
362
|
-
}
|
|
363
|
-
function renderHookFailures(rows, limit) {
|
|
364
|
-
const lines = [];
|
|
365
|
-
lines.push(`## Hook Failures Top-${limit}`);
|
|
366
|
-
lines.push("");
|
|
367
|
-
if (rows.length === 0) {
|
|
368
|
-
lines.push("_No hook failures recorded._");
|
|
369
|
-
lines.push("");
|
|
370
|
-
return lines.join("\n");
|
|
371
|
-
}
|
|
372
|
-
lines.push("| Hook | Count | Last stderr | Source |");
|
|
373
|
-
lines.push("| --- | --- | --- | --- |");
|
|
374
|
-
for (const r of rows) {
|
|
375
|
-
lines.push(`| ${escapeCell(r.hook)} | ${r.count} | ${escapeCell(r.lastStderr)} | ${r.source} |`);
|
|
376
|
-
}
|
|
377
|
-
lines.push("");
|
|
378
|
-
return lines.join("\n");
|
|
379
|
-
}
|
|
380
|
-
function rollupSlashCommands(events, limit) {
|
|
381
|
-
const counts = /* @__PURE__ */ new Map();
|
|
382
|
-
for (const ev of events) {
|
|
383
|
-
if (ev.kind === "assistant_turn") {
|
|
384
|
-
const skill = ev.payload.attributionSkill;
|
|
385
|
-
if (typeof skill === "string" && skill.length > 0) {
|
|
386
|
-
counts.set(skill, (counts.get(skill) ?? 0) + 1);
|
|
387
|
-
}
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (ev.kind === "user_turn") {
|
|
391
|
-
const message = ev.payload.message;
|
|
392
|
-
if (!message || typeof message !== "object") continue;
|
|
393
|
-
const content = message.content;
|
|
394
|
-
const texts = [];
|
|
395
|
-
if (Array.isArray(content)) {
|
|
396
|
-
for (const part of content) {
|
|
397
|
-
if (part && typeof part === "object") {
|
|
398
|
-
const t = part.text;
|
|
399
|
-
if (typeof t === "string") texts.push(t);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
} else if (typeof content === "string") {
|
|
403
|
-
texts.push(content);
|
|
404
|
-
}
|
|
405
|
-
for (const t of texts) {
|
|
406
|
-
const m = COMMAND_NAME_RE2.exec(t);
|
|
407
|
-
if (m && m[1]) {
|
|
408
|
-
const cmd = m[1].trim();
|
|
409
|
-
if (cmd) counts.set(cmd, (counts.get(cmd) ?? 0) + 1);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
const rows = Array.from(counts.entries()).map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count);
|
|
415
|
-
return rows.slice(0, limit);
|
|
416
|
-
}
|
|
417
|
-
function renderSlashCommands(rows, limit) {
|
|
418
|
-
const lines = [];
|
|
419
|
-
lines.push(`## Slash Commands Top-${limit}`);
|
|
420
|
-
lines.push("");
|
|
421
|
-
if (rows.length === 0) {
|
|
422
|
-
lines.push("_No slash command activity recorded._");
|
|
423
|
-
lines.push("");
|
|
424
|
-
return lines.join("\n");
|
|
425
|
-
}
|
|
426
|
-
lines.push("| Command | Count |");
|
|
427
|
-
lines.push("| --- | --- |");
|
|
428
|
-
for (const r of rows) {
|
|
429
|
-
lines.push(`| ${escapeCell(r.command)} | ${r.count} |`);
|
|
430
|
-
}
|
|
431
|
-
lines.push("");
|
|
432
|
-
return lines.join("\n");
|
|
433
|
-
}
|
|
434
|
-
function rollupSubagents(events, limit) {
|
|
435
|
-
const counts = /* @__PURE__ */ new Map();
|
|
436
|
-
for (const ev of events) {
|
|
437
|
-
if (ev.kind !== "tool_call" && ev.kind !== "assistant_turn") continue;
|
|
438
|
-
const message = ev.payload.message;
|
|
439
|
-
if (message && typeof message === "object") {
|
|
440
|
-
const content = message.content;
|
|
441
|
-
if (Array.isArray(content)) {
|
|
442
|
-
for (const part of content) {
|
|
443
|
-
if (!part || typeof part !== "object") continue;
|
|
444
|
-
const p = part;
|
|
445
|
-
if (p.type !== "tool_use") continue;
|
|
446
|
-
if (p.name !== "Agent" && p.name !== "Task") continue;
|
|
447
|
-
const input = p.input;
|
|
448
|
-
const sub = input && typeof input.subagent_type === "string" ? input.subagent_type : void 0;
|
|
449
|
-
if (sub) counts.set(sub, (counts.get(sub) ?? 0) + 1);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
const direct = ev.payload;
|
|
454
|
-
if (direct.name === "Agent" || direct.name === "Task") {
|
|
455
|
-
const input = direct.input;
|
|
456
|
-
const sub = input && typeof input.subagent_type === "string" ? input.subagent_type : void 0;
|
|
457
|
-
if (sub) counts.set(sub, (counts.get(sub) ?? 0) + 1);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
const rows = Array.from(counts.entries()).map(([subagent, count]) => ({ subagent, count })).sort((a, b) => b.count - a.count);
|
|
461
|
-
return rows.slice(0, limit);
|
|
462
|
-
}
|
|
463
|
-
function renderSubagents(rows, limit) {
|
|
464
|
-
const lines = [];
|
|
465
|
-
lines.push(`## Subagents Top-${limit}`);
|
|
466
|
-
lines.push("");
|
|
467
|
-
if (rows.length === 0) {
|
|
468
|
-
lines.push("_No subagent dispatches recorded._");
|
|
469
|
-
lines.push("");
|
|
470
|
-
return lines.join("\n");
|
|
471
|
-
}
|
|
472
|
-
lines.push("| Subagent | Count |");
|
|
473
|
-
lines.push("| --- | --- |");
|
|
474
|
-
for (const r of rows) {
|
|
475
|
-
lines.push(`| ${escapeCell(r.subagent)} | ${r.count} |`);
|
|
476
|
-
}
|
|
477
|
-
lines.push("");
|
|
478
|
-
return lines.join("\n");
|
|
479
|
-
}
|
|
480
|
-
var PHASE_ORDER = ["research", "requirements", "design", "tasks", "execution", "done"];
|
|
481
|
-
function rollupSpecFunnel(specStates) {
|
|
482
|
-
const counts = /* @__PURE__ */ new Map();
|
|
483
|
-
for (const s of specStates) {
|
|
484
|
-
counts.set(s.phase, (counts.get(s.phase) ?? 0) + 1);
|
|
485
|
-
}
|
|
486
|
-
const rows = [];
|
|
487
|
-
for (const p of PHASE_ORDER) {
|
|
488
|
-
rows.push({ phase: p, count: counts.get(p) ?? 0 });
|
|
489
|
-
counts.delete(p);
|
|
490
|
-
}
|
|
491
|
-
for (const [phase, count] of Array.from(counts.entries()).sort()) {
|
|
492
|
-
rows.push({ phase, count });
|
|
493
|
-
}
|
|
494
|
-
return rows;
|
|
495
|
-
}
|
|
496
|
-
function renderSpecFunnel(rows) {
|
|
497
|
-
const lines = [];
|
|
498
|
-
lines.push("## Spec Funnel");
|
|
499
|
-
lines.push("");
|
|
500
|
-
lines.push("| Phase | Count |");
|
|
501
|
-
lines.push("| --- | --- |");
|
|
502
|
-
for (const r of rows) {
|
|
503
|
-
lines.push(`| ${escapeCell(r.phase)} | ${r.count} |`);
|
|
504
|
-
}
|
|
505
|
-
lines.push("");
|
|
506
|
-
return lines.join("\n");
|
|
507
|
-
}
|
|
508
|
-
function percentile(sortedAsc, p) {
|
|
509
|
-
if (sortedAsc.length === 0) return null;
|
|
510
|
-
const n = sortedAsc.length;
|
|
511
|
-
const idx = Math.min(Math.max(Math.ceil(p * n) - 1, 0), n - 1);
|
|
512
|
-
return sortedAsc[idx] ?? null;
|
|
513
|
-
}
|
|
514
|
-
function rollupHookDuration(events) {
|
|
515
|
-
const samples = /* @__PURE__ */ new Map();
|
|
516
|
-
for (const ev of events) {
|
|
517
|
-
if (ev.kind !== "hook_invocation") continue;
|
|
518
|
-
const att = ev.payload.attachment;
|
|
519
|
-
if (!att || typeof att !== "object") continue;
|
|
520
|
-
const a = att;
|
|
521
|
-
if (a.type !== "hook_success") continue;
|
|
522
|
-
const hookName = typeof a.hookName === "string" ? a.hookName : void 0;
|
|
523
|
-
const dur = typeof a.durationMs === "number" ? a.durationMs : void 0;
|
|
524
|
-
if (!hookName || dur === void 0) continue;
|
|
525
|
-
if (!samples.has(hookName)) samples.set(hookName, []);
|
|
526
|
-
samples.get(hookName).push(dur);
|
|
527
|
-
}
|
|
528
|
-
const rows = [];
|
|
529
|
-
for (const [hook, arr] of samples.entries()) {
|
|
530
|
-
arr.sort((a, b) => a - b);
|
|
531
|
-
rows.push({
|
|
532
|
-
hook,
|
|
533
|
-
samples: arr.length,
|
|
534
|
-
p50: percentile(arr, 0.5),
|
|
535
|
-
p95: percentile(arr, 0.95),
|
|
536
|
-
p99: percentile(arr, 0.99)
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
rows.sort((a, b) => b.samples - a.samples);
|
|
540
|
-
return rows;
|
|
541
|
-
}
|
|
542
|
-
function renderHookDuration(rows) {
|
|
543
|
-
const lines = [];
|
|
544
|
-
lines.push("## Hook Duration");
|
|
545
|
-
lines.push("");
|
|
546
|
-
if (rows.length === 0) {
|
|
547
|
-
lines.push("_No hook duration samples recorded._");
|
|
548
|
-
lines.push("");
|
|
549
|
-
return lines.join("\n");
|
|
550
|
-
}
|
|
551
|
-
lines.push("| Hook | Samples | P50 (ms) | P95 (ms) | P99 (ms) |");
|
|
552
|
-
lines.push("| --- | --- | --- | --- | --- |");
|
|
553
|
-
for (const r of rows) {
|
|
554
|
-
if (r.samples < MIN_SAMPLES_FOR_PCT) {
|
|
555
|
-
lines.push(`| ${escapeCell(r.hook)} | ${r.samples} | (\u6837\u672C\u4E0D\u8DB3: ${r.samples}) | (\u6837\u672C\u4E0D\u8DB3: ${r.samples}) | (\u6837\u672C\u4E0D\u8DB3: ${r.samples}) |`);
|
|
556
|
-
} else {
|
|
557
|
-
lines.push(`| ${escapeCell(r.hook)} | ${r.samples} | ${r.p50 ?? "-"} | ${r.p95 ?? "-"} | ${r.p99 ?? "-"} |`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
lines.push("");
|
|
561
|
-
return lines.join("\n");
|
|
562
|
-
}
|
|
563
|
-
function renderSchemaDrift(input) {
|
|
564
|
-
const lines = [];
|
|
565
|
-
lines.push("## Schema Drift");
|
|
566
|
-
lines.push("");
|
|
567
|
-
lines.push("| Metric | Value |");
|
|
568
|
-
lines.push("| --- | --- |");
|
|
569
|
-
lines.push(`| unknown_type_count | ${input.unknownTypeCount} |`);
|
|
570
|
-
lines.push(`| parse_error_count | ${input.parseErrorCount} |`);
|
|
571
|
-
lines.push("");
|
|
572
|
-
if (input.unknownTypeCount > 0 || input.parseErrorCount > 0) {
|
|
573
|
-
lines.push("_Hint: \u5982\u957F\u671F > 0 \u8868\u660E Claude Code \u5347\u7EA7\u4E86 schema\uFF0C\u8BF7\u68C0\u67E5 plugins/curdx-flow/schemas/transcript-events.json_");
|
|
574
|
-
} else {
|
|
575
|
-
lines.push("_no drift detected_");
|
|
576
|
-
}
|
|
577
|
-
lines.push("");
|
|
578
|
-
return lines.join("\n");
|
|
579
|
-
}
|
|
580
|
-
function rollupParentChain(events) {
|
|
581
|
-
const uuids = /* @__PURE__ */ new Set();
|
|
582
|
-
for (const ev of events) {
|
|
583
|
-
if (ev.uuid) uuids.add(ev.uuid);
|
|
584
|
-
}
|
|
585
|
-
let withParent = 0;
|
|
586
|
-
let broken = 0;
|
|
587
|
-
for (const ev of events) {
|
|
588
|
-
const p = ev.payload.parentUuid;
|
|
589
|
-
if (typeof p === "string" && p.length > 0) {
|
|
590
|
-
withParent += 1;
|
|
591
|
-
if (!uuids.has(p)) broken += 1;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
totalEvents: events.length,
|
|
596
|
-
withParent,
|
|
597
|
-
brokenLinks: broken,
|
|
598
|
-
brokenRatio: withParent === 0 ? 0 : broken / withParent,
|
|
599
|
-
hasData: withParent > 0
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
function renderParentChain(s) {
|
|
603
|
-
const lines = [];
|
|
604
|
-
lines.push("## Parent Chain");
|
|
605
|
-
lines.push("");
|
|
606
|
-
if (!s.hasData) {
|
|
607
|
-
lines.push("_(no parent chain data)_");
|
|
608
|
-
lines.push("");
|
|
609
|
-
return lines.join("\n");
|
|
610
|
-
}
|
|
611
|
-
lines.push("| Metric | Value |");
|
|
612
|
-
lines.push("| --- | --- |");
|
|
613
|
-
lines.push(`| total_events | ${s.totalEvents} |`);
|
|
614
|
-
lines.push(`| events_with_parent | ${s.withParent} |`);
|
|
615
|
-
lines.push(`| broken_links | ${s.brokenLinks} |`);
|
|
616
|
-
lines.push(`| parentUuid_broken_ratio | ${s.brokenRatio.toFixed(4)} |`);
|
|
617
|
-
lines.push("");
|
|
618
|
-
return lines.join("\n");
|
|
619
|
-
}
|
|
620
|
-
function renderReport(events, errorEntries, specStates, opts) {
|
|
621
|
-
const limit = typeof opts.limit === "number" && opts.limit > 0 ? opts.limit : DEFAULT_LIMIT;
|
|
622
|
-
const hookFailures = rollupHookFailures(events, errorEntries, limit);
|
|
623
|
-
const slashCommands = rollupSlashCommands(events, limit);
|
|
624
|
-
const subagents = rollupSubagents(events, limit);
|
|
625
|
-
const specFunnel = rollupSpecFunnel(specStates);
|
|
626
|
-
const hookDuration = rollupHookDuration(events);
|
|
627
|
-
const parentChain = rollupParentChain(events);
|
|
628
|
-
const markdown = renderHookFailures(hookFailures, limit) + "\n" + renderSlashCommands(slashCommands, limit) + "\n" + renderSubagents(subagents, limit) + "\n" + renderSpecFunnel(specFunnel) + "\n" + renderHookDuration(hookDuration) + "\n" + renderSchemaDrift(opts.schemaDrift) + "\n" + renderParentChain(parentChain);
|
|
629
|
-
const json = {
|
|
630
|
-
hookFailures,
|
|
631
|
-
slashCommands,
|
|
632
|
-
subagents,
|
|
633
|
-
specFunnel,
|
|
634
|
-
hookDuration,
|
|
635
|
-
schemaDrift: {
|
|
636
|
-
unknownTypeCount: opts.schemaDrift.unknownTypeCount,
|
|
637
|
-
parseErrorCount: opts.schemaDrift.parseErrorCount
|
|
638
|
-
},
|
|
639
|
-
parentChain
|
|
640
|
-
};
|
|
641
|
-
return { markdown, json };
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// src/analyze/index.ts
|
|
645
|
-
var POC_FIXTURE_REL = "tests/analyze/fixtures/sample.jsonl";
|
|
646
|
-
var STATE_DIR = path3.join(homedir(), ".claude", "curdx-flow");
|
|
647
|
-
var STATE_PATH = path3.join(STATE_DIR, "observability-state.json");
|
|
648
|
-
var ERRORS_LOG_PATH = path3.join(STATE_DIR, "errors.jsonl");
|
|
649
|
-
var SPECS_DIR_REL = "specs";
|
|
650
|
-
function readState() {
|
|
651
|
-
if (!existsSync(STATE_PATH)) return { version: 1, files: {} };
|
|
652
|
-
try {
|
|
653
|
-
const raw = readFileSync2(STATE_PATH, "utf8");
|
|
654
|
-
const parsed = JSON.parse(raw);
|
|
655
|
-
if (parsed && parsed.version === 1 && parsed.files) return parsed;
|
|
656
|
-
} catch {
|
|
657
|
-
}
|
|
658
|
-
return { version: 1, files: {} };
|
|
659
|
-
}
|
|
660
|
-
function writeState(state) {
|
|
661
|
-
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
|
|
662
|
-
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
663
|
-
}
|
|
664
|
-
function loadSpecStates() {
|
|
665
|
-
const specsDir = path3.resolve(process.cwd(), SPECS_DIR_REL);
|
|
666
|
-
if (!existsSync(specsDir)) return [];
|
|
667
|
-
let entries;
|
|
668
|
-
try {
|
|
669
|
-
entries = readdirSync(specsDir);
|
|
670
|
-
} catch {
|
|
671
|
-
return [];
|
|
672
|
-
}
|
|
673
|
-
const out = [];
|
|
674
|
-
for (const name of entries) {
|
|
675
|
-
if (name.startsWith(".")) continue;
|
|
676
|
-
const stateFile = path3.join(specsDir, name, ".curdx-state.json");
|
|
677
|
-
if (!existsSync(stateFile)) continue;
|
|
678
|
-
try {
|
|
679
|
-
const raw = readFileSync2(stateFile, "utf8");
|
|
680
|
-
const parsed = JSON.parse(raw);
|
|
681
|
-
const phase = typeof parsed.phase === "string" ? parsed.phase : void 0;
|
|
682
|
-
if (!phase) continue;
|
|
683
|
-
out.push({ name, phase });
|
|
684
|
-
} catch {
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return out;
|
|
689
|
-
}
|
|
690
|
-
function loadErrorEntries() {
|
|
691
|
-
if (!existsSync(ERRORS_LOG_PATH)) return [];
|
|
692
|
-
let raw;
|
|
693
|
-
try {
|
|
694
|
-
raw = readFileSync2(ERRORS_LOG_PATH, "utf8");
|
|
695
|
-
} catch {
|
|
696
|
-
return [];
|
|
697
|
-
}
|
|
698
|
-
const out = [];
|
|
699
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
700
|
-
if (!line) continue;
|
|
701
|
-
try {
|
|
702
|
-
const parsed = JSON.parse(line);
|
|
703
|
-
out.push({
|
|
704
|
-
ts: typeof parsed.ts === "string" ? parsed.ts : "",
|
|
705
|
-
...typeof parsed.hook === "string" ? { hook: parsed.hook } : {},
|
|
706
|
-
...typeof parsed.event === "string" ? { event: parsed.event } : {},
|
|
707
|
-
...typeof parsed.msg === "string" ? { msg: parsed.msg } : {},
|
|
708
|
-
...typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {},
|
|
709
|
-
...typeof parsed.transcript_path === "string" ? { transcript_path: parsed.transcript_path } : {}
|
|
710
|
-
});
|
|
711
|
-
} catch {
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
return out;
|
|
716
|
-
}
|
|
717
|
-
async function runAnalyze(opts) {
|
|
718
|
-
const fixturePath = path3.resolve(process.cwd(), POC_FIXTURE_REL);
|
|
719
|
-
const limit = Number(opts.limit) || 10;
|
|
720
|
-
const state = readState();
|
|
721
|
-
const stat = statSync2(fixturePath);
|
|
722
|
-
const prevForPath = state.files[fixturePath];
|
|
723
|
-
const rotate = shouldRotate(prevForPath, { sizeBytes: stat.size, lastModifiedMs: stat.mtimeMs });
|
|
724
|
-
const startOffset = rotate || !prevForPath ? 0 : prevForPath.byteOffset;
|
|
725
|
-
const includePrompts = Boolean(opts.includePrompts);
|
|
726
|
-
const cacheCompatible = (state.lastIncludePrompts ?? false) === includePrompts;
|
|
727
|
-
if (!rotate && prevForPath && startOffset >= stat.size && cacheCompatible && (state.lastReportJson || state.lastReportMarkdown)) {
|
|
728
|
-
if (opts.json && state.lastReportJson) {
|
|
729
|
-
process.stdout.write(state.lastReportJson);
|
|
730
|
-
writeState(state);
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (!opts.json && state.lastReportMarkdown) {
|
|
734
|
-
process.stdout.write(state.lastReportMarkdown);
|
|
735
|
-
writeState(state);
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
const schemaMap = loadSchemaMap();
|
|
740
|
-
const counters = { unknown_type: 0, parse_error: 0, processed: 0 };
|
|
741
|
-
const collected = [];
|
|
742
|
-
try {
|
|
743
|
-
for await (const ev of parseTranscript(fixturePath, startOffset, schemaMap, counters)) {
|
|
744
|
-
collected.push(ev);
|
|
745
|
-
}
|
|
746
|
-
const redacted = [];
|
|
747
|
-
for (const ev of collected) {
|
|
748
|
-
const r = redactEvent(ev, { includePrompts });
|
|
749
|
-
if (r) redacted.push(r);
|
|
750
|
-
}
|
|
751
|
-
const filtered = filterEvents(redacted, { ...opts, limit });
|
|
752
|
-
const errorEntries = loadErrorEntries();
|
|
753
|
-
const specStates = loadSpecStates();
|
|
754
|
-
const { markdown, json } = renderReport(filtered, errorEntries, specStates, {
|
|
755
|
-
...opts,
|
|
756
|
-
limit,
|
|
757
|
-
schemaDrift: {
|
|
758
|
-
unknownTypeCount: counters.unknown_type,
|
|
759
|
-
parseErrorCount: counters.parse_error
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
const safeJson = redactReportFields(json, { includePrompts });
|
|
763
|
-
void opts.out;
|
|
764
|
-
const jsonStr = `${JSON.stringify(safeJson)}
|
|
765
|
-
`;
|
|
766
|
-
const markdownStr = markdown;
|
|
767
|
-
state.lastReportJson = jsonStr;
|
|
768
|
-
state.lastReportMarkdown = markdownStr;
|
|
769
|
-
state.lastIncludePrompts = includePrompts;
|
|
770
|
-
if (opts.json) {
|
|
771
|
-
process.stdout.write(jsonStr);
|
|
772
|
-
} else {
|
|
773
|
-
process.stdout.write(markdownStr);
|
|
774
|
-
}
|
|
775
|
-
void safeJson;
|
|
776
|
-
if (counters.parse_error || counters.unknown_type) {
|
|
777
|
-
process.stderr.write(
|
|
778
|
-
`(analyze: parse_error=${counters.parse_error} unknown_type=${counters.unknown_type} processed=${counters.processed})
|
|
779
|
-
`
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
} finally {
|
|
783
|
-
state.files[fixturePath] = {
|
|
784
|
-
byteOffset: stat.size,
|
|
785
|
-
lastModifiedMs: stat.mtimeMs,
|
|
786
|
-
sizeBytes: stat.size
|
|
787
|
-
};
|
|
788
|
-
void getStateForPath;
|
|
789
|
-
writeState(state);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
export {
|
|
793
|
-
runAnalyze
|
|
794
|
-
};
|