@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.
- package/README.md +11 -4
- package/dist/index.js +3258 -491
- package/dist/web/assets/ActiveShapeUtils-CtEvtbYg.js +1 -0
- package/dist/web/assets/Bar-DEgd_1jq.js +1 -0
- package/dist/web/assets/BarChart-CLHGmIz1.js +1 -0
- package/dist/web/assets/CartesianChart-DgJKUG1O.js +1 -0
- package/dist/web/assets/CategoricalChart-DEGrE0Qv.js +33 -0
- package/dist/web/assets/ErrorBarContext-BMnGmrrB.js +1 -0
- package/dist/web/assets/Legend-CEa2zCmi.js +4 -0
- package/dist/web/assets/Line-CCyE37cb.js +1 -0
- package/dist/web/assets/LineChart-DmMubyEd.js +1 -0
- package/dist/web/assets/ProjectActivity-BG5hEcJl.js +1 -0
- package/dist/web/assets/ProjectYield-vVxXaT6e.js +1 -0
- package/dist/web/assets/SessionActivity-BUzJpWwc.js +1 -0
- package/dist/web/assets/SessionCompaction-Dx3tbM9N.js +1 -0
- package/dist/web/assets/{SessionDiffs-LZrXV0AY.js → SessionDiffs-CDUkVqua.js} +3 -3
- package/dist/web/assets/SessionGraph-DWP1YyP0.js +1 -0
- package/dist/web/assets/SessionReplay-kz1EhiaI.js +1 -0
- package/dist/web/assets/SessionTokens-BjuXcVM2.js +1 -0
- package/dist/web/assets/SessionYield-DXF-Xb5T.js +1 -0
- package/dist/web/assets/Settings-CXCDYvsc.js +1 -0
- package/dist/web/assets/Trends-wdNqOtnn.js +1 -0
- package/dist/web/assets/arrow-left-B-iQ08P0.js +1 -0
- package/dist/web/assets/chart-column-BwF4X7mz.js +1 -0
- package/dist/web/assets/clsx-DnEFlO87.js +1 -0
- package/dist/web/assets/{clsx-DsHpp3Uj.js → createLucideIcon-Bjwrp8ZV.js} +1 -1
- package/dist/web/assets/dist-BYqqB4pa.js +1 -0
- package/dist/web/assets/file-pen-CHXP3wF7.js +1 -0
- package/dist/web/assets/getRadiusAndStrokeWidthFromDot-CFcPogcT.js +1 -0
- package/dist/web/assets/graphicalItemSelectors-DJeUEv0O.js +1 -0
- package/dist/web/assets/index-CJArQihV.js +2 -0
- package/dist/web/assets/index-CTAomrBX.css +1 -0
- package/dist/web/assets/play-ONoP4Jfr.js +1 -0
- package/dist/web/assets/tooltipContext-BT7phkqZ.js +1 -0
- package/dist/web/assets/triangle-alert-BhvAiLmv.js +1 -0
- package/dist/web/assets/useMutation-CfQYV-vU.js +1 -0
- package/dist/web/index.html +12 -11
- package/package.json +1 -1
- package/dist/web/assets/CartesianChart-CZSKepVZ.js +0 -33
- package/dist/web/assets/LineChart-BYjz-1bE.js +0 -1
- package/dist/web/assets/SessionCompaction-Duzo69wv.js +0 -1
- package/dist/web/assets/SessionGraph-Bb1BdCWf.js +0 -1
- package/dist/web/assets/SessionReplay-C3AZoYFc.js +0 -1
- package/dist/web/assets/SessionTokens-B6wfOhyn.js +0 -1
- package/dist/web/assets/Settings-HKanGbBq.js +0 -1
- package/dist/web/assets/Trends-p9DnVxWQ.js +0 -1
- package/dist/web/assets/arrow-left-Bg6VjX8-.js +0 -1
- package/dist/web/assets/chart-column-Brz7pC96.js +0 -1
- package/dist/web/assets/dist-w_zu0rIf.js +0 -1
- package/dist/web/assets/file-pen-DWwu4Q-r.js +0 -1
- package/dist/web/assets/graphicalItemSelectors-Dk1a_HU_.js +0 -4
- package/dist/web/assets/index-Bu9taSiK.js +0 -2
- package/dist/web/assets/index-CJPUO3dh.css +0 -1
- package/dist/web/assets/play-B0mJPrwl.js +0 -1
- package/dist/web/assets/triangle-alert-Bx2lGvGN.js +0 -1
- package/dist/web/assets/useMutation-BvFLVjYz.js +0 -1
- /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
|
|
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
|
|
107
|
+
var DEFAULT_RATES, cachedRates;
|
|
46
108
|
var init_cost = __esm({
|
|
47
109
|
"src/util/cost.ts"() {
|
|
48
110
|
"use strict";
|
|
49
|
-
|
|
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
|
-
|
|
113
|
-
|
|
175
|
+
join2(here, "..", "..", "package.json"),
|
|
176
|
+
join2(here, "..", "package.json")
|
|
114
177
|
]) {
|
|
115
178
|
try {
|
|
116
|
-
return JSON.parse(
|
|
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
|
|
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 :
|
|
206
|
-
return
|
|
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 =
|
|
272
|
+
const home = homedir2();
|
|
210
273
|
const os12 = platform2();
|
|
211
|
-
const clineDir = os12 === "darwin" ?
|
|
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
|
-
) :
|
|
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:
|
|
225
|
-
present:
|
|
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:
|
|
232
|
-
present:
|
|
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:
|
|
239
|
-
present:
|
|
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:
|
|
247
|
-
present:
|
|
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:
|
|
256
|
-
present:
|
|
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:
|
|
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:
|
|
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:
|
|
340
|
+
present: existsSync2(clineDir),
|
|
278
341
|
instrumented: false
|
|
279
342
|
},
|
|
280
343
|
{
|
|
281
344
|
name: "continue",
|
|
282
345
|
label: "Continue.dev",
|
|
283
|
-
configPath:
|
|
284
|
-
present:
|
|
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:
|
|
291
|
-
present:
|
|
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:
|
|
298
|
-
present:
|
|
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
|
|
352
|
-
import { homedir as
|
|
353
|
-
import { join as
|
|
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 =
|
|
421
|
+
const home = homedir3();
|
|
358
422
|
const candidates = [
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
435
|
+
return join4(homedir3(), ".claude", "projects");
|
|
372
436
|
}
|
|
373
437
|
function isDir(p) {
|
|
374
438
|
try {
|
|
375
|
-
return
|
|
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 {
|
|
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
|
|
831
|
+
const normalized = normalizeSink(sink);
|
|
832
|
+
const { emit, enrich } = normalized;
|
|
687
833
|
const dir = claudeProjectsDir();
|
|
688
|
-
if (!
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (!line.trim())
|
|
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
|
-
|
|
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
|
-
|
|
772
|
-
|
|
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 +
|
|
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 " +
|
|
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 +
|
|
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: ${
|
|
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}: ${
|
|
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: ${
|
|
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}: ${
|
|
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
|
|
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 {
|
|
1182
|
-
import {
|
|
1183
|
-
import {
|
|
1184
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1215
|
-
|
|
1216
|
-
|
|
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 =
|
|
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,
|
|
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 =
|
|
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(
|
|
1244
|
-
|
|
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,
|
|
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
|
|
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
|
|
1533
|
+
const { lines, consumed } = readNewlineTerminatedLines(
|
|
1534
|
+
file,
|
|
1304
1535
|
start,
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
let
|
|
1309
|
-
|
|
1310
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
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
|
|
1526
|
-
existsSync as
|
|
1757
|
+
readFileSync as readFileSync4,
|
|
1758
|
+
existsSync as existsSync6,
|
|
1527
1759
|
readdirSync,
|
|
1528
1760
|
statSync as statSync4
|
|
1529
1761
|
} from "fs";
|
|
1530
|
-
import { homedir as
|
|
1531
|
-
import { join as
|
|
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 =
|
|
1535
|
-
const installed =
|
|
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 =
|
|
1558
|
-
if (
|
|
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 =
|
|
1578
|
-
if (
|
|
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 =
|
|
1599
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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 (!
|
|
1697
|
-
const rootRules =
|
|
1698
|
-
if (
|
|
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 =
|
|
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 =
|
|
1715
|
-
if (
|
|
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
|
|
1736
|
-
import { homedir as
|
|
1737
|
-
import { basename as basename4, join as
|
|
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 =
|
|
1741
|
-
if (!
|
|
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(
|
|
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
|
|
1766
|
-
if (!
|
|
1767
|
-
|
|
1768
|
-
emittedIds.set(file,
|
|
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 ||
|
|
1780
|
-
|
|
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 +
|
|
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
|
|
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 {
|
|
1992
|
-
import {
|
|
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
|
|
2227
|
+
return join8(home, ".codex", "sessions");
|
|
1997
2228
|
}
|
|
1998
2229
|
function startCodexAdapter(sink) {
|
|
1999
2230
|
const dir = codexSessionsDir();
|
|
2000
|
-
if (!
|
|
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
|
|
2257
|
+
const { lines, consumed } = readNewlineTerminatedLines(
|
|
2258
|
+
file,
|
|
2026
2259
|
start,
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
let
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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
|
-
|
|
2341
|
+
continue;
|
|
2135
2342
|
}
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
cursor.
|
|
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}: ${
|
|
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: ${
|
|
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
|
|
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
|
|
2299
|
-
import { homedir as
|
|
2300
|
-
import { join as
|
|
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 :
|
|
2307
|
-
return
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3436
|
-
import { homedir as
|
|
3437
|
-
import { join as
|
|
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 = [
|
|
3688
|
+
const sources = [join10(homedir8(), ".claude", "settings.json")];
|
|
3440
3689
|
if (workspace) {
|
|
3441
|
-
sources.push(
|
|
3442
|
-
sources.push(
|
|
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 (!
|
|
3701
|
+
if (!existsSync10(path12)) return null;
|
|
3453
3702
|
try {
|
|
3454
|
-
const raw =
|
|
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
|
|
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(
|
|
3874
|
+
if (!fs8.existsSync(settingsPath2)) return out;
|
|
3626
3875
|
out.present = true;
|
|
3627
3876
|
try {
|
|
3628
|
-
const raw = JSON.parse(fs8.readFileSync(
|
|
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
|
|
3661
|
-
import { homedir as
|
|
3662
|
-
import { join as
|
|
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 =
|
|
3665
|
-
if (!
|
|
3913
|
+
const path12 = join11(homedir9(), ".openclaw", "openclaw.json");
|
|
3914
|
+
if (!existsSync11(path12)) return null;
|
|
3666
3915
|
try {
|
|
3667
|
-
const raw =
|
|
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/
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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
|
|
4278
|
-
|
|
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
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
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
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
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
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
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
|
-
|
|
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 =
|
|
5174
|
+
CONFIG_DIR = join13(homedir10(), ".agentwatch");
|
|
4349
5175
|
PATHS = {
|
|
4350
|
-
budgets:
|
|
4351
|
-
anomaly:
|
|
4352
|
-
triggers:
|
|
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((
|
|
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
|
-
|
|
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:
|
|
4567
|
-
stderr:
|
|
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
|
-
|
|
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:
|
|
4582
|
-
stderr:
|
|
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
|
-
|
|
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:
|
|
4598
|
-
stderr:
|
|
5423
|
+
stdout: truncate7(stdout, 4e4),
|
|
5424
|
+
stderr: truncate7(stderr, 4e4)
|
|
4599
5425
|
});
|
|
4600
5426
|
});
|
|
4601
5427
|
});
|
|
4602
5428
|
}
|
|
4603
5429
|
);
|
|
4604
5430
|
}
|
|
4605
|
-
function
|
|
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
|
|
4626
|
-
import { existsSync as
|
|
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
|
-
|
|
5468
|
+
join14(here, "web"),
|
|
4643
5469
|
// built: dist/index.js → dist/web
|
|
4644
|
-
|
|
5470
|
+
join14(here, "..", "dist", "web"),
|
|
4645
5471
|
// dev: src/server → dist/web
|
|
4646
|
-
|
|
5472
|
+
join14(here, "..", "..", "dist", "web")
|
|
4647
5473
|
// nested fallback
|
|
4648
5474
|
];
|
|
4649
|
-
for (const c of candidates) if (
|
|
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
|
|
4738
|
-
|
|
4739
|
-
"src/server/index.ts"() {
|
|
6649
|
+
var init_sink = __esm({
|
|
6650
|
+
"src/classify/sink.ts"() {
|
|
4740
6651
|
"use strict";
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
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
|
|
4769
|
-
import { homedir as
|
|
4770
|
-
import { join as
|
|
4771
|
-
import
|
|
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
|
|
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
|
|
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
|
|
5086
|
-
const projPath =
|
|
6997
|
+
for (const proj of readdirSync3(cdir)) {
|
|
6998
|
+
const projPath = join16(cdir, proj);
|
|
5087
6999
|
try {
|
|
5088
|
-
for (const f of
|
|
7000
|
+
for (const f of readdirSync3(projPath)) {
|
|
5089
7001
|
if (!f.endsWith(".jsonl")) continue;
|
|
5090
|
-
const full =
|
|
5091
|
-
const s =
|
|
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 =
|
|
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
|
|
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 :
|
|
5135
|
-
return
|
|
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 (!
|
|
5139
|
-
const agentsDir =
|
|
7050
|
+
if (!existsSync15(root)) return;
|
|
7051
|
+
const agentsDir = join16(root, "agents");
|
|
5140
7052
|
let agents;
|
|
5141
7053
|
try {
|
|
5142
|
-
agents =
|
|
7054
|
+
agents = readdirSync3(agentsDir);
|
|
5143
7055
|
} catch {
|
|
5144
7056
|
return;
|
|
5145
7057
|
}
|
|
5146
7058
|
for (const agent of agents) {
|
|
5147
|
-
const sessionsDir =
|
|
7059
|
+
const sessionsDir = join16(agentsDir, agent, "sessions");
|
|
5148
7060
|
let files;
|
|
5149
7061
|
try {
|
|
5150
|
-
files =
|
|
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 =
|
|
7068
|
+
const full = join16(sessionsDir, name);
|
|
5157
7069
|
let st;
|
|
5158
7070
|
try {
|
|
5159
|
-
st =
|
|
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 (!
|
|
7087
|
+
if (!existsSync15(dbPath)) return;
|
|
5176
7088
|
const db2 = openHermesDb(dbPath);
|
|
5177
7089
|
if (!db2) return;
|
|
5178
7090
|
try {
|
|
5179
|
-
const st =
|
|
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 =
|
|
7117
|
+
projects = readdirSync3(dir);
|
|
5206
7118
|
} catch {
|
|
5207
7119
|
return;
|
|
5208
7120
|
}
|
|
5209
7121
|
for (const project of projects) {
|
|
5210
|
-
const chatsDir =
|
|
7122
|
+
const chatsDir = join16(dir, project, "chats");
|
|
5211
7123
|
let files;
|
|
5212
7124
|
try {
|
|
5213
|
-
files =
|
|
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 =
|
|
7131
|
+
const full = join16(chatsDir, name);
|
|
5220
7132
|
let st;
|
|
5221
7133
|
try {
|
|
5222
|
-
st =
|
|
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 =
|
|
7154
|
+
entries = readdirSync3(dir);
|
|
5243
7155
|
} catch {
|
|
5244
7156
|
return;
|
|
5245
7157
|
}
|
|
5246
7158
|
for (const name of entries) {
|
|
5247
|
-
const full =
|
|
7159
|
+
const full = join16(dir, name);
|
|
5248
7160
|
let st;
|
|
5249
7161
|
try {
|
|
5250
|
-
st =
|
|
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
|
|
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 (
|
|
5519
|
-
|
|
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
|
|
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(() =>
|
|
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,
|
|
9356
|
+
const sliceEnd = Math.min(40, source.length);
|
|
6737
9357
|
const historyByAgent = /* @__PURE__ */ new Map();
|
|
6738
|
-
for (const e of
|
|
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 =
|
|
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(
|
|
9375
|
+
const stuckLoop = detectStuckLoop(source.slice(0, 20).reverse());
|
|
6756
9376
|
if (stuckLoop) {
|
|
6757
|
-
const first =
|
|
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
|
|
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
|
|
7005
|
-
AGENTWATCH_PORT
|
|
7006
|
-
AGENTWATCH_HOST
|
|
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((
|
|
7015
|
-
process.stdin.on("end",
|
|
7016
|
-
process.stdin.on("close",
|
|
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
|
-
|
|
7058
|
-
|
|
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);
|