@compaction/cli 0.1.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.
Files changed (143) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +319 -0
  3. package/dist/cli/commands/adapter.d.ts +2 -0
  4. package/dist/cli/commands/adapter.js +49 -0
  5. package/dist/cli/commands/aggregate.d.ts +2 -0
  6. package/dist/cli/commands/aggregate.js +60 -0
  7. package/dist/cli/commands/analyze.d.ts +2 -0
  8. package/dist/cli/commands/analyze.js +55 -0
  9. package/dist/cli/commands/apply-context.d.ts +12 -0
  10. package/dist/cli/commands/apply-context.js +80 -0
  11. package/dist/cli/commands/apply.d.ts +2 -0
  12. package/dist/cli/commands/apply.js +65 -0
  13. package/dist/cli/commands/approve.d.ts +2 -0
  14. package/dist/cli/commands/approve.js +55 -0
  15. package/dist/cli/commands/audit.d.ts +2 -0
  16. package/dist/cli/commands/audit.js +63 -0
  17. package/dist/cli/commands/billing-delta.d.ts +2 -0
  18. package/dist/cli/commands/billing-delta.js +246 -0
  19. package/dist/cli/commands/capture-claude-code.d.ts +19 -0
  20. package/dist/cli/commands/capture-claude-code.js +198 -0
  21. package/dist/cli/commands/capture-provider-usage.d.ts +35 -0
  22. package/dist/cli/commands/capture-provider-usage.js +113 -0
  23. package/dist/cli/commands/capture.d.ts +2 -0
  24. package/dist/cli/commands/capture.js +76 -0
  25. package/dist/cli/commands/compact.d.ts +2 -0
  26. package/dist/cli/commands/compact.js +206 -0
  27. package/dist/cli/commands/eval.d.ts +2 -0
  28. package/dist/cli/commands/eval.js +66 -0
  29. package/dist/cli/commands/export-session-seed.d.ts +11 -0
  30. package/dist/cli/commands/export-session-seed.js +42 -0
  31. package/dist/cli/commands/feedback.d.ts +2 -0
  32. package/dist/cli/commands/feedback.js +233 -0
  33. package/dist/cli/commands/import.d.ts +2 -0
  34. package/dist/cli/commands/import.js +60 -0
  35. package/dist/cli/commands/init.d.ts +2 -0
  36. package/dist/cli/commands/init.js +216 -0
  37. package/dist/cli/commands/integrations.d.ts +2 -0
  38. package/dist/cli/commands/integrations.js +29 -0
  39. package/dist/cli/commands/launch-treatment-session.d.ts +14 -0
  40. package/dist/cli/commands/launch-treatment-session.js +50 -0
  41. package/dist/cli/commands/optimize.d.ts +2 -0
  42. package/dist/cli/commands/optimize.js +33 -0
  43. package/dist/cli/commands/recommend.d.ts +4 -0
  44. package/dist/cli/commands/recommend.js +66 -0
  45. package/dist/cli/commands/review.d.ts +2 -0
  46. package/dist/cli/commands/review.js +60 -0
  47. package/dist/cli/commands/run.d.ts +2 -0
  48. package/dist/cli/commands/run.js +49 -0
  49. package/dist/cli/commands/spend.d.ts +2 -0
  50. package/dist/cli/commands/spend.js +24 -0
  51. package/dist/cli/commands/summary.d.ts +2 -0
  52. package/dist/cli/commands/summary.js +19 -0
  53. package/dist/cli/engine-degrade.d.ts +40 -0
  54. package/dist/cli/engine-degrade.js +59 -0
  55. package/dist/cli/index.d.ts +2 -0
  56. package/dist/cli/index.js +59 -0
  57. package/dist/core/adapter-validation.d.ts +32 -0
  58. package/dist/core/adapter-validation.js +185 -0
  59. package/dist/core/adapters/claude-code-adapter.d.ts +51 -0
  60. package/dist/core/adapters/claude-code-adapter.js +539 -0
  61. package/dist/core/adapters/claude-code-discovery.d.ts +40 -0
  62. package/dist/core/adapters/claude-code-discovery.js +100 -0
  63. package/dist/core/aggregate-format.d.ts +62 -0
  64. package/dist/core/aggregate-format.js +349 -0
  65. package/dist/core/api-client/client.d.ts +43 -0
  66. package/dist/core/api-client/client.js +143 -0
  67. package/dist/core/api-client/config.d.ts +36 -0
  68. package/dist/core/api-client/config.js +44 -0
  69. package/dist/core/api-client/index.d.ts +13 -0
  70. package/dist/core/api-client/index.js +14 -0
  71. package/dist/core/api-client/payload.d.ts +93 -0
  72. package/dist/core/api-client/payload.js +180 -0
  73. package/dist/core/api-client/types.d.ts +95 -0
  74. package/dist/core/api-client/types.js +21 -0
  75. package/dist/core/artifact-writer.d.ts +2 -0
  76. package/dist/core/artifact-writer.js +14 -0
  77. package/dist/core/audit-report.d.ts +91 -0
  78. package/dist/core/audit-report.js +422 -0
  79. package/dist/core/billing-delta/billing-delta-record.d.ts +290 -0
  80. package/dist/core/billing-delta/billing-delta-record.js +394 -0
  81. package/dist/core/capture-adapter.d.ts +74 -0
  82. package/dist/core/capture-adapter.js +2 -0
  83. package/dist/core/command-runner.d.ts +29 -0
  84. package/dist/core/command-runner.js +96 -0
  85. package/dist/core/content-hash.d.ts +1 -0
  86. package/dist/core/content-hash.js +5 -0
  87. package/dist/core/cost-calculator.d.ts +6 -0
  88. package/dist/core/cost-calculator.js +20 -0
  89. package/dist/core/feedback-bundle.d.ts +159 -0
  90. package/dist/core/feedback-bundle.js +406 -0
  91. package/dist/core/openai-agents-capture.d.ts +66 -0
  92. package/dist/core/openai-agents-capture.js +475 -0
  93. package/dist/core/policy-types.d.ts +85 -0
  94. package/dist/core/policy-types.js +29 -0
  95. package/dist/core/pricing.d.ts +23 -0
  96. package/dist/core/pricing.js +55 -0
  97. package/dist/core/provider-usage/credential-redaction.d.ts +23 -0
  98. package/dist/core/provider-usage/credential-redaction.js +60 -0
  99. package/dist/core/provider-usage/provider-usage-adapter.d.ts +65 -0
  100. package/dist/core/provider-usage/provider-usage-adapter.js +147 -0
  101. package/dist/core/provider-usage/provider-usage-client.d.ts +89 -0
  102. package/dist/core/provider-usage/provider-usage-client.js +96 -0
  103. package/dist/core/report-generator.d.ts +7 -0
  104. package/dist/core/report-generator.js +275 -0
  105. package/dist/core/run-aggregator.d.ts +50 -0
  106. package/dist/core/run-aggregator.js +251 -0
  107. package/dist/core/run-labels.d.ts +52 -0
  108. package/dist/core/run-labels.js +80 -0
  109. package/dist/core/run-trace-converter.d.ts +3 -0
  110. package/dist/core/run-trace-converter.js +94 -0
  111. package/dist/core/safety-report.d.ts +52 -0
  112. package/dist/core/safety-report.js +440 -0
  113. package/dist/core/savings.d.ts +22 -0
  114. package/dist/core/savings.js +36 -0
  115. package/dist/core/session-aggregate.d.ts +211 -0
  116. package/dist/core/session-aggregate.js +490 -0
  117. package/dist/core/skill-injection-detector.d.ts +51 -0
  118. package/dist/core/skill-injection-detector.js +146 -0
  119. package/dist/core/spend-attribution.d.ts +169 -0
  120. package/dist/core/spend-attribution.js +611 -0
  121. package/dist/core/standalone-capsule-provenance.d.ts +4 -0
  122. package/dist/core/standalone-capsule-provenance.js +62 -0
  123. package/dist/core/state-capsule.d.ts +3 -0
  124. package/dist/core/state-capsule.js +147 -0
  125. package/dist/core/token-accounting.d.ts +62 -0
  126. package/dist/core/token-accounting.js +81 -0
  127. package/dist/core/token-estimator.d.ts +3 -0
  128. package/dist/core/token-estimator.js +18 -0
  129. package/dist/core/trace-adapters.d.ts +53 -0
  130. package/dist/core/trace-adapters.js +714 -0
  131. package/dist/core/trace-fingerprint.d.ts +79 -0
  132. package/dist/core/trace-fingerprint.js +102 -0
  133. package/dist/core/trace-intake.d.ts +41 -0
  134. package/dist/core/trace-intake.js +164 -0
  135. package/dist/core/trace-parser.d.ts +203 -0
  136. package/dist/core/trace-parser.js +143 -0
  137. package/dist/core/types.d.ts +238 -0
  138. package/dist/core/types.js +2 -0
  139. package/dist/core/usage-metadata.d.ts +51 -0
  140. package/dist/core/usage-metadata.js +122 -0
  141. package/dist/core/waste-detector.d.ts +8 -0
  142. package/dist/core/waste-detector.js +58 -0
  143. package/package.json +90 -0
@@ -0,0 +1,539 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join, dirname, basename } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { createUsageMetadata } from "../usage-metadata.js";
7
+ import { computeTraceFingerprint } from "../trace-fingerprint.js";
8
+ import { CURRENT_AGENT_TRACE_ARTIFACT_VERSION } from "../trace-parser.js";
9
+ // ─── Constants ────────────────────────────────────────────────────────────────
10
+ const MAX_TOOL_RESULT_CHARS = 32_000;
11
+ const TRUNCATION_SUFFIX = "[TRUNCATED — full content in source session JSONL]";
12
+ export const PRIVACY_WARNING = "WARNING: The Claude Code session file may contain file content read during your session\n" +
13
+ "(source code, config files, Bash output, etc.). The captured trace is written locally\n" +
14
+ "to the output directory only. Do not share captured-trace.json artifacts without reviewing\n" +
15
+ "them for sensitive content first.";
16
+ const SUBAGENT_WARNING = "Subagent JSONL files were not included in this capture. Pass --include-subagents to include them.";
17
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
18
+ function extractTextContent(content) {
19
+ if (!content)
20
+ return "";
21
+ if (typeof content === "string")
22
+ return content;
23
+ return content
24
+ .filter((item) => item.type === "text" && item.text !== undefined)
25
+ .map((item) => item.text ?? "")
26
+ .join("\n");
27
+ }
28
+ function serializeToolInput(input) {
29
+ if (input === null || input === undefined)
30
+ return "";
31
+ if (typeof input === "string")
32
+ return input;
33
+ // Deterministic key order at each nesting level
34
+ return JSON.stringify(sortKeys(input));
35
+ }
36
+ function sortKeys(value) {
37
+ if (Array.isArray(value)) {
38
+ return value.map(sortKeys);
39
+ }
40
+ if (value !== null && typeof value === "object") {
41
+ const obj = value;
42
+ return Object.fromEntries(Object.keys(obj)
43
+ .sort()
44
+ .map((k) => [k, sortKeys(obj[k])]));
45
+ }
46
+ return value;
47
+ }
48
+ function truncate(text, maxChars) {
49
+ if (text.length <= maxChars)
50
+ return { text, truncated: false };
51
+ return { text: text.slice(0, maxChars) + "\n" + TRUNCATION_SUFFIX, truncated: true };
52
+ }
53
+ function extractToolResultContent(content, maxChars) {
54
+ if (!content)
55
+ return { text: "", truncated: false };
56
+ if (typeof content === "string") {
57
+ return truncate(content, maxChars);
58
+ }
59
+ // Join multiple content items
60
+ const joined = content
61
+ .map((item) => {
62
+ if (item.type === "text")
63
+ return item.text ?? "";
64
+ if (item.type === "tool_result") {
65
+ const inner = item.content;
66
+ if (typeof inner === "string")
67
+ return inner;
68
+ if (Array.isArray(inner))
69
+ return inner.map((c) => c.text ?? "").join("\n");
70
+ }
71
+ return "";
72
+ })
73
+ .filter(Boolean)
74
+ .join("\n---\n");
75
+ return truncate(joined, maxChars);
76
+ }
77
+ // ─── Read JSONL entries ───────────────────────────────────────────────────────
78
+ async function readJsonlEntries(sourcePath) {
79
+ const entries = [];
80
+ const rl = createInterface({
81
+ input: createReadStream(sourcePath, { encoding: "utf8" }),
82
+ crlfDelay: Infinity
83
+ });
84
+ for await (const line of rl) {
85
+ const trimmed = line.trim();
86
+ if (!trimmed)
87
+ continue;
88
+ try {
89
+ entries.push(JSON.parse(trimmed));
90
+ }
91
+ catch {
92
+ // Skip malformed lines
93
+ }
94
+ }
95
+ return entries;
96
+ }
97
+ /**
98
+ * Process a list of JSONL entries into messages and usage accumulators.
99
+ *
100
+ * @param entries - Parsed JSONL entries.
101
+ * @param maxToolResultChars - Max characters for tool result content.
102
+ * @param includeAllIsSidechain - When true (subagent mode), include user/assistant entries
103
+ * regardless of isSidechain (all subagent entries have isSidechain:true by design).
104
+ * When false (main session mode), exclude entries with isSidechain:true.
105
+ * @param idPrefix - If provided, prefix each message id with "<idPrefix>-".
106
+ */
107
+ function processEntries(entries, maxToolResultChars, includeAllIsSidechain, idPrefix) {
108
+ let sessionId = null;
109
+ let model = "unknown";
110
+ let aiTitle;
111
+ let firstTimestamp;
112
+ let lastTimestamp;
113
+ let thinkingBlockCount = 0;
114
+ let truncationCount = 0;
115
+ let inputTokens = 0;
116
+ let outputTokens = 0;
117
+ let cacheCreationTokens = 0;
118
+ let cacheReadTokens = 0;
119
+ let assistantCount = 0;
120
+ let userCount = 0;
121
+ let otherCount = 0;
122
+ let agentId;
123
+ // Build a lookup map from tool_use_id → tool type name by scanning all assistant entries.
124
+ // This enables tool_result messages to carry the human-readable tool type name (e.g. "Read",
125
+ // "Bash") instead of the globally-unique tool_use_id UUID, which would make waste detection
126
+ // structurally impossible since no two UUIDs ever match.
127
+ const toolUseIdToName = new Map();
128
+ for (const entry of entries) {
129
+ if (entry.type !== "assistant")
130
+ continue;
131
+ const msg = entry.message;
132
+ if (!msg || !msg.content || typeof msg.content === "string")
133
+ continue;
134
+ for (const item of msg.content) {
135
+ if (item.type === "tool_use" && item.id && item.name) {
136
+ toolUseIdToName.set(item.id, item.name);
137
+ }
138
+ }
139
+ }
140
+ const messages = [];
141
+ for (const entry of entries) {
142
+ // Capture sessionId from any entry
143
+ if (!sessionId && entry.sessionId) {
144
+ sessionId = entry.sessionId;
145
+ }
146
+ // Capture agentId from any entry (present in subagent files)
147
+ if (!agentId && typeof entry["agentId"] === "string") {
148
+ agentId = entry["agentId"];
149
+ }
150
+ // Capture ai-title
151
+ if (entry.type === "ai-title" && entry.aiTitle) {
152
+ aiTitle = entry.aiTitle;
153
+ continue;
154
+ }
155
+ // Skip non-conversation entries
156
+ if (!["user", "assistant"].includes(entry.type)) {
157
+ otherCount++;
158
+ continue;
159
+ }
160
+ // isSidechain handling:
161
+ // Main session: exclude entries with isSidechain === true (they are subagent branches)
162
+ // Subagent file: include all user/assistant entries regardless of isSidechain
163
+ // (all subagent entries have isSidechain:true by definition)
164
+ if (!includeAllIsSidechain && entry.isSidechain === true) {
165
+ otherCount++;
166
+ continue;
167
+ }
168
+ const timestamp = entry.timestamp ?? new Date().toISOString();
169
+ if (!firstTimestamp)
170
+ firstTimestamp = timestamp;
171
+ lastTimestamp = timestamp;
172
+ const makeId = (fallback) => {
173
+ const base = entry.uuid ?? fallback;
174
+ return idPrefix ? `${idPrefix}-${base}` : base;
175
+ };
176
+ if (entry.type === "assistant") {
177
+ assistantCount++;
178
+ const msg = entry.message;
179
+ if (!msg)
180
+ continue;
181
+ // Capture model from first assistant entry
182
+ if (model === "unknown" && msg.model) {
183
+ model = msg.model;
184
+ }
185
+ // Aggregate usage
186
+ if (msg.usage) {
187
+ inputTokens += msg.usage.input_tokens ?? 0;
188
+ outputTokens += msg.usage.output_tokens ?? 0;
189
+ cacheCreationTokens += msg.usage.cache_creation_input_tokens ?? 0;
190
+ cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
191
+ }
192
+ // Extract content items
193
+ const content = msg.content;
194
+ if (!content)
195
+ continue;
196
+ const items = typeof content === "string"
197
+ ? [{ type: "text", text: content }]
198
+ : content;
199
+ for (const item of items) {
200
+ if (item.type === "thinking") {
201
+ thinkingBlockCount++;
202
+ continue; // Exclude thinking blocks
203
+ }
204
+ if (item.type === "text" && item.text !== undefined) {
205
+ messages.push({
206
+ id: makeId(`${sessionId ?? "unknown"}-${messages.length}`),
207
+ role: "assistant",
208
+ content: item.text,
209
+ timestamp
210
+ });
211
+ }
212
+ else if (item.type === "tool_use") {
213
+ const serialized = serializeToolInput(item.input);
214
+ const { text: truncatedContent, truncated } = truncate(serialized, maxToolResultChars);
215
+ if (truncated) {
216
+ truncationCount++;
217
+ }
218
+ messages.push({
219
+ id: makeId(`${sessionId ?? "unknown"}-${messages.length}`),
220
+ role: "assistant",
221
+ content: truncatedContent,
222
+ timestamp,
223
+ toolName: item.name ?? undefined
224
+ });
225
+ }
226
+ }
227
+ }
228
+ else if (entry.type === "user") {
229
+ userCount++;
230
+ const msg = entry.message;
231
+ if (!msg)
232
+ continue;
233
+ const content = msg.content;
234
+ if (!content)
235
+ continue;
236
+ const items = typeof content === "string"
237
+ ? [{ type: "text", text: content }]
238
+ : content;
239
+ for (const item of items) {
240
+ if (item.type === "tool_result") {
241
+ const { text: extracted, truncated } = extractToolResultContent(item.content, maxToolResultChars);
242
+ if (truncated) {
243
+ truncationCount++;
244
+ }
245
+ // Resolve tool type name from the lookup map built from assistant tool_use items.
246
+ // Fall back to the raw tool_use_id if not found (preserves backward compatibility).
247
+ const resolvedToolName = item.tool_use_id
248
+ ? (toolUseIdToName.get(item.tool_use_id) ?? item.tool_use_id)
249
+ : undefined;
250
+ messages.push({
251
+ id: makeId(`${sessionId ?? "unknown"}-${messages.length}`),
252
+ role: "tool",
253
+ content: extracted,
254
+ timestamp,
255
+ toolName: resolvedToolName
256
+ });
257
+ }
258
+ else if (item.type === "text" && item.text !== undefined) {
259
+ messages.push({
260
+ id: makeId(`${sessionId ?? "unknown"}-${messages.length}`),
261
+ role: "user",
262
+ content: item.text,
263
+ timestamp
264
+ });
265
+ }
266
+ }
267
+ }
268
+ }
269
+ return {
270
+ messages,
271
+ sessionId,
272
+ model,
273
+ aiTitle,
274
+ firstTimestamp,
275
+ lastTimestamp,
276
+ thinkingBlockCount,
277
+ truncationCount,
278
+ inputTokens,
279
+ outputTokens,
280
+ cacheCreationTokens,
281
+ cacheReadTokens,
282
+ assistantCount,
283
+ userCount,
284
+ otherCount,
285
+ agentId
286
+ };
287
+ }
288
+ async function readSubagentMeta(metaPath) {
289
+ try {
290
+ const raw = await readFile(metaPath, "utf8");
291
+ return JSON.parse(raw);
292
+ }
293
+ catch {
294
+ return null;
295
+ }
296
+ }
297
+ // ─── Discover subagent JSONL files ────────────────────────────────────────────
298
+ /**
299
+ * Discover subagent JSONL files from the subagents directory adjacent to the session file.
300
+ * The session file is expected at: <parent>/<session-id>.jsonl
301
+ * The subagents directory is at: <parent>/<session-id>/subagents/
302
+ */
303
+ function discoverSubagentJSONLFiles(sessionPath) {
304
+ const sessionFileName = basename(sessionPath);
305
+ const sessionId = sessionFileName.replace(/\.jsonl$/, "");
306
+ const sessionDir = dirname(sessionPath);
307
+ const subagentsDir = join(sessionDir, sessionId, "subagents");
308
+ if (!existsSync(subagentsDir)) {
309
+ return [];
310
+ }
311
+ let entries;
312
+ try {
313
+ entries = readdirSync(subagentsDir);
314
+ }
315
+ catch {
316
+ return [];
317
+ }
318
+ return entries
319
+ .filter((name) => name.endsWith(".jsonl"))
320
+ .map((name) => join(subagentsDir, name));
321
+ }
322
+ /**
323
+ * Scan a single Claude Code session JSONL file for discovery metadata.
324
+ *
325
+ * Reuses the existing parser exactly (no re-implementation): it reads the same
326
+ * entries `normalize()` reads, runs the same `processEntries` over the main
327
+ * session, and counts subagent files via the same `discoverSubagentJSONLFiles`.
328
+ * Reads only the given session file (and lists its subagents directory).
329
+ * Surfaces metadata only; never returns message content.
330
+ */
331
+ export async function scanSessionMetadata(sessionPath, projectSlug) {
332
+ const entries = await readJsonlEntries(sessionPath);
333
+ // Same parse the adapter uses for the main session (subagents excluded by default).
334
+ const main = processEntries(entries, MAX_TOOL_RESULT_CHARS, false, undefined);
335
+ // provider-reported-usage present iff any assistant entry carried a usage object.
336
+ let providerReportedUsagePresent = false;
337
+ let projectDir;
338
+ for (const entry of entries) {
339
+ if (!providerReportedUsagePresent && entry.type === "assistant" && entry.message?.usage) {
340
+ providerReportedUsagePresent = true;
341
+ }
342
+ if (projectDir === undefined && typeof entry.cwd === "string" && entry.cwd.length > 0) {
343
+ projectDir = entry.cwd;
344
+ }
345
+ }
346
+ const subagentCount = discoverSubagentJSONLFiles(sessionPath).length;
347
+ return {
348
+ sessionPath,
349
+ projectSlug,
350
+ projectDir: projectDir ?? decodeProjectSlug(projectSlug),
351
+ sessionId: main.sessionId,
352
+ firstTimestamp: main.firstTimestamp,
353
+ lastTimestamp: main.lastTimestamp,
354
+ messageCount: main.messages.length,
355
+ subagentCount,
356
+ providerReportedUsagePresent
357
+ };
358
+ }
359
+ /**
360
+ * Best-effort decode of a Claude Code project slug back to a directory path.
361
+ * Claude Code encodes the absolute working directory by replacing path
362
+ * separators with "-" (e.g. "-Users-me-Projects-app"). The decode is lossy
363
+ * (real "-" in path segments are indistinguishable from separators), so this is
364
+ * only a best-effort hint; the on-disk `cwd` from entries is preferred when
365
+ * available. Returns undefined when the slug does not look like an encoded path.
366
+ */
367
+ function decodeProjectSlug(slug) {
368
+ if (!slug.startsWith("-"))
369
+ return undefined;
370
+ return slug.replace(/-/g, "/");
371
+ }
372
+ // ─── ClaudeCodeAdapter ───────────────────────────────────────────────────────
373
+ export class ClaudeCodeAdapter {
374
+ id = "claude-code";
375
+ async normalize(input) {
376
+ const maxToolResultChars = typeof input.options?.["maxToolResultChars"] === "number"
377
+ ? input.options["maxToolResultChars"]
378
+ : MAX_TOOL_RESULT_CHARS;
379
+ const includeSubagents = input.options?.["includeSubagents"] === true;
380
+ // ── Process main session ──────────────────────────────────────────────────
381
+ const mainEntries = await readJsonlEntries(input.sourcePath);
382
+ const main = processEntries(mainEntries, maxToolResultChars, false, undefined);
383
+ const { sessionId, model, aiTitle, firstTimestamp: mainFirstTs, lastTimestamp: mainLastTs, thinkingBlockCount, truncationCount, assistantCount, userCount, otherCount } = main;
384
+ let inputTokens = main.inputTokens;
385
+ let outputTokens = main.outputTokens;
386
+ let cacheCreationTokens = main.cacheCreationTokens;
387
+ let cacheReadTokens = main.cacheReadTokens;
388
+ let allMessages = [...main.messages];
389
+ let firstTimestamp = mainFirstTs;
390
+ let lastTimestamp = mainLastTs;
391
+ const warnings = [PRIVACY_WARNING];
392
+ // Add thinking block warning if any were excluded
393
+ if (thinkingBlockCount > 0) {
394
+ warnings.push(`${thinkingBlockCount} thinking block(s) excluded — content is an opaque signed blob not suitable for trace analysis.`);
395
+ }
396
+ // Add truncation warning (updated after subagent processing below if needed)
397
+ // (handled after subagent loop)
398
+ // ── Process subagents ─────────────────────────────────────────────────────
399
+ const subagentProvenanceRecords = [];
400
+ let totalSubagentThinkingBlocks = thinkingBlockCount;
401
+ let totalTruncationCount = truncationCount;
402
+ if (includeSubagents) {
403
+ const subagentFiles = discoverSubagentJSONLFiles(input.sourcePath);
404
+ for (const subagentFile of subagentFiles) {
405
+ const subagentEntries = await readJsonlEntries(subagentFile);
406
+ // Derive agentId from the filename (e.g. a10161f797ef2c3ee.jsonl -> a10161f797ef2c3ee)
407
+ const subagentFileName = basename(subagentFile);
408
+ const fileAgentId = subagentFileName.replace(/\.jsonl$/, "");
409
+ const sub = processEntries(subagentEntries, maxToolResultChars, true, fileAgentId);
410
+ // Use agentId from entries if present; fall back to filename-derived id
411
+ const agentId = sub.agentId ?? fileAgentId;
412
+ // Accumulate totals
413
+ inputTokens += sub.inputTokens;
414
+ outputTokens += sub.outputTokens;
415
+ cacheCreationTokens += sub.cacheCreationTokens;
416
+ cacheReadTokens += sub.cacheReadTokens;
417
+ totalSubagentThinkingBlocks += sub.thinkingBlockCount;
418
+ totalTruncationCount += sub.truncationCount;
419
+ // Track timestamps across all sources
420
+ if (sub.firstTimestamp && (!firstTimestamp || sub.firstTimestamp < firstTimestamp)) {
421
+ firstTimestamp = sub.firstTimestamp;
422
+ }
423
+ if (sub.lastTimestamp && (!lastTimestamp || sub.lastTimestamp > lastTimestamp)) {
424
+ lastTimestamp = sub.lastTimestamp;
425
+ }
426
+ allMessages = allMessages.concat(sub.messages);
427
+ // Build provenance record
428
+ const subagentDir = dirname(subagentFile);
429
+ const metaPath = join(subagentDir, agentId, "meta.json");
430
+ const meta = await readSubagentMeta(metaPath);
431
+ const record = {
432
+ agentId,
433
+ entryCount: sub.assistantCount + sub.userCount
434
+ };
435
+ if (meta?.agentType !== undefined)
436
+ record.agentType = meta.agentType;
437
+ if (meta?.description !== undefined)
438
+ record.description = meta.description;
439
+ if (meta?.toolUseId !== undefined)
440
+ record.toolUseId = meta.toolUseId;
441
+ subagentProvenanceRecords.push(record);
442
+ }
443
+ // Sort all messages by timestamp ascending
444
+ allMessages.sort((a, b) => {
445
+ const ta = a.timestamp ?? "";
446
+ const tb = b.timestamp ?? "";
447
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
448
+ });
449
+ }
450
+ // Add thinking block warning (combined across main + subagents)
451
+ // Already added for main above; update if subagents added more
452
+ if (includeSubagents && totalSubagentThinkingBlocks > thinkingBlockCount) {
453
+ // Remove the main-only thinking block warning if it was added (update count)
454
+ const thinkingIdx = warnings.findIndex((w) => w.includes("thinking block(s) excluded"));
455
+ const totalThinking = totalSubagentThinkingBlocks;
456
+ if (thinkingIdx >= 0) {
457
+ warnings[thinkingIdx] = `${totalThinking} thinking block(s) excluded — content is an opaque signed blob not suitable for trace analysis.`;
458
+ }
459
+ else if (totalThinking > 0) {
460
+ warnings.push(`${totalThinking} thinking block(s) excluded — content is an opaque signed blob not suitable for trace analysis.`);
461
+ }
462
+ }
463
+ // Add truncation warning
464
+ if (totalTruncationCount > 0) {
465
+ warnings.push(`${totalTruncationCount} content item(s) truncated at ${maxToolResultChars} characters — full content in source session JSONL.`);
466
+ }
467
+ // Add subagent warning (conditional)
468
+ if (!includeSubagents) {
469
+ warnings.push(SUBAGENT_WARNING);
470
+ }
471
+ const now = new Date().toISOString();
472
+ const sessionPrefix = sessionId ? sessionId.slice(0, 8) : "unknown";
473
+ const title = aiTitle ?? `Claude Code session ${sessionPrefix}`;
474
+ const traceId = `claude-code-${sessionId ?? "unknown"}`;
475
+ const createdAt = firstTimestamp ?? now;
476
+ const durationMs = firstTimestamp && lastTimestamp
477
+ ? Math.max(0, new Date(lastTimestamp).getTime() - new Date(firstTimestamp).getTime())
478
+ : 0;
479
+ const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
480
+ const trace = {
481
+ id: traceId,
482
+ title,
483
+ artifactVersion: CURRENT_AGENT_TRACE_ARTIFACT_VERSION,
484
+ source: "real_captured",
485
+ createdAt,
486
+ generatedAt: now,
487
+ model,
488
+ durationMs,
489
+ messages: allMessages
490
+ };
491
+ const usage = createUsageMetadata({
492
+ inputTokens,
493
+ outputTokens,
494
+ totalTokens,
495
+ cacheReadInputTokens: cacheReadTokens,
496
+ cacheCreationInputTokens: cacheCreationTokens,
497
+ providerReportedTokens: true,
498
+ estimatedTokens: false,
499
+ model,
500
+ provider: "anthropic",
501
+ limitations: [
502
+ "Token counts are from the Claude Code session JSONL usage fields (actual API response metadata).",
503
+ "Cost is estimated from the Anthropic price table, not from billing records.",
504
+ "Cache read tokens priced at ~10% of standard input rate (estimated). Cache creation tokens priced at ~125% of standard input rate (estimated). Not billing-confirmed."
505
+ ]
506
+ });
507
+ const limitations = [
508
+ `Assistant entries: ${assistantCount}`,
509
+ `User entries: ${userCount}`,
510
+ `Other/excluded entries: ${otherCount}`,
511
+ `Thinking blocks excluded: ${thinkingBlockCount}`,
512
+ `Cache creation tokens (stored in provenance, not in cost estimate): ${cacheCreationTokens}`,
513
+ `Cache read tokens (stored in provenance, not in cost estimate): ${cacheReadTokens}`
514
+ ];
515
+ if (includeSubagents) {
516
+ limitations.push(`Subagents included: ${subagentProvenanceRecords.length} subagent JSONL file(s) processed.`);
517
+ }
518
+ // Per-run distinctness fingerprint: a one-way digest over the canonical normalized
519
+ // trace content (NOT raw content). It is computed from `trace` after capture-time
520
+ // fields (capturedAt/generatedAt/sourcePath/durationMs) are set, but those varying
521
+ // fields are intentionally NOT part of the digest input (see trace-fingerprint.ts),
522
+ // so re-capturing the same session yields the same fingerprint. Surfacing it lets
523
+ // distinct real sessions be told apart from re-captures without ever counting a
524
+ // re-capture as new.
525
+ const traceFingerprint = computeTraceFingerprint(trace);
526
+ const provenance = {
527
+ captureAdapter: this.id,
528
+ sourcePath: input.sourcePath,
529
+ capturedAt: now,
530
+ sessionId,
531
+ warnings,
532
+ limitations,
533
+ traceFingerprint,
534
+ ...(includeSubagents ? { subagents: subagentProvenanceRecords } : {})
535
+ };
536
+ return { trace, usage, provenance };
537
+ }
538
+ }
539
+ //# sourceMappingURL=claude-code-adapter.js.map
@@ -0,0 +1,40 @@
1
+ import type { ClaudeCodeSessionMetadata } from "./claude-code-adapter.js";
2
+ /**
3
+ * Local, read-only discovery of available Claude Code sessions.
4
+ *
5
+ * Scans the standard Claude Code projects layout
6
+ * (`~/.claude/projects/<slug>/<session-id>.jsonl`) and returns metadata-only
7
+ * records for each discoverable session, reusing the existing adapter parser
8
+ * (`scanSessionMetadata`) so counts match what `capture claude-code --session`
9
+ * would produce. This module performs NO network calls, NO upload, and NO
10
+ * provider-API access — it only reads the local filesystem under the projects
11
+ * root (default `~/.claude/projects`, overridable for testability).
12
+ *
13
+ * Privacy: discovery surfaces metadata ONLY. It never reads or returns session
14
+ * message content into the listing. Session files themselves may contain local
15
+ * file content (consistent with the capture privacy warning).
16
+ */
17
+ export declare const DISCOVERY_PRIVACY_NOTE: string;
18
+ /** Resolve the default Claude Code projects root (~/.claude/projects). */
19
+ export declare function defaultProjectsDir(): string;
20
+ export interface DiscoverSessionsOptions {
21
+ /** Override the projects root directory (default: ~/.claude/projects). Read-only. */
22
+ projectsDir?: string;
23
+ }
24
+ export interface DiscoverSessionsResult {
25
+ /** The projects root that was scanned. */
26
+ projectsDir: string;
27
+ /** Whether the projects root exists. */
28
+ projectsDirExists: boolean;
29
+ /** Discovered sessions, sorted newest-first by lastTimestamp (then path). */
30
+ sessions: ClaudeCodeSessionMetadata[];
31
+ }
32
+ /**
33
+ * Discover all Claude Code sessions under the projects root.
34
+ *
35
+ * Layout scanned (read-only): <projectsDir>/<slug>/<session-id>.jsonl
36
+ * Each `<slug>` directory is one project; each top-level `.jsonl` within it is
37
+ * one session. (The `<session-id>/subagents/` sub-directory is NOT treated as a
38
+ * session; its file count is reported as the session's subagent count.)
39
+ */
40
+ export declare function discoverClaudeCodeSessions(options?: DiscoverSessionsOptions): Promise<DiscoverSessionsResult>;
@@ -0,0 +1,100 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { scanSessionMetadata } from "./claude-code-adapter.js";
5
+ /**
6
+ * Local, read-only discovery of available Claude Code sessions.
7
+ *
8
+ * Scans the standard Claude Code projects layout
9
+ * (`~/.claude/projects/<slug>/<session-id>.jsonl`) and returns metadata-only
10
+ * records for each discoverable session, reusing the existing adapter parser
11
+ * (`scanSessionMetadata`) so counts match what `capture claude-code --session`
12
+ * would produce. This module performs NO network calls, NO upload, and NO
13
+ * provider-API access — it only reads the local filesystem under the projects
14
+ * root (default `~/.claude/projects`, overridable for testability).
15
+ *
16
+ * Privacy: discovery surfaces metadata ONLY. It never reads or returns session
17
+ * message content into the listing. Session files themselves may contain local
18
+ * file content (consistent with the capture privacy warning).
19
+ */
20
+ export const DISCOVERY_PRIVACY_NOTE = "Discovery is local and read-only: it scans your local Claude Code projects directory only " +
21
+ "(no network calls, no upload). It surfaces session metadata only — never session content. " +
22
+ "Note: the underlying session files may contain local file content; review captured artifacts " +
23
+ "before sharing.";
24
+ /** Resolve the default Claude Code projects root (~/.claude/projects). */
25
+ export function defaultProjectsDir() {
26
+ return join(homedir(), ".claude", "projects");
27
+ }
28
+ function isJsonlFile(name) {
29
+ return name.endsWith(".jsonl");
30
+ }
31
+ /**
32
+ * Discover all Claude Code sessions under the projects root.
33
+ *
34
+ * Layout scanned (read-only): <projectsDir>/<slug>/<session-id>.jsonl
35
+ * Each `<slug>` directory is one project; each top-level `.jsonl` within it is
36
+ * one session. (The `<session-id>/subagents/` sub-directory is NOT treated as a
37
+ * session; its file count is reported as the session's subagent count.)
38
+ */
39
+ export async function discoverClaudeCodeSessions(options = {}) {
40
+ const projectsDir = options.projectsDir ?? defaultProjectsDir();
41
+ if (!existsSync(projectsDir)) {
42
+ return { projectsDir, projectsDirExists: false, sessions: [] };
43
+ }
44
+ let slugDirs;
45
+ try {
46
+ slugDirs = readdirSync(projectsDir).filter((name) => {
47
+ const full = join(projectsDir, name);
48
+ try {
49
+ return statSync(full).isDirectory();
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ });
55
+ }
56
+ catch {
57
+ return { projectsDir, projectsDirExists: true, sessions: [] };
58
+ }
59
+ const sessions = [];
60
+ for (const slug of slugDirs) {
61
+ const slugPath = join(projectsDir, slug);
62
+ let sessionFiles;
63
+ try {
64
+ sessionFiles = readdirSync(slugPath).filter((name) => {
65
+ if (!isJsonlFile(name))
66
+ return false;
67
+ const full = join(slugPath, name);
68
+ try {
69
+ return statSync(full).isFile();
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ });
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ for (const fileName of sessionFiles) {
80
+ const sessionPath = join(slugPath, fileName);
81
+ try {
82
+ const metadata = await scanSessionMetadata(sessionPath, slug);
83
+ sessions.push(metadata);
84
+ }
85
+ catch {
86
+ // Skip unreadable / malformed session files; discovery stays resilient.
87
+ }
88
+ }
89
+ }
90
+ // Sort newest-first by lastTimestamp, falling back to path for determinism.
91
+ sessions.sort((a, b) => {
92
+ const ta = a.lastTimestamp ?? "";
93
+ const tb = b.lastTimestamp ?? "";
94
+ if (ta !== tb)
95
+ return ta < tb ? 1 : -1;
96
+ return a.sessionPath < b.sessionPath ? -1 : a.sessionPath > b.sessionPath ? 1 : 0;
97
+ });
98
+ return { projectsDir, projectsDirExists: true, sessions };
99
+ }
100
+ //# sourceMappingURL=claude-code-discovery.js.map