agent-trace 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 (2) hide show
  1. package/agent-trace.cjs +4462 -0
  2. package/package.json +31 -0
@@ -0,0 +1,4462 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // packages/runtime/src/standalone-entry.ts
27
+ var import_node_path3 = __toESM(require("node:path"));
28
+ var import_node_os2 = __toESM(require("node:os"));
29
+ var import_node_fs2 = __toESM(require("node:fs"));
30
+
31
+ // packages/dashboard/src/web-server.ts
32
+ var import_node_http = __toESM(require("node:http"));
33
+
34
+ // packages/dashboard/src/web-render.ts
35
+ function escapeHtml(value) {
36
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
37
+ }
38
+ function renderDashboardHtml(options = {}) {
39
+ const title = options.title ?? "agent-trace dashboard";
40
+ const safeTitle = escapeHtml(title);
41
+ return `<!doctype html>
42
+ <html lang="en">
43
+ <head>
44
+ <meta charset="utf-8" />
45
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
46
+ <title>${safeTitle}</title>
47
+ <style>
48
+ :root {
49
+ --paper: #f7f7f2;
50
+ --ink: #121212;
51
+ --accent: #0f766e;
52
+ --accent-soft: #d1fae5;
53
+ --grid: #d6d3d1;
54
+ --warn: #9a3412;
55
+ }
56
+
57
+ * {
58
+ box-sizing: border-box;
59
+ }
60
+
61
+ body {
62
+ margin: 0;
63
+ color: var(--ink);
64
+ font-family: "Space Grotesk", "IBM Plex Sans", system-ui, sans-serif;
65
+ background:
66
+ radial-gradient(circle at 20% 10%, rgba(15, 118, 110, 0.18), transparent 30%),
67
+ radial-gradient(circle at 80% 0%, rgba(217, 119, 6, 0.15), transparent 35%),
68
+ linear-gradient(145deg, #f5f5f4, var(--paper));
69
+ }
70
+
71
+ .shell {
72
+ max-width: 1100px;
73
+ margin: 0 auto;
74
+ padding: 28px 18px 40px;
75
+ }
76
+
77
+ .hero {
78
+ border: 2px solid var(--ink);
79
+ background: #fff;
80
+ box-shadow: 8px 8px 0 var(--ink);
81
+ padding: 22px 20px;
82
+ }
83
+
84
+ .hero h1 {
85
+ margin: 0;
86
+ font-size: clamp(1.7rem, 2.4vw, 2.3rem);
87
+ letter-spacing: -0.02em;
88
+ }
89
+
90
+ .hero p {
91
+ margin: 10px 0 0;
92
+ color: #3f3f46;
93
+ }
94
+
95
+ .grid {
96
+ margin-top: 18px;
97
+ display: grid;
98
+ grid-template-columns: repeat(3, minmax(0, 1fr));
99
+ gap: 12px;
100
+ }
101
+
102
+ .metric {
103
+ border: 1px solid var(--grid);
104
+ background: #fff;
105
+ padding: 12px;
106
+ }
107
+
108
+ .metric .label {
109
+ font-size: 0.78rem;
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.08em;
112
+ color: #52525b;
113
+ }
114
+
115
+ .metric .value {
116
+ margin-top: 6px;
117
+ font-size: 1.35rem;
118
+ font-weight: 700;
119
+ }
120
+
121
+ .panel {
122
+ margin-top: 18px;
123
+ border: 2px solid var(--ink);
124
+ background: #fff;
125
+ box-shadow: 8px 8px 0 var(--ink);
126
+ overflow: hidden;
127
+ }
128
+
129
+ .panel header {
130
+ padding: 12px 14px;
131
+ background: var(--accent-soft);
132
+ border-bottom: 2px solid var(--ink);
133
+ font-weight: 700;
134
+ }
135
+
136
+ table {
137
+ width: 100%;
138
+ border-collapse: collapse;
139
+ }
140
+
141
+ th, td {
142
+ padding: 10px 12px;
143
+ border-bottom: 1px solid var(--grid);
144
+ text-align: left;
145
+ font-size: 0.95rem;
146
+ }
147
+
148
+ th {
149
+ font-size: 0.8rem;
150
+ text-transform: uppercase;
151
+ letter-spacing: 0.06em;
152
+ color: #57534e;
153
+ background: #fafaf9;
154
+ }
155
+
156
+ .status {
157
+ margin-top: 12px;
158
+ padding: 10px 12px;
159
+ border: 1px solid var(--grid);
160
+ background: #fff;
161
+ }
162
+
163
+ .status.error {
164
+ color: var(--warn);
165
+ border-color: #fca5a5;
166
+ background: #fef2f2;
167
+ }
168
+
169
+ @media (max-width: 840px) {
170
+ .grid {
171
+ grid-template-columns: 1fr;
172
+ }
173
+
174
+ th:nth-child(4),
175
+ td:nth-child(4) {
176
+ display: none;
177
+ }
178
+ }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ <main class="shell">
183
+ <section class="hero">
184
+ <h1>${safeTitle}</h1>
185
+ <p>Session-level observability for coding agents, running locally.</p>
186
+ </section>
187
+
188
+ <section class="grid">
189
+ <article class="metric">
190
+ <div class="label">Sessions</div>
191
+ <div class="value" id="metric-sessions">0</div>
192
+ </article>
193
+ <article class="metric">
194
+ <div class="label">Total Cost (USD)</div>
195
+ <div class="value" id="metric-cost">$0.00</div>
196
+ </article>
197
+ <article class="metric">
198
+ <div class="label">Latest Start</div>
199
+ <div class="value" id="metric-latest">-</div>
200
+ </article>
201
+ </section>
202
+
203
+ <section class="panel">
204
+ <header>Recent Sessions</header>
205
+ <table>
206
+ <thead>
207
+ <tr>
208
+ <th>Session</th>
209
+ <th>User</th>
210
+ <th>Repo</th>
211
+ <th>Started</th>
212
+ <th>Cost</th>
213
+ <th>Replay</th>
214
+ </tr>
215
+ </thead>
216
+ <tbody id="sessions-body">
217
+ <tr><td colspan="6">Loading sessions...</td></tr>
218
+ </tbody>
219
+ </table>
220
+ </section>
221
+
222
+ <section class="panel">
223
+ <header>Session Replay</header>
224
+ <div id="replay-meta" class="status">Select a session to inspect timeline events.</div>
225
+ <table>
226
+ <thead>
227
+ <tr>
228
+ <th>Timestamp</th>
229
+ <th>Type</th>
230
+ <th>Status</th>
231
+ <th>Cost</th>
232
+ <th>Prompt</th>
233
+ </tr>
234
+ </thead>
235
+ <tbody id="replay-body">
236
+ <tr><td colspan="5">No session selected.</td></tr>
237
+ </tbody>
238
+ </table>
239
+ </section>
240
+
241
+ <section id="status" class="status">Fetching data from local API bridge...</section>
242
+ </main>
243
+ <script>
244
+ const sessionsBody = document.getElementById("sessions-body");
245
+ const status = document.getElementById("status");
246
+ const sessionsMetric = document.getElementById("metric-sessions");
247
+ const costMetric = document.getElementById("metric-cost");
248
+ const latestMetric = document.getElementById("metric-latest");
249
+ const replayMeta = document.getElementById("replay-meta");
250
+ const replayBody = document.getElementById("replay-body");
251
+ let selectedSessionId = null;
252
+
253
+ function formatMoney(value) {
254
+ return "$" + value.toFixed(2);
255
+ }
256
+
257
+ function formatDate(value) {
258
+ try {
259
+ return new Date(value).toLocaleString();
260
+ } catch {
261
+ return value;
262
+ }
263
+ }
264
+
265
+ function escapeHtml(value) {
266
+ return String(value)
267
+ .replaceAll("&", "&amp;")
268
+ .replaceAll("<", "&lt;")
269
+ .replaceAll(">", "&gt;")
270
+ .replaceAll(""", "&quot;")
271
+ .replaceAll("'", "&#39;");
272
+ }
273
+
274
+ function setReplayPlaceholder(message) {
275
+ replayBody.innerHTML = "<tr><td colspan=\\"5\\">" + escapeHtml(message) + "</td></tr>";
276
+ }
277
+
278
+ function renderSessionReplay(session) {
279
+ if (typeof session !== "object" || session === null) {
280
+ replayMeta.classList.add("error");
281
+ replayMeta.textContent = "Replay payload is invalid.";
282
+ setReplayPlaceholder("Replay payload is invalid.");
283
+ return;
284
+ }
285
+
286
+ const timeline = Array.isArray(session.timeline) ? session.timeline : [];
287
+ const promptCount = typeof session.metrics?.promptCount === "number" ? session.metrics.promptCount : 0;
288
+ const toolCallCount = typeof session.metrics?.toolCallCount === "number" ? session.metrics.toolCallCount : 0;
289
+ const totalCostUsd = typeof session.metrics?.totalCostUsd === "number" ? session.metrics.totalCostUsd : 0;
290
+ replayMeta.classList.remove("error");
291
+ replayMeta.textContent = "Session " + session.sessionId + " | prompts " + promptCount
292
+ + " | tools " + toolCallCount + " | cost " + formatMoney(totalCostUsd);
293
+
294
+ if (timeline.length === 0) {
295
+ setReplayPlaceholder("No timeline events for this session.");
296
+ return;
297
+ }
298
+
299
+ const rows = timeline.map((event) => {
300
+ const timestamp = typeof event.timestamp === "string" ? formatDate(event.timestamp) : "-";
301
+ const type = typeof event.type === "string" ? event.type : "-";
302
+ const eventStatus = typeof event.status === "string" ? event.status : "-";
303
+ const cost = typeof event.costUsd === "number" ? formatMoney(event.costUsd) : "-";
304
+ const prompt = typeof event.promptId === "string" ? event.promptId : "-";
305
+ return "<tr>"
306
+ + "<td>" + escapeHtml(timestamp) + "</td>"
307
+ + "<td>" + escapeHtml(type) + "</td>"
308
+ + "<td>" + escapeHtml(eventStatus) + "</td>"
309
+ + "<td>" + escapeHtml(cost) + "</td>"
310
+ + "<td>" + escapeHtml(prompt) + "</td>"
311
+ + "</tr>";
312
+ }).join("");
313
+ replayBody.innerHTML = rows;
314
+ }
315
+
316
+ async function loadSessionReplay(sessionId) {
317
+ selectedSessionId = sessionId;
318
+ try {
319
+ const response = await fetch("/api/session/" + encodeURIComponent(sessionId));
320
+ if (response.status === 404) {
321
+ replayMeta.classList.add("error");
322
+ replayMeta.textContent = "Session replay not found.";
323
+ setReplayPlaceholder("Session replay not found.");
324
+ return;
325
+ }
326
+ if (!response.ok) {
327
+ throw new Error("session replay bridge failed with status " + response.status);
328
+ }
329
+ const payload = await response.json();
330
+ if (payload?.status !== "ok" || typeof payload.session !== "object") {
331
+ throw new Error("unexpected replay payload format");
332
+ }
333
+ renderSessionReplay(payload.session);
334
+ } catch (error) {
335
+ replayMeta.classList.add("error");
336
+ replayMeta.textContent = String(error);
337
+ setReplayPlaceholder("Failed to load replay.");
338
+ }
339
+ }
340
+
341
+ function bindReplayButtons() {
342
+ const buttons = sessionsBody.querySelectorAll(".replay-button");
343
+ buttons.forEach((button) => {
344
+ button.addEventListener("click", () => {
345
+ const sessionId = button.getAttribute("data-session-id");
346
+ if (sessionId === null || sessionId.length === 0) {
347
+ return;
348
+ }
349
+ void loadSessionReplay(sessionId);
350
+ });
351
+ });
352
+ }
353
+
354
+ function renderSessions(sessions) {
355
+ if (!Array.isArray(sessions) || sessions.length === 0) {
356
+ sessionsBody.innerHTML = "<tr><td colspan=\\"6\\">No sessions yet.</td></tr>";
357
+ sessionsMetric.textContent = "0";
358
+ costMetric.textContent = "$0.00";
359
+ latestMetric.textContent = "-";
360
+ replayMeta.classList.remove("error");
361
+ replayMeta.textContent = "Select a session to inspect timeline events.";
362
+ setReplayPlaceholder("No session selected.");
363
+ return;
364
+ }
365
+
366
+ const rows = sessions.map((session) => {
367
+ const repo = session.gitRepo ?? "-";
368
+ const cost = typeof session.totalCostUsd === "number" ? session.totalCostUsd : 0;
369
+ return "<tr>"
370
+ + "<td>" + escapeHtml(session.sessionId) + "</td>"
371
+ + "<td>" + escapeHtml(session.userId) + "</td>"
372
+ + "<td>" + escapeHtml(repo) + "</td>"
373
+ + "<td>" + escapeHtml(formatDate(session.startedAt)) + "</td>"
374
+ + "<td>" + escapeHtml(formatMoney(cost)) + "</td>"
375
+ + "<td><button type=\\"button\\" class=\\"replay-button\\" data-session-id=\\""
376
+ + escapeHtml(session.sessionId)
377
+ + "\\">View</button></td>"
378
+ + "</tr>";
379
+ }).join("");
380
+ sessionsBody.innerHTML = rows;
381
+ bindReplayButtons();
382
+
383
+ const totalCost = sessions.reduce((sum, session) => {
384
+ const value = typeof session.totalCostUsd === "number" ? session.totalCostUsd : 0;
385
+ return sum + value;
386
+ }, 0);
387
+ const latest = sessions
388
+ .map((session) => session.startedAt)
389
+ .filter((value) => typeof value === "string")
390
+ .sort()
391
+ .at(-1) ?? "-";
392
+
393
+ sessionsMetric.textContent = String(sessions.length);
394
+ costMetric.textContent = formatMoney(totalCost);
395
+ latestMetric.textContent = latest === "-" ? "-" : formatDate(latest);
396
+
397
+ const selectedInList = selectedSessionId !== null && sessions.some((session) => session.sessionId === selectedSessionId);
398
+ if (!selectedInList && sessions[0]?.sessionId !== undefined) {
399
+ void loadSessionReplay(sessions[0].sessionId);
400
+ }
401
+ }
402
+
403
+ async function loadSessions() {
404
+ try {
405
+ const response = await fetch("/api/sessions");
406
+ if (!response.ok) {
407
+ throw new Error("dashboard bridge failed with status " + response.status);
408
+ }
409
+ const payload = await response.json();
410
+ if (payload?.status !== "ok" || !Array.isArray(payload.sessions)) {
411
+ throw new Error("unexpected payload format");
412
+ }
413
+ renderSessions(payload.sessions);
414
+ status.classList.remove("error");
415
+ status.textContent = "Data source connected.";
416
+ } catch (error) {
417
+ sessionsBody.innerHTML = "<tr><td colspan=\\"6\\">Failed to load sessions.</td></tr>";
418
+ status.classList.add("error");
419
+ status.textContent = String(error);
420
+ }
421
+ }
422
+
423
+ function startLiveSessionsStream() {
424
+ if (typeof EventSource === "undefined") {
425
+ status.classList.remove("error");
426
+ status.textContent = "Live stream unavailable. Snapshot mode enabled.";
427
+ return;
428
+ }
429
+
430
+ status.classList.remove("error");
431
+ status.textContent = "Connecting to live session stream...";
432
+
433
+ const stream = new EventSource("/api/sessions/stream");
434
+ stream.addEventListener("sessions", (event) => {
435
+ try {
436
+ const payload = JSON.parse(event.data);
437
+ if (payload?.status !== "ok" || !Array.isArray(payload.sessions)) {
438
+ throw new Error("unexpected stream payload");
439
+ }
440
+ renderSessions(payload.sessions);
441
+ status.classList.remove("error");
442
+ status.textContent = "Live session stream connected.";
443
+ } catch (error) {
444
+ status.classList.add("error");
445
+ status.textContent = String(error);
446
+ }
447
+ });
448
+
449
+ stream.addEventListener("bridge_error", (event) => {
450
+ status.classList.add("error");
451
+ status.textContent = "Bridge error: " + event.data;
452
+ });
453
+
454
+ stream.onerror = () => {
455
+ status.classList.add("error");
456
+ status.textContent = "Live stream disconnected. Retrying...";
457
+ };
458
+ }
459
+
460
+ async function boot() {
461
+ await loadSessions();
462
+ startLiveSessionsStream();
463
+ }
464
+
465
+ void boot();
466
+ </script>
467
+ </body>
468
+ </html>`;
469
+ }
470
+
471
+ // packages/dashboard/src/web-server.ts
472
+ function parsePathname(url) {
473
+ try {
474
+ return new URL(url, "http://localhost").pathname;
475
+ } catch {
476
+ return url;
477
+ }
478
+ }
479
+ function parsePathSegments(pathname) {
480
+ return pathname.split("/").filter((segment) => segment.length > 0);
481
+ }
482
+ function toAddress(server) {
483
+ const address = server.address();
484
+ if (address === null) {
485
+ return "unknown";
486
+ }
487
+ if (typeof address === "string") {
488
+ return address;
489
+ }
490
+ return `${address.address}:${String(address.port)}`;
491
+ }
492
+ async function listen(server, port, host) {
493
+ await new Promise((resolve, reject) => {
494
+ server.listen(port, host, () => resolve());
495
+ server.once("error", (error) => reject(error));
496
+ });
497
+ }
498
+ async function close(server) {
499
+ await new Promise((resolve, reject) => {
500
+ server.close((error) => {
501
+ if (error !== void 0 && error !== null) {
502
+ reject(error);
503
+ return;
504
+ }
505
+ resolve();
506
+ });
507
+ });
508
+ }
509
+ async function fetchSessionsFromApi(apiBaseUrl) {
510
+ const response = await fetch(`${apiBaseUrl}/v1/sessions`);
511
+ if (!response.ok) {
512
+ throw new Error(`api returned status ${String(response.status)}`);
513
+ }
514
+ const payload = await response.json();
515
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
516
+ throw new Error("api payload is not an object");
517
+ }
518
+ const record = payload;
519
+ const sessionsRaw = record["sessions"];
520
+ if (!Array.isArray(sessionsRaw)) {
521
+ throw new Error("api payload sessions is not an array");
522
+ }
523
+ const sessions = [];
524
+ sessionsRaw.forEach((entry) => {
525
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
526
+ return;
527
+ }
528
+ const row = entry;
529
+ const sessionId = row["sessionId"];
530
+ const userId = row["userId"];
531
+ const startedAt = row["startedAt"];
532
+ if (typeof sessionId !== "string" || typeof userId !== "string" || typeof startedAt !== "string") {
533
+ return;
534
+ }
535
+ sessions.push({
536
+ sessionId,
537
+ userId,
538
+ gitRepo: typeof row["gitRepo"] === "string" ? row["gitRepo"] : null,
539
+ gitBranch: typeof row["gitBranch"] === "string" ? row["gitBranch"] : null,
540
+ startedAt,
541
+ endedAt: typeof row["endedAt"] === "string" ? row["endedAt"] : null,
542
+ promptCount: typeof row["promptCount"] === "number" ? row["promptCount"] : 0,
543
+ toolCallCount: typeof row["toolCallCount"] === "number" ? row["toolCallCount"] : 0,
544
+ totalCostUsd: typeof row["totalCostUsd"] === "number" ? row["totalCostUsd"] : 0,
545
+ commitCount: typeof row["commitCount"] === "number" ? row["commitCount"] : 0,
546
+ linesAdded: typeof row["linesAdded"] === "number" ? row["linesAdded"] : 0,
547
+ linesRemoved: typeof row["linesRemoved"] === "number" ? row["linesRemoved"] : 0
548
+ });
549
+ });
550
+ return sessions;
551
+ }
552
+ function parseTimelineEvent(entry) {
553
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
554
+ return void 0;
555
+ }
556
+ const record = entry;
557
+ const id = record["id"];
558
+ const type = record["type"];
559
+ const timestamp = record["timestamp"];
560
+ if (typeof id !== "string" || typeof type !== "string" || typeof timestamp !== "string") {
561
+ return void 0;
562
+ }
563
+ return {
564
+ id,
565
+ type,
566
+ timestamp,
567
+ ...typeof record["promptId"] === "string" ? { promptId: record["promptId"] } : {},
568
+ ...typeof record["status"] === "string" ? { status: record["status"] } : {},
569
+ ...typeof record["costUsd"] === "number" ? { costUsd: record["costUsd"] } : {},
570
+ ...typeof record["details"] === "object" && record["details"] !== null && !Array.isArray(record["details"]) ? { details: record["details"] } : {}
571
+ };
572
+ }
573
+ async function fetchSessionReplayFromApi(apiBaseUrl, sessionId) {
574
+ const response = await fetch(`${apiBaseUrl}/v1/sessions/${encodeURIComponent(sessionId)}`);
575
+ if (response.status === 404) {
576
+ return void 0;
577
+ }
578
+ if (!response.ok) {
579
+ throw new Error(`api returned status ${String(response.status)}`);
580
+ }
581
+ const payload = await response.json();
582
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
583
+ throw new Error("api session payload is not an object");
584
+ }
585
+ const root = payload;
586
+ const sessionRaw = root["session"];
587
+ if (typeof sessionRaw !== "object" || sessionRaw === null || Array.isArray(sessionRaw)) {
588
+ throw new Error("api session payload missing session");
589
+ }
590
+ const session = sessionRaw;
591
+ const sessionIdValue = session["sessionId"];
592
+ const startedAt = session["startedAt"];
593
+ const metricsRaw = session["metrics"];
594
+ const timelineRaw = session["timeline"];
595
+ if (typeof sessionIdValue !== "string" || typeof startedAt !== "string" || typeof metricsRaw !== "object" || metricsRaw === null || Array.isArray(metricsRaw) || !Array.isArray(timelineRaw)) {
596
+ throw new Error("api session payload format is invalid");
597
+ }
598
+ const metrics = metricsRaw;
599
+ const timeline = timelineRaw.map((entry) => parseTimelineEvent(entry)).filter(
600
+ (entry) => entry !== void 0
601
+ );
602
+ const envRaw = session["environment"];
603
+ const envRecord = typeof envRaw === "object" && envRaw !== null && !Array.isArray(envRaw) ? envRaw : void 0;
604
+ const gitBranch = envRecord !== void 0 && typeof envRecord["gitBranch"] === "string" ? envRecord["gitBranch"] : void 0;
605
+ const gitRaw = session["git"];
606
+ const gitRecord = typeof gitRaw === "object" && gitRaw !== null && !Array.isArray(gitRaw) ? gitRaw : void 0;
607
+ const commits = [];
608
+ if (gitRecord !== void 0 && Array.isArray(gitRecord["commits"])) {
609
+ for (const entry of gitRecord["commits"]) {
610
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
611
+ const c = entry;
612
+ if (typeof c["sha"] !== "string") continue;
613
+ commits.push({
614
+ sha: c["sha"],
615
+ ...typeof c["message"] === "string" ? { message: c["message"] } : {},
616
+ ...typeof c["promptId"] === "string" ? { promptId: c["promptId"] } : {},
617
+ ...typeof c["committedAt"] === "string" ? { committedAt: c["committedAt"] } : {}
618
+ });
619
+ }
620
+ }
621
+ const pullRequests = [];
622
+ if (gitRecord !== void 0 && Array.isArray(gitRecord["pullRequests"])) {
623
+ for (const entry of gitRecord["pullRequests"]) {
624
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
625
+ const pr = entry;
626
+ if (typeof pr["repo"] !== "string" || typeof pr["prNumber"] !== "number") continue;
627
+ pullRequests.push({
628
+ repo: pr["repo"],
629
+ prNumber: pr["prNumber"],
630
+ state: typeof pr["state"] === "string" ? pr["state"] : "open",
631
+ ...typeof pr["url"] === "string" ? { url: pr["url"] } : {}
632
+ });
633
+ }
634
+ }
635
+ const modelsUsed = [];
636
+ if (Array.isArray(metrics["modelsUsed"])) {
637
+ for (const item of metrics["modelsUsed"]) {
638
+ if (typeof item === "string" && item.length > 0) modelsUsed.push(item);
639
+ }
640
+ }
641
+ const toolsUsed = [];
642
+ if (Array.isArray(metrics["toolsUsed"])) {
643
+ for (const item of metrics["toolsUsed"]) {
644
+ if (typeof item === "string" && item.length > 0) toolsUsed.push(item);
645
+ }
646
+ }
647
+ const filesTouched = [];
648
+ if (Array.isArray(metrics["filesTouched"])) {
649
+ for (const item of metrics["filesTouched"]) {
650
+ if (typeof item === "string" && item.length > 0) filesTouched.push(item);
651
+ }
652
+ }
653
+ return {
654
+ sessionId: sessionIdValue,
655
+ startedAt,
656
+ ...typeof session["endedAt"] === "string" ? { endedAt: session["endedAt"] } : {},
657
+ metrics: {
658
+ promptCount: typeof metrics["promptCount"] === "number" ? metrics["promptCount"] : 0,
659
+ toolCallCount: typeof metrics["toolCallCount"] === "number" ? metrics["toolCallCount"] : 0,
660
+ totalCostUsd: typeof metrics["totalCostUsd"] === "number" ? metrics["totalCostUsd"] : 0,
661
+ totalInputTokens: typeof metrics["totalInputTokens"] === "number" ? metrics["totalInputTokens"] : 0,
662
+ totalOutputTokens: typeof metrics["totalOutputTokens"] === "number" ? metrics["totalOutputTokens"] : 0,
663
+ linesAdded: typeof metrics["linesAdded"] === "number" ? metrics["linesAdded"] : 0,
664
+ linesRemoved: typeof metrics["linesRemoved"] === "number" ? metrics["linesRemoved"] : 0,
665
+ modelsUsed,
666
+ toolsUsed,
667
+ filesTouched
668
+ },
669
+ ...gitBranch !== void 0 ? { environment: { gitBranch } } : {},
670
+ ...commits.length > 0 || pullRequests.length > 0 ? { git: { commits, pullRequests } } : {},
671
+ timeline
672
+ };
673
+ }
674
+ function createDefaultSessionsProvider(apiBaseUrl) {
675
+ return {
676
+ fetchSessions: async () => fetchSessionsFromApi(apiBaseUrl)
677
+ };
678
+ }
679
+ function createDefaultSessionReplayProvider(apiBaseUrl) {
680
+ return {
681
+ fetchSession: async (sessionId) => fetchSessionReplayFromApi(apiBaseUrl, sessionId)
682
+ };
683
+ }
684
+ function sendJson(res, statusCode, payload) {
685
+ const body = JSON.stringify(payload);
686
+ res.statusCode = statusCode;
687
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
688
+ res.end(body);
689
+ }
690
+ function sendHtml(res, statusCode, html) {
691
+ res.statusCode = statusCode;
692
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
693
+ res.end(html);
694
+ }
695
+ async function writeSessionsSnapshot(res, sessionsProvider) {
696
+ const sessions = await sessionsProvider.fetchSessions();
697
+ const payload = {
698
+ status: "ok",
699
+ count: sessions.length,
700
+ sessions
701
+ };
702
+ res.write("event: sessions\n");
703
+ res.write(`data: ${JSON.stringify(payload)}
704
+
705
+ `);
706
+ }
707
+ function startSessionsSseBridge(req, res, sessionsProvider) {
708
+ res.statusCode = 200;
709
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
710
+ res.setHeader("Cache-Control", "no-cache");
711
+ res.setHeader("Connection", "keep-alive");
712
+ res.setHeader("X-Accel-Buffering", "no");
713
+ res.flushHeaders();
714
+ let closed = false;
715
+ let writing = false;
716
+ const writeSnapshot = async () => {
717
+ if (closed || writing) {
718
+ return;
719
+ }
720
+ writing = true;
721
+ try {
722
+ await writeSessionsSnapshot(res, sessionsProvider);
723
+ } catch (error) {
724
+ res.write("event: bridge_error\n");
725
+ res.write(`data: ${JSON.stringify({ message: String(error) })}
726
+
727
+ `);
728
+ } finally {
729
+ writing = false;
730
+ }
731
+ };
732
+ void writeSnapshot();
733
+ const interval = setInterval(() => {
734
+ void writeSnapshot();
735
+ }, 2e3);
736
+ const cleanup = () => {
737
+ if (closed) {
738
+ return;
739
+ }
740
+ closed = true;
741
+ clearInterval(interval);
742
+ if (!res.writableEnded) {
743
+ res.end();
744
+ }
745
+ };
746
+ req.on("close", cleanup);
747
+ res.on("close", cleanup);
748
+ }
749
+ async function startDashboardServer(options = {}) {
750
+ const host = options.host ?? "127.0.0.1";
751
+ const port = options.port ?? 3100;
752
+ const startedAtMs = options.startedAtMs ?? Date.now();
753
+ const apiBaseUrl = options.apiBaseUrl ?? "http://127.0.0.1:8318";
754
+ const sessionsProvider = options.sessionsProvider ?? createDefaultSessionsProvider(apiBaseUrl);
755
+ const sessionReplayProvider = options.sessionReplayProvider ?? createDefaultSessionReplayProvider(apiBaseUrl);
756
+ const server = import_node_http.default.createServer((req, res) => {
757
+ const url = req.url ?? "/";
758
+ const pathname = parsePathname(url);
759
+ const segments = parsePathSegments(pathname);
760
+ const method = req.method ?? "GET";
761
+ if (method !== "GET") {
762
+ sendJson(res, 405, {
763
+ status: "error",
764
+ message: "method not allowed"
765
+ });
766
+ return;
767
+ }
768
+ if (pathname === "/health") {
769
+ const payload = {
770
+ status: "ok",
771
+ service: "dashboard",
772
+ uptimeSec: Math.floor((Date.now() - startedAtMs) / 1e3)
773
+ };
774
+ sendJson(res, 200, payload);
775
+ return;
776
+ }
777
+ if (pathname === "/api/sessions") {
778
+ void sessionsProvider.fetchSessions().then((sessions) => {
779
+ const payload = {
780
+ status: "ok",
781
+ count: sessions.length,
782
+ sessions
783
+ };
784
+ sendJson(res, 200, payload);
785
+ }).catch((error) => {
786
+ sendJson(res, 502, {
787
+ status: "error",
788
+ message: `failed to fetch sessions: ${String(error)}`
789
+ });
790
+ });
791
+ return;
792
+ }
793
+ if (pathname === "/api/sessions/stream") {
794
+ startSessionsSseBridge(req, res, sessionsProvider);
795
+ return;
796
+ }
797
+ if (segments.length === 3 && segments[0] === "api" && segments[1] === "session") {
798
+ const encodedSessionId = segments[2];
799
+ let sessionId = "";
800
+ if (encodedSessionId !== void 0) {
801
+ try {
802
+ sessionId = decodeURIComponent(encodedSessionId);
803
+ } catch {
804
+ sendJson(res, 400, {
805
+ status: "error",
806
+ message: "session id is invalid"
807
+ });
808
+ return;
809
+ }
810
+ }
811
+ if (sessionId.length === 0) {
812
+ sendJson(res, 400, {
813
+ status: "error",
814
+ message: "session id is required"
815
+ });
816
+ return;
817
+ }
818
+ void sessionReplayProvider.fetchSession(sessionId).then((session) => {
819
+ if (session === void 0) {
820
+ sendJson(res, 404, {
821
+ status: "error",
822
+ message: "session not found"
823
+ });
824
+ return;
825
+ }
826
+ const payload = {
827
+ status: "ok",
828
+ session
829
+ };
830
+ sendJson(res, 200, payload);
831
+ }).catch((error) => {
832
+ sendJson(res, 502, {
833
+ status: "error",
834
+ message: `failed to fetch session replay: ${String(error)}`
835
+ });
836
+ });
837
+ return;
838
+ }
839
+ if (pathname === "/") {
840
+ sendHtml(res, 200, renderDashboardHtml());
841
+ return;
842
+ }
843
+ sendJson(res, 404, {
844
+ status: "error",
845
+ message: "not found"
846
+ });
847
+ });
848
+ await listen(server, port, host);
849
+ return {
850
+ address: toAddress(server),
851
+ apiBaseUrl,
852
+ close: async () => {
853
+ await close(server);
854
+ }
855
+ };
856
+ }
857
+
858
+ // packages/platform/src/sqlite-client.ts
859
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
860
+ var SCHEMA_SQL = `
861
+ CREATE TABLE IF NOT EXISTS agent_events (
862
+ event_id TEXT NOT NULL,
863
+ event_type TEXT NOT NULL,
864
+ event_timestamp TEXT NOT NULL,
865
+ session_id TEXT NOT NULL,
866
+ prompt_id TEXT,
867
+ user_id TEXT NOT NULL DEFAULT 'unknown_user',
868
+ source TEXT NOT NULL DEFAULT 'hook',
869
+ agent_type TEXT NOT NULL DEFAULT 'claude_code',
870
+ tool_name TEXT,
871
+ tool_success INTEGER,
872
+ tool_duration_ms REAL,
873
+ model TEXT,
874
+ cost_usd REAL,
875
+ input_tokens INTEGER,
876
+ output_tokens INTEGER,
877
+ api_duration_ms REAL,
878
+ lines_added INTEGER,
879
+ lines_removed INTEGER,
880
+ files_changed TEXT NOT NULL DEFAULT '[]',
881
+ commit_sha TEXT,
882
+ attributes TEXT NOT NULL DEFAULT '{}'
883
+ );
884
+
885
+ CREATE INDEX IF NOT EXISTS idx_events_session ON agent_events(session_id);
886
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON agent_events(event_timestamp);
887
+
888
+ CREATE TABLE IF NOT EXISTS session_traces (
889
+ session_id TEXT NOT NULL,
890
+ version INTEGER NOT NULL DEFAULT 1,
891
+ started_at TEXT NOT NULL,
892
+ ended_at TEXT,
893
+ user_id TEXT NOT NULL DEFAULT 'unknown_user',
894
+ git_repo TEXT,
895
+ git_branch TEXT,
896
+ prompt_count INTEGER NOT NULL DEFAULT 0,
897
+ tool_call_count INTEGER NOT NULL DEFAULT 0,
898
+ api_call_count INTEGER NOT NULL DEFAULT 0,
899
+ total_cost_usd REAL NOT NULL DEFAULT 0,
900
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
901
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
902
+ lines_added INTEGER NOT NULL DEFAULT 0,
903
+ lines_removed INTEGER NOT NULL DEFAULT 0,
904
+ models_used TEXT NOT NULL DEFAULT '[]',
905
+ tools_used TEXT NOT NULL DEFAULT '[]',
906
+ files_touched TEXT NOT NULL DEFAULT '[]',
907
+ commit_count INTEGER NOT NULL DEFAULT 0,
908
+ updated_at TEXT NOT NULL,
909
+ PRIMARY KEY (session_id)
910
+ );
911
+
912
+ CREATE TABLE IF NOT EXISTS users (
913
+ id TEXT PRIMARY KEY
914
+ );
915
+
916
+ CREATE TABLE IF NOT EXISTS sessions (
917
+ session_id TEXT PRIMARY KEY,
918
+ user_id TEXT NOT NULL,
919
+ started_at TEXT NOT NULL,
920
+ ended_at TEXT,
921
+ status TEXT NOT NULL DEFAULT 'active',
922
+ project_path TEXT,
923
+ git_repo TEXT,
924
+ git_branch TEXT,
925
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
926
+ );
927
+
928
+ CREATE TABLE IF NOT EXISTS commits (
929
+ sha TEXT PRIMARY KEY,
930
+ session_id TEXT NOT NULL,
931
+ prompt_id TEXT,
932
+ message TEXT,
933
+ lines_added INTEGER NOT NULL DEFAULT 0,
934
+ lines_removed INTEGER NOT NULL DEFAULT 0,
935
+ chain_cost_usd REAL NOT NULL DEFAULT 0,
936
+ committed_at TEXT
937
+ );
938
+
939
+ CREATE INDEX IF NOT EXISTS idx_commits_session ON commits(session_id);
940
+ `;
941
+ function toJsonArray(value) {
942
+ return JSON.stringify(value);
943
+ }
944
+ function fromJsonArray(value) {
945
+ if (typeof value !== "string") return [];
946
+ try {
947
+ const parsed = JSON.parse(value);
948
+ if (!Array.isArray(parsed)) return [];
949
+ return parsed.filter((item) => typeof item === "string");
950
+ } catch {
951
+ return [];
952
+ }
953
+ }
954
+ function fromJsonObject(value) {
955
+ if (typeof value !== "string") return {};
956
+ try {
957
+ const parsed = JSON.parse(value);
958
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
959
+ const result = {};
960
+ for (const [k, v] of Object.entries(parsed)) {
961
+ if (typeof v === "string") result[k] = v;
962
+ }
963
+ return result;
964
+ } catch {
965
+ return {};
966
+ }
967
+ }
968
+ var SqliteClient = class {
969
+ db;
970
+ constructor(dbPath) {
971
+ this.db = new import_better_sqlite3.default(dbPath);
972
+ this.db.pragma("journal_mode = WAL");
973
+ this.db.pragma("synchronous = NORMAL");
974
+ this.db.exec(SCHEMA_SQL);
975
+ }
976
+ async insertJsonEachRow(request) {
977
+ if (request.rows.length === 0) return;
978
+ if (request.table === "agent_events" || request.table === void 0) {
979
+ this.insertEvents(request.rows);
980
+ }
981
+ }
982
+ insertSessionTraces(rows) {
983
+ if (rows.length === 0) return;
984
+ const upsert = this.db.prepare(`
985
+ INSERT INTO session_traces
986
+ (session_id, version, started_at, ended_at, user_id, git_repo, git_branch,
987
+ prompt_count, tool_call_count, api_call_count, total_cost_usd,
988
+ total_input_tokens, total_output_tokens, lines_added, lines_removed,
989
+ models_used, tools_used, files_touched, commit_count, updated_at)
990
+ VALUES
991
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
992
+ ON CONFLICT(session_id) DO UPDATE SET
993
+ version = excluded.version,
994
+ started_at = excluded.started_at,
995
+ ended_at = excluded.ended_at,
996
+ user_id = excluded.user_id,
997
+ git_repo = excluded.git_repo,
998
+ git_branch = excluded.git_branch,
999
+ prompt_count = excluded.prompt_count,
1000
+ tool_call_count = excluded.tool_call_count,
1001
+ api_call_count = excluded.api_call_count,
1002
+ total_cost_usd = excluded.total_cost_usd,
1003
+ total_input_tokens = excluded.total_input_tokens,
1004
+ total_output_tokens = excluded.total_output_tokens,
1005
+ lines_added = excluded.lines_added,
1006
+ lines_removed = excluded.lines_removed,
1007
+ models_used = excluded.models_used,
1008
+ tools_used = excluded.tools_used,
1009
+ files_touched = excluded.files_touched,
1010
+ commit_count = excluded.commit_count,
1011
+ updated_at = excluded.updated_at
1012
+ `);
1013
+ const transaction = this.db.transaction((traceRows) => {
1014
+ for (const row of traceRows) {
1015
+ upsert.run(
1016
+ row.session_id,
1017
+ row.version,
1018
+ row.started_at,
1019
+ row.ended_at,
1020
+ row.user_id,
1021
+ row.git_repo,
1022
+ row.git_branch,
1023
+ row.prompt_count,
1024
+ row.tool_call_count,
1025
+ row.api_call_count,
1026
+ row.total_cost_usd,
1027
+ row.total_input_tokens,
1028
+ row.total_output_tokens,
1029
+ row.lines_added,
1030
+ row.lines_removed,
1031
+ toJsonArray(row.models_used),
1032
+ toJsonArray(row.tools_used),
1033
+ toJsonArray(row.files_touched),
1034
+ row.commit_count,
1035
+ row.updated_at
1036
+ );
1037
+ }
1038
+ });
1039
+ transaction(rows);
1040
+ }
1041
+ async queryJsonEachRow(query) {
1042
+ const sqliteQuery = this.translateQuery(query);
1043
+ const rawRows = this.db.prepare(sqliteQuery).all();
1044
+ return rawRows.map((raw) => this.normalizeRow(raw, query));
1045
+ }
1046
+ async upsertSessions(rows) {
1047
+ if (rows.length === 0) return;
1048
+ const insertUser = this.db.prepare("INSERT OR IGNORE INTO users (id) VALUES (?)");
1049
+ const upsertSession = this.db.prepare(`
1050
+ INSERT INTO sessions
1051
+ (session_id, user_id, started_at, ended_at, status, project_path, git_repo, git_branch)
1052
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1053
+ ON CONFLICT(session_id) DO UPDATE SET
1054
+ user_id = excluded.user_id,
1055
+ started_at = excluded.started_at,
1056
+ ended_at = excluded.ended_at,
1057
+ status = excluded.status,
1058
+ project_path = excluded.project_path,
1059
+ git_repo = excluded.git_repo,
1060
+ git_branch = excluded.git_branch,
1061
+ updated_at = datetime('now')
1062
+ `);
1063
+ const transaction = this.db.transaction((sessionRows) => {
1064
+ const userIds = new Set(sessionRows.map((r) => r.user_id));
1065
+ for (const userId of userIds) {
1066
+ insertUser.run(userId);
1067
+ }
1068
+ for (const row of sessionRows) {
1069
+ upsertSession.run(
1070
+ row.session_id,
1071
+ row.user_id,
1072
+ row.started_at,
1073
+ row.ended_at,
1074
+ row.status,
1075
+ row.project_path,
1076
+ row.git_repo,
1077
+ row.git_branch
1078
+ );
1079
+ }
1080
+ });
1081
+ transaction(rows);
1082
+ }
1083
+ async upsertCommits(rows) {
1084
+ if (rows.length === 0) return;
1085
+ const upsert = this.db.prepare(`
1086
+ INSERT INTO commits
1087
+ (sha, session_id, prompt_id, message, lines_added, lines_removed, chain_cost_usd, committed_at)
1088
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1089
+ ON CONFLICT(sha) DO UPDATE SET
1090
+ session_id = excluded.session_id,
1091
+ prompt_id = excluded.prompt_id,
1092
+ message = excluded.message,
1093
+ lines_added = excluded.lines_added,
1094
+ lines_removed = excluded.lines_removed,
1095
+ chain_cost_usd = excluded.chain_cost_usd,
1096
+ committed_at = excluded.committed_at
1097
+ `);
1098
+ const transaction = this.db.transaction((commitRows) => {
1099
+ for (const row of commitRows) {
1100
+ upsert.run(
1101
+ row.sha,
1102
+ row.session_id,
1103
+ row.prompt_id,
1104
+ row.message,
1105
+ row.lines_added,
1106
+ row.lines_removed,
1107
+ row.chain_cost_usd,
1108
+ row.committed_at
1109
+ );
1110
+ }
1111
+ });
1112
+ transaction(rows);
1113
+ }
1114
+ listCommitsBySessionId(sessionId) {
1115
+ const rows = this.db.prepare(
1116
+ "SELECT sha, session_id, prompt_id, message, lines_added, lines_removed, committed_at FROM commits WHERE session_id = ? ORDER BY committed_at ASC"
1117
+ ).all(sessionId);
1118
+ return rows;
1119
+ }
1120
+ listSessionTraces(limit = 200) {
1121
+ const rows = this.db.prepare(
1122
+ `SELECT * FROM session_traces ORDER BY updated_at DESC LIMIT ?`
1123
+ ).all(limit);
1124
+ return rows.map((raw) => ({
1125
+ session_id: raw["session_id"],
1126
+ version: raw["version"],
1127
+ started_at: raw["started_at"],
1128
+ ended_at: raw["ended_at"] ?? null,
1129
+ user_id: raw["user_id"],
1130
+ git_repo: raw["git_repo"] ?? null,
1131
+ git_branch: raw["git_branch"] ?? null,
1132
+ prompt_count: raw["prompt_count"],
1133
+ tool_call_count: raw["tool_call_count"],
1134
+ api_call_count: raw["api_call_count"],
1135
+ total_cost_usd: raw["total_cost_usd"],
1136
+ total_input_tokens: raw["total_input_tokens"],
1137
+ total_output_tokens: raw["total_output_tokens"],
1138
+ lines_added: raw["lines_added"],
1139
+ lines_removed: raw["lines_removed"],
1140
+ models_used: fromJsonArray(raw["models_used"]),
1141
+ tools_used: fromJsonArray(raw["tools_used"]),
1142
+ files_touched: fromJsonArray(raw["files_touched"]),
1143
+ commit_count: raw["commit_count"],
1144
+ updated_at: raw["updated_at"]
1145
+ }));
1146
+ }
1147
+ listEventsBySessionId(sessionId, limit = 2e3) {
1148
+ const rows = this.db.prepare(
1149
+ `SELECT event_id, event_type, event_timestamp, session_id, prompt_id,
1150
+ tool_success, tool_name, tool_duration_ms, model, cost_usd,
1151
+ input_tokens, output_tokens, attributes
1152
+ FROM agent_events
1153
+ WHERE session_id = ?
1154
+ ORDER BY event_timestamp ASC
1155
+ LIMIT ?`
1156
+ ).all(sessionId, limit);
1157
+ return rows.map((raw) => ({
1158
+ event_id: raw["event_id"],
1159
+ event_type: raw["event_type"],
1160
+ event_timestamp: raw["event_timestamp"],
1161
+ session_id: raw["session_id"],
1162
+ prompt_id: raw["prompt_id"] ?? null,
1163
+ tool_success: raw["tool_success"],
1164
+ tool_name: raw["tool_name"] ?? null,
1165
+ tool_duration_ms: raw["tool_duration_ms"],
1166
+ model: raw["model"] ?? null,
1167
+ cost_usd: raw["cost_usd"],
1168
+ input_tokens: raw["input_tokens"],
1169
+ output_tokens: raw["output_tokens"],
1170
+ attributes: fromJsonObject(raw["attributes"])
1171
+ }));
1172
+ }
1173
+ listDailyCosts(limit = 30) {
1174
+ const rows = this.db.prepare(`
1175
+ SELECT
1176
+ substr(started_at, 1, 10) AS metric_date,
1177
+ COUNT(DISTINCT session_id) AS sessions_count,
1178
+ COALESCE(SUM(total_cost_usd), 0) AS total_cost_usd
1179
+ FROM session_traces
1180
+ GROUP BY metric_date
1181
+ ORDER BY metric_date ASC
1182
+ LIMIT ?
1183
+ `).all(limit);
1184
+ return rows.map((raw) => ({
1185
+ date: raw["metric_date"],
1186
+ totalCostUsd: Number(raw["total_cost_usd"] ?? 0),
1187
+ sessionCount: Number(raw["sessions_count"] ?? 0)
1188
+ }));
1189
+ }
1190
+ close() {
1191
+ this.db.close();
1192
+ }
1193
+ insertEvents(rows) {
1194
+ const insert = this.db.prepare(`
1195
+ INSERT INTO agent_events
1196
+ (event_id, event_type, event_timestamp, session_id, prompt_id, user_id, source, agent_type,
1197
+ tool_name, tool_success, tool_duration_ms, model, cost_usd, input_tokens, output_tokens,
1198
+ api_duration_ms, lines_added, lines_removed, files_changed, commit_sha, attributes)
1199
+ VALUES
1200
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1201
+ `);
1202
+ const transaction = this.db.transaction((eventRows) => {
1203
+ for (const row of eventRows) {
1204
+ insert.run(
1205
+ row.event_id,
1206
+ row.event_type,
1207
+ row.event_timestamp,
1208
+ row.session_id,
1209
+ row.prompt_id,
1210
+ row.user_id,
1211
+ row.source,
1212
+ row.agent_type,
1213
+ row.tool_name,
1214
+ row.tool_success,
1215
+ row.tool_duration_ms,
1216
+ row.model,
1217
+ row.cost_usd,
1218
+ row.input_tokens,
1219
+ row.output_tokens,
1220
+ row.api_duration_ms,
1221
+ row.lines_added,
1222
+ row.lines_removed,
1223
+ toJsonArray(row.files_changed),
1224
+ row.commit_sha,
1225
+ JSON.stringify(row.attributes)
1226
+ );
1227
+ }
1228
+ });
1229
+ transaction(rows);
1230
+ }
1231
+ translateQuery(query) {
1232
+ let q = query;
1233
+ q = q.replace(/\bFINAL\b/g, "");
1234
+ q = q.replace(/::jsonb/g, "");
1235
+ return q.trim();
1236
+ }
1237
+ normalizeRow(raw, originalQuery) {
1238
+ const isSessionTrace = originalQuery.includes("session_traces");
1239
+ const isEvent = originalQuery.includes("agent_events");
1240
+ const isDailyCost = originalQuery.includes("daily_user_metrics") || originalQuery.includes("metric_date");
1241
+ if (isSessionTrace) {
1242
+ return {
1243
+ ...raw,
1244
+ models_used: fromJsonArray(raw["models_used"]),
1245
+ tools_used: fromJsonArray(raw["tools_used"]),
1246
+ files_touched: fromJsonArray(raw["files_touched"])
1247
+ };
1248
+ }
1249
+ if (isEvent) {
1250
+ return {
1251
+ ...raw,
1252
+ attributes: fromJsonObject(raw["attributes"])
1253
+ };
1254
+ }
1255
+ if (isDailyCost) {
1256
+ return raw;
1257
+ }
1258
+ return raw;
1259
+ }
1260
+ };
1261
+
1262
+ // packages/platform/src/clickhouse-datetime.ts
1263
+ var DATE_TIME64_FRACTION_DIGITS = 3;
1264
+ function pad(value, size) {
1265
+ return value.toString().padStart(size, "0");
1266
+ }
1267
+ function formatUtcDateTime64(timestampMs) {
1268
+ const date = new Date(timestampMs);
1269
+ const year = date.getUTCFullYear();
1270
+ const month = pad(date.getUTCMonth() + 1, 2);
1271
+ const day = pad(date.getUTCDate(), 2);
1272
+ const hours = pad(date.getUTCHours(), 2);
1273
+ const minutes = pad(date.getUTCMinutes(), 2);
1274
+ const seconds = pad(date.getUTCSeconds(), 2);
1275
+ const milliseconds = pad(date.getUTCMilliseconds(), DATE_TIME64_FRACTION_DIGITS);
1276
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
1277
+ }
1278
+ function toClickHouseDateTime64(value) {
1279
+ const timestampMs = Date.parse(value);
1280
+ if (Number.isNaN(timestampMs)) {
1281
+ throw new Error(`Invalid date-time value for ClickHouse DateTime64: ${value}`);
1282
+ }
1283
+ return formatUtcDateTime64(timestampMs);
1284
+ }
1285
+
1286
+ // packages/platform/src/clickhouse-uuid.ts
1287
+ var import_node_crypto = require("node:crypto");
1288
+ function bytesToUuid(bytes) {
1289
+ const hex = Buffer.from(bytes).toString("hex");
1290
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
1291
+ }
1292
+ function toDeterministicUuid(input) {
1293
+ const digest = (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest();
1294
+ const bytes = Uint8Array.from(digest.subarray(0, 16));
1295
+ const versionByte = bytes[6];
1296
+ const variantByte = bytes[8];
1297
+ if (versionByte === void 0 || variantByte === void 0) {
1298
+ throw new Error("Failed to derive UUID bytes from hash digest");
1299
+ }
1300
+ bytes[6] = versionByte & 15 | 80;
1301
+ bytes[8] = variantByte & 63 | 128;
1302
+ return bytesToUuid(bytes);
1303
+ }
1304
+
1305
+ // packages/platform/src/clickhouse-event-writer.ts
1306
+ var DEFAULT_TABLE_NAME = "agent_events";
1307
+ function pickUnknown(payload, snakeCaseKey, camelCaseKey) {
1308
+ if (snakeCaseKey in payload) {
1309
+ return payload[snakeCaseKey];
1310
+ }
1311
+ if (camelCaseKey in payload) {
1312
+ return payload[camelCaseKey];
1313
+ }
1314
+ return void 0;
1315
+ }
1316
+ function pickString(payload, snakeCaseKey, camelCaseKey) {
1317
+ const value = pickUnknown(payload, snakeCaseKey, camelCaseKey);
1318
+ if (typeof value === "string" && value.length > 0) {
1319
+ return value;
1320
+ }
1321
+ return void 0;
1322
+ }
1323
+ function pickNumber(payload, snakeCaseKey, camelCaseKey) {
1324
+ const value = pickUnknown(payload, snakeCaseKey, camelCaseKey);
1325
+ if (typeof value === "number" && Number.isFinite(value)) {
1326
+ return value;
1327
+ }
1328
+ return void 0;
1329
+ }
1330
+ function pickBoolean(payload, snakeCaseKey, camelCaseKey) {
1331
+ const value = pickUnknown(payload, snakeCaseKey, camelCaseKey);
1332
+ if (typeof value === "boolean") {
1333
+ return value;
1334
+ }
1335
+ if (value === 1) {
1336
+ return true;
1337
+ }
1338
+ if (value === 0) {
1339
+ return false;
1340
+ }
1341
+ return void 0;
1342
+ }
1343
+ function pickStringArray(payload, snakeCaseKey, camelCaseKey) {
1344
+ const value = pickUnknown(payload, snakeCaseKey, camelCaseKey);
1345
+ if (!Array.isArray(value)) {
1346
+ return [];
1347
+ }
1348
+ const values = [];
1349
+ value.forEach((item) => {
1350
+ if (typeof item === "string" && item.length > 0) {
1351
+ values.push(item);
1352
+ }
1353
+ });
1354
+ return values;
1355
+ }
1356
+ function buildAttributes(event) {
1357
+ const attributes = {};
1358
+ const payload = event.payload;
1359
+ if (event.attributes !== void 0) {
1360
+ Object.keys(event.attributes).forEach((key) => {
1361
+ const value = event.attributes?.[key];
1362
+ if (typeof value === "string") {
1363
+ attributes[key] = value;
1364
+ }
1365
+ });
1366
+ }
1367
+ attributes["privacy_tier"] = String(event.privacyTier);
1368
+ attributes["event_id_raw"] = event.eventId;
1369
+ if (event.sourceVersion !== void 0) {
1370
+ attributes["source_version"] = event.sourceVersion;
1371
+ }
1372
+ if (event.privacyTier >= 2) {
1373
+ const promptText = pickString(payload, "prompt_text", "promptText");
1374
+ if (promptText !== void 0) {
1375
+ attributes["prompt_text"] = promptText;
1376
+ }
1377
+ const command = pickString(payload, "command", "command");
1378
+ if (command !== void 0) {
1379
+ attributes["command"] = command;
1380
+ }
1381
+ const toolInput = pickUnknown(payload, "tool_input", "toolInput");
1382
+ if (toolInput !== void 0) {
1383
+ const inputRecord = typeof toolInput === "object" && toolInput !== null && !Array.isArray(toolInput) ? toolInput : void 0;
1384
+ const filePath = inputRecord !== void 0 ? typeof inputRecord["file_path"] === "string" ? inputRecord["file_path"] : typeof inputRecord["filePath"] === "string" ? inputRecord["filePath"] : void 0 : void 0;
1385
+ if (filePath !== void 0) {
1386
+ attributes["file_path"] = filePath;
1387
+ }
1388
+ const toolName = pickString(payload, "tool_name", "toolName");
1389
+ if (toolName === "Edit" && inputRecord !== void 0) {
1390
+ const oldStr = typeof inputRecord["old_string"] === "string" ? inputRecord["old_string"] : void 0;
1391
+ const newStr = typeof inputRecord["new_string"] === "string" ? inputRecord["new_string"] : void 0;
1392
+ if (oldStr !== void 0) {
1393
+ attributes["old_string"] = oldStr.length > 2e3 ? oldStr.slice(0, 1997) + "..." : oldStr;
1394
+ }
1395
+ if (newStr !== void 0) {
1396
+ attributes["new_string"] = newStr.length > 2e3 ? newStr.slice(0, 1997) + "..." : newStr;
1397
+ }
1398
+ }
1399
+ if (toolName === "Write" && inputRecord !== void 0) {
1400
+ const content = typeof inputRecord["content"] === "string" ? inputRecord["content"] : void 0;
1401
+ if (content !== void 0) {
1402
+ attributes["write_content"] = content.length > 5e3 ? content.slice(0, 4997) + "..." : content;
1403
+ }
1404
+ }
1405
+ const serialized = typeof toolInput === "string" ? toolInput : JSON.stringify(toolInput);
1406
+ attributes["tool_input"] = serialized.length > 500 ? serialized.slice(0, 497) + "..." : serialized;
1407
+ }
1408
+ const responseText = pickString(payload, "response_text", "responseText");
1409
+ if (responseText !== void 0) {
1410
+ attributes["response_text"] = responseText.length > 2e3 ? responseText.slice(0, 1997) + "..." : responseText;
1411
+ }
1412
+ }
1413
+ return attributes;
1414
+ }
1415
+ function toClickHouseAgentEventRow(event) {
1416
+ const payload = event.payload;
1417
+ const toolSuccess = pickBoolean(payload, "tool_success", "toolSuccess");
1418
+ return {
1419
+ event_id: toDeterministicUuid(event.eventId),
1420
+ event_type: event.eventType,
1421
+ event_timestamp: toClickHouseDateTime64(event.eventTimestamp),
1422
+ session_id: event.sessionId,
1423
+ prompt_id: event.promptId ?? null,
1424
+ user_id: pickString(payload, "user_id", "userId") ?? "unknown_user",
1425
+ source: event.source,
1426
+ agent_type: pickString(payload, "agent_type", "agentType") ?? "claude_code",
1427
+ tool_name: pickString(payload, "tool_name", "toolName") ?? null,
1428
+ tool_success: toolSuccess === void 0 ? null : toolSuccess ? 1 : 0,
1429
+ tool_duration_ms: pickNumber(payload, "tool_duration_ms", "toolDurationMs") ?? null,
1430
+ model: pickString(payload, "model", "model") ?? null,
1431
+ cost_usd: pickNumber(payload, "cost_usd", "costUsd") ?? null,
1432
+ input_tokens: pickNumber(payload, "input_tokens", "inputTokens") ?? null,
1433
+ output_tokens: pickNumber(payload, "output_tokens", "outputTokens") ?? null,
1434
+ api_duration_ms: pickNumber(payload, "api_duration_ms", "apiDurationMs") ?? null,
1435
+ lines_added: pickNumber(payload, "lines_added", "linesAdded") ?? null,
1436
+ lines_removed: pickNumber(payload, "lines_removed", "linesRemoved") ?? null,
1437
+ files_changed: pickStringArray(payload, "files_changed", "filesChanged"),
1438
+ commit_sha: pickString(payload, "commit_sha", "commitSha") ?? null,
1439
+ attributes: buildAttributes(event)
1440
+ };
1441
+ }
1442
+ var ClickHouseEventWriter = class {
1443
+ tableName;
1444
+ client;
1445
+ constructor(client, options = {}) {
1446
+ this.client = client;
1447
+ this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
1448
+ }
1449
+ async writeEvent(event) {
1450
+ return this.writeEvents([event]);
1451
+ }
1452
+ async writeEvents(events) {
1453
+ if (events.length === 0) {
1454
+ return {
1455
+ tableName: this.tableName,
1456
+ writtenRows: 0
1457
+ };
1458
+ }
1459
+ const rows = events.map(toClickHouseAgentEventRow);
1460
+ await this.client.insertJsonEachRow({
1461
+ table: this.tableName,
1462
+ rows
1463
+ });
1464
+ return {
1465
+ tableName: this.tableName,
1466
+ writtenRows: rows.length
1467
+ };
1468
+ }
1469
+ };
1470
+
1471
+ // packages/platform/src/clickhouse-session-trace-writer.ts
1472
+ var DEFAULT_TABLE_NAME2 = "session_traces";
1473
+ function toNonNegativeInteger(value) {
1474
+ if (!Number.isFinite(value)) {
1475
+ return 0;
1476
+ }
1477
+ const normalized = Math.trunc(value);
1478
+ if (normalized < 0) {
1479
+ return 0;
1480
+ }
1481
+ return normalized;
1482
+ }
1483
+ function toUniqueStringArray(values) {
1484
+ const seen = /* @__PURE__ */ new Set();
1485
+ const output = [];
1486
+ values.forEach((value) => {
1487
+ if (value.length === 0) {
1488
+ return;
1489
+ }
1490
+ if (seen.has(value)) {
1491
+ return;
1492
+ }
1493
+ seen.add(value);
1494
+ output.push(value);
1495
+ });
1496
+ return output;
1497
+ }
1498
+ function normalizeVersion(rawVersion) {
1499
+ if (!Number.isFinite(rawVersion)) {
1500
+ return 1;
1501
+ }
1502
+ const version = Math.trunc(rawVersion);
1503
+ if (version < 1) {
1504
+ return 1;
1505
+ }
1506
+ return version;
1507
+ }
1508
+ function toClickHouseSessionTraceRow(trace, version, updatedAt) {
1509
+ return {
1510
+ session_id: trace.sessionId,
1511
+ version: normalizeVersion(version),
1512
+ started_at: toClickHouseDateTime64(trace.startedAt),
1513
+ ended_at: trace.endedAt === void 0 ? null : toClickHouseDateTime64(trace.endedAt),
1514
+ user_id: trace.user.id,
1515
+ git_repo: trace.environment.gitRepo ?? null,
1516
+ git_branch: trace.environment.gitBranch ?? null,
1517
+ prompt_count: toNonNegativeInteger(trace.metrics.promptCount),
1518
+ tool_call_count: toNonNegativeInteger(trace.metrics.toolCallCount),
1519
+ api_call_count: toNonNegativeInteger(trace.metrics.apiCallCount),
1520
+ total_cost_usd: Number.isFinite(trace.metrics.totalCostUsd) ? trace.metrics.totalCostUsd : 0,
1521
+ total_input_tokens: toNonNegativeInteger(trace.metrics.totalInputTokens),
1522
+ total_output_tokens: toNonNegativeInteger(trace.metrics.totalOutputTokens),
1523
+ lines_added: Math.trunc(trace.metrics.linesAdded),
1524
+ lines_removed: Math.trunc(trace.metrics.linesRemoved),
1525
+ models_used: toUniqueStringArray(trace.metrics.modelsUsed),
1526
+ tools_used: toUniqueStringArray(trace.metrics.toolsUsed),
1527
+ files_touched: toUniqueStringArray(trace.metrics.filesTouched),
1528
+ commit_count: toNonNegativeInteger(trace.git.commits.length),
1529
+ updated_at: toClickHouseDateTime64(updatedAt)
1530
+ };
1531
+ }
1532
+ var ClickHouseSessionTraceWriter = class {
1533
+ tableName;
1534
+ client;
1535
+ versionProvider;
1536
+ updatedAtProvider;
1537
+ constructor(client, options = {}) {
1538
+ this.client = client;
1539
+ this.tableName = options.tableName ?? DEFAULT_TABLE_NAME2;
1540
+ this.versionProvider = options.versionProvider ?? (() => Date.now());
1541
+ this.updatedAtProvider = options.updatedAtProvider ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1542
+ }
1543
+ async writeTrace(trace) {
1544
+ return this.writeTraces([trace]);
1545
+ }
1546
+ async writeTraces(traces) {
1547
+ if (traces.length === 0) {
1548
+ return {
1549
+ tableName: this.tableName,
1550
+ writtenRows: 0
1551
+ };
1552
+ }
1553
+ const baseVersion = normalizeVersion(this.versionProvider());
1554
+ const updatedAt = this.updatedAtProvider();
1555
+ const rows = traces.map(
1556
+ (trace, index) => toClickHouseSessionTraceRow(trace, baseVersion + index, updatedAt)
1557
+ );
1558
+ await this.client.insertJsonEachRow({
1559
+ table: this.tableName,
1560
+ rows
1561
+ });
1562
+ return {
1563
+ tableName: this.tableName,
1564
+ writtenRows: rows.length
1565
+ };
1566
+ }
1567
+ };
1568
+
1569
+ // packages/platform/src/postgres-writer.ts
1570
+ function toNullableString(value) {
1571
+ if (typeof value !== "string" || value.length === 0) {
1572
+ return null;
1573
+ }
1574
+ return value;
1575
+ }
1576
+ function toNonNegativeInteger2(value) {
1577
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
1578
+ return 0;
1579
+ }
1580
+ return Math.floor(value);
1581
+ }
1582
+ function toSessionStatus(trace) {
1583
+ return trace.endedAt !== void 0 ? "completed" : "active";
1584
+ }
1585
+ function toPostgresSessionRow(trace) {
1586
+ return {
1587
+ session_id: trace.sessionId,
1588
+ user_id: trace.user.id,
1589
+ started_at: trace.startedAt,
1590
+ ended_at: toNullableString(trace.endedAt),
1591
+ status: toSessionStatus(trace),
1592
+ project_path: toNullableString(trace.environment.projectPath),
1593
+ git_repo: toNullableString(trace.environment.gitRepo),
1594
+ git_branch: toNullableString(trace.environment.gitBranch)
1595
+ };
1596
+ }
1597
+ function toPostgresCommitRow(trace, commit) {
1598
+ return {
1599
+ sha: commit.sha,
1600
+ session_id: trace.sessionId,
1601
+ prompt_id: toNullableString(commit.promptId),
1602
+ message: toNullableString(commit.message),
1603
+ lines_added: toNonNegativeInteger2(commit.linesAdded),
1604
+ lines_removed: toNonNegativeInteger2(commit.linesRemoved),
1605
+ chain_cost_usd: 0,
1606
+ committed_at: toNullableString(commit.committedAt)
1607
+ };
1608
+ }
1609
+ function toPostgresCommitRows(trace) {
1610
+ return trace.git.commits.map((commit) => toPostgresCommitRow(trace, commit));
1611
+ }
1612
+ function dedupeBySessionId(rows) {
1613
+ const bySession = /* @__PURE__ */ new Map();
1614
+ rows.forEach((row) => {
1615
+ bySession.set(row.session_id, row);
1616
+ });
1617
+ return [...bySession.values()];
1618
+ }
1619
+ function dedupeBySha(rows) {
1620
+ const bySha = /* @__PURE__ */ new Map();
1621
+ rows.forEach((row) => {
1622
+ bySha.set(row.sha, row);
1623
+ });
1624
+ return [...bySha.values()];
1625
+ }
1626
+ var PostgresSessionWriter = class {
1627
+ client;
1628
+ constructor(client) {
1629
+ this.client = client;
1630
+ }
1631
+ async writeTrace(trace) {
1632
+ return this.writeTraces([trace]);
1633
+ }
1634
+ async writeTraces(traces) {
1635
+ if (traces.length === 0) {
1636
+ return {
1637
+ writtenSessions: 0,
1638
+ writtenCommits: 0
1639
+ };
1640
+ }
1641
+ const sessions = dedupeBySessionId(traces.map(toPostgresSessionRow));
1642
+ const commitRows = dedupeBySha(traces.flatMap((trace) => toPostgresCommitRows(trace)));
1643
+ const sessionsPromise = this.client.upsertSessions(sessions);
1644
+ const commitsPromise = this.client.upsertCommits(commitRows);
1645
+ await Promise.all([sessionsPromise, commitsPromise]);
1646
+ return {
1647
+ writtenSessions: sessions.length,
1648
+ writtenCommits: commitRows.length
1649
+ };
1650
+ }
1651
+ };
1652
+
1653
+ // packages/platform/src/clickhouse-session-trace-reader.ts
1654
+ var SESSION_TRACE_SELECT_COLUMNS = [
1655
+ "session_id",
1656
+ "version",
1657
+ "started_at",
1658
+ "ended_at",
1659
+ "user_id",
1660
+ "git_repo",
1661
+ "git_branch",
1662
+ "prompt_count",
1663
+ "tool_call_count",
1664
+ "api_call_count",
1665
+ "total_cost_usd",
1666
+ "total_input_tokens",
1667
+ "total_output_tokens",
1668
+ "lines_added",
1669
+ "lines_removed",
1670
+ "models_used",
1671
+ "tools_used",
1672
+ "files_touched",
1673
+ "commit_count",
1674
+ "updated_at"
1675
+ ].join(", ");
1676
+ function toNonNegativeInteger3(value) {
1677
+ if (!Number.isFinite(value)) {
1678
+ return 0;
1679
+ }
1680
+ const normalized = Math.trunc(value);
1681
+ if (normalized < 0) {
1682
+ return 0;
1683
+ }
1684
+ return normalized;
1685
+ }
1686
+ function toUniqueStrings(values) {
1687
+ const seen = /* @__PURE__ */ new Set();
1688
+ const output = [];
1689
+ values.forEach((value) => {
1690
+ if (value.length === 0 || seen.has(value)) {
1691
+ return;
1692
+ }
1693
+ seen.add(value);
1694
+ output.push(value);
1695
+ });
1696
+ return output;
1697
+ }
1698
+ function toActiveDurationMs(startedAt, endedAt) {
1699
+ if (endedAt === null) {
1700
+ return 0;
1701
+ }
1702
+ const started = Date.parse(startedAt);
1703
+ const ended = Date.parse(endedAt);
1704
+ if (Number.isNaN(started) || Number.isNaN(ended)) {
1705
+ return 0;
1706
+ }
1707
+ return Math.max(0, ended - started);
1708
+ }
1709
+ function buildPlaceholderCommits(count) {
1710
+ if (count <= 0) return [];
1711
+ const result = [];
1712
+ for (let i = 0; i < count; i++) {
1713
+ result.push({ sha: `placeholder_${String(i)}` });
1714
+ }
1715
+ return result;
1716
+ }
1717
+ function toAgentSessionTraceFromClickHouseRow(row) {
1718
+ return {
1719
+ sessionId: row.session_id,
1720
+ agentType: "claude_code",
1721
+ user: {
1722
+ id: row.user_id
1723
+ },
1724
+ environment: {
1725
+ ...row.git_repo !== null ? { gitRepo: row.git_repo } : {},
1726
+ ...row.git_branch !== null ? { gitBranch: row.git_branch } : {}
1727
+ },
1728
+ startedAt: row.started_at,
1729
+ ...row.ended_at !== null ? { endedAt: row.ended_at } : {},
1730
+ activeDurationMs: toActiveDurationMs(row.started_at, row.ended_at),
1731
+ timeline: [],
1732
+ metrics: {
1733
+ promptCount: toNonNegativeInteger3(row.prompt_count),
1734
+ apiCallCount: toNonNegativeInteger3(row.api_call_count),
1735
+ toolCallCount: toNonNegativeInteger3(row.tool_call_count),
1736
+ totalCostUsd: Number.isFinite(row.total_cost_usd) ? row.total_cost_usd : 0,
1737
+ totalInputTokens: toNonNegativeInteger3(row.total_input_tokens),
1738
+ totalOutputTokens: toNonNegativeInteger3(row.total_output_tokens),
1739
+ linesAdded: Math.trunc(row.lines_added),
1740
+ linesRemoved: Math.trunc(row.lines_removed),
1741
+ filesTouched: toUniqueStrings(row.files_touched),
1742
+ modelsUsed: toUniqueStrings(row.models_used),
1743
+ toolsUsed: toUniqueStrings(row.tools_used)
1744
+ },
1745
+ git: {
1746
+ commits: buildPlaceholderCommits(toNonNegativeInteger3(row.commit_count)),
1747
+ pullRequests: []
1748
+ }
1749
+ };
1750
+ }
1751
+
1752
+ // packages/platform/src/clickhouse-event-reader.ts
1753
+ var EVENT_SELECT_COLUMNS = [
1754
+ "event_id",
1755
+ "event_type",
1756
+ "event_timestamp",
1757
+ "session_id",
1758
+ "prompt_id",
1759
+ "tool_success",
1760
+ "tool_name",
1761
+ "tool_duration_ms",
1762
+ "model",
1763
+ "cost_usd",
1764
+ "input_tokens",
1765
+ "output_tokens",
1766
+ "attributes"
1767
+ ].join(", ");
1768
+ function toNumber(value) {
1769
+ if (typeof value === "number" && Number.isFinite(value)) {
1770
+ return value;
1771
+ }
1772
+ if (typeof value === "string" && value.length > 0) {
1773
+ const parsed = Number(value);
1774
+ if (Number.isFinite(parsed)) {
1775
+ return parsed;
1776
+ }
1777
+ }
1778
+ return void 0;
1779
+ }
1780
+ function normalizeTimestamp(value) {
1781
+ if (value.includes("T")) {
1782
+ return value;
1783
+ }
1784
+ const normalized = value.trim();
1785
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,6})?$/.test(normalized)) {
1786
+ return `${normalized.replace(" ", "T")}Z`;
1787
+ }
1788
+ return value;
1789
+ }
1790
+ function readRawEventId(attributes) {
1791
+ const raw = attributes["event_id_raw"];
1792
+ if (typeof raw === "string" && raw.length > 0) {
1793
+ return raw;
1794
+ }
1795
+ return void 0;
1796
+ }
1797
+ function readStatus(row) {
1798
+ if (row.tool_success === 1) {
1799
+ return "success";
1800
+ }
1801
+ if (row.tool_success === 0) {
1802
+ return "error";
1803
+ }
1804
+ if (row.event_type.toLowerCase().includes("error")) {
1805
+ return "error";
1806
+ }
1807
+ return void 0;
1808
+ }
1809
+ function toTimelineEventFromClickHouseRow(row) {
1810
+ const id = readRawEventId(row.attributes) ?? row.event_id;
1811
+ const costUsd = toNumber(row.cost_usd);
1812
+ const inputTokens = toNumber(row.input_tokens);
1813
+ const outputTokens = toNumber(row.output_tokens);
1814
+ const status2 = readStatus(row);
1815
+ const details = {};
1816
+ if (row.tool_name !== null) {
1817
+ details["toolName"] = row.tool_name;
1818
+ }
1819
+ const toolDurationMs = toNumber(row.tool_duration_ms);
1820
+ if (toolDurationMs !== void 0) {
1821
+ details["toolDurationMs"] = toolDurationMs;
1822
+ }
1823
+ if (row.model !== null) {
1824
+ details["model"] = row.model;
1825
+ }
1826
+ const hookName = row.attributes["hook_name"];
1827
+ if (hookName !== void 0) {
1828
+ details["hookName"] = hookName;
1829
+ }
1830
+ const sourceVersion = row.attributes["source_version"];
1831
+ if (sourceVersion !== void 0) {
1832
+ details["sourceVersion"] = sourceVersion;
1833
+ }
1834
+ const promptText = row.attributes["prompt_text"];
1835
+ if (promptText !== void 0) {
1836
+ details["promptText"] = promptText;
1837
+ }
1838
+ const command = row.attributes["command"];
1839
+ if (command !== void 0) {
1840
+ details["command"] = command;
1841
+ }
1842
+ const filePath = row.attributes["file_path"];
1843
+ if (filePath !== void 0) {
1844
+ details["filePath"] = filePath;
1845
+ }
1846
+ const toolInput = row.attributes["tool_input"];
1847
+ if (toolInput !== void 0) {
1848
+ try {
1849
+ details["toolInput"] = JSON.parse(toolInput);
1850
+ } catch {
1851
+ details["toolInput"] = toolInput;
1852
+ }
1853
+ }
1854
+ const oldString = row.attributes["old_string"];
1855
+ if (oldString !== void 0) {
1856
+ details["oldString"] = oldString;
1857
+ }
1858
+ const newString = row.attributes["new_string"];
1859
+ if (newString !== void 0) {
1860
+ details["newString"] = newString;
1861
+ }
1862
+ const writeContent = row.attributes["write_content"];
1863
+ if (writeContent !== void 0) {
1864
+ details["writeContent"] = writeContent;
1865
+ }
1866
+ const responseText = row.attributes["response_text"];
1867
+ if (responseText !== void 0) {
1868
+ details["responseText"] = responseText;
1869
+ }
1870
+ return {
1871
+ id,
1872
+ type: row.event_type,
1873
+ timestamp: normalizeTimestamp(row.event_timestamp),
1874
+ ...row.prompt_id !== null ? { promptId: row.prompt_id } : {},
1875
+ ...status2 !== void 0 ? { status: status2 } : {},
1876
+ ...costUsd !== void 0 ? { costUsd } : {},
1877
+ ...inputTokens !== void 0 && outputTokens !== void 0 ? {
1878
+ tokens: {
1879
+ input: inputTokens,
1880
+ output: outputTokens
1881
+ }
1882
+ } : {},
1883
+ ...Object.keys(details).length > 0 ? { details } : {}
1884
+ };
1885
+ }
1886
+
1887
+ // packages/runtime/src/runtime.ts
1888
+ var import_node_http2 = __toESM(require("node:http"));
1889
+
1890
+ // packages/api/src/mapper.ts
1891
+ function toSessionSummary(trace) {
1892
+ return {
1893
+ sessionId: trace.sessionId,
1894
+ userId: trace.user.id,
1895
+ gitRepo: trace.environment.gitRepo ?? null,
1896
+ gitBranch: trace.environment.gitBranch ?? null,
1897
+ startedAt: trace.startedAt,
1898
+ endedAt: trace.endedAt ?? null,
1899
+ promptCount: trace.metrics.promptCount,
1900
+ toolCallCount: trace.metrics.toolCallCount,
1901
+ totalCostUsd: trace.metrics.totalCostUsd,
1902
+ commitCount: trace.git.commits.length,
1903
+ linesAdded: trace.metrics.linesAdded,
1904
+ linesRemoved: trace.metrics.linesRemoved
1905
+ };
1906
+ }
1907
+
1908
+ // packages/api/src/handler.ts
1909
+ function buildError(message) {
1910
+ return {
1911
+ status: "error",
1912
+ message
1913
+ };
1914
+ }
1915
+ function buildHealth(startedAtMs) {
1916
+ return {
1917
+ status: "ok",
1918
+ service: "api",
1919
+ uptimeSec: Math.floor((Date.now() - startedAtMs) / 1e3)
1920
+ };
1921
+ }
1922
+ function parseFilters(searchParams) {
1923
+ const userId = searchParams.get("userId");
1924
+ const repo = searchParams.get("repo");
1925
+ return {
1926
+ ...userId !== null ? { userId } : {},
1927
+ ...repo !== null ? { repo } : {}
1928
+ };
1929
+ }
1930
+ function parseSessionPath(pathname) {
1931
+ return pathname.split("/").filter((segment) => segment.length > 0);
1932
+ }
1933
+ function toMetricDate(startedAt) {
1934
+ const parsed = Date.parse(startedAt);
1935
+ if (Number.isNaN(parsed)) {
1936
+ return startedAt.slice(0, 10);
1937
+ }
1938
+ return new Date(parsed).toISOString().slice(0, 10);
1939
+ }
1940
+ function buildDailyCostResponseFromTraces(dependencies, filters) {
1941
+ const traces = dependencies.repository.list(filters);
1942
+ const byDate = /* @__PURE__ */ new Map();
1943
+ traces.forEach((trace) => {
1944
+ const date = toMetricDate(trace.startedAt);
1945
+ const current = byDate.get(date) ?? {
1946
+ totalCostUsd: 0,
1947
+ sessionCount: 0,
1948
+ promptCount: 0,
1949
+ toolCallCount: 0
1950
+ };
1951
+ current.totalCostUsd += trace.metrics.totalCostUsd;
1952
+ current.sessionCount += 1;
1953
+ current.promptCount += trace.metrics.promptCount;
1954
+ current.toolCallCount += trace.metrics.toolCallCount;
1955
+ byDate.set(date, current);
1956
+ });
1957
+ const points = [...byDate.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([date, entry]) => ({
1958
+ date,
1959
+ totalCostUsd: Number(entry.totalCostUsd.toFixed(6)),
1960
+ sessionCount: entry.sessionCount,
1961
+ promptCount: entry.promptCount,
1962
+ toolCallCount: entry.toolCallCount
1963
+ }));
1964
+ return {
1965
+ status: "ok",
1966
+ points
1967
+ };
1968
+ }
1969
+ async function buildDailyCostResponse(dependencies, filters) {
1970
+ if (dependencies.dailyCostReader !== void 0) {
1971
+ try {
1972
+ const points = await dependencies.dailyCostReader.listDailyCosts(30);
1973
+ return { status: "ok", points };
1974
+ } catch {
1975
+ }
1976
+ }
1977
+ return buildDailyCostResponseFromTraces(dependencies, filters);
1978
+ }
1979
+ async function handleApiRequest(request, dependencies) {
1980
+ const parsedUrl = new URL(request.url, "http://localhost");
1981
+ const pathname = parsedUrl.pathname;
1982
+ if (request.method === "GET" && pathname === "/health") {
1983
+ return {
1984
+ statusCode: 200,
1985
+ payload: buildHealth(dependencies.startedAtMs)
1986
+ };
1987
+ }
1988
+ if (request.method === "GET" && pathname === "/v1/sessions") {
1989
+ const filters = parseFilters(parsedUrl.searchParams);
1990
+ const sessions = dependencies.repository.list(filters).map(toSessionSummary);
1991
+ const payload = {
1992
+ status: "ok",
1993
+ count: sessions.length,
1994
+ sessions
1995
+ };
1996
+ return {
1997
+ statusCode: 200,
1998
+ payload
1999
+ };
2000
+ }
2001
+ if (request.method === "GET" && pathname === "/v1/analytics/cost/daily") {
2002
+ const filters = parseFilters(parsedUrl.searchParams);
2003
+ return {
2004
+ statusCode: 200,
2005
+ payload: await buildDailyCostResponse(dependencies, filters)
2006
+ };
2007
+ }
2008
+ if (request.method === "GET") {
2009
+ const segments = parseSessionPath(pathname);
2010
+ if (segments.length >= 3 && segments[0] === "v1" && segments[1] === "sessions") {
2011
+ const sessionId = segments[2];
2012
+ if (sessionId === void 0) {
2013
+ return {
2014
+ statusCode: 404,
2015
+ payload: buildError("not found")
2016
+ };
2017
+ }
2018
+ const trace = dependencies.repository.getBySessionId(sessionId);
2019
+ if (trace === void 0) {
2020
+ return {
2021
+ statusCode: 404,
2022
+ payload: buildError("session not found")
2023
+ };
2024
+ }
2025
+ if (segments.length === 3) {
2026
+ const payload = {
2027
+ status: "ok",
2028
+ session: trace
2029
+ };
2030
+ return {
2031
+ statusCode: 200,
2032
+ payload
2033
+ };
2034
+ }
2035
+ if (segments.length === 4 && segments[3] === "timeline") {
2036
+ const payload = {
2037
+ status: "ok",
2038
+ timeline: trace.timeline
2039
+ };
2040
+ return {
2041
+ statusCode: 200,
2042
+ payload
2043
+ };
2044
+ }
2045
+ }
2046
+ }
2047
+ return {
2048
+ statusCode: 404,
2049
+ payload: buildError("not found")
2050
+ };
2051
+ }
2052
+
2053
+ // packages/api/src/http.ts
2054
+ function normalizeMethod(method) {
2055
+ if (method === "GET") {
2056
+ return method;
2057
+ }
2058
+ return void 0;
2059
+ }
2060
+ function sendJson2(res, statusCode, payload) {
2061
+ const body = JSON.stringify(payload);
2062
+ res.statusCode = statusCode;
2063
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
2064
+ res.end(body);
2065
+ }
2066
+ function parsePathname2(url) {
2067
+ try {
2068
+ const parsed = new URL(url, "http://localhost");
2069
+ return parsed.pathname;
2070
+ } catch {
2071
+ return url;
2072
+ }
2073
+ }
2074
+ function isSseSessionsRoute(method, url) {
2075
+ return method === "GET" && parsePathname2(url) === "/v1/sessions/stream";
2076
+ }
2077
+ function startSessionsSseStream(req, res, dependencies) {
2078
+ res.statusCode = 200;
2079
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
2080
+ res.setHeader("Cache-Control", "no-cache");
2081
+ res.setHeader("Connection", "keep-alive");
2082
+ res.setHeader("X-Accel-Buffering", "no");
2083
+ res.flushHeaders();
2084
+ const writeSnapshot = () => {
2085
+ const sessions = dependencies.repository.list({}).map(toSessionSummary);
2086
+ const payload = JSON.stringify({
2087
+ status: "ok",
2088
+ count: sessions.length,
2089
+ sessions,
2090
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString()
2091
+ });
2092
+ res.write(`event: sessions
2093
+ `);
2094
+ res.write(`data: ${payload}
2095
+
2096
+ `);
2097
+ };
2098
+ writeSnapshot();
2099
+ const interval = setInterval(writeSnapshot, 2e3);
2100
+ const cleanup = () => {
2101
+ clearInterval(interval);
2102
+ if (!res.writableEnded) {
2103
+ res.end();
2104
+ }
2105
+ };
2106
+ req.on("close", cleanup);
2107
+ }
2108
+ async function handleApiRawHttpRequest(request, dependencies) {
2109
+ const method = normalizeMethod(request.method);
2110
+ if (method === void 0) {
2111
+ return {
2112
+ statusCode: 405,
2113
+ payload: {
2114
+ status: "error",
2115
+ message: "method not allowed"
2116
+ }
2117
+ };
2118
+ }
2119
+ return handleApiRequest(
2120
+ {
2121
+ method,
2122
+ url: request.url
2123
+ },
2124
+ dependencies
2125
+ );
2126
+ }
2127
+ function createApiHttpHandler(dependencies) {
2128
+ return (req, res) => {
2129
+ const method = req.method ?? "GET";
2130
+ const url = req.url ?? "/";
2131
+ if (isSseSessionsRoute(method, url)) {
2132
+ startSessionsSseStream(req, res, dependencies);
2133
+ return;
2134
+ }
2135
+ void handleApiRawHttpRequest(
2136
+ {
2137
+ method,
2138
+ url
2139
+ },
2140
+ dependencies
2141
+ ).then((response) => {
2142
+ sendJson2(res, response.statusCode, response.payload);
2143
+ }).catch(() => {
2144
+ sendJson2(res, 500, { status: "error", message: "internal server error" });
2145
+ });
2146
+ };
2147
+ }
2148
+
2149
+ // packages/api/src/repository.ts
2150
+ var InMemorySessionRepository = class {
2151
+ tracesById;
2152
+ constructor(seedTraces = []) {
2153
+ this.tracesById = /* @__PURE__ */ new Map();
2154
+ seedTraces.forEach((trace) => {
2155
+ this.tracesById.set(trace.sessionId, trace);
2156
+ });
2157
+ }
2158
+ list(filters) {
2159
+ const traces = [...this.tracesById.values()];
2160
+ return traces.filter((trace) => {
2161
+ if (filters.userId !== void 0 && trace.user.id !== filters.userId) {
2162
+ return false;
2163
+ }
2164
+ if (filters.repo !== void 0 && trace.environment.gitRepo !== filters.repo) {
2165
+ return false;
2166
+ }
2167
+ return true;
2168
+ });
2169
+ }
2170
+ getBySessionId(sessionId) {
2171
+ return this.tracesById.get(sessionId);
2172
+ }
2173
+ upsert(trace) {
2174
+ this.tracesById.set(trace.sessionId, trace);
2175
+ }
2176
+ };
2177
+
2178
+ // packages/schema/src/constants.ts
2179
+ var SCHEMA_VERSION = "1.0";
2180
+ var EVENT_SOURCES = ["otel", "hook", "transcript", "git"];
2181
+ var PRIVACY_TIERS = [1, 2, 3];
2182
+
2183
+ // packages/schema/src/validators.ts
2184
+ function isRecord(value) {
2185
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2186
+ }
2187
+ function isNonEmptyString(value) {
2188
+ return typeof value === "string" && value.trim().length > 0;
2189
+ }
2190
+ function isValidIsoDate(value) {
2191
+ if (!isNonEmptyString(value)) {
2192
+ return false;
2193
+ }
2194
+ if (Number.isNaN(Date.parse(value))) {
2195
+ return false;
2196
+ }
2197
+ return value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value);
2198
+ }
2199
+ function isEventSource(value) {
2200
+ return EVENT_SOURCES.some((source) => source === value);
2201
+ }
2202
+ function isPrivacyTier(value) {
2203
+ return PRIVACY_TIERS.some((tier) => tier === value);
2204
+ }
2205
+ function addError(errors, path4, message) {
2206
+ errors.push(`${path4}: ${message}`);
2207
+ }
2208
+ function readRequiredString(source, key, errors, path4) {
2209
+ const value = source[key];
2210
+ if (!isNonEmptyString(value)) {
2211
+ addError(errors, path4, "must be a non-empty string");
2212
+ return void 0;
2213
+ }
2214
+ return value;
2215
+ }
2216
+ function readOptionalString(source, key, errors, path4) {
2217
+ const value = source[key];
2218
+ if (value === void 0) {
2219
+ return void 0;
2220
+ }
2221
+ if (!isNonEmptyString(value)) {
2222
+ addError(errors, path4, "must be a non-empty string when provided");
2223
+ return void 0;
2224
+ }
2225
+ return value;
2226
+ }
2227
+ function readRequiredIsoDate(source, key, errors, path4) {
2228
+ const value = source[key];
2229
+ if (!isValidIsoDate(value)) {
2230
+ addError(errors, path4, "must be a valid ISO-8601 date");
2231
+ return void 0;
2232
+ }
2233
+ return value;
2234
+ }
2235
+ function parseAttributes(source, errors) {
2236
+ const rawAttributes = source["attributes"];
2237
+ if (rawAttributes === void 0) {
2238
+ return void 0;
2239
+ }
2240
+ if (!isRecord(rawAttributes)) {
2241
+ addError(errors, "attributes", "must be an object");
2242
+ return void 0;
2243
+ }
2244
+ const attributes = {};
2245
+ for (const [key, value] of Object.entries(rawAttributes)) {
2246
+ if (!isNonEmptyString(key)) {
2247
+ addError(errors, "attributes", "keys must be non-empty strings");
2248
+ continue;
2249
+ }
2250
+ if (!isNonEmptyString(value)) {
2251
+ addError(errors, `attributes.${key}`, "must be a non-empty string");
2252
+ continue;
2253
+ }
2254
+ attributes[key] = value;
2255
+ }
2256
+ return attributes;
2257
+ }
2258
+ function failure(errors) {
2259
+ return {
2260
+ ok: false,
2261
+ value: void 0,
2262
+ errors
2263
+ };
2264
+ }
2265
+ function success(value) {
2266
+ return {
2267
+ ok: true,
2268
+ value,
2269
+ errors: []
2270
+ };
2271
+ }
2272
+ function validateEventEnvelope(input) {
2273
+ if (!isRecord(input)) {
2274
+ return failure(["event: must be an object"]);
2275
+ }
2276
+ const errors = [];
2277
+ const schemaVersionRaw = input["schemaVersion"];
2278
+ if (schemaVersionRaw !== SCHEMA_VERSION) {
2279
+ addError(errors, "schemaVersion", `must equal ${SCHEMA_VERSION}`);
2280
+ }
2281
+ const sourceRaw = input["source"];
2282
+ if (!isEventSource(sourceRaw)) {
2283
+ addError(errors, "source", `must be one of: ${EVENT_SOURCES.join(", ")}`);
2284
+ }
2285
+ const eventId = readRequiredString(input, "eventId", errors, "eventId");
2286
+ const sessionId = readRequiredString(input, "sessionId", errors, "sessionId");
2287
+ const promptId = readOptionalString(input, "promptId", errors, "promptId");
2288
+ const eventType = readRequiredString(input, "eventType", errors, "eventType");
2289
+ const eventTimestamp = readRequiredIsoDate(input, "eventTimestamp", errors, "eventTimestamp");
2290
+ const ingestedAt = readRequiredIsoDate(input, "ingestedAt", errors, "ingestedAt");
2291
+ const privacyTierRaw = input["privacyTier"];
2292
+ if (!isPrivacyTier(privacyTierRaw)) {
2293
+ addError(errors, "privacyTier", `must be one of: ${PRIVACY_TIERS.join(", ")}`);
2294
+ }
2295
+ const sourceVersion = readOptionalString(input, "sourceVersion", errors, "sourceVersion");
2296
+ const attributes = parseAttributes(input, errors);
2297
+ const hasPayload = Object.prototype.hasOwnProperty.call(input, "payload");
2298
+ if (!hasPayload) {
2299
+ addError(errors, "payload", "is required");
2300
+ }
2301
+ const payload = input["payload"];
2302
+ if (errors.length > 0 || !isEventSource(sourceRaw) || !isPrivacyTier(privacyTierRaw) || eventId === void 0 || sessionId === void 0 || eventType === void 0 || eventTimestamp === void 0 || ingestedAt === void 0) {
2303
+ return failure(errors);
2304
+ }
2305
+ const eventEnvelope = {
2306
+ schemaVersion: SCHEMA_VERSION,
2307
+ source: sourceRaw,
2308
+ ...sourceVersion !== void 0 ? { sourceVersion } : {},
2309
+ eventId,
2310
+ sessionId,
2311
+ ...promptId !== void 0 ? { promptId } : {},
2312
+ eventType,
2313
+ eventTimestamp,
2314
+ ingestedAt,
2315
+ privacyTier: privacyTierRaw,
2316
+ payload,
2317
+ ...attributes !== void 0 ? { attributes } : {}
2318
+ };
2319
+ return success(eventEnvelope);
2320
+ }
2321
+
2322
+ // packages/collector/src/handler.ts
2323
+ function buildHealthPayload(startedAtMs) {
2324
+ return {
2325
+ status: "ok",
2326
+ service: "collector",
2327
+ uptimeSec: Math.floor((Date.now() - startedAtMs) / 1e3)
2328
+ };
2329
+ }
2330
+ function buildErrorPayload(message, errors) {
2331
+ return {
2332
+ status: "error",
2333
+ message,
2334
+ ...errors !== void 0 ? { errors } : {}
2335
+ };
2336
+ }
2337
+ function buildStatsPayload(storedEvents, dedupedEvents) {
2338
+ return {
2339
+ status: "ok",
2340
+ stats: {
2341
+ storedEvents,
2342
+ dedupedEvents
2343
+ }
2344
+ };
2345
+ }
2346
+ function buildAcceptedPayload(accepted, deduped) {
2347
+ return {
2348
+ status: "accepted",
2349
+ accepted,
2350
+ deduped
2351
+ };
2352
+ }
2353
+ function isPromiseLike(value) {
2354
+ if (typeof value !== "object" || value === null) {
2355
+ return false;
2356
+ }
2357
+ const maybePromise = value;
2358
+ return typeof maybePromise.then === "function";
2359
+ }
2360
+ function handleCollectorRequest(request, dependencies) {
2361
+ if (request.method === "GET" && request.url === "/health") {
2362
+ return {
2363
+ statusCode: 200,
2364
+ payload: buildHealthPayload(dependencies.startedAtMs)
2365
+ };
2366
+ }
2367
+ if (request.method === "GET" && request.url === "/v1/hooks/stats") {
2368
+ const stats = dependencies.store.getStats();
2369
+ return {
2370
+ statusCode: 200,
2371
+ payload: buildStatsPayload(stats.storedEvents, stats.dedupedEvents)
2372
+ };
2373
+ }
2374
+ if (request.method === "POST" && request.url === "/v1/hooks") {
2375
+ const validation = dependencies.validateEvent(request.body);
2376
+ if (!validation.ok) {
2377
+ return {
2378
+ statusCode: 400,
2379
+ payload: buildErrorPayload("invalid event payload", validation.errors)
2380
+ };
2381
+ }
2382
+ const eventId = dependencies.getEventId(validation.value);
2383
+ const ingest = dependencies.store.ingest(validation.value, eventId);
2384
+ if (ingest.accepted && dependencies.onAcceptedEvent !== void 0) {
2385
+ try {
2386
+ const callbackResult = dependencies.onAcceptedEvent(validation.value);
2387
+ if (isPromiseLike(callbackResult)) {
2388
+ void callbackResult.catch(() => {
2389
+ });
2390
+ }
2391
+ } catch {
2392
+ }
2393
+ }
2394
+ return {
2395
+ statusCode: 202,
2396
+ payload: buildAcceptedPayload(ingest.accepted, ingest.deduped)
2397
+ };
2398
+ }
2399
+ return {
2400
+ statusCode: 404,
2401
+ payload: buildErrorPayload("not found")
2402
+ };
2403
+ }
2404
+
2405
+ // packages/collector/src/http.ts
2406
+ function normalizeMethod2(method) {
2407
+ if (method === "GET" || method === "POST") {
2408
+ return method;
2409
+ }
2410
+ return void 0;
2411
+ }
2412
+ function parsePathname3(url) {
2413
+ try {
2414
+ const parsed = new URL(url, "http://localhost");
2415
+ return parsed.pathname;
2416
+ } catch {
2417
+ return url;
2418
+ }
2419
+ }
2420
+ function handleCollectorRawHttpRequest(request, dependencies) {
2421
+ const method = normalizeMethod2(request.method);
2422
+ if (method === void 0) {
2423
+ return {
2424
+ statusCode: 405,
2425
+ payload: {
2426
+ status: "error",
2427
+ message: "method not allowed"
2428
+ }
2429
+ };
2430
+ }
2431
+ const pathname = parsePathname3(request.url);
2432
+ if (method === "POST" && pathname === "/v1/hooks") {
2433
+ const rawBody = request.rawBody ?? "";
2434
+ if (rawBody.trim().length === 0) {
2435
+ return handleCollectorRequest(
2436
+ {
2437
+ method,
2438
+ url: pathname,
2439
+ body: void 0
2440
+ },
2441
+ dependencies
2442
+ );
2443
+ }
2444
+ try {
2445
+ const body = JSON.parse(rawBody);
2446
+ return handleCollectorRequest(
2447
+ {
2448
+ method,
2449
+ url: pathname,
2450
+ body
2451
+ },
2452
+ dependencies
2453
+ );
2454
+ } catch {
2455
+ return {
2456
+ statusCode: 400,
2457
+ payload: {
2458
+ status: "error",
2459
+ message: "invalid JSON body"
2460
+ }
2461
+ };
2462
+ }
2463
+ }
2464
+ return handleCollectorRequest(
2465
+ {
2466
+ method,
2467
+ url: pathname
2468
+ },
2469
+ dependencies
2470
+ );
2471
+ }
2472
+ async function readRequestBody(req) {
2473
+ return new Promise((resolve, reject) => {
2474
+ const chunks = [];
2475
+ req.on("data", (chunk) => chunks.push(chunk));
2476
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
2477
+ req.on("error", (error) => reject(error));
2478
+ });
2479
+ }
2480
+ function sendJson3(res, statusCode, payload) {
2481
+ const json = JSON.stringify(payload);
2482
+ res.statusCode = statusCode;
2483
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
2484
+ res.end(json);
2485
+ }
2486
+ function createCollectorHttpHandler(dependencies) {
2487
+ return async (req, res) => {
2488
+ const method = req.method ?? "GET";
2489
+ const url = req.url ?? "/";
2490
+ const rawBody = method === "POST" ? await readRequestBody(req) : void 0;
2491
+ const response = handleCollectorRawHttpRequest(
2492
+ {
2493
+ method,
2494
+ url,
2495
+ ...rawBody !== void 0 ? { rawBody } : {}
2496
+ },
2497
+ dependencies
2498
+ );
2499
+ sendJson3(res, response.statusCode, response.payload);
2500
+ };
2501
+ }
2502
+
2503
+ // packages/collector/src/service.ts
2504
+ function toReadonlyStats(state) {
2505
+ return {
2506
+ acceptedEvents: state.acceptedEvents,
2507
+ processingFailures: state.processingFailures,
2508
+ ...state.lastProcessingFailure !== void 0 ? { lastProcessingFailure: state.lastProcessingFailure } : {}
2509
+ };
2510
+ }
2511
+ function createCollectorService(options) {
2512
+ const state = {
2513
+ acceptedEvents: 0,
2514
+ processingFailures: 0
2515
+ };
2516
+ const baseOnAcceptedEvent = options.dependencies.onAcceptedEvent;
2517
+ const dependencies = {
2518
+ ...options.dependencies,
2519
+ onAcceptedEvent: async (event) => {
2520
+ state.acceptedEvents += 1;
2521
+ const runBase = async () => {
2522
+ if (baseOnAcceptedEvent !== void 0) {
2523
+ await baseOnAcceptedEvent(event);
2524
+ }
2525
+ };
2526
+ const runProcessor = async () => {
2527
+ if (options.processor !== void 0) {
2528
+ await options.processor.processAcceptedEvent(event);
2529
+ }
2530
+ };
2531
+ try {
2532
+ await Promise.all([runBase(), runProcessor()]);
2533
+ } catch (error) {
2534
+ state.processingFailures += 1;
2535
+ state.lastProcessingFailure = String(error);
2536
+ }
2537
+ }
2538
+ };
2539
+ return {
2540
+ dependencies,
2541
+ handleRaw: (request) => handleCollectorRawHttpRequest(request, dependencies),
2542
+ getProcessingStats: () => toReadonlyStats(state)
2543
+ };
2544
+ }
2545
+
2546
+ // packages/collector/src/store.ts
2547
+ var InMemoryCollectorStore = class {
2548
+ seenEventIds;
2549
+ events;
2550
+ dedupedEventsCount;
2551
+ constructor() {
2552
+ this.seenEventIds = /* @__PURE__ */ new Set();
2553
+ this.events = /* @__PURE__ */ new Map();
2554
+ this.dedupedEventsCount = 0;
2555
+ }
2556
+ ingest(event, eventId) {
2557
+ if (this.seenEventIds.has(eventId)) {
2558
+ this.dedupedEventsCount += 1;
2559
+ return {
2560
+ accepted: false,
2561
+ deduped: true
2562
+ };
2563
+ }
2564
+ this.seenEventIds.add(eventId);
2565
+ this.events.set(eventId, event);
2566
+ return {
2567
+ accepted: true,
2568
+ deduped: false
2569
+ };
2570
+ }
2571
+ getStats() {
2572
+ return {
2573
+ storedEvents: this.events.size,
2574
+ dedupedEvents: this.dedupedEventsCount
2575
+ };
2576
+ }
2577
+ clear() {
2578
+ this.seenEventIds.clear();
2579
+ this.events.clear();
2580
+ this.dedupedEventsCount = 0;
2581
+ }
2582
+ };
2583
+
2584
+ // packages/collector/src/transcript-ingestion.ts
2585
+ var import_node_os = __toESM(require("node:os"));
2586
+ var import_node_path = __toESM(require("node:path"));
2587
+
2588
+ // packages/collector/src/transcript-parser.ts
2589
+ var import_node_crypto2 = __toESM(require("node:crypto"));
2590
+ var import_node_fs = __toESM(require("node:fs"));
2591
+ function isIsoDate(value) {
2592
+ if (Number.isNaN(Date.parse(value))) {
2593
+ return false;
2594
+ }
2595
+ return value.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(value);
2596
+ }
2597
+ function asRecord(value) {
2598
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2599
+ return void 0;
2600
+ }
2601
+ return value;
2602
+ }
2603
+ function asArray(value) {
2604
+ if (!Array.isArray(value)) {
2605
+ return void 0;
2606
+ }
2607
+ return value;
2608
+ }
2609
+ function readString(record, keys) {
2610
+ for (const key of keys) {
2611
+ const value = record[key];
2612
+ if (typeof value === "string" && value.length > 0) {
2613
+ return value;
2614
+ }
2615
+ }
2616
+ return void 0;
2617
+ }
2618
+ function readNumber(record, keys) {
2619
+ if (record === void 0) {
2620
+ return void 0;
2621
+ }
2622
+ for (const key of keys) {
2623
+ const value = record[key];
2624
+ if (typeof value === "number" && Number.isFinite(value)) {
2625
+ return value;
2626
+ }
2627
+ if (typeof value === "string" && value.length > 0) {
2628
+ const parsed = Number(value);
2629
+ if (Number.isFinite(parsed)) {
2630
+ return parsed;
2631
+ }
2632
+ }
2633
+ }
2634
+ return void 0;
2635
+ }
2636
+ function buildEventId(filePath, lineNumber, rawLine) {
2637
+ return import_node_crypto2.default.createHash("sha256").update(`${filePath}:${String(lineNumber)}:${rawLine}`).digest("hex");
2638
+ }
2639
+ function readMessageRecord(record) {
2640
+ return asRecord(record["message"]);
2641
+ }
2642
+ function findMessageContentRecordByType(message, contentType) {
2643
+ if (message === void 0) {
2644
+ return void 0;
2645
+ }
2646
+ const content = asArray(message["content"]);
2647
+ if (content === void 0) {
2648
+ return void 0;
2649
+ }
2650
+ for (const item of content) {
2651
+ const contentRecord = asRecord(item);
2652
+ if (contentRecord === void 0) {
2653
+ continue;
2654
+ }
2655
+ const type = readString(contentRecord, ["type"]);
2656
+ if (type === contentType) {
2657
+ return contentRecord;
2658
+ }
2659
+ }
2660
+ return void 0;
2661
+ }
2662
+ function hasMessageContentType(message, contentType) {
2663
+ return findMessageContentRecordByType(message, contentType) !== void 0;
2664
+ }
2665
+ function readPromptText(message) {
2666
+ if (message === void 0) {
2667
+ return void 0;
2668
+ }
2669
+ const content = message["content"];
2670
+ if (typeof content === "string" && content.length > 0) {
2671
+ return content;
2672
+ }
2673
+ const contentItems = asArray(content);
2674
+ if (contentItems === void 0) {
2675
+ return void 0;
2676
+ }
2677
+ for (const item of contentItems) {
2678
+ const contentRecord = asRecord(item);
2679
+ if (contentRecord === void 0) {
2680
+ continue;
2681
+ }
2682
+ const text = readString(contentRecord, ["text", "content"]);
2683
+ if (text !== void 0) {
2684
+ return text;
2685
+ }
2686
+ }
2687
+ return void 0;
2688
+ }
2689
+ function normalizeEventType(record, message) {
2690
+ const explicitEventType = readString(record, ["event", "kind"]);
2691
+ if (explicitEventType !== void 0) {
2692
+ return explicitEventType;
2693
+ }
2694
+ const transcriptType = readString(record, ["type"]);
2695
+ if (transcriptType === void 0) {
2696
+ return "transcript_event";
2697
+ }
2698
+ if (transcriptType === "user") {
2699
+ const role = message !== void 0 ? readString(message, ["role"]) : void 0;
2700
+ if (role === "user") {
2701
+ if (hasMessageContentType(message, "tool_result")) {
2702
+ return "tool_result";
2703
+ }
2704
+ return "user_prompt";
2705
+ }
2706
+ return "user_event";
2707
+ }
2708
+ if (transcriptType === "assistant") {
2709
+ if (hasMessageContentType(message, "tool_use")) {
2710
+ return "api_tool_use";
2711
+ }
2712
+ return "api_response";
2713
+ }
2714
+ if (transcriptType === "progress") {
2715
+ const data = asRecord(record["data"]);
2716
+ const hookEvent = data !== void 0 ? readString(data, ["hookEvent", "hook_event"]) : void 0;
2717
+ if (hookEvent !== void 0) {
2718
+ return hookEvent;
2719
+ }
2720
+ return "progress";
2721
+ }
2722
+ if (transcriptType === "system") {
2723
+ return readString(record, ["subtype"]) ?? "system_event";
2724
+ }
2725
+ return transcriptType;
2726
+ }
2727
+ function buildNormalizedPayload(record, message, eventType) {
2728
+ const payload = {
2729
+ ...record,
2730
+ normalized_event_type: eventType
2731
+ };
2732
+ const userId = readString(record, ["user_id", "userId", "userType"]);
2733
+ if (userId !== void 0) {
2734
+ payload["user_id"] = userId;
2735
+ }
2736
+ const projectPath = readString(record, ["project_path", "projectPath", "cwd"]);
2737
+ if (projectPath !== void 0) {
2738
+ payload["project_path"] = projectPath;
2739
+ }
2740
+ const gitBranch = readString(record, ["git_branch", "gitBranch"]);
2741
+ if (gitBranch !== void 0) {
2742
+ payload["git_branch"] = gitBranch;
2743
+ }
2744
+ const model = (message !== void 0 ? readString(message, ["model"]) : void 0) ?? readString(record, ["model"]);
2745
+ if (model !== void 0) {
2746
+ payload["model"] = model;
2747
+ }
2748
+ const requestId = readString(record, ["request_id", "requestId"]);
2749
+ if (requestId !== void 0) {
2750
+ payload["request_id"] = requestId;
2751
+ }
2752
+ const usage = asRecord(message?.["usage"]);
2753
+ const inputTokens = readNumber(usage, ["input_tokens", "inputTokens"]);
2754
+ if (inputTokens !== void 0) {
2755
+ payload["input_tokens"] = inputTokens;
2756
+ }
2757
+ const outputTokens = readNumber(usage, ["output_tokens", "outputTokens"]);
2758
+ if (outputTokens !== void 0) {
2759
+ payload["output_tokens"] = outputTokens;
2760
+ }
2761
+ const cacheReadTokens = readNumber(usage, ["cache_read_input_tokens", "cacheReadInputTokens"]);
2762
+ if (cacheReadTokens !== void 0) {
2763
+ payload["cache_read_tokens"] = cacheReadTokens;
2764
+ }
2765
+ const promptText = readPromptText(message);
2766
+ if (promptText !== void 0) {
2767
+ payload["prompt_text"] = promptText;
2768
+ }
2769
+ if (eventType === "api_response") {
2770
+ const responseText = readPromptText(message);
2771
+ if (responseText !== void 0) {
2772
+ payload["response_text"] = responseText;
2773
+ }
2774
+ }
2775
+ const toolUse = findMessageContentRecordByType(message, "tool_use");
2776
+ if (toolUse !== void 0) {
2777
+ const toolName = readString(toolUse, ["name"]);
2778
+ if (toolName !== void 0) {
2779
+ payload["tool_name"] = toolName;
2780
+ }
2781
+ const toolUseId = readString(toolUse, ["id", "tool_use_id", "toolUseId"]);
2782
+ if (toolUseId !== void 0) {
2783
+ payload["tool_use_id"] = toolUseId;
2784
+ }
2785
+ const toolInput = asRecord(toolUse["input"]);
2786
+ if (toolInput !== void 0) {
2787
+ payload["tool_input"] = toolInput;
2788
+ }
2789
+ const filePath = toolInput !== void 0 ? readString(toolInput, ["file_path", "filePath"]) : void 0;
2790
+ if (filePath !== void 0) {
2791
+ payload["file_path"] = filePath;
2792
+ }
2793
+ const command = toolInput !== void 0 ? readString(toolInput, ["command", "cmd"]) : void 0;
2794
+ if (command !== void 0) {
2795
+ payload["command"] = command;
2796
+ }
2797
+ }
2798
+ const toolResult = findMessageContentRecordByType(message, "tool_result");
2799
+ if (toolResult !== void 0) {
2800
+ const toolUseId = readString(toolResult, ["tool_use_id", "toolUseId"]);
2801
+ if (toolUseId !== void 0) {
2802
+ payload["tool_use_id"] = toolUseId;
2803
+ }
2804
+ }
2805
+ const data = asRecord(record["data"]);
2806
+ const hookEvent = data !== void 0 ? readString(data, ["hookEvent", "hook_event"]) : void 0;
2807
+ if (hookEvent !== void 0) {
2808
+ payload["hook_event"] = hookEvent;
2809
+ }
2810
+ return payload;
2811
+ }
2812
+ function pickPromptId(record, message) {
2813
+ return readString(record, ["prompt_id", "promptId"]) ?? (message !== void 0 ? readString(message, ["id"]) : void 0) ?? readString(record, ["requestId", "request_id", "messageId", "message_id", "uuid"]);
2814
+ }
2815
+ function buildEnvelope(record, filePath, lineNumber, rawLine, input) {
2816
+ const sessionId = readString(record, ["session_id", "sessionId"]) ?? input.sessionIdFallback;
2817
+ if (sessionId === void 0) {
2818
+ return void 0;
2819
+ }
2820
+ const message = readMessageRecord(record);
2821
+ const promptId = pickPromptId(record, message);
2822
+ const eventType = normalizeEventType(record, message);
2823
+ const payload = buildNormalizedPayload(record, message, eventType);
2824
+ const eventTimestampFromLine = readString(record, ["timestamp", "time", "created_at", "createdAt"]);
2825
+ const ingestedAt = input.ingestedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2826
+ const eventTimestamp = eventTimestampFromLine ?? ingestedAt;
2827
+ if (!isIsoDate(eventTimestamp) || !isIsoDate(ingestedAt)) {
2828
+ return void 0;
2829
+ }
2830
+ return {
2831
+ schemaVersion: "1.0",
2832
+ source: "transcript",
2833
+ sourceVersion: "claude-jsonl-v1",
2834
+ eventId: buildEventId(filePath, lineNumber, rawLine),
2835
+ sessionId,
2836
+ ...promptId !== void 0 ? { promptId } : {},
2837
+ eventType,
2838
+ eventTimestamp,
2839
+ ingestedAt,
2840
+ privacyTier: input.privacyTier,
2841
+ payload,
2842
+ attributes: {
2843
+ transcript_file: filePath,
2844
+ transcript_line: String(lineNumber)
2845
+ }
2846
+ };
2847
+ }
2848
+ function parseLines(contents, input) {
2849
+ const lines = contents.split("\n");
2850
+ const parsedEvents = [];
2851
+ const errors = [];
2852
+ let skippedLines = 0;
2853
+ lines.forEach((line, index) => {
2854
+ const lineNumber = index + 1;
2855
+ const trimmed = line.trim();
2856
+ if (trimmed.length === 0) {
2857
+ return;
2858
+ }
2859
+ let parsedUnknown;
2860
+ try {
2861
+ parsedUnknown = JSON.parse(trimmed);
2862
+ } catch {
2863
+ skippedLines += 1;
2864
+ errors.push(`line ${String(lineNumber)}: invalid JSON`);
2865
+ return;
2866
+ }
2867
+ const payload = asRecord(parsedUnknown);
2868
+ if (payload === void 0) {
2869
+ skippedLines += 1;
2870
+ errors.push(`line ${String(lineNumber)}: entry must be object`);
2871
+ return;
2872
+ }
2873
+ const envelope = buildEnvelope(payload, input.filePath, lineNumber, trimmed, input);
2874
+ if (envelope === void 0) {
2875
+ skippedLines += 1;
2876
+ errors.push(`line ${String(lineNumber)}: missing or invalid required transcript fields`);
2877
+ return;
2878
+ }
2879
+ parsedEvents.push(envelope);
2880
+ });
2881
+ if (errors.length > 0) {
2882
+ const failure2 = {
2883
+ ok: false,
2884
+ filePath: input.filePath,
2885
+ parsedEvents,
2886
+ skippedLines,
2887
+ errors
2888
+ };
2889
+ return failure2;
2890
+ }
2891
+ const success2 = {
2892
+ ok: true,
2893
+ filePath: input.filePath,
2894
+ parsedEvents,
2895
+ skippedLines,
2896
+ errors: []
2897
+ };
2898
+ return success2;
2899
+ }
2900
+ function parseTranscriptJsonl(input) {
2901
+ if (!import_node_fs.default.existsSync(input.filePath)) {
2902
+ return {
2903
+ ok: false,
2904
+ filePath: input.filePath,
2905
+ parsedEvents: [],
2906
+ skippedLines: 0,
2907
+ errors: ["transcript file does not exist"]
2908
+ };
2909
+ }
2910
+ const raw = import_node_fs.default.readFileSync(input.filePath, "utf8");
2911
+ return parseLines(raw, input);
2912
+ }
2913
+
2914
+ // packages/collector/src/transcript-ingestion.ts
2915
+ function asRecord2(value) {
2916
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2917
+ return void 0;
2918
+ }
2919
+ return value;
2920
+ }
2921
+ function readString2(record, keys) {
2922
+ for (const key of keys) {
2923
+ const value = record[key];
2924
+ if (typeof value === "string" && value.length > 0) {
2925
+ return value;
2926
+ }
2927
+ }
2928
+ return void 0;
2929
+ }
2930
+ function shouldIngestTranscript(eventType) {
2931
+ const normalized = eventType.toLowerCase();
2932
+ return normalized === "session_end" || normalized === "sessionend" || normalized === "stop" || normalized === "task_completed" || normalized === "taskcompleted";
2933
+ }
2934
+ function pickTranscriptPath(payload) {
2935
+ const record = asRecord2(payload);
2936
+ if (record === void 0) {
2937
+ return void 0;
2938
+ }
2939
+ return readString2(record, ["transcript_path", "transcriptPath"]);
2940
+ }
2941
+ function resolveTranscriptPath(filePath) {
2942
+ if (filePath === "~") {
2943
+ return import_node_os.default.homedir();
2944
+ }
2945
+ if (filePath.startsWith("~/")) {
2946
+ return import_node_path.default.join(import_node_os.default.homedir(), filePath.slice(2));
2947
+ }
2948
+ if (filePath.startsWith("$HOME/")) {
2949
+ return import_node_path.default.join(import_node_os.default.homedir(), filePath.slice("$HOME/".length));
2950
+ }
2951
+ if (filePath.startsWith("${HOME}/")) {
2952
+ return import_node_path.default.join(import_node_os.default.homedir(), filePath.slice("${HOME}/".length));
2953
+ }
2954
+ return filePath;
2955
+ }
2956
+ function createTranscriptIngestionProcessor(options) {
2957
+ return {
2958
+ processAcceptedEvent: async (event) => {
2959
+ if (event.source === "transcript") {
2960
+ return;
2961
+ }
2962
+ if (!shouldIngestTranscript(event.eventType)) {
2963
+ return;
2964
+ }
2965
+ const transcriptPath = pickTranscriptPath(event.payload);
2966
+ if (transcriptPath === void 0) {
2967
+ return;
2968
+ }
2969
+ const resolvedTranscriptPath = resolveTranscriptPath(transcriptPath);
2970
+ const parseResult = parseTranscriptJsonl({
2971
+ filePath: resolvedTranscriptPath,
2972
+ privacyTier: event.privacyTier,
2973
+ sessionIdFallback: event.sessionId,
2974
+ ingestedAt: event.ingestedAt
2975
+ });
2976
+ if (!parseResult.ok && options.onParseErrors !== void 0) {
2977
+ options.onParseErrors(parseResult.errors);
2978
+ }
2979
+ if (parseResult.parsedEvents.length === 0) {
2980
+ return;
2981
+ }
2982
+ await options.sink.ingestTranscriptEvents(parseResult.parsedEvents);
2983
+ }
2984
+ };
2985
+ }
2986
+
2987
+ // packages/collector/src/git-enrichment.ts
2988
+ function asRecord3(value) {
2989
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2990
+ return void 0;
2991
+ }
2992
+ return value;
2993
+ }
2994
+ function readString3(record, key) {
2995
+ const value = record[key];
2996
+ if (typeof value === "string" && value.length > 0) {
2997
+ return value;
2998
+ }
2999
+ return void 0;
3000
+ }
3001
+ function readNumber2(record, key) {
3002
+ const value = record[key];
3003
+ if (typeof value === "number" && Number.isFinite(value)) {
3004
+ return value;
3005
+ }
3006
+ return void 0;
3007
+ }
3008
+ function readStringArray(record, key) {
3009
+ const value = record[key];
3010
+ if (!Array.isArray(value)) {
3011
+ return void 0;
3012
+ }
3013
+ const output = [];
3014
+ value.forEach((item) => {
3015
+ if (typeof item === "string" && item.length > 0) {
3016
+ output.push(item);
3017
+ }
3018
+ });
3019
+ if (output.length === 0) {
3020
+ return void 0;
3021
+ }
3022
+ return output;
3023
+ }
3024
+ function readNestedString(record, path4) {
3025
+ let current = record;
3026
+ for (const key of path4) {
3027
+ const asObject = asRecord3(current);
3028
+ if (asObject === void 0) {
3029
+ return void 0;
3030
+ }
3031
+ current = asObject[key];
3032
+ }
3033
+ if (typeof current === "string" && current.length > 0) {
3034
+ return current;
3035
+ }
3036
+ return void 0;
3037
+ }
3038
+ function pickCommand(payload) {
3039
+ const record = payload;
3040
+ return readString3(record, "command") ?? readString3(record, "bash_command") ?? readString3(record, "bashCommand") ?? readNestedString(record, ["tool_input", "command"]);
3041
+ }
3042
+ function pickStdout(payload) {
3043
+ const record = payload;
3044
+ return readString3(record, "stdout") ?? readString3(record, "output");
3045
+ }
3046
+ function pickToolName(payload) {
3047
+ const record = payload;
3048
+ return readString3(record, "tool_name") ?? readString3(record, "toolName");
3049
+ }
3050
+ function isGitCommand(command) {
3051
+ return command.trim().startsWith("git ");
3052
+ }
3053
+ function parseCommitMessage(command) {
3054
+ const regex = /(?:^|\s)-m\s+["']([^"']+)["']/;
3055
+ const match = command.match(regex);
3056
+ if (match === null || match[1] === void 0 || match[1].length === 0) {
3057
+ return void 0;
3058
+ }
3059
+ return match[1];
3060
+ }
3061
+ function parseCommitSha(stdout) {
3062
+ const match = stdout.match(/\b[0-9a-f]{7,40}\b/i);
3063
+ if (match === null || match[0] === void 0) {
3064
+ return void 0;
3065
+ }
3066
+ return match[0].toLowerCase();
3067
+ }
3068
+ function parseBranch(command) {
3069
+ const checkoutMatch = command.match(/git\s+checkout\s+-b\s+([^\s]+)/);
3070
+ if (checkoutMatch?.[1] !== void 0 && checkoutMatch[1].length > 0) {
3071
+ return checkoutMatch[1];
3072
+ }
3073
+ const switchMatch = command.match(/git\s+switch\s+-c\s+([^\s]+)/);
3074
+ if (switchMatch?.[1] !== void 0 && switchMatch[1].length > 0) {
3075
+ return switchMatch[1];
3076
+ }
3077
+ return void 0;
3078
+ }
3079
+ function toUniqueStrings2(values) {
3080
+ const seen = /* @__PURE__ */ new Set();
3081
+ const output = [];
3082
+ values.forEach((value) => {
3083
+ if (value.length === 0 || seen.has(value)) {
3084
+ return;
3085
+ }
3086
+ seen.add(value);
3087
+ output.push(value);
3088
+ });
3089
+ return output;
3090
+ }
3091
+ function parseInteger(input) {
3092
+ const parsed = Number.parseInt(input, 10);
3093
+ if (!Number.isFinite(parsed) || parsed < 0) {
3094
+ return void 0;
3095
+ }
3096
+ return parsed;
3097
+ }
3098
+ function parseNumstat(stdout) {
3099
+ const lines = stdout.split(/\r?\n/);
3100
+ let linesAdded = 0;
3101
+ let linesRemoved = 0;
3102
+ const filesChanged = [];
3103
+ let matched = false;
3104
+ lines.forEach((line) => {
3105
+ const match = line.trim().match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
3106
+ if (match === null) {
3107
+ return;
3108
+ }
3109
+ const filePath = match[3]?.trim();
3110
+ if (filePath === void 0 || filePath.length === 0) {
3111
+ return;
3112
+ }
3113
+ const add = match[1];
3114
+ const remove = match[2];
3115
+ if (add !== void 0 && add !== "-") {
3116
+ linesAdded += parseInteger(add) ?? 0;
3117
+ }
3118
+ if (remove !== void 0 && remove !== "-") {
3119
+ linesRemoved += parseInteger(remove) ?? 0;
3120
+ }
3121
+ filesChanged.push(filePath);
3122
+ matched = true;
3123
+ });
3124
+ if (!matched) {
3125
+ return void 0;
3126
+ }
3127
+ return {
3128
+ linesAdded,
3129
+ linesRemoved,
3130
+ filesChanged: toUniqueStrings2(filesChanged)
3131
+ };
3132
+ }
3133
+ function parseShortStat(stdout) {
3134
+ const insertionMatch = stdout.match(/(\d+)\s+insertions?\(\+\)/);
3135
+ const deletionMatch = stdout.match(/(\d+)\s+deletions?\(-\)/);
3136
+ const linesAdded = insertionMatch?.[1] === void 0 ? void 0 : parseInteger(insertionMatch[1]);
3137
+ const linesRemoved = deletionMatch?.[1] === void 0 ? void 0 : parseInteger(deletionMatch[1]);
3138
+ if (linesAdded === void 0 && linesRemoved === void 0) {
3139
+ return void 0;
3140
+ }
3141
+ return {
3142
+ ...linesAdded !== void 0 ? { linesAdded } : {},
3143
+ ...linesRemoved !== void 0 ? { linesRemoved } : {}
3144
+ };
3145
+ }
3146
+ function parseNameOnlyFiles(command, stdout) {
3147
+ if (!command.includes("--name-only")) {
3148
+ return void 0;
3149
+ }
3150
+ const filesChanged = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !line.startsWith("commit ")).filter((line) => !line.startsWith("Author:")).filter((line) => !line.startsWith("Date:")).filter((line) => !line.startsWith("[")).filter((line) => !line.includes("|")).filter((line) => !line.includes("files changed"));
3151
+ if (filesChanged.length === 0) {
3152
+ return void 0;
3153
+ }
3154
+ return toUniqueStrings2(filesChanged);
3155
+ }
3156
+ function parseStatFiles(command, stdout) {
3157
+ if (!command.includes("--stat")) {
3158
+ return void 0;
3159
+ }
3160
+ const filesChanged = [];
3161
+ stdout.split(/\r?\n/).forEach((line) => {
3162
+ const match = line.match(/^\s*(.+?)\s+\|\s+\d+/);
3163
+ const filePath = match?.[1]?.trim();
3164
+ if (filePath !== void 0 && filePath.length > 0) {
3165
+ filesChanged.push(filePath);
3166
+ }
3167
+ });
3168
+ if (filesChanged.length === 0) {
3169
+ return void 0;
3170
+ }
3171
+ return toUniqueStrings2(filesChanged);
3172
+ }
3173
+ function parseGitChangeMetadata(command, stdout) {
3174
+ const numstat = parseNumstat(stdout);
3175
+ const shortStat = parseShortStat(stdout);
3176
+ const nameOnlyFiles = parseNameOnlyFiles(command, stdout);
3177
+ const statFiles = parseStatFiles(command, stdout);
3178
+ const filesChanged = nameOnlyFiles ?? numstat?.filesChanged ?? statFiles;
3179
+ const linesAdded = numstat?.linesAdded ?? shortStat?.linesAdded;
3180
+ const linesRemoved = numstat?.linesRemoved ?? shortStat?.linesRemoved;
3181
+ if (filesChanged === void 0 && linesAdded === void 0 && linesRemoved === void 0) {
3182
+ return void 0;
3183
+ }
3184
+ return {
3185
+ ...filesChanged !== void 0 ? { filesChanged } : {},
3186
+ ...linesAdded !== void 0 ? { linesAdded } : {},
3187
+ ...linesRemoved !== void 0 ? { linesRemoved } : {}
3188
+ };
3189
+ }
3190
+ function isBashTool(payload) {
3191
+ const toolName = pickToolName(payload);
3192
+ if (toolName === void 0) {
3193
+ return false;
3194
+ }
3195
+ const normalized = toolName.toLowerCase();
3196
+ return normalized === "bash";
3197
+ }
3198
+ function enrichCollectorEventWithGitMetadata(event) {
3199
+ if (event.source !== "hook") {
3200
+ return event;
3201
+ }
3202
+ if (!isBashTool(event.payload)) {
3203
+ return event;
3204
+ }
3205
+ const command = pickCommand(event.payload);
3206
+ if (command === void 0 || !isGitCommand(command)) {
3207
+ return event;
3208
+ }
3209
+ const payloadRecord = event.payload;
3210
+ const commitMessage = parseCommitMessage(command);
3211
+ const commitSha = parseCommitSha(pickStdout(event.payload) ?? "");
3212
+ const branch = parseBranch(command);
3213
+ const changes = parseGitChangeMetadata(command, pickStdout(event.payload) ?? "");
3214
+ const existingCommitSha = readString3(payloadRecord, "commit_sha") ?? readString3(payloadRecord, "commitSha");
3215
+ const existingCommitMessage = readString3(payloadRecord, "commit_message") ?? readString3(payloadRecord, "commitMessage");
3216
+ const existingGitBranch = readString3(payloadRecord, "git_branch") ?? readString3(payloadRecord, "gitBranch");
3217
+ const existingLinesAdded = readNumber2(payloadRecord, "lines_added") ?? readNumber2(payloadRecord, "linesAdded");
3218
+ const existingLinesRemoved = readNumber2(payloadRecord, "lines_removed") ?? readNumber2(payloadRecord, "linesRemoved");
3219
+ const existingFilesChanged = readStringArray(payloadRecord, "files_changed") ?? readStringArray(payloadRecord, "filesChanged");
3220
+ const patch = {};
3221
+ if (existingCommitSha === void 0 && commitSha !== void 0) {
3222
+ patch["commit_sha"] = commitSha;
3223
+ }
3224
+ if (existingCommitMessage === void 0 && commitMessage !== void 0) {
3225
+ patch["commit_message"] = commitMessage;
3226
+ }
3227
+ if (existingGitBranch === void 0 && branch !== void 0) {
3228
+ patch["git_branch"] = branch;
3229
+ }
3230
+ if (existingLinesAdded === void 0 && changes?.linesAdded !== void 0) {
3231
+ patch["lines_added"] = changes.linesAdded;
3232
+ }
3233
+ if (existingLinesRemoved === void 0 && changes?.linesRemoved !== void 0) {
3234
+ patch["lines_removed"] = changes.linesRemoved;
3235
+ }
3236
+ if (existingFilesChanged === void 0 && changes?.filesChanged !== void 0) {
3237
+ patch["files_changed"] = changes.filesChanged;
3238
+ }
3239
+ if (Object.keys(patch).length === 0) {
3240
+ return event;
3241
+ }
3242
+ const payload = {
3243
+ ...event.payload,
3244
+ ...patch
3245
+ };
3246
+ return {
3247
+ ...event,
3248
+ payload,
3249
+ attributes: {
3250
+ ...event.attributes ?? {},
3251
+ git_enriched: "1"
3252
+ }
3253
+ };
3254
+ }
3255
+
3256
+ // packages/collector/src/envelope-service.ts
3257
+ function toCollectorValidationResult(input) {
3258
+ const validation = validateEventEnvelope(input);
3259
+ if (!validation.ok) {
3260
+ return {
3261
+ ok: false,
3262
+ value: void 0,
3263
+ errors: validation.errors
3264
+ };
3265
+ }
3266
+ return {
3267
+ ok: true,
3268
+ value: validation.value,
3269
+ errors: []
3270
+ };
3271
+ }
3272
+ function toEnrichedValidationResult(input) {
3273
+ const validation = toCollectorValidationResult(input);
3274
+ if (!validation.ok) {
3275
+ return validation;
3276
+ }
3277
+ return {
3278
+ ok: true,
3279
+ value: enrichCollectorEventWithGitMetadata(validation.value),
3280
+ errors: []
3281
+ };
3282
+ }
3283
+ function toCollectorEnvelopeEvent(event) {
3284
+ return event;
3285
+ }
3286
+ async function ingestEventIntoDependencies(event, dependencies) {
3287
+ const enrichedEvent = enrichCollectorEventWithGitMetadata(event);
3288
+ const ingest = dependencies.store.ingest(enrichedEvent, enrichedEvent.eventId);
3289
+ if (!ingest.accepted) {
3290
+ return;
3291
+ }
3292
+ if (dependencies.onAcceptedEvent !== void 0) {
3293
+ await dependencies.onAcceptedEvent(enrichedEvent);
3294
+ }
3295
+ }
3296
+ function combineProcessors(processors) {
3297
+ if (processors.length === 0) {
3298
+ return void 0;
3299
+ }
3300
+ return {
3301
+ processAcceptedEvent: async (event) => {
3302
+ for (const processor of processors) {
3303
+ await processor.processAcceptedEvent(event);
3304
+ }
3305
+ }
3306
+ };
3307
+ }
3308
+ function createEnvelopeCollectorService(options = {}) {
3309
+ const store = new InMemoryCollectorStore();
3310
+ let ingestEventsRef;
3311
+ const transcriptSink = {
3312
+ ingestTranscriptEvents: async (events) => {
3313
+ if (ingestEventsRef === void 0) {
3314
+ return;
3315
+ }
3316
+ const collectorEvents = events.map((event) => toCollectorEnvelopeEvent(event));
3317
+ await ingestEventsRef(collectorEvents);
3318
+ }
3319
+ };
3320
+ const processors = [];
3321
+ if (options.enableTranscriptIngestion ?? true) {
3322
+ const transcriptProcessor = createTranscriptIngestionProcessor({
3323
+ sink: transcriptSink
3324
+ });
3325
+ processors.push({
3326
+ processAcceptedEvent: async (event) => {
3327
+ await transcriptProcessor.processAcceptedEvent(event);
3328
+ }
3329
+ });
3330
+ }
3331
+ if (options.processor !== void 0) {
3332
+ processors.push(options.processor);
3333
+ }
3334
+ const processor = combineProcessors(processors);
3335
+ const dependencies = {
3336
+ startedAtMs: options.startedAtMs ?? Date.now(),
3337
+ validateEvent: toEnrichedValidationResult,
3338
+ getEventId: (event) => event.eventId,
3339
+ store,
3340
+ ...options.onAcceptedEvent !== void 0 ? { onAcceptedEvent: options.onAcceptedEvent } : {}
3341
+ };
3342
+ const service = createCollectorService({
3343
+ dependencies,
3344
+ ...processor !== void 0 ? { processor } : {}
3345
+ });
3346
+ const ingestEvents = async (events) => {
3347
+ for (const event of events) {
3348
+ await ingestEventIntoDependencies(event, service.dependencies);
3349
+ }
3350
+ };
3351
+ ingestEventsRef = ingestEvents;
3352
+ const otelSink = {
3353
+ ingestOtelEvents: async (events) => {
3354
+ const collectorEvents = events.map((event) => toCollectorEnvelopeEvent(event));
3355
+ await ingestEvents(collectorEvents);
3356
+ }
3357
+ };
3358
+ return {
3359
+ dependencies: service.dependencies,
3360
+ store,
3361
+ handleRaw: service.handleRaw,
3362
+ getProcessingStats: service.getProcessingStats,
3363
+ ingestEvents,
3364
+ otelSink,
3365
+ transcriptSink
3366
+ };
3367
+ }
3368
+
3369
+ // packages/collector/src/otel-grpc-receiver.ts
3370
+ var import_node_path2 = __toESM(require("node:path"));
3371
+ var grpc = __toESM(require("@grpc/grpc-js"));
3372
+ var protoLoader = __toESM(require("@grpc/proto-loader"));
3373
+
3374
+ // packages/collector/src/otel-normalizer.ts
3375
+ var import_node_crypto3 = __toESM(require("node:crypto"));
3376
+ function asRecord4(value) {
3377
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3378
+ return void 0;
3379
+ }
3380
+ return value;
3381
+ }
3382
+ function asArray2(value) {
3383
+ if (!Array.isArray(value)) {
3384
+ return [];
3385
+ }
3386
+ return value;
3387
+ }
3388
+ function pickString2(record, keys) {
3389
+ for (const key of keys) {
3390
+ const value = record[key];
3391
+ if (typeof value === "string" && value.length > 0) {
3392
+ return value;
3393
+ }
3394
+ }
3395
+ return void 0;
3396
+ }
3397
+ function pickNumber2(record, keys) {
3398
+ for (const key of keys) {
3399
+ const value = record[key];
3400
+ if (typeof value === "number" && Number.isFinite(value)) {
3401
+ return value;
3402
+ }
3403
+ }
3404
+ return void 0;
3405
+ }
3406
+ function mergeRecordIntoPayload(target, source) {
3407
+ Object.entries(source).forEach(([key, value]) => {
3408
+ if (typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean") {
3409
+ target[key] = value;
3410
+ }
3411
+ });
3412
+ }
3413
+ function extractPrimitiveFromAnyValue(value) {
3414
+ const record = asRecord4(value);
3415
+ if (record === void 0) {
3416
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3417
+ return value;
3418
+ }
3419
+ return void 0;
3420
+ }
3421
+ const stringValue = record["stringValue"];
3422
+ if (typeof stringValue === "string") {
3423
+ return stringValue;
3424
+ }
3425
+ const boolValue = record["boolValue"];
3426
+ if (typeof boolValue === "boolean") {
3427
+ return boolValue;
3428
+ }
3429
+ const intValue = record["intValue"];
3430
+ if (typeof intValue === "string" && intValue.length > 0) {
3431
+ const parsed = Number(intValue);
3432
+ if (Number.isFinite(parsed)) {
3433
+ return parsed;
3434
+ }
3435
+ }
3436
+ if (typeof intValue === "number" && Number.isFinite(intValue)) {
3437
+ return intValue;
3438
+ }
3439
+ const doubleValue = record["doubleValue"];
3440
+ if (typeof doubleValue === "number" && Number.isFinite(doubleValue)) {
3441
+ return doubleValue;
3442
+ }
3443
+ return void 0;
3444
+ }
3445
+ function attributesToPayload(attributes) {
3446
+ const values = {};
3447
+ asArray2(attributes).forEach((entry) => {
3448
+ const entryRecord = asRecord4(entry);
3449
+ if (entryRecord === void 0) {
3450
+ return;
3451
+ }
3452
+ const key = entryRecord["key"];
3453
+ const value = extractPrimitiveFromAnyValue(entryRecord["value"]);
3454
+ if (typeof key !== "string" || key.length === 0 || value === void 0) {
3455
+ return;
3456
+ }
3457
+ values[key] = value;
3458
+ });
3459
+ return values;
3460
+ }
3461
+ function unixNanoToIso(value, fallbackIso) {
3462
+ if (typeof value === "number" && Number.isFinite(value)) {
3463
+ const millis = Math.floor(value / 1e6);
3464
+ const iso = new Date(millis).toISOString();
3465
+ return Number.isNaN(Date.parse(iso)) ? fallbackIso : iso;
3466
+ }
3467
+ if (typeof value === "string" && value.length > 0) {
3468
+ try {
3469
+ const millis = Number(BigInt(value) / BigInt(1e6));
3470
+ if (Number.isFinite(millis)) {
3471
+ return new Date(millis).toISOString();
3472
+ }
3473
+ } catch {
3474
+ return fallbackIso;
3475
+ }
3476
+ }
3477
+ return fallbackIso;
3478
+ }
3479
+ function buildEventId2(sessionId, eventTimestamp, eventType, salt) {
3480
+ return import_node_crypto3.default.createHash("sha256").update(`${sessionId}:${eventTimestamp}:${eventType}:${salt}`).digest("hex");
3481
+ }
3482
+ function normalizeLogRecord(logRecord, input, salt) {
3483
+ const logRecordObject = asRecord4(logRecord);
3484
+ if (logRecordObject === void 0) {
3485
+ return void 0;
3486
+ }
3487
+ const ingestedAt = input.ingestedAt ?? (/* @__PURE__ */ new Date()).toISOString();
3488
+ const payload = attributesToPayload(logRecordObject["attributes"]);
3489
+ const body = extractPrimitiveFromAnyValue(logRecordObject["body"]);
3490
+ if (body !== void 0) {
3491
+ payload["body"] = body;
3492
+ if (typeof body === "string" && body.trim().startsWith("{")) {
3493
+ try {
3494
+ const parsedBody = JSON.parse(body);
3495
+ const bodyRecord = asRecord4(parsedBody);
3496
+ if (bodyRecord !== void 0) {
3497
+ mergeRecordIntoPayload(payload, bodyRecord);
3498
+ }
3499
+ } catch {
3500
+ }
3501
+ }
3502
+ }
3503
+ const severityText = pickString2(logRecordObject, ["severityText"]);
3504
+ if (severityText !== void 0) {
3505
+ payload["severity_text"] = severityText;
3506
+ }
3507
+ const severityNumber = pickNumber2(logRecordObject, ["severityNumber"]);
3508
+ if (severityNumber !== void 0) {
3509
+ payload["severity_number"] = severityNumber;
3510
+ }
3511
+ const eventType = pickString2(payload, ["event_type", "event.name", "event.type", "type"]) ?? "otel_log";
3512
+ const sessionId = pickString2(payload, ["session_id", "session.id", "sessionId"]) ?? "unknown_session";
3513
+ const promptId = pickString2(payload, ["prompt_id", "prompt.id", "promptId"]);
3514
+ const eventTimestamp = unixNanoToIso(
3515
+ pickString2(logRecordObject, ["timeUnixNano"]) ?? pickNumber2(logRecordObject, ["timeUnixNano"]),
3516
+ ingestedAt
3517
+ );
3518
+ return {
3519
+ schemaVersion: "1.0",
3520
+ source: "otel",
3521
+ sourceVersion: "otlp-log-v1",
3522
+ eventId: buildEventId2(sessionId, eventTimestamp, eventType, salt),
3523
+ sessionId,
3524
+ ...promptId !== void 0 ? { promptId } : {},
3525
+ eventType,
3526
+ eventTimestamp,
3527
+ ingestedAt,
3528
+ privacyTier: input.privacyTier,
3529
+ payload
3530
+ };
3531
+ }
3532
+ function collectLogRecords(payload) {
3533
+ const root = asRecord4(payload);
3534
+ if (root === void 0) {
3535
+ return [];
3536
+ }
3537
+ const resourceLogs = asArray2(root["resourceLogs"]);
3538
+ const collected = [];
3539
+ resourceLogs.forEach((resourceLog) => {
3540
+ const resourceRecord = asRecord4(resourceLog);
3541
+ if (resourceRecord === void 0) {
3542
+ return;
3543
+ }
3544
+ const scopeLogs = asArray2(resourceRecord["scopeLogs"]);
3545
+ const instrumentationLibraryLogs = asArray2(resourceRecord["instrumentationLibraryLogs"]);
3546
+ [...scopeLogs, ...instrumentationLibraryLogs].forEach((scopeEntry) => {
3547
+ const scopeRecord = asRecord4(scopeEntry);
3548
+ if (scopeRecord === void 0) {
3549
+ return;
3550
+ }
3551
+ const logRecords = asArray2(scopeRecord["logRecords"]);
3552
+ logRecords.forEach((logRecord) => {
3553
+ collected.push(logRecord);
3554
+ });
3555
+ });
3556
+ });
3557
+ return collected;
3558
+ }
3559
+ function normalizeOtelExport(input) {
3560
+ const logRecords = collectLogRecords(input.payload);
3561
+ const events = [];
3562
+ const errors = [];
3563
+ let droppedRecords = 0;
3564
+ if (logRecords.length === 0) {
3565
+ return {
3566
+ ok: false,
3567
+ events: [],
3568
+ droppedRecords: 0,
3569
+ errors: ["payload does not contain OTEL log records"]
3570
+ };
3571
+ }
3572
+ logRecords.forEach((record, index) => {
3573
+ const envelope = normalizeLogRecord(record, input, String(index + 1));
3574
+ if (envelope === void 0) {
3575
+ droppedRecords += 1;
3576
+ errors.push(`log record ${String(index + 1)} is invalid`);
3577
+ return;
3578
+ }
3579
+ events.push(envelope);
3580
+ });
3581
+ if (errors.length > 0) {
3582
+ const failure2 = {
3583
+ ok: false,
3584
+ events,
3585
+ droppedRecords,
3586
+ errors
3587
+ };
3588
+ return failure2;
3589
+ }
3590
+ const success2 = {
3591
+ ok: true,
3592
+ events,
3593
+ droppedRecords,
3594
+ errors: []
3595
+ };
3596
+ return success2;
3597
+ }
3598
+
3599
+ // packages/collector/src/otel-grpc-receiver.ts
3600
+ function getOtlpLogsProtoPath() {
3601
+ return import_node_path2.default.resolve(__dirname, "../../proto/otlp_logs_service.proto");
3602
+ }
3603
+ function toReadonlyStats2(stats) {
3604
+ return {
3605
+ exportCalls: stats.exportCalls,
3606
+ normalizedEvents: stats.normalizedEvents,
3607
+ droppedRecords: stats.droppedRecords,
3608
+ normalizationFailures: stats.normalizationFailures,
3609
+ sinkFailures: stats.sinkFailures
3610
+ };
3611
+ }
3612
+ function parseHost(address) {
3613
+ const lastColon = address.lastIndexOf(":");
3614
+ if (lastColon < 0) {
3615
+ return "0.0.0.0";
3616
+ }
3617
+ return address.slice(0, lastColon);
3618
+ }
3619
+ function loadLogsServiceDefinition() {
3620
+ const packageDefinition = protoLoader.loadSync(getOtlpLogsProtoPath(), {
3621
+ longs: String,
3622
+ enums: String,
3623
+ defaults: false,
3624
+ oneofs: true
3625
+ });
3626
+ const grpcObject = grpc.loadPackageDefinition(packageDefinition);
3627
+ const otel = grpcObject["opentelemetry"];
3628
+ const protoNamespace = otel?.["proto"];
3629
+ const collector = protoNamespace?.["collector"];
3630
+ const logs = collector?.["logs"];
3631
+ const v1 = logs?.["v1"];
3632
+ const logsService = v1?.["LogsService"];
3633
+ if (logsService?.service === void 0) {
3634
+ throw new Error("failed to load OTLP logs service definition");
3635
+ }
3636
+ return logsService.service;
3637
+ }
3638
+ async function processOtelExportPayload(payload, dependencies) {
3639
+ const normalized = normalizeOtelExport({
3640
+ payload,
3641
+ privacyTier: dependencies.privacyTier
3642
+ });
3643
+ const errors = [...normalized.errors];
3644
+ if (!normalized.ok && dependencies.onNormalizationErrors !== void 0) {
3645
+ dependencies.onNormalizationErrors(normalized.errors);
3646
+ }
3647
+ let sinkFailed = false;
3648
+ if (dependencies.sink !== void 0 && normalized.events.length > 0) {
3649
+ try {
3650
+ await dependencies.sink.ingestOtelEvents(normalized.events);
3651
+ } catch (error) {
3652
+ sinkFailed = true;
3653
+ errors.push(`otel sink failed: ${String(error)}`);
3654
+ }
3655
+ }
3656
+ return {
3657
+ normalizedEvents: normalized.events.length,
3658
+ droppedRecords: normalized.droppedRecords,
3659
+ normalizationFailed: !normalized.ok,
3660
+ sinkFailed,
3661
+ errors
3662
+ };
3663
+ }
3664
+ async function startOtelGrpcReceiver(options = {}) {
3665
+ const server = new grpc.Server();
3666
+ const address = options.address ?? "0.0.0.0:4717";
3667
+ const host = parseHost(address);
3668
+ const stats = {
3669
+ exportCalls: 0,
3670
+ normalizedEvents: 0,
3671
+ droppedRecords: 0,
3672
+ normalizationFailures: 0,
3673
+ sinkFailures: 0
3674
+ };
3675
+ const handler = (call, callback) => {
3676
+ stats.exportCalls += 1;
3677
+ void processOtelExportPayload(call.request, {
3678
+ privacyTier: options.privacyTier ?? 1,
3679
+ ...options.sink !== void 0 ? { sink: options.sink } : {},
3680
+ ...options.onNormalizationErrors !== void 0 ? { onNormalizationErrors: options.onNormalizationErrors } : {}
3681
+ }).then((result) => {
3682
+ stats.normalizedEvents += result.normalizedEvents;
3683
+ stats.droppedRecords += result.droppedRecords;
3684
+ if (result.normalizationFailed) {
3685
+ stats.normalizationFailures += 1;
3686
+ }
3687
+ if (result.sinkFailed) {
3688
+ stats.sinkFailures += 1;
3689
+ }
3690
+ callback(null, {});
3691
+ }).catch((error) => {
3692
+ stats.sinkFailures += 1;
3693
+ const serviceError = Object.assign(new Error(String(error)), {
3694
+ name: "otel_export_failed",
3695
+ code: grpc.status.INTERNAL,
3696
+ details: String(error),
3697
+ metadata: new grpc.Metadata()
3698
+ });
3699
+ callback(serviceError, void 0);
3700
+ });
3701
+ };
3702
+ const logsServiceDefinition = loadLogsServiceDefinition();
3703
+ server.addService(logsServiceDefinition, {
3704
+ Export: handler
3705
+ });
3706
+ const boundPort = await new Promise((resolve, reject) => {
3707
+ server.bindAsync(address, grpc.ServerCredentials.createInsecure(), (error, port) => {
3708
+ if (error !== null) {
3709
+ reject(error);
3710
+ return;
3711
+ }
3712
+ resolve(port);
3713
+ });
3714
+ });
3715
+ return {
3716
+ address: `${host}:${String(boundPort)}`,
3717
+ getStats: () => toReadonlyStats2(stats),
3718
+ close: async () => new Promise((resolve, reject) => {
3719
+ server.tryShutdown((error) => {
3720
+ if (error !== void 0 && error !== null) {
3721
+ reject(error);
3722
+ return;
3723
+ }
3724
+ resolve();
3725
+ });
3726
+ })
3727
+ };
3728
+ }
3729
+
3730
+ // packages/runtime/src/persistence.ts
3731
+ var InMemoryRuntimeClickHouseClient = class {
3732
+ rows = [];
3733
+ async insertJsonEachRow(request) {
3734
+ request.rows.forEach((row) => {
3735
+ this.rows.push(row);
3736
+ });
3737
+ }
3738
+ listRows() {
3739
+ return this.rows;
3740
+ }
3741
+ };
3742
+ var InMemoryRuntimePostgresClient = class {
3743
+ sessionsById = /* @__PURE__ */ new Map();
3744
+ commitsBySha = /* @__PURE__ */ new Map();
3745
+ async upsertSessions(rows) {
3746
+ rows.forEach((row) => {
3747
+ this.sessionsById.set(row.session_id, row);
3748
+ });
3749
+ }
3750
+ async upsertCommits(rows) {
3751
+ rows.forEach((row) => {
3752
+ this.commitsBySha.set(row.sha, row);
3753
+ });
3754
+ }
3755
+ listSessions() {
3756
+ return [...this.sessionsById.values()];
3757
+ }
3758
+ listCommits() {
3759
+ return [...this.commitsBySha.values()];
3760
+ }
3761
+ };
3762
+ var InMemoryRuntimeSessionTraceClient = class {
3763
+ rows = [];
3764
+ async insertJsonEachRow(request) {
3765
+ request.rows.forEach((row) => {
3766
+ this.rows.push(row);
3767
+ });
3768
+ }
3769
+ listRows() {
3770
+ return this.rows;
3771
+ }
3772
+ };
3773
+ var WriterBackedRuntimePersistence = class {
3774
+ clickHouseWriter;
3775
+ clickHouseSessionTraceWriter;
3776
+ postgresSessionWriter;
3777
+ writeFailures;
3778
+ constructor(clickHouseWriter, postgresSessionWriter, clickHouseSessionTraceWriter = void 0) {
3779
+ this.clickHouseWriter = clickHouseWriter;
3780
+ this.postgresSessionWriter = postgresSessionWriter;
3781
+ this.clickHouseSessionTraceWriter = clickHouseSessionTraceWriter;
3782
+ this.writeFailures = [];
3783
+ }
3784
+ async persistAcceptedEvent(event, trace) {
3785
+ const clickHouseWrite = this.clickHouseWriter.writeEvent(event).catch((error) => {
3786
+ this.writeFailures.push(`clickhouse: ${String(error)}`);
3787
+ });
3788
+ const clickHouseSessionTraceWrite = this.clickHouseSessionTraceWriter === void 0 ? Promise.resolve() : this.clickHouseSessionTraceWriter.writeTrace(trace).catch((error) => {
3789
+ this.writeFailures.push(`clickhouse_session_traces: ${String(error)}`);
3790
+ });
3791
+ const postgresWrite = this.postgresSessionWriter.writeTrace(trace).catch((error) => {
3792
+ this.writeFailures.push(`postgres: ${String(error)}`);
3793
+ });
3794
+ await Promise.all([clickHouseWrite, clickHouseSessionTraceWrite, postgresWrite]);
3795
+ }
3796
+ getSnapshot() {
3797
+ return {
3798
+ clickHouseRows: [],
3799
+ clickHouseSessionTraceRows: [],
3800
+ postgresSessionRows: [],
3801
+ postgresCommitRows: [],
3802
+ writeFailures: this.writeFailures
3803
+ };
3804
+ }
3805
+ };
3806
+ var InMemoryRuntimePersistence = class extends WriterBackedRuntimePersistence {
3807
+ clickHouseClient;
3808
+ clickHouseSessionTraceClient;
3809
+ postgresClient;
3810
+ constructor() {
3811
+ const clickHouseClient = new InMemoryRuntimeClickHouseClient();
3812
+ const clickHouseSessionTraceClient = new InMemoryRuntimeSessionTraceClient();
3813
+ const postgresClient = new InMemoryRuntimePostgresClient();
3814
+ super(
3815
+ new ClickHouseEventWriter(clickHouseClient),
3816
+ new PostgresSessionWriter(postgresClient),
3817
+ new ClickHouseSessionTraceWriter(clickHouseSessionTraceClient)
3818
+ );
3819
+ this.clickHouseClient = clickHouseClient;
3820
+ this.clickHouseSessionTraceClient = clickHouseSessionTraceClient;
3821
+ this.postgresClient = postgresClient;
3822
+ }
3823
+ getSnapshot() {
3824
+ const base = super.getSnapshot();
3825
+ return {
3826
+ ...base,
3827
+ clickHouseRows: this.clickHouseClient.listRows(),
3828
+ clickHouseSessionTraceRows: this.clickHouseSessionTraceClient.listRows(),
3829
+ postgresSessionRows: this.postgresClient.listSessions(),
3830
+ postgresCommitRows: this.postgresClient.listCommits()
3831
+ };
3832
+ }
3833
+ };
3834
+
3835
+ // packages/runtime/src/projector.ts
3836
+ function asRecord5(value) {
3837
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3838
+ return void 0;
3839
+ }
3840
+ return value;
3841
+ }
3842
+ function readString4(payload, keys) {
3843
+ if (payload === void 0) {
3844
+ return void 0;
3845
+ }
3846
+ for (const key of keys) {
3847
+ const value = payload[key];
3848
+ if (typeof value === "string" && value.length > 0) {
3849
+ return value;
3850
+ }
3851
+ }
3852
+ return void 0;
3853
+ }
3854
+ function readNumber3(payload, keys) {
3855
+ if (payload === void 0) {
3856
+ return void 0;
3857
+ }
3858
+ for (const key of keys) {
3859
+ const value = payload[key];
3860
+ if (typeof value === "number" && Number.isFinite(value)) {
3861
+ return value;
3862
+ }
3863
+ }
3864
+ return void 0;
3865
+ }
3866
+ function readNonEmptyString(payload, keys) {
3867
+ const value = readString4(payload, keys);
3868
+ if (value === void 0 || value.length === 0) {
3869
+ return void 0;
3870
+ }
3871
+ return value;
3872
+ }
3873
+ function readStringArray2(payload, keys) {
3874
+ if (payload === void 0) {
3875
+ return [];
3876
+ }
3877
+ for (const key of keys) {
3878
+ const value = payload[key];
3879
+ if (Array.isArray(value)) {
3880
+ const items = value.filter((item) => typeof item === "string" && item.length > 0);
3881
+ return items;
3882
+ }
3883
+ }
3884
+ return [];
3885
+ }
3886
+ function uniqueMerge(existing, additions) {
3887
+ const set = new Set(existing);
3888
+ additions.forEach((item) => {
3889
+ set.add(item);
3890
+ });
3891
+ return [...set];
3892
+ }
3893
+ function timelineContainsEvent(trace, eventId) {
3894
+ return trace.timeline.some((event) => event.id === eventId);
3895
+ }
3896
+ function addCamelAlias(target, snakeKey, camelKey) {
3897
+ if (target[snakeKey] !== void 0 && target[camelKey] === void 0) {
3898
+ target[camelKey] = target[snakeKey];
3899
+ }
3900
+ }
3901
+ function buildNormalizedDetails(payload) {
3902
+ const record = asRecord5(payload);
3903
+ if (record === void 0) {
3904
+ return void 0;
3905
+ }
3906
+ const normalized = { ...record };
3907
+ addCamelAlias(normalized, "tool_name", "toolName");
3908
+ addCamelAlias(normalized, "tool_input", "toolInput");
3909
+ addCamelAlias(normalized, "tool_response", "toolResponse");
3910
+ addCamelAlias(normalized, "tool_duration_ms", "toolDurationMs");
3911
+ addCamelAlias(normalized, "prompt_text", "promptText");
3912
+ addCamelAlias(normalized, "hook_event_name", "hookEventName");
3913
+ addCamelAlias(normalized, "tool_use_id", "toolUseId");
3914
+ addCamelAlias(normalized, "last_assistant_message", "lastAssistantMessage");
3915
+ addCamelAlias(normalized, "response_text", "responseText");
3916
+ addCamelAlias(normalized, "file_path", "filePath");
3917
+ return normalized;
3918
+ }
3919
+ function toTimelineEvent(envelope) {
3920
+ const payload = asRecord5(envelope.payload);
3921
+ const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]);
3922
+ const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]);
3923
+ const costUsd = readNumber3(payload, ["cost_usd", "costUsd"]);
3924
+ const details = buildNormalizedDetails(envelope.payload);
3925
+ return {
3926
+ id: envelope.eventId,
3927
+ type: envelope.eventType,
3928
+ timestamp: envelope.eventTimestamp,
3929
+ ...envelope.promptId !== void 0 ? { promptId: envelope.promptId } : {},
3930
+ ...costUsd !== void 0 ? { costUsd } : {},
3931
+ ...inputTokens !== void 0 && outputTokens !== void 0 ? {
3932
+ tokens: {
3933
+ input: inputTokens,
3934
+ output: outputTokens
3935
+ }
3936
+ } : {},
3937
+ ...details !== void 0 ? { details } : {}
3938
+ };
3939
+ }
3940
+ function toBaseTrace(envelope) {
3941
+ const payload = asRecord5(envelope.payload);
3942
+ const gitRepo = readString4(payload, ["git_repo", "gitRepo"]);
3943
+ const gitBranch = readString4(payload, ["git_branch", "gitBranch"]);
3944
+ const projectPath = readString4(payload, ["project_path", "projectPath"]);
3945
+ const userId = readString4(payload, ["user_id", "userId"]) ?? "unknown_user";
3946
+ return {
3947
+ sessionId: envelope.sessionId,
3948
+ agentType: "claude_code",
3949
+ user: {
3950
+ id: userId
3951
+ },
3952
+ environment: {
3953
+ ...projectPath !== void 0 ? { projectPath } : {},
3954
+ ...gitRepo !== void 0 ? { gitRepo } : {},
3955
+ ...gitBranch !== void 0 ? { gitBranch } : {}
3956
+ },
3957
+ startedAt: envelope.eventTimestamp,
3958
+ activeDurationMs: 0,
3959
+ timeline: [],
3960
+ metrics: {
3961
+ promptCount: 0,
3962
+ apiCallCount: 0,
3963
+ toolCallCount: 0,
3964
+ totalCostUsd: 0,
3965
+ totalInputTokens: 0,
3966
+ totalOutputTokens: 0,
3967
+ linesAdded: 0,
3968
+ linesRemoved: 0,
3969
+ filesTouched: [],
3970
+ modelsUsed: [],
3971
+ toolsUsed: []
3972
+ },
3973
+ git: {
3974
+ commits: [],
3975
+ pullRequests: []
3976
+ }
3977
+ };
3978
+ }
3979
+ function updateDurationMs(startedAt, endedAt) {
3980
+ const startMs = Date.parse(startedAt);
3981
+ const endMs = Date.parse(endedAt);
3982
+ if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) {
3983
+ return 0;
3984
+ }
3985
+ return endMs - startMs;
3986
+ }
3987
+ function shouldMarkEnded(eventType) {
3988
+ const normalized = eventType.toLowerCase();
3989
+ return normalized === "session_end" || normalized === "sessionend" || normalized === "stop" || normalized === "task_completed" || normalized === "taskcompleted";
3990
+ }
3991
+ function incrementMetricIfMatches(metric, eventType, match) {
3992
+ const normalized = eventType.toLowerCase();
3993
+ const shouldIncrement = match.some((entry) => normalized.includes(entry));
3994
+ return shouldIncrement ? metric + 1 : metric;
3995
+ }
3996
+ function toUpdatedTrace(existing, envelope) {
3997
+ if (timelineContainsEvent(existing, envelope.eventId)) {
3998
+ return existing;
3999
+ }
4000
+ const payload = asRecord5(envelope.payload);
4001
+ const timelineEvent = toTimelineEvent(envelope);
4002
+ const mergedTimeline = [...existing.timeline, timelineEvent];
4003
+ const endedAt = shouldMarkEnded(envelope.eventType) ? envelope.eventTimestamp : existing.endedAt;
4004
+ const latestTime = endedAt ?? envelope.eventTimestamp;
4005
+ const cost = readNumber3(payload, ["cost_usd", "costUsd"]) ?? 0;
4006
+ const inputTokens = readNumber3(payload, ["input_tokens", "inputTokens"]) ?? 0;
4007
+ const outputTokens = readNumber3(payload, ["output_tokens", "outputTokens"]) ?? 0;
4008
+ const linesAdded = readNumber3(payload, ["lines_added", "linesAdded"]) ?? 0;
4009
+ const linesRemoved = readNumber3(payload, ["lines_removed", "linesRemoved"]) ?? 0;
4010
+ const model = readString4(payload, ["model"]);
4011
+ const toolName = readString4(payload, ["tool_name", "toolName"]);
4012
+ const filesTouchedFromArray = readStringArray2(payload, ["files_changed", "filesChanged"]);
4013
+ const singleFileTouched = readString4(payload, ["file_path", "filePath"]);
4014
+ const filesTouched = uniqueMerge(
4015
+ existing.metrics.filesTouched,
4016
+ singleFileTouched !== void 0 ? [...filesTouchedFromArray, singleFileTouched] : filesTouchedFromArray
4017
+ );
4018
+ const existingCommits = [...existing.git.commits];
4019
+ const commitSha = readString4(payload, ["commit_sha", "commitSha"]);
4020
+ const isCommitEvent = payload !== void 0 && payload["is_commit"] === true;
4021
+ const commitMessage = readNonEmptyString(payload, ["commit_message", "commitMessage"]);
4022
+ if (commitSha !== void 0 && (isCommitEvent || commitMessage !== void 0) && !existingCommits.some((commit) => commit.sha === commitSha)) {
4023
+ const commitLinesAdded = readNumber3(payload, ["lines_added", "linesAdded"]);
4024
+ const commitLinesRemoved = readNumber3(payload, ["lines_removed", "linesRemoved"]);
4025
+ existingCommits.push({
4026
+ sha: commitSha,
4027
+ ...envelope.promptId !== void 0 ? { promptId: envelope.promptId } : {},
4028
+ ...commitMessage !== void 0 ? { message: commitMessage } : {},
4029
+ ...commitLinesAdded !== void 0 ? { linesAdded: commitLinesAdded } : {},
4030
+ ...commitLinesRemoved !== void 0 ? { linesRemoved: commitLinesRemoved } : {},
4031
+ committedAt: envelope.eventTimestamp
4032
+ });
4033
+ }
4034
+ const existingPullRequests = [...existing.git.pullRequests];
4035
+ const prUrl = readString4(payload, ["pr_url", "prUrl"]);
4036
+ const prRepo = readString4(payload, ["pr_repo", "prRepo"]);
4037
+ const prNumberRaw = readNumber3(payload, ["pr_number", "prNumber"]);
4038
+ if (prUrl !== void 0 && prRepo !== void 0 && prNumberRaw !== void 0) {
4039
+ const alreadyTracked = existingPullRequests.some((pr) => pr.prNumber === prNumberRaw && pr.repo === prRepo);
4040
+ if (!alreadyTracked) {
4041
+ existingPullRequests.push({
4042
+ repo: prRepo,
4043
+ prNumber: prNumberRaw,
4044
+ state: "open",
4045
+ url: prUrl
4046
+ });
4047
+ }
4048
+ }
4049
+ return {
4050
+ ...existing,
4051
+ ...endedAt !== void 0 ? { endedAt } : {},
4052
+ activeDurationMs: updateDurationMs(existing.startedAt, latestTime),
4053
+ timeline: mergedTimeline,
4054
+ metrics: {
4055
+ promptCount: incrementMetricIfMatches(existing.metrics.promptCount, envelope.eventType, ["prompt"]),
4056
+ apiCallCount: incrementMetricIfMatches(existing.metrics.apiCallCount, envelope.eventType, ["api"]),
4057
+ toolCallCount: incrementMetricIfMatches(existing.metrics.toolCallCount, envelope.eventType, ["tool"]),
4058
+ totalCostUsd: Number((existing.metrics.totalCostUsd + cost).toFixed(6)),
4059
+ totalInputTokens: existing.metrics.totalInputTokens + inputTokens,
4060
+ totalOutputTokens: existing.metrics.totalOutputTokens + outputTokens,
4061
+ linesAdded: existing.metrics.linesAdded + linesAdded,
4062
+ linesRemoved: existing.metrics.linesRemoved + linesRemoved,
4063
+ filesTouched,
4064
+ modelsUsed: model !== void 0 ? uniqueMerge(existing.metrics.modelsUsed, [model]) : existing.metrics.modelsUsed,
4065
+ toolsUsed: toolName !== void 0 ? uniqueMerge(existing.metrics.toolsUsed, [toolName]) : existing.metrics.toolsUsed
4066
+ },
4067
+ git: {
4068
+ ...existing.git,
4069
+ commits: existingCommits,
4070
+ pullRequests: existingPullRequests
4071
+ }
4072
+ };
4073
+ }
4074
+ function projectEnvelopeToTrace(currentTrace, envelope) {
4075
+ const base = currentTrace ?? toBaseTrace(envelope);
4076
+ return toUpdatedTrace(base, envelope);
4077
+ }
4078
+
4079
+ // packages/runtime/src/runtime.ts
4080
+ function toAddress2(server) {
4081
+ const address = server.address();
4082
+ if (address === null) {
4083
+ return "unknown";
4084
+ }
4085
+ if (typeof address === "string") {
4086
+ return address;
4087
+ }
4088
+ return `${address.address}:${String(address.port)}`;
4089
+ }
4090
+ async function listen2(server, port, host) {
4091
+ await new Promise((resolve, reject) => {
4092
+ server.listen(port, host, () => resolve());
4093
+ server.once("error", (error) => reject(error));
4094
+ });
4095
+ }
4096
+ async function close2(server) {
4097
+ await new Promise((resolve, reject) => {
4098
+ server.close((error) => {
4099
+ if (error !== void 0 && error !== null) {
4100
+ reject(error);
4101
+ return;
4102
+ }
4103
+ resolve();
4104
+ });
4105
+ });
4106
+ }
4107
+ async function projectAndPersistEvent(event, sessionRepository, persistence) {
4108
+ const current = sessionRepository.getBySessionId(event.sessionId);
4109
+ const projected = projectEnvelopeToTrace(current, event);
4110
+ sessionRepository.upsert(projected);
4111
+ await persistence.persistAcceptedEvent(event, projected);
4112
+ }
4113
+ function createRuntimeOtelSink(runtime) {
4114
+ return runtime.collectorService.otelSink;
4115
+ }
4116
+ function resolveRuntimeOptions(input) {
4117
+ if (typeof input === "number") {
4118
+ return {
4119
+ startedAtMs: input,
4120
+ persistence: new InMemoryRuntimePersistence(),
4121
+ dailyCostReader: void 0
4122
+ };
4123
+ }
4124
+ const startedAtMs = input?.startedAtMs ?? Date.now();
4125
+ const persistence = input?.persistence ?? new InMemoryRuntimePersistence();
4126
+ return {
4127
+ startedAtMs,
4128
+ persistence,
4129
+ dailyCostReader: input?.dailyCostReader
4130
+ };
4131
+ }
4132
+ function createInMemoryRuntime(input) {
4133
+ const options = resolveRuntimeOptions(input);
4134
+ const sessionRepository = new InMemorySessionRepository();
4135
+ const persistence = options.persistence;
4136
+ const collectorService = createEnvelopeCollectorService({
4137
+ startedAtMs: options.startedAtMs,
4138
+ onAcceptedEvent: async (event) => {
4139
+ await projectAndPersistEvent(event, sessionRepository, persistence);
4140
+ }
4141
+ });
4142
+ const collectorDependencies = collectorService.dependencies;
4143
+ const collectorStore = collectorService.store;
4144
+ const apiDependencies = {
4145
+ startedAtMs: options.startedAtMs,
4146
+ repository: sessionRepository,
4147
+ ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {}
4148
+ };
4149
+ return {
4150
+ sessionRepository,
4151
+ collectorStore,
4152
+ collectorDependencies,
4153
+ collectorService,
4154
+ persistence,
4155
+ ...options.dailyCostReader !== void 0 ? { dailyCostReader: options.dailyCostReader } : {},
4156
+ handleCollectorRaw: collectorService.handleRaw,
4157
+ handleApiRaw: (request) => handleApiRawHttpRequest(request, apiDependencies)
4158
+ };
4159
+ }
4160
+ async function startInMemoryRuntimeServers(runtime, options = {}) {
4161
+ const host = options.host ?? "127.0.0.1";
4162
+ const collectorPort = options.collectorPort ?? 8317;
4163
+ const apiPort = options.apiPort ?? 8318;
4164
+ const enableCollectorServer = options.enableCollectorServer ?? true;
4165
+ const enableApiServer = options.enableApiServer ?? true;
4166
+ const enableOtelReceiver = options.enableOtelReceiver ?? enableCollectorServer;
4167
+ if (!enableCollectorServer && !enableApiServer) {
4168
+ throw new Error("runtime requires at least one enabled HTTP service");
4169
+ }
4170
+ const otelGrpcAddress = options.otelGrpcAddress ?? `${host}:4717`;
4171
+ const collectorServer = enableCollectorServer ? import_node_http2.default.createServer(createCollectorHttpHandler(runtime.collectorDependencies)) : void 0;
4172
+ const apiServer = enableApiServer ? import_node_http2.default.createServer(
4173
+ createApiHttpHandler({
4174
+ startedAtMs: runtime.collectorDependencies.startedAtMs,
4175
+ repository: runtime.sessionRepository,
4176
+ ...runtime.dailyCostReader !== void 0 ? { dailyCostReader: runtime.dailyCostReader } : {}
4177
+ })
4178
+ ) : void 0;
4179
+ let otelReceiver;
4180
+ try {
4181
+ if (enableOtelReceiver) {
4182
+ otelReceiver = await startOtelGrpcReceiver({
4183
+ address: otelGrpcAddress,
4184
+ privacyTier: options.otelPrivacyTier ?? 2,
4185
+ sink: createRuntimeOtelSink(runtime)
4186
+ });
4187
+ }
4188
+ if (collectorServer !== void 0) {
4189
+ await listen2(collectorServer, collectorPort, host);
4190
+ }
4191
+ if (apiServer !== void 0) {
4192
+ await listen2(apiServer, apiPort, host);
4193
+ }
4194
+ } catch (error) {
4195
+ if (apiServer !== void 0) {
4196
+ await close2(apiServer).catch(() => void 0);
4197
+ }
4198
+ if (collectorServer !== void 0) {
4199
+ await close2(collectorServer).catch(() => void 0);
4200
+ }
4201
+ if (otelReceiver !== void 0) {
4202
+ await otelReceiver.close().catch(() => void 0);
4203
+ }
4204
+ throw error;
4205
+ }
4206
+ return {
4207
+ ...collectorServer !== void 0 ? { collectorAddress: toAddress2(collectorServer) } : {},
4208
+ ...apiServer !== void 0 ? { apiAddress: toAddress2(apiServer) } : {},
4209
+ ...otelReceiver !== void 0 ? { otelGrpcAddress: otelReceiver.address } : {},
4210
+ close: async () => {
4211
+ if (collectorServer !== void 0) {
4212
+ await close2(collectorServer);
4213
+ }
4214
+ if (apiServer !== void 0) {
4215
+ await close2(apiServer);
4216
+ }
4217
+ if (otelReceiver !== void 0) {
4218
+ await otelReceiver.close();
4219
+ }
4220
+ }
4221
+ };
4222
+ }
4223
+
4224
+ // packages/runtime/src/sqlite-runtime.ts
4225
+ var SqliteSessionTraceInsertAdapter = class {
4226
+ sqlite;
4227
+ constructor(sqlite) {
4228
+ this.sqlite = sqlite;
4229
+ }
4230
+ async insertJsonEachRow(request) {
4231
+ this.sqlite.insertSessionTraces(request.rows);
4232
+ }
4233
+ };
4234
+ var SqlitePersistence = class {
4235
+ eventWriter;
4236
+ sessionTraceWriter;
4237
+ postgresWriter;
4238
+ writeFailures = [];
4239
+ constructor(sqlite) {
4240
+ this.eventWriter = new ClickHouseEventWriter(sqlite);
4241
+ this.sessionTraceWriter = new ClickHouseSessionTraceWriter(new SqliteSessionTraceInsertAdapter(sqlite));
4242
+ this.postgresWriter = new PostgresSessionWriter(sqlite);
4243
+ }
4244
+ async persistAcceptedEvent(event, trace) {
4245
+ const eventWrite = this.eventWriter.writeEvent(event).catch((error) => {
4246
+ this.writeFailures.push(`sqlite_events: ${String(error)}`);
4247
+ });
4248
+ const traceWrite = this.sessionTraceWriter.writeTrace(trace).catch((error) => {
4249
+ this.writeFailures.push(`sqlite_traces: ${String(error)}`);
4250
+ });
4251
+ const sessionWrite = this.postgresWriter.writeTrace(trace).catch((error) => {
4252
+ this.writeFailures.push(`sqlite_sessions: ${String(error)}`);
4253
+ });
4254
+ await Promise.all([eventWrite, traceWrite, sessionWrite]);
4255
+ }
4256
+ getSnapshot() {
4257
+ return {
4258
+ clickHouseRows: [],
4259
+ clickHouseSessionTraceRows: [],
4260
+ postgresSessionRows: [],
4261
+ postgresCommitRows: [],
4262
+ writeFailures: [...this.writeFailures]
4263
+ };
4264
+ }
4265
+ };
4266
+ var SqliteDailyCostReader = class {
4267
+ sqlite;
4268
+ constructor(sqlite) {
4269
+ this.sqlite = sqlite;
4270
+ }
4271
+ async listDailyCosts(limit) {
4272
+ const rows = this.sqlite.listDailyCosts(limit);
4273
+ return rows.map((r) => ({
4274
+ date: r.date,
4275
+ totalCostUsd: r.totalCostUsd,
4276
+ sessionCount: r.sessionCount,
4277
+ promptCount: 0,
4278
+ toolCallCount: 0
4279
+ }));
4280
+ }
4281
+ };
4282
+ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
4283
+ const traceRows = sqlite.listSessionTraces(limit);
4284
+ const traces = traceRows.map((row) => {
4285
+ const trace = toAgentSessionTraceFromClickHouseRow(row);
4286
+ const eventRows = sqlite.listEventsBySessionId(row.session_id, eventLimit);
4287
+ const timeline = eventRows.map((e) => toTimelineEventFromClickHouseRow(e));
4288
+ let commits = trace.git.commits.filter((c) => !c.sha.startsWith("placeholder_"));
4289
+ const pgCommits = sqlite.listCommitsBySessionId(row.session_id);
4290
+ if (pgCommits.length > 0) {
4291
+ const mapped = pgCommits.map((c) => ({
4292
+ sha: c.sha,
4293
+ ...c.prompt_id !== null ? { promptId: c.prompt_id } : {},
4294
+ ...c.message !== null ? { message: c.message } : {},
4295
+ ...c.lines_added > 0 ? { linesAdded: c.lines_added } : {},
4296
+ ...c.lines_removed > 0 ? { linesRemoved: c.lines_removed } : {},
4297
+ ...c.committed_at !== null ? { committedAt: c.committed_at } : {}
4298
+ }));
4299
+ const pgShas = new Set(mapped.map((c) => c.sha));
4300
+ const extra = commits.filter((c) => !pgShas.has(c.sha));
4301
+ commits = [...mapped, ...extra];
4302
+ }
4303
+ return {
4304
+ ...trace,
4305
+ timeline,
4306
+ git: { ...trace.git, commits }
4307
+ };
4308
+ });
4309
+ for (const trace of traces) {
4310
+ runtime.sessionRepository.upsert(trace);
4311
+ }
4312
+ return traces.length;
4313
+ }
4314
+ function createSqliteBackedRuntime(options) {
4315
+ const sqlite = new SqliteClient(options.dbPath);
4316
+ const persistence = new SqlitePersistence(sqlite);
4317
+ const dailyCostReader = new SqliteDailyCostReader(sqlite);
4318
+ const runtime = createInMemoryRuntime({
4319
+ ...options.startedAtMs !== void 0 ? { startedAtMs: options.startedAtMs } : {},
4320
+ persistence,
4321
+ dailyCostReader
4322
+ });
4323
+ const hydratedCount = hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
4324
+ const syncIntervalMs = options.syncIntervalMs ?? 5e3;
4325
+ let syncInFlight = false;
4326
+ const syncInterval = setInterval(() => {
4327
+ if (syncInFlight) return;
4328
+ syncInFlight = true;
4329
+ try {
4330
+ hydrateFromSqlite(runtime, sqlite, options.bootstrapLimit, options.eventLimit);
4331
+ } finally {
4332
+ syncInFlight = false;
4333
+ }
4334
+ }, syncIntervalMs);
4335
+ return {
4336
+ runtime,
4337
+ sqlite,
4338
+ hydratedCount,
4339
+ close: async () => {
4340
+ clearInterval(syncInterval);
4341
+ sqlite.close();
4342
+ }
4343
+ };
4344
+ }
4345
+
4346
+ // packages/runtime/src/standalone-entry.ts
4347
+ function readNumberEnv(name, fallback) {
4348
+ const raw = process.env[name];
4349
+ if (raw === void 0) return fallback;
4350
+ const parsed = Number(raw);
4351
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
4352
+ return parsed;
4353
+ }
4354
+ function readPrivacyTierEnv(name) {
4355
+ const raw = process.env[name];
4356
+ if (raw === "1") return 1;
4357
+ if (raw === "3") return 3;
4358
+ return 2;
4359
+ }
4360
+ function resolveDefaultSqlitePath() {
4361
+ const dataDir = import_node_path3.default.join(import_node_os2.default.homedir(), ".agent-trace");
4362
+ import_node_fs2.default.mkdirSync(dataDir, { recursive: true });
4363
+ return import_node_path3.default.join(dataDir, "data.db");
4364
+ }
4365
+ async function main() {
4366
+ const host = process.env["RUNTIME_HOST"] ?? "127.0.0.1";
4367
+ const collectorPort = readNumberEnv("COLLECTOR_PORT", 8317);
4368
+ const apiPort = readNumberEnv("API_PORT", 8318);
4369
+ const dashboardPort = readNumberEnv("DASHBOARD_PORT", 3100);
4370
+ const otelPrivacyTier = readPrivacyTierEnv("OTEL_PRIVACY_TIER");
4371
+ const startedAtMs = Date.now();
4372
+ const sqlitePath = process.env["SQLITE_DB_PATH"] ?? resolveDefaultSqlitePath();
4373
+ let sqliteHandle;
4374
+ let runtime;
4375
+ let hydratedCount = 0;
4376
+ const hasExternalDb = process.env["CLICKHOUSE_URL"] !== void 0 && process.env["POSTGRES_CONNECTION_STRING"] !== void 0;
4377
+ if (hasExternalDb) {
4378
+ process.stderr.write("standalone mode does not support CLICKHOUSE_URL/POSTGRES_CONNECTION_STRING.\n");
4379
+ process.stderr.write("use Docker or the full runtime for external database support.\n");
4380
+ process.exit(1);
4381
+ }
4382
+ sqliteHandle = createSqliteBackedRuntime({ dbPath: sqlitePath, startedAtMs });
4383
+ runtime = sqliteHandle.runtime;
4384
+ hydratedCount = sqliteHandle.hydratedCount;
4385
+ let servers;
4386
+ try {
4387
+ servers = await startInMemoryRuntimeServers(runtime, {
4388
+ host,
4389
+ collectorPort,
4390
+ apiPort,
4391
+ enableCollectorServer: true,
4392
+ enableApiServer: true,
4393
+ enableOtelReceiver: false,
4394
+ otelPrivacyTier
4395
+ });
4396
+ } catch (error) {
4397
+ if (sqliteHandle !== void 0) await sqliteHandle.close();
4398
+ throw error;
4399
+ }
4400
+ const apiBaseUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${String(apiPort)}`;
4401
+ let dashboardAddress;
4402
+ try {
4403
+ const dashboard = await startDashboardServer({
4404
+ host,
4405
+ port: dashboardPort,
4406
+ apiBaseUrl,
4407
+ startedAtMs
4408
+ });
4409
+ dashboardAddress = dashboard.address;
4410
+ const originalClose = servers.close;
4411
+ servers = {
4412
+ ...servers,
4413
+ close: async () => {
4414
+ await dashboard.close();
4415
+ await originalClose();
4416
+ }
4417
+ };
4418
+ } catch {
4419
+ }
4420
+ process.stdout.write("\n");
4421
+ process.stdout.write(" agent-trace\n");
4422
+ process.stdout.write(` database: ${sqlitePath}
4423
+ `);
4424
+ if (hydratedCount > 0) {
4425
+ process.stdout.write(` sessions loaded: ${String(hydratedCount)}
4426
+ `);
4427
+ }
4428
+ process.stdout.write("\n");
4429
+ if (servers.collectorAddress !== void 0) {
4430
+ process.stdout.write(` collector http://${servers.collectorAddress}
4431
+ `);
4432
+ }
4433
+ if (servers.apiAddress !== void 0) {
4434
+ process.stdout.write(` api http://${servers.apiAddress}
4435
+ `);
4436
+ }
4437
+ if (servers.otelGrpcAddress !== void 0) {
4438
+ process.stdout.write(` otel grpc ${servers.otelGrpcAddress}
4439
+ `);
4440
+ }
4441
+ if (dashboardAddress !== void 0) {
4442
+ process.stdout.write(` dashboard http://${dashboardAddress}
4443
+ `);
4444
+ }
4445
+ process.stdout.write("\n");
4446
+ const shutdown = async () => {
4447
+ await servers.close();
4448
+ if (sqliteHandle !== void 0) await sqliteHandle.close();
4449
+ process.exit(0);
4450
+ };
4451
+ process.on("SIGINT", () => {
4452
+ void shutdown();
4453
+ });
4454
+ process.on("SIGTERM", () => {
4455
+ void shutdown();
4456
+ });
4457
+ }
4458
+ void main().catch((error) => {
4459
+ process.stderr.write(`${String(error)}
4460
+ `);
4461
+ process.exit(1);
4462
+ });