@mmerterden/multi-agent-pipeline 20.23.2 → 20.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +8 -6
- package/README.tr.md +7 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +7 -6
- package/docs/features.md +9 -4
- package/install/catalog-history.json +1 -1
- package/install/templates/claude-hooks.json +14 -1
- package/install/templates/codex-hooks.json +18 -0
- package/manifest.json +126 -98
- package/package.json +1 -1
- package/pipeline/agents/task-clarifier.md +1 -1
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
- package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/postmortem/SKILL.md +90 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
- package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
- package/pipeline/contract/fixtures/runs-failed.json +1 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
- package/pipeline/contract/fixtures/runs-running.json +1 -1
- package/pipeline/lib/analysis-jira-write.sh +70 -0
- package/pipeline/lib/credential-inventory.sh +5 -5
- package/pipeline/lib/decisions.mjs +130 -0
- package/pipeline/lib/fetch-crashlytics.sh +6 -8
- package/pipeline/lib/model-rung.sh +1 -1
- package/pipeline/lib/plan-todos.sh +177 -37
- package/pipeline/lib/post-pr-review.sh +4 -6
- package/pipeline/lib/route-state.sh +1 -1
- package/pipeline/lib/shadow-git.sh +4 -5
- package/pipeline/lib/task-graph.mjs +446 -0
- package/pipeline/lib/tdd-evidence.mjs +579 -0
- package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
- package/pipeline/multi-agent-refs/analysis-template.md +4 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -11
- package/pipeline/multi-agent-refs/features/analysis-jira.md +42 -0
- package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
- package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
- package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +21 -14
- package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
- package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
- package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
- package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
- package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
- package/pipeline/multi-agent-refs/help/en.md +2 -0
- package/pipeline/multi-agent-refs/help/tr.md +2 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +18 -16
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
- package/pipeline/multi-agent-refs/picker-contract.md +3 -2
- package/pipeline/multi-agent-refs/response-shape.md +65 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
- package/pipeline/rules/response-shape.md +18 -0
- package/pipeline/schemas/agent-state.schema.json +36 -1
- package/pipeline/schemas/phases.json +4 -0
- package/pipeline/schemas/plan-todos.schema.json +104 -3
- package/pipeline/schemas/planning-output.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +122 -3
- package/pipeline/scripts/_code-graph.mjs +2 -2
- package/pipeline/scripts/_jsonl.mjs +151 -0
- package/pipeline/scripts/_plan-overlap.mjs +231 -0
- package/pipeline/scripts/_redact.mjs +124 -10
- package/pipeline/scripts/_transcript-hosts.mjs +406 -0
- package/pipeline/scripts/analysis-story-tree.mjs +153 -8
- package/pipeline/scripts/autopilot-publish.mjs +12 -1
- package/pipeline/scripts/build-stack-plugins.mjs +3 -3
- package/pipeline/scripts/capture-flush.sh +7 -7
- package/pipeline/scripts/code-graph-rules/android.json +1 -1
- package/pipeline/scripts/cost-analyze.mjs +32 -43
- package/pipeline/scripts/cost-table.json +35 -19
- package/pipeline/scripts/decisions-log.mjs +118 -0
- package/pipeline/scripts/dep-bump.mjs +1556 -0
- package/pipeline/scripts/edit-syntax-gate.mjs +362 -0
- package/pipeline/scripts/edit-syntax-gate.sh +32 -0
- package/pipeline/scripts/find-polluter.mjs +281 -0
- package/pipeline/scripts/gate-ledger.mjs +46 -13
- package/pipeline/scripts/gc-abandoned.sh +2 -2
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
- package/pipeline/scripts/git-path.mjs +1 -2
- package/pipeline/scripts/host-hooks.mjs +265 -0
- package/pipeline/scripts/jira-attach.sh +1 -1
- package/pipeline/scripts/keychain.py +3 -3
- package/pipeline/scripts/mutation-probe.mjs +484 -0
- package/pipeline/scripts/output-quality-check.sh +1 -1
- package/pipeline/scripts/phase-tracker.sh +5 -6
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
- package/pipeline/scripts/plan-complexity.mjs +259 -0
- package/pipeline/scripts/pre-push-check.sh +3 -6
- package/pipeline/scripts/review-decision-gate.mjs +33 -1
- package/pipeline/scripts/rework-escalation.mjs +264 -0
- package/pipeline/scripts/test-strength.mjs +5 -3
- package/pipeline/scripts/transcript-index.mjs +1131 -0
- package/pipeline/scripts/uninstall.mjs +90 -4
- package/pipeline/scripts/update-issue-progress.sh +4 -6
- package/pipeline/scripts/usage-report.mjs +2 -2
- package/pipeline/scripts/validate-planning.mjs +48 -6
- package/pipeline/skills/.skill-manifest.json +20 -12
- package/pipeline/skills/.skills-index.json +23 -1
- package/pipeline/skills/shared/README.md +5 -3
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
- package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +69 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/skills-index.md +3 -1
|
@@ -0,0 +1,1131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file transcript-index.mjs - what a run did, read out of its session
|
|
4
|
+
* transcripts, as evidence a post-mortem can cite.
|
|
5
|
+
*
|
|
6
|
+
* The run log says what each phase decided. The transcript says what the
|
|
7
|
+
* session actually did on the way: which tool calls it repeated, where it sat
|
|
8
|
+
* waiting, where the context was compacted, which subagent failed, what was
|
|
9
|
+
* denied. This reads a Claude Code session file (`<session>.jsonl`, plus the
|
|
10
|
+
* subagent files beside it), a Copilot CLI session log
|
|
11
|
+
* (`session-state/<session>/events.jsonl`) or a Codex rollout
|
|
12
|
+
* (`sessions/YYYY/MM/DD/rollout-*.jsonl`), detecting the format per file
|
|
13
|
+
* (_transcript-hosts.mjs), and extracts those facts deterministically, so the
|
|
14
|
+
* same transcript always gives the same index and every finding carries the
|
|
15
|
+
* `file:line` it came from. Interpreting them is left to the model that writes
|
|
16
|
+
* the post-mortem (/multi-agent:postmortem); nothing here guesses.
|
|
17
|
+
*
|
|
18
|
+
* Extracted:
|
|
19
|
+
* turns one per prompt the user typed (or slash command), with tool
|
|
20
|
+
* call count, token usage and duration
|
|
21
|
+
* toolCalls name, a redacted input excerpt, duration (reported by the
|
|
22
|
+
* tool when it says, else the timestamp difference), error flag
|
|
23
|
+
* loops identical calls (same tool, same input) made at least
|
|
24
|
+
* `loopThreshold` times in one file, with the longest streak
|
|
25
|
+
* stalls gaps of at least `stallMinutes` between two records while the
|
|
26
|
+
* session was working, with the tool that was running
|
|
27
|
+
* compactions boundaries with trigger and token figures, and the summary
|
|
28
|
+
* turn that follows each
|
|
29
|
+
* subagents each dispatch with its type, result status, duration, tokens,
|
|
30
|
+
* and the background notification when one arrives
|
|
31
|
+
* errors tool errors, denials, hook blocks, API errors, interrupts
|
|
32
|
+
* invocations slash commands and skills
|
|
33
|
+
* usage token totals, each assistant message counted once
|
|
34
|
+
*
|
|
35
|
+
* PRIVACY. A transcript holds whatever the session read. Every string taken
|
|
36
|
+
* from one passes through the redactor before it is placed in the index:
|
|
37
|
+
* credentials by the outbound gate's certain rules (lib/redact.mjs) and the
|
|
38
|
+
* shapes those have no prefix for (_redact.mjs redactCredentials), then links, addresses, hosts, machine paths and home directories (_redact.mjs
|
|
39
|
+
* scrubCounted), then the transcript directory's own project slug, which
|
|
40
|
+
* spells the home directory with dashes. The index reports how many items of
|
|
41
|
+
* each kind were replaced, and before anything is printed the whole output is
|
|
42
|
+
* run through the outbound gate once more: a certain finding left in it stops
|
|
43
|
+
* the script with nothing written. Nothing is sent anywhere.
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
* transcript-index.mjs <session.jsonl>... index these files
|
|
47
|
+
* transcript-index.mjs --run <task-id|#N> find the run's session files, index them
|
|
48
|
+
* transcript-index.mjs --run <id> --locate only report where the run and its files are
|
|
49
|
+
* options: --json --no-subagents --max-mb N --max-events N
|
|
50
|
+
* --loop-threshold N --stall-minutes N --excerpt N
|
|
51
|
+
*
|
|
52
|
+
* TRANSCRIPTS_ROOT overrides ~/.claude/projects, COPILOT_TRANSCRIPTS_ROOT
|
|
53
|
+
* ~/.copilot/session-state (else $COPILOT_HOME/session-state),
|
|
54
|
+
* CODEX_TRANSCRIPTS_ROOT ~/.codex/sessions (else $CODEX_HOME/sessions), and
|
|
55
|
+
* LOGS_ROOT the run log root.
|
|
56
|
+
*
|
|
57
|
+
* Exit codes: 0 indexed | 2 usage | 3 nothing to read | 4 redaction guard
|
|
58
|
+
*
|
|
59
|
+
* @module pipeline/scripts/transcript-index
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { createHash } from "node:crypto";
|
|
63
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
64
|
+
import { homedir } from "node:os";
|
|
65
|
+
import { basename, dirname, join } from "node:path";
|
|
66
|
+
import { readJsonl } from "./_jsonl.mjs";
|
|
67
|
+
import { createAdapter, detectFormat, sessionCwd } from "./_transcript-hosts.mjs";
|
|
68
|
+
import { redactCredentials, scrubCounted } from "./_redact.mjs";
|
|
69
|
+
import { listRuns, logsRoot, taskIdVariants } from "./_run-paths.mjs";
|
|
70
|
+
import { redact } from "../lib/redact.mjs";
|
|
71
|
+
import { scan } from "../lib/outbound-gate.mjs";
|
|
72
|
+
import { readRegularJson } from "../lib/regular-file.mjs";
|
|
73
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
74
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
75
|
+
|
|
76
|
+
export const DEFAULTS = {
|
|
77
|
+
loopThreshold: 3,
|
|
78
|
+
stallMinutes: 5,
|
|
79
|
+
excerpt: 160,
|
|
80
|
+
maxEvents: 400,
|
|
81
|
+
maxBytes: 256 * 1024 * 1024,
|
|
82
|
+
subagents: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const SUBAGENT_TOOLS = new Set(["Agent", "Task", "task", "spawn_agent"]);
|
|
86
|
+
const SKILL_TOOL = /^skill$/i;
|
|
87
|
+
const DENIAL =
|
|
88
|
+
/doesn't want to proceed|User rejected tool use|^Permission to use \S+ (?:has been|was) denied|permission denied by the user/i;
|
|
89
|
+
const HOOK_BLOCK = /\bhook (?:error|blocked)|PreToolUse:|PostToolUse:/;
|
|
90
|
+
const INTERRUPT = /^\[Request interrupted by user/;
|
|
91
|
+
|
|
92
|
+
export const defaultProjectsRoot = () =>
|
|
93
|
+
process.env.TRANSCRIPTS_ROOT || join(homedir(), ".claude", "projects");
|
|
94
|
+
|
|
95
|
+
export const defaultCopilotRoot = () =>
|
|
96
|
+
process.env.COPILOT_TRANSCRIPTS_ROOT ||
|
|
97
|
+
join(process.env.COPILOT_HOME || join(homedir(), ".copilot"), "session-state");
|
|
98
|
+
|
|
99
|
+
export const defaultCodexRoot = () =>
|
|
100
|
+
process.env.CODEX_TRANSCRIPTS_ROOT ||
|
|
101
|
+
join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
|
|
102
|
+
|
|
103
|
+
export { detectFormat };
|
|
104
|
+
|
|
105
|
+
const COPILOT_LOG = "events.jsonl";
|
|
106
|
+
|
|
107
|
+
const CODEX_NAME = /^rollout-[\dT:-]+-([0-9a-f]{8})[0-9a-f-]*\.jsonl$/i;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The name a file is cited by. Every Copilot CLI log is `events.jsonl`, so it
|
|
111
|
+
* is prefixed with the first eight characters of its session directory; a
|
|
112
|
+
* Codex rollout name carries its start time and full session id, so it is cut
|
|
113
|
+
* to `rollout-<first eight of the id>.jsonl`. Claude Code names stay as they are.
|
|
114
|
+
*/
|
|
115
|
+
export function displayName(path) {
|
|
116
|
+
const name = basename(path);
|
|
117
|
+
if (name === COPILOT_LOG) return `${basename(dirname(path)).slice(0, 8)}/${name}`;
|
|
118
|
+
const codex = name.match(CODEX_NAME);
|
|
119
|
+
return codex ? `rollout-${codex[1]}.jsonl` : name;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A session id as Claude Code writes it (a UUID), or any token safe as a file name. */
|
|
123
|
+
const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
124
|
+
|
|
125
|
+
/** The directory name Claude Code gives a working directory under ~/.claude/projects. */
|
|
126
|
+
export const projectSlug = (dir) => String(dir).replace(/[^A-Za-z0-9]/g, "-");
|
|
127
|
+
|
|
128
|
+
// --- redaction ---------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
const MARKER = /\[redacted:([a-z0-9-]+)\]/g;
|
|
131
|
+
const countMarkers = (s) => {
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const m of s.matchAll(MARKER)) out[m[1]] = (out[m[1]] || 0) + 1;
|
|
134
|
+
return out;
|
|
135
|
+
};
|
|
136
|
+
const TOKEN_CHAR = /[A-Za-z0-9_\-./+=:@~%]/;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `text` cut to `limit` characters, backed off to the start of a token that
|
|
140
|
+
* the cut would split, so no half-credential survives on the boundary.
|
|
141
|
+
*/
|
|
142
|
+
function cut(text, limit) {
|
|
143
|
+
if (text.length <= limit) return text;
|
|
144
|
+
let end = limit;
|
|
145
|
+
while (end > 0 && TOKEN_CHAR.test(text[end]) && TOKEN_CHAR.test(text[end - 1])) end--;
|
|
146
|
+
return `${text.slice(0, end).trimEnd()} ...`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One redactor per index, so its counts are the index's counts.
|
|
151
|
+
*
|
|
152
|
+
* @param {{home?: string}} [opts] the home directory whose dashed spelling (a
|
|
153
|
+
* transcript directory name) is replaced; defaults to this machine's
|
|
154
|
+
*/
|
|
155
|
+
export function createRedactor({ home = homedir() } = {}) {
|
|
156
|
+
const byKind = {};
|
|
157
|
+
const bump = (kind, n = 1) => (byKind[kind] = (byKind[kind] || 0) + n);
|
|
158
|
+
const homeSlug = home && home.length > 1 ? projectSlug(home) : null;
|
|
159
|
+
const GENERIC_SLUG = /(?<![\w~])-(?:Users|home)-[A-Za-z0-9_]+/g;
|
|
160
|
+
|
|
161
|
+
function clean(value) {
|
|
162
|
+
if (value === null || value === undefined) return "";
|
|
163
|
+
const raw = String(value);
|
|
164
|
+
if (!raw) return "";
|
|
165
|
+
const before = countMarkers(raw);
|
|
166
|
+
let out = redactCredentials(redact(raw));
|
|
167
|
+
for (const [k, n] of Object.entries(countMarkers(out))) {
|
|
168
|
+
const added = n - (before[k] || 0);
|
|
169
|
+
if (added > 0) bump(k, added);
|
|
170
|
+
}
|
|
171
|
+
const s = scrubCounted(out);
|
|
172
|
+
out = s.text;
|
|
173
|
+
for (const [k, n] of Object.entries(s.counts)) bump(k, n);
|
|
174
|
+
if (homeSlug && out.includes(homeSlug)) {
|
|
175
|
+
const parts = out.split(homeSlug);
|
|
176
|
+
bump("home-path", parts.length - 1);
|
|
177
|
+
out = parts.join("~");
|
|
178
|
+
}
|
|
179
|
+
out = out.replace(GENERIC_SLUG, () => {
|
|
180
|
+
bump("home-path");
|
|
181
|
+
return "~";
|
|
182
|
+
});
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A short, single-line, redacted excerpt. */
|
|
187
|
+
function excerpt(value, limit = DEFAULTS.excerpt) {
|
|
188
|
+
const flat = String(value ?? "")
|
|
189
|
+
.replace(/\s+/g, " ")
|
|
190
|
+
.trim();
|
|
191
|
+
return cut(clean(cut(flat, Math.max(limit * 4, 1024))), limit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const total = () => Object.values(byKind).reduce((a, n) => a + n, 0);
|
|
195
|
+
return { clean, excerpt, byKind, total };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Throws when `text` still carries something the outbound gate is certain is a
|
|
200
|
+
* credential. Only the rule names are reported, never the match.
|
|
201
|
+
*/
|
|
202
|
+
export function assertClean(text) {
|
|
203
|
+
const hits = scan(text).filter((f) => f.certain);
|
|
204
|
+
if (hits.length) {
|
|
205
|
+
const names = [...new Set(hits.map((h) => h.rule))].join(", ");
|
|
206
|
+
const err = new Error(`redaction incomplete: ${names}`);
|
|
207
|
+
err.code = "REDACTION";
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- reading -----------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
const textOf = (content) => {
|
|
215
|
+
if (typeof content === "string") return content;
|
|
216
|
+
if (!Array.isArray(content)) return "";
|
|
217
|
+
return content
|
|
218
|
+
.map((b) => (typeof b === "string" ? b : b?.type === "text" ? b.text || "" : ""))
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.join("\n");
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const stable = (v) => {
|
|
224
|
+
if (Array.isArray(v)) return `[${v.map(stable).join(",")}]`;
|
|
225
|
+
if (v && typeof v === "object")
|
|
226
|
+
return `{${Object.keys(v)
|
|
227
|
+
.sort()
|
|
228
|
+
.map((k) => `${JSON.stringify(k)}:${stable(v[k])}`)
|
|
229
|
+
.join(",")}}`;
|
|
230
|
+
return JSON.stringify(v ?? null);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const digest = (s) => createHash("sha256").update(s).digest("hex").slice(0, 12);
|
|
234
|
+
|
|
235
|
+
/** The part of a tool input worth showing: the command, the path, the pattern. */
|
|
236
|
+
function inputGist(name, input) {
|
|
237
|
+
if (!input || typeof input !== "object") return "";
|
|
238
|
+
for (const k of [
|
|
239
|
+
"command",
|
|
240
|
+
"cmd",
|
|
241
|
+
"file_path",
|
|
242
|
+
"path",
|
|
243
|
+
"pattern",
|
|
244
|
+
"url",
|
|
245
|
+
"query",
|
|
246
|
+
"skill",
|
|
247
|
+
"description",
|
|
248
|
+
"input",
|
|
249
|
+
]) {
|
|
250
|
+
if (typeof input[k] === "string" && input[k]) return input[k];
|
|
251
|
+
}
|
|
252
|
+
return stable(input);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const tsOf = (rec) => {
|
|
256
|
+
const t = Date.parse(rec?.timestamp || "");
|
|
257
|
+
return Number.isFinite(t) ? t : null;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const numberOr = (v, fallback = 0) => (Number.isFinite(Number(v)) ? Number(v) : fallback);
|
|
261
|
+
|
|
262
|
+
/** The .jsonl names in `dir`, sorted; none when it cannot be read. */
|
|
263
|
+
function listJsonl(dir) {
|
|
264
|
+
try {
|
|
265
|
+
return readdirSync(dir)
|
|
266
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
267
|
+
.sort();
|
|
268
|
+
} catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Session files plus, unless disabled, the subagent files Claude Code keeps beside them. */
|
|
274
|
+
function expandFiles(paths, withSubagents) {
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const p of paths) {
|
|
277
|
+
out.push(p);
|
|
278
|
+
if (!withSubagents) continue;
|
|
279
|
+
const dir = join(dirname(p), basename(p, ".jsonl"), "subagents");
|
|
280
|
+
for (const f of listJsonl(dir)) out.push(join(dir, f));
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Index one or more transcript files.
|
|
287
|
+
*
|
|
288
|
+
* @param {string[]} paths
|
|
289
|
+
* @param {Partial<typeof DEFAULTS> & {home?: string}} [options]
|
|
290
|
+
*/
|
|
291
|
+
export async function indexTranscripts(paths, options = {}) {
|
|
292
|
+
const o = { ...DEFAULTS, ...options };
|
|
293
|
+
const R = createRedactor({ home: o.home });
|
|
294
|
+
const ex = (v) => R.excerpt(v, o.excerpt);
|
|
295
|
+
const stallMs = o.stallMinutes * 60_000;
|
|
296
|
+
|
|
297
|
+
const files = [];
|
|
298
|
+
const turns = [];
|
|
299
|
+
const toolCalls = [];
|
|
300
|
+
const compactions = [];
|
|
301
|
+
const subagents = [];
|
|
302
|
+
const errors = [];
|
|
303
|
+
const invocations = [];
|
|
304
|
+
const stalls = [];
|
|
305
|
+
const loops = [];
|
|
306
|
+
const timeline = [];
|
|
307
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, messages: 0 };
|
|
308
|
+
const seenMessages = new Set();
|
|
309
|
+
const subagentByToolId = new Map();
|
|
310
|
+
const subagentByAgentId = new Map();
|
|
311
|
+
let idleGaps = 0;
|
|
312
|
+
|
|
313
|
+
for (const path of expandFiles(paths, o.subagents)) {
|
|
314
|
+
const name = displayName(path);
|
|
315
|
+
const at = (line) => `${name}:${line}`;
|
|
316
|
+
const stats = {};
|
|
317
|
+
const fileRow = {
|
|
318
|
+
name,
|
|
319
|
+
path: R.clean(path),
|
|
320
|
+
format: null,
|
|
321
|
+
sidechain: false,
|
|
322
|
+
usagePartial: false,
|
|
323
|
+
malformedAt: [],
|
|
324
|
+
};
|
|
325
|
+
let adapt = null;
|
|
326
|
+
const pending = new Map();
|
|
327
|
+
const groups = new Map();
|
|
328
|
+
let lastKey = null;
|
|
329
|
+
let streak = 0;
|
|
330
|
+
let turn = null;
|
|
331
|
+
let prev = null;
|
|
332
|
+
|
|
333
|
+
const openTurn = (line, ts, prompt) => {
|
|
334
|
+
turn = {
|
|
335
|
+
at: at(line),
|
|
336
|
+
prompt,
|
|
337
|
+
firstTs: ts,
|
|
338
|
+
lastTs: ts,
|
|
339
|
+
toolCalls: 0,
|
|
340
|
+
assistantMessages: 0,
|
|
341
|
+
errors: 0,
|
|
342
|
+
durationMs: null,
|
|
343
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, peakContext: 0 },
|
|
344
|
+
};
|
|
345
|
+
turns.push(turn);
|
|
346
|
+
};
|
|
347
|
+
const push = (event) => timeline.push(event);
|
|
348
|
+
|
|
349
|
+
for await (const row of readJsonl(path, { stats, maxBytes: o.maxBytes })) {
|
|
350
|
+
if (!row.record) {
|
|
351
|
+
if (row.reason !== "truncated") fileRow.malformedAt.push(at(row.line));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (!adapt) {
|
|
355
|
+
fileRow.format = detectFormat(row.record);
|
|
356
|
+
adapt = createAdapter(fileRow.format);
|
|
357
|
+
}
|
|
358
|
+
const line = row.line;
|
|
359
|
+
for (const rec of adapt(row.record)) {
|
|
360
|
+
const ts = tsOf(rec);
|
|
361
|
+
if (rec.isSidechain && fileRow.format === "claude") fileRow.sidechain = true;
|
|
362
|
+
|
|
363
|
+
if (ts !== null && prev && prev.ts !== null) {
|
|
364
|
+
const gap = ts - prev.ts;
|
|
365
|
+
if (gap >= stallMs) {
|
|
366
|
+
const startsTurn = rec.type === "user" && isPrompt(rec);
|
|
367
|
+
if (startsTurn && pending.size === 0) idleGaps++;
|
|
368
|
+
else {
|
|
369
|
+
const running = [...pending.values()].at(-1);
|
|
370
|
+
stalls.push({
|
|
371
|
+
from: at(prev.line),
|
|
372
|
+
to: at(line),
|
|
373
|
+
gapMs: gap,
|
|
374
|
+
pendingTool: running ? running.name : null,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (ts !== null) prev = { ts, line };
|
|
380
|
+
if (turn && ts !== null) turn.lastTs = ts;
|
|
381
|
+
|
|
382
|
+
if (rec.type === "x-usage") {
|
|
383
|
+
const add = rec.usage || {};
|
|
384
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
|
|
385
|
+
const n = numberOr(add[key]);
|
|
386
|
+
usage[key] += n;
|
|
387
|
+
if (turn && rec.inTurn) turn.usage[key] += n;
|
|
388
|
+
}
|
|
389
|
+
if (rec.countsMessage) usage.messages++;
|
|
390
|
+
if (turn && rec.inTurn) {
|
|
391
|
+
const ctx = numberOr(add.input) + numberOr(add.cacheRead) + numberOr(add.cacheWrite);
|
|
392
|
+
if (ctx > turn.usage.peakContext) turn.usage.peakContext = ctx;
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (rec.type === "system") {
|
|
398
|
+
if (rec.subtype === "compact_boundary") {
|
|
399
|
+
const m = rec.compactMetadata || {};
|
|
400
|
+
compactions.push({
|
|
401
|
+
at: at(line),
|
|
402
|
+
kind: "boundary",
|
|
403
|
+
trigger: R.clean(m.trigger || "unknown"),
|
|
404
|
+
preTokens: numberOr(m.preTokens, null),
|
|
405
|
+
postTokens: numberOr(m.postTokens, null),
|
|
406
|
+
});
|
|
407
|
+
push({ at: at(line), role: "system", kind: "compaction" });
|
|
408
|
+
} else if (rec.subtype === "turn_duration") {
|
|
409
|
+
if (turn) turn.durationMs = numberOr(rec.durationMs, null);
|
|
410
|
+
} else if (Array.isArray(rec.hookErrors) && rec.hookErrors.length) {
|
|
411
|
+
errors.push({
|
|
412
|
+
at: at(line),
|
|
413
|
+
kind: "hook",
|
|
414
|
+
tool: null,
|
|
415
|
+
text: ex(textOf(rec.hookErrors)),
|
|
416
|
+
});
|
|
417
|
+
if (turn) turn.errors++;
|
|
418
|
+
} else if (rec.level === "error") {
|
|
419
|
+
errors.push({ at: at(line), kind: "system", tool: null, text: ex(rec.content) });
|
|
420
|
+
if (turn) turn.errors++;
|
|
421
|
+
}
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (rec.type === "user") {
|
|
426
|
+
const content = rec.message?.content;
|
|
427
|
+
if (rec.isCompactSummary) {
|
|
428
|
+
compactions.push({ at: at(line), kind: "summary" });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (Array.isArray(content) && content.some((b) => b?.type === "tool_result")) {
|
|
432
|
+
let first = true;
|
|
433
|
+
for (const b of content) {
|
|
434
|
+
if (b?.type !== "tool_result") continue;
|
|
435
|
+
const call = pending.get(b.tool_use_id);
|
|
436
|
+
pending.delete(b.tool_use_id);
|
|
437
|
+
const result =
|
|
438
|
+
first && rec.toolUseResult !== undefined ? rec.toolUseResult : undefined;
|
|
439
|
+
first = false;
|
|
440
|
+
const body = textOf(b.content) || (typeof result === "string" ? result : "");
|
|
441
|
+
const denied =
|
|
442
|
+
b.denied === true ||
|
|
443
|
+
DENIAL.test(body) ||
|
|
444
|
+
(typeof result === "string" && DENIAL.test(result));
|
|
445
|
+
const isError = b.is_error === true || denied;
|
|
446
|
+
if (call) {
|
|
447
|
+
call.resultAt = at(line);
|
|
448
|
+
call.isError = isError;
|
|
449
|
+
const reported =
|
|
450
|
+
result && typeof result === "object"
|
|
451
|
+
? (result.durationMs ?? result.totalDurationMs)
|
|
452
|
+
: undefined;
|
|
453
|
+
if (Number.isFinite(reported)) {
|
|
454
|
+
call.durationMs = reported;
|
|
455
|
+
call.durationSource = "reported";
|
|
456
|
+
} else if (call.ts !== null && ts !== null) {
|
|
457
|
+
call.durationMs = ts - call.ts;
|
|
458
|
+
call.durationSource = "timestamps";
|
|
459
|
+
}
|
|
460
|
+
if (call.group) call.group.errors += isError ? 1 : 0;
|
|
461
|
+
const sub = subagentByToolId.get(b.tool_use_id);
|
|
462
|
+
if (sub) {
|
|
463
|
+
sub.resultAt = at(line);
|
|
464
|
+
if (result && typeof result === "object") {
|
|
465
|
+
sub.status = R.clean(result.status || (isError ? "error" : "completed"));
|
|
466
|
+
if (result.agentId) subagentByAgentId.set(String(result.agentId), sub);
|
|
467
|
+
if (Number.isFinite(result.totalDurationMs))
|
|
468
|
+
sub.durationMs = result.totalDurationMs;
|
|
469
|
+
if (Number.isFinite(result.totalTokens)) sub.tokens = result.totalTokens;
|
|
470
|
+
if (Number.isFinite(result.totalToolUseCount))
|
|
471
|
+
sub.toolUses = result.totalToolUseCount;
|
|
472
|
+
} else {
|
|
473
|
+
sub.status = isError ? "error" : "completed";
|
|
474
|
+
}
|
|
475
|
+
if (
|
|
476
|
+
!sub.durationMs &&
|
|
477
|
+
call.durationMs !== undefined &&
|
|
478
|
+
sub.status !== "async_launched"
|
|
479
|
+
)
|
|
480
|
+
sub.durationMs = call.durationMs;
|
|
481
|
+
sub.result = ex(body);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (isError) {
|
|
485
|
+
const kind = denied ? "denial" : HOOK_BLOCK.test(body) ? "hook" : "error";
|
|
486
|
+
errors.push({ at: at(line), kind, tool: call ? call.name : null, text: ex(body) });
|
|
487
|
+
if (turn) turn.errors++;
|
|
488
|
+
push({ at: at(line), role: "user", kind, name: call ? call.name : null });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const text = textOf(content);
|
|
495
|
+
if (rec.isSidechain) continue;
|
|
496
|
+
const notice = text.match(/<task-notification>([\s\S]*?)<\/task-notification>/);
|
|
497
|
+
if (notice) {
|
|
498
|
+
const id = (notice[1].match(/<task-id>([^<]+)<\/task-id>/) || [])[1];
|
|
499
|
+
const status = (notice[1].match(/<status>([^<]+)<\/status>/) || [])[1];
|
|
500
|
+
const sub = id ? subagentByAgentId.get(id.trim()) : null;
|
|
501
|
+
if (sub) {
|
|
502
|
+
sub.notifiedAt = at(line);
|
|
503
|
+
if (status) sub.status = R.clean(status.trim());
|
|
504
|
+
const st = rec.subagentStats;
|
|
505
|
+
if (st) {
|
|
506
|
+
if (Number.isFinite(st.tokens)) sub.tokens = st.tokens;
|
|
507
|
+
if (Number.isFinite(st.durationMs)) sub.durationMs = st.durationMs;
|
|
508
|
+
if (Number.isFinite(st.toolUses)) sub.toolUses = st.toolUses;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
push({ at: at(line), role: "user", kind: "notification", text: ex(status || "") });
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (INTERRUPT.test(text)) {
|
|
515
|
+
errors.push({ at: at(line), kind: "interrupt", tool: null, text: ex(text) });
|
|
516
|
+
if (turn) turn.errors++;
|
|
517
|
+
push({ at: at(line), role: "user", kind: "interrupt" });
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (rec.isMeta) continue;
|
|
521
|
+
const command = text.match(/<command-name>\s*([^<\s]+)\s*<\/command-name>/);
|
|
522
|
+
if (command) {
|
|
523
|
+
const cmd = R.clean(command[1]);
|
|
524
|
+
invocations.push({ at: at(line), kind: "command", name: cmd });
|
|
525
|
+
openTurn(line, ts, cmd);
|
|
526
|
+
push({ at: at(line), role: "user", kind: "command", name: cmd });
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (!text.trim()) continue;
|
|
530
|
+
const prompt = ex(text);
|
|
531
|
+
openTurn(line, ts, prompt);
|
|
532
|
+
push({ at: at(line), role: "user", kind: "prompt", text: prompt });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (rec.type === "assistant") {
|
|
537
|
+
const msg = rec.message || {};
|
|
538
|
+
if (!turn && !rec.isSidechain) openTurn(line, ts, null);
|
|
539
|
+
if (turn) turn.assistantMessages++;
|
|
540
|
+
const u = msg.usage;
|
|
541
|
+
if (u && !(msg.id && seenMessages.has(msg.id))) {
|
|
542
|
+
if (msg.id) seenMessages.add(msg.id);
|
|
543
|
+
const add = {
|
|
544
|
+
input: numberOr(u.input_tokens),
|
|
545
|
+
output: numberOr(u.output_tokens),
|
|
546
|
+
cacheRead: numberOr(u.cache_read_input_tokens),
|
|
547
|
+
cacheWrite: numberOr(u.cache_creation_input_tokens),
|
|
548
|
+
};
|
|
549
|
+
for (const k of Object.keys(add)) usage[k] += add[k];
|
|
550
|
+
usage.messages++;
|
|
551
|
+
if (turn) {
|
|
552
|
+
for (const k of Object.keys(add)) turn.usage[k] += add[k];
|
|
553
|
+
const ctx = add.input + add.cacheRead + add.cacheWrite;
|
|
554
|
+
if (ctx > turn.usage.peakContext) turn.usage.peakContext = ctx;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (rec.isApiErrorMessage) {
|
|
558
|
+
errors.push({
|
|
559
|
+
at: at(line),
|
|
560
|
+
kind: "api-error",
|
|
561
|
+
tool: null,
|
|
562
|
+
text: ex(textOf(msg.content)),
|
|
563
|
+
});
|
|
564
|
+
if (turn) turn.errors++;
|
|
565
|
+
}
|
|
566
|
+
for (const b of Array.isArray(msg.content) ? msg.content : []) {
|
|
567
|
+
if (b?.type === "text" && b.text?.trim()) {
|
|
568
|
+
push({ at: at(line), role: "assistant", kind: "text", text: ex(b.text) });
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (b?.type !== "tool_use") continue;
|
|
572
|
+
const toolName = R.clean(b.name || "unknown");
|
|
573
|
+
const input = b.input || {};
|
|
574
|
+
const key = `${b.name}\u0000${stable(input)}`;
|
|
575
|
+
let group = groups.get(key);
|
|
576
|
+
if (!group) {
|
|
577
|
+
group = { name: toolName, hash: digest(key), at: [], errors: 0, consecutive: 0 };
|
|
578
|
+
group.input = ex(inputGist(b.name, input));
|
|
579
|
+
groups.set(key, group);
|
|
580
|
+
}
|
|
581
|
+
group.at.push(at(line));
|
|
582
|
+
streak = key === lastKey ? streak + 1 : 1;
|
|
583
|
+
lastKey = key;
|
|
584
|
+
if (streak > group.consecutive) group.consecutive = streak;
|
|
585
|
+
const call = {
|
|
586
|
+
at: at(line),
|
|
587
|
+
name: toolName,
|
|
588
|
+
input: group.input,
|
|
589
|
+
ts,
|
|
590
|
+
durationMs: undefined,
|
|
591
|
+
durationSource: null,
|
|
592
|
+
resultAt: null,
|
|
593
|
+
isError: false,
|
|
594
|
+
group,
|
|
595
|
+
};
|
|
596
|
+
toolCalls.push(call);
|
|
597
|
+
if (b.id) pending.set(b.id, call);
|
|
598
|
+
if (turn) turn.toolCalls++;
|
|
599
|
+
push({
|
|
600
|
+
at: at(line),
|
|
601
|
+
role: "assistant",
|
|
602
|
+
kind: "tool",
|
|
603
|
+
name: toolName,
|
|
604
|
+
text: call.input,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
if (SUBAGENT_TOOLS.has(b.name)) {
|
|
608
|
+
const sub = {
|
|
609
|
+
at: at(line),
|
|
610
|
+
agentType: R.clean(input.subagent_type || input.agent_type || "general-purpose"),
|
|
611
|
+
description: ex(input.description || input.message || ""),
|
|
612
|
+
background: input.run_in_background === true || input.mode === "background",
|
|
613
|
+
model: input.model ? R.clean(input.model) : null,
|
|
614
|
+
status: null,
|
|
615
|
+
resultAt: null,
|
|
616
|
+
notifiedAt: null,
|
|
617
|
+
durationMs: null,
|
|
618
|
+
tokens: null,
|
|
619
|
+
toolUses: null,
|
|
620
|
+
result: null,
|
|
621
|
+
};
|
|
622
|
+
subagents.push(sub);
|
|
623
|
+
if (b.id) subagentByToolId.set(b.id, sub);
|
|
624
|
+
}
|
|
625
|
+
if (SKILL_TOOL.test(b.name || "") && (input.skill || input.command)) {
|
|
626
|
+
invocations.push({
|
|
627
|
+
at: at(line),
|
|
628
|
+
kind: "skill",
|
|
629
|
+
name: R.clean(input.skill || input.command),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (b.name === "SlashCommand" && input.command) {
|
|
633
|
+
invocations.push({ at: at(line), kind: "command", name: R.clean(input.command) });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
for (const g of groups.values()) {
|
|
641
|
+
if (g.at.length >= o.loopThreshold) {
|
|
642
|
+
loops.push({
|
|
643
|
+
name: g.name,
|
|
644
|
+
hash: g.hash,
|
|
645
|
+
count: g.at.length,
|
|
646
|
+
consecutive: g.consecutive,
|
|
647
|
+
errors: g.errors,
|
|
648
|
+
input: g.input,
|
|
649
|
+
at: g.at,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
fileRow.usagePartial = Boolean(adapt?.usagePartial?.());
|
|
654
|
+
fileRow.lines = stats.lines;
|
|
655
|
+
fileRow.malformed = stats.malformed;
|
|
656
|
+
fileRow.oversized = stats.oversized;
|
|
657
|
+
fileRow.truncated = stats.truncated;
|
|
658
|
+
fileRow.unreadable = stats.unreadable;
|
|
659
|
+
fileRow.bytesRead = stats.bytes;
|
|
660
|
+
files.push(fileRow);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
loops.sort((a, b) => b.count - a.count || b.consecutive - a.consecutive);
|
|
664
|
+
for (const t of turns) {
|
|
665
|
+
if (t.durationMs === null && t.firstTs !== null && t.lastTs !== null)
|
|
666
|
+
t.durationMs = t.lastTs - t.firstTs;
|
|
667
|
+
delete t.firstTs;
|
|
668
|
+
delete t.lastTs;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
let elided = 0;
|
|
672
|
+
let kept = timeline;
|
|
673
|
+
if (timeline.length > o.maxEvents) {
|
|
674
|
+
const half = Math.floor(o.maxEvents / 2);
|
|
675
|
+
elided = timeline.length - 2 * half;
|
|
676
|
+
kept = [...timeline.slice(0, half), ...timeline.slice(-half)];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
schema: "transcript-index/1",
|
|
681
|
+
files,
|
|
682
|
+
counts: {
|
|
683
|
+
turns: turns.length,
|
|
684
|
+
toolCalls: toolCalls.length,
|
|
685
|
+
loops: loops.length,
|
|
686
|
+
stalls: stalls.length,
|
|
687
|
+
idleGaps,
|
|
688
|
+
compactions: compactions.filter((c) => c.kind === "boundary").length,
|
|
689
|
+
subagents: subagents.length,
|
|
690
|
+
errors: errors.length,
|
|
691
|
+
malformedLines: files.reduce((a, f) => a + f.malformed, 0),
|
|
692
|
+
},
|
|
693
|
+
usage: { ...usage, partial: files.some((f) => f.usagePartial) },
|
|
694
|
+
turns,
|
|
695
|
+
loops,
|
|
696
|
+
stalls,
|
|
697
|
+
compactions,
|
|
698
|
+
subagents,
|
|
699
|
+
errors,
|
|
700
|
+
invocations,
|
|
701
|
+
toolCalls: toolCalls.map(({ group: _group, ts: _ts, ...c }) => c),
|
|
702
|
+
timeline: kept,
|
|
703
|
+
timelineElided: elided,
|
|
704
|
+
redaction: { total: R.total(), byKind: { ...R.byKind } },
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function isPrompt(rec) {
|
|
709
|
+
if (rec.isMeta || rec.isCompactSummary || rec.isSidechain) return false;
|
|
710
|
+
const c = rec.message?.content;
|
|
711
|
+
if (Array.isArray(c) && c.some((b) => b?.type === "tool_result")) return false;
|
|
712
|
+
const t = textOf(c);
|
|
713
|
+
return Boolean(t.trim()) && !t.includes("<task-notification>");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// --- finding a run's transcripts -----------------------------------------------
|
|
717
|
+
|
|
718
|
+
function firstTimestamp(path) {
|
|
719
|
+
return (async () => {
|
|
720
|
+
for await (const row of readJsonl(path, { maxBytes: 1024 * 1024 })) {
|
|
721
|
+
const t = row.record ? tsOf(row.record) : null;
|
|
722
|
+
if (t !== null) return t;
|
|
723
|
+
}
|
|
724
|
+
return null;
|
|
725
|
+
})();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function readState(dir) {
|
|
729
|
+
for (const p of [join(dir, "agent-state.json"), join(dir, "artifacts", "agent-state.json")]) {
|
|
730
|
+
if (!existsSync(p)) continue;
|
|
731
|
+
const doc = readRegularJson(p, null);
|
|
732
|
+
if (doc && typeof doc === "object") return { path: p, doc };
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function locateRun(id, root) {
|
|
738
|
+
const runs = listRuns(root);
|
|
739
|
+
const variants = new Set(taskIdVariants(id));
|
|
740
|
+
for (const r of runs) {
|
|
741
|
+
if (!variants.has(r.taskId)) continue;
|
|
742
|
+
const s = readState(r.dir);
|
|
743
|
+
return { run: r, state: s };
|
|
744
|
+
}
|
|
745
|
+
const short = String(id).replace(/^#/, "");
|
|
746
|
+
if (/^\d+$/.test(short)) {
|
|
747
|
+
for (const r of runs) {
|
|
748
|
+
const s = readState(r.dir);
|
|
749
|
+
if (s && Number(s.doc.shortId) === Number(short)) return { run: r, state: s };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function windowMatches(dirs, start, end) {
|
|
756
|
+
const files = [];
|
|
757
|
+
for (const d of dirs) {
|
|
758
|
+
for (const f of listJsonl(d)) {
|
|
759
|
+
const p = join(d, f);
|
|
760
|
+
let mtime;
|
|
761
|
+
try {
|
|
762
|
+
mtime = statSync(p).mtimeMs;
|
|
763
|
+
} catch {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (mtime < start) continue;
|
|
767
|
+
const first = await firstTimestamp(p);
|
|
768
|
+
if (first === null || first > end) continue;
|
|
769
|
+
files.push(p);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return files;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Every Copilot CLI session log under `root` (`<session>/events.jsonl`). */
|
|
776
|
+
function copilotLogs(root) {
|
|
777
|
+
let dirs;
|
|
778
|
+
try {
|
|
779
|
+
dirs = readdirSync(root, { withFileTypes: true });
|
|
780
|
+
} catch {
|
|
781
|
+
return [];
|
|
782
|
+
}
|
|
783
|
+
return dirs
|
|
784
|
+
.filter((e) => e.isDirectory())
|
|
785
|
+
.map((e) => join(root, e.name, COPILOT_LOG))
|
|
786
|
+
.filter((p) => existsSync(p))
|
|
787
|
+
.sort();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const CODEX_ROLLOUT = /^rollout-.*\.jsonl$/;
|
|
791
|
+
|
|
792
|
+
/** Every Codex rollout under `root` (`YYYY/MM/DD/rollout-*.jsonl`), at most four levels down. */
|
|
793
|
+
function codexRollouts(root, depth = 0) {
|
|
794
|
+
let entries;
|
|
795
|
+
try {
|
|
796
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
797
|
+
} catch {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
const out = [];
|
|
801
|
+
for (const e of entries) {
|
|
802
|
+
const p = join(root, e.name);
|
|
803
|
+
if (e.isDirectory() && depth < 3) out.push(...codexRollouts(p, depth + 1));
|
|
804
|
+
else if (e.isFile() && CODEX_ROLLOUT.test(e.name)) out.push(p);
|
|
805
|
+
}
|
|
806
|
+
return out.sort();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** The first timestamp and the starting cwd of a Copilot CLI or Codex session. */
|
|
810
|
+
async function sessionHead(path) {
|
|
811
|
+
let first = null;
|
|
812
|
+
let cwd = null;
|
|
813
|
+
let seen = 0;
|
|
814
|
+
for await (const row of readJsonl(path, { maxBytes: 1024 * 1024 })) {
|
|
815
|
+
if (!row.record) continue;
|
|
816
|
+
if (first === null) first = tsOf(row.record);
|
|
817
|
+
cwd = cwd ?? sessionCwd(row.record);
|
|
818
|
+
if ((first !== null && cwd !== null) || ++seen >= 8) break;
|
|
819
|
+
}
|
|
820
|
+
return { first, cwd };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const samePath = (p) => String(p).replace(/\/+$/, "");
|
|
824
|
+
|
|
825
|
+
async function hostWindowMatches(paths, cwds, start, end) {
|
|
826
|
+
const files = [];
|
|
827
|
+
for (const p of paths) {
|
|
828
|
+
let mtime;
|
|
829
|
+
try {
|
|
830
|
+
mtime = statSync(p).mtimeMs;
|
|
831
|
+
} catch {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (mtime < start) continue;
|
|
835
|
+
const head = await sessionHead(p);
|
|
836
|
+
if (head.first === null || head.first > end) continue;
|
|
837
|
+
if (head.cwd === null || !cwds.has(samePath(head.cwd))) continue;
|
|
838
|
+
files.push(p);
|
|
839
|
+
}
|
|
840
|
+
return files;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* The session files a run ran in, across Claude Code, Copilot CLI and Codex.
|
|
845
|
+
*
|
|
846
|
+
* A run launched with a session id (continuous mode, a client that set
|
|
847
|
+
* MULTI_AGENT_SESSION_ID) has it recorded in agent-state.json, and the file is
|
|
848
|
+
* `<sessionId>.jsonl` in whichever Claude Code project directory holds it,
|
|
849
|
+
* `<sessionId>/events.jsonl` under Copilot CLI's session-state, or the Codex
|
|
850
|
+
* rollout whose name ends in `-<sessionId>.jsonl`. An attended run records
|
|
851
|
+
* none, so the fallback is the sessions whose working directory was the
|
|
852
|
+
* worktree or the main checkout and whose time span overlaps the run's:
|
|
853
|
+
* started before the run finished, last written after it started. Claude Code
|
|
854
|
+
* keys its directories by the working directory; Copilot CLI and Codex record
|
|
855
|
+
* it in the session's opening record. That match is a heuristic and is
|
|
856
|
+
* reported as `time-window`.
|
|
857
|
+
*
|
|
858
|
+
* @returns {Promise<{taskId:string|null, runDir:string|null, matchedBy:"session"|"time-window"|null, files:string[], reason?:string}>}
|
|
859
|
+
*/
|
|
860
|
+
export async function findRunTranscripts(
|
|
861
|
+
id,
|
|
862
|
+
{
|
|
863
|
+
logsRoot: root = logsRoot(),
|
|
864
|
+
projectsRoot = defaultProjectsRoot(),
|
|
865
|
+
copilotRoot = defaultCopilotRoot(),
|
|
866
|
+
codexRoot = defaultCodexRoot(),
|
|
867
|
+
} = {},
|
|
868
|
+
) {
|
|
869
|
+
const hit = locateRun(id, root);
|
|
870
|
+
if (!hit)
|
|
871
|
+
return {
|
|
872
|
+
taskId: null,
|
|
873
|
+
runDir: null,
|
|
874
|
+
matchedBy: null,
|
|
875
|
+
files: [],
|
|
876
|
+
reason: "no run with this id",
|
|
877
|
+
};
|
|
878
|
+
const doc = hit.state?.doc || {};
|
|
879
|
+
const base = { taskId: doc.taskId || hit.run.taskId, runDir: hit.run.dir };
|
|
880
|
+
const none = (reason) => ({ ...base, matchedBy: null, files: [], reason });
|
|
881
|
+
|
|
882
|
+
let projectDirs = [];
|
|
883
|
+
try {
|
|
884
|
+
projectDirs = readdirSync(projectsRoot).map((d) => join(projectsRoot, d));
|
|
885
|
+
} catch {
|
|
886
|
+
// No Claude Code transcripts on this machine; the other hosts may have some.
|
|
887
|
+
}
|
|
888
|
+
const copilot = copilotLogs(copilotRoot);
|
|
889
|
+
const codex = codexRollouts(codexRoot);
|
|
890
|
+
if (!projectDirs.length && !copilot.length && !codex.length)
|
|
891
|
+
return none("no transcript directory");
|
|
892
|
+
|
|
893
|
+
if (typeof doc.sessionId === "string" && SAFE_SESSION_ID.test(doc.sessionId)) {
|
|
894
|
+
const sid = doc.sessionId;
|
|
895
|
+
const files = [
|
|
896
|
+
...projectDirs.map((d) => join(d, `${sid}.jsonl`)).filter((p) => existsSync(p)),
|
|
897
|
+
...copilot.filter((p) => basename(dirname(p)) === sid),
|
|
898
|
+
...codex.filter((p) => basename(p).endsWith(`-${sid}.jsonl`)),
|
|
899
|
+
];
|
|
900
|
+
if (files.length) return { ...base, matchedBy: "session", files };
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const start = Date.parse(doc.startedAt || "");
|
|
904
|
+
let end = Date.parse(doc.finishedAt || "");
|
|
905
|
+
if (!Number.isFinite(end)) {
|
|
906
|
+
try {
|
|
907
|
+
end = statSync(hit.state.path).mtimeMs;
|
|
908
|
+
} catch {
|
|
909
|
+
end = Date.now();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const cwds = [doc.worktreePath, doc.projectRoot];
|
|
913
|
+
for (const p of Array.isArray(doc.projects) ? doc.projects : [])
|
|
914
|
+
cwds.push(p?.worktreePath, p?.projectRoot);
|
|
915
|
+
const known = cwds.filter((c) => typeof c === "string" && c);
|
|
916
|
+
const slugs = new Set(known.map(projectSlug));
|
|
917
|
+
const dirs = projectDirs.filter((d) => slugs.has(basename(d)));
|
|
918
|
+
if (!Number.isFinite(start) || (!dirs.length && !copilot.length && !codex.length))
|
|
919
|
+
return none("no session id recorded and no transcript directory for the run's checkout");
|
|
920
|
+
|
|
921
|
+
const files = [
|
|
922
|
+
...(await windowMatches(dirs, start, end)),
|
|
923
|
+
...(await hostWindowMatches([...copilot, ...codex], new Set(known.map(samePath)), start, end)),
|
|
924
|
+
];
|
|
925
|
+
return files.length
|
|
926
|
+
? { ...base, matchedBy: "time-window", files }
|
|
927
|
+
: none("no session overlaps the run's time window");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// --- rendering -----------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
const dur = (ms) => {
|
|
933
|
+
if (ms === null || ms === undefined || !Number.isFinite(ms)) return "-";
|
|
934
|
+
const s = Math.round(ms / 1000);
|
|
935
|
+
if (s < 60) return `${s}s`;
|
|
936
|
+
const m = Math.floor(s / 60);
|
|
937
|
+
if (m < 60) return `${m}m${String(s % 60).padStart(2, "0")}s`;
|
|
938
|
+
return `${Math.floor(m / 60)}h${String(m % 60).padStart(2, "0")}m`;
|
|
939
|
+
};
|
|
940
|
+
const k = (n) => (n >= 1000 ? `${Math.round(n / 1000)}k` : String(n));
|
|
941
|
+
const citeList = (ats, max = 6) => {
|
|
942
|
+
const shown = ats.slice(0, max).join(", ");
|
|
943
|
+
return ats.length > max ? `${shown}, ... (${ats.length - max} more)` : shown;
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
/** The index as compact text, one section per kind of finding. */
|
|
947
|
+
export function renderIndex(idx) {
|
|
948
|
+
const L = [];
|
|
949
|
+
const c = idx.counts;
|
|
950
|
+
const by = Object.entries(idx.redaction.byKind)
|
|
951
|
+
.sort((a, b) => b[1] - a[1])
|
|
952
|
+
.map(([kind, n]) => `${kind} ${n}`)
|
|
953
|
+
.join(", ");
|
|
954
|
+
L.push(
|
|
955
|
+
`transcript index - ${idx.files.length} file(s), ${c.turns} turn(s), ${c.toolCalls} tool call(s), ` +
|
|
956
|
+
`${c.malformedLines} malformed line(s) skipped, redacted ${idx.redaction.total} item(s)${by ? ` (${by})` : ""}`,
|
|
957
|
+
);
|
|
958
|
+
L.push(
|
|
959
|
+
`usage: in ${k(idx.usage.input)} / out ${k(idx.usage.output)} / cache read ${k(idx.usage.cacheRead)} / cache write ${k(idx.usage.cacheWrite)} over ${idx.usage.messages} message(s)` +
|
|
960
|
+
(idx.usage.partial
|
|
961
|
+
? " - partial: a Copilot CLI session wrote no session.shutdown record, so only its output tokens are counted"
|
|
962
|
+
: ""),
|
|
963
|
+
);
|
|
964
|
+
L.push("", "files");
|
|
965
|
+
for (const f of idx.files) {
|
|
966
|
+
const flags = [
|
|
967
|
+
f.sidechain ? "subagent" : null,
|
|
968
|
+
f.malformed ? `malformed ${f.malformed} (${citeList(f.malformedAt, 4)})` : null,
|
|
969
|
+
f.oversized ? `oversized ${f.oversized}` : null,
|
|
970
|
+
f.truncated ? "truncated at the byte cap" : null,
|
|
971
|
+
f.unreadable ? "unreadable" : null,
|
|
972
|
+
f.usagePartial ? "usage partial (no session.shutdown)" : null,
|
|
973
|
+
].filter(Boolean);
|
|
974
|
+
L.push(
|
|
975
|
+
` ${f.name} ${f.format ?? "empty"} ${f.lines} lines${flags.length ? ` ${flags.join(" ")}` : ""}`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
L.push("", `turns (${idx.turns.length})`);
|
|
979
|
+
idx.turns.forEach((t, i) => {
|
|
980
|
+
const u = t.usage;
|
|
981
|
+
L.push(
|
|
982
|
+
` #${i + 1} ${t.at} ${t.toolCalls} tool(s) ${t.errors} error(s) out ${k(u.output)} peak ctx ${k(u.peakContext)} ${dur(t.durationMs)} ${t.prompt ? `"${t.prompt}"` : "(no prompt)"}`,
|
|
983
|
+
);
|
|
984
|
+
});
|
|
985
|
+
L.push("", `loops - identical calls repeated (${idx.loops.length})`);
|
|
986
|
+
if (!idx.loops.length) L.push(" none");
|
|
987
|
+
for (const l of idx.loops)
|
|
988
|
+
L.push(
|
|
989
|
+
` ${l.name} x${l.count} longest streak ${l.consecutive} errored ${l.errors} "${l.input}" at ${citeList(l.at)}`,
|
|
990
|
+
);
|
|
991
|
+
L.push(
|
|
992
|
+
"",
|
|
993
|
+
`stalls - gaps while working (${idx.stalls.length}; idle gaps between turns: ${idx.counts.idleGaps})`,
|
|
994
|
+
);
|
|
995
|
+
if (!idx.stalls.length) L.push(" none");
|
|
996
|
+
for (const s of idx.stalls)
|
|
997
|
+
L.push(
|
|
998
|
+
` ${dur(s.gapMs)} ${s.from} -> ${s.to}${s.pendingTool ? ` running: ${s.pendingTool}` : ""}`,
|
|
999
|
+
);
|
|
1000
|
+
L.push("", `compactions (${idx.counts.compactions})`);
|
|
1001
|
+
if (!idx.compactions.length) L.push(" none");
|
|
1002
|
+
for (const m of idx.compactions)
|
|
1003
|
+
L.push(
|
|
1004
|
+
m.kind === "boundary"
|
|
1005
|
+
? ` ${m.at} ${m.trigger} ${m.preTokens === null ? "?" : k(m.preTokens)} -> ${m.postTokens === null ? "?" : k(m.postTokens)} tokens`
|
|
1006
|
+
: ` ${m.at} summary turn`,
|
|
1007
|
+
);
|
|
1008
|
+
L.push("", `subagents (${idx.subagents.length})`);
|
|
1009
|
+
if (!idx.subagents.length) L.push(" none");
|
|
1010
|
+
for (const s of idx.subagents)
|
|
1011
|
+
L.push(
|
|
1012
|
+
` ${s.at} ${s.agentType}${s.background ? " (background)" : ""} ${s.status ?? "no result"} ${dur(s.durationMs)}` +
|
|
1013
|
+
`${s.tokens !== null ? ` ${k(s.tokens)} tokens` : ""} "${s.description}"` +
|
|
1014
|
+
`${s.resultAt ? ` result ${s.resultAt}` : ""}${s.notifiedAt ? ` notified ${s.notifiedAt}` : ""}`,
|
|
1015
|
+
);
|
|
1016
|
+
L.push("", `errors and denials (${idx.errors.length})`);
|
|
1017
|
+
if (!idx.errors.length) L.push(" none");
|
|
1018
|
+
for (const e of idx.errors.slice(0, 40))
|
|
1019
|
+
L.push(` ${e.at} ${e.kind}${e.tool ? ` ${e.tool}` : ""} "${e.text}"`);
|
|
1020
|
+
if (idx.errors.length > 40) L.push(` ... ${idx.errors.length - 40} more`);
|
|
1021
|
+
L.push("", `invocations (${idx.invocations.length})`);
|
|
1022
|
+
if (!idx.invocations.length) L.push(" none");
|
|
1023
|
+
for (const v of idx.invocations) L.push(` ${v.at} ${v.kind} ${v.name}`);
|
|
1024
|
+
const slow = idx.toolCalls
|
|
1025
|
+
.filter((t) => Number.isFinite(t.durationMs))
|
|
1026
|
+
.sort((a, b) => b.durationMs - a.durationMs)
|
|
1027
|
+
.slice(0, 5);
|
|
1028
|
+
L.push("", "slowest tool calls");
|
|
1029
|
+
if (!slow.length) L.push(" none timed");
|
|
1030
|
+
for (const t of slow) L.push(` ${dur(t.durationMs)} ${t.at} ${t.name} "${t.input}"`);
|
|
1031
|
+
L.push(
|
|
1032
|
+
"",
|
|
1033
|
+
`timeline (${idx.timeline.length} event(s)${idx.timelineElided ? `, ${idx.timelineElided} in the middle elided` : ""})`,
|
|
1034
|
+
);
|
|
1035
|
+
for (const e of idx.timeline)
|
|
1036
|
+
L.push(
|
|
1037
|
+
` ${e.at} ${e.role} ${e.kind}${e.name ? ` ${e.name}` : ""}${e.text ? ` "${e.text}"` : ""}`,
|
|
1038
|
+
);
|
|
1039
|
+
return `${L.join("\n")}\n`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// --- CLI -----------------------------------------------------------------------
|
|
1043
|
+
|
|
1044
|
+
const NUMERIC = {
|
|
1045
|
+
"--max-mb": "maxBytes",
|
|
1046
|
+
"--max-events": "maxEvents",
|
|
1047
|
+
"--loop-threshold": "loopThreshold",
|
|
1048
|
+
"--stall-minutes": "stallMinutes",
|
|
1049
|
+
"--excerpt": "excerpt",
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
function parseArgs(argv) {
|
|
1053
|
+
const a = { files: [], json: false, locate: false, run: null, opts: {} };
|
|
1054
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1055
|
+
const v = argv[i];
|
|
1056
|
+
if (v === "--json") a.json = true;
|
|
1057
|
+
else if (v === "--locate") a.locate = true;
|
|
1058
|
+
else if (v === "--no-subagents") a.opts.subagents = false;
|
|
1059
|
+
else if (v === "--run") a.run = argv[++i] ?? null;
|
|
1060
|
+
else if (NUMERIC[v]) {
|
|
1061
|
+
const n = Number(argv[++i]);
|
|
1062
|
+
if (!Number.isFinite(n) || n <= 0) return { error: `${v} takes a positive number` };
|
|
1063
|
+
a.opts[NUMERIC[v]] = v === "--max-mb" ? Math.floor(n * 1024 * 1024) : n;
|
|
1064
|
+
} else if (v.startsWith("--")) return { error: `unknown option ${v}` };
|
|
1065
|
+
else a.files.push(v);
|
|
1066
|
+
}
|
|
1067
|
+
if (a.run === "") a.run = null;
|
|
1068
|
+
if (!a.run && !a.files.length) return { error: "give transcript files or --run <id>" };
|
|
1069
|
+
if (a.locate && !a.run) return { error: "--locate needs --run" };
|
|
1070
|
+
return a;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const tilde = (p) => {
|
|
1074
|
+
const h = homedir();
|
|
1075
|
+
return p && h && p.startsWith(`${h}/`) ? `~${p.slice(h.length)}` : p;
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
async function main() {
|
|
1079
|
+
const a = parseArgs(process.argv.slice(2));
|
|
1080
|
+
if (a.error) {
|
|
1081
|
+
process.stderr.write(
|
|
1082
|
+
`transcript-index: ${a.error}\nusage: transcript-index.mjs <file.jsonl>... | --run <id> [--locate] [--json] [--no-subagents] [--max-mb N] [--max-events N] [--loop-threshold N] [--stall-minutes N] [--excerpt N]\n`,
|
|
1083
|
+
);
|
|
1084
|
+
process.exitCode = 2;
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
let files = a.files;
|
|
1088
|
+
let run = null;
|
|
1089
|
+
if (a.run) {
|
|
1090
|
+
run = await findRunTranscripts(a.run);
|
|
1091
|
+
if (a.locate) {
|
|
1092
|
+
const R = createRedactor();
|
|
1093
|
+
const out = {
|
|
1094
|
+
taskId: run.taskId ? R.clean(run.taskId) : null,
|
|
1095
|
+
runDir: run.runDir ? tilde(run.runDir) : null,
|
|
1096
|
+
matchedBy: run.matchedBy,
|
|
1097
|
+
files: run.files.map((f) => displayName(f)),
|
|
1098
|
+
reason: run.reason ?? null,
|
|
1099
|
+
};
|
|
1100
|
+
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
|
1101
|
+
process.exitCode = run.files.length ? 0 : 3;
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
files = run.files;
|
|
1105
|
+
}
|
|
1106
|
+
if (!files.length) {
|
|
1107
|
+
process.stderr.write(
|
|
1108
|
+
`transcript-index: nothing to read${run?.reason ? ` (${run.reason})` : ""}\n`,
|
|
1109
|
+
);
|
|
1110
|
+
process.exitCode = 3;
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const idx = await indexTranscripts(files, a.opts);
|
|
1114
|
+
if (run) idx.run = { taskId: run.taskId, matchedBy: run.matchedBy };
|
|
1115
|
+
if (!idx.files.some((f) => f.lines > 0)) {
|
|
1116
|
+
process.stderr.write("transcript-index: nothing to read (empty or unreadable files)\n");
|
|
1117
|
+
process.exitCode = 3;
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const out = a.json ? `${JSON.stringify(idx, null, 2)}\n` : renderIndex(idx);
|
|
1121
|
+
try {
|
|
1122
|
+
assertClean(out);
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
process.stderr.write(`transcript-index: ${err.message}; nothing was printed\n`);
|
|
1125
|
+
process.exitCode = 4;
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
process.stdout.write(out);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (invokedDirectly(import.meta.url)) runMain("transcript-index", main, { code: 2 });
|