@misha_misha/agentwatch 0.0.4 → 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 (57) hide show
  1. package/README.md +11 -4
  2. package/dist/index.js +3258 -491
  3. package/dist/web/assets/ActiveShapeUtils-CtEvtbYg.js +1 -0
  4. package/dist/web/assets/Bar-DEgd_1jq.js +1 -0
  5. package/dist/web/assets/BarChart-CLHGmIz1.js +1 -0
  6. package/dist/web/assets/CartesianChart-DgJKUG1O.js +1 -0
  7. package/dist/web/assets/CategoricalChart-DEGrE0Qv.js +33 -0
  8. package/dist/web/assets/ErrorBarContext-BMnGmrrB.js +1 -0
  9. package/dist/web/assets/Legend-CEa2zCmi.js +4 -0
  10. package/dist/web/assets/Line-CCyE37cb.js +1 -0
  11. package/dist/web/assets/LineChart-DmMubyEd.js +1 -0
  12. package/dist/web/assets/ProjectActivity-BG5hEcJl.js +1 -0
  13. package/dist/web/assets/ProjectYield-vVxXaT6e.js +1 -0
  14. package/dist/web/assets/SessionActivity-BUzJpWwc.js +1 -0
  15. package/dist/web/assets/SessionCompaction-Dx3tbM9N.js +1 -0
  16. package/dist/web/assets/{SessionDiffs-LZrXV0AY.js → SessionDiffs-CDUkVqua.js} +3 -3
  17. package/dist/web/assets/SessionGraph-DWP1YyP0.js +1 -0
  18. package/dist/web/assets/SessionReplay-kz1EhiaI.js +1 -0
  19. package/dist/web/assets/SessionTokens-BjuXcVM2.js +1 -0
  20. package/dist/web/assets/SessionYield-DXF-Xb5T.js +1 -0
  21. package/dist/web/assets/Settings-CXCDYvsc.js +1 -0
  22. package/dist/web/assets/Trends-wdNqOtnn.js +1 -0
  23. package/dist/web/assets/arrow-left-B-iQ08P0.js +1 -0
  24. package/dist/web/assets/chart-column-BwF4X7mz.js +1 -0
  25. package/dist/web/assets/clsx-DnEFlO87.js +1 -0
  26. package/dist/web/assets/{clsx-DsHpp3Uj.js → createLucideIcon-Bjwrp8ZV.js} +1 -1
  27. package/dist/web/assets/dist-BYqqB4pa.js +1 -0
  28. package/dist/web/assets/file-pen-CHXP3wF7.js +1 -0
  29. package/dist/web/assets/getRadiusAndStrokeWidthFromDot-CFcPogcT.js +1 -0
  30. package/dist/web/assets/graphicalItemSelectors-DJeUEv0O.js +1 -0
  31. package/dist/web/assets/index-CJArQihV.js +2 -0
  32. package/dist/web/assets/index-CTAomrBX.css +1 -0
  33. package/dist/web/assets/play-ONoP4Jfr.js +1 -0
  34. package/dist/web/assets/tooltipContext-BT7phkqZ.js +1 -0
  35. package/dist/web/assets/triangle-alert-BhvAiLmv.js +1 -0
  36. package/dist/web/assets/useMutation-CfQYV-vU.js +1 -0
  37. package/dist/web/index.html +12 -11
  38. package/package.json +1 -1
  39. package/dist/web/assets/CartesianChart-CZSKepVZ.js +0 -33
  40. package/dist/web/assets/LineChart-BYjz-1bE.js +0 -1
  41. package/dist/web/assets/SessionCompaction-Duzo69wv.js +0 -1
  42. package/dist/web/assets/SessionGraph-Bb1BdCWf.js +0 -1
  43. package/dist/web/assets/SessionReplay-C3AZoYFc.js +0 -1
  44. package/dist/web/assets/SessionTokens-B6wfOhyn.js +0 -1
  45. package/dist/web/assets/Settings-HKanGbBq.js +0 -1
  46. package/dist/web/assets/Trends-p9DnVxWQ.js +0 -1
  47. package/dist/web/assets/arrow-left-Bg6VjX8-.js +0 -1
  48. package/dist/web/assets/chart-column-Brz7pC96.js +0 -1
  49. package/dist/web/assets/dist-w_zu0rIf.js +0 -1
  50. package/dist/web/assets/file-pen-DWwu4Q-r.js +0 -1
  51. package/dist/web/assets/graphicalItemSelectors-Dk1a_HU_.js +0 -4
  52. package/dist/web/assets/index-Bu9taSiK.js +0 -2
  53. package/dist/web/assets/index-CJPUO3dh.css +0 -1
  54. package/dist/web/assets/play-B0mJPrwl.js +0 -1
  55. package/dist/web/assets/triangle-alert-Bx2lGvGN.js +0 -1
  56. package/dist/web/assets/useMutation-BvFLVjYz.js +0 -1
  57. /package/dist/web/assets/{format-zw6IoTwZ.js → format-zsCsqELF.js} +0 -0
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
6
  var __esm = (fn, res) => function __init() {
5
7
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
8
  };
@@ -8,10 +10,70 @@ var __export = (target, all) => {
8
10
  for (var name in all)
9
11
  __defProp(target, name, { get: all[name], enumerable: true });
10
12
  };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
11
22
 
12
23
  // src/util/cost.ts
24
+ import { existsSync, readFileSync } from "fs";
25
+ import { homedir } from "os";
26
+ import { join } from "path";
27
+ function pricingFilePath() {
28
+ return process.env.AGENTWATCH_PRICING_PATH ?? join(homedir(), ".agentwatch", "pricing.json");
29
+ }
30
+ function coerceRate(v) {
31
+ if (!v || typeof v !== "object") return null;
32
+ const r = v;
33
+ const isNonNegNumber = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0;
34
+ if (!isNonNegNumber(r.input) || !isNonNegNumber(r.cacheCreate) || !isNonNegNumber(r.cacheRead) || !isNonNegNumber(r.output)) {
35
+ return null;
36
+ }
37
+ return {
38
+ input: r.input,
39
+ cacheCreate: r.cacheCreate,
40
+ cacheRead: r.cacheRead,
41
+ output: r.output
42
+ };
43
+ }
44
+ function loadRates() {
45
+ if (cachedRates) return cachedRates;
46
+ const path12 = pricingFilePath();
47
+ const merged = { ...DEFAULT_RATES };
48
+ if (existsSync(path12)) {
49
+ try {
50
+ const raw = readFileSync(path12, "utf8");
51
+ const doc = JSON.parse(raw);
52
+ if (doc && typeof doc === "object") {
53
+ for (const [model, value] of Object.entries(
54
+ doc
55
+ )) {
56
+ const rate = coerceRate(value);
57
+ if (rate) merged[model] = rate;
58
+ else if (process.env.AGENTWATCH_PRICING_DEBUG) {
59
+ console.error(
60
+ `[agentwatch/cost] dropping invalid pricing entry for "${model}" \u2014 needs input/cacheCreate/cacheRead/output non-negative numbers`
61
+ );
62
+ }
63
+ }
64
+ }
65
+ } catch (err) {
66
+ console.error(
67
+ `[agentwatch/cost] failed to read ${path12}: ${String(err)}; using built-in defaults`
68
+ );
69
+ }
70
+ }
71
+ cachedRates = merged;
72
+ return merged;
73
+ }
13
74
  function costOf(model, u) {
14
- const rate = RATES[normalizeModel(model)] ?? RATES.default;
75
+ const rates = loadRates();
76
+ const rate = rates[normalizeModel(model)] ?? rates.default;
15
77
  return (u.input * rate.input + u.cacheCreate * rate.cacheCreate + u.cacheRead * rate.cacheRead + u.output * rate.output) / 1e6;
16
78
  }
17
79
  function formatUSD(n) {
@@ -42,11 +104,11 @@ function parseUsage(obj) {
42
104
  if (input + cacheCreate + cacheRead + output === 0) return null;
43
105
  return { input, cacheCreate, cacheRead, output };
44
106
  }
45
- var RATES;
107
+ var DEFAULT_RATES, cachedRates;
46
108
  var init_cost = __esm({
47
109
  "src/util/cost.ts"() {
48
110
  "use strict";
49
- RATES = {
111
+ DEFAULT_RATES = {
50
112
  "claude-opus-4-6": {
51
113
  input: 15,
52
114
  cacheCreate: 18.75,
@@ -99,21 +161,22 @@ var init_cost = __esm({
99
161
  output: 15
100
162
  }
101
163
  };
164
+ cachedRates = null;
102
165
  }
103
166
  });
104
167
 
105
168
  // src/util/version.ts
106
- import { readFileSync } from "fs";
169
+ import { readFileSync as readFileSync2 } from "fs";
107
170
  import { fileURLToPath } from "url";
108
- import { dirname, join } from "path";
171
+ import { dirname, join as join2 } from "path";
109
172
  function readVersion() {
110
173
  const here = dirname(fileURLToPath(import.meta.url));
111
174
  for (const p of [
112
- join(here, "..", "..", "package.json"),
113
- join(here, "..", "package.json")
175
+ join2(here, "..", "..", "package.json"),
176
+ join2(here, "..", "package.json")
114
177
  ]) {
115
178
  try {
116
- return JSON.parse(readFileSync(p, "utf8")).version;
179
+ return JSON.parse(readFileSync2(p, "utf8")).version;
117
180
  } catch {
118
181
  }
119
182
  }
@@ -197,18 +260,18 @@ var detect_exports = {};
197
260
  __export(detect_exports, {
198
261
  detectAgents: () => detectAgents
199
262
  });
200
- import { existsSync } from "fs";
201
- import { homedir, platform as platform2 } from "os";
202
- import { join as join2 } from "path";
263
+ import { existsSync as existsSync2 } from "fs";
264
+ import { homedir as homedir2, platform as platform2 } from "os";
265
+ import { join as join3 } from "path";
203
266
  function hermesStateDb(home) {
204
267
  const override = process.env.HERMES_HOME?.trim();
205
- const base = override && override.length > 0 ? override : join2(home, ".hermes");
206
- return join2(base, "state.db");
268
+ const base = override && override.length > 0 ? override : join3(home, ".hermes");
269
+ return join3(base, "state.db");
207
270
  }
208
271
  function detectAgents() {
209
- const home = homedir();
272
+ const home = homedir2();
210
273
  const os12 = platform2();
211
- const clineDir = os12 === "darwin" ? join2(
274
+ const clineDir = os12 === "darwin" ? join3(
212
275
  home,
213
276
  "Library",
214
277
  "Application Support",
@@ -216,35 +279,35 @@ function detectAgents() {
216
279
  "User",
217
280
  "globalStorage",
218
281
  "saoudrizwan.claude-dev"
219
- ) : join2(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev");
282
+ ) : join3(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev");
220
283
  return [
221
284
  {
222
285
  name: "claude-code",
223
286
  label: "Claude Code",
224
- configPath: join2(home, ".claude", "settings.json"),
225
- present: existsSync(join2(home, ".claude", "projects")),
287
+ configPath: join3(home, ".claude", "settings.json"),
288
+ present: existsSync2(join3(home, ".claude", "projects")),
226
289
  instrumented: true
227
290
  },
228
291
  {
229
292
  name: "openclaw",
230
293
  label: "OpenClaw",
231
- configPath: join2(home, ".openclaw"),
232
- present: existsSync(join2(home, ".openclaw")),
294
+ configPath: join3(home, ".openclaw"),
295
+ present: existsSync2(join3(home, ".openclaw")),
233
296
  instrumented: true
234
297
  },
235
298
  {
236
299
  name: "cursor",
237
300
  label: "Cursor",
238
- configPath: join2(home, ".cursor", "mcp.json"),
239
- present: existsSync(join2(home, ".cursor")),
301
+ configPath: join3(home, ".cursor", "mcp.json"),
302
+ present: existsSync2(join3(home, ".cursor")),
240
303
  instrumented: true
241
304
  // config-level only in v0; SQLite DB TBD
242
305
  },
243
306
  {
244
307
  name: "gemini",
245
308
  label: "Gemini CLI",
246
- configPath: join2(home, ".gemini", "settings.json"),
247
- present: existsSync(join2(home, ".gemini")),
309
+ configPath: join3(home, ".gemini", "settings.json"),
310
+ present: existsSync2(join3(home, ".gemini")),
248
311
  instrumented: true
249
312
  },
250
313
  // Detected but not yet instrumented — surfaced so users don't think
@@ -252,50 +315,50 @@ function detectAgents() {
252
315
  {
253
316
  name: "codex",
254
317
  label: "Codex",
255
- configPath: join2(home, ".codex", "sessions"),
256
- present: existsSync(join2(home, ".codex")),
318
+ configPath: join3(home, ".codex", "sessions"),
319
+ present: existsSync2(join3(home, ".codex")),
257
320
  instrumented: true
258
321
  },
259
322
  {
260
323
  name: "hermes",
261
324
  label: "Hermes Agent",
262
325
  configPath: hermesStateDb(home),
263
- present: existsSync(hermesStateDb(home)),
326
+ present: existsSync2(hermesStateDb(home)),
264
327
  instrumented: true
265
328
  },
266
329
  {
267
330
  name: "aider",
268
331
  label: "Aider",
269
332
  configPath: "./.aider.chat.history.md (per-repo)",
270
- present: existsSync(join2(home, ".aider.chat.history.md")) || existsSync(join2(home, ".aider.input.history")),
333
+ present: existsSync2(join3(home, ".aider.chat.history.md")) || existsSync2(join3(home, ".aider.input.history")),
271
334
  instrumented: false
272
335
  },
273
336
  {
274
337
  name: "cline",
275
338
  label: "Cline (VS Code)",
276
339
  configPath: clineDir,
277
- present: existsSync(clineDir),
340
+ present: existsSync2(clineDir),
278
341
  instrumented: false
279
342
  },
280
343
  {
281
344
  name: "continue",
282
345
  label: "Continue.dev",
283
- configPath: join2(home, ".continue"),
284
- present: existsSync(join2(home, ".continue")),
346
+ configPath: join3(home, ".continue"),
347
+ present: existsSync2(join3(home, ".continue")),
285
348
  instrumented: false
286
349
  },
287
350
  {
288
351
  name: "windsurf",
289
352
  label: "Windsurf",
290
- configPath: join2(home, ".codeium"),
291
- present: existsSync(join2(home, ".codeium")),
353
+ configPath: join3(home, ".codeium"),
354
+ present: existsSync2(join3(home, ".codeium")),
292
355
  instrumented: false
293
356
  },
294
357
  {
295
358
  name: "goose",
296
359
  label: "Goose (Block)",
297
- configPath: join2(home, ".config", "goose"),
298
- present: existsSync(join2(home, ".config", "goose")),
360
+ configPath: join3(home, ".config", "goose"),
361
+ present: existsSync2(join3(home, ".config", "goose")),
299
362
  instrumented: false
300
363
  }
301
364
  ];
@@ -334,6 +397,7 @@ function riskOf(type, path12, cmd) {
334
397
  return 2;
335
398
  }
336
399
  if (type === "tool_call") return 3;
400
+ if (type === "parse_error") return 1;
337
401
  return 1;
338
402
  }
339
403
  var init_schema = __esm({
@@ -348,19 +412,19 @@ __export(workspace_exports, {
348
412
  claudeProjectsDir: () => claudeProjectsDir,
349
413
  detectWorkspaceRoot: () => detectWorkspaceRoot
350
414
  });
351
- import { existsSync as existsSync2, statSync } from "fs";
352
- import { homedir as homedir2 } from "os";
353
- import { join as join3 } from "path";
415
+ import { existsSync as existsSync3, statSync } from "fs";
416
+ import { homedir as homedir3 } from "os";
417
+ import { join as join4 } from "path";
354
418
  function detectWorkspaceRoot() {
355
419
  const envRoot = process.env.WORKSPACE_ROOT;
356
420
  if (envRoot && isDir(envRoot)) return envRoot;
357
- const home = homedir2();
421
+ const home = homedir3();
358
422
  const candidates = [
359
- join3(home, "IdeaProjects"),
360
- join3(home, "src"),
361
- join3(home, "code"),
362
- join3(home, "Projects"),
363
- join3(home, "dev")
423
+ join4(home, "IdeaProjects"),
424
+ join4(home, "src"),
425
+ join4(home, "code"),
426
+ join4(home, "Projects"),
427
+ join4(home, "dev")
364
428
  ];
365
429
  for (const c of candidates) {
366
430
  if (isDir(c)) return c;
@@ -368,11 +432,11 @@ function detectWorkspaceRoot() {
368
432
  return home;
369
433
  }
370
434
  function claudeProjectsDir() {
371
- return join3(homedir2(), ".claude", "projects");
435
+ return join4(homedir3(), ".claude", "projects");
372
436
  }
373
437
  function isDir(p) {
374
438
  try {
375
- return existsSync2(p) && statSync(p).isDirectory();
439
+ return existsSync3(p) && statSync(p).isDirectory();
376
440
  } catch {
377
441
  return false;
378
442
  }
@@ -670,10 +734,91 @@ var init_recent_writes = __esm({
670
734
  }
671
735
  });
672
736
 
737
+ // src/util/jsonl-stream.ts
738
+ import { closeSync, openSync, readSync } from "fs";
739
+ function readNewlineTerminatedLines(file, start, end) {
740
+ if (end < start) return { lines: [], consumed: 0 };
741
+ const size = end - start + 1;
742
+ const buf = Buffer.alloc(size);
743
+ let read = 0;
744
+ const fd = openSync(file, "r");
745
+ try {
746
+ while (read < size) {
747
+ const n = readSync(fd, buf, read, size - read, start + read);
748
+ if (n <= 0) break;
749
+ read += n;
750
+ }
751
+ } finally {
752
+ closeSync(fd);
753
+ }
754
+ const slice = read < size ? buf.subarray(0, read) : buf;
755
+ const lastNl = slice.lastIndexOf(10);
756
+ if (lastNl < 0) return { lines: [], consumed: 0 };
757
+ const terminated = slice.subarray(0, lastNl).toString("utf8");
758
+ const lines = terminated === "" ? [] : terminated.split("\n");
759
+ return { lines, consumed: lastNl + 1 };
760
+ }
761
+ var init_jsonl_stream = __esm({
762
+ "src/util/jsonl-stream.ts"() {
763
+ "use strict";
764
+ }
765
+ });
766
+
767
+ // src/util/parse-errors.ts
768
+ function createParseErrorTracker(agent, sink, options = {}) {
769
+ const entries = /* @__PURE__ */ new Map();
770
+ return {
771
+ recordFailure(sessionKey, line) {
772
+ let entry = entries.get(sessionKey);
773
+ if (!entry) {
774
+ entry = { count: 0 };
775
+ entries.set(sessionKey, entry);
776
+ }
777
+ entry.count += 1;
778
+ const sample = truncate(line, SAMPLE_BYTES);
779
+ if (!entry.eventId) {
780
+ const prefix = options.summaryPrefix?.(sessionKey) ?? `[${sessionKey.slice(0, 8)}] `;
781
+ const event = {
782
+ id: nextId(),
783
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
784
+ agent,
785
+ type: "parse_error",
786
+ sessionId: sessionKey,
787
+ riskScore: riskOf("parse_error"),
788
+ summary: `${prefix}\u26A0 unparseable line \u2014 context loss possible`,
789
+ details: {
790
+ parseErrorCount: 1,
791
+ parseErrorSample: sample
792
+ }
793
+ };
794
+ entry.eventId = event.id;
795
+ sink.emit(event);
796
+ } else {
797
+ sink.enrich(entry.eventId, {
798
+ parseErrorCount: entry.count,
799
+ parseErrorSample: sample
800
+ });
801
+ }
802
+ }
803
+ };
804
+ }
805
+ function truncate(s, max) {
806
+ if (s.length <= max) return s;
807
+ return s.slice(0, max - 1) + "\u2026";
808
+ }
809
+ var SAMPLE_BYTES;
810
+ var init_parse_errors = __esm({
811
+ "src/util/parse-errors.ts"() {
812
+ "use strict";
813
+ init_schema();
814
+ init_ids();
815
+ SAMPLE_BYTES = 200;
816
+ }
817
+ });
818
+
673
819
  // src/adapters/claude-code.ts
674
820
  import chokidar2 from "chokidar";
675
- import { createReadStream, existsSync as existsSync3, statSync as statSync2 } from "fs";
676
- import { createInterface } from "readline";
821
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
677
822
  import { basename as basename2, sep } from "path";
678
823
  function capMap(m, max) {
679
824
  while (m.size > max) {
@@ -683,12 +828,14 @@ function capMap(m, max) {
683
828
  }
684
829
  }
685
830
  function startClaudeAdapter(sink) {
686
- const { emit, enrich } = normalizeSink(sink);
831
+ const normalized = normalizeSink(sink);
832
+ const { emit, enrich } = normalized;
687
833
  const dir = claudeProjectsDir();
688
- if (!existsSync3(dir)) {
834
+ if (!existsSync4(dir)) {
689
835
  return () => {
690
836
  };
691
837
  }
838
+ const parseErrors = createParseErrorTracker("claude-code", normalized);
692
839
  const cursors = /* @__PURE__ */ new Map();
693
840
  const mainRe = /[\\/]projects[\\/][^\\/]+[\\/][^\\/]+\.jsonl$/;
694
841
  const subRe = /[\\/]projects[\\/][^\\/]+[\\/][^\\/]+[\\/]subagents[\\/][^\\/]+\.jsonl$/;
@@ -709,68 +856,62 @@ function startClaudeAdapter(sink) {
709
856
  }
710
857
  if (size <= cursor.offset) return;
711
858
  const start = cursor.offset;
712
- const stream = createReadStream(file, {
713
- start,
714
- end: size - 1,
715
- encoding: "utf8"
716
- });
717
859
  const sessionId = basename2(file, ".jsonl");
718
860
  const project = extractProject(file);
719
861
  const subAgentId = isSub ? extractSubAgentId(file) : void 0;
720
- let consumed = 0;
721
- let skippedFirst = false;
722
- const rl = createInterface({ input: stream, crlfDelay: Infinity });
723
- rl.on("line", (line) => {
724
- consumed += Buffer.byteLength(line, "utf8") + 1;
725
- if (isInitialAdd && start > 0 && !skippedFirst) {
726
- skippedFirst = true;
727
- return;
728
- }
729
- if (!line.trim()) return;
862
+ const { lines, consumed } = readNewlineTerminatedLines(
863
+ file,
864
+ start,
865
+ size - 1
866
+ );
867
+ cursor.offset = start + consumed;
868
+ for (let i = 0; i < lines.length; i++) {
869
+ if (i === 0 && isInitialAdd && start > 0) continue;
870
+ const line = lines[i];
871
+ if (!line.trim()) continue;
872
+ let obj;
730
873
  try {
731
- const obj = JSON.parse(line);
732
- handleToolResults(obj, enrich);
733
- const event = translateClaudeLine(obj, sessionId, project, subAgentId);
734
- if (event) {
735
- emit(event);
736
- if (event.details?.agentCall) {
737
- const cwd = typeof obj.cwd === "string" ? obj.cwd : "";
738
- registerSpawn({
739
- parentEventId: event.id,
740
- callee: event.details.agentCall.callee,
741
- cwd,
742
- registeredMs: new Date(event.ts).getTime()
743
- });
744
- }
745
- if (event.path && (event.type === "file_write" || event.type === "file_read")) {
746
- markAgentWrite(event.path, event.ts);
747
- }
748
- const toolUseId = event.details?.toolUseId;
749
- if (toolUseId && orphanResults.has(toolUseId)) {
750
- const orphan = orphanResults.get(toolUseId);
751
- orphanResults.delete(toolUseId);
752
- enrich(event.id, {
753
- toolResult: orphan.content,
754
- toolError: orphan.isError,
755
- durationMs: Math.max(
756
- 0,
757
- new Date(orphan.ts).getTime() - new Date(event.ts).getTime()
758
- )
759
- });
760
- } else if (toolUseId) {
761
- pendingToolUses.set(toolUseId, {
762
- eventId: event.id,
763
- ts: event.ts
764
- });
765
- capMap(pendingToolUses, MAX_PENDING_TOOL_USES);
766
- }
767
- }
874
+ obj = JSON.parse(line);
768
875
  } catch {
876
+ parseErrors.recordFailure(sessionId, line);
877
+ continue;
769
878
  }
770
- });
771
- rl.on("close", () => {
772
- cursor.offset = start + consumed;
773
- });
879
+ handleToolResults(obj, enrich);
880
+ const event = translateClaudeLine(obj, sessionId, project, subAgentId);
881
+ if (!event) continue;
882
+ emit(event);
883
+ if (event.details?.agentCall) {
884
+ const cwd = typeof obj.cwd === "string" ? obj.cwd : "";
885
+ registerSpawn({
886
+ parentEventId: event.id,
887
+ callee: event.details.agentCall.callee,
888
+ cwd,
889
+ registeredMs: new Date(event.ts).getTime()
890
+ });
891
+ }
892
+ if (event.path && (event.type === "file_write" || event.type === "file_read")) {
893
+ markAgentWrite(event.path, event.ts);
894
+ }
895
+ const toolUseId = event.details?.toolUseId;
896
+ if (toolUseId && orphanResults.has(toolUseId)) {
897
+ const orphan = orphanResults.get(toolUseId);
898
+ orphanResults.delete(toolUseId);
899
+ enrich(event.id, {
900
+ toolResult: orphan.content,
901
+ toolError: orphan.isError,
902
+ durationMs: Math.max(
903
+ 0,
904
+ new Date(orphan.ts).getTime() - new Date(event.ts).getTime()
905
+ )
906
+ });
907
+ } else if (toolUseId) {
908
+ pendingToolUses.set(toolUseId, {
909
+ eventId: event.id,
910
+ ts: event.ts
911
+ });
912
+ capMap(pendingToolUses, MAX_PENDING_TOOL_USES);
913
+ }
914
+ }
774
915
  };
775
916
  watcher2.on("add", (f) => process2(f, true));
776
917
  watcher2.on("change", (f) => process2(f, false));
@@ -901,6 +1042,7 @@ function translateClaudeLine(obj, sessionId, project = "", subAgentId) {
901
1042
  const evType = inferToolType(toolUse.name);
902
1043
  const summary = buildToolSummary(toolUse);
903
1044
  const agentCall = evType === "shell_exec" && toolUse.cmd ? detectAgentCall(toolUse.cmd) : null;
1045
+ const cwd = typeof o.cwd === "string" ? o.cwd : void 0;
904
1046
  return {
905
1047
  id: nextId(),
906
1048
  ts,
@@ -919,7 +1061,8 @@ function translateClaudeLine(obj, sessionId, project = "", subAgentId) {
919
1061
  usage,
920
1062
  cost,
921
1063
  model,
922
- ...agentCall ? { agentCall } : {}
1064
+ ...agentCall ? { agentCall } : {},
1065
+ ...evType === "file_write" && cwd ? { cwd } : {}
923
1066
  }
924
1067
  };
925
1068
  }
@@ -931,7 +1074,7 @@ function translateClaudeLine(obj, sessionId, project = "", subAgentId) {
931
1074
  ts,
932
1075
  agent: "claude-code",
933
1076
  type: "response",
934
- summary: prefix + truncate(text || thinking || ""),
1077
+ summary: prefix + truncate2(text || thinking || ""),
935
1078
  sessionId,
936
1079
  riskScore: riskOf("response"),
937
1080
  details: {
@@ -952,7 +1095,7 @@ function translateClaudeLine(obj, sessionId, project = "", subAgentId) {
952
1095
  ts,
953
1096
  agent: "claude-code",
954
1097
  type: "compaction",
955
- summary: prefix + "\u22C8 context compacted \u2014 " + truncate(text, 60),
1098
+ summary: prefix + "\u22C8 context compacted \u2014 " + truncate2(text, 60),
956
1099
  sessionId,
957
1100
  riskScore: riskOf("compaction"),
958
1101
  details: { fullText: text }
@@ -963,7 +1106,7 @@ function translateClaudeLine(obj, sessionId, project = "", subAgentId) {
963
1106
  ts,
964
1107
  agent: "claude-code",
965
1108
  type: "prompt",
966
- summary: prefix + truncate(text),
1109
+ summary: prefix + truncate2(text),
967
1110
  sessionId,
968
1111
  riskScore: riskOf("prompt"),
969
1112
  details: { fullText: text }
@@ -999,17 +1142,17 @@ function extractThinking(content) {
999
1142
  return parts.join("\n").trim();
1000
1143
  }
1001
1144
  function buildToolSummary(t) {
1002
- if (/^Bash/i.test(t.name) && t.cmd) return `Bash: ${truncate(t.cmd, 100)}`;
1145
+ if (/^Bash/i.test(t.name) && t.cmd) return `Bash: ${truncate2(t.cmd, 100)}`;
1003
1146
  if (/^(Write|Edit|MultiEdit|Read)/i.test(t.name) && t.path) {
1004
1147
  return `${t.name}: ${t.path}`;
1005
1148
  }
1006
1149
  if (/^(Grep|Glob)/i.test(t.name)) {
1007
1150
  const pat = typeof t.input.pattern === "string" ? t.input.pattern : typeof t.input.glob === "string" ? t.input.glob : "";
1008
- return `${t.name}: ${truncate(pat, 100)}`;
1151
+ return `${t.name}: ${truncate2(pat, 100)}`;
1009
1152
  }
1010
1153
  if (/^Task/i.test(t.name)) {
1011
1154
  const desc = typeof t.input.description === "string" ? t.input.description : "";
1012
- return `Task: ${truncate(desc, 100)}`;
1155
+ return `Task: ${truncate2(desc, 100)}`;
1013
1156
  }
1014
1157
  if (/^WebFetch/i.test(t.name)) {
1015
1158
  const url = typeof t.input.url === "string" ? t.input.url : "";
@@ -1018,7 +1161,7 @@ function buildToolSummary(t) {
1018
1161
  const firstVal = Object.values(t.input).find(
1019
1162
  (v) => typeof v === "string"
1020
1163
  );
1021
- return firstVal ? `${t.name}: ${truncate(firstVal, 100)}` : t.name;
1164
+ return firstVal ? `${t.name}: ${truncate2(firstVal, 100)}` : t.name;
1022
1165
  }
1023
1166
  function extractText(content) {
1024
1167
  if (typeof content === "string") return content;
@@ -1054,7 +1197,7 @@ function inferToolType(name) {
1054
1197
  if (/^(Write|Edit|MultiEdit)/i.test(name)) return "file_write";
1055
1198
  return "tool_call";
1056
1199
  }
1057
- function truncate(s, max = 140) {
1200
+ function truncate2(s, max = 140) {
1058
1201
  const clean = s.replace(/\s+/g, " ").trim();
1059
1202
  if (!clean) return "";
1060
1203
  return clean.length <= max ? clean : clean.slice(0, max - 1) + "\u2026";
@@ -1070,6 +1213,8 @@ var init_claude_code = __esm({
1070
1213
  init_spawn_tracker();
1071
1214
  init_cost();
1072
1215
  init_recent_writes();
1216
+ init_jsonl_stream();
1217
+ init_parse_errors();
1073
1218
  MAX_PENDING_TOOL_USES = 5e3;
1074
1219
  pendingToolUses = /* @__PURE__ */ new Map();
1075
1220
  orphanResults = /* @__PURE__ */ new Map();
@@ -1178,18 +1323,24 @@ var init_openclaw_cron = __esm({
1178
1323
 
1179
1324
  // src/adapters/openclaw.ts
1180
1325
  import chokidar3 from "chokidar";
1181
- import { createReadStream as createReadStream2, existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
1182
- import { createInterface as createInterface2 } from "readline";
1183
- import { basename as basename3, join as join4, sep as sep2 } from "path";
1184
- import { homedir as homedir3 } from "os";
1326
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1327
+ import { basename as basename3, join as join5, sep as sep2 } from "path";
1328
+ import { homedir as homedir4 } from "os";
1329
+ function capMap2(m, max) {
1330
+ while (m.size > max) {
1331
+ const first = m.keys().next().value;
1332
+ if (first === void 0) break;
1333
+ m.delete(first);
1334
+ }
1335
+ }
1185
1336
  function loadScheduledMarkers(file) {
1186
1337
  const dir = file.split(sep2).slice(0, -1).join(sep2);
1187
- const jsonPath = join4(dir, "sessions.json");
1338
+ const jsonPath = join5(dir, "sessions.json");
1188
1339
  if (sessionsJsonRead.has(jsonPath)) return;
1189
1340
  sessionsJsonRead.add(jsonPath);
1190
1341
  let raw;
1191
1342
  try {
1192
- raw = readFileSync2(jsonPath, "utf8");
1343
+ raw = readFileSync3(jsonPath, "utf8");
1193
1344
  } catch {
1194
1345
  return;
1195
1346
  }
@@ -1211,13 +1362,16 @@ function loadScheduledMarkers(file) {
1211
1362
  }
1212
1363
  }
1213
1364
  function startOpenClawAdapter(sink) {
1214
- const emit = typeof sink === "function" ? sink : sink.emit;
1215
- const root = join4(homedir3(), ".openclaw");
1216
- if (!existsSync4(root)) return () => {
1365
+ const normalized = typeof sink === "function" ? { emit: sink, enrich: () => {
1366
+ } } : sink;
1367
+ const emit = normalized.emit;
1368
+ const root = join5(homedir4(), ".openclaw");
1369
+ if (!existsSync5(root)) return () => {
1217
1370
  };
1371
+ const parseErrors = createParseErrorTracker("openclaw", normalized);
1218
1372
  const cursors = /* @__PURE__ */ new Map();
1219
1373
  const stoppers = [];
1220
- const agentsDir = join4(root, "agents");
1374
+ const agentsDir = join5(root, "agents");
1221
1375
  const sessionRe = /[\\/]agents[\\/][^\\/]+[\\/]sessions[\\/][^\\/]+\.jsonl$/;
1222
1376
  const sessionsWatcher = chokidar3.watch(agentsDir, {
1223
1377
  persistent: true,
@@ -1227,7 +1381,7 @@ function startOpenClawAdapter(sink) {
1227
1381
  });
1228
1382
  const handleSession = (f, initial) => {
1229
1383
  if (!sessionRe.test(f)) return;
1230
- processSession(f, initial, cursors, emit);
1384
+ processSession(f, initial, cursors, normalized, parseErrors);
1231
1385
  };
1232
1386
  sessionsWatcher.on("add", (f) => handleSession(f, true));
1233
1387
  sessionsWatcher.on("change", (f) => handleSession(f, false));
@@ -1235,13 +1389,19 @@ function startOpenClawAdapter(sink) {
1235
1389
  stoppers.push(() => {
1236
1390
  void sessionsWatcher.close();
1237
1391
  });
1238
- const auditPath = join4(root, "logs", "config-audit.jsonl");
1392
+ const auditPath = join5(root, "logs", "config-audit.jsonl");
1239
1393
  const auditWatcher = chokidar3.watch(auditPath, {
1240
1394
  persistent: true,
1241
1395
  ignoreInitial: false
1242
1396
  });
1243
- auditWatcher.on("add", (f) => processAudit(f, true, cursors, emit));
1244
- auditWatcher.on("change", (f) => processAudit(f, false, cursors, emit));
1397
+ auditWatcher.on(
1398
+ "add",
1399
+ (f) => processAudit(f, true, cursors, emit, parseErrors)
1400
+ );
1401
+ auditWatcher.on(
1402
+ "change",
1403
+ (f) => processAudit(f, false, cursors, emit, parseErrors)
1404
+ );
1245
1405
  auditWatcher.on("error", swallow);
1246
1406
  stoppers.push(() => {
1247
1407
  void auditWatcher.close();
@@ -1250,7 +1410,7 @@ function startOpenClawAdapter(sink) {
1250
1410
  for (const s of stoppers) s();
1251
1411
  };
1252
1412
  }
1253
- function processSession(file, startFromEnd, cursors, emit) {
1413
+ function processSession(file, startFromEnd, cursors, sink, parseErrors) {
1254
1414
  const subAgent = extractSubAgent(file);
1255
1415
  const sessionId = basename3(file, ".jsonl");
1256
1416
  loadScheduledMarkers(file);
@@ -1260,8 +1420,10 @@ function processSession(file, startFromEnd, cursors, emit) {
1260
1420
  try {
1261
1421
  obj = JSON.parse(line);
1262
1422
  } catch {
1423
+ parseErrors.recordFailure(sessionId, line);
1263
1424
  return;
1264
1425
  }
1426
+ handleOpenClawToolResult(obj, sink.enrich);
1265
1427
  const event = translateSession(obj, subAgent, sessionId);
1266
1428
  if (!event) return;
1267
1429
  if (marker) {
@@ -1275,15 +1437,83 @@ function processSession(file, startFromEnd, cursors, emit) {
1275
1437
  }
1276
1438
  };
1277
1439
  }
1278
- emit(event);
1440
+ sink.emit(event);
1441
+ const callId = event.details?.toolUseId;
1442
+ if (callId && orphanOpenClawResults.has(callId)) {
1443
+ const orphan = orphanOpenClawResults.get(callId);
1444
+ orphanOpenClawResults.delete(callId);
1445
+ sink.enrich(event.id, {
1446
+ toolResult: orphan.content,
1447
+ toolError: orphan.isError,
1448
+ durationMs: Math.max(
1449
+ 0,
1450
+ new Date(orphan.ts).getTime() - new Date(event.ts).getTime()
1451
+ )
1452
+ });
1453
+ } else if (callId) {
1454
+ pendingOpenClawCalls.set(callId, { eventId: event.id, ts: event.ts });
1455
+ capMap2(pendingOpenClawCalls, MAX_PENDING_OPENCLAW_CALLS);
1456
+ }
1279
1457
  });
1280
1458
  }
1281
- function processAudit(file, startFromEnd, cursors, emit) {
1459
+ function capBytes2(s, max = MAX_TOOL_RESULT_BYTES2) {
1460
+ if (s.length <= max) return s;
1461
+ const truncated = s.length - max;
1462
+ return s.slice(0, max) + `
1463
+
1464
+ \u2026 [${truncated.toLocaleString()} bytes truncated]`;
1465
+ }
1466
+ function flattenToolResultContent(content) {
1467
+ if (typeof content === "string") return capBytes2(content);
1468
+ if (!Array.isArray(content)) return "";
1469
+ const parts = [];
1470
+ for (const c of content) {
1471
+ if (typeof c === "string") {
1472
+ parts.push(c);
1473
+ } else if (typeof c === "object" && c !== null) {
1474
+ const rec = c;
1475
+ if (typeof rec.text === "string") parts.push(rec.text);
1476
+ }
1477
+ }
1478
+ return capBytes2(parts.join("\n"));
1479
+ }
1480
+ function handleOpenClawToolResult(obj, enrich) {
1481
+ if (!obj || typeof obj !== "object") return;
1482
+ const o = obj;
1483
+ if (o.type !== "message") return;
1484
+ const msg = o.message;
1485
+ if (!msg || msg.role !== "toolResult") return;
1486
+ const callId = typeof msg.toolCallId === "string" ? msg.toolCallId : void 0;
1487
+ if (!callId) return;
1488
+ const isError = msg.isError === true;
1489
+ const content = flattenToolResultContent(msg.content);
1490
+ const ts = typeof o.timestamp === "string" && o.timestamp || (typeof msg.timestamp === "number" ? new Date(msg.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString());
1491
+ const pending2 = pendingOpenClawCalls.get(callId);
1492
+ if (pending2) {
1493
+ pendingOpenClawCalls.delete(callId);
1494
+ const details = msg.details;
1495
+ const explicitDuration = typeof details?.durationMs === "number" ? details.durationMs : void 0;
1496
+ const computedDuration = Math.max(
1497
+ 0,
1498
+ new Date(ts).getTime() - new Date(pending2.ts).getTime()
1499
+ );
1500
+ enrich(pending2.eventId, {
1501
+ toolResult: content,
1502
+ toolError: isError,
1503
+ durationMs: explicitDuration ?? computedDuration
1504
+ });
1505
+ } else {
1506
+ orphanOpenClawResults.set(callId, { ts, content, isError });
1507
+ capMap2(orphanOpenClawResults, 1e3);
1508
+ }
1509
+ }
1510
+ function processAudit(file, startFromEnd, cursors, emit, parseErrors) {
1282
1511
  streamLines(file, startFromEnd, cursors, (line) => {
1283
1512
  let obj;
1284
1513
  try {
1285
1514
  obj = JSON.parse(line);
1286
1515
  } catch {
1516
+ parseErrors.recordFailure("config-audit", line);
1287
1517
  return;
1288
1518
  }
1289
1519
  const event = translateAudit(obj);
@@ -1300,25 +1530,17 @@ function streamLines(file, isInitialAdd, cursors, onLine) {
1300
1530
  }
1301
1531
  if (size <= cursor.offset) return;
1302
1532
  const start = cursor.offset;
1303
- const stream = createReadStream2(file, {
1533
+ const { lines, consumed } = readNewlineTerminatedLines(
1534
+ file,
1304
1535
  start,
1305
- end: size - 1,
1306
- encoding: "utf8"
1307
- });
1308
- let consumed = 0;
1309
- let skippedFirst = false;
1310
- const rl = createInterface2({ input: stream, crlfDelay: Infinity });
1311
- rl.on("line", (line) => {
1312
- consumed += Buffer.byteLength(line, "utf8") + 1;
1313
- if (isInitialAdd && start > 0 && !skippedFirst) {
1314
- skippedFirst = true;
1315
- return;
1316
- }
1536
+ size - 1
1537
+ );
1538
+ cursor.offset = start + consumed;
1539
+ for (let i = 0; i < lines.length; i++) {
1540
+ if (i === 0 && isInitialAdd && start > 0) continue;
1541
+ const line = lines[i];
1317
1542
  if (line.trim()) onLine(line);
1318
- });
1319
- rl.on("close", () => {
1320
- cursor.offset = start + consumed;
1321
- });
1543
+ }
1322
1544
  }
1323
1545
  function swallow(err) {
1324
1546
  if (typeof err !== "object" || err === null) return;
@@ -1410,7 +1632,7 @@ function translateSession(obj, subAgent, sessionId) {
1410
1632
  const text = extractText2(content);
1411
1633
  if (role === "user") {
1412
1634
  return base("prompt", {
1413
- summary: truncate2(text),
1635
+ summary: truncate3(text),
1414
1636
  details: { fullText: text }
1415
1637
  });
1416
1638
  }
@@ -1421,22 +1643,25 @@ function translateSession(obj, subAgent, sessionId) {
1421
1643
  const toolUse = extractToolUse(content);
1422
1644
  if (toolUse) {
1423
1645
  const type = inferToolType2(toolUse.name);
1646
+ const cwd = type === "file_write" ? sessionCwd.get(sessionId) : void 0;
1424
1647
  return base(type, {
1425
1648
  tool: `openclaw:${subAgent}:${toolUse.name}`,
1426
1649
  path: toolUse.path,
1427
1650
  cmd: toolUse.cmd,
1428
- summary: truncate2(toolUse.summary),
1651
+ summary: truncate3(toolUse.summary),
1429
1652
  details: {
1430
1653
  toolInput: toolUse.input,
1654
+ ...toolUse.id ? { toolUseId: toolUse.id } : {},
1431
1655
  ...usage ? { usage } : {},
1432
1656
  ...precomputedCost != null ? { cost: precomputedCost } : {},
1433
- ...model ? { model } : {}
1657
+ ...model ? { model } : {},
1658
+ ...cwd ? { cwd } : {}
1434
1659
  }
1435
1660
  });
1436
1661
  }
1437
1662
  if (!text) return null;
1438
1663
  return base("response", {
1439
- summary: truncate2(text),
1664
+ summary: truncate3(text),
1440
1665
  details: {
1441
1666
  fullText: text,
1442
1667
  ...usage ? { usage } : {},
@@ -1482,15 +1707,16 @@ function extractText2(content) {
1482
1707
  function extractToolUse(content) {
1483
1708
  if (!Array.isArray(content)) return null;
1484
1709
  for (const c of content) {
1485
- if (typeof c === "object" && c !== null && c.type === "tool_use") {
1486
- const r = c;
1487
- const name = typeof r.name === "string" ? r.name : "unknown";
1488
- const input = r.input ?? {};
1489
- const path12 = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : void 0;
1490
- const cmd = typeof input.command === "string" ? input.command : void 0;
1491
- const summary = cmd ?? path12 ?? name;
1492
- return { name, path: path12, cmd, summary, input };
1493
- }
1710
+ if (typeof c !== "object" || c === null) continue;
1711
+ const r = c;
1712
+ if (r.type !== "tool_use" && r.type !== "toolCall") continue;
1713
+ const name = typeof r.name === "string" ? r.name : "unknown";
1714
+ const id = typeof r.id === "string" ? r.id : void 0;
1715
+ const input = r.input ?? r.arguments ?? {};
1716
+ const path12 = typeof input.file_path === "string" ? input.file_path : typeof input.path === "string" ? input.path : typeof input.file === "string" ? input.file : void 0;
1717
+ const cmd = typeof input.command === "string" ? input.command : typeof input.cmd === "string" ? input.cmd : void 0;
1718
+ const summary = cmd ?? path12 ?? name;
1719
+ return { name, path: path12, cmd, summary, input, id };
1494
1720
  }
1495
1721
  return null;
1496
1722
  }
@@ -1500,21 +1726,27 @@ function inferToolType2(name) {
1500
1726
  if (/^(Write|Edit|MultiEdit|Create)/i.test(name)) return "file_write";
1501
1727
  return "tool_call";
1502
1728
  }
1503
- function truncate2(s, max = 140) {
1729
+ function truncate3(s, max = 140) {
1504
1730
  const clean = s.replace(/\s+/g, " ").trim();
1505
1731
  if (!clean) return "";
1506
1732
  return clean.length <= max ? clean : clean.slice(0, max - 1) + "\u2026";
1507
1733
  }
1508
- var sessionCwd, scheduledBySessionId, sessionsJsonRead, BACKFILL_BYTES2;
1734
+ var sessionCwd, MAX_PENDING_OPENCLAW_CALLS, pendingOpenClawCalls, orphanOpenClawResults, scheduledBySessionId, sessionsJsonRead, MAX_TOOL_RESULT_BYTES2, BACKFILL_BYTES2;
1509
1735
  var init_openclaw = __esm({
1510
1736
  "src/adapters/openclaw.ts"() {
1511
1737
  "use strict";
1512
1738
  init_schema();
1513
1739
  init_ids();
1740
+ init_jsonl_stream();
1741
+ init_parse_errors();
1514
1742
  init_openclaw_cron();
1515
1743
  sessionCwd = /* @__PURE__ */ new Map();
1744
+ MAX_PENDING_OPENCLAW_CALLS = 5e3;
1745
+ pendingOpenClawCalls = /* @__PURE__ */ new Map();
1746
+ orphanOpenClawResults = /* @__PURE__ */ new Map();
1516
1747
  scheduledBySessionId = /* @__PURE__ */ new Map();
1517
1748
  sessionsJsonRead = /* @__PURE__ */ new Set();
1749
+ MAX_TOOL_RESULT_BYTES2 = 256 * 1024;
1518
1750
  BACKFILL_BYTES2 = 4 * 1024 * 1024;
1519
1751
  }
1520
1752
  });
@@ -1522,17 +1754,17 @@ var init_openclaw = __esm({
1522
1754
  // src/adapters/cursor.ts
1523
1755
  import chokidar4 from "chokidar";
1524
1756
  import {
1525
- readFileSync as readFileSync3,
1526
- existsSync as existsSync5,
1757
+ readFileSync as readFileSync4,
1758
+ existsSync as existsSync6,
1527
1759
  readdirSync,
1528
1760
  statSync as statSync4
1529
1761
  } from "fs";
1530
- import { homedir as homedir4 } from "os";
1531
- import { join as join5 } from "path";
1762
+ import { homedir as homedir5 } from "os";
1763
+ import { join as join6 } from "path";
1532
1764
  function startCursorAdapter(workspace, sink) {
1533
1765
  const emit = typeof sink === "function" ? sink : sink.emit;
1534
- const cursorDir = join5(homedir4(), ".cursor");
1535
- const installed = existsSync5(cursorDir);
1766
+ const cursorDir = join6(homedir5(), ".cursor");
1767
+ const installed = existsSync6(cursorDir);
1536
1768
  const status = {
1537
1769
  installed,
1538
1770
  mcpServers: [],
@@ -1554,8 +1786,8 @@ function startCursorAdapter(workspace, sink) {
1554
1786
  ...opts
1555
1787
  });
1556
1788
  };
1557
- const mcpPath = join5(cursorDir, "mcp.json");
1558
- if (existsSync5(mcpPath)) {
1789
+ const mcpPath = join6(cursorDir, "mcp.json");
1790
+ if (existsSync6(mcpPath)) {
1559
1791
  status.mcpServers = readMcpServers(mcpPath);
1560
1792
  const w = chokidar4.watch(mcpPath, {
1561
1793
  persistent: true,
@@ -1574,8 +1806,8 @@ function startCursorAdapter(workspace, sink) {
1574
1806
  void w.close();
1575
1807
  });
1576
1808
  }
1577
- const permPath = join5(cursorDir, "cli-config.json");
1578
- if (existsSync5(permPath)) {
1809
+ const permPath = join6(cursorDir, "cli-config.json");
1810
+ if (existsSync6(permPath)) {
1579
1811
  status.permissions = readPermissions(permPath);
1580
1812
  const w = chokidar4.watch(permPath, {
1581
1813
  persistent: true,
@@ -1595,8 +1827,8 @@ function startCursorAdapter(workspace, sink) {
1595
1827
  void w.close();
1596
1828
  });
1597
1829
  }
1598
- const stateFile = join5(cursorDir, "ide_state.json");
1599
- if (existsSync5(stateFile)) {
1830
+ const stateFile = join6(cursorDir, "ide_state.json");
1831
+ if (existsSync6(stateFile)) {
1600
1832
  for (const p of readRecentFiles(stateFile)) lastRecentFiles.add(p);
1601
1833
  const w = chokidar4.watch(stateFile, {
1602
1834
  persistent: true,
@@ -1654,7 +1886,7 @@ function swallow2(err) {
1654
1886
  }
1655
1887
  function readMcpServers(path12) {
1656
1888
  try {
1657
- const obj = JSON.parse(readFileSync3(path12, "utf8"));
1889
+ const obj = JSON.parse(readFileSync4(path12, "utf8"));
1658
1890
  return Object.keys(obj.mcpServers ?? {});
1659
1891
  } catch {
1660
1892
  return [];
@@ -1662,7 +1894,7 @@ function readMcpServers(path12) {
1662
1894
  }
1663
1895
  function readPermissions(path12) {
1664
1896
  try {
1665
- const obj = JSON.parse(readFileSync3(path12, "utf8"));
1897
+ const obj = JSON.parse(readFileSync4(path12, "utf8"));
1666
1898
  const perms = obj.permissions ?? {};
1667
1899
  const sandbox = obj.sandbox?.mode;
1668
1900
  return {
@@ -1677,7 +1909,7 @@ function readPermissions(path12) {
1677
1909
  }
1678
1910
  function readRecentFiles(stateFile) {
1679
1911
  try {
1680
- const obj = JSON.parse(readFileSync3(stateFile, "utf8"));
1912
+ const obj = JSON.parse(readFileSync4(stateFile, "utf8"));
1681
1913
  const list = obj.recentlyViewedFiles;
1682
1914
  if (!Array.isArray(list)) return [];
1683
1915
  return list.map((r) => {
@@ -1693,9 +1925,9 @@ function readRecentFiles(stateFile) {
1693
1925
  }
1694
1926
  function discoverCursorrules(workspace) {
1695
1927
  const hits = [];
1696
- if (!existsSync5(workspace)) return hits;
1697
- const rootRules = join5(workspace, ".cursorrules");
1698
- if (existsSync5(rootRules)) hits.push(rootRules);
1928
+ if (!existsSync6(workspace)) return hits;
1929
+ const rootRules = join6(workspace, ".cursorrules");
1930
+ if (existsSync6(rootRules)) hits.push(rootRules);
1699
1931
  let entries = [];
1700
1932
  try {
1701
1933
  entries = readdirSync(workspace);
@@ -1705,14 +1937,14 @@ function discoverCursorrules(workspace) {
1705
1937
  for (const name of entries) {
1706
1938
  if (name.startsWith(".")) continue;
1707
1939
  if (name === "node_modules") continue;
1708
- const dir = join5(workspace, name);
1940
+ const dir = join6(workspace, name);
1709
1941
  try {
1710
1942
  if (!statSync4(dir).isDirectory()) continue;
1711
1943
  } catch {
1712
1944
  continue;
1713
1945
  }
1714
- const candidate = join5(dir, ".cursorrules");
1715
- if (existsSync5(candidate)) hits.push(candidate);
1946
+ const candidate = join6(dir, ".cursorrules");
1947
+ if (existsSync6(candidate)) hits.push(candidate);
1716
1948
  }
1717
1949
  return hits;
1718
1950
  }
@@ -1732,13 +1964,13 @@ var init_cursor = __esm({
1732
1964
 
1733
1965
  // src/adapters/gemini.ts
1734
1966
  import chokidar5 from "chokidar";
1735
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1736
- import { homedir as homedir5 } from "os";
1737
- import { basename as basename4, join as join6, sep as sep3 } from "path";
1967
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1968
+ import { homedir as homedir6 } from "os";
1969
+ import { basename as basename4, join as join7, sep as sep3 } from "path";
1738
1970
  function startGeminiAdapter(sink) {
1739
1971
  const { emit } = normalizeSink2(sink);
1740
- const root = join6(homedir5(), ".gemini", "tmp");
1741
- if (!existsSync6(root)) return () => {
1972
+ const root = join7(homedir6(), ".gemini", "tmp");
1973
+ if (!existsSync7(root)) return () => {
1742
1974
  };
1743
1975
  const emittedIds = /* @__PURE__ */ new Map();
1744
1976
  const pendingParentByFile = /* @__PURE__ */ new Map();
@@ -1752,7 +1984,7 @@ function startGeminiAdapter(sink) {
1752
1984
  if (!sessionRe.test(file)) return;
1753
1985
  let doc;
1754
1986
  try {
1755
- doc = JSON.parse(readFileSync4(file, "utf8"));
1987
+ doc = JSON.parse(readFileSync5(file, "utf8"));
1756
1988
  } catch {
1757
1989
  return;
1758
1990
  }
@@ -1762,10 +1994,10 @@ function startGeminiAdapter(sink) {
1762
1994
  const kind = typeof d.kind === "string" ? d.kind : "main";
1763
1995
  const project = extractProject3(file);
1764
1996
  const messages = Array.isArray(d.messages) ? d.messages : [];
1765
- let seen = emittedIds.get(file);
1766
- if (!seen) {
1767
- seen = /* @__PURE__ */ new Set();
1768
- emittedIds.set(file, seen);
1997
+ let seen2 = emittedIds.get(file);
1998
+ if (!seen2) {
1999
+ seen2 = /* @__PURE__ */ new Set();
2000
+ emittedIds.set(file, seen2);
1769
2001
  const startTime = typeof d.startTime === "string" ? d.startTime : void 0;
1770
2002
  const spawnTs = startTime ? new Date(startTime).getTime() : Date.now();
1771
2003
  const parent = consumeSpawn("gemini", "", spawnTs);
@@ -1776,8 +2008,8 @@ function startGeminiAdapter(sink) {
1776
2008
  if (!m || typeof m !== "object") continue;
1777
2009
  const msg = m;
1778
2010
  const id = typeof msg.id === "string" ? msg.id : void 0;
1779
- if (!id || seen.has(id)) continue;
1780
- seen.add(id);
2011
+ if (!id || seen2.has(id)) continue;
2012
+ seen2.add(id);
1781
2013
  const ev = translate(msg, sessionId, kind, project);
1782
2014
  if (ev) {
1783
2015
  const parent = pendingParentByFile.get(file);
@@ -1834,7 +2066,7 @@ function translate(msg, sessionId, kind, project) {
1834
2066
  agent: "gemini",
1835
2067
  type: eventType,
1836
2068
  sessionId,
1837
- summary: prefix + truncate3(text),
2069
+ summary: prefix + truncate4(text),
1838
2070
  riskScore: type === "error" ? 6 : riskOf(eventType),
1839
2071
  tool: kind === "subagent" ? "gemini:subagent" : "gemini",
1840
2072
  details: {
@@ -1960,7 +2192,7 @@ function extractProject3(file) {
1960
2192
  }
1961
2193
  return "";
1962
2194
  }
1963
- function truncate3(s, max = 140) {
2195
+ function truncate4(s, max = 140) {
1964
2196
  const clean = s.replace(/\s+/g, " ").trim();
1965
2197
  if (!clean) return "";
1966
2198
  return clean.length <= max ? clean : clean.slice(0, max - 1) + "\u2026";
@@ -1988,17 +2220,17 @@ var init_gemini = __esm({
1988
2220
 
1989
2221
  // src/adapters/codex.ts
1990
2222
  import chokidar6 from "chokidar";
1991
- import { createReadStream as createReadStream3, existsSync as existsSync7, statSync as statSync5 } from "fs";
1992
- import { createInterface as createInterface3 } from "readline";
1993
- import { basename as basename5, join as join7, sep as sep4 } from "path";
2223
+ import { existsSync as existsSync8, statSync as statSync5 } from "fs";
2224
+ import { basename as basename5, join as join8, sep as sep4 } from "path";
1994
2225
  import os5 from "os";
1995
2226
  function codexSessionsDir(home = os5.homedir()) {
1996
- return join7(home, ".codex", "sessions");
2227
+ return join8(home, ".codex", "sessions");
1997
2228
  }
1998
2229
  function startCodexAdapter(sink) {
1999
2230
  const dir = codexSessionsDir();
2000
- if (!existsSync7(dir)) return () => {
2231
+ if (!existsSync8(dir)) return () => {
2001
2232
  };
2233
+ const parseErrors = createParseErrorTracker("codex", sink);
2002
2234
  const cursors = /* @__PURE__ */ new Map();
2003
2235
  const rolloutRe = /rollout-[^/\\]+\.jsonl$/;
2004
2236
  const watcher2 = chokidar6.watch(dir, {
@@ -2022,121 +2254,115 @@ function startCodexAdapter(sink) {
2022
2254
  if (size <= cursor.offset) return;
2023
2255
  const start = cursor.offset;
2024
2256
  const sessionId = extractSessionId(file);
2025
- const stream = createReadStream3(file, {
2257
+ const { lines, consumed } = readNewlineTerminatedLines(
2258
+ file,
2026
2259
  start,
2027
- end: size - 1,
2028
- encoding: "utf8"
2029
- });
2030
- let consumed = 0;
2031
- let skippedFirst = false;
2032
- const rl = createInterface3({ input: stream, crlfDelay: Infinity });
2033
- rl.on("line", (line) => {
2034
- consumed += Buffer.byteLength(line, "utf8") + 1;
2035
- if (isInitialAdd && start > 0 && !skippedFirst) {
2036
- skippedFirst = true;
2037
- return;
2038
- }
2039
- if (!line.trim()) return;
2260
+ size - 1
2261
+ );
2262
+ cursor.offset = start + consumed;
2263
+ for (let i = 0; i < lines.length; i++) {
2264
+ if (i === 0 && isInitialAdd && start > 0) continue;
2265
+ const line = lines[i];
2266
+ if (!line.trim()) continue;
2267
+ let obj;
2040
2268
  try {
2041
- const obj = JSON.parse(line);
2042
- if (obj.type === "session_meta") {
2043
- const cwd = obj.payload?.cwd;
2044
- if (typeof cwd === "string") {
2045
- cursor.project = projectOf2(cwd);
2046
- cursor.cwd = cwd;
2047
- const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
2048
- const parent = consumeSpawn(
2049
- "codex",
2050
- cwd,
2051
- ts ? new Date(ts).getTime() : Date.now()
2052
- );
2053
- if (parent) cursor.pendingParentSpawnId = parent.parentEventId;
2054
- }
2055
- const model = obj.payload?.model;
2056
- if (typeof model === "string") cursor.model = model;
2057
- return;
2058
- }
2059
- if (obj.type === "turn_context") {
2060
- const model = obj.payload?.model;
2061
- if (typeof model === "string") cursor.model = model;
2062
- return;
2269
+ obj = JSON.parse(line);
2270
+ } catch {
2271
+ parseErrors.recordFailure(sessionId, line);
2272
+ continue;
2273
+ }
2274
+ const payload = obj.payload ?? {};
2275
+ if (obj.type === "session_meta") {
2276
+ const cwd = payload.cwd;
2277
+ if (typeof cwd === "string") {
2278
+ cursor.project = projectOf2(cwd);
2279
+ cursor.cwd = cwd;
2280
+ const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
2281
+ const parent = consumeSpawn(
2282
+ "codex",
2283
+ cwd,
2284
+ ts ? new Date(ts).getTime() : Date.now()
2285
+ );
2286
+ if (parent) cursor.pendingParentSpawnId = parent.parentEventId;
2063
2287
  }
2064
- if (obj.type === "event_msg") {
2065
- const usage = extractTokenUsage(obj);
2066
- if (usage && cursor.lastResponseId) {
2067
- const key = `${usage.input}|${usage.cacheRead}|${usage.output}`;
2068
- if (cursor.lastUsageKey !== key) {
2069
- cursor.lastUsageKey = key;
2070
- const model = cursor.model ?? "gpt-5";
2071
- const cost = costOf(model, usage);
2072
- sink.enrich(cursor.lastResponseId, { usage, cost, model });
2073
- }
2074
- }
2075
- if (isCompactionEvent(obj)) {
2076
- sink.emit({
2077
- id: nextId(),
2078
- ts: clampTs(
2079
- typeof obj.timestamp === "string" ? obj.timestamp : (/* @__PURE__ */ new Date()).toISOString()
2080
- ),
2081
- agent: "codex",
2082
- type: "compaction",
2083
- sessionId,
2084
- riskScore: riskOf("compaction"),
2085
- summary: `[${cursor.project}] \u22C8 context compacted`
2086
- });
2288
+ const model = payload.model;
2289
+ if (typeof model === "string") cursor.model = model;
2290
+ continue;
2291
+ }
2292
+ if (obj.type === "turn_context") {
2293
+ const model = payload.model;
2294
+ if (typeof model === "string") cursor.model = model;
2295
+ continue;
2296
+ }
2297
+ if (obj.type === "event_msg") {
2298
+ const usage = extractTokenUsage(obj);
2299
+ if (usage && cursor.lastResponseId) {
2300
+ const key = `${usage.input}|${usage.cacheRead}|${usage.output}`;
2301
+ if (cursor.lastUsageKey !== key) {
2302
+ cursor.lastUsageKey = key;
2303
+ const model = cursor.model ?? "gpt-5";
2304
+ const cost = costOf(model, usage);
2305
+ sink.enrich(cursor.lastResponseId, { usage, cost, model });
2087
2306
  }
2088
- return;
2089
2307
  }
2090
- const payload = obj.payload ?? {};
2091
- if (obj.type === "response_item" && payload.type === "function_call_output") {
2092
- const callId = typeof payload.call_id === "string" ? payload.call_id : "";
2093
- const pend = callId ? cursor.pendingCalls.get(callId) : void 0;
2094
- if (pend) {
2095
- cursor.pendingCalls.delete(callId);
2096
- const out = payload.output;
2097
- const outText = typeof out === "string" ? out : out && typeof out === "object" ? String(
2098
- out.content ?? JSON.stringify(out)
2099
- ) : "";
2100
- const isError = !!out && typeof out === "object" && out.status === "error";
2101
- const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
2102
- const duration = ts ? Math.max(0, new Date(ts).getTime() - pend.startMs) : void 0;
2103
- sink.enrich(pend.eventId, {
2104
- toolResult: outText.slice(0, 5e4),
2105
- toolError: isError,
2106
- ...duration != null ? { durationMs: duration } : {}
2107
- });
2108
- }
2109
- return;
2308
+ if (isCompactionEvent(obj)) {
2309
+ sink.emit({
2310
+ id: nextId(),
2311
+ ts: clampTs(
2312
+ typeof obj.timestamp === "string" ? obj.timestamp : (/* @__PURE__ */ new Date()).toISOString()
2313
+ ),
2314
+ agent: "codex",
2315
+ type: "compaction",
2316
+ sessionId,
2317
+ riskScore: riskOf("compaction"),
2318
+ summary: `[${cursor.project}] \u22C8 context compacted`
2319
+ });
2110
2320
  }
2111
- const event = translate2(obj, sessionId, cursor.project);
2112
- if (event) {
2113
- if (cursor.pendingParentSpawnId) {
2114
- event.details = {
2115
- ...event.details ?? {},
2116
- parentSpawnId: cursor.pendingParentSpawnId
2117
- };
2118
- cursor.pendingParentSpawnId = void 0;
2119
- }
2120
- sink.emit(event);
2121
- if (event.type === "response") cursor.lastResponseId = event.id;
2122
- const cid = event.details?.toolUseId;
2123
- if (cid && event.type !== "response" && event.type !== "prompt") {
2124
- cursor.pendingCalls.set(cid, {
2125
- eventId: event.id,
2126
- startMs: new Date(event.ts).getTime()
2127
- });
2128
- if (cursor.pendingCalls.size > MAX_PENDING) {
2129
- const firstKey = cursor.pendingCalls.keys().next().value;
2130
- if (firstKey !== void 0) cursor.pendingCalls.delete(firstKey);
2131
- }
2132
- }
2321
+ continue;
2322
+ }
2323
+ if (obj.type === "response_item" && payload.type === "function_call_output") {
2324
+ const callId = typeof payload.call_id === "string" ? payload.call_id : "";
2325
+ const pend = callId ? cursor.pendingCalls.get(callId) : void 0;
2326
+ if (pend) {
2327
+ cursor.pendingCalls.delete(callId);
2328
+ const out = payload.output;
2329
+ const outText = typeof out === "string" ? out : out && typeof out === "object" ? String(
2330
+ out.content ?? JSON.stringify(out)
2331
+ ) : "";
2332
+ const isError = !!out && typeof out === "object" && out.status === "error";
2333
+ const ts = typeof obj.timestamp === "string" ? obj.timestamp : "";
2334
+ const duration = ts ? Math.max(0, new Date(ts).getTime() - pend.startMs) : void 0;
2335
+ sink.enrich(pend.eventId, {
2336
+ toolResult: outText.slice(0, 5e4),
2337
+ toolError: isError,
2338
+ ...duration != null ? { durationMs: duration } : {}
2339
+ });
2133
2340
  }
2134
- } catch {
2341
+ continue;
2135
2342
  }
2136
- });
2137
- rl.on("close", () => {
2138
- cursor.offset = start + consumed;
2139
- });
2343
+ const event = translate2(obj, sessionId, cursor.project);
2344
+ if (!event) continue;
2345
+ if (cursor.pendingParentSpawnId) {
2346
+ event.details = {
2347
+ ...event.details ?? {},
2348
+ parentSpawnId: cursor.pendingParentSpawnId
2349
+ };
2350
+ cursor.pendingParentSpawnId = void 0;
2351
+ }
2352
+ sink.emit(event);
2353
+ if (event.type === "response") cursor.lastResponseId = event.id;
2354
+ const cid = event.details?.toolUseId;
2355
+ if (cid && event.type !== "response" && event.type !== "prompt") {
2356
+ cursor.pendingCalls.set(cid, {
2357
+ eventId: event.id,
2358
+ startMs: new Date(event.ts).getTime()
2359
+ });
2360
+ if (cursor.pendingCalls.size > MAX_PENDING) {
2361
+ const firstKey = cursor.pendingCalls.keys().next().value;
2362
+ if (firstKey !== void 0) cursor.pendingCalls.delete(firstKey);
2363
+ }
2364
+ }
2365
+ }
2140
2366
  };
2141
2367
  watcher2.on("add", (f) => handle(f, true));
2142
2368
  watcher2.on("change", (f) => handle(f, false));
@@ -2173,7 +2399,7 @@ function translate2(obj, sessionId, project) {
2173
2399
  type,
2174
2400
  sessionId,
2175
2401
  riskScore: 0,
2176
- summary: `[${project}] ${type}: ${truncate4(text, 80)}`,
2402
+ summary: `[${project}] ${type}: ${truncate5(text, 80)}`,
2177
2403
  details: { fullText: text }
2178
2404
  };
2179
2405
  }
@@ -2193,7 +2419,7 @@ function translate2(obj, sessionId, project) {
2193
2419
  cmd,
2194
2420
  tool: name,
2195
2421
  riskScore: riskOf("shell_exec", void 0, cmd),
2196
- summary: `[${project}] shell: ${truncate4(cmd, 80)}`,
2422
+ summary: `[${project}] shell: ${truncate5(cmd, 80)}`,
2197
2423
  details: {
2198
2424
  toolInput: args ?? void 0,
2199
2425
  toolUseId: callId || void 0
@@ -2270,7 +2496,7 @@ function extractSessionId(file) {
2270
2496
  const m = base.match(/rollout-[0-9T:\-.]+-(.+)$/);
2271
2497
  return m?.[1] ?? base;
2272
2498
  }
2273
- function truncate4(s, n) {
2499
+ function truncate5(s, n) {
2274
2500
  return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
2275
2501
  }
2276
2502
  function safeSize3(file) {
@@ -2288,6 +2514,8 @@ var init_codex = __esm({
2288
2514
  init_ids();
2289
2515
  init_cost();
2290
2516
  init_spawn_tracker();
2517
+ init_jsonl_stream();
2518
+ init_parse_errors();
2291
2519
  BACKFILL_BYTES3 = 4 * 1024 * 1024;
2292
2520
  MAX_PENDING = 2e3;
2293
2521
  }
@@ -2295,16 +2523,16 @@ var init_codex = __esm({
2295
2523
 
2296
2524
  // src/adapters/hermes.ts
2297
2525
  import chokidar7 from "chokidar";
2298
- import { existsSync as existsSync8 } from "fs";
2299
- import { homedir as homedir6 } from "os";
2300
- import { join as join8 } from "path";
2526
+ import { existsSync as existsSync9 } from "fs";
2527
+ import { homedir as homedir7 } from "os";
2528
+ import { join as join9 } from "path";
2301
2529
  import Database from "better-sqlite3";
2302
2530
  function resolveHermesDbPath() {
2303
2531
  const explicit = process.env.HERMES_DB_PATH?.trim();
2304
2532
  if (explicit && explicit.length > 0) return explicit;
2305
2533
  const hermesHome = process.env.HERMES_HOME?.trim();
2306
- const base = hermesHome && hermesHome.length > 0 ? hermesHome : join8(homedir6(), ".hermes");
2307
- return join8(base, "state.db");
2534
+ const base = hermesHome && hermesHome.length > 0 ? hermesHome : join9(homedir7(), ".hermes");
2535
+ return join9(base, "state.db");
2308
2536
  }
2309
2537
  function translateHermesSessionStart(s, source) {
2310
2538
  const ts = new Date(Math.floor(s.started_at * 1e3)).toISOString();
@@ -2411,7 +2639,7 @@ function hermesSummaryFor(type, m, toolName) {
2411
2639
  function startHermesAdapter(sink) {
2412
2640
  const emit = typeof sink === "function" ? sink : sink.emit;
2413
2641
  const dbPath = resolveHermesDbPath();
2414
- if (!existsSync8(dbPath)) return () => {
2642
+ if (!existsSync9(dbPath)) return () => {
2415
2643
  };
2416
2644
  let db2 = null;
2417
2645
  let lastMessageId = 0;
@@ -2505,7 +2733,7 @@ var init_hermes = __esm({
2505
2733
  "use strict";
2506
2734
  init_schema();
2507
2735
  init_ids();
2508
- DEFAULT_DB_PATH = join8(homedir6(), ".hermes", "state.db");
2736
+ DEFAULT_DB_PATH = join9(homedir7(), ".hermes", "state.db");
2509
2737
  }
2510
2738
  });
2511
2739
 
@@ -2915,8 +3143,11 @@ var init_project_index = __esm({
2915
3143
  });
2916
3144
 
2917
3145
  // src/server/routes/projects.ts
2918
- function registerProjectRoutes(app, events) {
3146
+ function registerProjectRoutes(app, events, store) {
2919
3147
  app.get("/api/projects", async () => {
3148
+ if (store) {
3149
+ return { projects: store.listProjects() };
3150
+ }
2920
3151
  const rows = buildProjectIndex(events).map((p) => ({
2921
3152
  name: p.name,
2922
3153
  eventCount: p.events,
@@ -2931,7 +3162,25 @@ function registerProjectRoutes(app, events) {
2931
3162
  "/api/projects/:name/sessions",
2932
3163
  async (req) => {
2933
3164
  const name = decodeURIComponent(req.params.name);
2934
- const sessions = buildSessionRows(events, name);
3165
+ if (store) {
3166
+ const sessions2 = store.listSessions({ project: name }).map((s) => ({
3167
+ sessionId: s.sessionId,
3168
+ agent: s.agent,
3169
+ project: s.project || name,
3170
+ eventCount: s.eventCount,
3171
+ events: s.eventCount,
3172
+ // for consumers expecting SessionRow
3173
+ cost: s.costUsd,
3174
+ firstTs: s.firstTs,
3175
+ lastTs: s.lastTs,
3176
+ firstPrompt: ""
3177
+ }));
3178
+ return { project: name, sessions: sessions2 };
3179
+ }
3180
+ const sessions = buildSessionRows(events, name).map((r) => ({
3181
+ ...r,
3182
+ eventCount: r.events
3183
+ }));
2935
3184
  return { project: name, sessions };
2936
3185
  }
2937
3186
  );
@@ -3324,10 +3573,10 @@ var init_export = __esm({
3324
3573
  });
3325
3574
 
3326
3575
  // src/server/routes/sessions.ts
3327
- function registerSessionRoutes(app, events) {
3576
+ function registerSessionRoutes(app, events, store) {
3328
3577
  app.get("/api/sessions/:id", async (req, reply) => {
3329
3578
  const id = decodeURIComponent(req.params.id);
3330
- const sessionEvents = events.filter((e) => e.sessionId === id);
3579
+ const sessionEvents = store ? store.listSessionEvents(id) : events.filter((e) => e.sessionId === id);
3331
3580
  if (sessionEvents.length === 0) {
3332
3581
  reply.code(404);
3333
3582
  return { error: "session not found (or events not yet loaded)" };
@@ -3345,8 +3594,8 @@ function registerSessionRoutes(app, events) {
3345
3594
  const id = decodeURIComponent(req.params.id);
3346
3595
  return {
3347
3596
  sessionId: id,
3348
- breakdown: attributeTokens(events, id),
3349
- turns: attributeTurns(events, id)
3597
+ breakdown: attributeTokens(store ? store.listSessionEvents(id) : events, id),
3598
+ turns: attributeTurns(store ? store.listSessionEvents(id) : events, id)
3350
3599
  };
3351
3600
  }
3352
3601
  );
@@ -3356,7 +3605,7 @@ function registerSessionRoutes(app, events) {
3356
3605
  const id = decodeURIComponent(req.params.id);
3357
3606
  return {
3358
3607
  sessionId: id,
3359
- series: buildCompactionSeries(events, id)
3608
+ series: buildCompactionSeries(store ? store.listSessionEvents(id) : events, id)
3360
3609
  };
3361
3610
  }
3362
3611
  );
@@ -3366,7 +3615,7 @@ function registerSessionRoutes(app, events) {
3366
3615
  const id = decodeURIComponent(req.params.id);
3367
3616
  return {
3368
3617
  sessionId: id,
3369
- graph: buildCallGraph(events, id)
3618
+ graph: buildCallGraph(store ? store.listSessionEvents(id) : events, id)
3370
3619
  };
3371
3620
  }
3372
3621
  );
@@ -3374,7 +3623,7 @@ function registerSessionRoutes(app, events) {
3374
3623
  "/api/sessions/:id/export",
3375
3624
  async (req, reply) => {
3376
3625
  const id = decodeURIComponent(req.params.id);
3377
- const sessionEvents = events.filter((e) => e.sessionId === id);
3626
+ const sessionEvents = store ? store.listSessionEvents(id) : events.filter((e) => e.sessionId === id);
3378
3627
  if (sessionEvents.length === 0) {
3379
3628
  reply.code(404);
3380
3629
  return { error: "session not found" };
@@ -3432,14 +3681,14 @@ var init_agents = __esm({
3432
3681
  });
3433
3682
 
3434
3683
  // src/util/claude-permissions.ts
3435
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
3436
- import { homedir as homedir7 } from "os";
3437
- import { join as join9 } from "path";
3684
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
3685
+ import { homedir as homedir8 } from "os";
3686
+ import { join as join10 } from "path";
3438
3687
  function readClaudePermissions(workspace) {
3439
- const sources = [join9(homedir7(), ".claude", "settings.json")];
3688
+ const sources = [join10(homedir8(), ".claude", "settings.json")];
3440
3689
  if (workspace) {
3441
- sources.push(join9(workspace, ".claude", "settings.json"));
3442
- sources.push(join9(workspace, ".claude", "settings.local.json"));
3690
+ sources.push(join10(workspace, ".claude", "settings.json"));
3691
+ sources.push(join10(workspace, ".claude", "settings.local.json"));
3443
3692
  }
3444
3693
  const out = [];
3445
3694
  for (const path12 of sources) {
@@ -3449,9 +3698,9 @@ function readClaudePermissions(workspace) {
3449
3698
  return out;
3450
3699
  }
3451
3700
  function readOne(path12) {
3452
- if (!existsSync9(path12)) return null;
3701
+ if (!existsSync10(path12)) return null;
3453
3702
  try {
3454
- const raw = readFileSync5(path12, "utf8");
3703
+ const raw = readFileSync6(path12, "utf8");
3455
3704
  const obj = JSON.parse(raw);
3456
3705
  const perms = obj.permissions ?? {};
3457
3706
  const allow = toStringArray(perms.allow);
@@ -3614,18 +3863,18 @@ import fs8 from "fs";
3614
3863
  import os8 from "os";
3615
3864
  import path8 from "path";
3616
3865
  function readGeminiPermissions(home = os8.homedir()) {
3617
- const settingsPath = path8.join(home, ".gemini", "settings.json");
3866
+ const settingsPath2 = path8.join(home, ".gemini", "settings.json");
3618
3867
  const trustedFoldersPath = path8.join(home, ".gemini", "trustedFolders.json");
3619
3868
  const out = {
3620
- settingsPath,
3869
+ settingsPath: settingsPath2,
3621
3870
  trustedFoldersPath,
3622
3871
  trustedFolders: [],
3623
3872
  present: false
3624
3873
  };
3625
- if (!fs8.existsSync(settingsPath)) return out;
3874
+ if (!fs8.existsSync(settingsPath2)) return out;
3626
3875
  out.present = true;
3627
3876
  try {
3628
- const raw = JSON.parse(fs8.readFileSync(settingsPath, "utf8"));
3877
+ const raw = JSON.parse(fs8.readFileSync(settingsPath2, "utf8"));
3629
3878
  const sec = raw?.security ?? {};
3630
3879
  const auth = sec.auth ?? {};
3631
3880
  out.authType = typeof auth.selectedType === "string" ? auth.selectedType : typeof auth.method === "string" ? auth.method : void 0;
@@ -3657,14 +3906,14 @@ var init_gemini_permissions = __esm({
3657
3906
  });
3658
3907
 
3659
3908
  // src/util/openclaw-config.ts
3660
- import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
3661
- import { homedir as homedir8 } from "os";
3662
- import { join as join10 } from "path";
3909
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
3910
+ import { homedir as homedir9 } from "os";
3911
+ import { join as join11 } from "path";
3663
3912
  function readOpenClawConfig() {
3664
- const path12 = join10(homedir8(), ".openclaw", "openclaw.json");
3665
- if (!existsSync10(path12)) return null;
3913
+ const path12 = join11(homedir9(), ".openclaw", "openclaw.json");
3914
+ if (!existsSync11(path12)) return null;
3666
3915
  try {
3667
- const raw = readFileSync6(path12, "utf8");
3916
+ const raw = readFileSync7(path12, "utf8");
3668
3917
  const obj = JSON.parse(raw);
3669
3918
  const agentsObj = obj.agents ?? {};
3670
3919
  const defaults = agentsObj.defaults ?? {};
@@ -4183,7 +4432,7 @@ var init_semantic_index = __esm({
4183
4432
  });
4184
4433
 
4185
4434
  // src/server/routes/search.ts
4186
- function registerSearchRoutes(app, events) {
4435
+ function registerSearchRoutes(app, events, store) {
4187
4436
  app.post("/api/search", async (req, reply) => {
4188
4437
  const query = (req.body?.query ?? "").trim();
4189
4438
  const mode = req.body?.mode ?? "live";
@@ -4200,6 +4449,28 @@ function registerSearchRoutes(app, events) {
4200
4449
  }
4201
4450
  return { mode, hits: hits.map((e) => ({ kind: "live", event: e })) };
4202
4451
  }
4452
+ if (mode === "history") {
4453
+ if (!store) {
4454
+ return {
4455
+ mode,
4456
+ hits: [],
4457
+ status: "history mode requires a SQLite store \u2014 pass --no-web off and ensure ~/.agentwatch is writable"
4458
+ };
4459
+ }
4460
+ const hits = store.searchFts(query, { limit }).map((h) => ({
4461
+ kind: "history",
4462
+ hit: {
4463
+ eventId: h.eventId,
4464
+ sessionId: h.sessionId,
4465
+ agent: h.agent,
4466
+ ts: h.ts,
4467
+ type: h.type,
4468
+ snippet: h.snippet,
4469
+ rank: h.rank
4470
+ }
4471
+ }));
4472
+ return { mode, hits };
4473
+ }
4203
4474
  if (mode === "cross") {
4204
4475
  const raw = searchAllSessions(query, Math.max(limit, 300));
4205
4476
  const sinceMs = req.body?.since ? Date.parse(req.body.since) : null;
@@ -4261,52 +4532,607 @@ var init_search = __esm({
4261
4532
  }
4262
4533
  });
4263
4534
 
4264
- // src/server/routes/config.ts
4265
- import { readFileSync as readFileSync7, writeFileSync, mkdirSync, existsSync as existsSync11 } from "fs";
4266
- import { homedir as homedir9 } from "os";
4267
- import { join as join11, dirname as dirname2 } from "path";
4268
- function readConfig(kind) {
4269
- const p = PATHS[kind];
4270
- if (!existsSync11(p)) return DEFAULTS[kind];
4271
- try {
4272
- return JSON.parse(readFileSync7(p, "utf8"));
4273
- } catch {
4274
- return DEFAULTS[kind];
4535
+ // src/adapters/hooks-dedup.ts
4536
+ var hooks_dedup_exports = {};
4537
+ __export(hooks_dedup_exports, {
4538
+ clearHookDedup: () => clearHookDedup,
4539
+ markHookSeen: () => markHookSeen,
4540
+ toolSignature: () => toolSignature,
4541
+ wasHookSeen: () => wasHookSeen,
4542
+ withClaudeHookDedup: () => withClaudeHookDedup
4543
+ });
4544
+ function markHookSeen(signature) {
4545
+ seen.set(signature, Date.now());
4546
+ if (seen.size > 1e3) evictOlderThan(6e4);
4547
+ }
4548
+ function wasHookSeen(signature) {
4549
+ const t = seen.get(signature);
4550
+ if (t == null) return false;
4551
+ if (Date.now() - t > WINDOW_MS) {
4552
+ seen.delete(signature);
4553
+ return false;
4275
4554
  }
4555
+ return true;
4276
4556
  }
4277
- function writeConfig(kind, value) {
4278
- const p = PATHS[kind];
4279
- mkdirSync(dirname2(p), { recursive: true });
4280
- writeFileSync(p, JSON.stringify(value, null, 2), "utf8");
4557
+ function clearHookDedup() {
4558
+ seen.clear();
4281
4559
  }
4282
- function validate(kind, value) {
4283
- if (kind === "budgets") {
4284
- if (typeof value !== "object" || value == null) return { ok: false, error: "budgets must be an object" };
4285
- const v = value;
4286
- for (const k of ["perSessionUsd", "perDayUsd"]) {
4287
- const n = v[k];
4288
- if (n != null && typeof n !== "number") return { ok: false, error: `${k} must be number or null` };
4289
- }
4290
- return { ok: true };
4560
+ function evictOlderThan(ms) {
4561
+ const cutoff = Date.now() - ms;
4562
+ for (const [sig, t] of seen) {
4563
+ if (t < cutoff) seen.delete(sig);
4291
4564
  }
4292
- if (kind === "anomaly") {
4293
- if (typeof value !== "object" || value == null) return { ok: false, error: "anomaly must be an object" };
4294
- const v = value;
4295
- for (const k of ["zScore", "loopWindow", "loopMinRepeats", "minSamples"]) {
4296
- if (v[k] != null && typeof v[k] !== "number") return { ok: false, error: `${k} must be number` };
4297
- }
4298
- return { ok: true };
4565
+ }
4566
+ function toolSignature(sessionId, toolUseId) {
4567
+ if (!sessionId || !toolUseId) return null;
4568
+ return `${sessionId}:${toolUseId}`;
4569
+ }
4570
+ function withClaudeHookDedup(inner) {
4571
+ return {
4572
+ emit: (e) => {
4573
+ if (e.agent === "claude-code" && e.details?.source !== "hooks") {
4574
+ const sig = toolSignature(e.sessionId, e.details?.toolUseId);
4575
+ if (sig && wasHookSeen(sig)) return;
4576
+ }
4577
+ inner.emit(e);
4578
+ },
4579
+ enrich: inner.enrich
4580
+ };
4581
+ }
4582
+ var WINDOW_MS, seen;
4583
+ var init_hooks_dedup = __esm({
4584
+ "src/adapters/hooks-dedup.ts"() {
4585
+ "use strict";
4586
+ WINDOW_MS = 5e3;
4587
+ seen = /* @__PURE__ */ new Map();
4299
4588
  }
4300
- if (kind === "triggers") {
4301
- if (!Array.isArray(value)) return { ok: false, error: "triggers must be an array" };
4302
- for (let i = 0; i < value.length; i++) {
4303
- const t = value[i];
4304
- if (typeof t !== "object" || t == null) return { ok: false, error: `triggers[${i}] must be an object` };
4305
- if (!t.title || !t.body)
4306
- return { ok: false, error: `triggers[${i}] requires title + body` };
4589
+ });
4590
+
4591
+ // src/adapters/claude-hooks.ts
4592
+ import { randomUUID } from "crypto";
4593
+ function registerClaudeHooksRoute(app, sink) {
4594
+ app.post(
4595
+ "/api/hooks/:event",
4596
+ async (req) => {
4597
+ const eventName = decodeURIComponent(req.params.event);
4598
+ const body = req.body ?? {};
4599
+ const event = translateHook(eventName, body);
4600
+ if (!event) return { ok: false, reason: "unrecognized payload" };
4601
+ const sig = toolSignature(event.sessionId, body.tool_use_id);
4602
+ if (sig) markHookSeen(sig);
4603
+ sink.emit(event);
4604
+ return { ok: true, eventId: event.id };
4307
4605
  }
4308
- return { ok: true };
4309
- }
4606
+ );
4607
+ }
4608
+ function translateHook(hookName, body) {
4609
+ const ts = clampTs((/* @__PURE__ */ new Date()).toISOString());
4610
+ const sessionId = body.session_id;
4611
+ const cwd = body.cwd;
4612
+ const id = `hooks:${randomUUID()}`;
4613
+ const details = {
4614
+ source: body.transcript_path ?? "hooks"
4615
+ };
4616
+ details.source = "hooks";
4617
+ const projectPrefix = cwd ? `[${basenameOf(cwd)}] ` : "";
4618
+ switch (hookName) {
4619
+ case "SessionStart": {
4620
+ return {
4621
+ id,
4622
+ ts,
4623
+ agent: "claude-code",
4624
+ type: "session_start",
4625
+ riskScore: 1,
4626
+ ...sessionId ? { sessionId } : {},
4627
+ summary: `${projectPrefix}SessionStart${body.source ? ` (${body.source})` : ""}`,
4628
+ details
4629
+ };
4630
+ }
4631
+ case "SessionEnd":
4632
+ case "Stop":
4633
+ case "SubagentStop": {
4634
+ return {
4635
+ id,
4636
+ ts,
4637
+ agent: "claude-code",
4638
+ type: "session_end",
4639
+ riskScore: 1,
4640
+ ...sessionId ? { sessionId } : {},
4641
+ summary: `${projectPrefix}${hookName}`,
4642
+ details
4643
+ };
4644
+ }
4645
+ case "UserPromptSubmit": {
4646
+ const text = body.prompt ?? "";
4647
+ return {
4648
+ id,
4649
+ ts,
4650
+ agent: "claude-code",
4651
+ type: "prompt",
4652
+ riskScore: 1,
4653
+ ...sessionId ? { sessionId } : {},
4654
+ summary: `${projectPrefix}${truncate6(text, 80)}`,
4655
+ details: { ...details, fullText: text }
4656
+ };
4657
+ }
4658
+ case "PreToolUse": {
4659
+ const tool = body.tool_name ?? "tool";
4660
+ const type = mapToolToType(tool, body.tool_input);
4661
+ const path12 = pathFromInput(body.tool_input);
4662
+ const cmd = cmdFromInput(body.tool_input);
4663
+ const summary = `${projectPrefix}${tool}: ${path12 ?? cmd ?? truncate6(JSON.stringify(body.tool_input ?? {}), 60)}`;
4664
+ return {
4665
+ id,
4666
+ ts,
4667
+ agent: "claude-code",
4668
+ type,
4669
+ riskScore: riskOf(type, path12, cmd),
4670
+ ...sessionId ? { sessionId } : {},
4671
+ tool,
4672
+ summary,
4673
+ ...path12 ? { path: path12 } : {},
4674
+ ...cmd ? { cmd } : {},
4675
+ details: {
4676
+ ...details,
4677
+ ...body.tool_input ? { toolInput: body.tool_input } : {},
4678
+ ...body.tool_use_id ? { toolUseId: body.tool_use_id } : {}
4679
+ }
4680
+ };
4681
+ }
4682
+ case "PostToolUse": {
4683
+ const tool = body.tool_name ?? "tool";
4684
+ const path12 = pathFromInput(body.tool_input);
4685
+ const cmd = cmdFromInput(body.tool_input);
4686
+ const result = typeof body.tool_response === "string" ? body.tool_response : body.tool_response ? JSON.stringify(body.tool_response) : "";
4687
+ const summary = `${projectPrefix}${tool} done: ${path12 ?? cmd ?? "result"}`;
4688
+ return {
4689
+ id,
4690
+ ts,
4691
+ agent: "claude-code",
4692
+ type: "tool_call",
4693
+ riskScore: 1,
4694
+ ...sessionId ? { sessionId } : {},
4695
+ tool,
4696
+ summary,
4697
+ ...path12 ? { path: path12 } : {},
4698
+ ...cmd ? { cmd } : {},
4699
+ details: {
4700
+ ...details,
4701
+ toolResult: result.slice(0, 8 * 1024),
4702
+ ...body.tool_use_id ? { toolUseId: body.tool_use_id } : {}
4703
+ }
4704
+ };
4705
+ }
4706
+ case "PreCompact":
4707
+ case "PostCompact": {
4708
+ return {
4709
+ id,
4710
+ ts,
4711
+ agent: "claude-code",
4712
+ type: "compaction",
4713
+ riskScore: 1,
4714
+ ...sessionId ? { sessionId } : {},
4715
+ summary: `${projectPrefix}${hookName}${body.trigger ? ` (${body.trigger})` : ""}`,
4716
+ details
4717
+ };
4718
+ }
4719
+ case "Notification": {
4720
+ const text = body.message ?? "";
4721
+ return {
4722
+ id,
4723
+ ts,
4724
+ agent: "claude-code",
4725
+ type: "response",
4726
+ riskScore: 1,
4727
+ ...sessionId ? { sessionId } : {},
4728
+ summary: `${projectPrefix}Notification: ${truncate6(text, 80)}`,
4729
+ details: { ...details, fullText: text }
4730
+ };
4731
+ }
4732
+ default: {
4733
+ return {
4734
+ id,
4735
+ ts,
4736
+ agent: "claude-code",
4737
+ type: "tool_call",
4738
+ riskScore: 1,
4739
+ ...sessionId ? { sessionId } : {},
4740
+ tool: hookName,
4741
+ summary: `${projectPrefix}hook:${hookName}`,
4742
+ details: { ...details, toolInput: body }
4743
+ };
4744
+ }
4745
+ }
4746
+ }
4747
+ function mapToolToType(tool, input) {
4748
+ const t = tool.toLowerCase();
4749
+ if (t === "bash") return "shell_exec";
4750
+ if (t === "read") return "file_read";
4751
+ if (t === "write" || t === "edit" || t === "multiedit") return "file_write";
4752
+ if (input && (input.command || input.cmd)) return "shell_exec";
4753
+ if (input && (input.file_path || input.path)) return "file_read";
4754
+ return "tool_call";
4755
+ }
4756
+ function pathFromInput(input) {
4757
+ if (!input) return void 0;
4758
+ const candidate = input.file_path ?? input.path ?? input.notebook_path;
4759
+ return typeof candidate === "string" ? candidate : void 0;
4760
+ }
4761
+ function cmdFromInput(input) {
4762
+ if (!input) return void 0;
4763
+ const candidate = input.command ?? input.cmd;
4764
+ return typeof candidate === "string" ? candidate : void 0;
4765
+ }
4766
+ function basenameOf(p) {
4767
+ const idx = p.replace(/\/$/, "").lastIndexOf("/");
4768
+ return idx === -1 ? p : p.slice(idx + 1);
4769
+ }
4770
+ function truncate6(s, n) {
4771
+ if (s.length <= n) return s;
4772
+ return `${s.slice(0, n - 1)}\u2026`;
4773
+ }
4774
+ var init_claude_hooks = __esm({
4775
+ "src/adapters/claude-hooks.ts"() {
4776
+ "use strict";
4777
+ init_schema();
4778
+ init_hooks_dedup();
4779
+ }
4780
+ });
4781
+
4782
+ // src/git/correlate.ts
4783
+ import { spawnSync as spawnSync3 } from "child_process";
4784
+ import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync6 } from "fs";
4785
+ import { join as join12, resolve } from "path";
4786
+ function runGit(args, opts = {}) {
4787
+ const verb = args[0];
4788
+ if (!verb || !READ_ONLY_GIT_VERBS.has(verb)) {
4789
+ throw new Error(`git verb "${verb}" not in read-only allow-list`);
4790
+ }
4791
+ const result = spawnSync3("git", args, {
4792
+ cwd: opts.cwd,
4793
+ encoding: "utf-8",
4794
+ timeout: opts.timeoutMs ?? 1e4,
4795
+ maxBuffer: 32 * 1024 * 1024
4796
+ });
4797
+ if (result.error) throw result.error;
4798
+ if (result.status !== 0) {
4799
+ throw new Error(
4800
+ `git ${args.join(" ")} exited ${result.status}: ${result.stderr.slice(0, 500)}`
4801
+ );
4802
+ }
4803
+ return result.stdout;
4804
+ }
4805
+ function findProjectGitRoot(workspaceRoot, projectName) {
4806
+ if (!existsSync12(workspaceRoot)) return null;
4807
+ let entries;
4808
+ try {
4809
+ entries = readdirSync2(workspaceRoot);
4810
+ } catch {
4811
+ return null;
4812
+ }
4813
+ for (const entry of entries) {
4814
+ if (entry !== projectName) continue;
4815
+ const candidate = join12(workspaceRoot, entry);
4816
+ try {
4817
+ const s = statSync6(candidate);
4818
+ if (!s.isDirectory()) continue;
4819
+ } catch {
4820
+ continue;
4821
+ }
4822
+ const gitEntry = join12(candidate, ".git");
4823
+ if (!existsSync12(gitEntry)) continue;
4824
+ return resolve(candidate);
4825
+ }
4826
+ return null;
4827
+ }
4828
+ function gitCommonDir(repoPath) {
4829
+ try {
4830
+ const out = runGit(["rev-parse", "--git-common-dir"], { cwd: repoPath });
4831
+ const trimmed = out.trim();
4832
+ if (!trimmed) return null;
4833
+ return resolve(repoPath, trimmed);
4834
+ } catch {
4835
+ return null;
4836
+ }
4837
+ }
4838
+ function getCurrentBranch(repoPath) {
4839
+ try {
4840
+ const out = runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoPath });
4841
+ const branch = out.trim();
4842
+ if (!branch || branch === "HEAD") return null;
4843
+ return branch;
4844
+ } catch {
4845
+ return null;
4846
+ }
4847
+ }
4848
+ function listCommits(repoPath, opts = {}) {
4849
+ const args = ["log", "--no-merges", "--reverse"];
4850
+ if (opts.since) args.push(`--since=${opts.since}`);
4851
+ if (opts.until) args.push(`--until=${opts.until}`);
4852
+ args.push("--pretty=format:%x02%H%x1f%aI%x1f%an%x1f%s", "--numstat");
4853
+ let out;
4854
+ try {
4855
+ out = runGit(args, { cwd: repoPath });
4856
+ } catch {
4857
+ return [];
4858
+ }
4859
+ const records = out.split("").map((r) => r.trim()).filter(Boolean);
4860
+ const commits = [];
4861
+ for (const rec of records) {
4862
+ const headerEnd = rec.indexOf("\n");
4863
+ const header = headerEnd === -1 ? rec : rec.slice(0, headerEnd);
4864
+ const numstat = headerEnd === -1 ? "" : rec.slice(headerEnd + 1);
4865
+ const [hash, authorDate, authorName, subject] = header.split("");
4866
+ if (!hash || !authorDate) continue;
4867
+ let insertions = 0;
4868
+ let deletions = 0;
4869
+ let files = 0;
4870
+ for (const line of numstat.split("\n")) {
4871
+ const trimmed = line.trim();
4872
+ if (!trimmed) continue;
4873
+ const parts = trimmed.split(/\s+/);
4874
+ const ins = parts[0] === "-" ? 0 : Number(parts[0] ?? "0");
4875
+ const del = parts[1] === "-" ? 0 : Number(parts[1] ?? "0");
4876
+ if (Number.isFinite(ins)) insertions += ins;
4877
+ if (Number.isFinite(del)) deletions += del;
4878
+ files += 1;
4879
+ }
4880
+ commits.push({
4881
+ hash,
4882
+ authorDate,
4883
+ authorName: authorName ?? "",
4884
+ filesChanged: files,
4885
+ insertions,
4886
+ deletions,
4887
+ subject: subject ?? ""
4888
+ });
4889
+ }
4890
+ return commits;
4891
+ }
4892
+ function correlateSessionYield(session, commits) {
4893
+ const firstMs = Date.parse(session.firstTs);
4894
+ const lastMs = Date.parse(session.lastTs);
4895
+ const upper = Number.isFinite(lastMs) ? lastMs + COMMIT_GRACE_MS : Infinity;
4896
+ const lower = Number.isFinite(firstMs) ? firstMs : -Infinity;
4897
+ const matched = commits.filter((c) => {
4898
+ const t = Date.parse(c.authorDate);
4899
+ if (!Number.isFinite(t)) return false;
4900
+ return t >= lower && t <= upper;
4901
+ });
4902
+ let totalInsertions = 0;
4903
+ let totalDeletions = 0;
4904
+ let totalFiles = 0;
4905
+ for (const c of matched) {
4906
+ totalInsertions += c.insertions;
4907
+ totalDeletions += c.deletions;
4908
+ totalFiles += c.filesChanged;
4909
+ }
4910
+ const totalLines = totalInsertions + totalDeletions;
4911
+ return {
4912
+ sessionId: session.sessionId,
4913
+ costUsd: session.costUsd,
4914
+ commits: matched,
4915
+ totalInsertions,
4916
+ totalDeletions,
4917
+ totalFilesChanged: totalFiles,
4918
+ costPerCommit: matched.length > 0 ? session.costUsd / matched.length : null,
4919
+ costPerLineChanged: totalLines > 0 ? session.costUsd / totalLines : null
4920
+ };
4921
+ }
4922
+ function aggregateProjectYield(project, sessions, commits) {
4923
+ const yields = sessions.map((s) => correlateSessionYield(s, commits));
4924
+ const weekly = /* @__PURE__ */ new Map();
4925
+ for (const y of yields) {
4926
+ const session = sessions.find((s) => s.sessionId === y.sessionId);
4927
+ if (!session) continue;
4928
+ const week = mondayOfWeekIso(session.firstTs);
4929
+ let bucket = weekly.get(week);
4930
+ if (!bucket) {
4931
+ bucket = { cost: 0, commits: /* @__PURE__ */ new Set() };
4932
+ weekly.set(week, bucket);
4933
+ }
4934
+ bucket.cost += session.costUsd;
4935
+ for (const c of y.commits) bucket.commits.add(c.hash);
4936
+ }
4937
+ const weeklyRows = Array.from(weekly.entries()).map(([weekStart, b]) => ({
4938
+ weekStart,
4939
+ costUsd: b.cost,
4940
+ commits: b.commits.size,
4941
+ costPerCommit: b.commits.size > 0 ? b.cost / b.commits.size : null
4942
+ })).sort((a, b) => a.weekStart < b.weekStart ? -1 : 1);
4943
+ const spendWithoutCommit = yields.filter((y) => y.commits.length === 0 && y.costUsd > 0).sort((a, b) => b.costUsd - a.costUsd);
4944
+ return { project, weekly: weeklyRows, spendWithoutCommit };
4945
+ }
4946
+ function mondayOfWeekIso(iso) {
4947
+ const d = new Date(iso);
4948
+ if (Number.isNaN(d.getTime())) return iso;
4949
+ const day = d.getUTCDay();
4950
+ const offsetToMonday = day === 0 ? -6 : 1 - day;
4951
+ const monday = new Date(d);
4952
+ monday.setUTCDate(d.getUTCDate() + offsetToMonday);
4953
+ monday.setUTCHours(0, 0, 0, 0);
4954
+ return monday.toISOString().slice(0, 10);
4955
+ }
4956
+ var COMMIT_GRACE_MS, READ_ONLY_GIT_VERBS;
4957
+ var init_correlate = __esm({
4958
+ "src/git/correlate.ts"() {
4959
+ "use strict";
4960
+ COMMIT_GRACE_MS = 30 * 60 * 1e3;
4961
+ READ_ONLY_GIT_VERBS = /* @__PURE__ */ new Set([
4962
+ "log",
4963
+ "rev-parse",
4964
+ "worktree",
4965
+ "config",
4966
+ "branch",
4967
+ "show",
4968
+ "blame",
4969
+ "diff",
4970
+ "status",
4971
+ "remote"
4972
+ ]);
4973
+ }
4974
+ });
4975
+
4976
+ // src/server/routes/yield.ts
4977
+ function registerYieldRoutes(app, store) {
4978
+ app.get(
4979
+ "/api/sessions/:id/yield",
4980
+ async (req, reply) => {
4981
+ const id = decodeURIComponent(req.params.id);
4982
+ if (!store) return { sessionId: id, ok: false, reason: "no store" };
4983
+ const sessions = store.listSessions({ limit: 1, since: void 0 });
4984
+ const session = store.listSessions({ limit: 5e3 }).find(
4985
+ (s) => s.sessionId === id
4986
+ );
4987
+ if (!session) {
4988
+ reply.code(404);
4989
+ return { sessionId: id, ok: false, reason: "session not found" };
4990
+ }
4991
+ void sessions;
4992
+ if (!session.project) {
4993
+ return {
4994
+ sessionId: id,
4995
+ ok: false,
4996
+ reason: "session has no project tag"
4997
+ };
4998
+ }
4999
+ const repo = findProjectGitRoot(detectWorkspaceRoot(), session.project);
5000
+ if (!repo) {
5001
+ return {
5002
+ sessionId: id,
5003
+ ok: false,
5004
+ reason: "project is not a git repo under WORKSPACE_ROOT"
5005
+ };
5006
+ }
5007
+ const commits = listCommits(repo, {
5008
+ since: session.firstTs,
5009
+ until: new Date(
5010
+ Date.parse(session.lastTs) + 60 * 60 * 1e3
5011
+ ).toISOString()
5012
+ });
5013
+ return {
5014
+ sessionId: id,
5015
+ ok: true,
5016
+ project: session.project,
5017
+ repoPath: repo,
5018
+ yield: correlateSessionYield(session, commits)
5019
+ };
5020
+ }
5021
+ );
5022
+ app.get(
5023
+ "/api/projects/:name/yield",
5024
+ async (req) => {
5025
+ const name = decodeURIComponent(req.params.name);
5026
+ if (!store) return { project: name, ok: false, reason: "no store" };
5027
+ const repo = findProjectGitRoot(detectWorkspaceRoot(), name);
5028
+ if (!repo) {
5029
+ return {
5030
+ project: name,
5031
+ ok: false,
5032
+ reason: "project is not a git repo under WORKSPACE_ROOT"
5033
+ };
5034
+ }
5035
+ const sessions = store.listSessions({ project: name, limit: 5e3 });
5036
+ if (sessions.length === 0) {
5037
+ return { project: name, ok: true, repoPath: repo, yield: emptyYield(name) };
5038
+ }
5039
+ const earliest = sessions.map((s) => s.firstTs).sort()[0] ?? (/* @__PURE__ */ new Date()).toISOString();
5040
+ const latest = sessions.map((s) => s.lastTs).sort().pop() ?? (/* @__PURE__ */ new Date()).toISOString();
5041
+ const commits = listCommits(repo, {
5042
+ since: earliest,
5043
+ until: new Date(Date.parse(latest) + 60 * 60 * 1e3).toISOString()
5044
+ });
5045
+ return {
5046
+ project: name,
5047
+ ok: true,
5048
+ repoPath: repo,
5049
+ yield: aggregateProjectYield(name, sessions, commits)
5050
+ };
5051
+ }
5052
+ );
5053
+ }
5054
+ function emptyYield(project) {
5055
+ return { project, weekly: [], spendWithoutCommit: [] };
5056
+ }
5057
+ var init_yield = __esm({
5058
+ "src/server/routes/yield.ts"() {
5059
+ "use strict";
5060
+ init_correlate();
5061
+ init_workspace();
5062
+ }
5063
+ });
5064
+
5065
+ // src/server/routes/activity.ts
5066
+ function registerActivityRoutes(app, store) {
5067
+ app.get(
5068
+ "/api/sessions/:id/activity",
5069
+ async (req) => {
5070
+ const id = decodeURIComponent(req.params.id);
5071
+ if (!store) return { sessionId: id, buckets: [] };
5072
+ return { sessionId: id, buckets: store.activityBySession(id) };
5073
+ }
5074
+ );
5075
+ app.get(
5076
+ "/api/projects/:name/activity",
5077
+ async (req) => {
5078
+ const name = decodeURIComponent(req.params.name);
5079
+ if (!store) return { project: name, buckets: [] };
5080
+ return { project: name, buckets: store.activityByProject(name) };
5081
+ }
5082
+ );
5083
+ }
5084
+ var init_activity = __esm({
5085
+ "src/server/routes/activity.ts"() {
5086
+ "use strict";
5087
+ }
5088
+ });
5089
+
5090
+ // src/server/routes/config.ts
5091
+ import { readFileSync as readFileSync8, writeFileSync, mkdirSync, existsSync as existsSync13 } from "fs";
5092
+ import { homedir as homedir10 } from "os";
5093
+ import { join as join13, dirname as dirname2 } from "path";
5094
+ function readConfig(kind) {
5095
+ const p = PATHS[kind];
5096
+ if (!existsSync13(p)) return DEFAULTS[kind];
5097
+ try {
5098
+ return JSON.parse(readFileSync8(p, "utf8"));
5099
+ } catch {
5100
+ return DEFAULTS[kind];
5101
+ }
5102
+ }
5103
+ function writeConfig(kind, value) {
5104
+ const p = PATHS[kind];
5105
+ mkdirSync(dirname2(p), { recursive: true });
5106
+ writeFileSync(p, JSON.stringify(value, null, 2), "utf8");
5107
+ }
5108
+ function validate(kind, value) {
5109
+ if (kind === "budgets") {
5110
+ if (typeof value !== "object" || value == null) return { ok: false, error: "budgets must be an object" };
5111
+ const v = value;
5112
+ for (const k of ["perSessionUsd", "perDayUsd"]) {
5113
+ const n = v[k];
5114
+ if (n != null && typeof n !== "number") return { ok: false, error: `${k} must be number or null` };
5115
+ }
5116
+ return { ok: true };
5117
+ }
5118
+ if (kind === "anomaly") {
5119
+ if (typeof value !== "object" || value == null) return { ok: false, error: "anomaly must be an object" };
5120
+ const v = value;
5121
+ for (const k of ["zScore", "loopWindow", "loopMinRepeats", "minSamples"]) {
5122
+ if (v[k] != null && typeof v[k] !== "number") return { ok: false, error: `${k} must be number` };
5123
+ }
5124
+ return { ok: true };
5125
+ }
5126
+ if (kind === "triggers") {
5127
+ if (!Array.isArray(value)) return { ok: false, error: "triggers must be an array" };
5128
+ for (let i = 0; i < value.length; i++) {
5129
+ const t = value[i];
5130
+ if (typeof t !== "object" || t == null) return { ok: false, error: `triggers[${i}] must be an object` };
5131
+ if (!t.title || !t.body)
5132
+ return { ok: false, error: `triggers[${i}] requires title + body` };
5133
+ }
5134
+ return { ok: true };
5135
+ }
4310
5136
  return { ok: false, error: "unknown config kind" };
4311
5137
  }
4312
5138
  function registerConfigRoutes(app) {
@@ -4345,11 +5171,11 @@ var CONFIG_DIR, PATHS, DEFAULTS;
4345
5171
  var init_config = __esm({
4346
5172
  "src/server/routes/config.ts"() {
4347
5173
  "use strict";
4348
- CONFIG_DIR = join11(homedir9(), ".agentwatch");
5174
+ CONFIG_DIR = join13(homedir10(), ".agentwatch");
4349
5175
  PATHS = {
4350
- budgets: join11(CONFIG_DIR, "budgets.json"),
4351
- anomaly: join11(CONFIG_DIR, "anomaly.json"),
4352
- triggers: join11(CONFIG_DIR, "triggers.json")
5176
+ budgets: join13(CONFIG_DIR, "budgets.json"),
5177
+ anomaly: join13(CONFIG_DIR, "anomaly.json"),
5178
+ triggers: join13(CONFIG_DIR, "triggers.json")
4353
5179
  };
4354
5180
  DEFAULTS = {
4355
5181
  budgets: { perSessionUsd: null, perDayUsd: null },
@@ -4540,7 +5366,7 @@ function registerReplayRoutes(app, events) {
4540
5366
  const binary = req.body?.binaryPath?.trim() || cmd;
4541
5367
  const timeoutMs = Math.min(3e5, Math.max(5e3, (req.body?.timeoutSec ?? 60) * 1e3));
4542
5368
  const started = Date.now();
4543
- return new Promise((resolve) => {
5369
+ return new Promise((resolve3) => {
4544
5370
  let stdout = "";
4545
5371
  let stderr = "";
4546
5372
  let settled = false;
@@ -4557,14 +5383,14 @@ function registerReplayRoutes(app, events) {
4557
5383
  child.kill("SIGTERM");
4558
5384
  } catch {
4559
5385
  }
4560
- resolve({
5386
+ resolve3({
4561
5387
  ok: false,
4562
5388
  agent,
4563
5389
  prompt,
4564
5390
  command: `${binary} ${args.map((a) => JSON.stringify(a)).join(" ")}`,
4565
5391
  durationMs: Date.now() - started,
4566
- stdout: truncate5(stdout, 4e4),
4567
- stderr: truncate5(stderr, 4e4),
5392
+ stdout: truncate7(stdout, 4e4),
5393
+ stderr: truncate7(stderr, 4e4),
4568
5394
  error: `timed out after ${timeoutMs} ms`
4569
5395
  });
4570
5396
  }, timeoutMs);
@@ -4572,14 +5398,14 @@ function registerReplayRoutes(app, events) {
4572
5398
  if (settled) return;
4573
5399
  settled = true;
4574
5400
  clearTimeout(timer);
4575
- resolve({
5401
+ resolve3({
4576
5402
  ok: false,
4577
5403
  agent,
4578
5404
  prompt,
4579
5405
  command: `${binary} ${args.map((a) => JSON.stringify(a)).join(" ")}`,
4580
5406
  durationMs: Date.now() - started,
4581
- stdout: truncate5(stdout, 4e4),
4582
- stderr: truncate5(stderr, 4e4),
5407
+ stdout: truncate7(stdout, 4e4),
5408
+ stderr: truncate7(stderr, 4e4),
4583
5409
  error: String(err)
4584
5410
  });
4585
5411
  });
@@ -4587,22 +5413,22 @@ function registerReplayRoutes(app, events) {
4587
5413
  if (settled) return;
4588
5414
  settled = true;
4589
5415
  clearTimeout(timer);
4590
- resolve({
5416
+ resolve3({
4591
5417
  ok: code === 0,
4592
5418
  exitCode: code,
4593
5419
  agent,
4594
5420
  prompt,
4595
5421
  command: `${binary} ${args.map((a) => JSON.stringify(a)).join(" ")}`,
4596
5422
  durationMs: Date.now() - started,
4597
- stdout: truncate5(stdout, 4e4),
4598
- stderr: truncate5(stderr, 4e4)
5423
+ stdout: truncate7(stdout, 4e4),
5424
+ stderr: truncate7(stderr, 4e4)
4599
5425
  });
4600
5426
  });
4601
5427
  });
4602
5428
  }
4603
5429
  );
4604
5430
  }
4605
- function truncate5(s, max) {
5431
+ function truncate7(s, max) {
4606
5432
  if (s.length <= max) return s;
4607
5433
  return s.slice(0, max) + `
4608
5434
  \u2026 (${s.length - max} more chars truncated)`;
@@ -4622,8 +5448,8 @@ __export(server_exports, {
4622
5448
  import Fastify from "fastify";
4623
5449
  import fastifyStatic from "@fastify/static";
4624
5450
  import { fileURLToPath as fileURLToPath2 } from "url";
4625
- import { dirname as dirname3, join as join12 } from "path";
4626
- import { existsSync as existsSync12 } from "fs";
5451
+ import { dirname as dirname3, join as join14 } from "path";
5452
+ import { existsSync as existsSync14 } from "fs";
4627
5453
  function addEventToServer(handle, e) {
4628
5454
  let bucket = handle.byAgent.get(e.agent);
4629
5455
  if (!bucket) {
@@ -4639,14 +5465,14 @@ function addEventToServer(handle, e) {
4639
5465
  function resolveWebDist() {
4640
5466
  const here = dirname3(fileURLToPath2(import.meta.url));
4641
5467
  const candidates = [
4642
- join12(here, "web"),
5468
+ join14(here, "web"),
4643
5469
  // built: dist/index.js → dist/web
4644
- join12(here, "..", "dist", "web"),
5470
+ join14(here, "..", "dist", "web"),
4645
5471
  // dev: src/server → dist/web
4646
- join12(here, "..", "..", "dist", "web")
5472
+ join14(here, "..", "..", "dist", "web")
4647
5473
  // nested fallback
4648
5474
  ];
4649
- for (const c of candidates) if (existsSync12(c)) return c;
5475
+ for (const c of candidates) if (existsSync14(c)) return c;
4650
5476
  return null;
4651
5477
  }
4652
5478
  async function startServer(opts) {
@@ -4694,12 +5520,19 @@ async function startServer(opts) {
4694
5520
  return reply;
4695
5521
  });
4696
5522
  registerEventRoutes(app, events);
4697
- registerProjectRoutes(app, events);
4698
- registerSessionRoutes(app, events);
5523
+ registerProjectRoutes(app, events, opts.store);
5524
+ registerSessionRoutes(app, events, opts.store);
4699
5525
  registerAgentRoutes(app, events, byAgent);
4700
5526
  registerPermissionRoutes(app);
4701
5527
  registerCronRoutes(app, events);
4702
- registerSearchRoutes(app, events);
5528
+ registerSearchRoutes(app, events, opts.store);
5529
+ registerYieldRoutes(app, opts.store);
5530
+ registerActivityRoutes(app, opts.store);
5531
+ let hookSink = null;
5532
+ registerClaudeHooksRoute(app, {
5533
+ emit: (event) => hookSink?.emit(event),
5534
+ enrich: (id, patch) => hookSink?.enrich(id, patch)
5535
+ });
4703
5536
  registerConfigRoutes(app);
4704
5537
  registerTrendsRoutes(app, events);
4705
5538
  registerDiffRoutes(app, events);
@@ -4727,33 +5560,1112 @@ async function startServer(opts) {
4727
5560
  byAgent,
4728
5561
  events,
4729
5562
  rebuildFlat,
5563
+ store: opts.store,
5564
+ setHookSink: (sink) => {
5565
+ hookSink = sink;
5566
+ },
4730
5567
  stop: async () => {
4731
5568
  broadcaster.closeAll();
4732
5569
  await app.close();
4733
5570
  }
4734
5571
  };
4735
- return handle;
5572
+ return handle;
5573
+ }
5574
+ var PER_AGENT_CAP, DEFAULT_HOST, DEFAULT_PORT;
5575
+ var init_server = __esm({
5576
+ "src/server/index.ts"() {
5577
+ "use strict";
5578
+ init_sse();
5579
+ init_events();
5580
+ init_projects();
5581
+ init_sessions();
5582
+ init_agents();
5583
+ init_permissions();
5584
+ init_cron();
5585
+ init_search();
5586
+ init_claude_hooks();
5587
+ init_yield();
5588
+ init_activity();
5589
+ init_config();
5590
+ init_trends();
5591
+ init_diffs();
5592
+ init_replay();
5593
+ init_version();
5594
+ PER_AGENT_CAP = 1e4;
5595
+ DEFAULT_HOST = "127.0.0.1";
5596
+ DEFAULT_PORT = 3456;
5597
+ }
5598
+ });
5599
+
5600
+ // src/store/sqlite.ts
5601
+ var sqlite_exports = {};
5602
+ __export(sqlite_exports, {
5603
+ DEFAULT_DB_PATH: () => DEFAULT_DB_PATH2,
5604
+ openStore: () => openStore
5605
+ });
5606
+ import Database3 from "better-sqlite3";
5607
+ import { mkdirSync as mkdirSync2 } from "fs";
5608
+ import { homedir as homedir11 } from "os";
5609
+ import { dirname as dirname4, join as join15 } from "path";
5610
+ function openStore(opts = {}) {
5611
+ const dbPath = opts.dbPath ?? DEFAULT_DB_PATH2;
5612
+ if (dbPath !== ":memory:") {
5613
+ mkdirSync2(dirname4(dbPath), { recursive: true });
5614
+ }
5615
+ const db2 = new Database3(dbPath);
5616
+ db2.pragma("journal_mode = WAL");
5617
+ db2.pragma("synchronous = NORMAL");
5618
+ db2.pragma("foreign_keys = ON");
5619
+ applyMigrations(db2);
5620
+ return buildStore(db2);
5621
+ }
5622
+ function applyMigrations(db2) {
5623
+ db2.exec(
5624
+ `CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY)`
5625
+ );
5626
+ const row = db2.prepare("SELECT version FROM schema_version LIMIT 1").get();
5627
+ const current = row?.version ?? 0;
5628
+ if (current < 1) applyV1(db2);
5629
+ if (current < 2) applyV2(db2);
5630
+ if (current < 3) applyV3(db2);
5631
+ db2.prepare(
5632
+ "INSERT OR REPLACE INTO schema_version (version) VALUES (?)"
5633
+ ).run(SCHEMA_VERSION);
5634
+ }
5635
+ function applyV3(db2) {
5636
+ for (const col of ["workspace_root", "git_branch"]) {
5637
+ try {
5638
+ db2.exec(`ALTER TABLE sessions ADD COLUMN ${col} TEXT`);
5639
+ } catch (err) {
5640
+ if (!String(err).includes("duplicate column name")) throw err;
5641
+ }
5642
+ }
5643
+ db2.exec(`
5644
+ CREATE TABLE IF NOT EXISTS session_link_candidates (
5645
+ a_session TEXT NOT NULL,
5646
+ b_session TEXT NOT NULL,
5647
+ a_agent TEXT NOT NULL,
5648
+ b_agent TEXT NOT NULL,
5649
+ first_link_ts TEXT NOT NULL,
5650
+ last_link_ts TEXT NOT NULL,
5651
+ link_count INTEGER NOT NULL DEFAULT 1,
5652
+ sample_path TEXT NOT NULL,
5653
+ workspace_root TEXT,
5654
+ git_branch TEXT,
5655
+ PRIMARY KEY (a_session, b_session)
5656
+ );
5657
+ CREATE INDEX IF NOT EXISTS idx_link_candidates_a ON session_link_candidates(a_session);
5658
+ CREATE INDEX IF NOT EXISTS idx_link_candidates_b ON session_link_candidates(b_session);
5659
+ CREATE INDEX IF NOT EXISTS idx_link_candidates_last_ts ON session_link_candidates(last_link_ts);
5660
+ `);
5661
+ }
5662
+ function applyV2(db2) {
5663
+ try {
5664
+ db2.exec(`ALTER TABLE events ADD COLUMN category TEXT`);
5665
+ } catch (err) {
5666
+ if (!String(err).includes("duplicate column name")) throw err;
5667
+ }
5668
+ try {
5669
+ db2.exec(`CREATE INDEX IF NOT EXISTS idx_events_category ON events(category)`);
5670
+ } catch {
5671
+ }
5672
+ }
5673
+ function applyV1(db2) {
5674
+ db2.exec(`
5675
+ CREATE TABLE IF NOT EXISTS events (
5676
+ id TEXT PRIMARY KEY,
5677
+ ts TEXT NOT NULL,
5678
+ agent TEXT NOT NULL,
5679
+ type TEXT NOT NULL,
5680
+ path TEXT,
5681
+ cmd TEXT,
5682
+ tool TEXT,
5683
+ summary TEXT,
5684
+ session_id TEXT,
5685
+ prompt_id TEXT,
5686
+ risk_score INTEGER NOT NULL,
5687
+ project TEXT,
5688
+ details_json TEXT,
5689
+ full_text TEXT,
5690
+ thinking TEXT,
5691
+ tool_input_json TEXT,
5692
+ tool_result TEXT,
5693
+ cost_usd REAL,
5694
+ model TEXT,
5695
+ duration_ms INTEGER,
5696
+ tool_error INTEGER,
5697
+ sub_agent_id TEXT,
5698
+ parent_spawn_id TEXT,
5699
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
5700
+ );
5701
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
5702
+ CREATE INDEX IF NOT EXISTS idx_events_agent ON events(agent);
5703
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
5704
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
5705
+ CREATE INDEX IF NOT EXISTS idx_events_session_ts ON events(session_id, ts);
5706
+ CREATE INDEX IF NOT EXISTS idx_events_project ON events(project);
5707
+
5708
+ CREATE TABLE IF NOT EXISTS sessions (
5709
+ session_id TEXT PRIMARY KEY,
5710
+ agent TEXT NOT NULL,
5711
+ project TEXT,
5712
+ first_ts TEXT NOT NULL,
5713
+ last_ts TEXT NOT NULL,
5714
+ event_count INTEGER NOT NULL DEFAULT 0,
5715
+ cost_usd REAL NOT NULL DEFAULT 0,
5716
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
5717
+ );
5718
+ CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
5719
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
5720
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_ts ON sessions(last_ts);
5721
+
5722
+ CREATE TABLE IF NOT EXISTS tool_calls (
5723
+ event_id TEXT PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE,
5724
+ tool TEXT NOT NULL,
5725
+ duration_ms INTEGER,
5726
+ error INTEGER NOT NULL DEFAULT 0
5727
+ );
5728
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool);
5729
+
5730
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
5731
+ full_text, thinking, tool_result, summary,
5732
+ content='events',
5733
+ content_rowid='rowid',
5734
+ tokenize='porter unicode61'
5735
+ );
5736
+
5737
+ CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
5738
+ INSERT INTO events_fts(rowid, full_text, thinking, tool_result, summary)
5739
+ VALUES (new.rowid, new.full_text, new.thinking, new.tool_result, new.summary);
5740
+ END;
5741
+
5742
+ CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
5743
+ INSERT INTO events_fts(events_fts, rowid, full_text, thinking, tool_result, summary)
5744
+ VALUES ('delete', old.rowid, old.full_text, old.thinking, old.tool_result, old.summary);
5745
+ END;
5746
+
5747
+ CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN
5748
+ INSERT INTO events_fts(events_fts, rowid, full_text, thinking, tool_result, summary)
5749
+ VALUES ('delete', old.rowid, old.full_text, old.thinking, old.tool_result, old.summary);
5750
+ INSERT INTO events_fts(rowid, full_text, thinking, tool_result, summary)
5751
+ VALUES (new.rowid, new.full_text, new.thinking, new.tool_result, new.summary);
5752
+ END;
5753
+
5754
+ CREATE TRIGGER IF NOT EXISTS sessions_upsert_on_event_insert
5755
+ AFTER INSERT ON events
5756
+ WHEN new.session_id IS NOT NULL BEGIN
5757
+ INSERT INTO sessions (session_id, agent, project, first_ts, last_ts, event_count, cost_usd)
5758
+ VALUES (new.session_id, new.agent, new.project, new.ts, new.ts, 1, COALESCE(new.cost_usd, 0))
5759
+ ON CONFLICT(session_id) DO UPDATE SET
5760
+ last_ts = CASE WHEN new.ts > last_ts THEN new.ts ELSE last_ts END,
5761
+ first_ts = CASE WHEN new.ts < first_ts THEN new.ts ELSE first_ts END,
5762
+ event_count = event_count + 1,
5763
+ cost_usd = cost_usd + COALESCE(new.cost_usd, 0),
5764
+ project = COALESCE(sessions.project, new.project),
5765
+ updated_at = strftime('%s','now');
5766
+ END;
5767
+ `);
5768
+ }
5769
+ function buildStore(db2) {
5770
+ const insertStmt = db2.prepare(`
5771
+ INSERT OR IGNORE INTO events (
5772
+ id, ts, agent, type, path, cmd, tool, summary,
5773
+ session_id, prompt_id, risk_score, project, details_json,
5774
+ full_text, thinking, tool_input_json, tool_result,
5775
+ cost_usd, model, duration_ms, tool_error,
5776
+ sub_agent_id, parent_spawn_id, category
5777
+ )
5778
+ VALUES (
5779
+ @id, @ts, @agent, @type, @path, @cmd, @tool, @summary,
5780
+ @session_id, @prompt_id, @risk_score, @project, @details_json,
5781
+ @full_text, @thinking, @tool_input_json, @tool_result,
5782
+ @cost_usd, @model, @duration_ms, @tool_error,
5783
+ @sub_agent_id, @parent_spawn_id, @category
5784
+ )
5785
+ `);
5786
+ const insertToolCallStmt = db2.prepare(`
5787
+ INSERT OR REPLACE INTO tool_calls (event_id, tool, duration_ms, error)
5788
+ VALUES (?, ?, ?, ?)
5789
+ `);
5790
+ const getStmt = db2.prepare(
5791
+ `SELECT * FROM events WHERE id = ?`
5792
+ );
5793
+ const hasStmt = db2.prepare(`SELECT 1 FROM events WHERE id = ?`);
5794
+ const sessionEventsStmt = db2.prepare(
5795
+ `SELECT * FROM events WHERE session_id = ? ORDER BY ts ASC`
5796
+ );
5797
+ const insertMany = db2.transaction((events) => {
5798
+ for (const e of events) doInsert(e);
5799
+ });
5800
+ function doInsert(event) {
5801
+ const d = event.details ?? {};
5802
+ const project = extractProject4(event);
5803
+ const params = {
5804
+ id: event.id,
5805
+ ts: event.ts,
5806
+ agent: event.agent,
5807
+ type: event.type,
5808
+ path: event.path ?? null,
5809
+ cmd: event.cmd ?? null,
5810
+ tool: event.tool ?? null,
5811
+ summary: event.summary ?? null,
5812
+ session_id: event.sessionId ?? null,
5813
+ prompt_id: event.promptId ?? null,
5814
+ risk_score: event.riskScore,
5815
+ project,
5816
+ details_json: d ? JSON.stringify(d) : null,
5817
+ full_text: d.fullText ?? null,
5818
+ thinking: d.thinking ?? null,
5819
+ tool_input_json: d.toolInput ? JSON.stringify(d.toolInput) : null,
5820
+ tool_result: d.toolResult ?? null,
5821
+ cost_usd: d.cost ?? null,
5822
+ model: d.model ?? null,
5823
+ duration_ms: d.durationMs ?? null,
5824
+ tool_error: d.toolError == null ? null : d.toolError ? 1 : 0,
5825
+ sub_agent_id: d.subAgentId ?? null,
5826
+ parent_spawn_id: d.parentSpawnId ?? null,
5827
+ category: d.category ?? null
5828
+ };
5829
+ const info = insertStmt.run(params);
5830
+ if (info.changes > 0 && event.tool) {
5831
+ insertToolCallStmt.run(
5832
+ event.id,
5833
+ event.tool,
5834
+ d.durationMs ?? null,
5835
+ d.toolError ? 1 : 0
5836
+ );
5837
+ }
5838
+ }
5839
+ const enrichSelectStmt = db2.prepare(
5840
+ `SELECT details_json, cost_usd FROM events WHERE id = ?`
5841
+ );
5842
+ const enrichUpdateStmt = db2.prepare(`
5843
+ UPDATE events SET
5844
+ details_json = @details_json,
5845
+ full_text = COALESCE(@full_text, full_text),
5846
+ thinking = COALESCE(@thinking, thinking),
5847
+ tool_input_json = COALESCE(@tool_input_json, tool_input_json),
5848
+ tool_result = COALESCE(@tool_result, tool_result),
5849
+ cost_usd = COALESCE(@cost_usd, cost_usd),
5850
+ model = COALESCE(@model, model),
5851
+ duration_ms = COALESCE(@duration_ms, duration_ms),
5852
+ tool_error = COALESCE(@tool_error, tool_error)
5853
+ WHERE id = @id
5854
+ `);
5855
+ const sessionCostBumpStmt = db2.prepare(`
5856
+ UPDATE sessions SET cost_usd = cost_usd + ?, updated_at = strftime('%s','now')
5857
+ WHERE session_id = (SELECT session_id FROM events WHERE id = ?)
5858
+ `);
5859
+ function doEnrich(eventId, patch) {
5860
+ const row = enrichSelectStmt.get(eventId);
5861
+ if (!row) return;
5862
+ const prev = row.details_json ? JSON.parse(row.details_json) : {};
5863
+ const merged = { ...prev, ...patch };
5864
+ enrichUpdateStmt.run({
5865
+ id: eventId,
5866
+ details_json: JSON.stringify(merged),
5867
+ full_text: patch.fullText ?? null,
5868
+ thinking: patch.thinking ?? null,
5869
+ tool_input_json: patch.toolInput ? JSON.stringify(patch.toolInput) : null,
5870
+ tool_result: patch.toolResult ?? null,
5871
+ cost_usd: patch.cost ?? null,
5872
+ model: patch.model ?? null,
5873
+ duration_ms: patch.durationMs ?? null,
5874
+ tool_error: patch.toolError == null ? null : patch.toolError ? 1 : 0
5875
+ });
5876
+ if (patch.cost && patch.cost !== row.cost_usd) {
5877
+ const delta = patch.cost - (row.cost_usd ?? 0);
5878
+ sessionCostBumpStmt.run(delta, eventId);
5879
+ }
5880
+ if (patch.durationMs != null || patch.toolError != null) {
5881
+ const eventRow = db2.prepare("SELECT tool FROM events WHERE id = ?").get(eventId);
5882
+ if (eventRow?.tool) {
5883
+ insertToolCallStmt.run(
5884
+ eventId,
5885
+ eventRow.tool,
5886
+ merged.durationMs ?? null,
5887
+ merged.toolError ? 1 : 0
5888
+ );
5889
+ }
5890
+ }
5891
+ }
5892
+ const upsertSessionWorkspaceStmt = db2.prepare(`
5893
+ UPDATE sessions
5894
+ SET workspace_root = COALESCE(workspace_root, @workspace_root),
5895
+ git_branch = COALESCE(git_branch, @git_branch),
5896
+ updated_at = strftime('%s','now')
5897
+ WHERE session_id = @session_id
5898
+ `);
5899
+ const getSessionWorkspaceStmt = db2.prepare(`
5900
+ SELECT workspace_root, git_branch
5901
+ FROM sessions
5902
+ WHERE session_id = ?
5903
+ `);
5904
+ const insertLinkCandidateStmt = db2.prepare(`
5905
+ INSERT OR IGNORE INTO session_link_candidates (
5906
+ a_session, b_session, a_agent, b_agent,
5907
+ first_link_ts, last_link_ts, link_count,
5908
+ sample_path, workspace_root, git_branch
5909
+ ) VALUES (
5910
+ @a_session, @b_session, @a_agent, @b_agent,
5911
+ @ts, @ts, 1,
5912
+ @sample_path, @workspace_root, @git_branch
5913
+ )
5914
+ `);
5915
+ const bumpLinkCandidateStmt = db2.prepare(`
5916
+ UPDATE session_link_candidates
5917
+ SET link_count = link_count + 1,
5918
+ last_link_ts = CASE WHEN @ts > last_link_ts THEN @ts ELSE last_link_ts END
5919
+ WHERE a_session = @a_session AND b_session = @b_session
5920
+ `);
5921
+ const listLinkCandidatesAllStmt = db2.prepare(`
5922
+ SELECT a_session, b_session, a_agent, b_agent,
5923
+ first_link_ts, last_link_ts, link_count,
5924
+ sample_path, workspace_root, git_branch
5925
+ FROM session_link_candidates
5926
+ ORDER BY last_link_ts DESC
5927
+ LIMIT ?
5928
+ `);
5929
+ const listLinkCandidatesForSessionStmt = db2.prepare(`
5930
+ SELECT a_session, b_session, a_agent, b_agent,
5931
+ first_link_ts, last_link_ts, link_count,
5932
+ sample_path, workspace_root, git_branch
5933
+ FROM session_link_candidates
5934
+ WHERE a_session = ? OR b_session = ?
5935
+ ORDER BY last_link_ts DESC
5936
+ LIMIT ?
5937
+ `);
5938
+ const countLinkCandidatesForSessionStmt = db2.prepare(`
5939
+ SELECT COUNT(*) AS c
5940
+ FROM session_link_candidates
5941
+ WHERE a_session = ? OR b_session = ?
5942
+ `);
5943
+ const countAllLinkCandidatesStmt = db2.prepare(`
5944
+ SELECT COUNT(*) AS c FROM session_link_candidates
5945
+ `);
5946
+ return {
5947
+ insert: doInsert,
5948
+ insertMany: (events) => insertMany(events),
5949
+ enrich: doEnrich,
5950
+ upsertSessionWorkspace(sessionId, workspace) {
5951
+ if (workspace.workspaceRoot == null && workspace.gitBranch == null) return;
5952
+ upsertSessionWorkspaceStmt.run({
5953
+ session_id: sessionId,
5954
+ workspace_root: workspace.workspaceRoot,
5955
+ git_branch: workspace.gitBranch
5956
+ });
5957
+ },
5958
+ getSessionWorkspace(sessionId) {
5959
+ const row = getSessionWorkspaceStmt.get(sessionId);
5960
+ return {
5961
+ workspaceRoot: row?.workspace_root ?? null,
5962
+ gitBranch: row?.git_branch ?? null
5963
+ };
5964
+ },
5965
+ recordSessionLinkCandidate(input) {
5966
+ const [aSession, bSession, aAgent, bAgent] = input.aSession < input.bSession ? [input.aSession, input.bSession, input.aAgent, input.bAgent] : [input.bSession, input.aSession, input.bAgent, input.aAgent];
5967
+ const params = {
5968
+ a_session: aSession,
5969
+ b_session: bSession,
5970
+ a_agent: aAgent,
5971
+ b_agent: bAgent,
5972
+ ts: input.ts,
5973
+ sample_path: input.samplePath,
5974
+ workspace_root: input.workspaceRoot,
5975
+ git_branch: input.gitBranch
5976
+ };
5977
+ const inserted = insertLinkCandidateStmt.run(params);
5978
+ if (inserted.changes === 0) {
5979
+ bumpLinkCandidateStmt.run({
5980
+ a_session: aSession,
5981
+ b_session: bSession,
5982
+ ts: input.ts
5983
+ });
5984
+ }
5985
+ },
5986
+ listSessionLinkCandidates(opts = {}) {
5987
+ const limit = clamp4(opts.limit ?? 200, 1, 5e3);
5988
+ const rows = opts.sessionId ? listLinkCandidatesForSessionStmt.all(
5989
+ opts.sessionId,
5990
+ opts.sessionId,
5991
+ limit
5992
+ ) : listLinkCandidatesAllStmt.all(limit);
5993
+ return rows.map((r) => ({
5994
+ aSession: r.a_session,
5995
+ bSession: r.b_session,
5996
+ aAgent: r.a_agent,
5997
+ bAgent: r.b_agent,
5998
+ firstLinkTs: r.first_link_ts,
5999
+ lastLinkTs: r.last_link_ts,
6000
+ linkCount: r.link_count,
6001
+ samplePath: r.sample_path,
6002
+ workspaceRoot: r.workspace_root,
6003
+ gitBranch: r.git_branch
6004
+ }));
6005
+ },
6006
+ countSessionLinkCandidates(sessionId) {
6007
+ const row = countLinkCandidatesForSessionStmt.get(
6008
+ sessionId,
6009
+ sessionId
6010
+ );
6011
+ return row.c;
6012
+ },
6013
+ countAllLinkCandidates() {
6014
+ return countAllLinkCandidatesStmt.get().c;
6015
+ },
6016
+ hasEvent(eventId) {
6017
+ return Boolean(hasStmt.get(eventId));
6018
+ },
6019
+ getEvent(eventId) {
6020
+ const row = getStmt.get(eventId);
6021
+ return row ? rowToEvent(row) : null;
6022
+ },
6023
+ listSessionEvents(sessionId) {
6024
+ const rows = sessionEventsStmt.all(sessionId);
6025
+ return rows.map(rowToEvent);
6026
+ },
6027
+ listRecentEvents(opts = {}) {
6028
+ const limit = clamp4(opts.limit ?? 1e3, 1, 5e4);
6029
+ const order = opts.order === "asc" ? "ASC" : "DESC";
6030
+ const where = [];
6031
+ const params = [];
6032
+ if (opts.sinceTs) {
6033
+ where.push("ts >= ?");
6034
+ params.push(opts.sinceTs);
6035
+ }
6036
+ const sql = `
6037
+ SELECT * FROM events
6038
+ ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
6039
+ ORDER BY ts ${order}
6040
+ LIMIT ?
6041
+ `;
6042
+ const rows = db2.prepare(sql).all(...params, limit);
6043
+ return rows.map(rowToEvent);
6044
+ },
6045
+ listSessions(opts = {}) {
6046
+ const limit = clamp4(opts.limit ?? 200, 1, 5e3);
6047
+ const where = [];
6048
+ const params = [];
6049
+ if (opts.agent) {
6050
+ where.push("agent = ?");
6051
+ params.push(opts.agent);
6052
+ }
6053
+ if (opts.project) {
6054
+ where.push("project = ?");
6055
+ params.push(opts.project);
6056
+ }
6057
+ if (opts.since) {
6058
+ where.push("last_ts >= ?");
6059
+ params.push(opts.since);
6060
+ }
6061
+ const sql = `
6062
+ SELECT session_id, agent, project, first_ts, last_ts, event_count, cost_usd
6063
+ FROM sessions
6064
+ ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
6065
+ ORDER BY last_ts DESC
6066
+ LIMIT ?
6067
+ `;
6068
+ const rows = db2.prepare(sql).all(...params, limit);
6069
+ return rows.map((r) => ({
6070
+ sessionId: r.session_id,
6071
+ agent: r.agent,
6072
+ project: r.project,
6073
+ firstTs: r.first_ts,
6074
+ lastTs: r.last_ts,
6075
+ eventCount: r.event_count,
6076
+ costUsd: r.cost_usd
6077
+ }));
6078
+ },
6079
+ listProjects() {
6080
+ const rows = db2.prepare(
6081
+ `SELECT project, agent, COUNT(*) AS event_count, MAX(ts) AS last_ts,
6082
+ COALESCE(SUM(cost_usd), 0) AS cost_total, session_id
6083
+ FROM events
6084
+ WHERE project IS NOT NULL
6085
+ GROUP BY project, agent, session_id`
6086
+ ).all();
6087
+ const byProject = /* @__PURE__ */ new Map();
6088
+ for (const r of rows) {
6089
+ let p = byProject.get(r.project);
6090
+ if (!p) {
6091
+ p = {
6092
+ name: r.project,
6093
+ eventCount: 0,
6094
+ byAgent: {},
6095
+ sessionIds: [],
6096
+ cost: 0,
6097
+ lastTs: r.last_ts
6098
+ };
6099
+ byProject.set(r.project, p);
6100
+ }
6101
+ p.eventCount += r.event_count;
6102
+ p.byAgent[r.agent] = (p.byAgent[r.agent] ?? 0) + r.event_count;
6103
+ if (r.session_id && !p.sessionIds.includes(r.session_id)) {
6104
+ p.sessionIds.push(r.session_id);
6105
+ }
6106
+ p.cost += r.cost_total ?? 0;
6107
+ if (r.last_ts > p.lastTs) p.lastTs = r.last_ts;
6108
+ }
6109
+ return Array.from(byProject.values()).sort(
6110
+ (a, b) => a.lastTs < b.lastTs ? 1 : -1
6111
+ );
6112
+ },
6113
+ searchFts(query, opts = {}) {
6114
+ const limit = clamp4(opts.limit ?? 100, 1, 500);
6115
+ const safe = sanitizeFtsQuery(query);
6116
+ if (!safe) return [];
6117
+ const rows = db2.prepare(
6118
+ `SELECT e.id AS id, e.session_id AS session_id, e.agent AS agent,
6119
+ e.ts AS ts, e.type AS type, fts.rank AS rank,
6120
+ snippet(events_fts, -1, '<<', '>>', '\u2026', 16) AS snip
6121
+ FROM events_fts AS fts
6122
+ JOIN events AS e ON e.rowid = fts.rowid
6123
+ WHERE events_fts MATCH ?
6124
+ ORDER BY rank
6125
+ LIMIT ?`
6126
+ ).all(safe, limit);
6127
+ return rows.map((r) => ({
6128
+ eventId: r.id,
6129
+ sessionId: r.session_id,
6130
+ agent: r.agent,
6131
+ ts: r.ts,
6132
+ type: r.type,
6133
+ snippet: r.snip,
6134
+ rank: r.rank
6135
+ }));
6136
+ },
6137
+ activityBySession(sessionId) {
6138
+ const rows = db2.prepare(
6139
+ `SELECT COALESCE(category, 'chat') AS category,
6140
+ COUNT(*) AS event_count,
6141
+ COALESCE(SUM(cost_usd), 0) AS cost_total
6142
+ FROM events
6143
+ WHERE session_id = ?
6144
+ GROUP BY COALESCE(category, 'chat')`
6145
+ ).all(sessionId);
6146
+ return rows.map((r) => ({
6147
+ category: r.category,
6148
+ eventCount: r.event_count,
6149
+ costUsd: r.cost_total
6150
+ })).sort((a, b) => b.eventCount - a.eventCount);
6151
+ },
6152
+ activityByProject(projectName) {
6153
+ const rows = db2.prepare(
6154
+ `SELECT COALESCE(category, 'chat') AS category,
6155
+ COUNT(*) AS event_count,
6156
+ COALESCE(SUM(cost_usd), 0) AS cost_total,
6157
+ COUNT(DISTINCT session_id) AS sessions_touched
6158
+ FROM events
6159
+ WHERE project = ?
6160
+ GROUP BY COALESCE(category, 'chat')`
6161
+ ).all(projectName);
6162
+ return rows.map((r) => ({
6163
+ category: r.category,
6164
+ eventCount: r.event_count,
6165
+ costUsd: r.cost_total,
6166
+ sessionsTouched: r.sessions_touched
6167
+ })).sort((a, b) => b.eventCount - a.eventCount);
6168
+ },
6169
+ prune({ olderThanDays }) {
6170
+ const cutoffMs = Date.now() - olderThanDays * 864e5;
6171
+ const cutoff = new Date(cutoffMs).toISOString();
6172
+ const events = db2.prepare(`DELETE FROM events WHERE ts < ?`).run(cutoff);
6173
+ const sessions = db2.prepare(`DELETE FROM sessions WHERE last_ts < ?`).run(cutoff);
6174
+ if (events.changes > 1e3) {
6175
+ try {
6176
+ db2.exec("VACUUM");
6177
+ } catch {
6178
+ }
6179
+ }
6180
+ return {
6181
+ deletedEvents: Number(events.changes),
6182
+ deletedSessions: Number(sessions.changes)
6183
+ };
6184
+ },
6185
+ stats() {
6186
+ const eventCount = db2.prepare("SELECT COUNT(*) AS c FROM events").get().c;
6187
+ const sessionCount = db2.prepare("SELECT COUNT(*) AS c FROM sessions").get().c;
6188
+ const pages = db2.prepare("PRAGMA page_count").get().page_count;
6189
+ const pageSize = db2.prepare("PRAGMA page_size").get().page_size;
6190
+ const versionRow = db2.prepare("SELECT version FROM schema_version LIMIT 1").get();
6191
+ return {
6192
+ events: eventCount,
6193
+ sessions: sessionCount,
6194
+ dbBytes: pages * pageSize,
6195
+ schemaVersion: versionRow?.version ?? 0
6196
+ };
6197
+ },
6198
+ close() {
6199
+ db2.close();
6200
+ }
6201
+ };
6202
+ }
6203
+ function rowToEvent(row) {
6204
+ const details = row.details_json ? JSON.parse(row.details_json) : void 0;
6205
+ return {
6206
+ id: row.id,
6207
+ ts: row.ts,
6208
+ agent: row.agent,
6209
+ type: row.type,
6210
+ path: row.path ?? void 0,
6211
+ cmd: row.cmd ?? void 0,
6212
+ tool: row.tool ?? void 0,
6213
+ summary: row.summary ?? void 0,
6214
+ sessionId: row.session_id ?? void 0,
6215
+ promptId: row.prompt_id ?? void 0,
6216
+ riskScore: row.risk_score,
6217
+ details
6218
+ };
6219
+ }
6220
+ function clamp4(n, min, max) {
6221
+ return Math.max(min, Math.min(max, n));
6222
+ }
6223
+ function extractProject4(e) {
6224
+ const m = (e.summary ?? "").match(/^\[([^\]/ ]+)/);
6225
+ return m ? m[1] ?? null : null;
6226
+ }
6227
+ function sanitizeFtsQuery(q) {
6228
+ const cleaned = q.replace(/[^\p{L}\p{N}\s_-]/gu, " ").replace(/\s+/g, " ").trim();
6229
+ if (!cleaned) return "";
6230
+ const tokens = cleaned.split(" ").filter((t) => t.length > 0 && !FTS_KEYWORDS.has(t.toUpperCase())).map((t) => `"${t}"`);
6231
+ if (tokens.length === 0) return "";
6232
+ return tokens.join(" OR ");
6233
+ }
6234
+ var SCHEMA_VERSION, DEFAULT_DB_PATH2, FTS_KEYWORDS;
6235
+ var init_sqlite = __esm({
6236
+ "src/store/sqlite.ts"() {
6237
+ "use strict";
6238
+ SCHEMA_VERSION = 3;
6239
+ DEFAULT_DB_PATH2 = join15(homedir11(), ".agentwatch", "events.db");
6240
+ FTS_KEYWORDS = /* @__PURE__ */ new Set(["AND", "OR", "NOT", "NEAR"]);
6241
+ }
6242
+ });
6243
+
6244
+ // src/correlate/branch-cache.ts
6245
+ function resolveWorkspace(cwd, deps = {}) {
6246
+ if (!cwd) return { workspaceRoot: null, gitBranch: null };
6247
+ const branchOf = deps.branchOf ?? getCurrentBranch;
6248
+ const commonDirOf = deps.commonDirOf ?? gitCommonDir;
6249
+ const now = deps.now ?? Date.now;
6250
+ const t = now();
6251
+ const cached4 = cache.get(cwd);
6252
+ if (cached4 && t - cached4.refreshedMs < TTL_MS2) {
6253
+ return { workspaceRoot: cached4.workspaceRoot, gitBranch: cached4.branch };
6254
+ }
6255
+ const workspaceRoot = commonDirOf(cwd) ?? cwd;
6256
+ const branch = branchOf(cwd);
6257
+ cache.set(cwd, { workspaceRoot, branch, refreshedMs: t });
6258
+ return { workspaceRoot, gitBranch: branch };
6259
+ }
6260
+ var TTL_MS2, cache;
6261
+ var init_branch_cache = __esm({
6262
+ "src/correlate/branch-cache.ts"() {
6263
+ "use strict";
6264
+ init_correlate();
6265
+ TTL_MS2 = 6e4;
6266
+ cache = /* @__PURE__ */ new Map();
6267
+ }
6268
+ });
6269
+
6270
+ // src/correlate/session-links.ts
6271
+ var WINDOW_MS2, MAX_ENTRIES, EVICT_FRACTION, RecentWritesIndex;
6272
+ var init_session_links = __esm({
6273
+ "src/correlate/session-links.ts"() {
6274
+ "use strict";
6275
+ WINDOW_MS2 = 30 * 60 * 1e3;
6276
+ MAX_ENTRIES = 5e4;
6277
+ EVICT_FRACTION = 0.1;
6278
+ RecentWritesIndex = class {
6279
+ byPath = /* @__PURE__ */ new Map();
6280
+ size = 0;
6281
+ /** Record a write and return any peer entries that should be linked
6282
+ * per the gate above. Returned entries are *not* removed from the
6283
+ * index — the same peer may legitimately link to multiple later
6284
+ * writes in the window. */
6285
+ recordAndQuery(path12, agent, sessionId, tsMs, branch, root) {
6286
+ const cutoff = tsMs - WINDOW_MS2;
6287
+ const bucket = this.byPath.get(path12);
6288
+ const matches = [];
6289
+ if (bucket) {
6290
+ let kept = 0;
6291
+ for (const entry of bucket) {
6292
+ if (entry.ts < cutoff) {
6293
+ this.size -= 1;
6294
+ continue;
6295
+ }
6296
+ bucket[kept++] = entry;
6297
+ if (entry.agent !== agent && entry.sessionId !== sessionId && entry.branch != null && branch != null && entry.branch === branch && entry.root != null && root != null && entry.root === root) {
6298
+ matches.push(entry);
6299
+ }
6300
+ }
6301
+ bucket.length = kept;
6302
+ if (kept === 0) this.byPath.delete(path12);
6303
+ }
6304
+ const newEntry = {
6305
+ agent,
6306
+ sessionId,
6307
+ ts: tsMs,
6308
+ branch,
6309
+ root
6310
+ };
6311
+ const next = this.byPath.get(path12);
6312
+ if (next) {
6313
+ next.push(newEntry);
6314
+ } else {
6315
+ this.byPath.set(path12, [newEntry]);
6316
+ }
6317
+ this.size += 1;
6318
+ if (this.size > MAX_ENTRIES) this.evictOldest();
6319
+ return matches;
6320
+ }
6321
+ /** Test/diagnostic: total entries currently held. */
6322
+ entryCount() {
6323
+ return this.size;
6324
+ }
6325
+ /** Test-only: drop everything. */
6326
+ reset() {
6327
+ this.byPath.clear();
6328
+ this.size = 0;
6329
+ }
6330
+ /** Hard-cap eviction: collect every entry, sort by ts ascending,
6331
+ * drop the oldest EVICT_FRACTION. Cheap relative to MAX_ENTRIES,
6332
+ * rare in practice — sweep keeps us well under the cap normally. */
6333
+ evictOldest() {
6334
+ const toDrop = Math.max(1, Math.floor(MAX_ENTRIES * EVICT_FRACTION));
6335
+ const allTs = [];
6336
+ for (const bucket of this.byPath.values()) {
6337
+ for (const e of bucket) allTs.push(e.ts);
6338
+ }
6339
+ if (allTs.length <= toDrop) {
6340
+ this.byPath.clear();
6341
+ this.size = 0;
6342
+ return;
6343
+ }
6344
+ allTs.sort((a, b) => a - b);
6345
+ const cutoff = allTs[toDrop - 1] ?? -Infinity;
6346
+ for (const [path12, bucket] of this.byPath) {
6347
+ let kept = 0;
6348
+ for (const e of bucket) {
6349
+ if (e.ts <= cutoff) {
6350
+ this.size -= 1;
6351
+ continue;
6352
+ }
6353
+ bucket[kept++] = e;
6354
+ }
6355
+ bucket.length = kept;
6356
+ if (kept === 0) this.byPath.delete(path12);
6357
+ }
6358
+ }
6359
+ };
6360
+ }
6361
+ });
6362
+
6363
+ // src/store/wire.ts
6364
+ function wrapSinkWithStore(inner, store) {
6365
+ let warnedInsert = false;
6366
+ let warnedEnrich = false;
6367
+ return {
6368
+ emit: (event) => {
6369
+ try {
6370
+ store.insert(event);
6371
+ } catch (err) {
6372
+ if (!warnedInsert) {
6373
+ warnedInsert = true;
6374
+ process.stderr.write(
6375
+ `[agentwatch] store.insert error (further occurrences suppressed): ${String(err)}
6376
+ `
6377
+ );
6378
+ }
6379
+ }
6380
+ inner.emit(event);
6381
+ },
6382
+ enrich: (eventId, patch) => {
6383
+ try {
6384
+ store.enrich(eventId, patch);
6385
+ } catch (err) {
6386
+ if (!warnedEnrich) {
6387
+ warnedEnrich = true;
6388
+ process.stderr.write(
6389
+ `[agentwatch] store.enrich error (further occurrences suppressed): ${String(err)}
6390
+ `
6391
+ );
6392
+ }
6393
+ }
6394
+ inner.enrich(eventId, patch);
6395
+ }
6396
+ };
6397
+ }
6398
+ function wrapSinkWithLinks(inner, store, deps = {}) {
6399
+ const index = new RecentWritesIndex();
6400
+ const resolve3 = deps.resolve ?? resolveWorkspace;
6401
+ let warned = false;
6402
+ return {
6403
+ emit: (event) => {
6404
+ inner.emit(event);
6405
+ try {
6406
+ if (isLinkableWrite(event)) processWrite(event, store, index, resolve3);
6407
+ } catch (err) {
6408
+ if (!warned) {
6409
+ warned = true;
6410
+ process.stderr.write(
6411
+ `[agentwatch] session-link error (further occurrences suppressed): ${String(err)}
6412
+ `
6413
+ );
6414
+ }
6415
+ }
6416
+ },
6417
+ enrich: (eventId, patch) => inner.enrich(eventId, patch)
6418
+ };
6419
+ }
6420
+ function isLinkableWrite(event) {
6421
+ if (event.type !== "file_write" && event.type !== "file_change") return false;
6422
+ if (!event.path) return false;
6423
+ if (!event.sessionId) return false;
6424
+ if (!event.details?.cwd) return false;
6425
+ return true;
6426
+ }
6427
+ function processWrite(event, store, index, resolve3) {
6428
+ const cwd = event.details?.cwd ?? null;
6429
+ const resolved = resolve3(cwd);
6430
+ store.upsertSessionWorkspace(event.sessionId, {
6431
+ workspaceRoot: resolved.workspaceRoot,
6432
+ gitBranch: resolved.gitBranch
6433
+ });
6434
+ if (resolved.workspaceRoot == null || resolved.gitBranch == null) return;
6435
+ const tsMs = Date.parse(event.ts);
6436
+ if (!Number.isFinite(tsMs)) return;
6437
+ const matches = index.recordAndQuery(
6438
+ event.path,
6439
+ event.agent,
6440
+ event.sessionId,
6441
+ tsMs,
6442
+ resolved.gitBranch,
6443
+ resolved.workspaceRoot
6444
+ );
6445
+ for (const peer of matches) {
6446
+ store.recordSessionLinkCandidate({
6447
+ aSession: event.sessionId,
6448
+ bSession: peer.sessionId,
6449
+ aAgent: event.agent,
6450
+ bAgent: peer.agent,
6451
+ samplePath: event.path,
6452
+ ts: event.ts,
6453
+ workspaceRoot: resolved.workspaceRoot,
6454
+ gitBranch: resolved.gitBranch
6455
+ });
6456
+ }
6457
+ }
6458
+ var init_wire = __esm({
6459
+ "src/store/wire.ts"() {
6460
+ "use strict";
6461
+ init_branch_cache();
6462
+ init_session_links();
6463
+ }
6464
+ });
6465
+
6466
+ // src/store/index.ts
6467
+ var store_exports = {};
6468
+ __export(store_exports, {
6469
+ DEFAULT_DB_PATH: () => DEFAULT_DB_PATH2,
6470
+ openStore: () => openStore,
6471
+ wrapSinkWithLinks: () => wrapSinkWithLinks,
6472
+ wrapSinkWithStore: () => wrapSinkWithStore
6473
+ });
6474
+ var init_store = __esm({
6475
+ "src/store/index.ts"() {
6476
+ "use strict";
6477
+ init_sqlite();
6478
+ init_wire();
6479
+ }
6480
+ });
6481
+
6482
+ // src/classify/activity.ts
6483
+ function classifyEvent(event) {
6484
+ const scores = scoreEvent2(event);
6485
+ let winner = "chat";
6486
+ let max = 0;
6487
+ for (const cat of ACTIVITY_CATEGORIES) {
6488
+ const s = scores[cat] ?? 0;
6489
+ if (s > max) {
6490
+ max = s;
6491
+ winner = cat;
6492
+ }
6493
+ }
6494
+ return winner;
6495
+ }
6496
+ function scoreEvent2(event) {
6497
+ const scores = {};
6498
+ const add = (cat, n) => {
6499
+ scores[cat] = (scores[cat] ?? 0) + n;
6500
+ };
6501
+ const path12 = (event.path ?? "").toLowerCase();
6502
+ const cmd = (event.cmd ?? "").toLowerCase();
6503
+ const tool = (event.tool ?? "").toLowerCase();
6504
+ const summary = (event.summary ?? "").toLowerCase();
6505
+ const fullText = (event.details?.fullText ?? "").toLowerCase();
6506
+ const thinking = (event.details?.thinking ?? "").toLowerCase();
6507
+ const toolError = event.details?.toolError === true;
6508
+ const text = `${summary} ${fullText} ${thinking}`;
6509
+ if (event.type === "file_write" || event.type === "file_change") {
6510
+ if (isTestPath(path12)) add("testing", 8);
6511
+ else if (isDocPath(path12)) add("docs", 8);
6512
+ else if (isConfigPath(path12)) add("config", 8);
6513
+ else add("coding", 7);
6514
+ } else if (event.type === "file_read") {
6515
+ if (isTestPath(path12)) add("testing", 3);
6516
+ else if (isDocPath(path12)) add("docs", 3);
6517
+ else if (isConfigPath(path12)) add("config", 3);
6518
+ else add("exploration", 4);
6519
+ }
6520
+ if (tool === "edit" || tool === "multiedit" || tool === "write") {
6521
+ if (isTestPath(path12)) add("testing", 4);
6522
+ else if (isDocPath(path12)) add("docs", 4);
6523
+ else add("coding", 4);
6524
+ }
6525
+ if (tool === "read") add("exploration", 1);
6526
+ if (tool === "grep" || tool === "glob") add("exploration", 3);
6527
+ if (tool === "webfetch" || tool === "websearch") add("research", 6);
6528
+ if (tool === "task") add("planning", 2);
6529
+ if (event.type === "shell_exec" || tool === "bash") {
6530
+ if (/(\bnpm test\b|\bvitest\b|\bjest\b|\bpytest\b|\bmocha\b|\bpnpm test\b|\bcargo test\b|\bgo test\b)/.test(cmd)) {
6531
+ add("testing", 8);
6532
+ }
6533
+ if (/(\bdocker\b|\bkubectl\b|\bterraform\b|\bansible\b|\bhelm\b|\baws\b|\bgcloud\b|\bsystemctl\b)/.test(cmd)) {
6534
+ add("devops", 7);
6535
+ }
6536
+ if (/\bgit\s+(diff|status|log|blame|show)/.test(cmd)) add("review", 4);
6537
+ if (/\bgit\s+(add|commit|push|merge|rebase|checkout)/.test(cmd)) add("coding", 3);
6538
+ if (/\b(eslint|prettier|tsc|typecheck|lint|mypy|pyright)\b/.test(cmd)) add("review", 3);
6539
+ if (/\b(make|cargo|npm run|pnpm run|yarn run|bun run)\b/.test(cmd)) add("coding", 2);
6540
+ if (toolError) add("debugging", 4);
6541
+ }
6542
+ if (event.type === "prompt" || event.type === "response") {
6543
+ if (/\b(refactor|rename|extract|inline|move|reorganize|restructure)\b/.test(text)) {
6544
+ add("refactor", 6);
6545
+ }
6546
+ if (/\b(error|exception|stack[- ]?trace|traceback|fail(?:ed|ing|ure)?|broken|bug|crash|throws?|undefined is not|cannot read prop|nullpointer)\b/.test(text)) {
6547
+ add("debugging", 5);
6548
+ }
6549
+ if (/\b(test(?:s|ing)?|assert(?:ion)?s?|spec(?:s|tests)?|coverage|mock(?:s|ing)?)\b/.test(text)) {
6550
+ add("testing", 3);
6551
+ }
6552
+ if (/\b(review|audit|check|look at|inspect|verify|critique)\b/.test(text)) {
6553
+ add("review", 4);
6554
+ }
6555
+ if (/\b(plan|approach|step\s\d|first[, ]|then\b|finally\b|let\s+me\s+think|let\s+us|design)\b/.test(text)) {
6556
+ add("planning", 2);
6557
+ }
6558
+ if (/\b(deploy|deployment|release|rollout|pipeline|ci\/cd|production|staging|prod\b)\b/.test(text)) {
6559
+ add("devops", 3);
6560
+ }
6561
+ if (/\b(document(?:ation)?|readme|changelog|docs?\b|comment[s]?|jsdoc|tsdoc|docstring)\b/.test(text)) {
6562
+ add("docs", 3);
6563
+ }
6564
+ if (/\b(config(?:ure|uration)?|settings|environment|env\s+var|toml|yaml|yml|tsconfig|package\.json)\b/.test(text)) {
6565
+ add("config", 3);
6566
+ }
6567
+ if (/\b(research|read about|articles?|paper(s)?|blog\b|reference|literature)\b/.test(text)) {
6568
+ add("research", 3);
6569
+ }
6570
+ if (/\b(what is|how does|how do i|where is|find\b|search\b|locate\b)\b/.test(text)) {
6571
+ add("exploration", 2);
6572
+ }
6573
+ }
6574
+ const thinkingLen = thinking.length;
6575
+ if (thinkingLen > 1500) add("planning", 5);
6576
+ else if (thinkingLen > 300) add("planning", 2);
6577
+ if (event.type === "prompt" || event.type === "response") {
6578
+ if (Object.keys(scores).length === 0) add("chat", 1);
6579
+ }
6580
+ if (event.type === "session_start" || event.type === "session_end") {
6581
+ return { chat: 1 };
6582
+ }
6583
+ if (event.type === "compaction") {
6584
+ return { planning: 1 };
6585
+ }
6586
+ if (event.type === "parse_error") {
6587
+ return { chat: 0 };
6588
+ }
6589
+ return scores;
6590
+ }
6591
+ function isTestPath(p) {
6592
+ if (!p) return false;
6593
+ return /(^|\/)(tests?|__tests__|spec)\//.test(p) || /\.(test|spec)\.[a-z0-9]+$/.test(p);
6594
+ }
6595
+ function isDocPath(p) {
6596
+ if (!p) return false;
6597
+ if (/\.(md|mdx|rst|adoc|txt)$/.test(p)) return true;
6598
+ if (/(^|\/)(docs?|guides?|examples?)\//.test(p)) return true;
6599
+ if (/(^|\/)(readme|changelog|contributing|license|security|code_of_conduct)/i.test(p)) {
6600
+ return true;
6601
+ }
6602
+ return false;
6603
+ }
6604
+ function isConfigPath(p) {
6605
+ if (!p) return false;
6606
+ if (/\.(json|ya?ml|toml|ini|env|config\.[jt]s|cjs|mjs)$/.test(p)) return true;
6607
+ if (/(^|\/)(\.env(?:\..*)?|tsconfig|tsup\.config|vitest\.config|vite\.config|jest\.config|babel\.config|webpack\.config|rollup\.config|prettier\.config|eslint\.config|tailwind\.config|postcss\.config)$/i.test(p)) {
6608
+ return true;
6609
+ }
6610
+ if (/(^|\/)package\.json$/i.test(p)) return true;
6611
+ return false;
6612
+ }
6613
+ var ACTIVITY_CATEGORIES;
6614
+ var init_activity2 = __esm({
6615
+ "src/classify/activity.ts"() {
6616
+ "use strict";
6617
+ ACTIVITY_CATEGORIES = [
6618
+ "coding",
6619
+ "debugging",
6620
+ "exploration",
6621
+ "planning",
6622
+ "refactor",
6623
+ "testing",
6624
+ "docs",
6625
+ "chat",
6626
+ "config",
6627
+ "review",
6628
+ "devops",
6629
+ "research"
6630
+ ];
6631
+ }
6632
+ });
6633
+
6634
+ // src/classify/sink.ts
6635
+ function withClassifier(inner) {
6636
+ return {
6637
+ emit: (event) => {
6638
+ if (!event.details) event.details = {};
6639
+ if (!event.details.category) {
6640
+ event.details.category = classifyEvent(event);
6641
+ }
6642
+ inner.emit(event);
6643
+ },
6644
+ enrich: (eventId, patch) => {
6645
+ inner.enrich(eventId, patch);
6646
+ }
6647
+ };
4736
6648
  }
4737
- var PER_AGENT_CAP, DEFAULT_HOST, DEFAULT_PORT;
4738
- var init_server = __esm({
4739
- "src/server/index.ts"() {
6649
+ var init_sink = __esm({
6650
+ "src/classify/sink.ts"() {
4740
6651
  "use strict";
4741
- init_sse();
4742
- init_events();
4743
- init_projects();
4744
- init_sessions();
4745
- init_agents();
4746
- init_permissions();
4747
- init_cron();
4748
- init_search();
4749
- init_config();
4750
- init_trends();
4751
- init_diffs();
4752
- init_replay();
4753
- init_version();
4754
- PER_AGENT_CAP = 1e4;
4755
- DEFAULT_HOST = "127.0.0.1";
4756
- DEFAULT_PORT = 3456;
6652
+ init_activity2();
6653
+ }
6654
+ });
6655
+
6656
+ // src/classify/index.ts
6657
+ var classify_exports = {};
6658
+ __export(classify_exports, {
6659
+ ACTIVITY_CATEGORIES: () => ACTIVITY_CATEGORIES,
6660
+ classifyEvent: () => classifyEvent,
6661
+ scoreEvent: () => scoreEvent2,
6662
+ withClassifier: () => withClassifier
6663
+ });
6664
+ var init_classify = __esm({
6665
+ "src/classify/index.ts"() {
6666
+ "use strict";
6667
+ init_activity2();
6668
+ init_sink();
4757
6669
  }
4758
6670
  });
4759
6671
 
@@ -4765,10 +6677,10 @@ __export(server_exports2, {
4765
6677
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4766
6678
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4767
6679
  import { z } from "zod";
4768
- import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
4769
- import { homedir as homedir10 } from "os";
4770
- import { join as join13 } from "path";
4771
- import Database3 from "better-sqlite3";
6680
+ import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
6681
+ import { homedir as homedir12 } from "os";
6682
+ import { join as join16 } from "path";
6683
+ import Database4 from "better-sqlite3";
4772
6684
  async function runMcpServer() {
4773
6685
  const server = new McpServer({
4774
6686
  name: "agentwatch",
@@ -4982,7 +6894,7 @@ async function runMcpServer() {
4982
6894
  }
4983
6895
  function safeReadFile(path12) {
4984
6896
  try {
4985
- return readFileSync8(path12, "utf8");
6897
+ return readFileSync9(path12, "utf8");
4986
6898
  } catch {
4987
6899
  return "";
4988
6900
  }
@@ -5010,7 +6922,7 @@ function dumpHermesSessionJsonl(ref) {
5010
6922
  }
5011
6923
  function openHermesDb(path12) {
5012
6924
  try {
5013
- const db2 = new Database3(path12, { readonly: true, fileMustExist: true });
6925
+ const db2 = new Database4(path12, { readonly: true, fileMustExist: true });
5014
6926
  db2.pragma("journal_mode = WAL");
5015
6927
  db2.pragma("busy_timeout = 2000");
5016
6928
  return db2;
@@ -5082,13 +6994,13 @@ function listAllSessions() {
5082
6994
  const out = [];
5083
6995
  try {
5084
6996
  const cdir = claudeProjectsDir();
5085
- for (const proj of readdirSync2(cdir)) {
5086
- const projPath = join13(cdir, proj);
6997
+ for (const proj of readdirSync3(cdir)) {
6998
+ const projPath = join16(cdir, proj);
5087
6999
  try {
5088
- for (const f of readdirSync2(projPath)) {
7000
+ for (const f of readdirSync3(projPath)) {
5089
7001
  if (!f.endsWith(".jsonl")) continue;
5090
- const full = join13(projPath, f);
5091
- const s = statSync6(full);
7002
+ const full = join16(projPath, f);
7003
+ const s = statSync7(full);
5092
7004
  out.push({
5093
7005
  agent: "claude-code",
5094
7006
  sessionId: f.replace(/\.jsonl$/, ""),
@@ -5109,7 +7021,7 @@ function listAllSessions() {
5109
7021
  } catch {
5110
7022
  }
5111
7023
  try {
5112
- const gdir = join13(process.env.HOME ?? "", ".gemini", "tmp");
7024
+ const gdir = join16(process.env.HOME ?? "", ".gemini", "tmp");
5113
7025
  walkGemini(gdir, out);
5114
7026
  } catch {
5115
7027
  }
@@ -5125,38 +7037,38 @@ function listAllSessions() {
5125
7037
  return out;
5126
7038
  }
5127
7039
  function resolveOpenClawRoot() {
5128
- return join13(homedir10(), ".openclaw");
7040
+ return join16(homedir12(), ".openclaw");
5129
7041
  }
5130
7042
  function resolveHermesDbPath2() {
5131
7043
  const explicit = process.env.HERMES_DB_PATH?.trim();
5132
7044
  if (explicit && explicit.length > 0) return explicit;
5133
7045
  const hermesHome = process.env.HERMES_HOME?.trim();
5134
- const base = hermesHome && hermesHome.length > 0 ? hermesHome : join13(homedir10(), ".hermes");
5135
- return join13(base, "state.db");
7046
+ const base = hermesHome && hermesHome.length > 0 ? hermesHome : join16(homedir12(), ".hermes");
7047
+ return join16(base, "state.db");
5136
7048
  }
5137
7049
  function walkOpenClaw(root, out) {
5138
- if (!existsSync13(root)) return;
5139
- const agentsDir = join13(root, "agents");
7050
+ if (!existsSync15(root)) return;
7051
+ const agentsDir = join16(root, "agents");
5140
7052
  let agents;
5141
7053
  try {
5142
- agents = readdirSync2(agentsDir);
7054
+ agents = readdirSync3(agentsDir);
5143
7055
  } catch {
5144
7056
  return;
5145
7057
  }
5146
7058
  for (const agent of agents) {
5147
- const sessionsDir = join13(agentsDir, agent, "sessions");
7059
+ const sessionsDir = join16(agentsDir, agent, "sessions");
5148
7060
  let files;
5149
7061
  try {
5150
- files = readdirSync2(sessionsDir);
7062
+ files = readdirSync3(sessionsDir);
5151
7063
  } catch {
5152
7064
  continue;
5153
7065
  }
5154
7066
  for (const name of files) {
5155
7067
  if (!name.endsWith(".jsonl")) continue;
5156
- const full = join13(sessionsDir, name);
7068
+ const full = join16(sessionsDir, name);
5157
7069
  let st;
5158
7070
  try {
5159
- st = statSync6(full);
7071
+ st = statSync7(full);
5160
7072
  } catch {
5161
7073
  continue;
5162
7074
  }
@@ -5172,11 +7084,11 @@ function walkOpenClaw(root, out) {
5172
7084
  }
5173
7085
  }
5174
7086
  function walkHermes(dbPath, out) {
5175
- if (!existsSync13(dbPath)) return;
7087
+ if (!existsSync15(dbPath)) return;
5176
7088
  const db2 = openHermesDb(dbPath);
5177
7089
  if (!db2) return;
5178
7090
  try {
5179
- const st = statSync6(dbPath);
7091
+ const st = statSync7(dbPath);
5180
7092
  const rows = db2.prepare(
5181
7093
  "SELECT id, source, message_count, started_at, ended_at FROM sessions"
5182
7094
  ).all();
@@ -5202,24 +7114,24 @@ function walkHermes(dbPath, out) {
5202
7114
  function walkGemini(dir, out) {
5203
7115
  let projects;
5204
7116
  try {
5205
- projects = readdirSync2(dir);
7117
+ projects = readdirSync3(dir);
5206
7118
  } catch {
5207
7119
  return;
5208
7120
  }
5209
7121
  for (const project of projects) {
5210
- const chatsDir = join13(dir, project, "chats");
7122
+ const chatsDir = join16(dir, project, "chats");
5211
7123
  let files;
5212
7124
  try {
5213
- files = readdirSync2(chatsDir);
7125
+ files = readdirSync3(chatsDir);
5214
7126
  } catch {
5215
7127
  continue;
5216
7128
  }
5217
7129
  for (const name of files) {
5218
7130
  if (!name.endsWith(".json")) continue;
5219
- const full = join13(chatsDir, name);
7131
+ const full = join16(chatsDir, name);
5220
7132
  let st;
5221
7133
  try {
5222
- st = statSync6(full);
7134
+ st = statSync7(full);
5223
7135
  } catch {
5224
7136
  continue;
5225
7137
  }
@@ -5239,15 +7151,15 @@ function walkGemini(dir, out) {
5239
7151
  function walkCodex(dir, out) {
5240
7152
  let entries;
5241
7153
  try {
5242
- entries = readdirSync2(dir);
7154
+ entries = readdirSync3(dir);
5243
7155
  } catch {
5244
7156
  return;
5245
7157
  }
5246
7158
  for (const name of entries) {
5247
- const full = join13(dir, name);
7159
+ const full = join16(dir, name);
5248
7160
  let st;
5249
7161
  try {
5250
- st = statSync6(full);
7162
+ st = statSync7(full);
5251
7163
  } catch {
5252
7164
  continue;
5253
7165
  }
@@ -5282,6 +7194,654 @@ var init_server2 = __esm({
5282
7194
  }
5283
7195
  });
5284
7196
 
7197
+ // src/daemon/install.ts
7198
+ import { homedir as homedir13, platform as platform4 } from "os";
7199
+ import { join as join17, resolve as resolve2 } from "path";
7200
+ import { existsSync as existsSync16, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, unlinkSync } from "fs";
7201
+ function resolveAgentwatchExec() {
7202
+ return {
7203
+ node: process.execPath,
7204
+ script: resolve2(process.argv[1] ?? "")
7205
+ };
7206
+ }
7207
+ function plistPath() {
7208
+ return join17(homedir13(), "Library", "LaunchAgents", `${DAEMON_LABEL}.plist`);
7209
+ }
7210
+ function systemdUnitPath() {
7211
+ return join17(homedir13(), ".config", "systemd", "user", "agentwatch.service");
7212
+ }
7213
+ function logPath() {
7214
+ return join17(homedir13(), ".agentwatch", "daemon.log");
7215
+ }
7216
+ function pidFilePath() {
7217
+ return join17(homedir13(), ".agentwatch", "daemon.pid");
7218
+ }
7219
+ function startTimeFilePath() {
7220
+ return join17(homedir13(), ".agentwatch", "daemon.started_at");
7221
+ }
7222
+ function renderPlist(exec, log) {
7223
+ return `<?xml version="1.0" encoding="UTF-8"?>
7224
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7225
+ <plist version="1.0">
7226
+ <dict>
7227
+ <key>Label</key>
7228
+ <string>${DAEMON_LABEL}</string>
7229
+ <key>ProgramArguments</key>
7230
+ <array>
7231
+ <string>${exec.node}</string>
7232
+ <string>${exec.script}</string>
7233
+ <string>daemon</string>
7234
+ <string>run</string>
7235
+ </array>
7236
+ <key>RunAtLoad</key>
7237
+ <true/>
7238
+ <key>KeepAlive</key>
7239
+ <true/>
7240
+ <key>ProcessType</key>
7241
+ <string>Background</string>
7242
+ <key>StandardOutPath</key>
7243
+ <string>${log}</string>
7244
+ <key>StandardErrorPath</key>
7245
+ <string>${log}</string>
7246
+ </dict>
7247
+ </plist>
7248
+ `;
7249
+ }
7250
+ function renderSystemdUnit(exec, log) {
7251
+ return `[Unit]
7252
+ Description=agentwatch event capture daemon
7253
+ After=network-online.target
7254
+
7255
+ [Service]
7256
+ Type=simple
7257
+ ExecStart=${exec.node} ${exec.script} daemon run
7258
+ Restart=on-failure
7259
+ RestartSec=5
7260
+ StandardOutput=append:${log}
7261
+ StandardError=append:${log}
7262
+
7263
+ [Install]
7264
+ WantedBy=default.target
7265
+ `;
7266
+ }
7267
+ function writeServiceUnit() {
7268
+ const exec = resolveAgentwatchExec();
7269
+ const log = logPath();
7270
+ mkdirSync3(join17(homedir13(), ".agentwatch"), { recursive: true });
7271
+ if (platform4() === "darwin") {
7272
+ const path12 = plistPath();
7273
+ mkdirSync3(join17(homedir13(), "Library", "LaunchAgents"), { recursive: true });
7274
+ writeFileSync2(path12, renderPlist(exec, log), "utf-8");
7275
+ return {
7276
+ unitPath: path12,
7277
+ manualSteps: [`launchctl load -w ${path12}`]
7278
+ };
7279
+ }
7280
+ if (platform4() === "linux") {
7281
+ const path12 = systemdUnitPath();
7282
+ mkdirSync3(join17(homedir13(), ".config", "systemd", "user"), { recursive: true });
7283
+ writeFileSync2(path12, renderSystemdUnit(exec, log), "utf-8");
7284
+ return {
7285
+ unitPath: path12,
7286
+ manualSteps: [
7287
+ "systemctl --user daemon-reload",
7288
+ "systemctl --user enable --now agentwatch.service"
7289
+ ]
7290
+ };
7291
+ }
7292
+ throw new Error(
7293
+ `agentwatch daemon: unsupported platform "${platform4()}" (Windows is on the v0.2 roadmap)`
7294
+ );
7295
+ }
7296
+ function removeServiceUnit() {
7297
+ const path12 = platform4() === "darwin" ? plistPath() : platform4() === "linux" ? systemdUnitPath() : null;
7298
+ if (!path12) return { unitPath: null };
7299
+ if (existsSync16(path12)) {
7300
+ try {
7301
+ unlinkSync(path12);
7302
+ } catch {
7303
+ }
7304
+ }
7305
+ return { unitPath: path12 };
7306
+ }
7307
+ var DAEMON_LABEL;
7308
+ var init_install = __esm({
7309
+ "src/daemon/install.ts"() {
7310
+ "use strict";
7311
+ DAEMON_LABEL = "com.agentwatch.daemon";
7312
+ }
7313
+ });
7314
+
7315
+ // src/daemon/log-rotate.ts
7316
+ import { closeSync as closeSync2, openSync as openSync2, renameSync, statSync as statSync8, writeSync } from "fs";
7317
+ import { dirname as dirname5 } from "path";
7318
+ import { mkdirSync as mkdirSync4 } from "fs";
7319
+ var DEFAULT_MAX_BYTES, RotatingLogStream;
7320
+ var init_log_rotate = __esm({
7321
+ "src/daemon/log-rotate.ts"() {
7322
+ "use strict";
7323
+ DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
7324
+ RotatingLogStream = class {
7325
+ fd;
7326
+ bytes;
7327
+ path;
7328
+ maxBytes;
7329
+ constructor(opts) {
7330
+ this.path = opts.path;
7331
+ this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
7332
+ mkdirSync4(dirname5(this.path), { recursive: true });
7333
+ this.fd = openSync2(this.path, "a");
7334
+ try {
7335
+ this.bytes = statSync8(this.path).size;
7336
+ } catch {
7337
+ this.bytes = 0;
7338
+ }
7339
+ }
7340
+ write(line) {
7341
+ const buf = Buffer.from(line.endsWith("\n") ? line : `${line}
7342
+ `);
7343
+ if (this.bytes + buf.length > this.maxBytes) {
7344
+ this.rotate();
7345
+ }
7346
+ writeSync(this.fd, buf);
7347
+ this.bytes += buf.length;
7348
+ }
7349
+ /** Test seam — read current bytes-on-disk for assertions. */
7350
+ byteCount() {
7351
+ return this.bytes;
7352
+ }
7353
+ close() {
7354
+ try {
7355
+ closeSync2(this.fd);
7356
+ } catch {
7357
+ }
7358
+ }
7359
+ rotate() {
7360
+ try {
7361
+ closeSync2(this.fd);
7362
+ } catch {
7363
+ }
7364
+ try {
7365
+ renameSync(this.path, `${this.path}.1`);
7366
+ } catch {
7367
+ }
7368
+ this.fd = openSync2(this.path, "a");
7369
+ this.bytes = 0;
7370
+ }
7371
+ };
7372
+ }
7373
+ });
7374
+
7375
+ // src/daemon/run.ts
7376
+ import {
7377
+ existsSync as existsSync17,
7378
+ readFileSync as readFileSync10,
7379
+ unlinkSync as unlinkSync2,
7380
+ writeFileSync as writeFileSync3
7381
+ } from "fs";
7382
+ import { mkdirSync as mkdirSync5 } from "fs";
7383
+ import { dirname as dirname6 } from "path";
7384
+ import { homedir as homedir14 } from "os";
7385
+ import { join as join18 } from "path";
7386
+ async function runDaemon() {
7387
+ const dataDir = join18(homedir14(), ".agentwatch");
7388
+ mkdirSync5(dataDir, { recursive: true });
7389
+ const lock = acquireLock();
7390
+ if (!lock.ok) {
7391
+ process.stderr.write(
7392
+ `[agentwatch daemon] another instance is already running (pid ${lock.existingPid}). Exiting.
7393
+ `
7394
+ );
7395
+ process.exit(2);
7396
+ }
7397
+ const log = new RotatingLogStream({ path: logPath() });
7398
+ const logLine = (msg) => {
7399
+ log.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${msg}`);
7400
+ };
7401
+ logLine(`daemon starting (pid ${process.pid})`);
7402
+ let store = null;
7403
+ let stoppingHooks = [];
7404
+ try {
7405
+ const { openStore: openStore2, wrapSinkWithStore: wrapSinkWithStore2, wrapSinkWithLinks: wrapSinkWithLinks2 } = await Promise.resolve().then(() => (init_store(), store_exports));
7406
+ const { startAllAdapters: startAllAdapters2, stopAllAdapters: stopAllAdapters2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
7407
+ const { detectWorkspaceRoot: detectWorkspaceRoot2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
7408
+ store = openStore2();
7409
+ const workspace = detectWorkspaceRoot2();
7410
+ let captured = 0;
7411
+ const inner = {
7412
+ emit: (e) => {
7413
+ e.ts = clampTs(e.ts);
7414
+ captured += 1;
7415
+ },
7416
+ enrich: (_id, _patch) => {
7417
+ }
7418
+ };
7419
+ const sink = wrapSinkWithLinks2(wrapSinkWithStore2(inner, store), store);
7420
+ const adapters = startAllAdapters2(sink, workspace);
7421
+ stoppingHooks.push(() => stopAllAdapters2(adapters));
7422
+ stoppingHooks.push(() => store?.close());
7423
+ logLine(`adapters started; workspace=${workspace}`);
7424
+ const heartbeat = setInterval(() => {
7425
+ logLine(`heartbeat captured=${captured}`);
7426
+ }, 6e4);
7427
+ heartbeat.unref();
7428
+ stoppingHooks.push(() => clearInterval(heartbeat));
7429
+ setupShutdown(stoppingHooks, log, lock.releaseLock);
7430
+ await new Promise(() => void 0);
7431
+ } catch (err) {
7432
+ logLine(`fatal: ${String(err)}`);
7433
+ for (const hook of stoppingHooks) {
7434
+ try {
7435
+ await hook();
7436
+ } catch {
7437
+ }
7438
+ }
7439
+ lock.releaseLock();
7440
+ log.close();
7441
+ process.exit(1);
7442
+ }
7443
+ }
7444
+ function acquireLock() {
7445
+ const pidFile = pidFilePath();
7446
+ const startFile = startTimeFilePath();
7447
+ if (existsSync17(pidFile)) {
7448
+ const raw = readFileSync10(pidFile, "utf-8").trim();
7449
+ const pid = Number(raw);
7450
+ if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {
7451
+ return { ok: false, existingPid: pid, releaseLock: () => void 0 };
7452
+ }
7453
+ try {
7454
+ unlinkSync2(pidFile);
7455
+ } catch {
7456
+ }
7457
+ }
7458
+ mkdirSync5(dirname6(pidFile), { recursive: true });
7459
+ writeFileSync3(pidFile, String(process.pid), "utf-8");
7460
+ writeFileSync3(startFile, String(Date.now()), "utf-8");
7461
+ return {
7462
+ ok: true,
7463
+ releaseLock: () => {
7464
+ try {
7465
+ unlinkSync2(pidFile);
7466
+ } catch {
7467
+ }
7468
+ try {
7469
+ unlinkSync2(startFile);
7470
+ } catch {
7471
+ }
7472
+ }
7473
+ };
7474
+ }
7475
+ function isProcessAlive(pid) {
7476
+ try {
7477
+ process.kill(pid, 0);
7478
+ return true;
7479
+ } catch (err) {
7480
+ const code = err.code;
7481
+ if (code === "EPERM") return true;
7482
+ return false;
7483
+ }
7484
+ }
7485
+ function setupShutdown(hooks2, log, releaseLock) {
7486
+ let shutting = false;
7487
+ const stop = async (sig) => {
7488
+ if (shutting) return;
7489
+ shutting = true;
7490
+ log.write(
7491
+ `${(/* @__PURE__ */ new Date()).toISOString()} shutdown signal=${sig} draining ${hooks2.length} hooks
7492
+ `
7493
+ );
7494
+ for (const hook of hooks2) {
7495
+ try {
7496
+ await hook();
7497
+ } catch (err) {
7498
+ log.write(
7499
+ `${(/* @__PURE__ */ new Date()).toISOString()} shutdown hook error: ${String(err)}
7500
+ `
7501
+ );
7502
+ }
7503
+ }
7504
+ releaseLock();
7505
+ log.write(`${(/* @__PURE__ */ new Date()).toISOString()} daemon stopped cleanly
7506
+ `);
7507
+ log.close();
7508
+ process.exit(0);
7509
+ };
7510
+ process.on("SIGTERM", () => void stop("SIGTERM"));
7511
+ process.on("SIGINT", () => void stop("SIGINT"));
7512
+ process.on("SIGHUP", () => void stop("SIGHUP"));
7513
+ }
7514
+ var init_run = __esm({
7515
+ "src/daemon/run.ts"() {
7516
+ "use strict";
7517
+ init_install();
7518
+ init_log_rotate();
7519
+ init_schema();
7520
+ }
7521
+ });
7522
+
7523
+ // src/daemon/index.ts
7524
+ var daemon_exports = {};
7525
+ __export(daemon_exports, {
7526
+ dispatchDaemon: () => dispatchDaemon,
7527
+ readDaemonStatus: () => readDaemonStatus
7528
+ });
7529
+ import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
7530
+ import { spawnSync as spawnSync4 } from "child_process";
7531
+ import { platform as platform5 } from "os";
7532
+ async function dispatchDaemon(sub) {
7533
+ switch (sub) {
7534
+ case void 0:
7535
+ case "--help":
7536
+ case "-h":
7537
+ console.log(HELP);
7538
+ process.exit(0);
7539
+ return;
7540
+ case "start":
7541
+ return startCmd();
7542
+ case "stop":
7543
+ return stopCmd();
7544
+ case "status":
7545
+ return statusCmd();
7546
+ case "logs":
7547
+ return logsCmd();
7548
+ case "run":
7549
+ await runDaemon();
7550
+ return;
7551
+ default:
7552
+ process.stderr.write(`agentwatch daemon: unknown subcommand "${sub}"
7553
+ `);
7554
+ process.stderr.write(HELP);
7555
+ process.exit(2);
7556
+ }
7557
+ }
7558
+ function startCmd() {
7559
+ const result = writeServiceUnit();
7560
+ console.log(`wrote service unit: ${result.unitPath}`);
7561
+ if (platform5() === "darwin") {
7562
+ runStep(["launchctl", "unload", result.unitPath], { allowFail: true });
7563
+ runStep(["launchctl", "load", "-w", result.unitPath]);
7564
+ console.log(`daemon loaded \u2014 events stream into ~/.agentwatch/events.db`);
7565
+ return;
7566
+ }
7567
+ if (platform5() === "linux") {
7568
+ runStep(["systemctl", "--user", "daemon-reload"]);
7569
+ runStep(["systemctl", "--user", "enable", "--now", "agentwatch.service"]);
7570
+ console.log(`daemon enabled + started`);
7571
+ return;
7572
+ }
7573
+ console.log(`unsupported platform; manual steps:`);
7574
+ for (const cmd of result.manualSteps) console.log(` ${cmd}`);
7575
+ }
7576
+ function stopCmd() {
7577
+ if (platform5() === "darwin") {
7578
+ const path12 = plistPath();
7579
+ if (existsSync18(path12)) {
7580
+ runStep(["launchctl", "unload", path12], { allowFail: true });
7581
+ }
7582
+ const removed = removeServiceUnit();
7583
+ console.log(`daemon stopped${removed.unitPath ? ` (removed ${removed.unitPath})` : ""}`);
7584
+ return;
7585
+ }
7586
+ if (platform5() === "linux") {
7587
+ runStep(["systemctl", "--user", "disable", "--now", "agentwatch.service"], {
7588
+ allowFail: true
7589
+ });
7590
+ runStep(["systemctl", "--user", "daemon-reload"], { allowFail: true });
7591
+ const removed = removeServiceUnit();
7592
+ console.log(`daemon stopped${removed.unitPath ? ` (removed ${removed.unitPath})` : ""}`);
7593
+ return;
7594
+ }
7595
+ console.log(`unsupported platform \u2014 kill the process manually`);
7596
+ }
7597
+ function statusCmd() {
7598
+ const status = readDaemonStatus();
7599
+ if (!status.running) {
7600
+ console.log(`daemon: not running`);
7601
+ if (status.unitInstalled) console.log(`unit installed at: ${status.unitPath}`);
7602
+ process.exit(0);
7603
+ }
7604
+ console.log(`daemon: running (pid ${status.pid})`);
7605
+ console.log(`uptime: ${formatUptime(status.uptimeMs)}`);
7606
+ console.log(
7607
+ `events captured: ${status.eventsCaptured}` + (status.lastEventTs ? ` \xB7 last at ${status.lastEventTs}` : "")
7608
+ );
7609
+ if (status.unitPath) console.log(`unit: ${status.unitPath}`);
7610
+ if (status.dbBytes != null) {
7611
+ console.log(`db size: ${(status.dbBytes / 1048576).toFixed(1)} MB`);
7612
+ }
7613
+ }
7614
+ function readDaemonStatus() {
7615
+ const pidFile = pidFilePath();
7616
+ const unitPath = platform5() === "darwin" ? plistPath() : platform5() === "linux" ? systemdUnitPath() : void 0;
7617
+ const unitInstalled = unitPath ? existsSync18(unitPath) : false;
7618
+ let pid;
7619
+ let uptimeMs = 0;
7620
+ if (existsSync18(pidFile)) {
7621
+ const raw = readFileSync11(pidFile, "utf-8").trim();
7622
+ const parsed = Number(raw);
7623
+ if (Number.isFinite(parsed) && parsed > 0 && isProcessAlive(parsed)) {
7624
+ pid = parsed;
7625
+ }
7626
+ }
7627
+ if (pid && existsSync18(startTimeFilePath())) {
7628
+ const startMs = Number(readFileSync11(startTimeFilePath(), "utf-8").trim());
7629
+ if (Number.isFinite(startMs)) uptimeMs = Math.max(0, Date.now() - startMs);
7630
+ }
7631
+ let eventsCaptured = 0;
7632
+ let lastEventTs;
7633
+ let dbBytes;
7634
+ try {
7635
+ const { openStore: openStore2 } = (init_sqlite(), __toCommonJS(sqlite_exports));
7636
+ const store = openStore2();
7637
+ const stats = store.stats();
7638
+ eventsCaptured = stats.events;
7639
+ dbBytes = stats.dbBytes;
7640
+ const sessions = store.listSessions({ limit: 1 });
7641
+ lastEventTs = sessions[0]?.lastTs;
7642
+ store.close();
7643
+ } catch {
7644
+ }
7645
+ return {
7646
+ running: pid != null,
7647
+ ...pid != null ? { pid } : {},
7648
+ uptimeMs,
7649
+ eventsCaptured,
7650
+ ...lastEventTs ? { lastEventTs } : {},
7651
+ unitInstalled,
7652
+ ...unitPath ? { unitPath } : {},
7653
+ ...dbBytes != null ? { dbBytes } : {}
7654
+ };
7655
+ }
7656
+ function logsCmd() {
7657
+ const path12 = logPath();
7658
+ if (!existsSync18(path12)) {
7659
+ console.log(`(no log yet at ${path12})`);
7660
+ return;
7661
+ }
7662
+ const tail = spawnSync4("tail", ["-n", "200", "-f", path12], {
7663
+ stdio: "inherit"
7664
+ });
7665
+ if (tail.error) {
7666
+ process.stdout.write(readFileSync11(path12, "utf-8"));
7667
+ }
7668
+ }
7669
+ function runStep(argv, opts = {}) {
7670
+ const [cmd, ...rest] = argv;
7671
+ if (!cmd) return;
7672
+ const result = spawnSync4(cmd, rest, { stdio: "inherit" });
7673
+ if (result.error) {
7674
+ if (opts.allowFail) return;
7675
+ throw new Error(`spawn ${cmd}: ${String(result.error)}`);
7676
+ }
7677
+ if (result.status !== 0 && !opts.allowFail) {
7678
+ throw new Error(`${argv.join(" ")} exited ${result.status}`);
7679
+ }
7680
+ }
7681
+ function formatUptime(ms) {
7682
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
7683
+ if (ms < 36e5) return `${Math.floor(ms / 6e4)}m`;
7684
+ if (ms < 864e5) {
7685
+ const h2 = Math.floor(ms / 36e5);
7686
+ const m = Math.floor(ms % 36e5 / 6e4);
7687
+ return `${h2}h ${m}m`;
7688
+ }
7689
+ const d = Math.floor(ms / 864e5);
7690
+ const h = Math.floor(ms % 864e5 / 36e5);
7691
+ return `${d}d ${h}h`;
7692
+ }
7693
+ var HELP;
7694
+ var init_daemon = __esm({
7695
+ "src/daemon/index.ts"() {
7696
+ "use strict";
7697
+ init_install();
7698
+ init_run();
7699
+ HELP = `agentwatch daemon \u2014 background event capture
7700
+
7701
+ Usage:
7702
+ agentwatch daemon start install + load the user-level service
7703
+ agentwatch daemon stop unload the service (events.db is preserved)
7704
+ agentwatch daemon status running state + uptime + capture stats
7705
+ agentwatch daemon logs tail the daemon log
7706
+ agentwatch daemon run foreground mode (used by launchd / systemd)
7707
+
7708
+ Service files:
7709
+ macOS: ~/Library/LaunchAgents/${DAEMON_LABEL}.plist
7710
+ Linux: ~/.config/systemd/user/agentwatch.service
7711
+
7712
+ The daemon writes every adapter event to ~/.agentwatch/events.db. The
7713
+ TUI and \`agentwatch serve\` read the same store, so events captured
7714
+ overnight are visible the moment you open them.
7715
+ `;
7716
+ }
7717
+ });
7718
+
7719
+ // src/adapters/claude-hooks-install.ts
7720
+ var claude_hooks_install_exports = {};
7721
+ __export(claude_hooks_install_exports, {
7722
+ DEFAULT_HOOKS_PORT: () => DEFAULT_HOOKS_PORT,
7723
+ HOOKS_MARKER: () => HOOKS_MARKER,
7724
+ MANAGED_HOOK_EVENTS: () => MANAGED_HOOK_EVENTS,
7725
+ buildHookCommand: () => buildHookCommand,
7726
+ claudeHooksStatus: () => claudeHooksStatus,
7727
+ installClaudeHooks: () => installClaudeHooks,
7728
+ settingsPath: () => settingsPath,
7729
+ uninstallClaudeHooks: () => uninstallClaudeHooks
7730
+ });
7731
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
7732
+ import { dirname as dirname7 } from "path";
7733
+ import { homedir as homedir15 } from "os";
7734
+ import { join as join19 } from "path";
7735
+ function settingsPath(home) {
7736
+ return join19(home ?? homedir15(), ".claude", "settings.json");
7737
+ }
7738
+ function buildHookCommand(port, eventName) {
7739
+ return `# [${HOOKS_MARKER}] ${eventName}
7740
+ curl -s -m 1 -X POST -H 'Content-Type: application/json' --data-binary @- http://127.0.0.1:${port}/api/hooks/${eventName} > /dev/null 2>&1; exit 0`;
7741
+ }
7742
+ function installClaudeHooks(opts = {}) {
7743
+ const port = opts.port ?? DEFAULT_HOOKS_PORT;
7744
+ const path12 = settingsPath(opts.home);
7745
+ mkdirSync6(dirname7(path12), { recursive: true });
7746
+ const current = readSettings(path12);
7747
+ const next = { ...current, hooks: { ...current.hooks ?? {} } };
7748
+ let alreadyManaged = false;
7749
+ for (const event of MANAGED_HOOK_EVENTS) {
7750
+ const existing = next.hooks[event] ?? [];
7751
+ const ourCommand = buildHookCommand(port, event);
7752
+ const filtered = existing.filter(
7753
+ (g) => !(g.hooks ?? []).some((h) => (h.command ?? "").includes(`[${HOOKS_MARKER}]`))
7754
+ );
7755
+ if (filtered.length !== existing.length) alreadyManaged = true;
7756
+ filtered.push({
7757
+ matcher: ".*",
7758
+ hooks: [{ type: "command", command: ourCommand }]
7759
+ });
7760
+ next.hooks[event] = filtered;
7761
+ }
7762
+ writeSettings(path12, next);
7763
+ return {
7764
+ settingsPath: path12,
7765
+ installedEvents: [...MANAGED_HOOK_EVENTS],
7766
+ alreadyManaged
7767
+ };
7768
+ }
7769
+ function uninstallClaudeHooks(opts = {}) {
7770
+ const path12 = settingsPath(opts.home);
7771
+ if (!existsSync19(path12)) {
7772
+ return { settingsPath: path12, removedEvents: [] };
7773
+ }
7774
+ const current = readSettings(path12);
7775
+ if (!current.hooks) return { settingsPath: path12, removedEvents: [] };
7776
+ const removed = [];
7777
+ const nextHooks = {};
7778
+ for (const [event, groups] of Object.entries(current.hooks)) {
7779
+ const filtered = groups.filter(
7780
+ (g) => !(g.hooks ?? []).some((h) => (h.command ?? "").includes(`[${HOOKS_MARKER}]`))
7781
+ );
7782
+ if (filtered.length !== groups.length) removed.push(event);
7783
+ if (filtered.length > 0) nextHooks[event] = filtered;
7784
+ }
7785
+ const next = { ...current, hooks: nextHooks };
7786
+ if (Object.keys(nextHooks).length === 0) delete next.hooks;
7787
+ writeSettings(path12, next);
7788
+ return { settingsPath: path12, removedEvents: removed };
7789
+ }
7790
+ function claudeHooksStatus(opts = {}) {
7791
+ const path12 = settingsPath(opts.home);
7792
+ if (!existsSync19(path12)) {
7793
+ return {
7794
+ status: "not-installed",
7795
+ managedEvents: [],
7796
+ missingEvents: [...MANAGED_HOOK_EVENTS],
7797
+ settingsPath: path12
7798
+ };
7799
+ }
7800
+ const settings = readSettings(path12);
7801
+ const managed = [];
7802
+ for (const event of MANAGED_HOOK_EVENTS) {
7803
+ const groups = settings.hooks?.[event] ?? [];
7804
+ const has = groups.some(
7805
+ (g) => (g.hooks ?? []).some((h) => (h.command ?? "").includes(`[${HOOKS_MARKER}]`))
7806
+ );
7807
+ if (has) managed.push(event);
7808
+ }
7809
+ const missing = MANAGED_HOOK_EVENTS.filter((e) => !managed.includes(e));
7810
+ const status = managed.length === 0 ? "not-installed" : missing.length === 0 ? "installed" : "partial";
7811
+ return { status, managedEvents: managed, missingEvents: missing, settingsPath: path12 };
7812
+ }
7813
+ function readSettings(path12) {
7814
+ if (!existsSync19(path12)) return {};
7815
+ try {
7816
+ return JSON.parse(readFileSync12(path12, "utf-8"));
7817
+ } catch {
7818
+ return {};
7819
+ }
7820
+ }
7821
+ function writeSettings(path12, value) {
7822
+ writeFileSync4(path12, JSON.stringify(value, null, 2) + "\n", "utf-8");
7823
+ }
7824
+ var HOOKS_MARKER, DEFAULT_HOOKS_PORT, MANAGED_HOOK_EVENTS;
7825
+ var init_claude_hooks_install = __esm({
7826
+ "src/adapters/claude-hooks-install.ts"() {
7827
+ "use strict";
7828
+ HOOKS_MARKER = "agentwatch-managed";
7829
+ DEFAULT_HOOKS_PORT = 3456;
7830
+ MANAGED_HOOK_EVENTS = [
7831
+ "SessionStart",
7832
+ "SessionEnd",
7833
+ "UserPromptSubmit",
7834
+ "PreToolUse",
7835
+ "PostToolUse",
7836
+ "Stop",
7837
+ "SubagentStop",
7838
+ "PreCompact",
7839
+ "PostCompact",
7840
+ "Notification"
7841
+ ];
7842
+ }
7843
+ });
7844
+
5285
7845
  // src/index.tsx
5286
7846
  import { render } from "ink";
5287
7847
 
@@ -5437,7 +7997,8 @@ function Header({
5437
7997
  budget,
5438
7998
  anomalies,
5439
7999
  sessionAnomalies,
5440
- webUrl
8000
+ webUrl,
8001
+ linkCandidateCount
5441
8002
  }) {
5442
8003
  const breached = budget?.breachedSession || budget?.dayBreach;
5443
8004
  const anomalyMessages = summarizeAnomalies(anomalies);
@@ -5468,6 +8029,10 @@ function Header({
5468
8029
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " web: " }),
5469
8030
  /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: webUrl }),
5470
8031
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " [w]" })
8032
+ ] }),
8033
+ linkCandidateCount != null && /* @__PURE__ */ jsxs3(Fragment, { children: [
8034
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " links: " }),
8035
+ /* @__PURE__ */ jsx3(Text3, { children: linkCandidateCount })
5471
8036
  ] })
5472
8037
  ] })
5473
8038
  ] }),
@@ -5510,13 +8075,13 @@ function Header({
5510
8075
  }
5511
8076
  function summarizeAnomalies(map) {
5512
8077
  if (!map || map.size === 0) return [];
5513
- const seen = /* @__PURE__ */ new Set();
8078
+ const seen2 = /* @__PURE__ */ new Set();
5514
8079
  const msgs = [];
5515
8080
  for (const flags of map.values()) {
5516
8081
  for (const f of flags) {
5517
8082
  const key = `${f.kind}:${f.message}`;
5518
- if (seen.has(key)) continue;
5519
- seen.add(key);
8083
+ if (seen2.has(key)) continue;
8084
+ seen2.add(key);
5520
8085
  msgs.push(f.message);
5521
8086
  if (msgs.length >= 3) return msgs;
5522
8087
  }
@@ -6629,6 +9194,9 @@ async function runShutdownHooks() {
6629
9194
  }
6630
9195
 
6631
9196
  // src/ui/App.tsx
9197
+ init_store();
9198
+ init_hooks_dedup();
9199
+ init_classify();
6632
9200
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
6633
9201
  function App() {
6634
9202
  const { exit } = useApp();
@@ -6637,9 +9205,34 @@ function App() {
6637
9205
  const { stdout } = useStdout();
6638
9206
  const [state, dispatch] = useReducer(reducer, void 0, initialState);
6639
9207
  const [server, setServer] = useState(null);
9208
+ const [store, setStore] = useState(null);
6640
9209
  const noWeb = process.argv.includes("--no-web");
9210
+ const [linkCandidateCount, setLinkCandidateCount] = useState(
9211
+ void 0
9212
+ );
9213
+ useEffect(() => {
9214
+ let s = null;
9215
+ let unregister = null;
9216
+ try {
9217
+ s = openStore();
9218
+ setStore(s);
9219
+ unregister = onShutdown(() => s?.close());
9220
+ } catch (err) {
9221
+ console.error(`[agentwatch] event store unavailable: ${String(err)}`);
9222
+ }
9223
+ return () => {
9224
+ unregister?.();
9225
+ if (s) {
9226
+ try {
9227
+ s.close();
9228
+ } catch {
9229
+ }
9230
+ }
9231
+ };
9232
+ }, []);
6641
9233
  useEffect(() => {
6642
9234
  if (noWeb) return;
9235
+ if (!store) return;
6643
9236
  const events = [];
6644
9237
  const port = Number(
6645
9238
  findFlag("--port") ?? process.env.AGENTWATCH_PORT ?? 3456
@@ -6648,7 +9241,7 @@ function App() {
6648
9241
  let handle = null;
6649
9242
  let cancelled = false;
6650
9243
  let unregister = null;
6651
- startServer({ host, port, events }).then((h) => {
9244
+ startServer({ host, port, events, store }).then((h) => {
6652
9245
  if (cancelled) {
6653
9246
  void h.stop();
6654
9247
  return;
@@ -6664,11 +9257,18 @@ function App() {
6664
9257
  unregister?.();
6665
9258
  if (handle) void handle.stop();
6666
9259
  };
6667
- }, []);
9260
+ }, [store]);
6668
9261
  useEffect(() => {
6669
9262
  const stopTriggersWatch = watchTriggers();
6670
9263
  if (otelEnabled()) void initOtel();
6671
9264
  const launchedAt = Date.now();
9265
+ if (store) {
9266
+ try {
9267
+ const seed = store.listRecentEvents({ limit: 500, order: "desc" });
9268
+ if (seed.length > 0) dispatch({ type: "events-batch", events: seed });
9269
+ } catch {
9270
+ }
9271
+ }
6672
9272
  let pending2 = [];
6673
9273
  let flushScheduled = false;
6674
9274
  const FLUSH_MS = 16;
@@ -6714,7 +9314,14 @@ function App() {
6714
9314
  }
6715
9315
  }
6716
9316
  };
6717
- const adapters = startAllAdapters(sink, workspace);
9317
+ const persistSink = store ? wrapSinkWithStore(sink, store) : sink;
9318
+ const linkedSink = store ? wrapSinkWithLinks(persistSink, store) : persistSink;
9319
+ const classifiedSink = withClassifier(linkedSink);
9320
+ const finalSink = withClaudeHookDedup(classifiedSink);
9321
+ if (server) {
9322
+ server.setHookSink(finalSink);
9323
+ }
9324
+ const adapters = startAllAdapters(finalSink, workspace);
6718
9325
  const unregisterShutdown = onShutdown(() => {
6719
9326
  flush();
6720
9327
  stopAllAdapters(adapters);
@@ -6726,16 +9333,29 @@ function App() {
6726
9333
  stopAllAdapters(adapters);
6727
9334
  stopTriggersWatch();
6728
9335
  };
6729
- }, [workspace, server]);
9336
+ }, [workspace, server, store]);
6730
9337
  const agentFiltered = state.filterAgent ? state.events.filter((e) => e.agent === state.filterAgent) : state.events;
6731
9338
  const filtered = state.searchQuery ? agentFiltered.filter((e) => matchesQuery(e, state.searchQuery)) : agentFiltered;
6732
9339
  const eventsRef = state.events;
6733
- const budgetStatus = useMemo(() => computeBudgetStatus(eventsRef), [eventsRef]);
9340
+ const budgetStatus = useMemo(() => {
9341
+ if (!store) return computeBudgetStatus(eventsRef);
9342
+ const todayStart = /* @__PURE__ */ new Date();
9343
+ todayStart.setUTCHours(0, 0, 0, 0);
9344
+ const since = todayStart.toISOString();
9345
+ const monthAgo = new Date(Date.now() - 30 * 864e5).toISOString();
9346
+ const events = store.listRecentEvents({
9347
+ sinceTs: monthAgo < since ? monthAgo : since,
9348
+ limit: 5e4,
9349
+ order: "asc"
9350
+ });
9351
+ return computeBudgetStatus(events);
9352
+ }, [eventsRef, store]);
6734
9353
  const anomalies = useMemo(() => {
9354
+ const source = store ? store.listRecentEvents({ limit: 5e3, order: "desc" }) : eventsRef;
6735
9355
  const out = /* @__PURE__ */ new Map();
6736
- const sliceEnd = Math.min(40, eventsRef.length);
9356
+ const sliceEnd = Math.min(40, source.length);
6737
9357
  const historyByAgent = /* @__PURE__ */ new Map();
6738
- for (const e of eventsRef) {
9358
+ for (const e of source) {
6739
9359
  let arr = historyByAgent.get(e.agent);
6740
9360
  if (!arr) {
6741
9361
  arr = [];
@@ -6744,7 +9364,7 @@ function App() {
6744
9364
  arr.push(e);
6745
9365
  }
6746
9366
  for (let i = 0; i < sliceEnd; i++) {
6747
- const ev = eventsRef[i];
9367
+ const ev = source[i];
6748
9368
  const agentHistory = historyByAgent.get(ev.agent) ?? [];
6749
9369
  const pos = agentHistory.indexOf(ev);
6750
9370
  const history = pos >= 0 ? agentHistory.slice(pos + 1) : agentHistory;
@@ -6752,9 +9372,9 @@ function App() {
6752
9372
  const flags = scoreEvent(ev, history);
6753
9373
  if (flags.length > 0) out.set(ev.id, flags);
6754
9374
  }
6755
- const stuckLoop = detectStuckLoop(eventsRef.slice(0, 20).reverse());
9375
+ const stuckLoop = detectStuckLoop(source.slice(0, 20).reverse());
6756
9376
  if (stuckLoop) {
6757
- const first = eventsRef[0];
9377
+ const first = source[0];
6758
9378
  if (first) {
6759
9379
  const prev = out.get(first.id) ?? [];
6760
9380
  const label = stuckLoop.period === 1 ? `same tool fired ${stuckLoop.count}\xD7 in a row` : `period-${stuckLoop.period} loop (${stuckLoop.count} cycles): ${stuckLoop.pattern}`;
@@ -6770,7 +9390,7 @@ function App() {
6770
9390
  }
6771
9391
  }
6772
9392
  return out;
6773
- }, [eventsRef]);
9393
+ }, [eventsRef, store]);
6774
9394
  const budgetBreachKey = [
6775
9395
  budgetStatus.breachedSession ?? "",
6776
9396
  budgetStatus.dayBreach ? "day" : ""
@@ -6790,6 +9410,19 @@ function App() {
6790
9410
  );
6791
9411
  }
6792
9412
  }, [budgetBreachKey]);
9413
+ useEffect(() => {
9414
+ if (!store) return;
9415
+ if (process.env.AGENTWATCH_DEBUG_LINKS !== "1") return;
9416
+ const refresh = () => {
9417
+ try {
9418
+ setLinkCandidateCount(store.countAllLinkCandidates());
9419
+ } catch {
9420
+ }
9421
+ };
9422
+ refresh();
9423
+ const handle = setInterval(refresh, 5e3);
9424
+ return () => clearInterval(handle);
9425
+ }, [store]);
6793
9426
  const sessionSummaries = useMemo(() => summarizeBySession(anomalies), [anomalies]);
6794
9427
  const anomalyKey = sessionSummaries.map((s) => `${s.sessionId}:${s.headline}`).join("|");
6795
9428
  const bannerSuppressed = state.anomalyDismissKey === anomalyKey;
@@ -6808,15 +9441,16 @@ function App() {
6808
9441
  }
6809
9442
  }, [anomalyKey]);
6810
9443
  const childCountByAgentId = useMemo(() => {
9444
+ const source = store ? store.listRecentEvents({ limit: 1e4, order: "desc" }) : eventsRef;
6811
9445
  const m = /* @__PURE__ */ new Map();
6812
- for (const e of eventsRef) {
9446
+ for (const e of source) {
6813
9447
  if (e.sessionId?.startsWith("agent-")) {
6814
9448
  const aid = e.sessionId.slice("agent-".length);
6815
9449
  m.set(aid, (m.get(aid) ?? 0) + 1);
6816
9450
  }
6817
9451
  }
6818
9452
  return m;
6819
- }, [eventsRef]);
9453
+ }, [eventsRef, store]);
6820
9454
  const cols = stdout.columns || 120;
6821
9455
  const rows = stdout.rows || 30;
6822
9456
  const tooNarrow = cols < 60;
@@ -6902,7 +9536,8 @@ function App() {
6902
9536
  budget: budgetStatus,
6903
9537
  anomalies: bannerSuppressed ? void 0 : anomalies,
6904
9538
  sessionAnomalies: bannerSuppressed ? [] : sessionSummaries,
6905
- webUrl: server?.url
9539
+ webUrl: server?.url,
9540
+ linkCandidateCount
6906
9541
  }
6907
9542
  ),
6908
9543
  /* @__PURE__ */ jsx5(
@@ -6985,6 +9620,12 @@ Usage:
6985
9620
  agentwatch serve run only the web server (no TUI, for remote boxes)
6986
9621
  agentwatch doctor detect installed agents and print readiness
6987
9622
  agentwatch mcp run as an MCP server over stdio
9623
+ agentwatch daemon ... install + manage the background capture service
9624
+ (subcommands: start | stop | status | logs)
9625
+ agentwatch hooks ... install / uninstall / status the Claude Code hooks adapter
9626
+ agentwatch prune drop events older than --older-than-days (default 90)
9627
+ agentwatch link-candidates dump AUR-276 session-correlation candidate pairs as JSON
9628
+ (--session <id> to scope; --limit <n> to cap)
6988
9629
  agentwatch --help show this help
6989
9630
 
6990
9631
  Flags:
@@ -7001,9 +9642,10 @@ Hotkeys inside the TUI:
7001
9642
  w open web UI in browser
7002
9643
 
7003
9644
  Environment:
7004
- WORKSPACE_ROOT override the detected workspace root
7005
- AGENTWATCH_PORT override the web server port
7006
- AGENTWATCH_HOST override the web server bind address
9645
+ WORKSPACE_ROOT override the detected workspace root
9646
+ AGENTWATCH_PORT override the web server port
9647
+ AGENTWATCH_HOST override the web server bind address
9648
+ AGENTWATCH_DEBUG_LINKS show AUR-276 candidate-pair counts in the agent panel
7007
9649
  `);
7008
9650
  process.exit(0);
7009
9651
  }
@@ -7011,9 +9653,9 @@ if (arg === "mcp") {
7011
9653
  try {
7012
9654
  const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_server2(), server_exports2));
7013
9655
  await runMcpServer2();
7014
- await new Promise((resolve) => {
7015
- process.stdin.on("end", resolve);
7016
- process.stdin.on("close", resolve);
9656
+ await new Promise((resolve3) => {
9657
+ process.stdin.on("end", resolve3);
9658
+ process.stdin.on("close", resolve3);
7017
9659
  });
7018
9660
  } catch (err) {
7019
9661
  process.stderr.write(`[agentwatch] mcp error: ${String(err)}
@@ -7022,9 +9664,103 @@ if (arg === "mcp") {
7022
9664
  }
7023
9665
  process.exit(0);
7024
9666
  }
9667
+ if (arg === "daemon") {
9668
+ const { dispatchDaemon: dispatchDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
9669
+ await dispatchDaemon2(process.argv[3]);
9670
+ process.exit(0);
9671
+ }
9672
+ if (arg === "hooks") {
9673
+ const sub = process.argv[3];
9674
+ const {
9675
+ installClaudeHooks: installClaudeHooks2,
9676
+ uninstallClaudeHooks: uninstallClaudeHooks2,
9677
+ claudeHooksStatus: claudeHooksStatus2
9678
+ } = await Promise.resolve().then(() => (init_claude_hooks_install(), claude_hooks_install_exports));
9679
+ if (sub === "install") {
9680
+ const port = Number(parseFlag("--port") ?? process.env.AGENTWATCH_PORT ?? "3456");
9681
+ const result = installClaudeHooks2({ port });
9682
+ console.log(`installed agentwatch hooks into ${result.settingsPath}`);
9683
+ console.log(`events: ${result.installedEvents.join(", ")}`);
9684
+ if (result.alreadyManaged) {
9685
+ console.log(`(replaced previously-installed agentwatch stanzas)`);
9686
+ }
9687
+ process.exit(0);
9688
+ }
9689
+ if (sub === "uninstall") {
9690
+ const result = uninstallClaudeHooks2();
9691
+ if (result.removedEvents.length === 0) {
9692
+ console.log(`no agentwatch hook stanzas found in ${result.settingsPath}`);
9693
+ } else {
9694
+ console.log(`removed ${result.removedEvents.length} hook stanzas from ${result.settingsPath}`);
9695
+ console.log(`events: ${result.removedEvents.join(", ")}`);
9696
+ }
9697
+ process.exit(0);
9698
+ }
9699
+ if (sub === "status" || sub === void 0) {
9700
+ const status = claudeHooksStatus2();
9701
+ console.log(`claude hooks: ${status.status}`);
9702
+ console.log(`settings: ${status.settingsPath}`);
9703
+ if (status.managedEvents.length > 0) {
9704
+ console.log(`installed: ${status.managedEvents.join(", ")}`);
9705
+ }
9706
+ if (status.missingEvents.length > 0) {
9707
+ console.log(`missing: ${status.missingEvents.join(", ")}`);
9708
+ }
9709
+ process.exit(0);
9710
+ }
9711
+ process.stderr.write(
9712
+ `agentwatch hooks: unknown subcommand "${sub}" (use install | uninstall | status)
9713
+ `
9714
+ );
9715
+ process.exit(2);
9716
+ }
9717
+ if (arg === "link-candidates") {
9718
+ const { openStore: openStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
9719
+ const sessionId = parseFlag("--session");
9720
+ const limitFlag = parseFlag("--limit");
9721
+ const limit = limitFlag ? Number(limitFlag) : void 0;
9722
+ if (limit != null && (!Number.isFinite(limit) || limit < 1)) {
9723
+ process.stderr.write(
9724
+ `[agentwatch] link-candidates: --limit must be a positive number, got ${limitFlag}
9725
+ `
9726
+ );
9727
+ process.exit(2);
9728
+ }
9729
+ const store = openStore2();
9730
+ try {
9731
+ const rows = store.listSessionLinkCandidates({
9732
+ ...sessionId ? { sessionId } : {},
9733
+ ...limit ? { limit } : {}
9734
+ });
9735
+ process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
9736
+ } finally {
9737
+ store.close();
9738
+ }
9739
+ process.exit(0);
9740
+ }
9741
+ if (arg === "prune") {
9742
+ const { openStore: openStore2 } = await Promise.resolve().then(() => (init_store(), store_exports));
9743
+ const days = Number(parseFlag("--older-than-days") ?? "90");
9744
+ if (!Number.isFinite(days) || days < 0) {
9745
+ process.stderr.write(
9746
+ `[agentwatch] prune: --older-than-days must be a non-negative number, got ${days}
9747
+ `
9748
+ );
9749
+ process.exit(2);
9750
+ }
9751
+ const store = openStore2();
9752
+ const result = store.prune({ olderThanDays: days });
9753
+ const stats = store.stats();
9754
+ store.close();
9755
+ console.log(
9756
+ `pruned ${result.deletedEvents} events / ${result.deletedSessions} sessions older than ${days}d (${stats.events} events / ${stats.sessions} sessions / ${(stats.dbBytes / 1048576).toFixed(1)} MB remaining)`
9757
+ );
9758
+ process.exit(0);
9759
+ }
7025
9760
  if (arg === "doctor") {
7026
9761
  const { detectAgents: detectAgents2 } = await Promise.resolve().then(() => (init_detect(), detect_exports));
7027
9762
  const { detectWorkspaceRoot: detectWorkspaceRoot2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
9763
+ const { claudeHooksStatus: claudeHooksStatus2 } = await Promise.resolve().then(() => (init_claude_hooks_install(), claude_hooks_install_exports));
7028
9764
  const agents = detectAgents2();
7029
9765
  console.log(`workspace: ${detectWorkspaceRoot2()}
7030
9766
  `);
@@ -7043,6 +9779,15 @@ if (arg === "doctor") {
7043
9779
  console.log(` - ${a.label}`);
7044
9780
  }
7045
9781
  }
9782
+ console.log("");
9783
+ const hooks2 = claudeHooksStatus2();
9784
+ console.log(`claude code hooks: ${hooks2.status}`);
9785
+ if (hooks2.status === "partial") {
9786
+ console.log(` missing: ${hooks2.missingEvents.join(", ")}`);
9787
+ }
9788
+ if (hooks2.status !== "installed") {
9789
+ console.log(` install with: agentwatch hooks install`);
9790
+ }
7046
9791
  process.exit(0);
7047
9792
  }
7048
9793
  if (arg === "serve") {
@@ -7050,12 +9795,26 @@ if (arg === "serve") {
7050
9795
  const { startAllAdapters: startAllAdapters2, stopAllAdapters: stopAllAdapters2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
7051
9796
  const { detectWorkspaceRoot: detectWorkspaceRoot2 } = await Promise.resolve().then(() => (init_workspace(), workspace_exports));
7052
9797
  const { clampTs: clampTs2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
9798
+ const { openStore: openStore2, wrapSinkWithStore: wrapSinkWithStore2, wrapSinkWithLinks: wrapSinkWithLinks2 } = await Promise.resolve().then(() => (init_store(), store_exports));
7053
9799
  const workspace = detectWorkspaceRoot2();
7054
9800
  const host = parseFlag("--host") ?? process.env.AGENTWATCH_HOST ?? "127.0.0.1";
7055
9801
  const port = Number(parseFlag("--port") ?? process.env.AGENTWATCH_PORT ?? 3456);
7056
9802
  const { addEventToServer: addEventToServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
7057
- const server = await startServer2({ host, port });
7058
- const sink = {
9803
+ let store = null;
9804
+ try {
9805
+ store = openStore2();
9806
+ } catch (err) {
9807
+ process.stderr.write(
9808
+ `[agentwatch] event store unavailable: ${String(err)}
9809
+ `
9810
+ );
9811
+ }
9812
+ const server = await startServer2({
9813
+ host,
9814
+ port,
9815
+ ...store ? { store } : {}
9816
+ });
9817
+ const innerSink = {
7059
9818
  emit: (e) => {
7060
9819
  e.ts = clampTs2(e.ts);
7061
9820
  addEventToServer2(server, e);
@@ -7072,9 +9831,17 @@ if (arg === "serve") {
7072
9831
  server.broadcaster.emitEnrich(eventId, patch);
7073
9832
  }
7074
9833
  };
9834
+ const { withClassifier: withClassifier2 } = await Promise.resolve().then(() => (init_classify(), classify_exports));
9835
+ const { withClaudeHookDedup: withClaudeHookDedup2 } = await Promise.resolve().then(() => (init_hooks_dedup(), hooks_dedup_exports));
9836
+ const persistSink = store ? wrapSinkWithStore2(innerSink, store) : innerSink;
9837
+ const linkedSink = store ? wrapSinkWithLinks2(persistSink, store) : persistSink;
9838
+ const classifiedSink = withClassifier2(linkedSink);
9839
+ const sink = withClaudeHookDedup2(classifiedSink);
9840
+ server.setHookSink(sink);
7075
9841
  const adapters = startAllAdapters2(sink, workspace);
7076
9842
  onShutdown(() => stopAllAdapters2(adapters));
7077
9843
  onShutdown(() => server.stop());
9844
+ if (store) onShutdown(() => store?.close());
7078
9845
  process.stderr.write(`[agentwatch] serving ${server.url}
7079
9846
  `);
7080
9847
  await new Promise(() => void 0);