@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.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/agent-lens.js +3572 -0
- package/dist/agent-lens.js.map +1 -0
- package/package.json +64 -0
- package/web/apple-touch-icon.png +0 -0
- package/web/assets/Dashboard-CPjOOCyK.js +69 -0
- package/web/assets/SessionView-iEtAsqjR.js +2 -0
- package/web/assets/SkillView-C6vlyg0Z.js +1 -0
- package/web/assets/SkillsView-B2W0OtAw.js +1 -0
- package/web/assets/WorkflowView-D5N-tAZr.js +1 -0
- package/web/assets/index-B0Y86jYR.css +1 -0
- package/web/assets/index-DaWyEmDk.js +68 -0
- package/web/assets/index-RKLKBrv5.js +29 -0
- package/web/assets/logo-Cd7Wsq5o.png +0 -0
- package/web/favicon-16.png +0 -0
- package/web/favicon-32.png +0 -0
- package/web/index.html +25 -0
|
@@ -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
|