@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.
- package/dist/config/__tests__/merge.test.js +1 -1
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +10 -2
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +6 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/distribution/__tests__/agent-discovery.test.d.ts +7 -0
- package/dist/distribution/__tests__/agent-discovery.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/agent-discovery.test.js +443 -0
- package/dist/distribution/__tests__/agent-discovery.test.js.map +1 -0
- package/dist/distribution/__tests__/agent-info.test.d.ts +7 -0
- package/dist/distribution/__tests__/agent-info.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/agent-info.test.js +568 -0
- package/dist/distribution/__tests__/agent-info.test.js.map +1 -0
- package/dist/distribution/__tests__/agent-remover.test.d.ts +7 -0
- package/dist/distribution/__tests__/agent-remover.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/agent-remover.test.js +498 -0
- package/dist/distribution/__tests__/agent-remover.test.js.map +1 -0
- package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts +5 -0
- package/dist/distribution/__tests__/agent-repo-metadata.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/agent-repo-metadata.test.js +500 -0
- package/dist/distribution/__tests__/agent-repo-metadata.test.js.map +1 -0
- package/dist/distribution/__tests__/env-scanner.test.d.ts +5 -0
- package/dist/distribution/__tests__/env-scanner.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/env-scanner.test.js +576 -0
- package/dist/distribution/__tests__/env-scanner.test.js.map +1 -0
- package/dist/distribution/__tests__/file-installer.test.d.ts +7 -0
- package/dist/distribution/__tests__/file-installer.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/file-installer.test.js +714 -0
- package/dist/distribution/__tests__/file-installer.test.js.map +1 -0
- package/dist/distribution/__tests__/fleet-config-updater.test.d.ts +7 -0
- package/dist/distribution/__tests__/fleet-config-updater.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/fleet-config-updater.test.js +531 -0
- package/dist/distribution/__tests__/fleet-config-updater.test.js.map +1 -0
- package/dist/distribution/__tests__/installation-metadata.test.d.ts +2 -0
- package/dist/distribution/__tests__/installation-metadata.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/installation-metadata.test.js +292 -0
- package/dist/distribution/__tests__/installation-metadata.test.js.map +1 -0
- package/dist/distribution/__tests__/integration.test.d.ts +10 -0
- package/dist/distribution/__tests__/integration.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/integration.test.js +522 -0
- package/dist/distribution/__tests__/integration.test.js.map +1 -0
- package/dist/distribution/__tests__/repository-fetcher.test.d.ts +5 -0
- package/dist/distribution/__tests__/repository-fetcher.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/repository-fetcher.test.js +386 -0
- package/dist/distribution/__tests__/repository-fetcher.test.js.map +1 -0
- package/dist/distribution/__tests__/repository-validator.test.d.ts +7 -0
- package/dist/distribution/__tests__/repository-validator.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/repository-validator.test.js +447 -0
- package/dist/distribution/__tests__/repository-validator.test.js.map +1 -0
- package/dist/distribution/__tests__/source-specifier.test.d.ts +5 -0
- package/dist/distribution/__tests__/source-specifier.test.d.ts.map +1 -0
- package/dist/distribution/__tests__/source-specifier.test.js +533 -0
- package/dist/distribution/__tests__/source-specifier.test.js.map +1 -0
- package/dist/distribution/agent-discovery.d.ts +81 -0
- package/dist/distribution/agent-discovery.d.ts.map +1 -0
- package/dist/distribution/agent-discovery.js +264 -0
- package/dist/distribution/agent-discovery.js.map +1 -0
- package/dist/distribution/agent-info.d.ts +86 -0
- package/dist/distribution/agent-info.d.ts.map +1 -0
- package/dist/distribution/agent-info.js +225 -0
- package/dist/distribution/agent-info.js.map +1 -0
- package/dist/distribution/agent-remover.d.ts +83 -0
- package/dist/distribution/agent-remover.d.ts.map +1 -0
- package/dist/distribution/agent-remover.js +222 -0
- package/dist/distribution/agent-remover.js.map +1 -0
- package/dist/distribution/agent-repo-metadata.d.ts +181 -0
- package/dist/distribution/agent-repo-metadata.d.ts.map +1 -0
- package/dist/distribution/agent-repo-metadata.js +143 -0
- package/dist/distribution/agent-repo-metadata.js.map +1 -0
- package/dist/distribution/env-scanner.d.ts +78 -0
- package/dist/distribution/env-scanner.d.ts.map +1 -0
- package/dist/distribution/env-scanner.js +144 -0
- package/dist/distribution/env-scanner.js.map +1 -0
- package/dist/distribution/file-installer.d.ts +80 -0
- package/dist/distribution/file-installer.d.ts.map +1 -0
- package/dist/distribution/file-installer.js +268 -0
- package/dist/distribution/file-installer.js.map +1 -0
- package/dist/distribution/fleet-config-updater.d.ts +96 -0
- package/dist/distribution/fleet-config-updater.d.ts.map +1 -0
- package/dist/distribution/fleet-config-updater.js +266 -0
- package/dist/distribution/fleet-config-updater.js.map +1 -0
- package/dist/distribution/index.d.ts +23 -0
- package/dist/distribution/index.d.ts.map +1 -0
- package/dist/distribution/index.js +42 -0
- package/dist/distribution/index.js.map +1 -0
- package/dist/distribution/installation-metadata.d.ts +191 -0
- package/dist/distribution/installation-metadata.d.ts.map +1 -0
- package/dist/distribution/installation-metadata.js +100 -0
- package/dist/distribution/installation-metadata.js.map +1 -0
- package/dist/distribution/repository-fetcher.d.ts +104 -0
- package/dist/distribution/repository-fetcher.d.ts.map +1 -0
- package/dist/distribution/repository-fetcher.js +246 -0
- package/dist/distribution/repository-fetcher.js.map +1 -0
- package/dist/distribution/repository-validator.d.ts +86 -0
- package/dist/distribution/repository-validator.d.ts.map +1 -0
- package/dist/distribution/repository-validator.js +296 -0
- package/dist/distribution/repository-validator.js.map +1 -0
- package/dist/distribution/source-specifier.d.ts +106 -0
- package/dist/distribution/source-specifier.d.ts.map +1 -0
- package/dist/distribution/source-specifier.js +247 -0
- package/dist/distribution/source-specifier.js.map +1 -0
- package/dist/fleet-manager/errors.d.ts +15 -0
- package/dist/fleet-manager/errors.d.ts.map +1 -1
- package/dist/fleet-manager/errors.js +16 -0
- package/dist/fleet-manager/errors.js.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +31 -9
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/runner/message-processor.d.ts.map +1 -1
- package/dist/runner/message-processor.js +7 -2
- package/dist/runner/message-processor.js.map +1 -1
- package/dist/runner/runtime/container-manager.js +1 -1
- package/dist/runner/runtime/container-manager.js.map +1 -1
- package/dist/scheduler/errors.d.ts +15 -0
- package/dist/scheduler/errors.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.js +6 -5
- package/dist/scheduler/schedule-runner.js.map +1 -1
- package/dist/state/__tests__/jsonl-parser.test.d.ts +5 -0
- package/dist/state/__tests__/jsonl-parser.test.d.ts.map +1 -0
- package/dist/state/__tests__/jsonl-parser.test.js +307 -0
- package/dist/state/__tests__/jsonl-parser.test.js.map +1 -0
- package/dist/state/__tests__/session-attribution.test.d.ts +2 -0
- package/dist/state/__tests__/session-attribution.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-attribution.test.js +567 -0
- package/dist/state/__tests__/session-attribution.test.js.map +1 -0
- package/dist/state/__tests__/session-discovery.test.d.ts +2 -0
- package/dist/state/__tests__/session-discovery.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-discovery.test.js +941 -0
- package/dist/state/__tests__/session-discovery.test.js.map +1 -0
- package/dist/state/__tests__/session-metadata.test.d.ts +2 -0
- package/dist/state/__tests__/session-metadata.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-metadata.test.js +422 -0
- package/dist/state/__tests__/session-metadata.test.js.map +1 -0
- package/dist/state/__tests__/tool-parsing.test.d.ts +5 -0
- package/dist/state/__tests__/tool-parsing.test.d.ts.map +1 -0
- package/dist/state/__tests__/tool-parsing.test.js +315 -0
- package/dist/state/__tests__/tool-parsing.test.js.map +1 -0
- package/dist/state/index.d.ts +5 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +10 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/jsonl-parser.d.ts +115 -0
- package/dist/state/jsonl-parser.d.ts.map +1 -0
- package/dist/state/jsonl-parser.js +437 -0
- package/dist/state/jsonl-parser.js.map +1 -0
- package/dist/state/session-attribution.d.ts +35 -0
- package/dist/state/session-attribution.d.ts.map +1 -0
- package/dist/state/session-attribution.js +179 -0
- package/dist/state/session-attribution.js.map +1 -0
- package/dist/state/session-discovery.d.ts +188 -0
- package/dist/state/session-discovery.d.ts.map +1 -0
- package/dist/state/session-discovery.js +513 -0
- package/dist/state/session-discovery.js.map +1 -0
- package/dist/state/session-metadata.d.ts +186 -0
- package/dist/state/session-metadata.d.ts.map +1 -0
- package/dist/state/session-metadata.js +297 -0
- package/dist/state/session-metadata.js.map +1 -0
- package/dist/state/tool-parsing.d.ts +88 -0
- package/dist/state/tool-parsing.d.ts.map +1 -0
- package/dist/state/tool-parsing.js +199 -0
- package/dist/state/tool-parsing.js.map +1 -0
- 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
|