@herdctl/core 5.5.0 → 5.7.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 (172) hide show
  1. package/dist/config/__tests__/merge.test.js +1 -1
  2. package/dist/config/__tests__/merge.test.js.map +1 -1
  3. package/dist/config/index.d.ts +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +3 -1
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/config/schema.d.ts +10 -2
  8. package/dist/config/schema.d.ts.map +1 -1
  9. package/dist/config/schema.js +6 -2
  10. package/dist/config/schema.js.map +1 -1
  11. package/dist/distribution/__tests__/agent-discovery.test.d.ts +7 -0
  12. package/dist/distribution/__tests__/agent-discovery.test.d.ts.map +1 -0
  13. package/dist/distribution/__tests__/agent-discovery.test.js +443 -0
  14. package/dist/distribution/__tests__/agent-discovery.test.js.map +1 -0
  15. package/dist/distribution/__tests__/agent-info.test.d.ts +7 -0
  16. package/dist/distribution/__tests__/agent-info.test.d.ts.map +1 -0
  17. package/dist/distribution/__tests__/agent-info.test.js +568 -0
  18. package/dist/distribution/__tests__/agent-info.test.js.map +1 -0
  19. package/dist/distribution/__tests__/agent-remover.test.d.ts +7 -0
  20. package/dist/distribution/__tests__/agent-remover.test.d.ts.map +1 -0
  21. package/dist/distribution/__tests__/agent-remover.test.js +498 -0
  22. package/dist/distribution/__tests__/agent-remover.test.js.map +1 -0
  23. package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts +5 -0
  24. package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts.map +1 -0
  25. package/dist/distribution/__tests__/agent-repo-metadata.test.js +500 -0
  26. package/dist/distribution/__tests__/agent-repo-metadata.test.js.map +1 -0
  27. package/dist/distribution/__tests__/env-scanner.test.d.ts +5 -0
  28. package/dist/distribution/__tests__/env-scanner.test.d.ts.map +1 -0
  29. package/dist/distribution/__tests__/env-scanner.test.js +576 -0
  30. package/dist/distribution/__tests__/env-scanner.test.js.map +1 -0
  31. package/dist/distribution/__tests__/file-installer.test.d.ts +7 -0
  32. package/dist/distribution/__tests__/file-installer.test.d.ts.map +1 -0
  33. package/dist/distribution/__tests__/file-installer.test.js +714 -0
  34. package/dist/distribution/__tests__/file-installer.test.js.map +1 -0
  35. package/dist/distribution/__tests__/fleet-config-updater.test.d.ts +7 -0
  36. package/dist/distribution/__tests__/fleet-config-updater.test.d.ts.map +1 -0
  37. package/dist/distribution/__tests__/fleet-config-updater.test.js +531 -0
  38. package/dist/distribution/__tests__/fleet-config-updater.test.js.map +1 -0
  39. package/dist/distribution/__tests__/installation-metadata.test.d.ts +2 -0
  40. package/dist/distribution/__tests__/installation-metadata.test.d.ts.map +1 -0
  41. package/dist/distribution/__tests__/installation-metadata.test.js +292 -0
  42. package/dist/distribution/__tests__/installation-metadata.test.js.map +1 -0
  43. package/dist/distribution/__tests__/integration.test.d.ts +10 -0
  44. package/dist/distribution/__tests__/integration.test.d.ts.map +1 -0
  45. package/dist/distribution/__tests__/integration.test.js +522 -0
  46. package/dist/distribution/__tests__/integration.test.js.map +1 -0
  47. package/dist/distribution/__tests__/repository-fetcher.test.d.ts +5 -0
  48. package/dist/distribution/__tests__/repository-fetcher.test.d.ts.map +1 -0
  49. package/dist/distribution/__tests__/repository-fetcher.test.js +386 -0
  50. package/dist/distribution/__tests__/repository-fetcher.test.js.map +1 -0
  51. package/dist/distribution/__tests__/repository-validator.test.d.ts +7 -0
  52. package/dist/distribution/__tests__/repository-validator.test.d.ts.map +1 -0
  53. package/dist/distribution/__tests__/repository-validator.test.js +447 -0
  54. package/dist/distribution/__tests__/repository-validator.test.js.map +1 -0
  55. package/dist/distribution/__tests__/source-specifier.test.d.ts +5 -0
  56. package/dist/distribution/__tests__/source-specifier.test.d.ts.map +1 -0
  57. package/dist/distribution/__tests__/source-specifier.test.js +533 -0
  58. package/dist/distribution/__tests__/source-specifier.test.js.map +1 -0
  59. package/dist/distribution/agent-discovery.d.ts +81 -0
  60. package/dist/distribution/agent-discovery.d.ts.map +1 -0
  61. package/dist/distribution/agent-discovery.js +264 -0
  62. package/dist/distribution/agent-discovery.js.map +1 -0
  63. package/dist/distribution/agent-info.d.ts +86 -0
  64. package/dist/distribution/agent-info.d.ts.map +1 -0
  65. package/dist/distribution/agent-info.js +225 -0
  66. package/dist/distribution/agent-info.js.map +1 -0
  67. package/dist/distribution/agent-remover.d.ts +83 -0
  68. package/dist/distribution/agent-remover.d.ts.map +1 -0
  69. package/dist/distribution/agent-remover.js +222 -0
  70. package/dist/distribution/agent-remover.js.map +1 -0
  71. package/dist/distribution/agent-repo-metadata.d.ts +181 -0
  72. package/dist/distribution/agent-repo-metadata.d.ts.map +1 -0
  73. package/dist/distribution/agent-repo-metadata.js +143 -0
  74. package/dist/distribution/agent-repo-metadata.js.map +1 -0
  75. package/dist/distribution/env-scanner.d.ts +78 -0
  76. package/dist/distribution/env-scanner.d.ts.map +1 -0
  77. package/dist/distribution/env-scanner.js +144 -0
  78. package/dist/distribution/env-scanner.js.map +1 -0
  79. package/dist/distribution/file-installer.d.ts +80 -0
  80. package/dist/distribution/file-installer.d.ts.map +1 -0
  81. package/dist/distribution/file-installer.js +268 -0
  82. package/dist/distribution/file-installer.js.map +1 -0
  83. package/dist/distribution/fleet-config-updater.d.ts +96 -0
  84. package/dist/distribution/fleet-config-updater.d.ts.map +1 -0
  85. package/dist/distribution/fleet-config-updater.js +266 -0
  86. package/dist/distribution/fleet-config-updater.js.map +1 -0
  87. package/dist/distribution/index.d.ts +23 -0
  88. package/dist/distribution/index.d.ts.map +1 -0
  89. package/dist/distribution/index.js +42 -0
  90. package/dist/distribution/index.js.map +1 -0
  91. package/dist/distribution/installation-metadata.d.ts +191 -0
  92. package/dist/distribution/installation-metadata.d.ts.map +1 -0
  93. package/dist/distribution/installation-metadata.js +100 -0
  94. package/dist/distribution/installation-metadata.js.map +1 -0
  95. package/dist/distribution/repository-fetcher.d.ts +104 -0
  96. package/dist/distribution/repository-fetcher.d.ts.map +1 -0
  97. package/dist/distribution/repository-fetcher.js +246 -0
  98. package/dist/distribution/repository-fetcher.js.map +1 -0
  99. package/dist/distribution/repository-validator.d.ts +86 -0
  100. package/dist/distribution/repository-validator.d.ts.map +1 -0
  101. package/dist/distribution/repository-validator.js +296 -0
  102. package/dist/distribution/repository-validator.js.map +1 -0
  103. package/dist/distribution/source-specifier.d.ts +106 -0
  104. package/dist/distribution/source-specifier.d.ts.map +1 -0
  105. package/dist/distribution/source-specifier.js +247 -0
  106. package/dist/distribution/source-specifier.js.map +1 -0
  107. package/dist/fleet-manager/errors.d.ts +15 -0
  108. package/dist/fleet-manager/errors.d.ts.map +1 -1
  109. package/dist/fleet-manager/errors.js +16 -0
  110. package/dist/fleet-manager/errors.js.map +1 -1
  111. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  112. package/dist/fleet-manager/fleet-manager.js +31 -9
  113. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  114. package/dist/index.d.ts +1 -0
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +2 -0
  117. package/dist/index.js.map +1 -1
  118. package/dist/runner/message-processor.d.ts.map +1 -1
  119. package/dist/runner/message-processor.js +7 -2
  120. package/dist/runner/message-processor.js.map +1 -1
  121. package/dist/runner/runtime/container-manager.js +1 -1
  122. package/dist/runner/runtime/container-manager.js.map +1 -1
  123. package/dist/scheduler/errors.d.ts +15 -0
  124. package/dist/scheduler/errors.d.ts.map +1 -1
  125. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  126. package/dist/scheduler/schedule-runner.js +6 -5
  127. package/dist/scheduler/schedule-runner.js.map +1 -1
  128. package/dist/state/__tests__/jsonl-parser.test.d.ts +5 -0
  129. package/dist/state/__tests__/jsonl-parser.test.d.ts.map +1 -0
  130. package/dist/state/__tests__/jsonl-parser.test.js +307 -0
  131. package/dist/state/__tests__/jsonl-parser.test.js.map +1 -0
  132. package/dist/state/__tests__/session-attribution.test.d.ts +2 -0
  133. package/dist/state/__tests__/session-attribution.test.d.ts.map +1 -0
  134. package/dist/state/__tests__/session-attribution.test.js +567 -0
  135. package/dist/state/__tests__/session-attribution.test.js.map +1 -0
  136. package/dist/state/__tests__/session-discovery.test.d.ts +2 -0
  137. package/dist/state/__tests__/session-discovery.test.d.ts.map +1 -0
  138. package/dist/state/__tests__/session-discovery.test.js +941 -0
  139. package/dist/state/__tests__/session-discovery.test.js.map +1 -0
  140. package/dist/state/__tests__/session-metadata.test.d.ts +2 -0
  141. package/dist/state/__tests__/session-metadata.test.d.ts.map +1 -0
  142. package/dist/state/__tests__/session-metadata.test.js +422 -0
  143. package/dist/state/__tests__/session-metadata.test.js.map +1 -0
  144. package/dist/state/__tests__/tool-parsing.test.d.ts +5 -0
  145. package/dist/state/__tests__/tool-parsing.test.d.ts.map +1 -0
  146. package/dist/state/__tests__/tool-parsing.test.js +315 -0
  147. package/dist/state/__tests__/tool-parsing.test.js.map +1 -0
  148. package/dist/state/index.d.ts +5 -0
  149. package/dist/state/index.d.ts.map +1 -1
  150. package/dist/state/index.js +10 -0
  151. package/dist/state/index.js.map +1 -1
  152. package/dist/state/jsonl-parser.d.ts +115 -0
  153. package/dist/state/jsonl-parser.d.ts.map +1 -0
  154. package/dist/state/jsonl-parser.js +437 -0
  155. package/dist/state/jsonl-parser.js.map +1 -0
  156. package/dist/state/session-attribution.d.ts +35 -0
  157. package/dist/state/session-attribution.d.ts.map +1 -0
  158. package/dist/state/session-attribution.js +179 -0
  159. package/dist/state/session-attribution.js.map +1 -0
  160. package/dist/state/session-discovery.d.ts +188 -0
  161. package/dist/state/session-discovery.d.ts.map +1 -0
  162. package/dist/state/session-discovery.js +513 -0
  163. package/dist/state/session-discovery.js.map +1 -0
  164. package/dist/state/session-metadata.d.ts +186 -0
  165. package/dist/state/session-metadata.d.ts.map +1 -0
  166. package/dist/state/session-metadata.js +297 -0
  167. package/dist/state/session-metadata.js.map +1 -0
  168. package/dist/state/tool-parsing.d.ts +88 -0
  169. package/dist/state/tool-parsing.d.ts.map +1 -0
  170. package/dist/state/tool-parsing.js +199 -0
  171. package/dist/state/tool-parsing.js.map +1 -0
  172. package/package.json +1 -1
@@ -0,0 +1,437 @@
1
+ /**
2
+ * JSONL session file parser
3
+ *
4
+ * Parses Claude Code `.jsonl` session files into structured ChatMessage arrays
5
+ * for the web frontend. Supports streaming parsing for memory efficiency,
6
+ * message deduplication, tool call/result pairing, and metadata extraction.
7
+ */
8
+ import { createReadStream } from "node:fs";
9
+ import { createInterface } from "node:readline";
10
+ import { extractToolResults, extractToolUseBlocks, getToolInputSummary, } from "./tool-parsing.js";
11
+ // =============================================================================
12
+ // Helper Functions
13
+ // =============================================================================
14
+ /**
15
+ * Extract text content from a message's content field.
16
+ *
17
+ * Content can be a plain string or an array of content blocks.
18
+ * For arrays, text blocks are filtered and joined with newlines.
19
+ */
20
+ function extractTextContent(content) {
21
+ if (typeof content === "string") {
22
+ return content;
23
+ }
24
+ if (Array.isArray(content)) {
25
+ const textParts = [];
26
+ for (const block of content) {
27
+ if (block &&
28
+ typeof block === "object" &&
29
+ "type" in block &&
30
+ block.type === "text" &&
31
+ "text" in block &&
32
+ typeof block.text === "string") {
33
+ textParts.push(block.text);
34
+ }
35
+ }
36
+ return textParts.join("\n");
37
+ }
38
+ return "";
39
+ }
40
+ /**
41
+ * Check whether a message's content contains tool_result blocks
42
+ */
43
+ function hasToolResultBlocks(content) {
44
+ if (!Array.isArray(content))
45
+ return false;
46
+ return content.some((block) => block && typeof block === "object" && "type" in block && block.type === "tool_result");
47
+ }
48
+ /**
49
+ * Create a readline interface that streams a JSONL file line by line.
50
+ *
51
+ * Returns null if the file cannot be opened (e.g., ENOENT).
52
+ */
53
+ function createLineReader(filePath) {
54
+ return new Promise((resolve) => {
55
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
56
+ stream.on("error", () => {
57
+ resolve(null);
58
+ });
59
+ stream.on("open", () => {
60
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
61
+ resolve(rl);
62
+ });
63
+ });
64
+ }
65
+ // =============================================================================
66
+ // parseSessionMessages
67
+ // =============================================================================
68
+ /**
69
+ * Parse a JSONL session file into an array of ChatMessages.
70
+ *
71
+ * Streams the file line by line for memory efficiency. Handles:
72
+ * - Plain user text messages
73
+ * - Assistant text messages (deduplicated by message.id)
74
+ * - Tool use blocks from assistant messages (stored as pending)
75
+ * - Tool result blocks from user messages (paired with pending tool uses)
76
+ *
77
+ * @param sessionFilePath - Absolute path to the .jsonl file
78
+ * @param options - Optional settings (limit caps total messages returned)
79
+ * @returns Array of ChatMessages in chronological order
80
+ */
81
+ export async function parseSessionMessages(sessionFilePath, options) {
82
+ const rl = await createLineReader(sessionFilePath);
83
+ if (!rl)
84
+ return [];
85
+ const messages = [];
86
+ const seenAssistantIds = new Set();
87
+ const pendingToolUses = new Map();
88
+ const limit = options?.limit;
89
+ for await (const line of rl) {
90
+ // Respect message limit
91
+ if (limit !== undefined && messages.length >= limit)
92
+ break;
93
+ const trimmed = line.trim();
94
+ if (trimmed.length === 0)
95
+ continue;
96
+ let parsed;
97
+ try {
98
+ parsed = JSON.parse(trimmed);
99
+ }
100
+ catch {
101
+ continue; // Skip malformed lines
102
+ }
103
+ const type = parsed.type;
104
+ if (type !== "user" && type !== "assistant")
105
+ continue;
106
+ const message = parsed.message;
107
+ if (!message)
108
+ continue;
109
+ const timestamp = typeof parsed.timestamp === "string" ? parsed.timestamp : new Date().toISOString();
110
+ // ── User messages ──────────────────────────────────────────────────
111
+ if (type === "user") {
112
+ const content = message.content;
113
+ // Tool result message
114
+ if (hasToolResultBlocks(content)) {
115
+ const toolResults = extractToolResults(parsed);
116
+ for (const result of toolResults) {
117
+ if (limit !== undefined && messages.length >= limit)
118
+ break;
119
+ const pending = result.toolUseId ? pendingToolUses.get(result.toolUseId) : undefined;
120
+ const toolName = pending?.name ?? "unknown";
121
+ const inputSummary = pending
122
+ ? getToolInputSummary(pending.name, pending.input)
123
+ : undefined;
124
+ // Calculate duration if we have timestamps
125
+ let durationMs;
126
+ if (pending) {
127
+ const startMs = new Date(pending.timestamp).getTime();
128
+ const endMs = new Date(timestamp).getTime();
129
+ if (!Number.isNaN(startMs) && !Number.isNaN(endMs) && endMs >= startMs) {
130
+ durationMs = endMs - startMs;
131
+ }
132
+ }
133
+ messages.push({
134
+ role: "tool",
135
+ content: result.output,
136
+ timestamp,
137
+ toolCall: {
138
+ toolName,
139
+ inputSummary,
140
+ output: result.output,
141
+ isError: result.isError,
142
+ durationMs,
143
+ },
144
+ });
145
+ // Clean up matched pending tool use
146
+ if (result.toolUseId) {
147
+ pendingToolUses.delete(result.toolUseId);
148
+ }
149
+ }
150
+ continue;
151
+ }
152
+ // Plain text user message
153
+ const text = extractTextContent(content);
154
+ if (text.length > 0) {
155
+ messages.push({ role: "user", content: text, timestamp });
156
+ }
157
+ continue;
158
+ }
159
+ // ── Assistant messages ──────────────────────────────────────────────
160
+ if (type === "assistant") {
161
+ const messageId = typeof message.id === "string" ? message.id : undefined;
162
+ // Deduplicate by message ID
163
+ if (messageId) {
164
+ if (seenAssistantIds.has(messageId))
165
+ continue;
166
+ seenAssistantIds.add(messageId);
167
+ }
168
+ const content = message.content;
169
+ // Simple string content
170
+ if (typeof content === "string") {
171
+ if (content.length > 0) {
172
+ messages.push({ role: "assistant", content, timestamp });
173
+ }
174
+ continue;
175
+ }
176
+ // Array of content blocks
177
+ if (Array.isArray(content)) {
178
+ // Extract text from text blocks
179
+ const text = extractTextContent(content);
180
+ // Extract tool_use blocks and store as pending
181
+ const toolUseBlocks = extractToolUseBlocks(parsed);
182
+ for (const block of toolUseBlocks) {
183
+ if (block.id) {
184
+ pendingToolUses.set(block.id, {
185
+ name: block.name,
186
+ input: block.input,
187
+ timestamp,
188
+ });
189
+ }
190
+ }
191
+ // Create assistant message for text content
192
+ if (text.length > 0 && (limit === undefined || messages.length < limit)) {
193
+ messages.push({ role: "assistant", content: text, timestamp });
194
+ }
195
+ }
196
+ }
197
+ }
198
+ return messages;
199
+ }
200
+ // =============================================================================
201
+ // extractSessionMetadata
202
+ // =============================================================================
203
+ /**
204
+ * Extract summary metadata from a JSONL session file.
205
+ *
206
+ * Streams the entire file to count messages and find timestamp bounds,
207
+ * but captures metadata fields from only the first relevant messages.
208
+ *
209
+ * @param sessionFilePath - Absolute path to the .jsonl file
210
+ * @returns Session metadata with counts and previews
211
+ */
212
+ export async function extractSessionMetadata(sessionFilePath) {
213
+ const rl = await createLineReader(sessionFilePath);
214
+ const metadata = {
215
+ sessionId: "",
216
+ firstMessagePreview: undefined,
217
+ gitBranch: undefined,
218
+ claudeCodeVersion: undefined,
219
+ messageCount: 0,
220
+ firstMessageAt: undefined,
221
+ lastMessageAt: undefined,
222
+ summary: undefined,
223
+ isSidechain: false,
224
+ };
225
+ if (!rl)
226
+ return metadata;
227
+ const seenAssistantIds = new Set();
228
+ let foundFirstUser = false;
229
+ for await (const line of rl) {
230
+ const trimmed = line.trim();
231
+ if (trimmed.length === 0)
232
+ continue;
233
+ let parsed;
234
+ try {
235
+ parsed = JSON.parse(trimmed);
236
+ }
237
+ catch {
238
+ continue;
239
+ }
240
+ const type = parsed.type;
241
+ // Track summary entries (type: "summary" with top-level summary field)
242
+ if (type === "summary" && typeof parsed.summary === "string") {
243
+ metadata.summary = parsed.summary;
244
+ continue;
245
+ }
246
+ if (type !== "user" && type !== "assistant")
247
+ continue;
248
+ const timestamp = typeof parsed.timestamp === "string" ? parsed.timestamp : undefined;
249
+ // Track sessionId from first line that has it
250
+ if (metadata.sessionId === "" && typeof parsed.sessionId === "string") {
251
+ metadata.sessionId = parsed.sessionId;
252
+ }
253
+ // Track timestamp bounds
254
+ if (timestamp) {
255
+ if (metadata.firstMessageAt === undefined) {
256
+ metadata.firstMessageAt = timestamp;
257
+ }
258
+ metadata.lastMessageAt = timestamp;
259
+ }
260
+ if (type === "user") {
261
+ // Extract first-user-message-specific fields
262
+ if (!foundFirstUser) {
263
+ foundFirstUser = true;
264
+ if (parsed.isSidechain === true) {
265
+ metadata.isSidechain = true;
266
+ }
267
+ if (typeof parsed.gitBranch === "string") {
268
+ metadata.gitBranch = parsed.gitBranch;
269
+ }
270
+ if (typeof parsed.version === "string") {
271
+ metadata.claudeCodeVersion = parsed.version;
272
+ }
273
+ const message = parsed.message;
274
+ if (message) {
275
+ const content = message.content;
276
+ // Only extract preview from plain text messages, not tool results
277
+ if (!hasToolResultBlocks(content)) {
278
+ const text = extractTextContent(content);
279
+ if (text.length > 0) {
280
+ metadata.firstMessagePreview =
281
+ text.length > 100 ? `${text.substring(0, 100)}...` : text;
282
+ }
283
+ }
284
+ }
285
+ }
286
+ metadata.messageCount++;
287
+ continue;
288
+ }
289
+ if (type === "assistant") {
290
+ const message = parsed.message;
291
+ const messageId = message && typeof message.id === "string" ? message.id : undefined;
292
+ // Deduplicate assistant messages by ID
293
+ if (messageId) {
294
+ if (seenAssistantIds.has(messageId))
295
+ continue;
296
+ seenAssistantIds.add(messageId);
297
+ }
298
+ metadata.messageCount++;
299
+ }
300
+ }
301
+ return metadata;
302
+ }
303
+ // =============================================================================
304
+ // extractSessionUsage
305
+ // =============================================================================
306
+ /**
307
+ * Extract token usage data from a JSONL session file.
308
+ *
309
+ * Streams the file and tracks the last seen inputTokens value from
310
+ * assistant messages. The most recent value represents the current
311
+ * context window fill level (not cumulative across turns).
312
+ *
313
+ * @param sessionFilePath - Absolute path to the .jsonl file
314
+ * @returns Usage summary with input tokens, turn count, and data availability flag
315
+ */
316
+ export async function extractSessionUsage(sessionFilePath) {
317
+ const rl = await createLineReader(sessionFilePath);
318
+ if (!rl) {
319
+ return { inputTokens: 0, turnCount: 0, hasData: false };
320
+ }
321
+ const seenIds = new Set();
322
+ let lastInputTokens = 0;
323
+ let hasData = false;
324
+ for await (const line of rl) {
325
+ const trimmed = line.trim();
326
+ if (trimmed.length === 0)
327
+ continue;
328
+ let parsed;
329
+ try {
330
+ parsed = JSON.parse(trimmed);
331
+ }
332
+ catch {
333
+ continue;
334
+ }
335
+ if (parsed.type !== "assistant")
336
+ continue;
337
+ const message = parsed.message;
338
+ if (!message)
339
+ continue;
340
+ // Deduplicate by message ID
341
+ const messageId = typeof message.id === "string" ? message.id : undefined;
342
+ if (messageId) {
343
+ if (seenIds.has(messageId))
344
+ continue;
345
+ seenIds.add(messageId);
346
+ }
347
+ // Extract usage
348
+ const usage = message.usage;
349
+ if (!usage)
350
+ continue;
351
+ hasData = true;
352
+ const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
353
+ const cacheCreation = typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : 0;
354
+ const cacheRead = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : 0;
355
+ lastInputTokens = inputTokens + cacheCreation + cacheRead;
356
+ }
357
+ return {
358
+ inputTokens: lastInputTokens,
359
+ turnCount: seenIds.size,
360
+ hasData,
361
+ };
362
+ }
363
+ // =============================================================================
364
+ // isSidechainSession
365
+ // =============================================================================
366
+ /**
367
+ * Check if a session file represents a sidechain (sub-agent) session.
368
+ *
369
+ * Claude Code sets `isSidechain: true` on the first JSONL entry when:
370
+ * - The session is a Task tool sub-agent (most common — prompt-cache warmups)
371
+ * - The `--resume` flag was used to start the session
372
+ *
373
+ * These sessions are typically noise (a single "Warmup" message + response)
374
+ * and are filtered out of UI-facing session discovery to avoid clutter.
375
+ *
376
+ * Reads only the first line of the JSONL file for efficiency — O(1) per file.
377
+ *
378
+ * @param sessionFilePath - Absolute path to the .jsonl file
379
+ * @returns true if the session is a sidechain session
380
+ */
381
+ export async function isSidechainSession(sessionFilePath) {
382
+ const rl = await createLineReader(sessionFilePath);
383
+ if (!rl)
384
+ return false;
385
+ for await (const line of rl) {
386
+ const trimmed = line.trim();
387
+ if (trimmed.length === 0)
388
+ continue;
389
+ try {
390
+ const parsed = JSON.parse(trimmed);
391
+ rl.close();
392
+ return parsed.isSidechain === true;
393
+ }
394
+ catch {
395
+ rl.close();
396
+ return false;
397
+ }
398
+ }
399
+ return false;
400
+ }
401
+ // =============================================================================
402
+ // extractLastSummary
403
+ // =============================================================================
404
+ /**
405
+ * Extract only the last summary from a JSONL session file.
406
+ *
407
+ * This is a lightweight alternative to extractSessionMetadata when only the
408
+ * auto-generated session name is needed. It streams the file and returns the
409
+ * last `summary` value from entries with `type: "summary"`.
410
+ *
411
+ * @param sessionFilePath - Absolute path to the .jsonl file
412
+ * @returns The last summary string, or undefined if none found
413
+ */
414
+ export async function extractLastSummary(sessionFilePath) {
415
+ const rl = await createLineReader(sessionFilePath);
416
+ if (!rl)
417
+ return undefined;
418
+ let lastSummary;
419
+ for await (const line of rl) {
420
+ const trimmed = line.trim();
421
+ if (trimmed.length === 0)
422
+ continue;
423
+ let parsed;
424
+ try {
425
+ parsed = JSON.parse(trimmed);
426
+ }
427
+ catch {
428
+ continue;
429
+ }
430
+ // Only process summary entries
431
+ if (parsed.type === "summary" && typeof parsed.summary === "string") {
432
+ lastSummary = parsed.summary;
433
+ }
434
+ }
435
+ return lastSummary;
436
+ }
437
+ //# sourceMappingURL=jsonl-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-parser.js","sourceRoot":"","sources":["../../src/state/jsonl-parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Session attribution module
3
+ *
4
+ * Determines the origin of a Claude Code session (web, discord, slack, schedule, or native CLI)
5
+ * by cross-referencing HerdCTL's job metadata and platform session YAML files.
6
+ */
7
+ export type SessionOrigin = "web" | "discord" | "slack" | "schedule" | "native";
8
+ export interface SessionAttribution {
9
+ origin: SessionOrigin;
10
+ agentName: string | undefined;
11
+ triggerType: string | undefined;
12
+ }
13
+ export interface AttributionIndex {
14
+ /** Attribute a single session ID */
15
+ getAttribute(sessionId: string): SessionAttribution;
16
+ /** Batch attribute multiple session IDs */
17
+ getAttributes(sessionIds: string[]): Map<string, SessionAttribution>;
18
+ /** Number of entries in the index (for diagnostics) */
19
+ readonly size: number;
20
+ }
21
+ /**
22
+ * Build an attribution index from job metadata and platform YAML files
23
+ *
24
+ * @param stateDir - Path to the .herdctl state directory
25
+ * @returns An AttributionIndex for looking up session origins
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const index = await buildAttributionIndex('/path/to/.herdctl');
30
+ * const attribution = index.getAttribute('session-123');
31
+ * console.log(attribution.origin); // 'discord'
32
+ * ```
33
+ */
34
+ export declare function buildAttributionIndex(stateDir: string): Promise<AttributionIndex>;
35
+ //# sourceMappingURL=session-attribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-attribution.d.ts","sourceRoot":"","sources":["../../src/state/session-attribution.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEhF,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAAC;IACpD,2CAA2C;IAC3C,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACrE,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AA8ID;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuDvF"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Session attribution module
3
+ *
4
+ * Determines the origin of a Claude Code session (web, discord, slack, schedule, or native CLI)
5
+ * by cross-referencing HerdCTL's job metadata and platform session YAML files.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import yaml from "yaml";
10
+ import { z } from "zod";
11
+ import { createLogger } from "../utils/logger.js";
12
+ import { listJobs } from "./job-metadata.js";
13
+ // =============================================================================
14
+ // Schemas
15
+ // =============================================================================
16
+ const PlatformSessionSchema = z.object({
17
+ version: z.union([z.literal(1), z.literal(2), z.literal(3)]),
18
+ agentName: z.string(),
19
+ channels: z.record(z.string(), z.object({
20
+ sessionId: z.string(),
21
+ lastMessageAt: z.string(),
22
+ })),
23
+ });
24
+ // =============================================================================
25
+ // Logger
26
+ // =============================================================================
27
+ const logger = createLogger("SessionAttribution");
28
+ // =============================================================================
29
+ // Helper Functions
30
+ // =============================================================================
31
+ /**
32
+ * Convert a trigger type to a session origin
33
+ */
34
+ function triggerTypeToOrigin(triggerType) {
35
+ switch (triggerType) {
36
+ case "web":
37
+ return "web";
38
+ case "discord":
39
+ return "discord";
40
+ case "slack":
41
+ return "slack";
42
+ case "schedule":
43
+ return "schedule";
44
+ // manual, webhook, chat, fork — all treated as native CLI usage
45
+ default:
46
+ return "native";
47
+ }
48
+ }
49
+ /**
50
+ * Build the job index from job metadata files
51
+ */
52
+ async function buildJobIndex(jobsDir) {
53
+ const index = new Map();
54
+ const result = await listJobs(jobsDir, {}, { logger });
55
+ for (const job of result.jobs) {
56
+ if (job.session_id) {
57
+ index.set(job.session_id, {
58
+ agent: job.agent,
59
+ triggerType: job.trigger_type,
60
+ });
61
+ }
62
+ }
63
+ return index;
64
+ }
65
+ /**
66
+ * Build the platform index from platform session YAML files
67
+ */
68
+ async function buildPlatformIndex(stateDir) {
69
+ const index = new Map();
70
+ const platforms = ["discord", "slack", "web"];
71
+ for (const platform of platforms) {
72
+ const sessionDir = path.join(stateDir, `${platform}-sessions`);
73
+ let fileNames;
74
+ try {
75
+ fileNames = await fs.readdir(sessionDir);
76
+ }
77
+ catch (error) {
78
+ if (error.code === "ENOENT") {
79
+ logger.debug(`Session directory does not exist: ${sessionDir}`);
80
+ continue;
81
+ }
82
+ throw error;
83
+ }
84
+ const yamlFiles = fileNames.filter((name) => name.endsWith(".yaml"));
85
+ for (const fileName of yamlFiles) {
86
+ const filePath = path.join(sessionDir, fileName);
87
+ try {
88
+ const content = await fs.readFile(filePath, "utf-8");
89
+ const parsed = yaml.parse(content);
90
+ const validated = PlatformSessionSchema.safeParse(parsed);
91
+ if (!validated.success) {
92
+ logger.warn(`Malformed platform session file: ${filePath}: ${validated.error.message}`);
93
+ continue;
94
+ }
95
+ const session = validated.data;
96
+ for (const channel of Object.values(session.channels)) {
97
+ index.set(channel.sessionId, {
98
+ platform,
99
+ agentName: session.agentName,
100
+ });
101
+ }
102
+ }
103
+ catch (error) {
104
+ if (error instanceof yaml.YAMLParseError) {
105
+ logger.warn(`Failed to parse YAML file: ${filePath}: ${error.message}`);
106
+ continue;
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+ }
112
+ return index;
113
+ }
114
+ // =============================================================================
115
+ // Public API
116
+ // =============================================================================
117
+ /**
118
+ * Build an attribution index from job metadata and platform YAML files
119
+ *
120
+ * @param stateDir - Path to the .herdctl state directory
121
+ * @returns An AttributionIndex for looking up session origins
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * const index = await buildAttributionIndex('/path/to/.herdctl');
126
+ * const attribution = index.getAttribute('session-123');
127
+ * console.log(attribution.origin); // 'discord'
128
+ * ```
129
+ */
130
+ export async function buildAttributionIndex(stateDir) {
131
+ const jobsDir = path.join(stateDir, "jobs");
132
+ const [jobIndex, platformIndex] = await Promise.all([
133
+ buildJobIndex(jobsDir),
134
+ buildPlatformIndex(stateDir),
135
+ ]);
136
+ const getAttribute = (sessionId) => {
137
+ // Check job index first
138
+ const jobEntry = jobIndex.get(sessionId);
139
+ if (jobEntry) {
140
+ return {
141
+ origin: triggerTypeToOrigin(jobEntry.triggerType),
142
+ agentName: jobEntry.agent,
143
+ triggerType: jobEntry.triggerType,
144
+ };
145
+ }
146
+ // Check platform index
147
+ const platformEntry = platformIndex.get(sessionId);
148
+ if (platformEntry) {
149
+ return {
150
+ origin: platformEntry.platform,
151
+ agentName: platformEntry.agentName,
152
+ triggerType: undefined,
153
+ };
154
+ }
155
+ // Default to native
156
+ return {
157
+ origin: "native",
158
+ agentName: undefined,
159
+ triggerType: undefined,
160
+ };
161
+ };
162
+ const getAttributes = (sessionIds) => {
163
+ const result = new Map();
164
+ for (const sessionId of sessionIds) {
165
+ result.set(sessionId, getAttribute(sessionId));
166
+ }
167
+ return result;
168
+ };
169
+ // Calculate unique session IDs across both indexes
170
+ const allSessionIds = new Set([...jobIndex.keys(), ...platformIndex.keys()]);
171
+ return {
172
+ getAttribute,
173
+ getAttributes,
174
+ get size() {
175
+ return allSessionIds.size;
176
+ },
177
+ };
178
+ }
179
+ //# sourceMappingURL=session-attribution.js.map