@swestash/agent-lens 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.
@@ -0,0 +1,3572 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync5 } from "fs";
5
+ import { dirname as dirname6, join as join9 } from "path";
6
+ import { fileURLToPath as fileURLToPath3 } from "url";
7
+
8
+ // ../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs
9
+ import { EventEmitter } from "events";
10
+ function toArr(any) {
11
+ return any == null ? [] : Array.isArray(any) ? any : [any];
12
+ }
13
+ function toVal(out, key, val, opts) {
14
+ var x, old = out[key], nxt = !!~opts.string.indexOf(key) ? val == null || val === true ? "" : String(val) : typeof val === "boolean" ? val : !!~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
15
+ out[key] = old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
16
+ }
17
+ function mri2(args, opts) {
18
+ args = args || [];
19
+ opts = opts || {};
20
+ var k, arr, arg, name, val, out = { _: [] };
21
+ var i = 0, j = 0, idx = 0, len = args.length;
22
+ const alibi = opts.alias !== void 0;
23
+ const strict = opts.unknown !== void 0;
24
+ const defaults = opts.default !== void 0;
25
+ opts.alias = opts.alias || {};
26
+ opts.string = toArr(opts.string);
27
+ opts.boolean = toArr(opts.boolean);
28
+ if (alibi) {
29
+ for (k in opts.alias) {
30
+ arr = opts.alias[k] = toArr(opts.alias[k]);
31
+ for (i = 0; i < arr.length; i++) {
32
+ (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
33
+ }
34
+ }
35
+ }
36
+ for (i = opts.boolean.length; i-- > 0; ) {
37
+ arr = opts.alias[opts.boolean[i]] || [];
38
+ for (j = arr.length; j-- > 0; ) opts.boolean.push(arr[j]);
39
+ }
40
+ for (i = opts.string.length; i-- > 0; ) {
41
+ arr = opts.alias[opts.string[i]] || [];
42
+ for (j = arr.length; j-- > 0; ) opts.string.push(arr[j]);
43
+ }
44
+ if (defaults) {
45
+ for (k in opts.default) {
46
+ name = typeof opts.default[k];
47
+ arr = opts.alias[k] = opts.alias[k] || [];
48
+ if (opts[name] !== void 0) {
49
+ opts[name].push(k);
50
+ for (i = 0; i < arr.length; i++) {
51
+ opts[name].push(arr[i]);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ const keys = strict ? Object.keys(opts.alias) : [];
57
+ for (i = 0; i < len; i++) {
58
+ arg = args[i];
59
+ if (arg === "--") {
60
+ out._ = out._.concat(args.slice(++i));
61
+ break;
62
+ }
63
+ for (j = 0; j < arg.length; j++) {
64
+ if (arg.charCodeAt(j) !== 45) break;
65
+ }
66
+ if (j === 0) {
67
+ out._.push(arg);
68
+ } else if (arg.substring(j, j + 3) === "no-") {
69
+ name = arg.substring(j + 3);
70
+ if (strict && !~keys.indexOf(name)) {
71
+ return opts.unknown(arg);
72
+ }
73
+ out[name] = false;
74
+ } else {
75
+ for (idx = j + 1; idx < arg.length; idx++) {
76
+ if (arg.charCodeAt(idx) === 61) break;
77
+ }
78
+ name = arg.substring(j, idx);
79
+ val = arg.substring(++idx) || (i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i]);
80
+ arr = j === 2 ? [name] : name;
81
+ for (idx = 0; idx < arr.length; idx++) {
82
+ name = arr[idx];
83
+ if (strict && !~keys.indexOf(name)) return opts.unknown("-".repeat(j) + name);
84
+ toVal(out, name, idx + 1 < arr.length || val, opts);
85
+ }
86
+ }
87
+ }
88
+ if (defaults) {
89
+ for (k in opts.default) {
90
+ if (out[k] === void 0) {
91
+ out[k] = opts.default[k];
92
+ }
93
+ }
94
+ }
95
+ if (alibi) {
96
+ for (k in out) {
97
+ arr = opts.alias[k] || [];
98
+ while (arr.length > 0) {
99
+ out[arr.shift()] = out[k];
100
+ }
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ var removeBrackets = (v) => v.replace(/[<[].+/, "").trim();
106
+ var findAllBrackets = (v) => {
107
+ const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
108
+ const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
109
+ const res = [];
110
+ const parse = (match) => {
111
+ let variadic = false;
112
+ let value = match[1];
113
+ if (value.startsWith("...")) {
114
+ value = value.slice(3);
115
+ variadic = true;
116
+ }
117
+ return {
118
+ required: match[0].startsWith("<"),
119
+ value,
120
+ variadic
121
+ };
122
+ };
123
+ let angledMatch;
124
+ while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {
125
+ res.push(parse(angledMatch));
126
+ }
127
+ let squareMatch;
128
+ while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {
129
+ res.push(parse(squareMatch));
130
+ }
131
+ return res;
132
+ };
133
+ var getMriOptions = (options) => {
134
+ const result = { alias: {}, boolean: [] };
135
+ for (const [index, option] of options.entries()) {
136
+ if (option.names.length > 1) {
137
+ result.alias[option.names[0]] = option.names.slice(1);
138
+ }
139
+ if (option.isBoolean) {
140
+ if (option.negated) {
141
+ const hasStringTypeOption = options.some((o, i) => {
142
+ return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === "boolean";
143
+ });
144
+ if (!hasStringTypeOption) {
145
+ result.boolean.push(option.names[0]);
146
+ }
147
+ } else {
148
+ result.boolean.push(option.names[0]);
149
+ }
150
+ }
151
+ }
152
+ return result;
153
+ };
154
+ var findLongest = (arr) => {
155
+ return arr.sort((a, b) => {
156
+ return a.length > b.length ? -1 : 1;
157
+ })[0];
158
+ };
159
+ var padRight = (str, length) => {
160
+ return str.length >= length ? str : `${str}${" ".repeat(length - str.length)}`;
161
+ };
162
+ var camelcase = (input) => {
163
+ return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
164
+ return p1 + p2.toUpperCase();
165
+ });
166
+ };
167
+ var setDotProp = (obj, keys, val) => {
168
+ let i = 0;
169
+ let length = keys.length;
170
+ let t = obj;
171
+ let x;
172
+ for (; i < length; ++i) {
173
+ x = t[keys[i]];
174
+ t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(".") || !(+keys[i + 1] > -1) ? {} : [];
175
+ }
176
+ };
177
+ var setByType = (obj, transforms) => {
178
+ for (const key of Object.keys(transforms)) {
179
+ const transform = transforms[key];
180
+ if (transform.shouldTransform) {
181
+ obj[key] = Array.prototype.concat.call([], obj[key]);
182
+ if (typeof transform.transformFunction === "function") {
183
+ obj[key] = obj[key].map(transform.transformFunction);
184
+ }
185
+ }
186
+ }
187
+ };
188
+ var getFileName = (input) => {
189
+ const m = /([^\\\/]+)$/.exec(input);
190
+ return m ? m[1] : "";
191
+ };
192
+ var camelcaseOptionName = (name) => {
193
+ return name.split(".").map((v, i) => {
194
+ return i === 0 ? camelcase(v) : v;
195
+ }).join(".");
196
+ };
197
+ var CACError = class extends Error {
198
+ constructor(message) {
199
+ super(message);
200
+ this.name = this.constructor.name;
201
+ if (typeof Error.captureStackTrace === "function") {
202
+ Error.captureStackTrace(this, this.constructor);
203
+ } else {
204
+ this.stack = new Error(message).stack;
205
+ }
206
+ }
207
+ };
208
+ var Option = class {
209
+ constructor(rawName, description, config) {
210
+ this.rawName = rawName;
211
+ this.description = description;
212
+ this.config = Object.assign({}, config);
213
+ rawName = rawName.replace(/\.\*/g, "");
214
+ this.negated = false;
215
+ this.names = removeBrackets(rawName).split(",").map((v) => {
216
+ let name = v.trim().replace(/^-{1,2}/, "");
217
+ if (name.startsWith("no-")) {
218
+ this.negated = true;
219
+ name = name.replace(/^no-/, "");
220
+ }
221
+ return camelcaseOptionName(name);
222
+ }).sort((a, b) => a.length > b.length ? 1 : -1);
223
+ this.name = this.names[this.names.length - 1];
224
+ if (this.negated && this.config.default == null) {
225
+ this.config.default = true;
226
+ }
227
+ if (rawName.includes("<")) {
228
+ this.required = true;
229
+ } else if (rawName.includes("[")) {
230
+ this.required = false;
231
+ } else {
232
+ this.isBoolean = true;
233
+ }
234
+ }
235
+ };
236
+ var processArgs = process.argv;
237
+ var platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
238
+ var Command = class {
239
+ constructor(rawName, description, config = {}, cli2) {
240
+ this.rawName = rawName;
241
+ this.description = description;
242
+ this.config = config;
243
+ this.cli = cli2;
244
+ this.options = [];
245
+ this.aliasNames = [];
246
+ this.name = removeBrackets(rawName);
247
+ this.args = findAllBrackets(rawName);
248
+ this.examples = [];
249
+ }
250
+ usage(text) {
251
+ this.usageText = text;
252
+ return this;
253
+ }
254
+ allowUnknownOptions() {
255
+ this.config.allowUnknownOptions = true;
256
+ return this;
257
+ }
258
+ ignoreOptionDefaultValue() {
259
+ this.config.ignoreOptionDefaultValue = true;
260
+ return this;
261
+ }
262
+ version(version2, customFlags = "-v, --version") {
263
+ this.versionNumber = version2;
264
+ this.option(customFlags, "Display version number");
265
+ return this;
266
+ }
267
+ example(example) {
268
+ this.examples.push(example);
269
+ return this;
270
+ }
271
+ option(rawName, description, config) {
272
+ const option = new Option(rawName, description, config);
273
+ this.options.push(option);
274
+ return this;
275
+ }
276
+ alias(name) {
277
+ this.aliasNames.push(name);
278
+ return this;
279
+ }
280
+ action(callback) {
281
+ this.commandAction = callback;
282
+ return this;
283
+ }
284
+ isMatched(name) {
285
+ return this.name === name || this.aliasNames.includes(name);
286
+ }
287
+ get isDefaultCommand() {
288
+ return this.name === "" || this.aliasNames.includes("!");
289
+ }
290
+ get isGlobalCommand() {
291
+ return this instanceof GlobalCommand;
292
+ }
293
+ hasOption(name) {
294
+ name = name.split(".")[0];
295
+ return this.options.find((option) => {
296
+ return option.names.includes(name);
297
+ });
298
+ }
299
+ outputHelp() {
300
+ const { name, commands } = this.cli;
301
+ const {
302
+ versionNumber,
303
+ options: globalOptions,
304
+ helpCallback
305
+ } = this.cli.globalCommand;
306
+ let sections = [
307
+ {
308
+ body: `${name}${versionNumber ? `/${versionNumber}` : ""}`
309
+ }
310
+ ];
311
+ sections.push({
312
+ title: "Usage",
313
+ body: ` $ ${name} ${this.usageText || this.rawName}`
314
+ });
315
+ const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
316
+ if (showCommands) {
317
+ const longestCommandName = findLongest(commands.map((command) => command.rawName));
318
+ sections.push({
319
+ title: "Commands",
320
+ body: commands.map((command) => {
321
+ return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
322
+ }).join("\n")
323
+ });
324
+ sections.push({
325
+ title: `For more info, run any command with the \`--help\` flag`,
326
+ body: commands.map((command) => ` $ ${name}${command.name === "" ? "" : ` ${command.name}`} --help`).join("\n")
327
+ });
328
+ }
329
+ let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
330
+ if (!this.isGlobalCommand && !this.isDefaultCommand) {
331
+ options = options.filter((option) => option.name !== "version");
332
+ }
333
+ if (options.length > 0) {
334
+ const longestOptionName = findLongest(options.map((option) => option.rawName));
335
+ sections.push({
336
+ title: "Options",
337
+ body: options.map((option) => {
338
+ return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === void 0 ? "" : `(default: ${option.config.default})`}`;
339
+ }).join("\n")
340
+ });
341
+ }
342
+ if (this.examples.length > 0) {
343
+ sections.push({
344
+ title: "Examples",
345
+ body: this.examples.map((example) => {
346
+ if (typeof example === "function") {
347
+ return example(name);
348
+ }
349
+ return example;
350
+ }).join("\n")
351
+ });
352
+ }
353
+ if (helpCallback) {
354
+ sections = helpCallback(sections) || sections;
355
+ }
356
+ console.log(sections.map((section) => {
357
+ return section.title ? `${section.title}:
358
+ ${section.body}` : section.body;
359
+ }).join("\n\n"));
360
+ }
361
+ outputVersion() {
362
+ const { name } = this.cli;
363
+ const { versionNumber } = this.cli.globalCommand;
364
+ if (versionNumber) {
365
+ console.log(`${name}/${versionNumber} ${platformInfo}`);
366
+ }
367
+ }
368
+ checkRequiredArgs() {
369
+ const minimalArgsCount = this.args.filter((arg) => arg.required).length;
370
+ if (this.cli.args.length < minimalArgsCount) {
371
+ throw new CACError(`missing required args for command \`${this.rawName}\``);
372
+ }
373
+ }
374
+ checkUnknownOptions() {
375
+ const { options, globalCommand } = this.cli;
376
+ if (!this.config.allowUnknownOptions) {
377
+ for (const name of Object.keys(options)) {
378
+ if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
379
+ throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
380
+ }
381
+ }
382
+ }
383
+ }
384
+ checkOptionValue() {
385
+ const { options: parsedOptions, globalCommand } = this.cli;
386
+ const options = [...globalCommand.options, ...this.options];
387
+ for (const option of options) {
388
+ const value = parsedOptions[option.name.split(".")[0]];
389
+ if (option.required) {
390
+ const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
391
+ if (value === true || value === false && !hasNegated) {
392
+ throw new CACError(`option \`${option.rawName}\` value is missing`);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ };
398
+ var GlobalCommand = class extends Command {
399
+ constructor(cli2) {
400
+ super("@@global@@", "", {}, cli2);
401
+ }
402
+ };
403
+ var __assign = Object.assign;
404
+ var CAC = class extends EventEmitter {
405
+ constructor(name = "") {
406
+ super();
407
+ this.name = name;
408
+ this.commands = [];
409
+ this.rawArgs = [];
410
+ this.args = [];
411
+ this.options = {};
412
+ this.globalCommand = new GlobalCommand(this);
413
+ this.globalCommand.usage("<command> [options]");
414
+ }
415
+ usage(text) {
416
+ this.globalCommand.usage(text);
417
+ return this;
418
+ }
419
+ command(rawName, description, config) {
420
+ const command = new Command(rawName, description || "", config, this);
421
+ command.globalCommand = this.globalCommand;
422
+ this.commands.push(command);
423
+ return command;
424
+ }
425
+ option(rawName, description, config) {
426
+ this.globalCommand.option(rawName, description, config);
427
+ return this;
428
+ }
429
+ help(callback) {
430
+ this.globalCommand.option("-h, --help", "Display this message");
431
+ this.globalCommand.helpCallback = callback;
432
+ this.showHelpOnExit = true;
433
+ return this;
434
+ }
435
+ version(version2, customFlags = "-v, --version") {
436
+ this.globalCommand.version(version2, customFlags);
437
+ this.showVersionOnExit = true;
438
+ return this;
439
+ }
440
+ example(example) {
441
+ this.globalCommand.example(example);
442
+ return this;
443
+ }
444
+ outputHelp() {
445
+ if (this.matchedCommand) {
446
+ this.matchedCommand.outputHelp();
447
+ } else {
448
+ this.globalCommand.outputHelp();
449
+ }
450
+ }
451
+ outputVersion() {
452
+ this.globalCommand.outputVersion();
453
+ }
454
+ setParsedInfo({ args, options }, matchedCommand, matchedCommandName) {
455
+ this.args = args;
456
+ this.options = options;
457
+ if (matchedCommand) {
458
+ this.matchedCommand = matchedCommand;
459
+ }
460
+ if (matchedCommandName) {
461
+ this.matchedCommandName = matchedCommandName;
462
+ }
463
+ return this;
464
+ }
465
+ unsetMatchedCommand() {
466
+ this.matchedCommand = void 0;
467
+ this.matchedCommandName = void 0;
468
+ }
469
+ parse(argv = processArgs, {
470
+ run = true
471
+ } = {}) {
472
+ this.rawArgs = argv;
473
+ if (!this.name) {
474
+ this.name = argv[1] ? getFileName(argv[1]) : "cli";
475
+ }
476
+ let shouldParse = true;
477
+ for (const command of this.commands) {
478
+ const parsed = this.mri(argv.slice(2), command);
479
+ const commandName = parsed.args[0];
480
+ if (command.isMatched(commandName)) {
481
+ shouldParse = false;
482
+ const parsedInfo = __assign(__assign({}, parsed), {
483
+ args: parsed.args.slice(1)
484
+ });
485
+ this.setParsedInfo(parsedInfo, command, commandName);
486
+ this.emit(`command:${commandName}`, command);
487
+ }
488
+ }
489
+ if (shouldParse) {
490
+ for (const command of this.commands) {
491
+ if (command.name === "") {
492
+ shouldParse = false;
493
+ const parsed = this.mri(argv.slice(2), command);
494
+ this.setParsedInfo(parsed, command);
495
+ this.emit(`command:!`, command);
496
+ }
497
+ }
498
+ }
499
+ if (shouldParse) {
500
+ const parsed = this.mri(argv.slice(2));
501
+ this.setParsedInfo(parsed);
502
+ }
503
+ if (this.options.help && this.showHelpOnExit) {
504
+ this.outputHelp();
505
+ run = false;
506
+ this.unsetMatchedCommand();
507
+ }
508
+ if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
509
+ this.outputVersion();
510
+ run = false;
511
+ this.unsetMatchedCommand();
512
+ }
513
+ const parsedArgv = { args: this.args, options: this.options };
514
+ if (run) {
515
+ this.runMatchedCommand();
516
+ }
517
+ if (!this.matchedCommand && this.args[0]) {
518
+ this.emit("command:*");
519
+ }
520
+ return parsedArgv;
521
+ }
522
+ mri(argv, command) {
523
+ const cliOptions = [
524
+ ...this.globalCommand.options,
525
+ ...command ? command.options : []
526
+ ];
527
+ const mriOptions = getMriOptions(cliOptions);
528
+ let argsAfterDoubleDashes = [];
529
+ const doubleDashesIndex = argv.indexOf("--");
530
+ if (doubleDashesIndex > -1) {
531
+ argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
532
+ argv = argv.slice(0, doubleDashesIndex);
533
+ }
534
+ let parsed = mri2(argv, mriOptions);
535
+ parsed = Object.keys(parsed).reduce((res, name) => {
536
+ return __assign(__assign({}, res), {
537
+ [camelcaseOptionName(name)]: parsed[name]
538
+ });
539
+ }, { _: [] });
540
+ const args = parsed._;
541
+ const options = {
542
+ "--": argsAfterDoubleDashes
543
+ };
544
+ const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
545
+ let transforms = /* @__PURE__ */ Object.create(null);
546
+ for (const cliOption of cliOptions) {
547
+ if (!ignoreDefault && cliOption.config.default !== void 0) {
548
+ for (const name of cliOption.names) {
549
+ options[name] = cliOption.config.default;
550
+ }
551
+ }
552
+ if (Array.isArray(cliOption.config.type)) {
553
+ if (transforms[cliOption.name] === void 0) {
554
+ transforms[cliOption.name] = /* @__PURE__ */ Object.create(null);
555
+ transforms[cliOption.name]["shouldTransform"] = true;
556
+ transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
557
+ }
558
+ }
559
+ }
560
+ for (const key of Object.keys(parsed)) {
561
+ if (key !== "_") {
562
+ const keys = key.split(".");
563
+ setDotProp(options, keys, parsed[key]);
564
+ setByType(options, transforms);
565
+ }
566
+ }
567
+ return {
568
+ args,
569
+ options
570
+ };
571
+ }
572
+ runMatchedCommand() {
573
+ const { args, options, matchedCommand: command } = this;
574
+ if (!command || !command.commandAction)
575
+ return;
576
+ command.checkUnknownOptions();
577
+ command.checkOptionValue();
578
+ command.checkRequiredArgs();
579
+ const actionArgs = [];
580
+ command.args.forEach((arg, index) => {
581
+ if (arg.variadic) {
582
+ actionArgs.push(args.slice(index));
583
+ } else {
584
+ actionArgs.push(args[index]);
585
+ }
586
+ });
587
+ actionArgs.push(options);
588
+ return command.commandAction.apply(this, actionArgs);
589
+ }
590
+ };
591
+ var cac = (name = "") => new CAC(name);
592
+
593
+ // ../core/dist/schema.js
594
+ var SCHEMA_VERSION = 8;
595
+ var SCHEMA_SQL = (
596
+ /* sql */
597
+ `
598
+ PRAGMA journal_mode = WAL;
599
+ PRAGMA foreign_keys = ON;
600
+
601
+ -- Schema/version bookkeeping ------------------------------------------------
602
+ CREATE TABLE IF NOT EXISTS meta (
603
+ key TEXT PRIMARY KEY,
604
+ value TEXT NOT NULL
605
+ );
606
+
607
+ -- An agent *type* (e.g. 'claude-code'). Extensible to other agents. ---------
608
+ CREATE TABLE IF NOT EXISTS agents (
609
+ id TEXT PRIMARY KEY, -- 'claude-code'
610
+ name TEXT NOT NULL, -- 'Claude Code CLI'
611
+ kind TEXT NOT NULL DEFAULT 'cli'
612
+ );
613
+
614
+ -- A configured source = a labeled agent *instance* (e.g. account 'personal' --
615
+ -- of agent 'claude-code' rooted at ~/.claude). Sessions belong to a source. --
616
+ CREATE TABLE IF NOT EXISTS sources (
617
+ id TEXT PRIMARY KEY, -- the (unique) label, e.g. 'personal'
618
+ label TEXT NOT NULL,
619
+ agent_id TEXT NOT NULL REFERENCES agents(id),
620
+ config_dir TEXT
621
+ );
622
+
623
+ -- A project = a working directory a session ran in. ------------------------
624
+ CREATE TABLE IF NOT EXISTS projects (
625
+ id TEXT PRIMARY KEY, -- stable hash of (agent_id, path)
626
+ agent_id TEXT NOT NULL REFERENCES agents(id),
627
+ path TEXT NOT NULL, -- decoded absolute cwd
628
+ encoded_dir TEXT, -- original '-home-...' archive dir name
629
+ first_seen TEXT, -- ISO8601
630
+ last_seen TEXT,
631
+ UNIQUE (agent_id, path)
632
+ );
633
+
634
+ -- A session = one transcript file (sessionId). -----------------------------
635
+ CREATE TABLE IF NOT EXISTS sessions (
636
+ id TEXT PRIMARY KEY, -- sessionId (UUID)
637
+ agent_id TEXT NOT NULL REFERENCES agents(id),
638
+ source_id TEXT REFERENCES sources(id),
639
+ project_id TEXT REFERENCES projects(id),
640
+ slug TEXT,
641
+ ai_title TEXT,
642
+ cli_version TEXT,
643
+ entrypoint TEXT,
644
+ git_branch TEXT,
645
+ is_sidechain INTEGER NOT NULL DEFAULT 0, -- 1 = subagent thread
646
+ spawn_parent_id TEXT, -- structural parent from path (<parent>/subagents/\u2026); raw link hint, set at ingest
647
+ workflow_run_id TEXT, -- for a Workflow-tool subagent: its run id (wf_<id>) from the path; groups fan-out by run
648
+ parent_session_id TEXT REFERENCES sessions(id), -- spawning session (subagents only); derived in rebuildDerived
649
+ parent_turn_id TEXT REFERENCES turns(id), -- the turn that spawned this subagent
650
+ started_at TEXT, -- ISO8601 of first event
651
+ ended_at TEXT, -- ISO8601 of last event
652
+ duration_ms INTEGER,
653
+ event_count INTEGER NOT NULL DEFAULT 0,
654
+ turn_count INTEGER NOT NULL DEFAULT 0
655
+ );
656
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
657
+ CREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_id);
658
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
659
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent_session ON sessions(parent_session_id);
660
+ CREATE INDEX IF NOT EXISTS idx_sessions_workflow_run ON sessions(workflow_run_id);
661
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent_turn ON sessions(parent_turn_id);
662
+
663
+ -- A turn = user prompt -> assistant completion within a session. ------------
664
+ CREATE TABLE IF NOT EXISTS turns (
665
+ id TEXT PRIMARY KEY, -- hash(session_id, seq)
666
+ session_id TEXT NOT NULL REFERENCES sessions(id),
667
+ seq INTEGER NOT NULL, -- 0-based order within session
668
+ user_event_uuid TEXT, -- the originating user event
669
+ prompt_preview TEXT, -- short, for lists (full text in events)
670
+ model TEXT, -- model that answered this turn
671
+ started_at TEXT,
672
+ ended_at TEXT,
673
+ duration_ms INTEGER,
674
+ UNIQUE (session_id, seq)
675
+ );
676
+ CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);
677
+
678
+ -- Every transcript line. uuid is the dedup key across archive versions. -----
679
+ CREATE TABLE IF NOT EXISTS events (
680
+ uuid TEXT PRIMARY KEY,
681
+ session_id TEXT NOT NULL REFERENCES sessions(id),
682
+ turn_id TEXT REFERENCES turns(id),
683
+ parent_uuid TEXT,
684
+ seq INTEGER, -- line order as first observed
685
+ type TEXT NOT NULL, -- user | assistant | system | attachment | ...
686
+ role TEXT, -- user | assistant (message.role)
687
+ timestamp TEXT, -- ISO8601
688
+ model TEXT,
689
+ is_sidechain INTEGER NOT NULL DEFAULT 0,
690
+ is_meta INTEGER NOT NULL DEFAULT 0,
691
+ text TEXT, -- flattened text/thinking for search + preview
692
+ raw_json BLOB NOT NULL, -- original line, verbatim, gzip-compressed (ADR-011); read via unpackRaw()
693
+ source_file TEXT -- archive path the canonical copy came from
694
+ );
695
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
696
+ CREATE INDEX IF NOT EXISTS idx_events_turn ON events(turn_id);
697
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
698
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp);
699
+
700
+ -- Tool invocations (incl. subagents / Task calls). -------------------------
701
+ CREATE TABLE IF NOT EXISTS tool_calls (
702
+ id TEXT PRIMARY KEY, -- tool_use id, or hash
703
+ event_uuid TEXT REFERENCES events(uuid),
704
+ session_id TEXT NOT NULL REFERENCES sessions(id),
705
+ turn_id TEXT REFERENCES turns(id),
706
+ tool_name TEXT NOT NULL, -- Edit, Write, Bash, Skill, Task, ...
707
+ caller TEXT,
708
+ skill_name TEXT, -- when tool_name = Skill
709
+ skill_id TEXT REFERENCES skills(id), -- the specific skill *version* this call fired (content hash); set in rebuildDerived
710
+ agent_type TEXT, -- subagent type (toolUseResult.agentType)
711
+ spawned_session_id TEXT, -- for Task/Agent: the subagent session id ('agent-'||agentId)
712
+ workflow_run_id TEXT, -- for Workflow: the run id (toolUseResult.runId, e.g. wf_<id>) \u2014 ties the run to its subagents
713
+ workflow_name TEXT, -- for Workflow: the workflow name (toolUseResult.workflowName)
714
+ resolved_model TEXT,
715
+ status TEXT, -- success | error | ...
716
+ total_duration_ms INTEGER,
717
+ total_tokens INTEGER,
718
+ total_tool_use_count INTEGER,
719
+ input_json TEXT, -- tool input, verbatim
720
+ result_summary TEXT -- short result projection (no secrets dumped wholesale)
721
+ );
722
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);
723
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);
724
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_skill ON tool_calls(skill_name);
725
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_skill_id ON tool_calls(skill_id);
726
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_spawned ON tool_calls(spawned_session_id);
727
+ CREATE INDEX IF NOT EXISTS idx_tool_calls_workflow_run ON tool_calls(workflow_run_id);
728
+
729
+ -- Skill *versions* (content-addressed). A skill firing injects the full SKILL.md body into the
730
+ -- transcript as an isMeta user event (begins "Base directory for this skill: \u2026", then the body,
731
+ -- then a trailing "ARGUMENTS: \u2026" block). That body is the only real skill content available (the
732
+ -- trace carries no description/created_at). We identify a version by hashing the *normalized* body
733
+ -- (Base-directory line + per-call ARGUMENTS block stripped, trailing whitespace trimmed) so the same
734
+ -- content from either install / any args is one version, and a content change shows as a new row.
735
+ -- Linked from tool_calls.skill_id in rebuildDerived. See plan: content-addressed skill versioning.
736
+ CREATE TABLE IF NOT EXISTS skills (
737
+ id TEXT PRIMARY KEY, -- sha1 of name + NUL + normalized_body
738
+ name TEXT NOT NULL,
739
+ base_dir TEXT, -- last-seen base directory (informational; NOT part of the hash)
740
+ body TEXT NOT NULL, -- normalized SKILL.md body (Base-dir line + ARGUMENTS block stripped)
741
+ summary TEXT, -- first heading/intro line, for list + detail display
742
+ body_bytes INTEGER,
743
+ first_seen TEXT, -- earliest firing timestamp (MIN across firings)
744
+ last_seen TEXT -- latest firing timestamp (MAX across firings)
745
+ );
746
+ CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
747
+
748
+ -- Token usage at the assistant-response grain. Cost derived later from model. --
749
+ -- One API response is logged across multiple JSONL lines (one per content block: text + each
750
+ -- tool_use), and Claude Code stamps the SAME usage object on every line. We dedup at the response
751
+ -- grain via the unique (session_id, message_id) index so a response's tokens are counted ONCE, not
752
+ -- once per content block (which over-counted tokens ~2.3x and cost ~3x). event_uuid stays the PK
753
+ -- (the row is attached to whichever content-block event won the insert). --
754
+ CREATE TABLE IF NOT EXISTS token_usage (
755
+ event_uuid TEXT PRIMARY KEY REFERENCES events(uuid),
756
+ session_id TEXT NOT NULL REFERENCES sessions(id),
757
+ turn_id TEXT REFERENCES turns(id),
758
+ message_id TEXT, -- Anthropic response id (msg_\u2026); dedup key
759
+ model TEXT,
760
+ input_tokens INTEGER NOT NULL DEFAULT 0,
761
+ output_tokens INTEGER NOT NULL DEFAULT 0,
762
+ cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,
763
+ cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,
764
+ service_tier TEXT
765
+ );
766
+ CREATE INDEX IF NOT EXISTS idx_token_usage_session ON token_usage(session_id);
767
+ -- Partial so rare usage rows lacking a message_id (e.g. <synthetic>) don't collide on NULL. --
768
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_token_usage_msg
769
+ ON token_usage(session_id, message_id) WHERE message_id IS NOT NULL;
770
+
771
+ -- Heuristic classification (category + complexity), re-runnable. -----------
772
+ -- scope: 'session' | 'turn'; target_id references that entity. -------------
773
+ CREATE TABLE IF NOT EXISTS classifications (
774
+ scope TEXT NOT NULL, -- 'session' | 'turn'
775
+ target_id TEXT NOT NULL,
776
+ category TEXT, -- feature | bugfix | refactor | docs | ops | review | ...
777
+ complexity_score REAL,
778
+ complexity_band TEXT, -- trivial | small | medium | large | xl
779
+ signals_json TEXT, -- inputs used, for transparency/repro
780
+ classifier_version INTEGER NOT NULL DEFAULT 1,
781
+ PRIMARY KEY (scope, target_id)
782
+ );
783
+
784
+ -- Idempotent ingest bookkeeping per archive file. --------------------------
785
+ CREATE TABLE IF NOT EXISTS ingest_state (
786
+ file_path TEXT PRIMARY KEY,
787
+ size INTEGER,
788
+ mtime_ms INTEGER,
789
+ sha256 TEXT,
790
+ events_ingested INTEGER NOT NULL DEFAULT 0,
791
+ ingested_at TEXT
792
+ );
793
+
794
+ -- Full-text search over event text (contentless external-content FTS). ------
795
+ CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
796
+ text,
797
+ content='events',
798
+ content_rowid='rowid'
799
+ );
800
+
801
+ -- Keep FTS in sync with events. --------------------------------------------
802
+ CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
803
+ INSERT INTO events_fts(rowid, text) VALUES (new.rowid, new.text);
804
+ END;
805
+ CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
806
+ INSERT INTO events_fts(events_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
807
+ END;
808
+ CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events
809
+ WHEN old.text IS NOT new.text BEGIN
810
+ INSERT INTO events_fts(events_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
811
+ INSERT INTO events_fts(rowid, text) VALUES (new.rowid, new.text);
812
+ END;
813
+ `
814
+ );
815
+
816
+ // ../core/dist/pricing.js
817
+ var PRICE_TABLE = {
818
+ // Opus 4.0 / 4.1 launched at $15/$75; Opus 4.5 cut the Opus tier to $5/$25 and 4.6/4.7/4.8 kept it.
819
+ // The bare 'claude-opus-4' fallback stays at the legacy rate for 4.0/4.1; 4.5+ override below.
820
+ "claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
821
+ "claude-opus-4-5": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
822
+ "claude-opus-4-6": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
823
+ "claude-opus-4-7": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
824
+ "claude-opus-4-8": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
825
+ "claude-fable-5": { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1 },
826
+ "claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
827
+ "claude-haiku-4": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
828
+ "claude-3-5-haiku": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },
829
+ "claude-3-5-sonnet": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
830
+ };
831
+ function rateForModel(model) {
832
+ if (!model)
833
+ return null;
834
+ let best = null;
835
+ for (const [prefix, rate] of Object.entries(PRICE_TABLE)) {
836
+ if (model.startsWith(prefix) && (!best || prefix.length > best.len)) {
837
+ best = { len: prefix.length, rate };
838
+ }
839
+ }
840
+ return best?.rate ?? null;
841
+ }
842
+ function costForUsage(model, u) {
843
+ const r = rateForModel(model);
844
+ if (!r)
845
+ return 0;
846
+ const M = 1e6;
847
+ return (u.input_tokens * r.input + u.output_tokens * r.output + u.cache_creation_input_tokens * r.cacheWrite + u.cache_read_input_tokens * r.cacheRead) / M;
848
+ }
849
+
850
+ // ../core/dist/markdown.js
851
+ function fence(s) {
852
+ return s.length > 2e3 ? s.slice(0, 2e3) + "\n\u2026 (truncated)" : s;
853
+ }
854
+ function sessionToMarkdown(s, events) {
855
+ const out = [];
856
+ out.push(`# ${s.title || "Session " + s.id.slice(0, 8)}`);
857
+ out.push("");
858
+ const meta = [
859
+ ["Session", s.id],
860
+ ["Source", s.source],
861
+ ["Project", s.project],
862
+ ["Model", s.model],
863
+ ["Started", s.started_at],
864
+ ["Ended", s.ended_at]
865
+ ].filter(([, v]) => v);
866
+ for (const [k, v] of meta)
867
+ out.push(`- **${k}:** ${v}`);
868
+ out.push("");
869
+ out.push("---");
870
+ out.push("");
871
+ for (const e of events) {
872
+ const who = e.role || e.type;
873
+ const ts = e.timestamp ? ` _(${e.timestamp})_` : "";
874
+ if (e.thinking) {
875
+ out.push(`### \u{1F9E0} thinking${ts}`);
876
+ out.push("");
877
+ out.push(fence(e.thinking));
878
+ out.push("");
879
+ }
880
+ if (e.text) {
881
+ const icon = who === "user" ? "\u{1F464}" : who === "assistant" ? "\u{1F916}" : "\u2699\uFE0F";
882
+ out.push(`### ${icon} ${who}${ts}`);
883
+ out.push("");
884
+ out.push(fence(e.text));
885
+ out.push("");
886
+ }
887
+ for (const tc of e.toolCalls) {
888
+ const label = tc.skill_name ? `Skill: ${tc.skill_name}` : tc.agent_type ? `${tc.tool_name} \u2192 ${tc.agent_type}` : tc.tool_name;
889
+ const status = tc.status ? ` _(${tc.status})_` : "";
890
+ out.push(`> \u{1F527} **${label}**${status}`);
891
+ if (tc.input_json && tc.input_json !== "{}") {
892
+ out.push("```json");
893
+ out.push(fence(tc.input_json));
894
+ out.push("```");
895
+ }
896
+ out.push("");
897
+ }
898
+ }
899
+ return out.join("\n");
900
+ }
901
+
902
+ // ../core/dist/rawjson.js
903
+ import { gzipSync, gunzipSync } from "zlib";
904
+ function packRaw(json) {
905
+ return gzipSync(Buffer.from(json, "utf8"));
906
+ }
907
+ function unpackRaw(value) {
908
+ if (Buffer.isBuffer(value)) {
909
+ if (value.length >= 2 && value[0] === 31 && value[1] === 139) {
910
+ return gunzipSync(value).toString("utf8");
911
+ }
912
+ return value.toString("utf8");
913
+ }
914
+ return value;
915
+ }
916
+
917
+ // ../core/dist/paths.js
918
+ import { existsSync, readFileSync } from "fs";
919
+ import { dirname, join, resolve } from "path";
920
+ import { fileURLToPath } from "url";
921
+ import { homedir, platform } from "os";
922
+ function expandPath(p) {
923
+ if (!p)
924
+ return p;
925
+ let out = p;
926
+ if (out === "~")
927
+ out = homedir();
928
+ else if (out.startsWith("~/"))
929
+ out = join(homedir(), out.slice(2));
930
+ out = out.replace(/\$HOME/g, homedir());
931
+ return resolve(out);
932
+ }
933
+ var moduleDir = dirname(fileURLToPath(import.meta.url));
934
+ function findRepoRoot(from = moduleDir) {
935
+ let dir = resolve(from);
936
+ for (; ; ) {
937
+ if (existsSync(join(dir, "pnpm-workspace.yaml"))) {
938
+ try {
939
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
940
+ if (pkg?.name === "agent-lens")
941
+ return dir;
942
+ } catch {
943
+ }
944
+ }
945
+ const parent = dirname(dir);
946
+ if (parent === dir)
947
+ return null;
948
+ dir = parent;
949
+ }
950
+ }
951
+ function userDataDir() {
952
+ const home = homedir();
953
+ if (platform() === "win32") {
954
+ return join(process.env.LOCALAPPDATA || join(home, "AppData", "Local"), "agent-lens");
955
+ }
956
+ if (platform() === "darwin") {
957
+ return join(home, "Library", "Application Support", "agent-lens");
958
+ }
959
+ return join(process.env.XDG_DATA_HOME || join(home, ".local", "share"), "agent-lens");
960
+ }
961
+ function resolveDataDir(repoRoot = findRepoRoot()) {
962
+ if (process.env.AGENT_LENS_DATA)
963
+ return resolve(process.env.AGENT_LENS_DATA);
964
+ if (repoRoot)
965
+ return join(repoRoot, "data");
966
+ return userDataDir();
967
+ }
968
+ function resolveConfigFile(repoRoot, dataDir) {
969
+ const candidates = [
970
+ process.env.AGENT_LENS_CONFIG,
971
+ join(dataDir, "agent-lens.config.json"),
972
+ repoRoot ? join(repoRoot, "agent-lens.config.json") : null,
973
+ repoRoot ? join(repoRoot, "agent-lens.config.example.json") : null
974
+ ].filter((x) => Boolean(x));
975
+ for (const f of candidates)
976
+ if (existsSync(f))
977
+ return f;
978
+ return null;
979
+ }
980
+ function resolveWebDist(fromDir, repoRoot = findRepoRoot(fromDir)) {
981
+ const installed = resolve(fromDir, "../web");
982
+ if (existsSync(installed))
983
+ return installed;
984
+ const devSibling = resolve(fromDir, "../../web/dist");
985
+ if (existsSync(devSibling))
986
+ return devSibling;
987
+ if (repoRoot)
988
+ return join(repoRoot, "packages/web/dist");
989
+ return installed;
990
+ }
991
+
992
+ // ../core/dist/sources.js
993
+ import { readFileSync as readFileSync2 } from "fs";
994
+ function readConfig() {
995
+ const repoRoot = findRepoRoot();
996
+ const dataDir = resolveDataDir(repoRoot);
997
+ const file = resolveConfigFile(repoRoot, dataDir);
998
+ return file ? JSON.parse(readFileSync2(file, "utf8")) : null;
999
+ }
1000
+ function loadSources() {
1001
+ if (process.env.CLAUDE_DIR) {
1002
+ return [
1003
+ {
1004
+ label: process.env.AGENT_LENS_LABEL || "default",
1005
+ agent: "claude-code",
1006
+ configDir: expandPath(process.env.CLAUDE_DIR)
1007
+ }
1008
+ ];
1009
+ }
1010
+ const cfg = readConfig() ?? { sources: [{ label: "default", agent: "claude-code", configDir: "$HOME/.claude" }] };
1011
+ const seen = /* @__PURE__ */ new Set();
1012
+ const out = [];
1013
+ for (const raw of cfg.sources || []) {
1014
+ const s = raw;
1015
+ if (!s || !s.label || !s.configDir)
1016
+ continue;
1017
+ if (seen.has(s.label))
1018
+ throw new Error(`duplicate source label: ${s.label}`);
1019
+ seen.add(s.label);
1020
+ out.push({ label: s.label, agent: s.agent || "claude-code", configDir: expandPath(s.configDir) });
1021
+ }
1022
+ if (!out.length)
1023
+ throw new Error("no valid sources configured");
1024
+ return out;
1025
+ }
1026
+ function loadExcludes() {
1027
+ const fromEnv = (process.env.AGENT_LENS_EXCLUDE || "").split(",");
1028
+ let fromCfg = [];
1029
+ if (!process.env.CLAUDE_DIR) {
1030
+ const cfg = readConfig();
1031
+ if (cfg && Array.isArray(cfg.exclude))
1032
+ fromCfg = cfg.exclude;
1033
+ }
1034
+ const seen = /* @__PURE__ */ new Set();
1035
+ const out = [];
1036
+ for (const p of [...fromCfg, ...fromEnv]) {
1037
+ const e = expandPath(String(p ?? "").trim());
1038
+ if (e && !seen.has(e)) {
1039
+ seen.add(e);
1040
+ out.push(e);
1041
+ }
1042
+ }
1043
+ return out;
1044
+ }
1045
+
1046
+ // ../core/dist/projects.js
1047
+ function encodeProjectPath(p) {
1048
+ return p.replace(/[/.]/g, "-").replace(/-+$/, "");
1049
+ }
1050
+
1051
+ // ../core/dist/collect.js
1052
+ import { appendFileSync, closeSync, copyFileSync, existsSync as existsSync2, mkdirSync, openSync, readSync, readdirSync, statSync, unlinkSync, utimesSync, writeSync, chmodSync } from "fs";
1053
+ import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
1054
+ import { platform as platform2 } from "os";
1055
+ var POSIX = platform2() !== "win32";
1056
+ var DIR_MODE = 448;
1057
+ var FILE_MODE = 384;
1058
+ var CHUNK = 1 << 16;
1059
+ function collectTimestamp(d = /* @__PURE__ */ new Date()) {
1060
+ const p = (n, w = 2) => String(n).padStart(w, "0");
1061
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}T${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`;
1062
+ }
1063
+ function ensureDir(dir) {
1064
+ mkdirSync(dir, { recursive: true, mode: DIR_MODE });
1065
+ }
1066
+ function syncMeta(dst, src) {
1067
+ if (POSIX) {
1068
+ try {
1069
+ chmodSync(dst, FILE_MODE);
1070
+ } catch {
1071
+ }
1072
+ }
1073
+ try {
1074
+ utimesSync(dst, src.atime, src.mtime);
1075
+ } catch {
1076
+ }
1077
+ }
1078
+ function copyWhole(src, dst, srcStat) {
1079
+ ensureDir(dirname2(dst));
1080
+ copyFileSync(src, dst);
1081
+ syncMeta(dst, srcStat);
1082
+ }
1083
+ function prefixEquals(src, arc, len) {
1084
+ const sf = openSync(src, "r");
1085
+ const af = openSync(arc, "r");
1086
+ try {
1087
+ const sb = Buffer.allocUnsafe(CHUNK);
1088
+ const ab = Buffer.allocUnsafe(CHUNK);
1089
+ let remaining = len;
1090
+ while (remaining > 0) {
1091
+ const want = Math.min(CHUNK, remaining);
1092
+ const sr = readSync(sf, sb, 0, want, null);
1093
+ const ar = readSync(af, ab, 0, want, null);
1094
+ if (sr !== ar || sr === 0)
1095
+ return false;
1096
+ if (Buffer.compare(sb.subarray(0, sr), ab.subarray(0, sr)) !== 0)
1097
+ return false;
1098
+ remaining -= sr;
1099
+ }
1100
+ return remaining === 0;
1101
+ } finally {
1102
+ closeSync(sf);
1103
+ closeSync(af);
1104
+ }
1105
+ }
1106
+ function appendRange(src, arc, from, to) {
1107
+ const sf = openSync(src, "r");
1108
+ const af = openSync(arc, "a");
1109
+ try {
1110
+ const b = Buffer.allocUnsafe(CHUNK);
1111
+ let pos = from;
1112
+ while (pos < to) {
1113
+ const want = Math.min(CHUNK, to - pos);
1114
+ const r = readSync(sf, b, 0, want, pos);
1115
+ if (r === 0)
1116
+ break;
1117
+ writeSync(af, b, 0, r);
1118
+ pos += r;
1119
+ }
1120
+ } finally {
1121
+ closeSync(sf);
1122
+ closeSync(af);
1123
+ }
1124
+ }
1125
+ function filesEqual(a, b) {
1126
+ const sa = statSync(a);
1127
+ const sb = statSync(b);
1128
+ if (sa.size !== sb.size)
1129
+ return false;
1130
+ return prefixEquals(a, b, sa.size);
1131
+ }
1132
+ function latestSnapshot(archive, relParts) {
1133
+ const versRoot = join2(archive, ".versions");
1134
+ if (!existsSync2(versRoot))
1135
+ return null;
1136
+ const stamps = readdirSync(versRoot).sort();
1137
+ for (let i = stamps.length - 1; i >= 0; i--) {
1138
+ const p = join2(versRoot, stamps[i], ...relParts);
1139
+ if (existsSync2(p))
1140
+ return p;
1141
+ }
1142
+ return null;
1143
+ }
1144
+ function snapshot(absPath, relParts, versionsDir, stats) {
1145
+ const dst = join2(versionsDir, ...relParts);
1146
+ ensureDir(dirname2(dst));
1147
+ copyFileSync(absPath, dst);
1148
+ syncMeta(dst, statSync(absPath));
1149
+ stats.snapshots++;
1150
+ }
1151
+ function walkJsonl(dir, out) {
1152
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
1153
+ const p = join2(dir, e.name);
1154
+ if (e.isDirectory())
1155
+ walkJsonl(p, out);
1156
+ else if (e.isFile() && e.name.endsWith(".jsonl"))
1157
+ out.push(p);
1158
+ }
1159
+ }
1160
+ function syncFile(src, dst, relParts, archive, versionsDir, stats) {
1161
+ const srcStat = statSync(src);
1162
+ if (!existsSync2(dst)) {
1163
+ copyWhole(src, dst, srcStat);
1164
+ stats.copied++;
1165
+ return;
1166
+ }
1167
+ const arcStat = statSync(dst);
1168
+ if (srcStat.size === arcStat.size && Math.trunc(srcStat.mtimeMs) === Math.trunc(arcStat.mtimeMs))
1169
+ return;
1170
+ const ssize = srcStat.size;
1171
+ const asize = arcStat.size;
1172
+ if (ssize >= asize && prefixEquals(src, dst, asize)) {
1173
+ if (ssize > asize) {
1174
+ appendRange(src, dst, asize, ssize);
1175
+ stats.appended++;
1176
+ }
1177
+ syncMeta(dst, srcStat);
1178
+ } else if (ssize < asize) {
1179
+ const last = latestSnapshot(archive, relParts);
1180
+ if (!last || !filesEqual(last, src))
1181
+ snapshot(src, relParts, versionsDir, stats);
1182
+ stats.compacted++;
1183
+ } else {
1184
+ snapshot(dst, relParts, versionsDir, stats);
1185
+ copyWhole(src, dst, srcStat);
1186
+ stats.diverged++;
1187
+ }
1188
+ }
1189
+ function removeCredentials(dir) {
1190
+ if (!existsSync2(dir))
1191
+ return;
1192
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
1193
+ const p = join2(dir, e.name);
1194
+ if (e.isDirectory())
1195
+ removeCredentials(p);
1196
+ else if (e.name === ".credentials.json") {
1197
+ try {
1198
+ unlinkSync(p);
1199
+ } catch {
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+ function collectSource(source, archiveBase, excludedDirs, runTs, stats, log) {
1205
+ const { label, configDir } = source;
1206
+ const archive = join2(archiveBase, label);
1207
+ const versionsDir = join2(archive, ".versions", runTs);
1208
+ if (!existsSync2(configDir)) {
1209
+ log(`agent-lens: [${label}] source not found: ${configDir} (skipping)`);
1210
+ return;
1211
+ }
1212
+ ensureDir(join2(archive, "projects"));
1213
+ ensureDir(join2(archive, "settings"));
1214
+ const files = [];
1215
+ const projectsDir = join2(configDir, "projects");
1216
+ if (existsSync2(projectsDir))
1217
+ walkJsonl(projectsDir, files);
1218
+ const historySrc = join2(configDir, "history.jsonl");
1219
+ if (existsSync2(historySrc))
1220
+ files.push(historySrc);
1221
+ const before = { scanned: stats.scanned, snapshots: stats.snapshots };
1222
+ for (const src of files) {
1223
+ const base = basename(src);
1224
+ if (base === ".credentials.json" || base.endsWith(".lock"))
1225
+ continue;
1226
+ const relParts = relative(configDir, src).split(sep);
1227
+ const relPosix = relParts.join("/");
1228
+ if (excludedDirs.some((enc) => relPosix.startsWith(`projects/${enc}/`)))
1229
+ continue;
1230
+ stats.scanned++;
1231
+ syncFile(src, join2(archive, ...relParts), relParts, archive, versionsDir, stats);
1232
+ }
1233
+ for (const f of ["settings.json", "settings.local.json"]) {
1234
+ const s = join2(configDir, f);
1235
+ if (!existsSync2(s))
1236
+ continue;
1237
+ const a = join2(archive, "settings", f);
1238
+ if (existsSync2(a) && !filesEqual(s, a))
1239
+ snapshot(a, ["settings", f], versionsDir, stats);
1240
+ ensureDir(dirname2(a));
1241
+ copyFileSync(s, a);
1242
+ syncMeta(a, statSync(s));
1243
+ }
1244
+ removeCredentials(archive);
1245
+ const scanned = stats.scanned - before.scanned;
1246
+ const snapshots = stats.snapshots - before.snapshots;
1247
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} run=${runTs} source=${label} scanned=${scanned} snapshots=${snapshots} archive=${archive}`;
1248
+ log(`agent-lens: ${line}`);
1249
+ try {
1250
+ appendFileSync(join2(archiveBase, ".collect.log"), line + "\n");
1251
+ } catch {
1252
+ }
1253
+ }
1254
+ function collectAll(opts = {}) {
1255
+ const log = opts.log ?? ((m) => console.log(m));
1256
+ const archiveBase = opts.archiveBase ?? join2(resolveDataDir(findRepoRoot()), "archive");
1257
+ const sources = opts.sources ?? loadSources();
1258
+ const excludedDirs = (opts.excludes ?? loadExcludes()).map(encodeProjectPath);
1259
+ const runTs = opts.runTimestamp ?? collectTimestamp();
1260
+ ensureDir(archiveBase);
1261
+ const stats = { sources: 0, scanned: 0, copied: 0, appended: 0, diverged: 0, compacted: 0, snapshots: 0 };
1262
+ if (!sources.length)
1263
+ throw new Error("no sources configured");
1264
+ for (const source of sources) {
1265
+ stats.sources++;
1266
+ collectSource(source, archiveBase, excludedDirs, runTs, stats, log);
1267
+ }
1268
+ return stats;
1269
+ }
1270
+
1271
+ // ../core/dist/lock.js
1272
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync } from "fs";
1273
+ import { dirname as dirname3 } from "path";
1274
+ import { hostname } from "os";
1275
+ function isAlive(pid) {
1276
+ if (!pid)
1277
+ return false;
1278
+ try {
1279
+ process.kill(pid, 0);
1280
+ return true;
1281
+ } catch (e) {
1282
+ return e.code === "EPERM";
1283
+ }
1284
+ }
1285
+ function acquireLock(lockPath, staleMs = 6 * 60 * 60 * 1e3) {
1286
+ mkdirSync2(dirname3(lockPath), { recursive: true });
1287
+ if (existsSync3(lockPath)) {
1288
+ try {
1289
+ const held = JSON.parse(readFileSync3(lockPath, "utf8"));
1290
+ const fresh = typeof held.ts === "number" && Date.now() - held.ts < staleMs;
1291
+ if (fresh && isAlive(held.pid ?? 0))
1292
+ return null;
1293
+ } catch {
1294
+ }
1295
+ }
1296
+ writeFileSync(lockPath, JSON.stringify({ pid: process.pid, ts: Date.now(), host: hostname() }), { mode: 384 });
1297
+ let released = false;
1298
+ const release = () => {
1299
+ if (released)
1300
+ return;
1301
+ released = true;
1302
+ try {
1303
+ const cur = JSON.parse(readFileSync3(lockPath, "utf8"));
1304
+ if (cur.pid === process.pid)
1305
+ unlinkSync2(lockPath);
1306
+ } catch {
1307
+ }
1308
+ };
1309
+ process.once("exit", release);
1310
+ return { release };
1311
+ }
1312
+
1313
+ // ../core/dist/service.js
1314
+ import { spawnSync } from "child_process";
1315
+ import { mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1316
+ import { homedir as homedir2, platform as platform3 } from "os";
1317
+ import { join as join3 } from "path";
1318
+ var DEFAULT_HOURS = [9, 13, 17, 21];
1319
+ var SYSTEMD_COLLECTOR_TIMER = "agent-lens-collect.timer";
1320
+ var SYSTEMD_COLLECTOR_SERVICE = "agent-lens-collect.service";
1321
+ var SYSTEMD_SERVER_SERVICE = "agent-lens-server.service";
1322
+ var LAUNCHD_COLLECTOR_LABEL = "org.agent-lens.collect";
1323
+ var LAUNCHD_SERVER_LABEL = "org.agent-lens.server";
1324
+ var WINDOWS_COLLECTOR_PREFIX = "AgentLens\\Collect";
1325
+ var WINDOWS_SERVER_TASK = "AgentLens\\Server";
1326
+ function parseTargets(spec) {
1327
+ if (!spec || spec === "all")
1328
+ return ["collector", "server"];
1329
+ if (spec === "collector" || spec === "server")
1330
+ return [spec];
1331
+ throw new Error(`invalid target '${spec}' (expected: collector | server | all)`);
1332
+ }
1333
+ function parseHours(spec) {
1334
+ if (!spec)
1335
+ return DEFAULT_HOURS;
1336
+ const hours = spec.split(",").map((s) => Number.parseInt(s.trim(), 10)).filter((n) => Number.isInteger(n) && n >= 0 && n <= 23);
1337
+ if (!hours.length)
1338
+ throw new Error(`invalid hours '${spec}' (expected comma-separated 0-23, e.g. 9,13,17,21)`);
1339
+ return [...new Set(hours)].sort((a, b) => a - b);
1340
+ }
1341
+ function serverEnv() {
1342
+ const env = {};
1343
+ if (process.env.AGENT_LENS_PORT)
1344
+ env.AGENT_LENS_PORT = process.env.AGENT_LENS_PORT;
1345
+ if (process.env.AGENT_LENS_HOST)
1346
+ env.AGENT_LENS_HOST = process.env.AGENT_LENS_HOST;
1347
+ return env;
1348
+ }
1349
+ function onCalendarHours(hours) {
1350
+ return hours.map((h) => String(h).padStart(2, "0")).join(",") + ":00";
1351
+ }
1352
+ function systemdCollectorService(node, cli2) {
1353
+ return `[Unit]
1354
+ Description=Agent Lens \u2014 collect + ingest Claude Code session traces
1355
+
1356
+ [Service]
1357
+ Type=oneshot
1358
+ # Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.
1359
+ ExecStart=${node} ${cli2} collect --then-ingest
1360
+ Nice=10
1361
+ IOSchedulingClass=idle
1362
+ `;
1363
+ }
1364
+ function systemdCollectorTimer(hours) {
1365
+ return `[Unit]
1366
+ Description=Agent Lens \u2014 periodic Claude Code trace collection
1367
+
1368
+ [Timer]
1369
+ # A few times a day. Persistent catches runs missed while the machine was off.
1370
+ OnCalendar=*-*-* ${onCalendarHours(hours)}
1371
+ Persistent=true
1372
+ RandomizedDelaySec=300
1373
+
1374
+ [Install]
1375
+ WantedBy=timers.target
1376
+ `;
1377
+ }
1378
+ function systemdServerService(node, cli2, env = {}) {
1379
+ const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${v}
1380
+ `).join("");
1381
+ return `[Unit]
1382
+ Description=Agent Lens \u2014 local web UI + read-only API server
1383
+ After=network.target
1384
+
1385
+ [Service]
1386
+ Type=simple
1387
+ # Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.
1388
+ ExecStart=${node} ${cli2} serve
1389
+ ${envLines}# Long-running: restart if it crashes.
1390
+ Restart=on-failure
1391
+ RestartSec=5
1392
+ Nice=10
1393
+
1394
+ [Install]
1395
+ WantedBy=default.target
1396
+ `;
1397
+ }
1398
+ function launchdCollectorPlist(node, cli2, hours, logPath) {
1399
+ const cal = hours.map((h) => ` <dict><key>Hour</key><integer>${h}</integer><key>Minute</key><integer>0</integer></dict>`).join("\n");
1400
+ return `<?xml version="1.0" encoding="UTF-8"?>
1401
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1402
+ <plist version="1.0">
1403
+ <dict>
1404
+ <key>Label</key><string>${LAUNCHD_COLLECTOR_LABEL}</string>
1405
+ <key>ProgramArguments</key>
1406
+ <array>
1407
+ <string>${node}</string>
1408
+ <string>${cli2}</string>
1409
+ <string>collect</string>
1410
+ <string>--then-ingest</string>
1411
+ </array>
1412
+ <key>StartCalendarInterval</key>
1413
+ <array>
1414
+ ${cal}
1415
+ </array>
1416
+ <key>StandardOutPath</key><string>${logPath}</string>
1417
+ <key>StandardErrorPath</key><string>${logPath}</string>
1418
+ </dict>
1419
+ </plist>
1420
+ `;
1421
+ }
1422
+ function launchdServerPlist(node, cli2, logPath, env = {}) {
1423
+ const envDict = Object.entries(env).length ? ` <key>EnvironmentVariables</key>
1424
+ <dict>
1425
+ ${Object.entries(env).map(([k, v]) => ` <key>${k}</key><string>${v}</string>`).join("\n")}
1426
+ </dict>
1427
+ ` : "";
1428
+ return `<?xml version="1.0" encoding="UTF-8"?>
1429
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1430
+ <plist version="1.0">
1431
+ <dict>
1432
+ <key>Label</key><string>${LAUNCHD_SERVER_LABEL}</string>
1433
+ <key>ProgramArguments</key>
1434
+ <array>
1435
+ <string>${node}</string>
1436
+ <string>${cli2}</string>
1437
+ <string>serve</string>
1438
+ </array>
1439
+ ${envDict} <key>RunAtLoad</key><true/>
1440
+ <key>KeepAlive</key><true/>
1441
+ <key>StandardOutPath</key><string>${logPath}</string>
1442
+ <key>StandardErrorPath</key><string>${logPath}</string>
1443
+ </dict>
1444
+ </plist>
1445
+ `;
1446
+ }
1447
+ function sh(cmd, args, quiet = false) {
1448
+ const r = spawnSync(cmd, args, { stdio: quiet ? "ignore" : "inherit", encoding: "utf8" });
1449
+ return r.status === 0;
1450
+ }
1451
+ function have(cmd) {
1452
+ const r = spawnSync(cmd, ["--version"], { stdio: "ignore" });
1453
+ return r.status === 0 || r.error === void 0;
1454
+ }
1455
+ function systemdUnitDir() {
1456
+ return join3(process.env.XDG_CONFIG_HOME || join3(homedir2(), ".config"), "systemd", "user");
1457
+ }
1458
+ function linuxInstall(targets, node, cli2, hours) {
1459
+ if (!have("systemctl")) {
1460
+ console.error("agent-lens service: systemctl not found \u2014 is this a systemd system?");
1461
+ process.exitCode = 1;
1462
+ return;
1463
+ }
1464
+ const dir = systemdUnitDir();
1465
+ mkdirSync3(dir, { recursive: true });
1466
+ if (targets.includes("collector")) {
1467
+ writeFileSync2(join3(dir, SYSTEMD_COLLECTOR_SERVICE), systemdCollectorService(node, cli2));
1468
+ writeFileSync2(join3(dir, SYSTEMD_COLLECTOR_TIMER), systemdCollectorTimer(hours));
1469
+ }
1470
+ if (targets.includes("server")) {
1471
+ writeFileSync2(join3(dir, SYSTEMD_SERVER_SERVICE), systemdServerService(node, cli2, serverEnv()));
1472
+ }
1473
+ sh("systemctl", ["--user", "daemon-reload"]);
1474
+ if (targets.includes("collector")) {
1475
+ sh("systemctl", ["--user", "enable", "--now", SYSTEMD_COLLECTOR_TIMER]);
1476
+ console.log(`agent-lens service: installed collector timer (${onCalendarHours(hours)})`);
1477
+ sh("systemctl", ["--user", "list-timers", SYSTEMD_COLLECTOR_TIMER, "--no-pager"]);
1478
+ }
1479
+ if (targets.includes("server")) {
1480
+ sh("systemctl", ["--user", "enable", "--now", SYSTEMD_SERVER_SERVICE]);
1481
+ console.log("agent-lens service: installed server daemon");
1482
+ sh("systemctl", ["--user", "status", SYSTEMD_SERVER_SERVICE, "--no-pager"]);
1483
+ }
1484
+ if (have("loginctl") && !sh("loginctl", ["enable-linger", process.env.USER || ""], true)) {
1485
+ console.log(`agent-lens: could not enable linger automatically; run: sudo loginctl enable-linger ${process.env.USER}`);
1486
+ }
1487
+ }
1488
+ function linuxUninstall(targets) {
1489
+ if (!have("systemctl"))
1490
+ return;
1491
+ const dir = systemdUnitDir();
1492
+ if (targets.includes("collector")) {
1493
+ sh("systemctl", ["--user", "disable", "--now", SYSTEMD_COLLECTOR_TIMER], true);
1494
+ for (const u of [SYSTEMD_COLLECTOR_TIMER, SYSTEMD_COLLECTOR_SERVICE])
1495
+ rmSync(join3(dir, u), { force: true });
1496
+ sh("systemctl", ["--user", "reset-failed", SYSTEMD_COLLECTOR_SERVICE], true);
1497
+ }
1498
+ if (targets.includes("server")) {
1499
+ sh("systemctl", ["--user", "disable", "--now", SYSTEMD_SERVER_SERVICE], true);
1500
+ rmSync(join3(dir, SYSTEMD_SERVER_SERVICE), { force: true });
1501
+ sh("systemctl", ["--user", "reset-failed", SYSTEMD_SERVER_SERVICE], true);
1502
+ }
1503
+ sh("systemctl", ["--user", "daemon-reload"]);
1504
+ console.log("agent-lens service: uninstalled (archive untouched)");
1505
+ }
1506
+ function linuxStatus(targets) {
1507
+ if (!have("systemctl"))
1508
+ return;
1509
+ if (targets.includes("collector"))
1510
+ sh("systemctl", ["--user", "list-timers", SYSTEMD_COLLECTOR_TIMER, "--no-pager"]);
1511
+ if (targets.includes("server"))
1512
+ sh("systemctl", ["--user", "status", SYSTEMD_SERVER_SERVICE, "--no-pager"]);
1513
+ }
1514
+ function launchdPlistPath(label) {
1515
+ return join3(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
1516
+ }
1517
+ function macBootstrap(plist) {
1518
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
1519
+ if (!sh("launchctl", ["bootstrap", `gui/${uid}`, plist], true))
1520
+ sh("launchctl", ["load", "-w", plist]);
1521
+ }
1522
+ function macBootout(label, plist) {
1523
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
1524
+ if (!sh("launchctl", ["bootout", `gui/${uid}/${label}`], true))
1525
+ sh("launchctl", ["unload", "-w", plist], true);
1526
+ rmSync(plist, { force: true });
1527
+ }
1528
+ function macInstall(targets, node, cli2, hours) {
1529
+ const agents = join3(homedir2(), "Library", "LaunchAgents");
1530
+ mkdirSync3(agents, { recursive: true });
1531
+ const dataDir = resolveDataDir(findRepoRoot());
1532
+ if (targets.includes("collector")) {
1533
+ const plist = launchdPlistPath(LAUNCHD_COLLECTOR_LABEL);
1534
+ writeFileSync2(plist, launchdCollectorPlist(node, cli2, hours, join3(dataDir, "schedule.log")));
1535
+ macBootstrap(plist);
1536
+ console.log(`agent-lens service: installed collector agent ${LAUNCHD_COLLECTOR_LABEL} (hours ${hours.join(",")})`);
1537
+ }
1538
+ if (targets.includes("server")) {
1539
+ const plist = launchdPlistPath(LAUNCHD_SERVER_LABEL);
1540
+ writeFileSync2(plist, launchdServerPlist(node, cli2, join3(dataDir, "server.log"), serverEnv()));
1541
+ macBootstrap(plist);
1542
+ console.log(`agent-lens service: installed server agent ${LAUNCHD_SERVER_LABEL}`);
1543
+ }
1544
+ }
1545
+ function macUninstall(targets) {
1546
+ if (targets.includes("collector"))
1547
+ macBootout(LAUNCHD_COLLECTOR_LABEL, launchdPlistPath(LAUNCHD_COLLECTOR_LABEL));
1548
+ if (targets.includes("server"))
1549
+ macBootout(LAUNCHD_SERVER_LABEL, launchdPlistPath(LAUNCHD_SERVER_LABEL));
1550
+ console.log("agent-lens service: uninstalled (archive untouched)");
1551
+ }
1552
+ function macStatus(targets) {
1553
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
1554
+ if (targets.includes("collector"))
1555
+ sh("launchctl", ["print", `gui/${uid}/${LAUNCHD_COLLECTOR_LABEL}`]);
1556
+ if (targets.includes("server"))
1557
+ sh("launchctl", ["print", `gui/${uid}/${LAUNCHD_SERVER_LABEL}`]);
1558
+ }
1559
+ function winCollectorTaskName(hour) {
1560
+ return `${WINDOWS_COLLECTOR_PREFIX}_${String(hour).padStart(2, "0")}`;
1561
+ }
1562
+ function windowsInstall(targets, node, cli2, hours) {
1563
+ if (targets.includes("collector")) {
1564
+ const tr = `"${node}" "${cli2}" collect --then-ingest`;
1565
+ for (const h of hours) {
1566
+ sh("schtasks", ["/Create", "/F", "/TN", winCollectorTaskName(h), "/TR", tr, "/SC", "DAILY", "/ST", `${String(h).padStart(2, "0")}:00`]);
1567
+ }
1568
+ console.log(`agent-lens service: installed ${hours.length} collector task(s) (hours ${hours.join(",")})`);
1569
+ }
1570
+ if (targets.includes("server")) {
1571
+ const tr = `"${node}" "${cli2}" serve`;
1572
+ sh("schtasks", ["/Create", "/F", "/TN", WINDOWS_SERVER_TASK, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED"]);
1573
+ console.log("agent-lens service: installed server task (starts at logon)");
1574
+ }
1575
+ }
1576
+ function windowsUninstall(targets) {
1577
+ if (targets.includes("collector")) {
1578
+ for (let h = 0; h < 24; h++)
1579
+ sh("schtasks", ["/Delete", "/F", "/TN", winCollectorTaskName(h)], true);
1580
+ }
1581
+ if (targets.includes("server"))
1582
+ sh("schtasks", ["/Delete", "/F", "/TN", WINDOWS_SERVER_TASK], true);
1583
+ console.log("agent-lens service: uninstalled (archive untouched)");
1584
+ }
1585
+ function windowsStatus(targets) {
1586
+ if (targets.includes("collector")) {
1587
+ sh("schtasks", ["/Query", "/FO", "LIST", "/TN", WINDOWS_COLLECTOR_PREFIX], true) || console.log("agent-lens service: no collector tasks found under " + WINDOWS_COLLECTOR_PREFIX);
1588
+ }
1589
+ if (targets.includes("server")) {
1590
+ sh("schtasks", ["/Query", "/FO", "LIST", "/TN", WINDOWS_SERVER_TASK], true) || console.log("agent-lens service: no server task found (" + WINDOWS_SERVER_TASK + ")");
1591
+ }
1592
+ }
1593
+ function runService(action, params) {
1594
+ const node = params.node ?? process.execPath;
1595
+ const cli2 = params.cliEntry;
1596
+ const hours = params.hours ?? DEFAULT_HOURS;
1597
+ const targets = params.targets;
1598
+ const plat = platform3();
1599
+ const impl = plat === "linux" ? {
1600
+ install: () => linuxInstall(targets, node, cli2, hours),
1601
+ uninstall: () => linuxUninstall(targets),
1602
+ status: () => linuxStatus(targets)
1603
+ } : plat === "darwin" ? {
1604
+ install: () => macInstall(targets, node, cli2, hours),
1605
+ uninstall: () => macUninstall(targets),
1606
+ status: () => macStatus(targets)
1607
+ } : plat === "win32" ? {
1608
+ install: () => windowsInstall(targets, node, cli2, hours),
1609
+ uninstall: () => windowsUninstall(targets),
1610
+ status: () => windowsStatus(targets)
1611
+ } : null;
1612
+ if (!impl) {
1613
+ console.error(`agent-lens service: unsupported platform '${plat}'`);
1614
+ process.exitCode = 1;
1615
+ return;
1616
+ }
1617
+ if (action === "install")
1618
+ impl.install();
1619
+ else if (action === "uninstall")
1620
+ impl.uninstall();
1621
+ else if (action === "status")
1622
+ impl.status();
1623
+ else {
1624
+ console.error(`agent-lens service: unknown action '${action}' (use: install | uninstall | status)`);
1625
+ process.exitCode = 1;
1626
+ }
1627
+ }
1628
+
1629
+ // ../ingest/dist/run.js
1630
+ import { readFileSync as readFileSync4, statSync as statSync2, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
1631
+ import { dirname as dirname4, join as join5 } from "path";
1632
+
1633
+ // ../ingest/dist/db.js
1634
+ import Database from "better-sqlite3";
1635
+ function openRaw(file) {
1636
+ const db = new Database(file);
1637
+ db.pragma("journal_mode = WAL");
1638
+ db.pragma("foreign_keys = ON");
1639
+ return db;
1640
+ }
1641
+ function applySchema(db) {
1642
+ db.exec(SCHEMA_SQL);
1643
+ db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(String(SCHEMA_VERSION));
1644
+ }
1645
+ function openDb(file) {
1646
+ const db = openRaw(file);
1647
+ applySchema(db);
1648
+ return db;
1649
+ }
1650
+ function resetSchema(db) {
1651
+ db.pragma("foreign_keys = OFF");
1652
+ db.exec(`
1653
+ DROP TRIGGER IF EXISTS events_au;
1654
+ DROP TRIGGER IF EXISTS events_ad;
1655
+ DROP TRIGGER IF EXISTS events_ai;
1656
+ DROP TABLE IF EXISTS events_fts;
1657
+ DROP TABLE IF EXISTS classifications;
1658
+ DROP TABLE IF EXISTS token_usage;
1659
+ DROP TABLE IF EXISTS tool_calls;
1660
+ DROP TABLE IF EXISTS skills;
1661
+ DROP TABLE IF EXISTS events;
1662
+ DROP TABLE IF EXISTS turns;
1663
+ DROP TABLE IF EXISTS sessions;
1664
+ DROP TABLE IF EXISTS projects;
1665
+ DROP TABLE IF EXISTS sources;
1666
+ DROP TABLE IF EXISTS agents;
1667
+ DROP TABLE IF EXISTS ingest_state;
1668
+ DROP TABLE IF EXISTS meta;
1669
+ `);
1670
+ applySchema(db);
1671
+ db.pragma("foreign_keys = ON");
1672
+ }
1673
+
1674
+ // ../ingest/dist/adapters/claude-code.js
1675
+ import { readdirSync as readdirSync2 } from "fs";
1676
+ import { basename as basename2, join as join4 } from "path";
1677
+ function flattenText(content) {
1678
+ if (content == null)
1679
+ return null;
1680
+ if (typeof content === "string")
1681
+ return content.trim() || null;
1682
+ if (!Array.isArray(content))
1683
+ return null;
1684
+ const parts = [];
1685
+ for (const block of content) {
1686
+ if (!block || typeof block !== "object")
1687
+ continue;
1688
+ const b = block;
1689
+ if (b.type === "text" && typeof b.text === "string")
1690
+ parts.push(b.text);
1691
+ else if (b.type === "thinking" && typeof b.thinking === "string")
1692
+ parts.push(b.thinking);
1693
+ }
1694
+ const text = parts.join("\n").trim();
1695
+ return text || null;
1696
+ }
1697
+ function summarizeResult(content) {
1698
+ let s = null;
1699
+ if (typeof content === "string")
1700
+ s = content;
1701
+ else if (Array.isArray(content)) {
1702
+ const parts = [];
1703
+ for (const block of content) {
1704
+ const b = block;
1705
+ if (b && b.type === "text" && typeof b.text === "string")
1706
+ parts.push(b.text);
1707
+ }
1708
+ s = parts.join("\n");
1709
+ } else if (content != null) {
1710
+ s = JSON.stringify(content);
1711
+ }
1712
+ if (!s)
1713
+ return null;
1714
+ s = s.replace(/\s+/g, " ").trim();
1715
+ return s.slice(0, 280) || null;
1716
+ }
1717
+ function asString(v) {
1718
+ return typeof v === "string" && v.length > 0 ? v : null;
1719
+ }
1720
+ function asInt(v) {
1721
+ return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : 0;
1722
+ }
1723
+ function parentSessionFromPath(path) {
1724
+ const parts = path.split("/");
1725
+ const i = parts.indexOf("subagents");
1726
+ return i > 0 ? parts[i - 1] : null;
1727
+ }
1728
+ function workflowRunFromPath(path) {
1729
+ const parts = path.split("/");
1730
+ const i = parts.indexOf("workflows");
1731
+ return i >= 0 && i + 1 < parts.length ? parts[i + 1] : null;
1732
+ }
1733
+ function recurseJsonl(dir, out) {
1734
+ let entries;
1735
+ try {
1736
+ entries = readdirSync2(dir, { withFileTypes: true });
1737
+ } catch {
1738
+ return;
1739
+ }
1740
+ for (const e of entries) {
1741
+ const p = join4(dir, e.name);
1742
+ if (e.isDirectory())
1743
+ recurseJsonl(p, out);
1744
+ else if (e.isFile() && e.name.endsWith(".jsonl"))
1745
+ out.push(p);
1746
+ }
1747
+ }
1748
+ var ClaudeCodeAdapter = class {
1749
+ agentId = "claude-code";
1750
+ agentName = "Claude Code CLI";
1751
+ discover(sourceArchiveDir, sourceId) {
1752
+ const files = [];
1753
+ const archiveRoot = sourceArchiveDir;
1754
+ const addFrom = (projectsDir, isVersion) => {
1755
+ const found = [];
1756
+ recurseJsonl(projectsDir, found);
1757
+ for (const path of found) {
1758
+ const parts = path.split("/");
1759
+ const sessionId = basename2(path, ".jsonl");
1760
+ const encodedDir = parts[parts.length - 2] ?? "";
1761
+ files.push({
1762
+ path,
1763
+ sessionId,
1764
+ encodedDir,
1765
+ isVersion,
1766
+ sourceId,
1767
+ parentSessionId: parentSessionFromPath(path),
1768
+ workflowRunId: workflowRunFromPath(path)
1769
+ });
1770
+ }
1771
+ };
1772
+ addFrom(join4(archiveRoot, "projects"), false);
1773
+ const versionsRoot = join4(archiveRoot, ".versions");
1774
+ try {
1775
+ for (const ts of readdirSync2(versionsRoot, { withFileTypes: true })) {
1776
+ if (ts.isDirectory())
1777
+ addFrom(join4(versionsRoot, ts.name, "projects"), true);
1778
+ }
1779
+ } catch {
1780
+ }
1781
+ return files;
1782
+ }
1783
+ parseLine(raw, file, seq) {
1784
+ if (!raw || typeof raw !== "object")
1785
+ return {};
1786
+ const r = raw;
1787
+ const meta = {};
1788
+ if (asString(r.cwd))
1789
+ meta.cwd = r.cwd;
1790
+ if (asString(r.slug))
1791
+ meta.slug = r.slug;
1792
+ if (asString(r.version))
1793
+ meta.cli_version = r.version;
1794
+ if (asString(r.entrypoint))
1795
+ meta.entrypoint = r.entrypoint;
1796
+ if (asString(r.gitBranch))
1797
+ meta.git_branch = r.gitBranch;
1798
+ if (typeof r.isSidechain === "boolean")
1799
+ meta.is_sidechain = r.isSidechain;
1800
+ if (r.type === "ai-title" && asString(r.aiTitle))
1801
+ meta.ai_title = r.aiTitle;
1802
+ if (!asString(r.uuid))
1803
+ return { meta };
1804
+ const message = r.message ?? {};
1805
+ const text = flattenText(message.content);
1806
+ const result = {
1807
+ meta,
1808
+ event: {
1809
+ uuid: r.uuid,
1810
+ session_id: file.sessionId,
1811
+ turn_id: null,
1812
+ parent_uuid: asString(r.parentUuid),
1813
+ seq,
1814
+ type: asString(r.type) ?? "unknown",
1815
+ role: asString(message.role),
1816
+ timestamp: asString(r.timestamp),
1817
+ model: asString(message.model),
1818
+ is_sidechain: r.isSidechain ? 1 : 0,
1819
+ is_meta: r.isMeta ? 1 : 0,
1820
+ text,
1821
+ raw_json: JSON.stringify(raw),
1822
+ source_file: file.path
1823
+ }
1824
+ };
1825
+ const usage = message.usage;
1826
+ if (usage) {
1827
+ result.tokenUsage = {
1828
+ event_uuid: r.uuid,
1829
+ session_id: file.sessionId,
1830
+ turn_id: null,
1831
+ message_id: asString(message.id),
1832
+ model: asString(message.model),
1833
+ input_tokens: asInt(usage.input_tokens),
1834
+ output_tokens: asInt(usage.output_tokens),
1835
+ cache_creation_input_tokens: asInt(usage.cache_creation_input_tokens),
1836
+ cache_read_input_tokens: asInt(usage.cache_read_input_tokens),
1837
+ service_tier: asString(usage.service_tier)
1838
+ };
1839
+ }
1840
+ const content = message.content;
1841
+ if (Array.isArray(content)) {
1842
+ const toolCalls = [];
1843
+ const toolResults = [];
1844
+ const tur = r.toolUseResult;
1845
+ for (const block of content) {
1846
+ if (!block || typeof block !== "object")
1847
+ continue;
1848
+ const b = block;
1849
+ if (b.type === "tool_use" && asString(b.id)) {
1850
+ const name = asString(b.name) ?? "unknown";
1851
+ const input = b.input ?? {};
1852
+ toolCalls.push({
1853
+ id: b.id,
1854
+ event_uuid: r.uuid,
1855
+ session_id: file.sessionId,
1856
+ turn_id: null,
1857
+ tool_name: name,
1858
+ caller: asString(b.caller),
1859
+ skill_name: name === "Skill" ? asString(input.skill) ?? asString(input.command) : null,
1860
+ skill_id: null,
1861
+ // content-addressed skill version; linked in rebuildDerived from the injected body
1862
+ agent_type: name === "Task" || name === "Agent" ? asString(input.subagent_type) : null,
1863
+ spawned_session_id: null,
1864
+ // patched in once the tool_result carries toolUseResult.agentId
1865
+ workflow_run_id: null,
1866
+ // patched in from the Workflow tool_result's runId
1867
+ workflow_name: name === "Workflow" ? asString(input.name) : null,
1868
+ // refined from the result
1869
+ resolved_model: null,
1870
+ status: null,
1871
+ total_duration_ms: null,
1872
+ total_tokens: null,
1873
+ total_tool_use_count: null,
1874
+ input_json: JSON.stringify(input),
1875
+ result_summary: null
1876
+ });
1877
+ } else if (b.type === "tool_result" && asString(b.tool_use_id)) {
1878
+ toolResults.push({
1879
+ tool_use_id: b.tool_use_id,
1880
+ result_summary: summarizeResult(b.content),
1881
+ ...tur ? {
1882
+ status: asString(tur.status),
1883
+ agent_type: asString(tur.agentType),
1884
+ // Deterministic link: a Task/Agent subagent's transcript is keyed off its agentId
1885
+ // ('agent-'||agentId is the session id assigned by discover()'s filename stem).
1886
+ spawned_session_id: asString(tur.agentId) ? `agent-${tur.agentId}` : null,
1887
+ // Workflow fan-out: the result carries the run id (wf_<id>) + workflow name; this is
1888
+ // how a run's subagents (nested under subagents/workflows/<runId>/) link to this turn.
1889
+ workflow_run_id: asString(tur.runId),
1890
+ workflow_name: asString(tur.workflowName),
1891
+ resolved_model: asString(tur.resolvedModel),
1892
+ total_duration_ms: typeof tur.totalDurationMs === "number" ? tur.totalDurationMs : null,
1893
+ total_tokens: typeof tur.totalTokens === "number" ? tur.totalTokens : null,
1894
+ total_tool_use_count: typeof tur.totalToolUseCount === "number" ? tur.totalToolUseCount : null
1895
+ } : {}
1896
+ });
1897
+ }
1898
+ }
1899
+ if (toolCalls.length)
1900
+ result.toolCalls = toolCalls;
1901
+ if (toolResults.length)
1902
+ result.toolResults = toolResults;
1903
+ }
1904
+ return result;
1905
+ }
1906
+ };
1907
+
1908
+ // ../ingest/dist/classify.js
1909
+ var CLASSIFIER_VERSION = 2;
1910
+ var CATEGORIES = ["bugfix", "refactor", "docs", "ops", "review", "feature", "chore"];
1911
+ var BANDS = [
1912
+ { max: 22, band: "trivial" },
1913
+ { max: 40, band: "small" },
1914
+ { max: 55, band: "medium" },
1915
+ { max: 68, band: "large" },
1916
+ { max: Infinity, band: "xl" }
1917
+ ];
1918
+ var REVIEW_SUBAGENT_ROLES = /* @__PURE__ */ new Set(["Explore", "Plan", "code-reviewer", "claude-code-guide"]);
1919
+ function categoryForSubagentRole(role) {
1920
+ if (!role)
1921
+ return null;
1922
+ return REVIEW_SUBAGENT_ROLES.has(role) ? "review" : null;
1923
+ }
1924
+ function countLines(s) {
1925
+ if (!s)
1926
+ return 0;
1927
+ const trimmed = s.endsWith("\n") ? s.slice(0, -1) : s;
1928
+ return trimmed.length === 0 ? 0 : trimmed.split("\n").length;
1929
+ }
1930
+ var DOC_EXT = /\.(md|markdown|rst|txt|adoc)$/i;
1931
+ var OPS_FILE = /(^|\/)(dockerfile|docker-compose|\.github\/|\.gitlab-ci|makefile)|\.(ya?ml|tf|sh|toml|ini|cfg|service|timer)$/i;
1932
+ function locDelta(toolName, inputJson) {
1933
+ if (!inputJson)
1934
+ return { added: 0, removed: 0, file: null };
1935
+ let input;
1936
+ try {
1937
+ input = JSON.parse(inputJson);
1938
+ } catch {
1939
+ return { added: 0, removed: 0, file: null };
1940
+ }
1941
+ const file = typeof input?.file_path === "string" ? input.file_path : null;
1942
+ if (toolName === "Write")
1943
+ return { added: countLines(input?.content), removed: 0, file };
1944
+ if (toolName === "Edit")
1945
+ return { added: countLines(input?.new_string), removed: countLines(input?.old_string), file };
1946
+ return { added: 0, removed: 0, file };
1947
+ }
1948
+ function band(score) {
1949
+ for (const b of BANDS)
1950
+ if (score < b.max)
1951
+ return b.band;
1952
+ return "xl";
1953
+ }
1954
+ var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
1955
+ function hits(text, words) {
1956
+ let n = 0;
1957
+ for (const w of words) {
1958
+ let i = text.indexOf(w);
1959
+ while (i !== -1) {
1960
+ n++;
1961
+ i = text.indexOf(w, i + w.length);
1962
+ }
1963
+ }
1964
+ return n;
1965
+ }
1966
+ function scoreCategories(s, text) {
1967
+ const t = text.toLowerCase();
1968
+ const editN = s.toolCounts["Edit"] ?? 0;
1969
+ const writeN = s.toolCounts["Write"] ?? 0;
1970
+ const readN = s.toolCounts["Read"] ?? 0;
1971
+ const writes = editN + writeN;
1972
+ const scores = {
1973
+ bugfix: 0,
1974
+ refactor: 0,
1975
+ docs: 0,
1976
+ ops: 0,
1977
+ review: 0,
1978
+ feature: 0,
1979
+ chore: 0
1980
+ };
1981
+ scores.bugfix += hits(t, ["bug", "fix", "broken", "crash", "regression", "traceback", "stack trace", "failing", "error", "exception", "not working"]) * 1;
1982
+ scores.refactor += hits(t, ["refactor", "clean up", "cleanup", "rename", "extract", "simplify", "dead code", "tidy", "reorganize", "restructure"]) * 1;
1983
+ scores.docs += hits(t, ["readme", "document", "documentation", "changelog", "docstring", "comment", "write docs", "usage guide"]) * 1;
1984
+ scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci ", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
1985
+ scores.review += hits(t, ["review", "audit", "inspect", "look over", "feedback on"]) * 1;
1986
+ scores.feature += hits(t, ["add ", "implement", "build ", "create ", "feature", "support for", "new feature"]) * 0.8;
1987
+ const docFiles = s.loc.files > 0 ? countMatch(s, DOC_EXT) : 0;
1988
+ const opsFiles = s.loc.files > 0 ? countMatch(s, OPS_FILE) : 0;
1989
+ if (s.loc.files > 0) {
1990
+ scores.docs += 3 * (docFiles / s.loc.files);
1991
+ scores.ops += 3 * (opsFiles / s.loc.files);
1992
+ }
1993
+ if (writes >= 3 && s.loc.churn > 0 && Math.abs(s.loc.net) / s.loc.churn < 0.34)
1994
+ scores.refactor += 1.5;
1995
+ if (editN >= 4 && editN >= writeN * 2)
1996
+ scores.refactor += 1;
1997
+ if (readN >= 5 && writes <= 2)
1998
+ scores.review += 1.5;
1999
+ if (writeN >= 1 && s.loc.net > 20)
2000
+ scores.feature += 1.5;
2001
+ const maxScore = Math.max(...CATEGORIES.map((c) => scores[c]));
2002
+ if (maxScore === 0)
2003
+ scores[writes > 0 ? "feature" : "chore"] += 0.01;
2004
+ return scores;
2005
+ }
2006
+ function countMatch(s, re) {
2007
+ const files = s._files ?? [];
2008
+ let n = 0;
2009
+ for (const f of files)
2010
+ if (re.test(f))
2011
+ n++;
2012
+ return n;
2013
+ }
2014
+ function complexity(s) {
2015
+ const weights = { loc: 0.25, files: 0.15, turns: 0.2, tokens: 0.2, duration: 0.1, subagents: 0.1 };
2016
+ const subscores = {
2017
+ loc: clamp01(Math.log1p(s.loc.churn) / Math.log1p(6e3)),
2018
+ files: clamp01(s.loc.files / 40),
2019
+ turns: clamp01(s.turn_count / 40),
2020
+ tokens: clamp01(Math.log1p(s.work_tokens) / Math.log1p(4e7)),
2021
+ duration: clamp01(s.duration_ms / (600 * 6e4)),
2022
+ subagents: clamp01(s.subagent_count / 10)
2023
+ };
2024
+ let acc = 0;
2025
+ for (const k of Object.keys(weights))
2026
+ acc += weights[k] * subscores[k];
2027
+ return { score: Math.round(acc * 1e3) / 10, subscores, weights };
2028
+ }
2029
+ function classify(db, dirty) {
2030
+ const incremental = dirty != null;
2031
+ if (incremental) {
2032
+ db.exec("DROP TABLE IF EXISTS _dirty");
2033
+ db.exec("CREATE TEMP TABLE _dirty (id TEXT PRIMARY KEY)");
2034
+ const ins = db.prepare("INSERT OR IGNORE INTO _dirty (id) VALUES (?)");
2035
+ db.transaction((ids) => {
2036
+ for (const id of ids)
2037
+ ins.run(id);
2038
+ })(dirty);
2039
+ }
2040
+ const wSess = incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "";
2041
+ const aSess = incremental ? " AND session_id IN (SELECT id FROM _dirty)" : "";
2042
+ const sessions = db.prepare(`SELECT id, is_sidechain, turn_count, event_count, duration_ms FROM sessions${incremental ? " WHERE id IN (SELECT id FROM _dirty)" : ""}`).all();
2043
+ const toolMix = /* @__PURE__ */ new Map();
2044
+ for (const r of db.prepare(`SELECT session_id, tool_name, COUNT(*) c FROM tool_calls${wSess} GROUP BY session_id, tool_name`).all()) {
2045
+ const m = toolMix.get(r.session_id) ?? {};
2046
+ m[r.tool_name] = r.c;
2047
+ toolMix.set(r.session_id, m);
2048
+ }
2049
+ const skillMix = /* @__PURE__ */ new Map();
2050
+ for (const r of db.prepare(`SELECT session_id, skill_name, COUNT(*) c FROM tool_calls WHERE skill_name IS NOT NULL${aSess} GROUP BY session_id, skill_name`).all()) {
2051
+ const m = skillMix.get(r.session_id) ?? {};
2052
+ m[r.skill_name] = r.c;
2053
+ skillMix.set(r.session_id, m);
2054
+ }
2055
+ const tokens = /* @__PURE__ */ new Map();
2056
+ for (const r of db.prepare(`SELECT session_id, SUM(input_tokens) i, SUM(output_tokens) o,
2057
+ SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
2058
+ FROM token_usage${wSess} GROUP BY session_id`).all()) {
2059
+ tokens.set(r.session_id, { work: (r.i ?? 0) + (r.o ?? 0) + (r.cw ?? 0), cacheRead: r.cr ?? 0 });
2060
+ }
2061
+ const loc = /* @__PURE__ */ new Map();
2062
+ for (const r of db.prepare(`SELECT session_id, tool_name, input_json FROM tool_calls WHERE tool_name IN ('Edit','Write')${aSess}`).all()) {
2063
+ const d = locDelta(r.tool_name, r.input_json);
2064
+ const acc = loc.get(r.session_id) ?? { added: 0, removed: 0, files: /* @__PURE__ */ new Set() };
2065
+ acc.added += d.added;
2066
+ acc.removed += d.removed;
2067
+ if (d.file)
2068
+ acc.files.add(d.file);
2069
+ loc.set(r.session_id, acc);
2070
+ }
2071
+ const promptText = /* @__PURE__ */ new Map();
2072
+ for (const r of db.prepare(`SELECT session_id, prompt_preview FROM turns WHERE prompt_preview IS NOT NULL${aSess}`).all()) {
2073
+ promptText.set(r.session_id, (promptText.get(r.session_id) ?? "") + " " + r.prompt_preview);
2074
+ }
2075
+ const subagentRole = /* @__PURE__ */ new Map();
2076
+ for (const r of db.prepare(`SELECT s.id id, tc.agent_type role FROM sessions s JOIN tool_calls tc ON tc.spawned_session_id = s.id WHERE s.is_sidechain = 1 AND tc.agent_type IS NOT NULL${incremental ? " AND s.id IN (SELECT id FROM _dirty)" : ""}`).all()) {
2077
+ if (!subagentRole.has(r.id))
2078
+ subagentRole.set(r.id, r.role);
2079
+ }
2080
+ const upsert = db.prepare(`INSERT INTO classifications (scope, target_id, category, complexity_score, complexity_band, signals_json, classifier_version)
2081
+ VALUES ('session', @target_id, @category, @complexity_score, @complexity_band, @signals_json, @classifier_version)
2082
+ ON CONFLICT(scope, target_id) DO UPDATE SET
2083
+ category=excluded.category, complexity_score=excluded.complexity_score,
2084
+ complexity_band=excluded.complexity_band, signals_json=excluded.signals_json,
2085
+ classifier_version=excluded.classifier_version`);
2086
+ const tx = db.transaction(() => {
2087
+ for (const sess of sessions) {
2088
+ const toolCounts = toolMix.get(sess.id) ?? {};
2089
+ const skills = skillMix.get(sess.id) ?? {};
2090
+ const l = loc.get(sess.id) ?? { added: 0, removed: 0, files: /* @__PURE__ */ new Set() };
2091
+ const tok = tokens.get(sess.id) ?? { work: 0, cacheRead: 0 };
2092
+ const filesArr = [...l.files].sort();
2093
+ const subagentCount = (toolCounts["Agent"] ?? 0) + (toolCounts["Task"] ?? 0);
2094
+ const signals = {
2095
+ toolCounts,
2096
+ skills,
2097
+ loc: { added: l.added, removed: l.removed, net: l.added - l.removed, churn: l.added + l.removed, files: l.files.size },
2098
+ turn_count: sess.turn_count,
2099
+ event_count: sess.event_count,
2100
+ work_tokens: tok.work,
2101
+ cache_read_tokens: tok.cacheRead,
2102
+ duration_ms: sess.duration_ms ?? 0,
2103
+ subagent_count: subagentCount,
2104
+ is_sidechain: sess.is_sidechain,
2105
+ _files: filesArr
2106
+ };
2107
+ const catScores = scoreCategories(signals, promptText.get(sess.id) ?? "");
2108
+ let category = "chore";
2109
+ let bestScore = -1;
2110
+ for (const c of CATEGORIES) {
2111
+ if (catScores[c] > bestScore) {
2112
+ bestScore = catScores[c];
2113
+ category = c;
2114
+ }
2115
+ }
2116
+ const role = subagentRole.get(sess.id);
2117
+ if (sess.is_sidechain) {
2118
+ const roleCat = categoryForSubagentRole(role);
2119
+ if (roleCat)
2120
+ category = roleCat;
2121
+ }
2122
+ const cx = complexity(signals);
2123
+ const signalsJson = JSON.stringify({
2124
+ tool_counts: toolCounts,
2125
+ skills,
2126
+ loc: signals.loc,
2127
+ files: filesArr,
2128
+ turn_count: signals.turn_count,
2129
+ event_count: signals.event_count,
2130
+ work_tokens: signals.work_tokens,
2131
+ cache_read_tokens: signals.cache_read_tokens,
2132
+ duration_ms: signals.duration_ms,
2133
+ subagent_count: signals.subagent_count,
2134
+ is_sidechain: signals.is_sidechain,
2135
+ subagent_role: role ?? null,
2136
+ category_scores: catScores,
2137
+ complexity_subscores: cx.subscores,
2138
+ complexity_weights: cx.weights,
2139
+ classifier_version: CLASSIFIER_VERSION
2140
+ });
2141
+ upsert.run({
2142
+ target_id: sess.id,
2143
+ category,
2144
+ complexity_score: cx.score,
2145
+ complexity_band: band(cx.score),
2146
+ signals_json: signalsJson,
2147
+ classifier_version: CLASSIFIER_VERSION
2148
+ });
2149
+ }
2150
+ });
2151
+ tx();
2152
+ if (incremental)
2153
+ db.exec("DROP TABLE IF EXISTS _dirty");
2154
+ return { count: sessions.length, version: CLASSIFIER_VERSION };
2155
+ }
2156
+
2157
+ // ../ingest/dist/pipeline.js
2158
+ import { createHash } from "crypto";
2159
+ function newStats() {
2160
+ return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
2161
+ }
2162
+ function preview(text) {
2163
+ return text.replace(/\s+/g, " ").trim().slice(0, 140);
2164
+ }
2165
+ var SKILL_INJECT_PREFIX = "Base directory for this skill:";
2166
+ function skillNameKey(name) {
2167
+ const afterSlash = name.split("/").pop() ?? name;
2168
+ const afterColon = afterSlash.split(":").pop() ?? afterSlash;
2169
+ return afterColon.trim();
2170
+ }
2171
+ function parseSkillInjection(text) {
2172
+ if (!text.startsWith(SKILL_INJECT_PREFIX))
2173
+ return null;
2174
+ const firstNl = text.indexOf("\n");
2175
+ const firstLine = (firstNl >= 0 ? text.slice(0, firstNl) : text).slice(SKILL_INJECT_PREFIX.length).trim();
2176
+ const baseDir = firstLine || null;
2177
+ let body = firstNl >= 0 ? text.slice(firstNl + 1) : "";
2178
+ let args = null;
2179
+ const argIdx = body.lastIndexOf("\nARGUMENTS:");
2180
+ if (argIdx >= 0) {
2181
+ args = body.slice(argIdx + "\nARGUMENTS:".length).trim() || null;
2182
+ body = body.slice(0, argIdx);
2183
+ }
2184
+ return { baseDir, nameKey: baseDir ? skillNameKey(baseDir) : "", body: body.trim(), args };
2185
+ }
2186
+ function skillSummary(body) {
2187
+ for (const line of body.split("\n")) {
2188
+ const t = line.trim();
2189
+ if (!t)
2190
+ continue;
2191
+ return (t.startsWith("#") ? t.replace(/^#+\s*/, "").trim() : t).slice(0, 200) || null;
2192
+ }
2193
+ return null;
2194
+ }
2195
+ function skillVersionId(name, body) {
2196
+ return createHash("sha1").update(name).update("\0").update(body).digest("hex").slice(0, 16);
2197
+ }
2198
+ function pruneExcluded(db, excludedPaths) {
2199
+ if (!excludedPaths.length)
2200
+ return 0;
2201
+ const projects = db.prepare("SELECT id, path FROM projects").all();
2202
+ const projIds = projects.filter((p) => excludedPaths.some((e) => p.path === e || p.path.startsWith(e + "/"))).map((p) => p.id);
2203
+ if (!projIds.length)
2204
+ return 0;
2205
+ db.exec("DROP TABLE IF EXISTS _prune");
2206
+ db.exec("CREATE TEMP TABLE _prune (id TEXT PRIMARY KEY)");
2207
+ const insP = db.prepare("INSERT OR IGNORE INTO _prune (id) VALUES (?)");
2208
+ const seed = db.prepare(`SELECT id FROM sessions WHERE project_id IN (${projIds.map(() => "?").join(",")})`).all(...projIds);
2209
+ db.transaction(() => {
2210
+ for (const s of seed)
2211
+ insP.run(s.id);
2212
+ })();
2213
+ const expand = db.prepare("INSERT OR IGNORE INTO _prune (id) SELECT id FROM sessions WHERE parent_session_id IN (SELECT id FROM _prune)");
2214
+ const cnt = db.prepare("SELECT COUNT(*) n FROM _prune");
2215
+ for (let prev = -1; ; ) {
2216
+ const n = cnt.get().n;
2217
+ if (n === prev)
2218
+ break;
2219
+ prev = n;
2220
+ expand.run();
2221
+ }
2222
+ const total = cnt.get().n;
2223
+ if (!total) {
2224
+ db.exec("DROP TABLE IF EXISTS _prune");
2225
+ return 0;
2226
+ }
2227
+ const files = db.prepare("SELECT DISTINCT source_file f FROM events WHERE session_id IN (SELECT id FROM _prune) AND source_file IS NOT NULL").all().map((r) => r.f);
2228
+ db.pragma("foreign_keys = OFF");
2229
+ db.transaction(() => {
2230
+ for (const t of ["token_usage", "tool_calls", "turns", "events"])
2231
+ db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);
2232
+ db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)");
2233
+ db.exec("DELETE FROM sessions WHERE id IN (SELECT id FROM _prune)");
2234
+ const delState = db.prepare("DELETE FROM ingest_state WHERE file_path = ?");
2235
+ for (const f of files)
2236
+ delState.run(f);
2237
+ db.exec("DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)");
2238
+ })();
2239
+ db.pragma("foreign_keys = ON");
2240
+ db.exec("DROP TABLE IF EXISTS _prune");
2241
+ return total;
2242
+ }
2243
+ function durationMs(start, end) {
2244
+ if (!start || !end)
2245
+ return null;
2246
+ const a = Date.parse(start);
2247
+ const b = Date.parse(end);
2248
+ if (Number.isNaN(a) || Number.isNaN(b))
2249
+ return null;
2250
+ return Math.max(0, b - a);
2251
+ }
2252
+ function buildTurns(sessionId, rows) {
2253
+ rows.sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || x.uuid.localeCompare(y.uuid));
2254
+ const turns = [];
2255
+ const eventTurn = /* @__PURE__ */ new Map();
2256
+ let cur = null;
2257
+ let seq = 0;
2258
+ for (const e of rows) {
2259
+ const isPrompt = e.type === "user" && e.is_meta === 0 && !!e.text && e.text.trim().length > 0;
2260
+ if (isPrompt) {
2261
+ cur = {
2262
+ id: `${sessionId}:${seq}`,
2263
+ session_id: sessionId,
2264
+ seq,
2265
+ user_event_uuid: e.uuid,
2266
+ prompt_preview: preview(e.text),
2267
+ model: null,
2268
+ started_at: e.timestamp,
2269
+ ended_at: e.timestamp,
2270
+ duration_ms: null
2271
+ };
2272
+ turns.push(cur);
2273
+ seq++;
2274
+ }
2275
+ if (cur) {
2276
+ eventTurn.set(e.uuid, cur.id);
2277
+ if (e.model)
2278
+ cur.model = e.model;
2279
+ if (e.timestamp && (!cur.ended_at || e.timestamp > cur.ended_at))
2280
+ cur.ended_at = e.timestamp;
2281
+ }
2282
+ }
2283
+ for (const t of turns)
2284
+ t.duration_ms = durationMs(t.started_at, t.ended_at);
2285
+ return { turns, eventTurn };
2286
+ }
2287
+ function rebuildDerived(db, dirty) {
2288
+ const incremental = dirty != null;
2289
+ const bySession = incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "";
2290
+ const byId = incremental ? " WHERE id IN (SELECT id FROM _dirty)" : "";
2291
+ const andId = incremental ? " AND id IN (SELECT id FROM _dirty)" : "";
2292
+ const andSelfId = incremental ? " AND sessions.id IN (SELECT id FROM _dirty)" : "";
2293
+ if (incremental) {
2294
+ db.exec("DROP TABLE IF EXISTS _dirty");
2295
+ db.exec("CREATE TEMP TABLE _dirty (id TEXT PRIMARY KEY)");
2296
+ const ins = db.prepare("INSERT OR IGNORE INTO _dirty (id) VALUES (?)");
2297
+ db.transaction((ids) => {
2298
+ for (const id of ids)
2299
+ ins.run(id);
2300
+ })(dirty);
2301
+ const expand = db.prepare(`
2302
+ INSERT OR IGNORE INTO _dirty (id)
2303
+ SELECT spawned_session_id FROM tool_calls
2304
+ WHERE session_id IN (SELECT id FROM _dirty) AND spawned_session_id IS NOT NULL
2305
+ UNION
2306
+ SELECT session_id FROM tool_calls
2307
+ WHERE spawned_session_id IN (SELECT id FROM _dirty)
2308
+ UNION
2309
+ -- structural (path-based) workflow linkage: a dirty parent pulls in its subagent children,
2310
+ -- and a dirty child pulls in its structural parent, so the fallback link stays correct when
2311
+ -- parent and child transcripts arrive in different runs (ADR-010).
2312
+ SELECT id FROM sessions
2313
+ WHERE spawn_parent_id IN (SELECT id FROM _dirty)
2314
+ UNION
2315
+ SELECT spawn_parent_id FROM sessions
2316
+ WHERE id IN (SELECT id FROM _dirty) AND spawn_parent_id IS NOT NULL
2317
+ `);
2318
+ const count = db.prepare("SELECT COUNT(*) n FROM _dirty");
2319
+ let prev = -1;
2320
+ for (; ; ) {
2321
+ const n = count.get().n;
2322
+ if (n === prev)
2323
+ break;
2324
+ prev = n;
2325
+ expand.run();
2326
+ }
2327
+ }
2328
+ db.exec(`UPDATE events SET turn_id = NULL${bySession}`);
2329
+ db.exec(`UPDATE token_usage SET turn_id = NULL${bySession}`);
2330
+ db.exec(`UPDATE tool_calls SET turn_id = NULL${bySession}`);
2331
+ db.exec(`UPDATE tool_calls SET skill_id = NULL${bySession}`);
2332
+ db.exec(`UPDATE sessions SET parent_turn_id = NULL${byId}`);
2333
+ db.exec(`DELETE FROM turns${bySession}`);
2334
+ const sessionIds = db.prepare(`SELECT id FROM sessions${byId}`).all();
2335
+ const selEvents = db.prepare("SELECT uuid, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?");
2336
+ const insTurn = db.prepare(`INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)
2337
+ VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`);
2338
+ const updEventTurn = db.prepare("UPDATE events SET turn_id = ? WHERE uuid = ?");
2339
+ const tx = db.transaction(() => {
2340
+ for (const { id } of sessionIds) {
2341
+ const rows = selEvents.all(id);
2342
+ const { turns, eventTurn } = buildTurns(id, rows);
2343
+ for (const t of turns)
2344
+ insTurn.run(t);
2345
+ for (const [uuid, turnId] of eventTurn)
2346
+ updEventTurn.run(turnId, uuid);
2347
+ }
2348
+ });
2349
+ tx();
2350
+ db.exec(`UPDATE token_usage SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = token_usage.event_uuid)${bySession}`);
2351
+ db.exec(`UPDATE tool_calls SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = tool_calls.event_uuid)${bySession}`);
2352
+ {
2353
+ const selEventsOrdered = db.prepare("SELECT uuid, type, is_meta, timestamp, text FROM events WHERE session_id = ?");
2354
+ const selSkillCalls = db.prepare("SELECT id, event_uuid, skill_name, input_json FROM tool_calls WHERE session_id = ? AND tool_name = 'Skill'");
2355
+ const upsertSkill = db.prepare(`INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)
2356
+ VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @ts, @ts)
2357
+ ON CONFLICT(id) DO UPDATE SET
2358
+ last_seen = MAX(last_seen, excluded.last_seen),
2359
+ first_seen = MIN(first_seen, excluded.first_seen),
2360
+ base_dir = excluded.base_dir`);
2361
+ const linkCall = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE id = ?");
2362
+ const linkTx = db.transaction(() => {
2363
+ for (const { id: sid } of sessionIds) {
2364
+ const calls = selSkillCalls.all(sid);
2365
+ if (!calls.length)
2366
+ continue;
2367
+ const events = selEventsOrdered.all(sid).sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || x.uuid.localeCompare(y.uuid));
2368
+ const callByEvent = /* @__PURE__ */ new Map();
2369
+ for (const c of calls) {
2370
+ if (!c.event_uuid)
2371
+ continue;
2372
+ const arr = callByEvent.get(c.event_uuid) ?? [];
2373
+ arr.push(c);
2374
+ callByEvent.set(c.event_uuid, arr);
2375
+ }
2376
+ const pending = /* @__PURE__ */ new Map();
2377
+ for (const e of events) {
2378
+ const here = callByEvent.get(e.uuid);
2379
+ if (here) {
2380
+ for (const c2 of here) {
2381
+ if (!c2.skill_name)
2382
+ continue;
2383
+ const key = skillNameKey(c2.skill_name);
2384
+ let args = null;
2385
+ try {
2386
+ args = c2.input_json ? JSON.parse(c2.input_json).args ?? null : null;
2387
+ if (typeof args === "string")
2388
+ args = args.trim() || null;
2389
+ else if (args != null)
2390
+ args = JSON.stringify(args);
2391
+ } catch {
2392
+ }
2393
+ const arr = pending.get(key) ?? [];
2394
+ arr.push({ id: c2.id, args });
2395
+ pending.set(key, arr);
2396
+ }
2397
+ }
2398
+ if (e.is_meta !== 1 || e.type !== "user" || !e.text)
2399
+ continue;
2400
+ const inj = parseSkillInjection(e.text);
2401
+ if (!inj)
2402
+ continue;
2403
+ const queue = pending.get(inj.nameKey);
2404
+ if (!queue || !queue.length)
2405
+ continue;
2406
+ let idx = inj.args != null ? queue.findIndex((q) => q.args === inj.args) : -1;
2407
+ if (idx < 0)
2408
+ idx = 0;
2409
+ const [match] = queue.splice(idx, 1);
2410
+ const c = calls.find((x) => x.id === match.id);
2411
+ const name = c.skill_name;
2412
+ const vid = skillVersionId(name, inj.body);
2413
+ upsertSkill.run({
2414
+ id: vid,
2415
+ name,
2416
+ base_dir: inj.baseDir,
2417
+ body: inj.body,
2418
+ summary: skillSummary(inj.body),
2419
+ body_bytes: Buffer.byteLength(inj.body, "utf8"),
2420
+ ts: e.timestamp
2421
+ });
2422
+ linkCall.run(vid, match.id);
2423
+ }
2424
+ }
2425
+ });
2426
+ linkTx();
2427
+ }
2428
+ db.exec(`
2429
+ UPDATE sessions SET
2430
+ parent_session_id = (SELECT tc.session_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id),
2431
+ parent_turn_id = (SELECT tc.turn_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)
2432
+ WHERE EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)${andSelfId}
2433
+ `);
2434
+ db.exec(`
2435
+ UPDATE sessions SET
2436
+ parent_session_id = COALESCE(parent_session_id, (SELECT tc.session_id FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id LIMIT 1)),
2437
+ parent_turn_id = COALESCE(parent_turn_id, (SELECT tc.turn_id FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id LIMIT 1))
2438
+ WHERE sessions.workflow_run_id IS NOT NULL
2439
+ AND EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id)${andSelfId}
2440
+ `);
2441
+ db.exec(`
2442
+ UPDATE sessions SET parent_session_id = spawn_parent_id
2443
+ WHERE parent_session_id IS NULL
2444
+ AND spawn_parent_id IS NOT NULL
2445
+ AND spawn_parent_id IN (SELECT id FROM sessions)${andSelfId}
2446
+ `);
2447
+ db.exec(`
2448
+ UPDATE sessions SET
2449
+ event_count = (SELECT COUNT(*) FROM events e WHERE e.session_id = sessions.id),
2450
+ started_at = (SELECT MIN(timestamp) FROM events e WHERE e.session_id = sessions.id),
2451
+ ended_at = (SELECT MAX(timestamp) FROM events e WHERE e.session_id = sessions.id),
2452
+ turn_count = (SELECT COUNT(*) FROM turns t WHERE t.session_id = sessions.id),
2453
+ is_sidechain = (SELECT CASE WHEN MIN(is_sidechain) = 1 THEN 1 ELSE 0 END FROM events e WHERE e.session_id = sessions.id)${byId}
2454
+ `);
2455
+ db.exec(`
2456
+ UPDATE sessions SET duration_ms =
2457
+ CAST((julianday(ended_at) - julianday(started_at)) * 86400000 AS INTEGER)
2458
+ WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${andId}
2459
+ `);
2460
+ db.exec(`DELETE FROM sessions WHERE event_count = 0${andId}`);
2461
+ db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)");
2462
+ if (!incremental) {
2463
+ db.exec("DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)");
2464
+ }
2465
+ if (incremental) {
2466
+ const expanded = new Set(db.prepare("SELECT id FROM _dirty").all().map((r) => r.id));
2467
+ db.exec("DROP TABLE IF EXISTS _dirty");
2468
+ return expanded;
2469
+ }
2470
+ return null;
2471
+ }
2472
+ function prepareStatements(db) {
2473
+ return {
2474
+ insAgent: db.prepare("INSERT OR IGNORE INTO agents (id, name, kind) VALUES (?, ?, 'cli')"),
2475
+ insSource: db.prepare(`INSERT INTO sources (id, label, agent_id, config_dir) VALUES (@id, @label, @agent_id, @config_dir)
2476
+ ON CONFLICT(id) DO UPDATE SET label=excluded.label, agent_id=excluded.agent_id, config_dir=excluded.config_dir`),
2477
+ getState: db.prepare("SELECT size, mtime_ms, sha256, events_ingested FROM ingest_state WHERE file_path = ?"),
2478
+ setState: db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
2479
+ VALUES (@file_path, @size, @mtime_ms, @sha256, @events_ingested, @ingested_at)
2480
+ ON CONFLICT(file_path) DO UPDATE SET
2481
+ size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256,
2482
+ events_ingested=excluded.events_ingested, ingested_at=excluded.ingested_at`),
2483
+ insEvent: db.prepare(`INSERT INTO events (uuid, session_id, turn_id, parent_uuid, seq, type, role, timestamp, model, is_sidechain, is_meta, text, raw_json, source_file)
2484
+ VALUES (@uuid, @session_id, @turn_id, @parent_uuid, @seq, @type, @role, @timestamp, @model, @is_sidechain, @is_meta, @text, @raw_json, @source_file)
2485
+ ON CONFLICT(uuid) DO NOTHING`),
2486
+ insTokens: db.prepare(
2487
+ // Bare ON CONFLICT (no target) so the insert is skipped on EITHER the event_uuid PK or the
2488
+ // unique (session_id, message_id) index — the latter collapses a response's repeated
2489
+ // per-content-block usage lines into a single row.
2490
+ `INSERT INTO token_usage (event_uuid, session_id, turn_id, message_id, model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens, service_tier)
2491
+ VALUES (@event_uuid, @session_id, @turn_id, @message_id, @model, @input_tokens, @output_tokens, @cache_creation_input_tokens, @cache_read_input_tokens, @service_tier)
2492
+ ON CONFLICT DO NOTHING`
2493
+ ),
2494
+ insTool: db.prepare(`INSERT INTO tool_calls (id, event_uuid, session_id, turn_id, tool_name, caller, skill_name, skill_id, agent_type, spawned_session_id, workflow_run_id, workflow_name, resolved_model, status, total_duration_ms, total_tokens, total_tool_use_count, input_json, result_summary)
2495
+ VALUES (@id, @event_uuid, @session_id, @turn_id, @tool_name, @caller, @skill_name, @skill_id, @agent_type, @spawned_session_id, @workflow_run_id, @workflow_name, @resolved_model, @status, @total_duration_ms, @total_tokens, @total_tool_use_count, @input_json, @result_summary)
2496
+ ON CONFLICT(id) DO NOTHING`),
2497
+ patchTool: db.prepare(`UPDATE tool_calls SET
2498
+ status = COALESCE(@status, status),
2499
+ agent_type = COALESCE(@agent_type, agent_type),
2500
+ spawned_session_id = COALESCE(@spawned_session_id, spawned_session_id),
2501
+ workflow_run_id = COALESCE(@workflow_run_id, workflow_run_id),
2502
+ workflow_name = COALESCE(@workflow_name, workflow_name),
2503
+ resolved_model = COALESCE(@resolved_model, resolved_model),
2504
+ total_duration_ms = COALESCE(@total_duration_ms, total_duration_ms),
2505
+ total_tokens = COALESCE(@total_tokens, total_tokens),
2506
+ total_tool_use_count = COALESCE(@total_tool_use_count, total_tool_use_count),
2507
+ result_summary = COALESCE(@result_summary, result_summary)
2508
+ WHERE id = @tool_use_id`),
2509
+ upsertProject: db.prepare(`INSERT INTO projects (id, agent_id, path, encoded_dir, first_seen, last_seen)
2510
+ VALUES (@id, @agent_id, @path, @encoded_dir, @now, @now)
2511
+ ON CONFLICT(id) DO UPDATE SET last_seen = @now, encoded_dir = COALESCE(excluded.encoded_dir, encoded_dir)`),
2512
+ insSessionStub: db.prepare("INSERT OR IGNORE INTO sessions (id, agent_id, source_id) VALUES (?, ?, ?)"),
2513
+ upsertSession: db.prepare(`INSERT INTO sessions (id, agent_id, source_id, project_id, slug, ai_title, cli_version, entrypoint, git_branch, spawn_parent_id, workflow_run_id)
2514
+ VALUES (@id, @agent_id, @source_id, @project_id, @slug, @ai_title, @cli_version, @entrypoint, @git_branch, @spawn_parent_id, @workflow_run_id)
2515
+ ON CONFLICT(id) DO UPDATE SET
2516
+ source_id = COALESCE(excluded.source_id, source_id),
2517
+ project_id = COALESCE(excluded.project_id, project_id),
2518
+ slug = COALESCE(excluded.slug, slug),
2519
+ ai_title = COALESCE(excluded.ai_title, ai_title),
2520
+ cli_version = COALESCE(excluded.cli_version, cli_version),
2521
+ entrypoint = COALESCE(excluded.entrypoint, entrypoint),
2522
+ git_branch = COALESCE(excluded.git_branch, git_branch),
2523
+ spawn_parent_id = COALESCE(excluded.spawn_parent_id, spawn_parent_id),
2524
+ workflow_run_id = COALESCE(excluded.workflow_run_id, workflow_run_id)`)
2525
+ };
2526
+ }
2527
+ function ingestFile(db, stmts, adapter, file, lines, meta, now, stats) {
2528
+ const sessionMeta = {};
2529
+ let eventsInFile = 0;
2530
+ const tx = db.transaction(() => {
2531
+ stmts.insSessionStub.run(file.sessionId, adapter.agentId, file.sourceId);
2532
+ let seq = 0;
2533
+ for (const line of lines) {
2534
+ if (!line.trim())
2535
+ continue;
2536
+ let raw;
2537
+ try {
2538
+ raw = JSON.parse(line);
2539
+ } catch {
2540
+ stats.malformed++;
2541
+ continue;
2542
+ }
2543
+ const parsed = adapter.parseLine(raw, file, seq++);
2544
+ if (parsed.meta)
2545
+ Object.assign(sessionMeta, parsed.meta);
2546
+ if (parsed.event) {
2547
+ const info = stmts.insEvent.run({ ...parsed.event, raw_json: packRaw(parsed.event.raw_json) });
2548
+ if (info.changes > 0) {
2549
+ stats.newEvents++;
2550
+ eventsInFile++;
2551
+ }
2552
+ }
2553
+ if (parsed.tokenUsage)
2554
+ stmts.insTokens.run(parsed.tokenUsage);
2555
+ if (parsed.toolCalls)
2556
+ for (const tc of parsed.toolCalls)
2557
+ stmts.insTool.run(tc);
2558
+ if (parsed.toolResults)
2559
+ for (const tr of parsed.toolResults)
2560
+ stmts.patchTool.run({
2561
+ tool_use_id: tr.tool_use_id,
2562
+ status: tr.status ?? null,
2563
+ agent_type: tr.agent_type ?? null,
2564
+ spawned_session_id: tr.spawned_session_id ?? null,
2565
+ workflow_run_id: tr.workflow_run_id ?? null,
2566
+ workflow_name: tr.workflow_name ?? null,
2567
+ resolved_model: tr.resolved_model ?? null,
2568
+ total_duration_ms: tr.total_duration_ms ?? null,
2569
+ total_tokens: tr.total_tokens ?? null,
2570
+ total_tool_use_count: tr.total_tool_use_count ?? null,
2571
+ result_summary: tr.result_summary ?? null
2572
+ });
2573
+ }
2574
+ let projectId = null;
2575
+ if (sessionMeta.cwd) {
2576
+ projectId = createHash("sha1").update(`${adapter.agentId}\0${sessionMeta.cwd}`).digest("hex").slice(0, 16);
2577
+ stmts.upsertProject.run({
2578
+ id: projectId,
2579
+ agent_id: adapter.agentId,
2580
+ path: sessionMeta.cwd,
2581
+ encoded_dir: file.encodedDir || null,
2582
+ now
2583
+ });
2584
+ }
2585
+ stmts.upsertSession.run({
2586
+ id: file.sessionId,
2587
+ agent_id: adapter.agentId,
2588
+ source_id: file.sourceId,
2589
+ project_id: projectId,
2590
+ slug: sessionMeta.slug ?? null,
2591
+ ai_title: sessionMeta.ai_title ?? null,
2592
+ cli_version: sessionMeta.cli_version ?? null,
2593
+ entrypoint: sessionMeta.entrypoint ?? null,
2594
+ git_branch: sessionMeta.git_branch ?? null,
2595
+ spawn_parent_id: file.parentSessionId ?? null,
2596
+ workflow_run_id: file.workflowRunId ?? null
2597
+ });
2598
+ stmts.setState.run({
2599
+ file_path: file.path,
2600
+ size: meta.size,
2601
+ mtime_ms: Math.trunc(meta.mtimeMs),
2602
+ sha256: meta.hash,
2603
+ events_ingested: eventsInFile,
2604
+ ingested_at: now
2605
+ });
2606
+ });
2607
+ tx();
2608
+ }
2609
+
2610
+ // ../ingest/dist/redact.js
2611
+ import { createHash as createHash2, randomBytes } from "crypto";
2612
+ function parseExcludes(csv) {
2613
+ return (csv ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(encodeProjectPath);
2614
+ }
2615
+ function isExcludedArchivePath(filePath, excluded) {
2616
+ return excluded.some((e) => filePath.includes(`/projects/${e}/`));
2617
+ }
2618
+
2619
+ // ../ingest/dist/fileread.js
2620
+ import { createHash as createHash3 } from "crypto";
2621
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
2622
+ import { StringDecoder } from "string_decoder";
2623
+ var STREAM_THRESHOLD = 8 * 1024 * 1024;
2624
+ var CHUNK2 = 64 * 1024;
2625
+ function sha256(buf) {
2626
+ return createHash3("sha256").update(buf).digest("hex");
2627
+ }
2628
+ function sha256File(path) {
2629
+ const fd = openSync2(path, "r");
2630
+ const h = createHash3("sha256");
2631
+ const buf = Buffer.allocUnsafe(CHUNK2);
2632
+ try {
2633
+ let n;
2634
+ while ((n = readSync2(fd, buf, 0, CHUNK2, null)) > 0)
2635
+ h.update(buf.subarray(0, n));
2636
+ } finally {
2637
+ closeSync2(fd);
2638
+ }
2639
+ return h.digest("hex");
2640
+ }
2641
+ function* streamLines(path) {
2642
+ const fd = openSync2(path, "r");
2643
+ const decoder = new StringDecoder("utf8");
2644
+ const buf = Buffer.allocUnsafe(CHUNK2);
2645
+ let leftover = "";
2646
+ try {
2647
+ let n;
2648
+ while ((n = readSync2(fd, buf, 0, CHUNK2, null)) > 0) {
2649
+ leftover += decoder.write(buf.subarray(0, n));
2650
+ let idx;
2651
+ while ((idx = leftover.indexOf("\n")) !== -1) {
2652
+ yield leftover.slice(0, idx);
2653
+ leftover = leftover.slice(idx + 1);
2654
+ }
2655
+ }
2656
+ leftover += decoder.end();
2657
+ if (leftover.length > 0)
2658
+ yield leftover;
2659
+ } finally {
2660
+ closeSync2(fd);
2661
+ }
2662
+ }
2663
+
2664
+ // ../ingest/dist/run.js
2665
+ function parseIngestArgs(argv) {
2666
+ const a = { full: false, db: "", archive: "" };
2667
+ for (let i = 0; i < argv.length; i++) {
2668
+ if (argv[i] === "--full")
2669
+ a.full = true;
2670
+ else if (argv[i] === "--db")
2671
+ a.db = argv[++i] ?? "";
2672
+ else if (argv[i] === "--archive")
2673
+ a.archive = argv[++i] ?? "";
2674
+ }
2675
+ return a;
2676
+ }
2677
+ function runIngest(argv = process.argv.slice(2)) {
2678
+ const args = parseIngestArgs(argv);
2679
+ const dataDir = resolveDataDir(findRepoRoot());
2680
+ const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE || join5(dataDir, "archive");
2681
+ const dbPath = args.db || process.env.AGENT_LENS_DB || join5(dataDir, "agent-lens.db");
2682
+ if (!existsSync4(archiveRoot)) {
2683
+ console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
2684
+ process.exit(1);
2685
+ }
2686
+ mkdirSync4(dirname4(dbPath), { recursive: true });
2687
+ const db = args.full ? openRaw(dbPath) : openDb(dbPath);
2688
+ if (args.full)
2689
+ resetSchema(db);
2690
+ const adapterList = [new ClaudeCodeAdapter()];
2691
+ const adapterById = new Map(adapterList.map((a) => [a.agentId, a]));
2692
+ const sources = loadSources();
2693
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2694
+ const excludePaths = loadExcludes();
2695
+ const excludedDirs = parseExcludes(excludePaths.join(","));
2696
+ const pruned = pruneExcluded(db, excludePaths);
2697
+ const stmts = prepareStatements(db);
2698
+ const stats = newStats();
2699
+ const dirty = /* @__PURE__ */ new Set();
2700
+ for (const source of sources) {
2701
+ const adapter = adapterById.get(source.agent);
2702
+ if (!adapter) {
2703
+ console.warn(`agent-lens-ingest: no adapter for agent '${source.agent}' (source '${source.label}') \u2014 skipping`);
2704
+ continue;
2705
+ }
2706
+ stmts.insAgent.run(adapter.agentId, adapter.agentName);
2707
+ stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
2708
+ let files = adapter.discover(join5(archiveRoot, source.label), source.label);
2709
+ if (excludedDirs.length)
2710
+ files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
2711
+ files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
2712
+ for (const file of files) {
2713
+ stats.files++;
2714
+ const st = statSync2(file.path);
2715
+ const mtimeMs = Math.trunc(st.mtimeMs);
2716
+ const prev = args.full ? void 0 : stmts.getState.get(file.path);
2717
+ if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
2718
+ stats.skipped++;
2719
+ continue;
2720
+ }
2721
+ const small = st.size <= STREAM_THRESHOLD;
2722
+ const buf = small ? readFileSync4(file.path) : null;
2723
+ const hash = small ? sha256(buf) : sha256File(file.path);
2724
+ if (prev && prev.sha256 === hash) {
2725
+ stmts.setState.run({
2726
+ file_path: file.path,
2727
+ size: st.size,
2728
+ mtime_ms: mtimeMs,
2729
+ sha256: hash,
2730
+ events_ingested: prev.events_ingested,
2731
+ ingested_at: now
2732
+ });
2733
+ stats.skipped++;
2734
+ continue;
2735
+ }
2736
+ const lines = small ? buf.toString("utf8").split("\n") : streamLines(file.path);
2737
+ ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash }, now, stats);
2738
+ dirty.add(file.sessionId);
2739
+ }
2740
+ }
2741
+ const expanded = rebuildDerived(db, args.full ? null : dirty);
2742
+ const classified = classify(db, args.full ? null : expanded);
2743
+ const count = (sql) => db.prepare(sql).get().n;
2744
+ const sessions = count("SELECT COUNT(*) n FROM sessions");
2745
+ const turns = count("SELECT COUNT(*) n FROM turns");
2746
+ const events = count("SELECT COUNT(*) n FROM events");
2747
+ const tools = count("SELECT COUNT(*) n FROM tool_calls");
2748
+ const usageRows = db.prepare("SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage").all();
2749
+ let cost = 0;
2750
+ let totalTokens = 0;
2751
+ for (const u of usageRows) {
2752
+ cost += costForUsage(u.model, u);
2753
+ totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
2754
+ }
2755
+ db.close();
2756
+ console.log(`agent-lens-ingest: files=${stats.files} skipped=${stats.skipped} new_events=${stats.newEvents} malformed=${stats.malformed}${pruned ? ` excluded_pruned=${pruned}` : ""}
2757
+ sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${classified.count}
2758
+ tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
2759
+ }
2760
+ function runMetrics(argv = process.argv.slice(2)) {
2761
+ let dbArg = "";
2762
+ for (let i = 0; i < argv.length; i++)
2763
+ if (argv[i] === "--db")
2764
+ dbArg = argv[++i] ?? "";
2765
+ const dataDir = resolveDataDir(findRepoRoot());
2766
+ const dbPath = dbArg || process.env.AGENT_LENS_DB || join5(dataDir, "agent-lens.db");
2767
+ if (!existsSync4(dbPath)) {
2768
+ console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);
2769
+ process.exit(1);
2770
+ }
2771
+ const db = openDb(dbPath);
2772
+ const r = classify(db);
2773
+ db.close();
2774
+ console.log(`agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} db=${dbPath}`);
2775
+ }
2776
+
2777
+ // ../server/dist/run.js
2778
+ import { existsSync as existsSync7 } from "fs";
2779
+ import { dirname as dirname5, join as join7 } from "path";
2780
+ import { fileURLToPath as fileURLToPath2 } from "url";
2781
+
2782
+ // ../server/dist/db.js
2783
+ import Database2 from "better-sqlite3";
2784
+ function openReadonly(file) {
2785
+ const db = new Database2(file, { readonly: true, fileMustExist: true });
2786
+ db.pragma("query_only = ON");
2787
+ return db;
2788
+ }
2789
+ function lastIngested(db) {
2790
+ const row = db.prepare("SELECT MAX(ingested_at) AS last FROM ingest_state").get();
2791
+ return row?.last ?? null;
2792
+ }
2793
+ function extractParts(rawJson) {
2794
+ let text = "";
2795
+ let thinking = "";
2796
+ try {
2797
+ const content = JSON.parse(unpackRaw(rawJson))?.message?.content;
2798
+ if (typeof content === "string")
2799
+ text = content;
2800
+ else if (Array.isArray(content)) {
2801
+ for (const b of content) {
2802
+ if (b?.type === "text" && typeof b.text === "string")
2803
+ text += (text ? "\n" : "") + b.text;
2804
+ else if (b?.type === "thinking" && typeof b.thinking === "string")
2805
+ thinking += (thinking ? "\n" : "") + b.thinking;
2806
+ }
2807
+ }
2808
+ } catch {
2809
+ }
2810
+ return { text: text || null, thinking: thinking || null };
2811
+ }
2812
+ function listSources(db) {
2813
+ return db.prepare(
2814
+ // Count MAIN sessions only (is_sidechain = 0): the dropdown filters the session list, which
2815
+ // defaults to main-only, so a source's count must match what you'd see — not be inflated by the
2816
+ // many subagent sidechains each task spawns.
2817
+ `SELECT s.id, s.label, s.agent_id, s.config_dir,
2818
+ (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
2819
+ FROM sources s ORDER BY s.label`
2820
+ ).all();
2821
+ }
2822
+ function listProjects(db) {
2823
+ return db.prepare(`SELECT p.id, p.path,
2824
+ (SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count
2825
+ FROM projects p ORDER BY session_count DESC, p.path`).all();
2826
+ }
2827
+ function listModels(db) {
2828
+ return db.prepare(`SELECT DISTINCT model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).all().map((r) => r.model);
2829
+ }
2830
+ function toFtsQuery(raw) {
2831
+ return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
2832
+ }
2833
+ function listSessions(db, f) {
2834
+ const where = [];
2835
+ const params = [];
2836
+ if (f.source)
2837
+ where.push("s.source_id = ?"), params.push(f.source);
2838
+ if (f.project)
2839
+ where.push("s.project_id = ?"), params.push(f.project);
2840
+ if (f.from)
2841
+ where.push("s.started_at >= ?"), params.push(f.from);
2842
+ if (f.to)
2843
+ where.push("s.started_at <= ?"), params.push(f.to);
2844
+ if (f.kind === "main")
2845
+ where.push("s.is_sidechain = 0");
2846
+ if (f.kind === "subagent")
2847
+ where.push("s.is_sidechain = 1");
2848
+ if (f.model) {
2849
+ where.push("EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND t.model = ?)");
2850
+ params.push(f.model);
2851
+ }
2852
+ if (f.q && f.q.trim()) {
2853
+ const like = `%${f.q.trim()}%`;
2854
+ where.push(`(s.id IN (SELECT DISTINCT e.session_id FROM events e JOIN events_fts f ON f.rowid = e.rowid WHERE events_fts MATCH ?)
2855
+ OR s.slug LIKE ? OR s.ai_title LIKE ?
2856
+ OR s.project_id IN (SELECT id FROM projects WHERE path LIKE ?))`);
2857
+ params.push(toFtsQuery(f.q), like, like, like);
2858
+ }
2859
+ const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
2860
+ const total = db.prepare(`SELECT COUNT(*) n FROM sessions s ${whereSql}`).get(...params).n;
2861
+ const rows = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at, s.ended_at,
2862
+ s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
2863
+ (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
2864
+ FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
2865
+ ${whereSql}
2866
+ ORDER BY s.started_at DESC
2867
+ LIMIT ? OFFSET ?`).all(...params, f.limit, f.offset);
2868
+ const ids = rows.map((r) => r.id);
2869
+ const costBySession = /* @__PURE__ */ new Map();
2870
+ if (ids.length) {
2871
+ const ph = ids.map(() => "?").join(",");
2872
+ const usage = db.prepare(`SELECT session_id, model,
2873
+ SUM(input_tokens) i, SUM(output_tokens) o,
2874
+ SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
2875
+ FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`).all(...ids);
2876
+ for (const u of usage) {
2877
+ const acc = costBySession.get(u.session_id) ?? { tokens: 0, cost: 0, split: { input: 0, output: 0, cache_creation: 0, cache_read: 0 } };
2878
+ acc.tokens += u.i + u.o + u.cw + u.cr;
2879
+ acc.split.input += u.i;
2880
+ acc.split.output += u.o;
2881
+ acc.split.cache_creation += u.cw;
2882
+ acc.split.cache_read += u.cr;
2883
+ acc.cost += costForUsage(u.model, {
2884
+ input_tokens: u.i,
2885
+ output_tokens: u.o,
2886
+ cache_creation_input_tokens: u.cw,
2887
+ cache_read_input_tokens: u.cr
2888
+ });
2889
+ costBySession.set(u.session_id, acc);
2890
+ }
2891
+ }
2892
+ for (const r of rows) {
2893
+ const c = costBySession.get(r.id);
2894
+ r.tokens = c?.tokens ?? 0;
2895
+ r.token_split = c?.split ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
2896
+ r.cost = c ? Number(c.cost.toFixed(4)) : 0;
2897
+ r.title = r.ai_title || r.slug || null;
2898
+ }
2899
+ return { total, sessions: rows };
2900
+ }
2901
+ function getSession(db, id) {
2902
+ const session = db.prepare(`SELECT s.*, p.path AS project_path FROM sessions s
2903
+ LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`).get(id);
2904
+ if (!session)
2905
+ return null;
2906
+ const turns = db.prepare("SELECT * FROM turns WHERE session_id = ? ORDER BY seq").all(id);
2907
+ const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
2908
+ FROM events WHERE session_id = ? ORDER BY timestamp, seq`).all(id);
2909
+ const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
2910
+ workflow_run_id, workflow_name, status,
2911
+ total_duration_ms, total_tokens, input_json, result_summary,
2912
+ (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
2913
+ FROM tool_calls WHERE session_id = ?`).all(id);
2914
+ const toolsByEvent = /* @__PURE__ */ new Map();
2915
+ for (const t of toolRows) {
2916
+ if (!t.event_uuid)
2917
+ continue;
2918
+ (toolsByEvent.get(t.event_uuid) ?? toolsByEvent.set(t.event_uuid, []).get(t.event_uuid)).push(t);
2919
+ }
2920
+ const events = eventRows.map((e) => {
2921
+ const { text, thinking } = extractParts(e.raw_json);
2922
+ return {
2923
+ uuid: e.uuid,
2924
+ type: e.type,
2925
+ role: e.role,
2926
+ timestamp: e.timestamp,
2927
+ model: e.model,
2928
+ is_sidechain: e.is_sidechain,
2929
+ turn_id: e.turn_id,
2930
+ text,
2931
+ thinking,
2932
+ toolCalls: toolsByEvent.get(e.uuid) ?? []
2933
+ };
2934
+ });
2935
+ const usage = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
2936
+ SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
2937
+ FROM token_usage WHERE session_id = ? GROUP BY model`).all(id);
2938
+ const split = { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
2939
+ let cost = 0;
2940
+ for (const u of usage) {
2941
+ split.input += u.i;
2942
+ split.output += u.o;
2943
+ split.cache_creation += u.cw;
2944
+ split.cache_read += u.cr;
2945
+ cost += costForUsage(u.model, {
2946
+ input_tokens: u.i,
2947
+ output_tokens: u.o,
2948
+ cache_creation_input_tokens: u.cw,
2949
+ cache_read_input_tokens: u.cr
2950
+ });
2951
+ }
2952
+ session.tokens = split.input + split.output + split.cache_creation + split.cache_read;
2953
+ session.token_split = split;
2954
+ session.cost = Number(cost.toFixed(4));
2955
+ session.title = session.ai_title || session.slug || null;
2956
+ const cls = db.prepare(`SELECT category, complexity_score, complexity_band, signals_json, classifier_version
2957
+ FROM classifications WHERE scope = 'session' AND target_id = ?`).get(id);
2958
+ let classification = null;
2959
+ if (cls) {
2960
+ let signals = null;
2961
+ try {
2962
+ signals = cls.signals_json ? JSON.parse(cls.signals_json) : null;
2963
+ } catch {
2964
+ }
2965
+ classification = {
2966
+ category: cls.category,
2967
+ complexity_score: cls.complexity_score,
2968
+ complexity_band: cls.complexity_band,
2969
+ classifier_version: cls.classifier_version,
2970
+ signals
2971
+ };
2972
+ }
2973
+ let parent = null;
2974
+ if (session.parent_session_id) {
2975
+ const p = db.prepare(`SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
2976
+ FROM sessions ps LEFT JOIN turns t ON t.id = ?
2977
+ WHERE ps.id = ?`).get(session.parent_turn_id, session.parent_session_id);
2978
+ if (p)
2979
+ parent = { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null };
2980
+ }
2981
+ const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
2982
+ (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
2983
+ FROM sessions s WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
2984
+ for (const ch of children) {
2985
+ ch.title = ch.ai_title || ch.slug || null;
2986
+ const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
2987
+ SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
2988
+ FROM token_usage WHERE session_id = ? GROUP BY model`).all(ch.id);
2989
+ ch.tokens = u.reduce((a, r) => a + r.i + r.o + r.cw + r.cr, 0);
2990
+ ch.cost = Number(u.reduce((a, r) => a + costForUsage(r.model, { input_tokens: r.i, output_tokens: r.o, cache_creation_input_tokens: r.cw, cache_read_input_tokens: r.cr }), 0).toFixed(4));
2991
+ }
2992
+ const workflow_runs = db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
2993
+ (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
2994
+ FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
2995
+ WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
2996
+ ORDER BY t.seq`).all(id);
2997
+ return { session, turns, events, classification, parent, children, workflow_runs };
2998
+ }
2999
+ function xmlTag(text, tag) {
3000
+ return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
3001
+ }
3002
+ function getWorkflow(db, runId) {
3003
+ const wf = db.prepare(`SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,
3004
+ tc.session_id AS parent_session_id, t.seq AS turn_seq,
3005
+ ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
3006
+ FROM tool_calls tc
3007
+ LEFT JOIN turns t ON t.id = tc.turn_id
3008
+ LEFT JOIN sessions ps ON ps.id = tc.session_id
3009
+ WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
3010
+ ORDER BY t.seq
3011
+ LIMIT 1`).get(runId);
3012
+ if (!wf)
3013
+ return null;
3014
+ const agents = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
3015
+ (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
3016
+ FROM sessions s WHERE s.workflow_run_id = ? ORDER BY s.started_at`).all(runId);
3017
+ let total_tokens = 0;
3018
+ let total_cost = 0;
3019
+ let started_at = null;
3020
+ let ended_at = null;
3021
+ for (const a of agents) {
3022
+ a.title = a.ai_title || a.slug || null;
3023
+ const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
3024
+ SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
3025
+ FROM token_usage WHERE session_id = ? GROUP BY model`).all(a.id);
3026
+ a.tokens = u.reduce((acc, r) => acc + r.i + r.o + r.cw + r.cr, 0);
3027
+ a.cost = Number(u.reduce((acc, r) => acc + costForUsage(r.model, { input_tokens: r.i, output_tokens: r.o, cache_creation_input_tokens: r.cw, cache_read_input_tokens: r.cr }), 0).toFixed(4));
3028
+ total_tokens += a.tokens;
3029
+ total_cost += a.cost;
3030
+ if (a.started_at && (!started_at || a.started_at < started_at))
3031
+ started_at = a.started_at;
3032
+ if (a.ended_at && (!ended_at || a.ended_at > ended_at))
3033
+ ended_at = a.ended_at;
3034
+ }
3035
+ const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;
3036
+ let completion = null;
3037
+ if (wf.tool_use_id && wf.parent_session_id) {
3038
+ const ev = db.prepare(`SELECT raw_json FROM events
3039
+ WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
3040
+ ORDER BY timestamp DESC LIMIT 1`).get(wf.parent_session_id, `%${wf.tool_use_id}%`);
3041
+ if (ev) {
3042
+ const { text } = extractParts(ev.raw_json);
3043
+ const t = text ?? "";
3044
+ completion = { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
3045
+ }
3046
+ }
3047
+ return {
3048
+ run_id: wf.run_id,
3049
+ name: wf.name ?? null,
3050
+ status: wf.status ?? null,
3051
+ result_summary: wf.result_summary ?? null,
3052
+ completion,
3053
+ parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },
3054
+ agents,
3055
+ stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
3056
+ };
3057
+ }
3058
+ function listSkills(db, f = {}) {
3059
+ const where = ["tc.tool_name = 'Skill'", "tc.skill_name IS NOT NULL"];
3060
+ const params = [];
3061
+ if (f.q && f.q.trim())
3062
+ where.push("tc.skill_name LIKE ?"), params.push(`%${f.q.trim()}%`);
3063
+ if (f.source)
3064
+ where.push("s.source_id = ?"), params.push(f.source);
3065
+ if (f.project)
3066
+ where.push("s.project_id = ?"), params.push(f.project);
3067
+ const rows = db.prepare(`SELECT tc.skill_name AS name,
3068
+ COUNT(*) AS call_count,
3069
+ COUNT(DISTINCT tc.skill_id) AS version_count,
3070
+ MAX(e.timestamp) AS last_fired,
3071
+ GROUP_CONCAT(DISTINCT s.source_id) AS sources
3072
+ FROM tool_calls tc
3073
+ JOIN sessions s ON s.id = tc.session_id
3074
+ LEFT JOIN events e ON e.uuid = tc.event_uuid
3075
+ WHERE ${where.join(" AND ")}
3076
+ GROUP BY tc.skill_name
3077
+ ORDER BY call_count DESC, name`).all(...params);
3078
+ for (const r of rows)
3079
+ r.sources = r.sources ? String(r.sources).split(",").filter(Boolean) : [];
3080
+ return rows;
3081
+ }
3082
+ function getSkill(db, name) {
3083
+ const versions = db.prepare(`SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
3084
+ (SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
3085
+ FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`).all(name);
3086
+ const sessions = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,
3087
+ tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count
3088
+ FROM tool_calls tc
3089
+ JOIN sessions s ON s.id = tc.session_id
3090
+ LEFT JOIN projects p ON p.id = s.project_id
3091
+ LEFT JOIN events e ON e.uuid = tc.event_uuid
3092
+ WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?
3093
+ GROUP BY s.id, tc.skill_id
3094
+ ORDER BY fired_at DESC`).all(name);
3095
+ if (!versions.length && !sessions.length)
3096
+ return null;
3097
+ for (const s of sessions)
3098
+ s.title = s.ai_title || s.slug || null;
3099
+ const call_count = sessions.reduce((a, s) => a + s.fire_count, 0);
3100
+ return { name, versions, sessions, call_count };
3101
+ }
3102
+
3103
+ // ../server/dist/app.js
3104
+ import { existsSync as existsSync6 } from "fs";
3105
+ import Fastify from "fastify";
3106
+ import fastifyStatic from "@fastify/static";
3107
+
3108
+ // ../server/dist/dashboard.js
3109
+ function sessionWhere(f) {
3110
+ const where = [];
3111
+ const params = [];
3112
+ if (f.source)
3113
+ where.push("s.source_id = ?"), params.push(f.source);
3114
+ if (f.from)
3115
+ where.push("s.started_at >= ?"), params.push(f.from);
3116
+ if (f.to)
3117
+ where.push("s.started_at <= ?"), params.push(f.to);
3118
+ return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
3119
+ }
3120
+ var zeroSplit = () => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });
3121
+ function addUsage(acc, r) {
3122
+ acc.input += r.i ?? 0;
3123
+ acc.output += r.o ?? 0;
3124
+ acc.cache_creation += r.cw ?? 0;
3125
+ acc.cache_read += r.cr ?? 0;
3126
+ }
3127
+ function pct(sorted, p) {
3128
+ if (sorted.length === 0)
3129
+ return 0;
3130
+ const rank = Math.ceil(p / 100 * sorted.length) - 1;
3131
+ return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)];
3132
+ }
3133
+ function dashboardOverview(db, f) {
3134
+ const w = sessionWhere(f);
3135
+ const counts = db.prepare(`SELECT
3136
+ COUNT(*) sessions,
3137
+ SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,
3138
+ SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,
3139
+ SUM(turn_count) turns,
3140
+ COUNT(DISTINCT project_id) projects
3141
+ FROM sessions s ${w.sql}`).get(...w.params);
3142
+ const toolCount = db.prepare(`SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id ${w.sql}`).get(...w.params).n;
3143
+ const usage = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
3144
+ SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
3145
+ FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
3146
+ GROUP BY t.model`).all(...w.params);
3147
+ const tokens = zeroSplit();
3148
+ let cost = 0;
3149
+ const unpriced = [];
3150
+ for (const u of usage) {
3151
+ addUsage(tokens, u);
3152
+ cost += costForUsage(u.model, { input_tokens: u.i, output_tokens: u.o, cache_creation_input_tokens: u.cw, cache_read_input_tokens: u.cr });
3153
+ if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr))
3154
+ unpriced.push(u.model);
3155
+ }
3156
+ const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
3157
+ const durs = db.prepare(`SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
3158
+ ${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL ORDER BY tn.duration_ms`).all(...w.params).map((r) => r.d);
3159
+ return {
3160
+ range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },
3161
+ sessions: counts.sessions ?? 0,
3162
+ sessions_main: counts.main ?? 0,
3163
+ sessions_subagent: counts.subagent ?? 0,
3164
+ turns: counts.turns ?? 0,
3165
+ projects: counts.projects ?? 0,
3166
+ tool_calls: toolCount ?? 0,
3167
+ tokens,
3168
+ // {input, output, cache_creation, cache_read}
3169
+ total_tokens: totalTokens,
3170
+ cache_read_ratio: totalTokens ? tokens.cache_read / totalTokens : 0,
3171
+ cost: Number(cost.toFixed(4)),
3172
+ unpriced_models: [...new Set(unpriced)].sort(),
3173
+ turn_duration_ms: { p50: pct(durs, 50), p95: pct(durs, 95), count: durs.length }
3174
+ };
3175
+ }
3176
+ function chooseBucket(db, f) {
3177
+ const w = sessionWhere(f);
3178
+ const r = db.prepare(`SELECT MIN(started_at) mn, MAX(started_at) mx FROM sessions s ${w.sql}`).get(...w.params);
3179
+ if (!r?.mn || !r?.mx)
3180
+ return "day";
3181
+ const spanDays = (Date.parse(r.mx) - Date.parse(r.mn)) / 864e5;
3182
+ if (spanDays <= 92)
3183
+ return "day";
3184
+ if (spanDays <= 730)
3185
+ return "week";
3186
+ return "month";
3187
+ }
3188
+ var BUCKET_EXPR = {
3189
+ day: "strftime('%Y-%m-%d', s.started_at)",
3190
+ week: "strftime('%Y-W%W', s.started_at)",
3191
+ month: "strftime('%Y-%m', s.started_at)"
3192
+ };
3193
+ function dashboardTimeseries(db, f, bucketParam) {
3194
+ const bucket = bucketParam === "day" || bucketParam === "week" || bucketParam === "month" ? bucketParam : chooseBucket(db, f);
3195
+ const w = sessionWhere(f);
3196
+ const expr = BUCKET_EXPR[bucket];
3197
+ const byBucket = /* @__PURE__ */ new Map();
3198
+ const get = (b) => {
3199
+ let r = byBucket.get(b);
3200
+ if (!r) {
3201
+ r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0 };
3202
+ byBucket.set(b, r);
3203
+ }
3204
+ return r;
3205
+ };
3206
+ for (const u of db.prepare(`SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
3207
+ SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
3208
+ FROM token_usage t JOIN sessions s ON s.id = t.session_id
3209
+ ${w.sql} GROUP BY b, t.model`).all(...w.params)) {
3210
+ if (!u.b)
3211
+ continue;
3212
+ const r = get(u.b);
3213
+ addUsage(r, u);
3214
+ r.cost += costForUsage(u.model, { input_tokens: u.i, output_tokens: u.o, cache_creation_input_tokens: u.cw, cache_read_input_tokens: u.cr });
3215
+ }
3216
+ for (const s of db.prepare(`SELECT ${expr} b, COUNT(*) n FROM sessions s ${w.sql} GROUP BY b`).all(...w.params)) {
3217
+ if (s.b)
3218
+ get(s.b).sessions = s.n;
3219
+ }
3220
+ for (const t of db.prepare(`SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${w.sql} GROUP BY b`).all(...w.params)) {
3221
+ if (t.b)
3222
+ get(t.b).turns = t.n;
3223
+ }
3224
+ const series = [...byBucket.values()].sort((a, b) => a.bucket.localeCompare(b.bucket));
3225
+ for (const r of series)
3226
+ r.cost = Number(r.cost.toFixed(4));
3227
+ return { bucket, series };
3228
+ }
3229
+ function dashboardBreakdowns(db, f) {
3230
+ const w = sessionWhere(f);
3231
+ const modelRows = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
3232
+ SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
3233
+ COUNT(DISTINCT t.session_id) sessions
3234
+ FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
3235
+ GROUP BY t.model ORDER BY (SUM(t.input_tokens)+SUM(t.output_tokens)+SUM(t.cache_creation_input_tokens)+SUM(t.cache_read_input_tokens)) DESC`).all(...w.params);
3236
+ const byModel = modelRows.map((u) => ({
3237
+ model: u.model,
3238
+ tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
3239
+ total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
3240
+ cost: Number(costForUsage(u.model, { input_tokens: u.i, output_tokens: u.o, cache_creation_input_tokens: u.cw, cache_read_input_tokens: u.cr }).toFixed(4)),
3241
+ sessions: u.sessions ?? 0,
3242
+ priced: !!rateForModel(u.model)
3243
+ }));
3244
+ const bySource = db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
3245
+ FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`).all(...w.params);
3246
+ const mainW = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
3247
+ const byCategory = db.prepare(`SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
3248
+ ${mainW.sql} GROUP BY c.category ORDER BY n DESC`).all(...mainW.params);
3249
+ const byComplexity = db.prepare(`SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
3250
+ ${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`).all(...mainW.params);
3251
+ const tools = db.prepare(`SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
3252
+ ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`).all(...w.params);
3253
+ const skills = db.prepare(`SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
3254
+ ${w.sql ? w.sql + " AND" : "WHERE"} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`).all(...w.params);
3255
+ const skillVersions = db.prepare(`SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
3256
+ FROM tool_calls tc
3257
+ JOIN skills sk ON sk.id = tc.skill_id
3258
+ JOIN sessions s ON s.id = tc.session_id
3259
+ ${w.sql ? w.sql + " AND" : "WHERE"} tc.skill_id IS NOT NULL
3260
+ GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`).all(...w.params);
3261
+ const subagentByType = db.prepare(`SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
3262
+ ${w.sql ? w.sql + " AND" : "WHERE"} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`).all(...w.params);
3263
+ const perSession = db.prepare(`SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
3264
+ ${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
3265
+ GROUP BY tc.session_id`).all(...w.params);
3266
+ const counts = perSession.map((r) => r.n);
3267
+ const subagentFanout = {
3268
+ by_type: subagentByType,
3269
+ sessions_with_subagents: counts.length,
3270
+ total_spawns: counts.reduce((a, b) => a + b, 0),
3271
+ max_per_session: counts.length ? Math.max(...counts) : 0,
3272
+ avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0
3273
+ };
3274
+ return { by_model: byModel, by_source: bySource, by_category: byCategory, by_complexity: byComplexity, tools, skills, skill_versions: skillVersions, subagent_fanout: subagentFanout };
3275
+ }
3276
+
3277
+ // ../server/dist/refresh.js
3278
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
3279
+ import { join as join6 } from "path";
3280
+ function originAllowed(origin) {
3281
+ if (!origin)
3282
+ return true;
3283
+ try {
3284
+ const h = new URL(origin).hostname;
3285
+ return h === "127.0.0.1" || h === "localhost" || h === "::1";
3286
+ } catch {
3287
+ return false;
3288
+ }
3289
+ }
3290
+ function runRefresh() {
3291
+ const dataDir = resolveDataDir(findRepoRoot());
3292
+ const lock = acquireLock(join6(dataDir, ".agent-lens.lock"));
3293
+ if (!lock)
3294
+ return null;
3295
+ try {
3296
+ const collected = collectAll();
3297
+ const archiveRoot = process.env.AGENT_LENS_ARCHIVE || join6(dataDir, "archive");
3298
+ if (!existsSync5(archiveRoot))
3299
+ mkdirSync5(archiveRoot, { recursive: true });
3300
+ runIngest([]);
3301
+ return { ok: true, collected };
3302
+ } finally {
3303
+ lock.release();
3304
+ }
3305
+ }
3306
+
3307
+ // ../server/dist/app.js
3308
+ async function createApp(db, opts = {}) {
3309
+ const app = Fastify({ logger: false });
3310
+ app.get("/api/health", async () => ({ ok: true, last_ingested: lastIngested(db) }));
3311
+ app.post("/api/refresh", async (req, reply) => {
3312
+ if (!originAllowed(req.headers.origin)) {
3313
+ return reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin refresh blocked" } });
3314
+ }
3315
+ let result;
3316
+ try {
3317
+ result = runRefresh();
3318
+ } catch (e) {
3319
+ return reply.code(500).send({ error: { code: "REFRESH_FAILED", message: String(e?.message ?? e) } });
3320
+ }
3321
+ if (!result) {
3322
+ return reply.code(409).send({ error: { code: "REFRESH_IN_PROGRESS", message: "a collect/ingest run is already in progress" } });
3323
+ }
3324
+ return { ok: true, collected: result.collected, last_ingested: lastIngested(db) };
3325
+ });
3326
+ app.get("/api/sources", async () => listSources(db));
3327
+ app.get("/api/projects", async () => listProjects(db));
3328
+ app.get("/api/models", async () => listModels(db));
3329
+ const dashFilters = (req) => {
3330
+ const q = req.query;
3331
+ return { source: q.source, from: q.from, to: q.to };
3332
+ };
3333
+ app.get("/api/dashboard/overview", async (req) => dashboardOverview(db, dashFilters(req)));
3334
+ app.get("/api/dashboard/timeseries", async (req) => {
3335
+ const q = req.query;
3336
+ return dashboardTimeseries(db, dashFilters(req), q.bucket);
3337
+ });
3338
+ app.get("/api/dashboard/breakdowns", async (req) => dashboardBreakdowns(db, dashFilters(req)));
3339
+ app.get("/api/sessions", async (req) => {
3340
+ const q = req.query;
3341
+ return listSessions(db, {
3342
+ source: q.source,
3343
+ project: q.project,
3344
+ model: q.model,
3345
+ q: q.q,
3346
+ from: q.from,
3347
+ to: q.to,
3348
+ kind: q.kind === "main" || q.kind === "subagent" ? q.kind : void 0,
3349
+ limit: Math.min(Number(q.limit) || 50, 200),
3350
+ offset: Number(q.offset) || 0
3351
+ });
3352
+ });
3353
+ app.get("/api/sessions/:id", async (req, reply) => {
3354
+ const { id } = req.params;
3355
+ const result = getSession(db, id);
3356
+ if (!result)
3357
+ return reply.code(404).send({ error: "not found" });
3358
+ return result;
3359
+ });
3360
+ app.get("/api/workflows/:run_id", async (req, reply) => {
3361
+ const { run_id } = req.params;
3362
+ const result = getWorkflow(db, run_id);
3363
+ if (!result)
3364
+ return reply.code(404).send({ error: "not found" });
3365
+ return result;
3366
+ });
3367
+ app.get("/api/skills", async (req) => {
3368
+ const q = req.query;
3369
+ return listSkills(db, { q: q.q, source: q.source, project: q.project });
3370
+ });
3371
+ app.get("/api/skills/:name", async (req, reply) => {
3372
+ const { name } = req.params;
3373
+ const result = getSkill(db, decodeURIComponent(name));
3374
+ if (!result)
3375
+ return reply.code(404).send({ error: "not found" });
3376
+ return result;
3377
+ });
3378
+ app.get("/api/sessions/:id/export.md", async (req, reply) => {
3379
+ const { id } = req.params;
3380
+ const result = getSession(db, id);
3381
+ if (!result)
3382
+ return reply.code(404).send({ error: "not found" });
3383
+ const s = result.session;
3384
+ const events = result.events.map((e) => ({
3385
+ type: e.type,
3386
+ role: e.role,
3387
+ timestamp: e.timestamp,
3388
+ text: e.text,
3389
+ thinking: e.thinking,
3390
+ toolCalls: e.toolCalls.map((t) => ({
3391
+ tool_name: t.tool_name,
3392
+ skill_name: t.skill_name,
3393
+ agent_type: t.agent_type,
3394
+ input_json: t.input_json,
3395
+ status: t.status
3396
+ }))
3397
+ }));
3398
+ const md = sessionToMarkdown({
3399
+ id: s.id,
3400
+ title: s.title,
3401
+ source: s.source_id,
3402
+ project: s.project_path,
3403
+ model: null,
3404
+ started_at: s.started_at,
3405
+ ended_at: s.ended_at
3406
+ }, events);
3407
+ reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="session-${id.slice(0, 8)}.md"`).send(md);
3408
+ });
3409
+ if (opts.webDist && existsSync6(opts.webDist)) {
3410
+ await app.register(fastifyStatic, { root: opts.webDist });
3411
+ app.setNotFoundHandler((req, reply) => {
3412
+ if (req.url.startsWith("/api/"))
3413
+ return reply.code(404).send({ error: "not found" });
3414
+ return reply.sendFile("index.html");
3415
+ });
3416
+ }
3417
+ return app;
3418
+ }
3419
+
3420
+ // ../server/dist/run.js
3421
+ async function startServer() {
3422
+ const here = dirname5(fileURLToPath2(import.meta.url));
3423
+ const repoRoot = findRepoRoot(here);
3424
+ const dbPath = process.env.AGENT_LENS_DB || join7(resolveDataDir(repoRoot), "agent-lens.db");
3425
+ const host = process.env.AGENT_LENS_HOST || "127.0.0.1";
3426
+ const port = Number(process.env.AGENT_LENS_PORT || 4477);
3427
+ const webDist = resolveWebDist(here, repoRoot);
3428
+ if (host !== "127.0.0.1" && host !== "localhost" && !process.env.AGENT_LENS_ALLOW_NONLOCAL) {
3429
+ console.error(`agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`);
3430
+ process.exit(1);
3431
+ }
3432
+ if (!existsSync7(dbPath)) {
3433
+ console.error(`agent-lens-server: db not found: ${dbPath} (run ingest first)`);
3434
+ process.exit(1);
3435
+ }
3436
+ const db = openReadonly(dbPath);
3437
+ const app = await createApp(db, { webDist });
3438
+ try {
3439
+ await app.listen({ host, port });
3440
+ console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);
3441
+ if (!existsSync7(webDist))
3442
+ console.log(" note: web SPA not built \u2014 run `pnpm --filter @agent-lens/web build`");
3443
+ } catch (err) {
3444
+ console.error(err);
3445
+ process.exit(1);
3446
+ }
3447
+ }
3448
+
3449
+ // src/watch.ts
3450
+ import { existsSync as existsSync8 } from "fs";
3451
+ import { join as join8 } from "path";
3452
+ import chokidar from "chokidar";
3453
+ async function runWatch(opts = {}) {
3454
+ const sources = loadSources();
3455
+ const lockPath = join8(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
3456
+ let running = false;
3457
+ let pending = false;
3458
+ const cycle = () => {
3459
+ if (running) {
3460
+ pending = true;
3461
+ return;
3462
+ }
3463
+ running = true;
3464
+ const lock = acquireLock(lockPath);
3465
+ if (!lock) {
3466
+ running = false;
3467
+ console.error("agent-lens watch: another collect/ingest run is in progress \u2014 skipping this cycle");
3468
+ return;
3469
+ }
3470
+ try {
3471
+ const stats = collectAll({ sources });
3472
+ console.log(
3473
+ `agent-lens watch: collected ${stats.scanned} files (${stats.appended} appended, ${stats.snapshots} snapshots) \u2014 ingesting`
3474
+ );
3475
+ runIngest([]);
3476
+ } catch (e) {
3477
+ console.error("agent-lens watch: cycle failed:", e);
3478
+ } finally {
3479
+ lock.release();
3480
+ running = false;
3481
+ if (pending) {
3482
+ pending = false;
3483
+ setTimeout(cycle, 0);
3484
+ }
3485
+ }
3486
+ };
3487
+ cycle();
3488
+ const watchPaths = sources.map((s) => join8(s.configDir, "projects")).filter((p) => existsSync8(p));
3489
+ if (watchPaths.length === 0) {
3490
+ console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
3491
+ }
3492
+ const watcher = chokidar.watch(watchPaths, {
3493
+ ignoreInitial: true,
3494
+ usePolling: !!opts.poll,
3495
+ awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 }
3496
+ });
3497
+ let debounce = null;
3498
+ const trigger = () => {
3499
+ if (debounce) clearTimeout(debounce);
3500
+ debounce = setTimeout(cycle, 3e3);
3501
+ };
3502
+ watcher.on("add", trigger).on("change", trigger);
3503
+ if (opts.intervalSec && opts.intervalSec > 0) {
3504
+ setInterval(cycle, opts.intervalSec * 1e3);
3505
+ }
3506
+ console.log(
3507
+ `agent-lens watch: watching ${watchPaths.length} source dir(s)` + (opts.intervalSec ? ` + every ${opts.intervalSec}s` : "") + " \u2014 Ctrl-C to stop"
3508
+ );
3509
+ return new Promise(() => {
3510
+ });
3511
+ }
3512
+
3513
+ // src/index.ts
3514
+ function version() {
3515
+ try {
3516
+ const pkg = JSON.parse(readFileSync5(join9(dirname6(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
3517
+ return pkg.version ?? "0.0.0";
3518
+ } catch {
3519
+ return "0.0.0";
3520
+ }
3521
+ }
3522
+ function withLock(fn) {
3523
+ const lockPath = join9(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
3524
+ const lock = acquireLock(lockPath);
3525
+ if (!lock) {
3526
+ console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");
3527
+ return;
3528
+ }
3529
+ try {
3530
+ fn();
3531
+ } finally {
3532
+ lock.release();
3533
+ }
3534
+ }
3535
+ var cli = cac("agent-lens");
3536
+ cli.command("collect", "Copy new session traces into the local archive").option("--then-ingest", "Run ingest immediately after collecting").action((opts) => {
3537
+ withLock(() => {
3538
+ const s = collectAll();
3539
+ console.log(
3540
+ `agent-lens: collect done \u2014 ${s.sources} source(s), ${s.scanned} scanned, ${s.copied} copied, ${s.appended} appended, ${s.diverged} diverged, ${s.compacted} compacted, ${s.snapshots} snapshots`
3541
+ );
3542
+ if (opts.thenIngest) runIngest([]);
3543
+ });
3544
+ });
3545
+ cli.command("ingest", "Build/update the SQLite projection from the archive").option("--full", "Ignore incremental state and re-read every file").option("--db <path>", "SQLite DB path").option("--archive <path>", "Archive directory").action((opts) => {
3546
+ const argv = [];
3547
+ if (opts.full) argv.push("--full");
3548
+ if (opts.db) argv.push("--db", opts.db);
3549
+ if (opts.archive) argv.push("--archive", opts.archive);
3550
+ withLock(() => runIngest(argv));
3551
+ });
3552
+ cli.command("serve", "Serve the web UI + read-only API on 127.0.0.1").action(async () => {
3553
+ await startServer();
3554
+ });
3555
+ cli.command("watch", "Watch sources and collect + ingest on change (resident process)").option("--interval <sec>", "Also run on a fixed interval (seconds)").option("--poll", "Use polling \u2014 for network filesystems where fs events don't fire").action(async (opts) => {
3556
+ await runWatch({ intervalSec: opts.interval ? Number(opts.interval) : void 0, poll: !!opts.poll });
3557
+ });
3558
+ cli.command("metrics", "Re-run classification over an already-ingested DB").option("--db <path>", "SQLite DB path").action((opts) => {
3559
+ runMetrics(opts.db ? ["--db", opts.db] : []);
3560
+ });
3561
+ cli.command(
3562
+ "service <action> [target]",
3563
+ "Install/uninstall/status OS services \u2014 action: install|uninstall|status; target: collector|server|all (default all)"
3564
+ ).option("--times <hours>", "Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)").action((action, target, opts) => {
3565
+ const targets = parseTargets(target);
3566
+ const hours = action === "install" ? parseHours(opts.times) : void 0;
3567
+ runService(action, { cliEntry: fileURLToPath3(import.meta.url), hours, targets });
3568
+ });
3569
+ cli.help();
3570
+ cli.version(version());
3571
+ cli.parse();
3572
+ //# sourceMappingURL=agent-lens.js.map