@swestash/agent-lens 0.13.3 → 0.14.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/dist/agent-lens.js +85 -74
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/404.html +1 -1
- package/web/assets/{AboutView-Bs9oC-qO.js → AboutView-BT3khsXF.js} +1 -1
- package/web/assets/{Dashboard-ChkGvwY5.js → Dashboard-BpHfCxya.js} +1 -1
- package/web/assets/{FileView-Ds04pgGe.js → FileView-BnBOTV_P.js} +1 -1
- package/web/assets/{FilesView-DQRiYUkL.js → FilesView-DxZpH4mQ.js} +1 -1
- package/web/assets/{SecurityView-CYJCfLrm.js → SecurityView-EWa7NdWS.js} +1 -1
- package/web/assets/SessionView-DwHI6Yj5.js +15 -0
- package/web/assets/{SkillView-BCH5ldzv.js → SkillView-BfgC6TUO.js} +1 -1
- package/web/assets/{SkillsView-Nlyh5MJW.js → SkillsView-CutJX709.js} +1 -1
- package/web/assets/{WorkflowView-JwRwz2E2.js → WorkflowView-D48zVacX.js} +1 -1
- package/web/assets/{index-r-fSz0IT.js → index-DAAJAs1W.js} +2 -2
- package/web/assets/{index-C2D0y2yH.js → index-DrIth0LC.js} +1 -1
- package/web/assets/{jsonish-mD--ddqP.js → jsonish-BI7lbDY8.js} +1 -1
- package/web/assets/{useResetOn-2tqWddId.js → useResetOn-DeM1LqwP.js} +1 -1
- package/web/index.html +1 -1
- package/web/assets/SessionView-BBpziIqZ.js +0 -15
package/dist/agent-lens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs","../../core/src/schema.ts","../../core/src/sqlite.ts","../../core/src/security.ts","../../core/src/secrets.ts","../../core/src/redact-export.ts","../../core/src/markdown.ts","../../core/src/errors.ts","../../core/src/user-error.ts","../../core/src/pricing.ts","../../core/src/rawjson.ts","../../core/src/paths.ts","../../core/src/config.ts","../../core/src/version.ts","../../core/src/sources.ts","../../core/src/projects.ts","../../core/src/collect.ts","../../core/src/lock.ts","../../core/src/service.ts","../../ingest/src/run.ts","../../ingest/src/db.ts","../../ingest/src/adapters/claude-code.ts","../../ingest/src/dirtyset.ts","../../ingest/src/storedjson.ts","../../ingest/src/classify.ts","../../ingest/src/detect.ts","../../ingest/src/errors.ts","../../ingest/src/filechanges.ts","../../ingest/src/canonicalize.ts","../../ingest/src/pipeline.ts","../../ingest/src/workflows.ts","../../ingest/src/sidecar.ts","../../ingest/src/redact.ts","../../ingest/src/fileread.ts","../../ingest/src/meta.ts","../../ingest/src/toolResults.ts","../../server/src/run.ts","../../server/src/db.ts","../../server/src/sql-util.ts","../../server/src/usage.ts","../../server/src/app.ts","../../server/src/export.ts","../../server/src/dashboard.ts","../../server/src/refresh.ts","../../server/src/triage.ts","../../server/src/about.ts","../../server/src/prefs.ts","../src/watch.ts"],"sourcesContent":["/**\n * `agent-lens` — the unified CLI. One binary dispatching the pipeline: collect → ingest → serve,\n * plus watch (resident collect+ingest) and service (install collect+ingest and/or serve as OS\n * services). Bundled into a single file by tsup so it installs as one npm package (ADR-010).\n */\nimport { existsSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { cac } from \"cac\";\nimport {\n acquireLock,\n collectAll,\n findRepoRoot,\n loadSources,\n parseHours,\n parseTargets,\n resolveArchiveDir,\n resolveConfigFile,\n resolveDataDir,\n resolveDbPath,\n resolvePricing,\n resolveRetention,\n resolveServerConfig,\n resolveVersion,\n runService,\n triageDbFor,\n UserError,\n} from \"@agent-lens/core\";\nimport { runIngest, runMetrics } from \"@agent-lens/ingest\";\nimport { startServer, openReadonly, renderSessionExport, parseRedactionLevel } from \"@agent-lens/server\";\nimport { runWatch } from \"./watch.js\";\n\n// `agent-lens --version`. Shared with the server's /api/health so the CLI and the web UI can never\n// disagree about what build this is (ADR-027). A published install reports \"0.9.6\"; a clone reports\n// the richer \"v0.9.6-3-gabc1234\", which is the more useful answer for a dev build.\nfunction version(): string {\n return resolveVersion().version;\n}\n\n/**\n * The process's error boundary. A `UserError` is something the user typed or configured, so it\n * prints as one line and exits 1 — a stack trace there is noise the user can't act on. Anything\n * else is a bug, and is re-thrown with its stack intact rather than flattened into a message that\n * would read like their mistake.\n */\nfunction die(e: unknown): never {\n if (e instanceof UserError) {\n console.error(`agent-lens: ${e.message}`);\n process.exit(1);\n }\n throw e;\n}\n\n/** Run `fn` under the single-instance lock so collect/ingest never overlap another run. */\nfunction withLock(fn: () => void): void {\n const lockPath = join(resolveDataDir(findRepoRoot()), \".agent-lens.lock\");\n const lock = acquireLock(lockPath);\n if (!lock) {\n console.error(\"agent-lens: another collect/ingest run is in progress — skipping\");\n return;\n }\n try {\n fn();\n } finally {\n lock.release();\n }\n}\n\nconst cli = cac(\"agent-lens\");\n\ncli\n .command(\"collect\", \"Copy new session traces into the local archive\")\n .option(\"--then-ingest\", \"Run ingest immediately after collecting\")\n .action((opts: { thenIngest?: boolean }) => {\n withLock(() => {\n const s = collectAll();\n console.log(\n `agent-lens: collect done — ${s.sources} source(s), ${s.scanned} scanned, ${s.copied} copied, ` +\n `${s.appended} appended, ${s.diverged} diverged, ${s.compacted} compacted, ${s.snapshots} snapshots`,\n );\n if (opts.thenIngest) runIngest([]);\n });\n });\n\ncli\n .command(\"ingest\", \"Build/update the SQLite projection from the archive\")\n .option(\"--full\", \"Ignore incremental state and re-read every file\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .option(\"--archive <path>\", \"Archive directory\")\n .action((opts: { full?: boolean; db?: string; archive?: string }) => {\n const argv: string[] = [];\n if (opts.full) argv.push(\"--full\");\n if (opts.db) argv.push(\"--db\", opts.db);\n if (opts.archive) argv.push(\"--archive\", opts.archive);\n withLock(() => runIngest(argv));\n });\n\ncli\n .command(\"serve\", \"Serve the web UI + read-only API on 127.0.0.1\")\n .option(\"--port <n>\", \"HTTP port (overrides AGENT_LENS_PORT / config server.port; default 4477)\")\n .option(\"--host <host>\", \"Bind host (overrides AGENT_LENS_HOST / config server.host; loopback only unless AGENT_LENS_ALLOW_NONLOCAL=1)\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .action(async (opts: { port?: string; host?: string; db?: string }) => {\n await startServer({ port: opts.port, host: opts.host, db: opts.db });\n });\n\ncli\n .command(\"watch\", \"Watch sources and collect + ingest on change (resident process)\")\n .option(\"--interval <sec>\", \"Also run on a fixed interval (seconds)\")\n .option(\"--poll\", \"Use polling — for network filesystems where fs events don't fire\")\n .action(async (opts: { interval?: string; poll?: boolean }) => {\n await runWatch({ intervalSec: opts.interval ? Number(opts.interval) : undefined, poll: !!opts.poll });\n });\n\ncli\n .command(\"metrics\", \"Re-run classification over an already-ingested DB\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .action((opts: { db?: string }) => {\n runMetrics(opts.db ? [\"--db\", opts.db] : []);\n });\n\ncli\n .command(\"export <sessionId>\", \"Write a session to a shareable Markdown file (redacted by default)\")\n .option(\"--out <file>\", \"Write to this file (default: stdout)\")\n .option(\"--level <level>\", \"Redaction level: secrets (default) | structure\")\n .option(\"--no-redact\", \"Verbatim, UNREDACTED output (opt-out)\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .action((sessionId: string, opts: { out?: string; level?: string; redact?: boolean; db?: string }) => {\n const { path: dbPath } = resolveDbPath(opts.db);\n if (!existsSync(dbPath)) {\n console.error(`agent-lens: db not found: ${dbPath} (run ingest first)`);\n process.exit(1);\n }\n // cac sets `redact:false` for --no-redact. Otherwise the level flag (default: secrets).\n const level = opts.redact === false ? \"off\" : parseRedactionLevel(opts.level);\n const out = renderSessionExport(openReadonly(dbPath), sessionId, level);\n if (!out) {\n console.error(`agent-lens: session not found: ${sessionId}`);\n process.exit(1);\n }\n if (opts.out) {\n writeFileSync(opts.out, out.markdown);\n console.error(`agent-lens: wrote ${out.filename} → ${opts.out} (redaction: ${level})`);\n } else {\n process.stdout.write(out.markdown);\n }\n });\n\ncli\n .command(\n \"service <action> [target]\",\n \"Install/uninstall/status OS services — action: install|uninstall|status; target: collector|server|all (default all)\",\n )\n .option(\"--times <hours>\", \"Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)\")\n .action((action: string, target: string | undefined, opts: { times?: string }) => {\n const targets = parseTargets(target);\n const hours = action === \"install\" ? parseHours(opts.times) : undefined;\n // The bundled CLI file is this module; bake its absolute path + node into every unit/plist/task.\n runService(action, { cliEntry: fileURLToPath(import.meta.url), hours, targets });\n });\n\ncli\n .command(\"config\", \"Show the effective configuration (sources, paths, server) and where each value came from\")\n .action(() => {\n const repoRoot = findRepoRoot();\n const dataDir = resolveDataDir(repoRoot);\n const configFile = resolveConfigFile(repoRoot, dataDir);\n const { port, host, portOrigin, hostOrigin } = resolveServerConfig();\n const { path: dbPath, origin: dbOrigin } = resolveDbPath();\n const archive = resolveArchiveDir(repoRoot);\n const triageDb = triageDbFor(dbPath);\n const { keepDays, origin: keepOrigin } = resolveRetention();\n // resolvePricing, not installPricing: this command reports the table, and the Pricing block below\n // already lists what was rejected — the stderr warning would just say it twice.\n const pricing = resolvePricing();\n\n console.log(\"agent-lens config (precedence: flag > env > config file > default)\\n\");\n console.log(\"Paths\");\n console.log(` config file ${configFile ?? \"(none — using built-in default source)\"}`);\n console.log(` data dir ${dataDir}${process.env.AGENT_LENS_DATA ? \" [env]\" : \"\"}`);\n console.log(` archive ${archive} [fixed: <data dir>/archive]`);\n console.log(` db ${dbPath} [${dbOrigin}]`);\n console.log(` triage db ${triageDb} [fixed: beside the db]`);\n console.log(\"\\nServer\");\n console.log(` host ${host} [${hostOrigin}]`);\n console.log(` port ${port} [${portOrigin}]`);\n console.log(` non-local bind ${process.env.AGENT_LENS_ALLOW_NONLOCAL ? \"allowed (AGENT_LENS_ALLOW_NONLOCAL)\" : \"blocked (loopback only)\"}`);\n console.log(\"\\nRetention\");\n console.log(` .versions keep ${keepDays} day(s) [${keepOrigin}]`);\n console.log(\"\\nPricing\");\n console.log(` models priced ${Object.keys(pricing.table).length} [${pricing.origin}]`);\n if (pricing.applied.length) console.log(` overrides ${pricing.applied.join(\", \")}`);\n if (pricing.invalid.length) console.log(` ignored (bad) ${pricing.invalid.join(\", \")}`);\n console.log(\"\\nSources\");\n for (const s of loadSources()) console.log(` ${s.label.padEnd(14)} ${s.configDir} (${s.agent})`);\n });\n\ncli.help();\ncli.version(version());\n// `serve` and `watch` are async, so their failures surface as a rejected promise cac never returns\n// to us — the synchronous catch below would miss them entirely.\nprocess.on(\"unhandledRejection\", die);\ntry {\n cli.parse();\n} catch (e) {\n die(e);\n}\n","import { EventEmitter } from 'events';\n\nfunction toArr(any) {\n\treturn any == null ? [] : Array.isArray(any) ? any : [any];\n}\n\nfunction toVal(out, key, val, opts) {\n\tvar x, old=out[key], nxt=(\n\t\t!!~opts.string.indexOf(key) ? (val == null || val === true ? '' : String(val))\n\t\t: typeof val === 'boolean' ? val\n\t\t: !!~opts.boolean.indexOf(key) ? (val === 'false' ? false : val === 'true' || (out._.push((x = +val,x * 0 === 0) ? x : val),!!val))\n\t\t: (x = +val,x * 0 === 0) ? x : val\n\t);\n\tout[key] = old == null ? nxt : (Array.isArray(old) ? old.concat(nxt) : [old, nxt]);\n}\n\nfunction mri2 (args, opts) {\n\targs = args || [];\n\topts = opts || {};\n\n\tvar k, arr, arg, name, val, out={ _:[] };\n\tvar i=0, j=0, idx=0, len=args.length;\n\n\tconst alibi = opts.alias !== void 0;\n\tconst strict = opts.unknown !== void 0;\n\tconst defaults = opts.default !== void 0;\n\n\topts.alias = opts.alias || {};\n\topts.string = toArr(opts.string);\n\topts.boolean = toArr(opts.boolean);\n\n\tif (alibi) {\n\t\tfor (k in opts.alias) {\n\t\t\tarr = opts.alias[k] = toArr(opts.alias[k]);\n\t\t\tfor (i=0; i < arr.length; i++) {\n\t\t\t\t(opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (i=opts.boolean.length; i-- > 0;) {\n\t\tarr = opts.alias[opts.boolean[i]] || [];\n\t\tfor (j=arr.length; j-- > 0;) opts.boolean.push(arr[j]);\n\t}\n\n\tfor (i=opts.string.length; i-- > 0;) {\n\t\tarr = opts.alias[opts.string[i]] || [];\n\t\tfor (j=arr.length; j-- > 0;) opts.string.push(arr[j]);\n\t}\n\n\tif (defaults) {\n\t\tfor (k in opts.default) {\n\t\t\tname = typeof opts.default[k];\n\t\t\tarr = opts.alias[k] = opts.alias[k] || [];\n\t\t\tif (opts[name] !== void 0) {\n\t\t\t\topts[name].push(k);\n\t\t\t\tfor (i=0; i < arr.length; i++) {\n\t\t\t\t\topts[name].push(arr[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst keys = strict ? Object.keys(opts.alias) : [];\n\n\tfor (i=0; i < len; i++) {\n\t\targ = args[i];\n\n\t\tif (arg === '--') {\n\t\t\tout._ = out._.concat(args.slice(++i));\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (j=0; j < arg.length; j++) {\n\t\t\tif (arg.charCodeAt(j) !== 45) break; // \"-\"\n\t\t}\n\n\t\tif (j === 0) {\n\t\t\tout._.push(arg);\n\t\t} else if (arg.substring(j, j + 3) === 'no-') {\n\t\t\tname = arg.substring(j + 3);\n\t\t\tif (strict && !~keys.indexOf(name)) {\n\t\t\t\treturn opts.unknown(arg);\n\t\t\t}\n\t\t\tout[name] = false;\n\t\t} else {\n\t\t\tfor (idx=j+1; idx < arg.length; idx++) {\n\t\t\t\tif (arg.charCodeAt(idx) === 61) break; // \"=\"\n\t\t\t}\n\n\t\t\tname = arg.substring(j, idx);\n\t\t\tval = arg.substring(++idx) || (i+1 === len || (''+args[i+1]).charCodeAt(0) === 45 || args[++i]);\n\t\t\tarr = (j === 2 ? [name] : name);\n\n\t\t\tfor (idx=0; idx < arr.length; idx++) {\n\t\t\t\tname = arr[idx];\n\t\t\t\tif (strict && !~keys.indexOf(name)) return opts.unknown('-'.repeat(j) + name);\n\t\t\t\ttoVal(out, name, (idx + 1 < arr.length) || val, opts);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (defaults) {\n\t\tfor (k in opts.default) {\n\t\t\tif (out[k] === void 0) {\n\t\t\t\tout[k] = opts.default[k];\n\t\t\t}\n\t\t}\n\t}\n\n\tif (alibi) {\n\t\tfor (k in out) {\n\t\t\tarr = opts.alias[k] || [];\n\t\t\twhile (arr.length > 0) {\n\t\t\t\tout[arr.shift()] = out[k];\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out;\n}\n\nconst removeBrackets = (v) => v.replace(/[<[].+/, \"\").trim();\nconst findAllBrackets = (v) => {\n const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;\n const SQUARE_BRACKET_RE_GLOBAL = /\\[([^\\]]+)\\]/g;\n const res = [];\n const parse = (match) => {\n let variadic = false;\n let value = match[1];\n if (value.startsWith(\"...\")) {\n value = value.slice(3);\n variadic = true;\n }\n return {\n required: match[0].startsWith(\"<\"),\n value,\n variadic\n };\n };\n let angledMatch;\n while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {\n res.push(parse(angledMatch));\n }\n let squareMatch;\n while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {\n res.push(parse(squareMatch));\n }\n return res;\n};\nconst getMriOptions = (options) => {\n const result = {alias: {}, boolean: []};\n for (const [index, option] of options.entries()) {\n if (option.names.length > 1) {\n result.alias[option.names[0]] = option.names.slice(1);\n }\n if (option.isBoolean) {\n if (option.negated) {\n const hasStringTypeOption = options.some((o, i) => {\n return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === \"boolean\";\n });\n if (!hasStringTypeOption) {\n result.boolean.push(option.names[0]);\n }\n } else {\n result.boolean.push(option.names[0]);\n }\n }\n }\n return result;\n};\nconst findLongest = (arr) => {\n return arr.sort((a, b) => {\n return a.length > b.length ? -1 : 1;\n })[0];\n};\nconst padRight = (str, length) => {\n return str.length >= length ? str : `${str}${\" \".repeat(length - str.length)}`;\n};\nconst camelcase = (input) => {\n return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {\n return p1 + p2.toUpperCase();\n });\n};\nconst setDotProp = (obj, keys, val) => {\n let i = 0;\n let length = keys.length;\n let t = obj;\n let x;\n for (; i < length; ++i) {\n x = t[keys[i]];\n t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(\".\") || !(+keys[i + 1] > -1) ? {} : [];\n }\n};\nconst setByType = (obj, transforms) => {\n for (const key of Object.keys(transforms)) {\n const transform = transforms[key];\n if (transform.shouldTransform) {\n obj[key] = Array.prototype.concat.call([], obj[key]);\n if (typeof transform.transformFunction === \"function\") {\n obj[key] = obj[key].map(transform.transformFunction);\n }\n }\n }\n};\nconst getFileName = (input) => {\n const m = /([^\\\\\\/]+)$/.exec(input);\n return m ? m[1] : \"\";\n};\nconst camelcaseOptionName = (name) => {\n return name.split(\".\").map((v, i) => {\n return i === 0 ? camelcase(v) : v;\n }).join(\".\");\n};\nclass CACError extends Error {\n constructor(message) {\n super(message);\n this.name = this.constructor.name;\n if (typeof Error.captureStackTrace === \"function\") {\n Error.captureStackTrace(this, this.constructor);\n } else {\n this.stack = new Error(message).stack;\n }\n }\n}\n\nclass Option {\n constructor(rawName, description, config) {\n this.rawName = rawName;\n this.description = description;\n this.config = Object.assign({}, config);\n rawName = rawName.replace(/\\.\\*/g, \"\");\n this.negated = false;\n this.names = removeBrackets(rawName).split(\",\").map((v) => {\n let name = v.trim().replace(/^-{1,2}/, \"\");\n if (name.startsWith(\"no-\")) {\n this.negated = true;\n name = name.replace(/^no-/, \"\");\n }\n return camelcaseOptionName(name);\n }).sort((a, b) => a.length > b.length ? 1 : -1);\n this.name = this.names[this.names.length - 1];\n if (this.negated && this.config.default == null) {\n this.config.default = true;\n }\n if (rawName.includes(\"<\")) {\n this.required = true;\n } else if (rawName.includes(\"[\")) {\n this.required = false;\n } else {\n this.isBoolean = true;\n }\n }\n}\n\nconst processArgs = process.argv;\nconst platformInfo = `${process.platform}-${process.arch} node-${process.version}`;\n\nclass Command {\n constructor(rawName, description, config = {}, cli) {\n this.rawName = rawName;\n this.description = description;\n this.config = config;\n this.cli = cli;\n this.options = [];\n this.aliasNames = [];\n this.name = removeBrackets(rawName);\n this.args = findAllBrackets(rawName);\n this.examples = [];\n }\n usage(text) {\n this.usageText = text;\n return this;\n }\n allowUnknownOptions() {\n this.config.allowUnknownOptions = true;\n return this;\n }\n ignoreOptionDefaultValue() {\n this.config.ignoreOptionDefaultValue = true;\n return this;\n }\n version(version, customFlags = \"-v, --version\") {\n this.versionNumber = version;\n this.option(customFlags, \"Display version number\");\n return this;\n }\n example(example) {\n this.examples.push(example);\n return this;\n }\n option(rawName, description, config) {\n const option = new Option(rawName, description, config);\n this.options.push(option);\n return this;\n }\n alias(name) {\n this.aliasNames.push(name);\n return this;\n }\n action(callback) {\n this.commandAction = callback;\n return this;\n }\n isMatched(name) {\n return this.name === name || this.aliasNames.includes(name);\n }\n get isDefaultCommand() {\n return this.name === \"\" || this.aliasNames.includes(\"!\");\n }\n get isGlobalCommand() {\n return this instanceof GlobalCommand;\n }\n hasOption(name) {\n name = name.split(\".\")[0];\n return this.options.find((option) => {\n return option.names.includes(name);\n });\n }\n outputHelp() {\n const {name, commands} = this.cli;\n const {\n versionNumber,\n options: globalOptions,\n helpCallback\n } = this.cli.globalCommand;\n let sections = [\n {\n body: `${name}${versionNumber ? `/${versionNumber}` : \"\"}`\n }\n ];\n sections.push({\n title: \"Usage\",\n body: ` $ ${name} ${this.usageText || this.rawName}`\n });\n const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;\n if (showCommands) {\n const longestCommandName = findLongest(commands.map((command) => command.rawName));\n sections.push({\n title: \"Commands\",\n body: commands.map((command) => {\n return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;\n }).join(\"\\n\")\n });\n sections.push({\n title: `For more info, run any command with the \\`--help\\` flag`,\n body: commands.map((command) => ` $ ${name}${command.name === \"\" ? \"\" : ` ${command.name}`} --help`).join(\"\\n\")\n });\n }\n let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];\n if (!this.isGlobalCommand && !this.isDefaultCommand) {\n options = options.filter((option) => option.name !== \"version\");\n }\n if (options.length > 0) {\n const longestOptionName = findLongest(options.map((option) => option.rawName));\n sections.push({\n title: \"Options\",\n body: options.map((option) => {\n return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === void 0 ? \"\" : `(default: ${option.config.default})`}`;\n }).join(\"\\n\")\n });\n }\n if (this.examples.length > 0) {\n sections.push({\n title: \"Examples\",\n body: this.examples.map((example) => {\n if (typeof example === \"function\") {\n return example(name);\n }\n return example;\n }).join(\"\\n\")\n });\n }\n if (helpCallback) {\n sections = helpCallback(sections) || sections;\n }\n console.log(sections.map((section) => {\n return section.title ? `${section.title}:\n${section.body}` : section.body;\n }).join(\"\\n\\n\"));\n }\n outputVersion() {\n const {name} = this.cli;\n const {versionNumber} = this.cli.globalCommand;\n if (versionNumber) {\n console.log(`${name}/${versionNumber} ${platformInfo}`);\n }\n }\n checkRequiredArgs() {\n const minimalArgsCount = this.args.filter((arg) => arg.required).length;\n if (this.cli.args.length < minimalArgsCount) {\n throw new CACError(`missing required args for command \\`${this.rawName}\\``);\n }\n }\n checkUnknownOptions() {\n const {options, globalCommand} = this.cli;\n if (!this.config.allowUnknownOptions) {\n for (const name of Object.keys(options)) {\n if (name !== \"--\" && !this.hasOption(name) && !globalCommand.hasOption(name)) {\n throw new CACError(`Unknown option \\`${name.length > 1 ? `--${name}` : `-${name}`}\\``);\n }\n }\n }\n }\n checkOptionValue() {\n const {options: parsedOptions, globalCommand} = this.cli;\n const options = [...globalCommand.options, ...this.options];\n for (const option of options) {\n const value = parsedOptions[option.name.split(\".\")[0]];\n if (option.required) {\n const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));\n if (value === true || value === false && !hasNegated) {\n throw new CACError(`option \\`${option.rawName}\\` value is missing`);\n }\n }\n }\n }\n}\nclass GlobalCommand extends Command {\n constructor(cli) {\n super(\"@@global@@\", \"\", {}, cli);\n }\n}\n\nvar __assign = Object.assign;\nclass CAC extends EventEmitter {\n constructor(name = \"\") {\n super();\n this.name = name;\n this.commands = [];\n this.rawArgs = [];\n this.args = [];\n this.options = {};\n this.globalCommand = new GlobalCommand(this);\n this.globalCommand.usage(\"<command> [options]\");\n }\n usage(text) {\n this.globalCommand.usage(text);\n return this;\n }\n command(rawName, description, config) {\n const command = new Command(rawName, description || \"\", config, this);\n command.globalCommand = this.globalCommand;\n this.commands.push(command);\n return command;\n }\n option(rawName, description, config) {\n this.globalCommand.option(rawName, description, config);\n return this;\n }\n help(callback) {\n this.globalCommand.option(\"-h, --help\", \"Display this message\");\n this.globalCommand.helpCallback = callback;\n this.showHelpOnExit = true;\n return this;\n }\n version(version, customFlags = \"-v, --version\") {\n this.globalCommand.version(version, customFlags);\n this.showVersionOnExit = true;\n return this;\n }\n example(example) {\n this.globalCommand.example(example);\n return this;\n }\n outputHelp() {\n if (this.matchedCommand) {\n this.matchedCommand.outputHelp();\n } else {\n this.globalCommand.outputHelp();\n }\n }\n outputVersion() {\n this.globalCommand.outputVersion();\n }\n setParsedInfo({args, options}, matchedCommand, matchedCommandName) {\n this.args = args;\n this.options = options;\n if (matchedCommand) {\n this.matchedCommand = matchedCommand;\n }\n if (matchedCommandName) {\n this.matchedCommandName = matchedCommandName;\n }\n return this;\n }\n unsetMatchedCommand() {\n this.matchedCommand = void 0;\n this.matchedCommandName = void 0;\n }\n parse(argv = processArgs, {\n run = true\n } = {}) {\n this.rawArgs = argv;\n if (!this.name) {\n this.name = argv[1] ? getFileName(argv[1]) : \"cli\";\n }\n let shouldParse = true;\n for (const command of this.commands) {\n const parsed = this.mri(argv.slice(2), command);\n const commandName = parsed.args[0];\n if (command.isMatched(commandName)) {\n shouldParse = false;\n const parsedInfo = __assign(__assign({}, parsed), {\n args: parsed.args.slice(1)\n });\n this.setParsedInfo(parsedInfo, command, commandName);\n this.emit(`command:${commandName}`, command);\n }\n }\n if (shouldParse) {\n for (const command of this.commands) {\n if (command.name === \"\") {\n shouldParse = false;\n const parsed = this.mri(argv.slice(2), command);\n this.setParsedInfo(parsed, command);\n this.emit(`command:!`, command);\n }\n }\n }\n if (shouldParse) {\n const parsed = this.mri(argv.slice(2));\n this.setParsedInfo(parsed);\n }\n if (this.options.help && this.showHelpOnExit) {\n this.outputHelp();\n run = false;\n this.unsetMatchedCommand();\n }\n if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {\n this.outputVersion();\n run = false;\n this.unsetMatchedCommand();\n }\n const parsedArgv = {args: this.args, options: this.options};\n if (run) {\n this.runMatchedCommand();\n }\n if (!this.matchedCommand && this.args[0]) {\n this.emit(\"command:*\");\n }\n return parsedArgv;\n }\n mri(argv, command) {\n const cliOptions = [\n ...this.globalCommand.options,\n ...command ? command.options : []\n ];\n const mriOptions = getMriOptions(cliOptions);\n let argsAfterDoubleDashes = [];\n const doubleDashesIndex = argv.indexOf(\"--\");\n if (doubleDashesIndex > -1) {\n argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);\n argv = argv.slice(0, doubleDashesIndex);\n }\n let parsed = mri2(argv, mriOptions);\n parsed = Object.keys(parsed).reduce((res, name) => {\n return __assign(__assign({}, res), {\n [camelcaseOptionName(name)]: parsed[name]\n });\n }, {_: []});\n const args = parsed._;\n const options = {\n \"--\": argsAfterDoubleDashes\n };\n const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;\n let transforms = Object.create(null);\n for (const cliOption of cliOptions) {\n if (!ignoreDefault && cliOption.config.default !== void 0) {\n for (const name of cliOption.names) {\n options[name] = cliOption.config.default;\n }\n }\n if (Array.isArray(cliOption.config.type)) {\n if (transforms[cliOption.name] === void 0) {\n transforms[cliOption.name] = Object.create(null);\n transforms[cliOption.name][\"shouldTransform\"] = true;\n transforms[cliOption.name][\"transformFunction\"] = cliOption.config.type[0];\n }\n }\n }\n for (const key of Object.keys(parsed)) {\n if (key !== \"_\") {\n const keys = key.split(\".\");\n setDotProp(options, keys, parsed[key]);\n setByType(options, transforms);\n }\n }\n return {\n args,\n options\n };\n }\n runMatchedCommand() {\n const {args, options, matchedCommand: command} = this;\n if (!command || !command.commandAction)\n return;\n command.checkUnknownOptions();\n command.checkOptionValue();\n command.checkRequiredArgs();\n const actionArgs = [];\n command.args.forEach((arg, index) => {\n if (arg.variadic) {\n actionArgs.push(args.slice(index));\n } else {\n actionArgs.push(args[index]);\n }\n });\n actionArgs.push(options);\n return command.commandAction.apply(this, actionArgs);\n }\n}\n\nconst cac = (name = \"\") => new CAC(name);\n\nexport default cac;\nexport { CAC, Command, cac };\n","/**\n * Agent Lens — normalized, agent-agnostic data model (SQLite DDL).\n *\n * Design notes\n * - Agent-agnostic: Claude Code is one *source adapter*. Adding another agent later means a new\n * adapter that emits rows for these same tables — no schema change (see ADR-003).\n * - Event-grained source of truth: every transcript line becomes a row in `events`, keyed by its\n * stable `uuid`. Re-ingesting the same event is an UPSERT, so ingesting the mirror plus all\n * `.versions/` divergence backups deduplicates to the *maximal* history and recovers\n * compaction-dropped events (see ADR-001 / ADR-002).\n * - Hierarchy: session › turn › event. A \"turn\" = one user prompt → assistant completion.\n * - `raw_json` keeps the original line verbatim for lossless re-derivation; structured columns are\n * projections for querying/dashboards. It is stored gzip-compressed as a BLOB (ADR-011) — write via\n * `packRaw`, read via `unpackRaw` (both in `rawjson.ts`).\n *\n * This module only declares the schema. Connection handling and migrations live in `db.ts`.\n * Bump SCHEMA_VERSION on any DDL change.\n */\n\nexport const SCHEMA_VERSION = 14;\n\nexport const SCHEMA_SQL = /* sql */ `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\n-- Schema/version bookkeeping ------------------------------------------------\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- An agent *type* (e.g. 'claude-code'). Extensible to other agents. ---------\nCREATE TABLE IF NOT EXISTS agents (\n id TEXT PRIMARY KEY, -- 'claude-code'\n name TEXT NOT NULL, -- 'Claude Code CLI'\n kind TEXT NOT NULL DEFAULT 'cli'\n);\n\n-- A configured source = a labeled agent *instance* (e.g. account 'personal' --\n-- of agent 'claude-code' rooted at ~/.claude). Sessions belong to a source. --\nCREATE TABLE IF NOT EXISTS sources (\n id TEXT PRIMARY KEY, -- the (unique) label, e.g. 'personal'\n label TEXT NOT NULL,\n agent_id TEXT NOT NULL REFERENCES agents(id),\n config_dir TEXT\n);\n\n-- A project = a working directory a session ran in. ------------------------\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY, -- stable hash of (agent_id, path)\n agent_id TEXT NOT NULL REFERENCES agents(id),\n path TEXT NOT NULL, -- decoded absolute cwd\n encoded_dir TEXT, -- original '-home-...' archive dir name\n first_seen TEXT, -- ISO8601\n last_seen TEXT,\n UNIQUE (agent_id, path)\n);\n\n-- A session = one transcript file (sessionId). -----------------------------\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY, -- sessionId (UUID)\n agent_id TEXT NOT NULL REFERENCES agents(id),\n source_id TEXT REFERENCES sources(id),\n project_id TEXT REFERENCES projects(id),\n slug TEXT,\n ai_title TEXT,\n cli_version TEXT,\n entrypoint TEXT,\n git_branch TEXT,\n is_sidechain INTEGER NOT NULL DEFAULT 0, -- 1 = subagent thread\n spawn_parent_id TEXT, -- structural parent from path (<parent>/subagents/…); raw link hint, set at ingest\n workflow_run_id TEXT, -- for a Workflow-tool subagent: its run id (wf_<id>) from the path; groups fan-out by run\n parent_session_id TEXT REFERENCES sessions(id), -- spawning session (subagents only); derived in rebuildDerived\n parent_turn_id TEXT REFERENCES turns(id), -- the turn that spawned this subagent\n started_at TEXT, -- ISO8601 of first event\n ended_at TEXT, -- ISO8601 of last event\n duration_ms INTEGER,\n event_count INTEGER NOT NULL DEFAULT 0,\n turn_count INTEGER NOT NULL DEFAULT 0\n);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);\nCREATE INDEX IF NOT EXISTS idx_sessions_parent_session ON sessions(parent_session_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_workflow_run ON sessions(workflow_run_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_parent_turn ON sessions(parent_turn_id);\n\n-- A turn = user prompt -> assistant completion within a session. ------------\nCREATE TABLE IF NOT EXISTS turns (\n id TEXT PRIMARY KEY, -- hash(session_id, seq)\n session_id TEXT NOT NULL REFERENCES sessions(id),\n seq INTEGER NOT NULL, -- 0-based order within session\n user_event_uuid TEXT, -- the originating user event\n prompt_preview TEXT, -- short, for lists (full text in events)\n model TEXT, -- model that answered this turn\n started_at TEXT,\n ended_at TEXT,\n duration_ms INTEGER,\n UNIQUE (session_id, seq)\n);\nCREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);\n\n-- Every transcript line. uuid is the dedup key across archive versions. -----\nCREATE TABLE IF NOT EXISTS events (\n uuid TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT REFERENCES turns(id),\n parent_uuid TEXT,\n seq INTEGER, -- line order as first observed\n type TEXT NOT NULL, -- user | assistant | system | attachment | ...\n role TEXT, -- user | assistant (message.role)\n timestamp TEXT, -- ISO8601\n model TEXT,\n is_sidechain INTEGER NOT NULL DEFAULT 0,\n is_meta INTEGER NOT NULL DEFAULT 0,\n text TEXT, -- flattened text/thinking for search + preview\n raw_json BLOB NOT NULL, -- original line, verbatim, gzip-compressed (ADR-011); read via unpackRaw()\n source_file TEXT -- archive path the canonical copy came from\n);\nCREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);\nCREATE INDEX IF NOT EXISTS idx_events_turn ON events(turn_id);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(type);\nCREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp);\n\n-- Tool invocations (incl. subagents / Task calls). -------------------------\nCREATE TABLE IF NOT EXISTS tool_calls (\n id TEXT PRIMARY KEY, -- tool_use id, or hash\n event_uuid TEXT REFERENCES events(uuid),\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT REFERENCES turns(id),\n tool_name TEXT NOT NULL, -- Edit, Write, Bash, Skill, Task, ...\n caller TEXT,\n skill_name TEXT, -- when tool_name = Skill\n skill_id TEXT REFERENCES skills(id), -- the specific skill *version* this call fired (content hash); set in rebuildDerived\n agent_type TEXT, -- subagent type (toolUseResult.agentType)\n spawned_session_id TEXT, -- for Task/Agent: the subagent session id ('agent-'||agentId)\n workflow_run_id TEXT, -- for Workflow: the run id (toolUseResult.runId, e.g. wf_<id>) — ties the run to its subagents\n workflow_name TEXT, -- for Workflow: the workflow name (toolUseResult.workflowName)\n resolved_model TEXT,\n status TEXT, -- success | error | ... ('error' when the tool_result had is_error)\n error_type TEXT, -- heuristic bucket for status='error' rows (see errors.ts); NULL otherwise\n total_duration_ms INTEGER,\n total_tokens INTEGER,\n total_tool_use_count INTEGER,\n input_json TEXT, -- tool input, verbatim\n result_summary TEXT -- short result projection (no secrets dumped wholesale)\n);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_skill ON tool_calls(skill_name);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_skill_id ON tool_calls(skill_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_spawned ON tool_calls(spawned_session_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_workflow_run ON tool_calls(workflow_run_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_error_type ON tool_calls(error_type);\n\n-- Skill *versions* (content-addressed). A skill firing injects the full SKILL.md body into the\n-- transcript as an isMeta user event (begins \"Base directory for this skill: …\", then the body,\n-- then a trailing \"ARGUMENTS: …\" block). That body is the only real skill content available (the\n-- trace carries no description/created_at). We identify a version by hashing the *normalized* body\n-- (Base-directory line + per-call ARGUMENTS block stripped, trailing whitespace trimmed) so the same\n-- content from either install / any args is one version, and a content change shows as a new row.\n-- Linked from tool_calls.skill_id in rebuildDerived. See plan: content-addressed skill versioning.\nCREATE TABLE IF NOT EXISTS skills (\n id TEXT PRIMARY KEY, -- sha1 of name + NUL + normalized_body\n name TEXT NOT NULL,\n base_dir TEXT, -- last-seen base directory (informational; NOT part of the hash)\n body TEXT NOT NULL, -- normalized SKILL.md body (Base-dir line + ARGUMENTS block stripped)\n summary TEXT, -- first heading/intro line, for list + detail display\n body_bytes INTEGER,\n first_seen TEXT, -- earliest firing timestamp (MIN across firings)\n last_seen TEXT -- latest firing timestamp (MAX across firings)\n);\nCREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);\n\n-- Token usage at the assistant-response grain. Cost derived later from model. --\n-- One API response is logged across multiple JSONL lines (one per content block: text + each\n-- tool_use), and Claude Code stamps the SAME usage object on every line. We dedup at the response\n-- grain via the unique (session_id, message_id) index so a response's tokens are counted ONCE, not\n-- once per content block (which over-counted tokens ~2.3x and cost ~3x). event_uuid stays the PK\n-- (the row is attached to whichever content-block event won the insert). --\nCREATE TABLE IF NOT EXISTS token_usage (\n event_uuid TEXT PRIMARY KEY REFERENCES events(uuid),\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT REFERENCES turns(id),\n message_id TEXT, -- Anthropic response id (msg_…); dedup key\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,\n service_tier TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_token_usage_session ON token_usage(session_id);\n-- Partial so rare usage rows lacking a message_id (e.g. <synthetic>) don't collide on NULL. --\nCREATE UNIQUE INDEX IF NOT EXISTS idx_token_usage_msg\n ON token_usage(session_id, message_id) WHERE message_id IS NOT NULL;\n\n-- Heuristic classification (category + complexity), re-runnable. -----------\n-- scope: 'session' | 'turn'; target_id references that entity. -------------\nCREATE TABLE IF NOT EXISTS classifications (\n scope TEXT NOT NULL, -- 'session' | 'turn'\n target_id TEXT NOT NULL,\n category TEXT, -- feature | bugfix | refactor | docs | ops | review | ...\n complexity_score REAL,\n complexity_band TEXT, -- trivial | small | medium | large | xl\n signals_json TEXT, -- inputs used, for transparency/repro\n classifier_version INTEGER NOT NULL DEFAULT 1,\n PRIMARY KEY (scope, target_id)\n);\n\n-- Security findings (ADR-017), re-runnable. A deterministic rule engine (detect.ts) scans each\n-- tool call's verbatim input/result for risky operations — destructive/data-loss, credential access,\n-- data exfiltration, privilege/guardrail bypass — and records one row per (tool_call, rule) match.\n-- Findings are 0..N per tool call (unlike classifications, which are 1-per-session), so an incremental\n-- re-run DELETEs the touched sessions' rows and re-INSERTs (delete-then-insert) rather than upserting.\n-- Every input/modifier is captured in signals_json so a finding is fully explainable; framework_ref\n-- anchors it to OWASP Agentic / MITRE ATLAS. detector_version lets a future engine supersede rows.\nCREATE TABLE IF NOT EXISTS findings (\n id TEXT PRIMARY KEY, -- deterministic hash(tool_call_id, rule_id)\n session_id TEXT NOT NULL REFERENCES sessions(id),\n tool_call_id TEXT REFERENCES tool_calls(id),\n event_uuid TEXT, -- the tool call's event, for jump-to-transcript\n turn_id TEXT,\n rule_id TEXT NOT NULL, -- 'destructive.rm_rf'\n category TEXT NOT NULL, -- destructive | credential-access | exfiltration | privilege-bypass\n framework_ref TEXT, -- 'OWASP ASI02' | 'MITRE ATLAS AML.T0086' | ...\n severity TEXT NOT NULL, -- info | low | medium | high | critical\n title TEXT,\n evidence TEXT, -- matched snippet, truncated (no wholesale secret dump)\n signals_json TEXT, -- why: rule, matched pattern, context modifiers\n detector_version INTEGER NOT NULL DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_findings_session ON findings(session_id);\nCREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);\nCREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);\nCREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule_id);\nCREATE INDEX IF NOT EXISTS idx_findings_tool_call ON findings(tool_call_id);\n\n-- File modifications derived from tool calls (ADR-022). One row per (tool_call, file): today\n-- Edit/Write/NotebookEdit are 1:1, but the hash id leaves room for multi-file sources (e.g. a\n-- future Bash-write heuristic) without a schema break. Failed calls (status='error') are excluded\n-- at derivation — they didn't change the file. Re-runnable like findings: incremental runs DELETE\n-- the dirty sessions' rows and re-INSERT (delete-then-insert). Populated by ingest's\n-- deriveFileChanges (filechanges.ts).\nCREATE TABLE IF NOT EXISTS file_changes (\n id TEXT PRIMARY KEY, -- sha1(tool_call_id + NUL + file_path), 16 hex\n tool_call_id TEXT NOT NULL REFERENCES tool_calls(id),\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT, -- no FK: rebuildDerived recreates turns (same stance as findings)\n event_uuid TEXT, -- jump-to-transcript anchor (#ev-<uuid>)\n project_id TEXT REFERENCES projects(id), -- from the session, for per-project grouping\n file_path TEXT NOT NULL, -- normalized absolute path\n tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit\n lines_added INTEGER, -- newline-count delta (NULL when unknowable)\n lines_removed INTEGER,\n timestamp TEXT, -- ISO8601, from the tool call's event\n derive_version INTEGER NOT NULL DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_file_changes_session ON file_changes(session_id);\nCREATE INDEX IF NOT EXISTS idx_file_changes_file ON file_changes(project_id, file_path, timestamp);\n\n-- Workflow-tool run results, ingested from the sidecar JSON the runner writes next to the launching\n-- session (…/projects/<enc>/<sessionId>/workflows/wf_<id>.json). The transcript only carries the\n-- \"launched in background\" ack for an async run; this sidecar is the authoritative record of how the\n-- run finished — status (completed/failed), summary, the returned result payload, and roll-up\n-- tokens/tool-calls/duration/agent-count. One row per run id; UPSERT on re-ingest. Stored verbatim\n-- (like tool_calls.input_json) — the DB is local; redaction is an export-time concern. --------------\nCREATE TABLE IF NOT EXISTS workflow_results (\n run_id TEXT PRIMARY KEY, -- wf_<id> (matches tool_calls.workflow_run_id / sessions.workflow_run_id)\n source_id TEXT, -- which configured source (account) it came from\n session_id TEXT, -- launching session (path segment before /workflows/)\n task_id TEXT, -- async task id (the \"Task ID: …\" in the launch ack)\n workflow_name TEXT,\n status TEXT, -- completed | failed | running | ...\n summary TEXT,\n default_model TEXT, -- model the run executed on (e.g. claude-fable-5)\n result_json TEXT, -- the returned result object, verbatim JSON (null when none)\n phases_json TEXT, -- [{title}] the run's phase structure (e.g. Generate → Judge)\n logs_json TEXT, -- concise per-item outcome lines the runner emitted\n progress_json TEXT, -- workflowProgress: the phase/agent event timeline\n agent_count INTEGER, -- agents the run reports having fanned out to\n total_tokens INTEGER,\n total_tool_calls INTEGER,\n duration_ms INTEGER,\n started_at TEXT, -- ISO, from the sidecar startTime/timestamp\n ended_at TEXT, -- ISO, started_at + duration\n ingested_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_workflow_results_session ON workflow_results(session_id);\n\n-- Per-subagent metadata sidecar (subagents/agent-<id>.meta.json). One row per subagent session,\n-- joined at read time onto sessions (LEFT JOIN, may precede the session row's arrival). Supplies the\n-- authoritative agentType/description/spawnDepth — Workflow fan-out subagents carry none in-transcript.\nCREATE TABLE IF NOT EXISTS session_meta (\n session_id TEXT PRIMARY KEY, -- 'agent-<id>' = the subagent session id (meta filename stem)\n source_id TEXT,\n agent_type TEXT, -- meta.agentType (e.g. 'Explore', 'claude-code-guide')\n agent_description TEXT, -- meta.description (a real human title)\n spawn_depth INTEGER, -- meta.spawnDepth (optional; absent ⇒ top-level)\n tool_use_id TEXT, -- meta.toolUseId (the spawning tool_call)\n ingested_at TEXT\n);\n\n-- Full un-truncated tool outputs spilled to disk (tool-results/<name>.txt) when a result was too large\n-- for the transcript, which keeps only a 280-char summary naming the file. Joined at read time by\n-- parsing that summary's '…/tool-results/<name>.txt' marker back to (session_id, name).\nCREATE TABLE IF NOT EXISTS tool_results (\n session_id TEXT, -- session the tool ran in (from the archive path)\n name TEXT, -- file basename without .txt (matches the marker in tool_calls.result_summary)\n path TEXT, -- archive path (provenance)\n bytes INTEGER, -- original size, for the UI\n text TEXT, -- full un-truncated output, stored inline (self-contained DB)\n ingested_at TEXT,\n PRIMARY KEY (session_id, name)\n);\n\n-- Idempotent ingest bookkeeping per archive file. --------------------------\nCREATE TABLE IF NOT EXISTS ingest_state (\n file_path TEXT PRIMARY KEY,\n size INTEGER,\n mtime_ms INTEGER,\n sha256 TEXT,\n events_ingested INTEGER NOT NULL DEFAULT 0,\n ingested_at TEXT\n);\n\n-- Full-text search over event text (contentless external-content FTS). ------\nCREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(\n text,\n content='events',\n content_rowid='rowid'\n);\n\n-- Keep FTS in sync with events. --------------------------------------------\nCREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN\n INSERT INTO events_fts(rowid, text) VALUES (new.rowid, new.text);\nEND;\nCREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN\n INSERT INTO events_fts(events_fts, rowid, text) VALUES ('delete', old.rowid, old.text);\nEND;\nCREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events\nWHEN old.text IS NOT new.text BEGIN\n INSERT INTO events_fts(events_fts, rowid, text) VALUES ('delete', old.rowid, old.text);\n INSERT INTO events_fts(rowid, text) VALUES (new.rowid, new.text);\nEND;\n`;\n","/**\n * SQLite access helpers over Node's built-in `node:sqlite` (ADR-029).\n *\n * `DatabaseSync` covers everything the store needs except `db.transaction(fn)`, which better-sqlite3\n * provided and `node:sqlite` does not. {@link transaction} is that one gap, kept here so `ingest` and\n * `server` share a single implementation of the rollback semantics.\n *\n * One behavioural difference worth knowing when reading rows back: `.all()` / `.get()` return\n * **null-prototype** objects. Spread, `Object.keys`, `JSON.stringify` and vitest's `toEqual` all\n * behave normally, but `row.hasOwnProperty(…)` and `toStrictEqual` do not.\n */\nimport type { DatabaseSync, StatementResultingChanges, StatementSync } from \"node:sqlite\";\n\n/** The open-database handle. Aliased so call sites don't each import from `node:sqlite`. */\nexport type SqliteDB = DatabaseSync;\n\n/**\n * Bind a declared row object to a named-parameter statement.\n *\n * `node:sqlite` types named parameters as `Record<string, SQLInputValue>`, which a declared interface\n * (`TurnRow`, `ToolCallRow`, …) never satisfies — an interface has no index signature even when every\n * field is bindable. The cast lives here rather than at each call site, mirroring the single read-side\n * boundary in the server's `queryAll`.\n */\nexport function runNamed(stmt: StatementSync, row: object): StatementResultingChanges {\n return stmt.run(row as Record<string, never>);\n}\n\n/** Distinguishes nested savepoints from each other; module-scoped so names never collide. */\nlet savepointSeq = 0;\n\n/**\n * Run `fn` inside a transaction, committing on return and rolling back on throw.\n *\n * Returns a *callable* rather than running immediately, matching the better-sqlite3 shape the call\n * sites were written against (`const tx = transaction(db, fn); tx(args)`) — arguments and the return\n * value pass straight through.\n *\n * Uses `SAVEPOINT` when a transaction is already open, because SQLite rejects a nested `BEGIN`.\n * Nothing nests today, but `createDirtySet` runs immediately before several of these and would\n * otherwise fail at runtime the first time one moved inside another.\n */\nexport function transaction<A extends unknown[], R>(db: DatabaseSync, fn: (...args: A) => R): (...args: A) => R {\n return (...args: A): R => {\n const savepoint = db.isTransaction ? `agent_lens_sp_${savepointSeq++}` : \"\";\n db.exec(savepoint ? `SAVEPOINT ${savepoint}` : \"BEGIN\");\n try {\n const result = fn(...args);\n db.exec(savepoint ? `RELEASE ${savepoint}` : \"COMMIT\");\n return result;\n } catch (err) {\n // The unwind can itself fail (a failed COMMIT may already have closed the transaction), and\n // that secondary error would mask the real one. Drop it and rethrow the original cause.\n try {\n db.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : \"ROLLBACK\");\n } catch {\n /* ignore — `err` below is the useful failure */\n }\n throw err;\n }\n };\n}\n","/**\n * Agent Lens — security findings: shared severity model + framework-anchored reference content\n * (ADR-017). This is the low-level, dependency-free source of truth so the ingest detector\n * (packages/ingest/src/detect.ts), the server queries, and the web UI all agree on the severity\n * ordering and the category taxonomy. The reference content also powers the \"what & why\" explainer\n * blocks on the /security page.\n */\n\n// Severity is defined once in @agent-lens/contracts (the node-free shared-types leaf) so the detector,\n// server, and web browser bundle all agree; re-exported here so existing `@agent-lens/core` importers\n// (e.g. the ingest detector) keep their import path.\nexport type { Severity } from \"@agent-lens/contracts\";\nimport type { Severity } from \"@agent-lens/contracts\";\n\n/** Ascending order of concern; index doubles as the numeric rank (info=0 … critical=4). */\nexport const SEVERITY_ORDER: Severity[] = [\"info\", \"low\", \"medium\", \"high\", \"critical\"];\n\n/** Numeric rank of a severity (info=0 … critical=4); -1 for an unknown value. */\nexport function severityRank(s: string): number {\n return SEVERITY_ORDER.indexOf(s as Severity);\n}\n\n/** Shift a severity by `delta` bands, clamped to the [info, critical] range. */\nexport function bumpSeverity(s: Severity, delta: number): Severity {\n const i = SEVERITY_ORDER.indexOf(s);\n const j = Math.max(0, Math.min(SEVERITY_ORDER.length - 1, i + delta));\n return SEVERITY_ORDER[j];\n}\n\n/** The four v1 risk families a finding is categorized into. */\nexport type SecurityCategoryKey = \"destructive\" | \"credential-access\" | \"exfiltration\" | \"privilege-bypass\";\n\nexport interface SecurityCategoryRef {\n key: SecurityCategoryKey;\n title: string;\n /** Short framework anchor shown as a chip (matches findings.framework_ref). */\n framework_ref: string;\n framework_url: string;\n /** What the detector looks for. */\n what: string;\n /** Why it matters for a host running an autonomous agent. */\n why: string;\n /** What to do when you see one. */\n remediation: string;\n}\n\n/**\n * Reference content for each category — the browsable \"security page with info\" the feature was scoped\n * around. Anchored to OWASP Top 10 for Agentic Apps 2026 and MITRE ATLAS. Kept deliberately concise;\n * the point is orientation, not a full framework reproduction.\n */\nexport const SECURITY_CATEGORIES: SecurityCategoryRef[] = [\n {\n key: \"destructive\",\n title: \"Destructive & data-loss operations\",\n framework_ref: \"OWASP ASI02\",\n framework_url: \"https://genai.owasp.org/\",\n what: \"Commands or edits that irreversibly delete, overwrite, or truncate data — rm -rf, git reset --hard, force-pushes, DROP/TRUNCATE, disk writes (dd/mkfs/shred), and overwrites of critical project files (lockfiles, CI config).\",\n why: \"An agent acting on a bad plan or a poisoned instruction can wipe work, history, or a database with a single tool call. Because agent-lens sees sessions after the fact, this is how you notice the damage — and the blast radius — even when nobody was watching live.\",\n remediation: \"Open the flagged session, confirm the target was intended, and check the tool result for whether it succeeded. Restore from git/backups if needed, and consider a PreToolUse hook to block the pattern going forward.\",\n },\n {\n key: \"credential-access\",\n title: \"Secret & credential access\",\n framework_ref: \"MITRE ATLAS: Credential Access\",\n framework_url: \"https://atlas.mitre.org/\",\n what: \"Reads of secret-bearing files (.env, ~/.ssh keys, ~/.aws/credentials, .npmrc, .pem) or secret-looking values (private keys, AWS keys, GitHub/Slack tokens) appearing in a command or a captured tool result.\",\n why: \"Credentials the agent touched may have been exposed to the model context, logs, or a downstream tool — and could be leaked or reused. Retrospective visibility tells you which secrets to rotate.\",\n remediation: \"Treat any accessed secret as potentially exposed: rotate it. Verify the access was purposeful, and keep secrets out of the working tree the agent operates on.\",\n },\n {\n key: \"exfiltration\",\n title: \"Data exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n framework_url: \"https://atlas.mitre.org/techniques/AML.T0086\",\n what: \"Local data flowing outbound: curl/wget uploads to external hosts, piping files into a network command, netcat/reverse-shell patterns, and other ways an agent's write-capable tools become an exfiltration channel.\",\n why: \"Exfiltration via the agent's own tool calls (AML.T0086) is a primary agentic attack path — a prompt-injected agent can POST your files or secrets to an attacker with one command. These are the highest-signal findings to review.\",\n remediation: \"Inspect the destination host and the data referenced in the command. If it was not you, treat it as an incident: rotate anything referenced, review what left the host, and audit the session for the injection source.\",\n },\n {\n key: \"privilege-bypass\",\n title: \"Privilege escalation & guardrail bypass\",\n framework_ref: \"OWASP LLM06\",\n framework_url: \"https://genai.owasp.org/\",\n what: \"Actions that widen the agent's authority or sidestep safety: sudo, chmod 777, curl | sh, --dangerously-skip-permissions, writes outside the working project, and persistence mechanisms (cron, systemd, shell rc files).\",\n why: \"Excessive agency (LLM06) is the root cause behind most agent incidents — every disabled guardrail or out-of-scope write removes a barrier the design relied on. Persistence in particular outlives the session that created it.\",\n remediation: \"Confirm the escalation was necessary and scoped. Remove any persistence the agent added, re-tighten permissions, and avoid running agents with skipped permission prompts on a trusted host.\",\n },\n];\n","/**\n * Canonical secret / PII patterns — single source of truth for masking and leak-scanning.\n *\n * Two consumers:\n * • the corpus redactor's fail-closed scan (`LEAK_PATTERNS` / `findLeak`, relocated here from\n * packages/ingest/src/redact.ts and re-exported there so its API is unchanged), and\n * • the redacted-export sanitizer (`maskSecrets`, `findShareLeak`) — see redact-export.ts.\n *\n * NOTE: packages/ingest/src/detect.ts keeps its own SECRET_VALUE_PATTERNS today (its v8 rules are\n * FP-tuned around them); a future cleanup can have it import SECRET_PATTERNS from here to dedupe.\n */\n\nexport interface LeakPattern {\n name: string;\n re: RegExp;\n}\n\n// Base (non-global) patterns reused across the leak-scan sets and the /g masking sets below, so a\n// tightening happens once (the module's \"single source of truth\" promise). The masking sets derive a\n// global copy via `new RegExp(base.source, \"g\")`; the leak-scan sets (findLeak → `re.exec`, non-global,\n// always searches from 0) can share the object directly.\nconst EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/;\nconst IPV4 = /\\b\\d{1,3}(?:\\.\\d{1,3}){3}\\b/;\nconst HOME_PATH_WITH_USER = /\\/(?:home|Users)\\/(?!user\\b)[A-Za-z0-9._-]+/;\n\n/** Patterns that must NEVER appear in redacted CORPUS output (strict, forbids URLs/IPs entirely). */\nexport const LEAK_PATTERNS: LeakPattern[] = [\n { name: \"email\", re: EMAIL },\n { name: \"home-path-with-user\", re: HOME_PATH_WITH_USER },\n { name: \"ipv4\", re: IPV4 },\n { name: \"url\", re: /https?:\\/\\/[^\\s\"']+/ },\n { name: \"aws-key\", re: /AKIA[0-9A-Z]{16}/ },\n { name: \"bearer/token\", re: /\\b(?:sk|pk|ghp|gho|xox[abp])[-_][A-Za-z0-9]{8,}/ },\n];\n\n/** Return the first leak found by `patterns` (default: strict corpus set), or null. */\nexport function findLeak(text: string, patterns: LeakPattern[] = LEAK_PATTERNS): { name: string; sample: string } | null {\n for (const { name, re } of patterns) {\n const m = re.exec(text);\n if (m) return { name, sample: m[0].slice(0, 40) };\n }\n return null;\n}\n\n/** Secret VALUE patterns → a human-readable placeholder label. Mirrors detect.ts's stronger set,\n * extended (2026-07-18 audit) with formats seen in the wild: Google/Stripe/OpenAI/Anthropic keys,\n * GitHub fine-grained PATs, npm tokens, Slack app tokens, and JWTs. Hyphen/underscore-bearing keys\n * (sk-proj-…, sk-ant-…, sk_live_…) need their own entries — the generic `sk-…{20,}` stops at the\n * first separator and would miss them. */\nexport const SECRET_PATTERNS: Array<{ re: RegExp; label: string }> = [\n { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g, label: \"PRIVATE-KEY\" },\n { re: /\\bAKIA[0-9A-Z]{16}\\b/g, label: \"AWS-KEY\" },\n { re: /\\bASIA[0-9A-Z]{16}\\b/g, label: \"AWS-KEY\" },\n { re: /\\bgithub_pat_[A-Za-z0-9_]{20,}/g, label: \"GITHUB-TOKEN\" },\n { re: /\\bgh[pousr]_[A-Za-z0-9]{30,}\\b/g, label: \"GITHUB-TOKEN\" },\n { re: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g, label: \"SLACK-TOKEN\" },\n { re: /\\bxapp-[0-9A-Za-z-]{10,}/g, label: \"SLACK-TOKEN\" },\n { re: /\\bnpm_[A-Za-z0-9]{30,}/g, label: \"NPM-TOKEN\" },\n { re: /\\bAIza[0-9A-Za-z_-]{35}\\b/g, label: \"GOOGLE-API-KEY\" },\n { re: /\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,}\\b/g, label: \"STRIPE-KEY\" },\n { re: /\\bsk-(?:proj|ant|or|svcacct)-[A-Za-z0-9_-]{20,}/g, label: \"API-KEY\" },\n { re: /\\bsk-[A-Za-z0-9]{20,}\\b/g, label: \"API-KEY\" },\n { re: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{6,}/g, label: \"JWT\" },\n];\n\n/** URL-embedded credentials survive the kept-readable-URLs policy, so mask them explicitly:\n * basic-auth userinfo (`scheme://user:pass@host` → keep the host) and secret query-param values. */\nfunction maskUrlCredentials(text: string): string {\n return text\n .replace(/([a-z][a-z0-9+.-]*:\\/\\/)[^/\\s:@\"'`]+:[^/\\s@\"'`]+@/gi, \"$1[REDACTED-AUTH]@\")\n .replace(\n /([?&](?:access_?token|api[_-]?key|apikey|token|secret|password|pwd|client[_-]?secret|auth|signature|sig)=)[^&\\s\"'`<>]+/gi,\n \"$1[REDACTED]\",\n );\n}\n\n/** Conservative env-var secret assignment: mask a LITERAL value of NAME=… when NAME looks secret\n * (…_TOKEN/_SECRET/_KEY/_PASSWORD/… or a bare PASSWORD/SECRET/TOKEN) and the value is not a `$VAR`\n * / `${VAR}` reference (those are safe and common, so kept). Known key formats are already handled\n * by SECRET_PATTERNS; this catches opaque literals like `DB_PASSWORD=hunter2`. */\nconst ENV_ASSIGN =\n /\\b([A-Za-z][A-Za-z0-9_]*_(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PWD|PW|CREDENTIALS?)|PASSWORD|PASSWD|SECRET|TOKEN)=([\"']?)(?!\\$)[^\\s\"'`&;|<>()]+/gi;\nfunction maskEnvAssignments(text: string): string {\n return text.replace(ENV_ASSIGN, (_m, name, q) => `${name}=${q}[REDACTED]`);\n}\n\n/** PII value patterns → placeholder label (home paths are handled separately, username-strip only). */\nconst PII_PATTERNS: Array<{ re: RegExp; label: string }> = [\n { re: new RegExp(EMAIL.source, \"g\"), label: \"EMAIL\" },\n { re: new RegExp(IPV4.source, \"g\"), label: \"IP\" },\n];\n\n/** Strip only the `<user>` segment of a home path so `/home/alice/x` → `/home/user/x` (rest kept). */\nconst HOME_USER = /\\/(home|Users)\\/[^/\\s\"'`]+/g;\n\n/** Claude Code encodes a project path by replacing \"/\" with \"-\", so `/home/alice/proj` becomes\n * `-home-alice-proj`. That form appears verbatim in scratchpad/task paths in transcript text and\n * leaks the username past the slash-form strip. Match `-home-<user>-` (not preceded by an\n * alphanumeric, so `flex-home-x` is left alone) and drop only the username segment. */\nconst ENCODED_HOME_USER = /(?<![A-Za-z0-9])(-(?:home|Users)-)[A-Za-z0-9._]+(?=-)/g;\n\n/** Windows profile path: `C:\\Users\\Alice\\…` → `C:\\Users\\user\\…`. */\nconst WINDOWS_USER = /([A-Za-z]:\\\\Users\\\\)[^\\\\/\\s\"'`]+/gi;\n\n/**\n * Selective share masking: replace secret values and PII with labels, and strip home usernames,\n * while leaving the surrounding narrative (and ordinary URLs) readable. Idempotent.\n */\nexport function maskSecrets(text: string): string {\n let out = maskUrlCredentials(text);\n out = maskEnvAssignments(out);\n for (const { re, label } of SECRET_PATTERNS) out = out.replace(re, `[${label}]`);\n for (const { re, label } of PII_PATTERNS) out = out.replace(re, `[${label}]`);\n out = out.replace(HOME_USER, (_m, root) => `/${root}/user`);\n out = out.replace(ENCODED_HOME_USER, (_m, root) => `${root}user`);\n out = out.replace(WINDOWS_USER, (_m, root) => `${root}user`);\n return out;\n}\n\n/**\n * Fail-closed scan set for the selective `secrets` share level: secret values + email +\n * home-path-with-user. Deliberately EXCLUDES generic url/ipv4 so legitimate doc links stay\n * readable (IPs are already masked by maskSecrets).\n */\nexport const SHARE_LEAK: LeakPattern[] = [\n ...SECRET_PATTERNS.map(({ re, label }) => ({ name: label, re: new RegExp(re.source) })),\n { name: \"email\", re: EMAIL },\n { name: \"home-path-with-user\", re: HOME_PATH_WITH_USER },\n { name: \"encoded-home-with-user\", re: /(?<![A-Za-z0-9])-(?:home|Users)-(?!user-)[A-Za-z0-9._]+-/ },\n { name: \"url-credentials\", re: /:\\/\\/[^/\\s:@\"'`]+:[^/\\s@\"'`]+@/ },\n];\n\n/** First leak under the selective share set, or null. */\nexport function findShareLeak(text: string): { name: string; sample: string } | null {\n return findLeak(text, SHARE_LEAK);\n}\n\n// Common / non-identifying home-dir owners — masking these would mangle prose (e.g. the OS name\n// \"Ubuntu\") without protecting an individual's identity, so they are never scrubbed as a username.\nconst USER_STOPLIST = new Set([\n \"user\", \"users\", \"home\", \"root\", \"admin\", \"ubuntu\", \"runner\", \"node\", \"app\", \"apps\",\n \"data\", \"tmp\", \"shared\", \"public\", \"guest\", \"vagrant\", \"ec2-user\", \"docker\",\n]);\n// A home-dir owner: the segment right after a home root, in slash, encoded, or Windows form.\n// The username capture excludes \"-\" so the encoded form (-home-u-proj) stops at the next segment.\nconst HOME_OWNER = /(?:\\/(?:home|Users)\\/|-(?:home|Users)-|[A-Za-z]:\\\\Users\\\\)([A-Za-z0-9._]{3,})/g;\n\n/** Derive the username(s) that own a home directory anywhere in the given texts. These are provably\n * PII (they own a home dir in the transcript), so they can be scrubbed everywhere — including bare\n * occurrences in URLs/prose that no path pattern would catch. Common/OS owners are excluded. */\nexport function deriveHomeUsers(texts: Array<string | null | undefined>): string[] {\n const found = new Set<string>();\n for (const t of texts) {\n if (!t) continue;\n const re = new RegExp(HOME_OWNER.source, \"g\");\n let m: RegExpExecArray | null;\n while ((m = re.exec(t))) {\n const u = m[1]!;\n if (u.length >= 3 && !USER_STOPLIST.has(u.toLowerCase())) found.add(u);\n }\n }\n return [...found];\n}\n\n/** Replace whole-token occurrences of each derived username with `[USER]`. */\nexport function scrubUsernames(text: string, users: string[]): string {\n let out = text;\n for (const u of users) {\n const esc = u.replace(/[.*+?^${}()|[\\]\\\\-]/g, \"\\\\$&\");\n out = out.replace(new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, \"g\"), \"[USER]\");\n }\n return out;\n}\n","/**\n * Redacted, export-only sharing (backlog #3). Produces a sanitized Markdown artifact the user\n * shares MANUALLY — no network, no upload, no server-side link. Distinct from the corpus Redactor\n * (packages/ingest/src/redact.ts), which is deny-by-default for a metric-preserving test corpus and\n * would scrub a share into a wall of \"[redacted]\". Here the default keeps the narrative readable and\n * masks only secrets/PII, with a fail-closed post-render scan; `structure` is the aggressive scrub;\n * `off` is the explicit verbatim opt-out.\n *\n * Redaction is BEST-EFFORT / pattern-based, not a guarantee — the emitted header says so.\n */\nimport { createHash } from \"node:crypto\";\nimport { sessionToMarkdown, type MarkdownSession, type MarkdownEvent } from \"./markdown.js\";\nimport { maskSecrets, deriveHomeUsers, scrubUsernames, LEAK_PATTERNS, SHARE_LEAK, type LeakPattern } from \"./secrets.js\";\n\nexport type RedactionLevel = \"secrets\" | \"structure\";\nexport interface ExportOptions {\n level?: RedactionLevel | \"off\";\n}\nexport interface ExportResult {\n markdown: string;\n /** True if the fail-closed post-render scan had to catch a leak the field pass missed. */\n residualLeak: boolean;\n level: RedactionLevel | \"off\";\n}\n\nfunction hash(s: string): string {\n return createHash(\"sha256\").update(s).digest(\"hex\").slice(0, 4);\n}\n\n/** Aggressive path pseudonymization for the `structure` level (home username still stripped). */\nfunction structurePath(p: string | null): string | null {\n if (!p) return p;\n const abs = p.startsWith(\"/\");\n const segs = p.split(\"/\").filter(Boolean);\n const out = segs.map((seg, i) => {\n if (segs[i - 1] === \"home\" || segs[i - 1] === \"Users\") return \"user\";\n if (seg === \"home\" || seg === \"Users\") return seg;\n const last = i === segs.length - 1;\n const dot = last ? seg.lastIndexOf(\".\") : -1;\n return dot > 0 ? `file-${hash(seg)}${seg.slice(dot)}` : `dir-${hash(seg)}`;\n });\n return (abs ? \"/\" : \"\") + out.join(\"/\");\n}\n\n/** Replace narrative text with a placeholder, preserving null/empty. */\nfunction scrub(s: string | null): string | null {\n return s && s.length > 0 ? \"[redacted]\" : s;\n}\n\n/** Apply the chosen redaction policy to the export data model, returning fresh copies. */\nfunction redactSession(\n session: MarkdownSession,\n events: MarkdownEvent[],\n level: RedactionLevel,\n): { session: MarkdownSession; events: MarkdownEvent[] } {\n // Derive the session's home-dir owner(s) from ALL raw fields, so a bare username in a github URL\n // or prose (no path form) is scrubbed too. Done pre-masking, before /home/<u>/ becomes /home/user/.\n const users = deriveHomeUsers([\n session.title, session.project, session.source,\n ...events.flatMap((e) => [e.text, e.thinking, ...e.toolCalls.map((t) => t.input_json)]),\n ]);\n const unuser = (s: string | null) => (s == null ? s : scrubUsernames(s, users));\n\n if (level === \"structure\") {\n return {\n session: { ...session, title: session.title ? \"Redacted session\" : session.title, project: structurePath(session.project), source: unuser(session.source) },\n events: events.map((e) => ({\n ...e,\n text: scrub(e.text),\n thinking: scrub(e.thinking),\n toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: t.input_json ? \"{}\" : t.input_json })),\n })),\n };\n }\n // \"secrets\": selective masking, narrative preserved.\n const mask = (s: string | null) => (s == null ? s : scrubUsernames(maskSecrets(s), users));\n return {\n session: { ...session, title: mask(session.title), project: mask(session.project), source: mask(session.source) },\n events: events.map((e) => ({\n ...e,\n text: mask(e.text),\n thinking: mask(e.thinking),\n toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: mask(t.input_json) })),\n })),\n };\n}\n\n/** Global replace of any scan-set match with [REDACTED]; reports whether anything was caught. */\nfunction sweep(md: string, patterns: LeakPattern[]): { md: string; caught: boolean } {\n let out = md;\n let caught = false;\n for (const { re } of patterns) {\n const g = new RegExp(re.source, re.flags.includes(\"g\") ? re.flags : re.flags + \"g\");\n if (g.test(out)) {\n caught = true;\n out = out.replace(g, \"[REDACTED]\");\n }\n }\n return { md: out, caught };\n}\n\nfunction disclaimer(level: RedactionLevel): string {\n return (\n `> 🔒 **Redacted export** (level: ${level}) — best-effort automatic masking of secrets, tokens, ` +\n `emails, IPs, and usernames. Not a guarantee; review before sharing. Generated locally by ` +\n `Agent Lens — never uploaded.`\n );\n}\n\n/**\n * Render a session to a shareable Markdown artifact. The single entry point used by the server\n * endpoint and the CLI. For redacted levels: redact fields → render → fail-closed sweep →\n * prepend the disclaimer.\n */\nexport function exportMarkdown(session: MarkdownSession, events: MarkdownEvent[], opts: ExportOptions = {}): ExportResult {\n const level = opts.level ?? \"secrets\";\n if (level === \"off\") {\n return { markdown: sessionToMarkdown(session, events), residualLeak: false, level: \"off\" };\n }\n const redacted = redactSession(session, events, level);\n const rendered = sessionToMarkdown(redacted.session, redacted.events);\n const scanSet = level === \"structure\" ? LEAK_PATTERNS : SHARE_LEAK;\n const { md, caught } = sweep(rendered, scanSet);\n return { markdown: `${disclaimer(level)}\\n\\n${md}`, residualLeak: caught, level };\n}\n","/** Render a session to Markdown — shared by the export endpoint (ADR: data exportable as markdown). */\n\nexport interface MarkdownSession {\n id: string;\n title: string | null;\n source: string | null;\n project: string | null;\n model: string | null;\n started_at: string | null;\n ended_at: string | null;\n}\n\nexport interface MarkdownToolCall {\n tool_name: string;\n skill_name: string | null;\n agent_type: string | null;\n input_json: string | null;\n status: string | null;\n}\n\nexport interface MarkdownEvent {\n type: string;\n role: string | null;\n timestamp: string | null;\n text: string | null;\n thinking: string | null;\n toolCalls: MarkdownToolCall[];\n}\n\n/** Cap long prose/JSON at 2000 chars so an export stays readable — this truncates, it does not fence. */\nfunction truncate(s: string): string {\n return s.length > 2000 ? s.slice(0, 2000) + \"\\n… (truncated)\" : s;\n}\n\nexport function sessionToMarkdown(s: MarkdownSession, events: MarkdownEvent[]): string {\n const out: string[] = [];\n out.push(`# ${s.title || \"Session \" + s.id.slice(0, 8)}`);\n out.push(\"\");\n const meta = [\n [\"Session\", s.id],\n [\"Source\", s.source],\n [\"Project\", s.project],\n [\"Model\", s.model],\n [\"Started\", s.started_at],\n [\"Ended\", s.ended_at],\n ].filter(([, v]) => v);\n for (const [k, v] of meta) out.push(`- **${k}:** ${v}`);\n out.push(\"\");\n out.push(\"---\");\n out.push(\"\");\n\n for (const e of events) {\n const who = e.role || e.type;\n const ts = e.timestamp ? ` _(${e.timestamp})_` : \"\";\n if (e.thinking) {\n out.push(`### 🧠 thinking${ts}`);\n out.push(\"\");\n out.push(truncate(e.thinking));\n out.push(\"\");\n }\n if (e.text) {\n const icon = who === \"user\" ? \"👤\" : who === \"assistant\" ? \"🤖\" : \"⚙️\";\n out.push(`### ${icon} ${who}${ts}`);\n out.push(\"\");\n out.push(truncate(e.text));\n out.push(\"\");\n }\n for (const tc of e.toolCalls) {\n const label = tc.skill_name\n ? `Skill: ${tc.skill_name}`\n : tc.agent_type\n ? `${tc.tool_name} → ${tc.agent_type}`\n : tc.tool_name;\n const status = tc.status ? ` _(${tc.status})_` : \"\";\n out.push(`> 🔧 **${label}**${status}`);\n if (tc.input_json && tc.input_json !== \"{}\") {\n out.push(\"```json\");\n out.push(truncate(tc.input_json));\n out.push(\"```\");\n }\n out.push(\"\");\n }\n }\n return out.join(\"\\n\");\n}\n","/**\n * Agent Lens — tool-error classification. No AI: an errored tool call is bucketed deterministically\n * by pattern-matching its verbatim `result_summary`. Re-runnable and idempotent — the same text always\n * yields the same bucket. `ERROR_CLASSIFIER_VERSION` lets a future engine supersede prior labels.\n *\n * ── The authority boundary (why this is careful) ────────────────────────────────────────────────\n * The Messages API's tool_result carries a single boolean `is_error` and **no field for *why*** a\n * result is an error — the API does not distinguish a tool that genuinely failed from one the user or\n * a permission/guardrail system rejected (see platform.claude.com tool-use docs). Claude Code sets\n * `is_error: true` for *both* real failures and permission denials / guardrail blocks; the only signal\n * of which is the human-readable result text (see code.claude.com/docs/en/errors + the auto-mode post).\n *\n * Therefore: the raw `is_error` / `tool_calls.status='error'` **count** is the authoritative, documented\n * signal. The `type` bucket and the failure-vs-rejection `kind` below are a **heuristic over Claude\n * Code's result wording** — best-effort, and liable to drift if that wording changes. Surface them as\n * such (not as an API-blessed fact), and bump ERROR_CLASSIFIER_VERSION when the patterns change.\n */\n\n// Bump on any pattern/bucket change so a reclassification is attributable to an engine version\n// (mirrors CLASSIFIER_VERSION / DETECTOR_VERSION).\nexport const ERROR_CLASSIFIER_VERSION = 1;\n\n/** The error bucket. `other` = errored but unmatched by the patterns below. */\nexport type ToolErrorType =\n | \"string-not-found\" // Edit: the old_string wasn't found in the file\n | \"command-failed\" // Bash: non-zero exit code\n | \"file-state\" // Read/Write/Edit: file missing / not-read-first / changed-since-read / is-a-directory\n | \"token-limit\" // Read: file content exceeds the tool's token cap\n | \"user-rejected\" // the human declined / interrupted / cancelled the tool use\n | \"guardrail-blocked\" // Claude Code's own guardrail blocked the command\n | \"other\";\n\n/**\n * Whether the error reflects the agent's tool genuinely failing (`failure`) or a human/harness\n * declining to run it (`rejection`). Rejections are NOT agent failures — separating them keeps the\n * \"how often did the agent's tools fail\" signal honest.\n */\nexport type ToolErrorKind = \"failure\" | \"rejection\";\n\nexport interface ToolErrorClass {\n type: ToolErrorType;\n kind: ToolErrorKind;\n}\n\n/** Error types that represent a human/harness declining the tool use — NOT the agent's tool failing. */\nconst REJECTION_TYPES: ReadonlySet<ToolErrorType> = new Set<ToolErrorType>([\"user-rejected\", \"guardrail-blocked\"]);\n\n/** The kind (failure vs rejection) implied by an error type — the single mapping used everywhere. */\nexport function errorKind(type: ToolErrorType): ToolErrorKind {\n return REJECTION_TYPES.has(type) ? \"rejection\" : \"failure\";\n}\n\n// Ordered most-specific → fallback; first match wins. Rejection patterns are checked first because a\n// rejection message can also mention a command that would otherwise look like a real failure. `kind` is\n// derived from `type` via errorKind() — the single source of that mapping — not re-encoded per row.\nconst PATTERNS: Array<{ re: RegExp; type: ToolErrorType }> = [\n // — Rejections / blocks: is_error, but the agent's tool didn't fail — a human or guardrail stopped it.\n { re: /interrupted by user|tool use was rejected|does(?:n['’]| not)t want to proceed|\\bCancelled:/i, type: \"user-rejected\" },\n { re: /\\bBlocked:/i, type: \"guardrail-blocked\" },\n // — Real tool failures.\n { re: /String to replace not found/i, type: \"string-not-found\" },\n { re: /exceeds maximum allowed tokens/i, type: \"token-limit\" },\n // A non-zero Bash exit code is a command failure regardless of what its stderr text mentions — check\n // it before the file-state fs-error patterns, since ENOENT/EISDIR also appear inside command stderr\n // (e.g. `Exit code 2 npm error ENOENT`). The file-state patterns then catch the Read/Write/Edit\n // tool_use_errors, which carry no exit code.\n { re: /\\bExit code\\b/i, type: \"command-failed\" },\n { re: /has not been read yet|modified since (?:you |it was )?read|File (?:content )?does not exist|\\bEISDIR\\b|\\bENOENT\\b/i, type: \"file-state\" },\n];\n\n/**\n * Classify an errored tool call from its `result_summary` text. Only call this on tool calls already\n * known to be errors (`status='error'` / `is_error: true`) — it always returns a class, defaulting to\n * `{ type: \"other\", kind: \"failure\" }` for an errored result whose text matches no pattern.\n */\nexport function classifyToolError(resultSummary: string | null | undefined): ToolErrorClass {\n const s = resultSummary ?? \"\";\n for (const p of PATTERNS) {\n if (p.re.test(s)) return { type: p.type, kind: errorKind(p.type) };\n }\n return { type: \"other\", kind: errorKind(\"other\") };\n}\n","/**\n * `UserError` — an error caused by what the user typed or configured, as opposed to a bug.\n *\n * The CLI's top-level boundary (packages/cli/src/index.ts) prints these as a single\n * `agent-lens: <message>` line and exits 1, and lets everything else through with its stack intact.\n * That split is the whole point: a bad `--times` value is not a crash and shouldn't read like one,\n * while a genuine TypeError is worthless without the trace.\n *\n * So throw this only where the message is already written *for the user* — it becomes the entire\n * output they see, with no stack to fall back on.\n *\n * (Unrelated to `errors.ts`, which classifies the tool-call errors found inside collected\n * transcripts. This one is about the CLI process itself.)\n */\nexport class UserError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"UserError\";\n }\n}\n","/**\n * Model pricing for deriving cost from token counts (ADR-003: no cost is stored in traces).\n * USD per 1M tokens. Cache-read is the discounted rate; cache-write the premium rate.\n *\n * Rates are matched by prefix so dated/variant model ids resolve (e.g. 'claude-opus-4-8[1m]').\n * Update as pricing changes; unknown models cost 0 and are reported separately.\n *\n * PRICE_TABLE is the built-in default. A `pricing` block in agent-lens.config.json can override or\n * extend it (ADR-028) — `resolvePricing()` in config.ts builds the merged table and the process\n * entry point installs it with {@link usePricing}, so a model that launches between releases can be\n * priced without one. Cost is still derived at read time, never stored.\n */\n\nexport interface Rate {\n input: number;\n output: number;\n cacheWrite: number;\n cacheRead: number;\n}\n\n/**\n * Claude Code stamps this on locally-generated messages — no API call was made, so it is correctly\n * unpriced and must never be reported as a missing rate. Excluded wherever unpriced models are\n * surfaced (ingest report, /api/about, per-session markers).\n */\nexport const SYNTHETIC_MODEL = \"<synthetic>\";\n\n/** Model ids with real token usage but no rate in the active table — the \"cost is understated\" set. */\nexport function unpricedModels(models: Iterable<string | null | undefined>): string[] {\n const out = new Set<string>();\n for (const m of models) {\n if (m && m !== SYNTHETIC_MODEL && !rateForModel(m)) out.add(m);\n }\n return [...out].sort();\n}\n\nexport interface UsageTokens {\n input_tokens: number;\n output_tokens: number;\n cache_creation_input_tokens: number;\n cache_read_input_tokens: number;\n}\n\n/**\n * Prefix → rate. Longest matching prefix wins, so dated/variant ids resolve to the most specific\n * entry (e.g. 'claude-opus-4-8' beats the legacy 'claude-opus-4' fallback).\n * USD per 1M tokens. Convention: cacheWrite (5-minute) = 1.25× input, cacheRead = 0.1× input.\n */\nexport const PRICE_TABLE: Record<string, Rate> = {\n // 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.\n // The bare 'claude-opus-4' fallback stays at the legacy rate for 4.0/4.1; 4.5+ override below.\n \"claude-opus-4\": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },\n \"claude-opus-4-5\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-4-6\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-4-7\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-4-8\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-5\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-fable-5\": { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1.0 },\n \"claude-sonnet-4\": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },\n // Sonnet 5 list price. An introductory $2/$10 rate runs through 2026-08-31; we bill the list\n // price so the estimate doesn't silently change under a date, matching the \"list price\" label.\n \"claude-sonnet-5\": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },\n \"claude-haiku-4\": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },\n \"claude-3-5-haiku\": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },\n \"claude-3-5-sonnet\": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },\n};\n\n/** Cache-rate convention when an override gives only input/output — mirrors the PRICE_TABLE entries. */\nconst CACHE_WRITE_MULTIPLIER = 1.25;\nconst CACHE_READ_MULTIPLIER = 0.1;\n\nexport interface MergedPricing {\n table: Record<string, Rate>;\n /** Keys rejected as malformed. Reported, never applied — a typo must not silently reprice a model. */\n invalid: string[];\n /** Keys that were accepted, so callers can report \"N overrides in force\" without diffing tables. */\n applied: string[];\n}\n\nfunction toRate(v: unknown): Rate | null {\n if (v === null || typeof v !== \"object\" || Array.isArray(v)) return null;\n const o = v as Record<string, unknown>;\n const num = (x: unknown): number | null =>\n typeof x === \"number\" && Number.isFinite(x) && x >= 0 ? x : null;\n const input = num(o.input);\n const output = num(o.output);\n if (input === null || output === null) return null;\n // Optional keys must be valid *if present* — a mistyped cacheRead falls back to the convention\n // silently otherwise, which is the same class of bug as the missing entry this all came from.\n if (o.cacheWrite !== undefined && num(o.cacheWrite) === null) return null;\n if (o.cacheRead !== undefined && num(o.cacheRead) === null) return null;\n return {\n input,\n output,\n cacheWrite: num(o.cacheWrite) ?? input * CACHE_WRITE_MULTIPLIER,\n cacheRead: num(o.cacheRead) ?? input * CACHE_READ_MULTIPLIER,\n };\n}\n\n/**\n * Overlay user-supplied rates on a base table, by exact prefix key. Pure: the caller decides what to\n * do with `invalid` (we warn and keep the built-in rate — cost is a labelled estimate, so a typo in\n * an optional block must not stop the tool from running).\n */\nexport function mergePricing(base: Record<string, Rate>, overrides: unknown): MergedPricing {\n const table = { ...base };\n const invalid: string[] = [];\n const applied: string[] = [];\n if (overrides === null || typeof overrides !== \"object\" || Array.isArray(overrides)) {\n return { table, invalid, applied };\n }\n for (const [prefix, value] of Object.entries(overrides as Record<string, unknown>)) {\n const rate = toRate(value);\n if (!prefix.trim() || !rate) invalid.push(prefix);\n else {\n table[prefix] = rate;\n applied.push(prefix);\n }\n }\n return { table, invalid: invalid.sort(), applied: applied.sort() };\n}\n\n/**\n * The table `rateForModel` reads. Process-wide and set once at startup by the CLI entry points, so\n * the ~6 cost call sites keep their signatures and no table has to be threaded through the server's\n * query layer. Tests that install a table must restore PRICE_TABLE afterwards.\n */\nlet activeTable: Record<string, Rate> = PRICE_TABLE;\n\n/** Install the effective price table (built-in defaults merged with config overrides). */\nexport function usePricing(table: Record<string, Rate>): void {\n activeTable = table;\n}\n\n/** The table currently in force — for diagnostics (`agent-lens config`, /api/about). */\nexport function activePricing(): Record<string, Rate> {\n return activeTable;\n}\n\nexport function rateForModel(model: string | null | undefined): Rate | null {\n if (!model) return null;\n let best: { len: number; rate: Rate } | null = null;\n for (const [prefix, rate] of Object.entries(activeTable)) {\n if (model.startsWith(prefix) && (!best || prefix.length > best.len)) {\n best = { len: prefix.length, rate };\n }\n }\n return best?.rate ?? null;\n}\n\n/** Cost in USD for one usage record. Returns 0 for unknown models. */\nexport function costForUsage(model: string | null | undefined, u: UsageTokens): number {\n const r = rateForModel(model);\n if (!r) return 0;\n const M = 1_000_000;\n return (\n (u.input_tokens * r.input +\n u.output_tokens * r.output +\n u.cache_creation_input_tokens * r.cacheWrite +\n u.cache_read_input_tokens * r.cacheRead) /\n M\n );\n}\n","/**\n * Agent Lens — `events.raw_json` storage codec (ADR-011).\n *\n * Every transcript line is kept verbatim in `events.raw_json` for lossless re-derivation. That column\n * dominates DB growth, so it is stored gzip-compressed as a BLOB. This module is the single chokepoint:\n * the ingest writer compresses with `packRaw`, the server reader decompresses with `unpackRaw`.\n *\n * `unpackRaw` sniffs the gzip magic bytes (0x1f 0x8b) so it transparently handles a mix of compressed\n * BLOBs and legacy plain-text rows (a DB written before the migration, or read before `--full` runs).\n * JSON always begins with `{`/`[`/`\"`/whitespace, never 0x1f, so the sniff is unambiguous.\n */\nimport { gzipSync, gunzipSync } from \"node:zlib\";\n\n/** Compress a raw transcript line for storage in `events.raw_json` (BLOB). */\nexport function packRaw(json: string): Buffer {\n return gzipSync(Buffer.from(json, \"utf8\"));\n}\n\n/**\n * Decompress a stored `raw_json` value. Accepts gzip BLOBs and legacy plain text/strings.\n *\n * Takes `Uint8Array`, not `Buffer`: `node:sqlite` hands BLOBs back as plain `Uint8Array` (ADR-029),\n * and `Buffer.isBuffer` is false for those. Gating on `Buffer` here would silently skip the gunzip\n * and return the compressed bytes as if they were legacy plain text. `Buffer` still satisfies this —\n * it is a `Uint8Array` subclass — so the write path is unchanged.\n */\nexport function unpackRaw(value: string | Uint8Array): string {\n if (typeof value === \"string\") return value;\n if (value.length >= 2 && value[0] === 0x1f && value[1] === 0x8b) {\n return gunzipSync(value).toString(\"utf8\");\n }\n return Buffer.from(value).toString(\"utf8\");\n}\n","/**\n * Path resolution shared by ingest, server, and the CLI. The goal is that agent-lens works both\n * from the dev monorepo (data under <repo>/data, config at the repo root) AND when installed as a\n * standalone package under node_modules (per-user OS data dir, no repo layout to assume).\n *\n * Nothing here reads process state beyond env vars + the filesystem, so it is safe to call from any\n * entrypoint. `import.meta.url`-relative resolution replaces the old hardcoded \"../../..\" repo anchor.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { homedir, platform } from \"node:os\";\n\n/** Expand a leading `~` or `$HOME` and resolve to an absolute path. Empty input passes through. */\nexport function expandPath(p: string): string {\n if (!p) return p;\n let out = p;\n if (out === \"~\") out = homedir();\n else if (out.startsWith(\"~/\")) out = join(homedir(), out.slice(2));\n out = out.replace(/\\$HOME/g, homedir());\n return resolve(out);\n}\n\nconst moduleDir = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Find the agent-lens monorepo root by walking up from `from` (default: this module's location).\n * A directory qualifies only if it has BOTH a `pnpm-workspace.yaml` AND a `package.json` named\n * \"agent-lens\" — so an unrelated pnpm workspace never matches, and the published/bundled package\n * (which ships neither) resolves to `null`, sending data to the per-user dir instead.\n */\nexport function findRepoRoot(from: string = moduleDir): string | null {\n let dir = resolve(from);\n for (;;) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) {\n try {\n const pkg = JSON.parse(readFileSync(join(dir, \"package.json\"), \"utf8\"));\n if (pkg?.name === \"agent-lens\") return dir;\n } catch {\n /* not the file we want — keep walking */\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** Per-user data directory used when not running from the repo (installed CLI). */\nfunction userDataDir(): string {\n const home = homedir();\n if (platform() === \"win32\") {\n return join(process.env.LOCALAPPDATA || join(home, \"AppData\", \"Local\"), \"agent-lens\");\n }\n if (platform() === \"darwin\") {\n return join(home, \"Library\", \"Application Support\", \"agent-lens\");\n }\n return join(process.env.XDG_DATA_HOME || join(home, \".local\", \"share\"), \"agent-lens\");\n}\n\n/** The data dir: `AGENT_LENS_DATA` → `<repo>/data` (dev) → per-user OS dir (installed). */\nexport function resolveDataDir(repoRoot: string | null = findRepoRoot()): string {\n if (process.env.AGENT_LENS_DATA) return resolve(process.env.AGENT_LENS_DATA);\n if (repoRoot) return join(repoRoot, \"data\");\n return userDataDir();\n}\n\n/**\n * The raw transcript mirror, always `<dataDir>/archive` (ADR-021). Deliberately not independently\n * configurable: it is the source of truth, so a stray override desyncs the writer (collect) from the\n * readers (ingest, refresh) and silently strands previously collected data. Move the data dir instead.\n */\nexport function resolveArchiveDir(repoRoot: string | null = findRepoRoot()): string {\n return join(resolveDataDir(repoRoot), \"archive\");\n}\n\n/**\n * The writable triage/prefs sidecar (ADR-018) for a given store: `triage.db` beside it. Also fixed\n * (ADR-021) — it holds hand-authored state that nothing can rebuild, so it travels with the db it\n * annotates rather than taking an override of its own.\n */\nexport function triageDbFor(dbPath: string): string {\n return join(dirname(dbPath), \"triage.db\");\n}\n\n/**\n * First existing config file, in precedence order:\n * `AGENT_LENS_CONFIG` → `<dataDir>/agent-lens.config.json` → repo config → repo example.\n * Returns `null` when none exist (callers fall back to the built-in default source).\n */\nexport function resolveConfigFile(repoRoot: string | null, dataDir: string): string | null {\n const candidates = [\n process.env.AGENT_LENS_CONFIG,\n join(dataDir, \"agent-lens.config.json\"),\n repoRoot ? join(repoRoot, \"agent-lens.config.json\") : null,\n repoRoot ? join(repoRoot, \"agent-lens.config.example.json\") : null,\n ].filter((x): x is string => Boolean(x));\n for (const f of candidates) if (existsSync(f)) return f;\n return null;\n}\n\n/**\n * The built web SPA directory. Tries, in order: the bundled layout (`<pkgDir>/web`, a sibling of the\n * CLI's `dist/`), the dev sibling (`packages/server/dist` → `packages/web/dist`), then the repo path.\n * `fromDir` is the directory of the calling module (`dirname(fileURLToPath(import.meta.url))`).\n */\nexport function resolveWebDist(fromDir: string, repoRoot: string | null = findRepoRoot(fromDir)): string {\n const installed = resolve(fromDir, \"../web\"); // bundled CLI: dist/ and web/ are siblings\n if (existsSync(installed)) return installed;\n const devSibling = resolve(fromDir, \"../../web/dist\"); // packages/server/dist → packages/web/dist\n if (existsSync(devSibling)) return devSibling;\n if (repoRoot) return join(repoRoot, \"packages/web/dist\");\n return installed; // best-effort default; caller guards with existsSync\n}\n","/**\n * Runtime configuration resolver — server settings (port/host) and the store path (db) layered over\n * the SAME agent-lens.config.json that carries `sources`/`exclude`. Kept here (not in sources.ts) so\n * the file is parsed in one place and both the source resolver and the runtime knobs share it.\n *\n * Precedence, highest wins: CLI flag (override arg) > env var > config file > built-in default.\n *\n * Deliberately NOT resolved here:\n * - The non-loopback bind guard (`AGENT_LENS_ALLOW_NONLOCAL`) — a privacy guardrail (ADR-005) that\n * stays an explicit env-only override enforced by the server, never a convenient flag/config key.\n * - The data dir (`AGENT_LENS_DATA`) — it is where the config file is *found* (see resolveConfigFile),\n * so reading it back out of that file would be circular. Env-only by construction.\n */\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { expandPath, findRepoRoot, resolveConfigFile, resolveDataDir } from \"./paths.js\";\nimport { mergePricing, PRICE_TABLE, usePricing, type Rate } from \"./pricing.js\";\nimport { UserError } from \"./user-error.js\";\n\n/** Shape of agent-lens.config.json (only the fields this module and sources.ts read). */\nexport interface AgentLensConfigFile {\n sources?: unknown[];\n exclude?: unknown[];\n /** SQLite store path. Top-level, not under `server`: ingest and export read it too. */\n db?: string;\n server?: { port?: number; host?: string };\n /** Model-price overrides, keyed by model-id prefix (ADR-028). Validated by mergePricing, not here. */\n pricing?: unknown;\n}\n\nexport const DEFAULT_PORT = 4477;\nexport const DEFAULT_HOST = \"127.0.0.1\"; // loopback only\nexport const DEFAULT_DB_NAME = \"agent-lens.db\";\n\n/** `.versions/` retention window. NOTE: `scripts/prune.sh` carries its own copy (bash can't import\n * this) — change both together. The TS side reads it here so the CLI and the server cannot drift. */\nexport const DEFAULT_VERSIONS_KEEP_DAYS = 90;\n\n/** The retention window in force. Narrower than ConfigOrigin on purpose: retention has no flag or\n * config-file layer, so \"env\" and \"default\" are the only outcomes this can ever produce. */\nexport function resolveRetention(): { keepDays: number; origin: \"env\" | \"default\" } {\n const raw = process.env.AGENT_LENS_VERSIONS_KEEP_DAYS;\n const parsed = raw ? Number(raw) : NaN;\n return Number.isFinite(parsed) && parsed >= 0\n ? { keepDays: parsed, origin: \"env\" }\n : { keepDays: DEFAULT_VERSIONS_KEEP_DAYS, origin: \"default\" };\n}\n\nexport interface ResolvedPricing {\n /** Built-in PRICE_TABLE with any valid config overrides applied. */\n table: Record<string, Rate>;\n /** \"file\" once any override is in force, else \"default\". No flag or env layer exists (see below). */\n origin: Extract<ConfigOrigin, \"file\" | \"default\">;\n /** Override keys applied, and those rejected as malformed — both surfaced by the diagnostics. */\n applied: string[];\n invalid: string[];\n}\n\n/**\n * Resolve the effective model-price table: built-in defaults overlaid with the config file's\n * `pricing` block (ADR-028). Same shape as the other resolvers here — pure over an injected config,\n * tagged with where the value came from.\n *\n * Narrower than ConfigOrigin on purpose, like resolveRetention: prices are a table, not a scalar, so\n * there is no sensible flag or env layer — \"file\" and \"default\" are the only reachable origins.\n *\n * Malformed entries are reported in `invalid` and dropped, leaving the built-in rate in place. That\n * is deliberately unlike validatePort, which throws: cost is a labelled estimate, so a typo in an\n * optional block should degrade the estimate, not stop the tool from starting.\n */\nexport function resolvePricing(cfg: AgentLensConfigFile | null = readConfigFile()): ResolvedPricing {\n const { table, invalid, applied } = mergePricing(PRICE_TABLE, cfg?.pricing);\n return { table, origin: applied.length ? \"file\" : \"default\", applied, invalid };\n}\n\n/**\n * Resolve the price table, install it process-wide, and warn about anything rejected. Called once at\n * startup by each entry point that derives cost (ingest, server) — the returned facts are what the\n * diagnostics report, so they describe the table actually in force rather than a fresh resolution.\n */\nexport function installPricing(cfg?: AgentLensConfigFile | null): ResolvedPricing {\n const resolved = cfg === undefined ? resolvePricing() : resolvePricing(cfg);\n usePricing(resolved.table);\n if (resolved.invalid.length) {\n console.error(\n `agent-lens: ignoring ${resolved.invalid.length} malformed pricing override(s): ` +\n `${resolved.invalid.join(\", \")} (each entry needs numeric input + output; built-in rates kept)`,\n );\n }\n return resolved;\n}\n\n/**\n * Read + parse the resolved config file once, or null when none exists. Shared by `sources.ts`\n * (sources/exclude) and this module (server settings) so the JSON is read a single time per call.\n */\nexport function readConfigFile(): AgentLensConfigFile | null {\n const repoRoot = findRepoRoot();\n const dataDir = resolveDataDir(repoRoot);\n const file = resolveConfigFile(repoRoot, dataDir);\n return file ? (JSON.parse(readFileSync(file, \"utf8\")) as AgentLensConfigFile) : null;\n}\n\n/** Where a resolved value came from — surfaced by `agent-lens config` for support/discoverability. */\nexport type ConfigOrigin = \"flag\" | \"env\" | \"file\" | \"default\";\n\nexport interface ResolvedDb {\n path: string;\n origin: ConfigOrigin;\n}\n\n/**\n * Resolve the SQLite store path with the same precedence as the server settings:\n * `--db` flag > AGENT_LENS_DB > config `db` > `<dataDir>/agent-lens.db`.\n *\n * The flag and env layers keep the pre-existing semantics EXACTLY — the raw string, any non-empty\n * value wins — so upgrading can never repoint the store of an install that already sets them. In\n * particular a literal `~`/`$HOME` in AGENT_LENS_DB keeps resolving the way it always did (as a\n * directory of that name); expanding it here would silently move such an install to a different,\n * empty database. Only the `db` config key — which no existing install can have — gets expandPath,\n * matching how `sources[].configDir` has always been treated.\n *\n * The triage store is derived from this path's directory by the server (ADR-018), so pointing `db`\n * elsewhere moves the pair together.\n */\nexport function resolveDbPath(override?: string, cfg: AgentLensConfigFile | null = readConfigFile()): ResolvedDb {\n if (override) return { path: override, origin: \"flag\" };\n if (process.env.AGENT_LENS_DB) return { path: process.env.AGENT_LENS_DB, origin: \"env\" };\n if (isSet(cfg?.db)) return { path: expandPath(String(cfg!.db).trim()), origin: \"file\" };\n return { path: join(resolveDataDir(findRepoRoot()), DEFAULT_DB_NAME), origin: \"default\" };\n}\n\nexport interface ServerOverrides {\n /** From a CLI flag (`serve --port/--host`); highest precedence. Empty/undefined means \"not set\". */\n port?: number | string;\n host?: string;\n}\n\nexport interface ResolvedServer {\n port: number;\n host: string;\n portOrigin: ConfigOrigin;\n hostOrigin: ConfigOrigin;\n}\n\n/** Validate a port from any source into an integer 1-65535, or throw a clear, source-tagged error. */\nexport function validatePort(value: number | string, source: string): number {\n const n = typeof value === \"number\" ? value : Number(String(value).trim());\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n // No \"agent-lens:\" prefix — the CLI's error boundary adds it, and both would read twice.\n throw new UserError(`invalid port from ${source}: '${value}' (expected an integer 1-65535)`);\n }\n return n;\n}\n\nfunction isSet(v: unknown): boolean {\n return v !== undefined && v !== null && String(v).trim() !== \"\";\n}\n\n/**\n * Resolve the effective server port + host with precedence flag > env > file > default. Validates the\n * port (range) and host (non-empty) at every layer so a bad value fails fast at startup rather than\n * silently coercing to NaN. Does NOT enforce the loopback guard — the server still does that.\n */\nexport function resolveServerConfig(\n overrides: ServerOverrides = {},\n cfg: AgentLensConfigFile | null = readConfigFile(),\n): ResolvedServer {\n let port: number;\n let portOrigin: ConfigOrigin;\n if (isSet(overrides.port)) {\n port = validatePort(overrides.port as number | string, \"--port\");\n portOrigin = \"flag\";\n } else if (isSet(process.env.AGENT_LENS_PORT)) {\n port = validatePort(process.env.AGENT_LENS_PORT!, \"AGENT_LENS_PORT\");\n portOrigin = \"env\";\n } else if (isSet(cfg?.server?.port)) {\n port = validatePort(cfg!.server!.port!, \"server.port (config file)\");\n portOrigin = \"file\";\n } else {\n port = DEFAULT_PORT;\n portOrigin = \"default\";\n }\n\n let host: string;\n let hostOrigin: ConfigOrigin;\n if (isSet(overrides.host)) {\n host = String(overrides.host).trim();\n hostOrigin = \"flag\";\n } else if (isSet(process.env.AGENT_LENS_HOST)) {\n host = process.env.AGENT_LENS_HOST!.trim();\n hostOrigin = \"env\";\n } else if (isSet(cfg?.server?.host)) {\n host = String(cfg!.server!.host).trim();\n hostOrigin = \"file\";\n } else {\n host = DEFAULT_HOST;\n hostOrigin = \"default\";\n }\n\n return { port, host, portOrigin, hostOrigin };\n}\n","/**\n * Resolve the running build's version (ADR-027).\n *\n * There is no version recorded in the repository: `@semantic-release/git` was dropped in PR #6\n * because its `git push HEAD:main` is rejected by the `protect-main` ruleset, so the committed\n * manifests are permanent placeholders (`0.0.0`, `0.0.0-development`). The real version lives in a\n * different place depending on how the app was installed, so resolution is a chain that reports\n * which link answered — the same \"say where the value came from\" idiom `agent-lens config` uses for\n * paths (see ConfigOrigin in config.ts).\n *\n * 1. package.json, when it is NOT a 0.0.0* placeholder → \"npm\" (published tarball:\n * @semantic-release/npm stamps the real version before packing)\n * 2. git describe --tags --always --dirty → \"git\" (a clone: a normal clone\n * fetches tags, and the extra detail — v0.9.6-3-gabc1234 — is what you want from a dev build)\n * 3. neither available → \"unknown\"\n *\n * \"unknown\" is reachable only from undocumented install paths (a shallow clone with no tags, or a\n * GitHub source zip, which has neither npm metadata nor .git). That is deliberate: reporting\n * \"unknown\" beats reporting the `0.0.0` placeholder as if it were real.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join, parse } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type VersionSource = \"npm\" | \"git\" | \"unknown\";\n\nexport interface ResolvedVersion {\n /** \"0.9.6\" (npm) · \"v0.9.6-3-gabc1234-dirty\" (git) · \"unknown\". Not necessarily semver — see source. */\n version: string;\n source: VersionSource;\n}\n\nconst moduleDir = dirname(fileURLToPath(import.meta.url));\n\n/** A committed manifest never carries a real version, only the release placeholders. */\nfunction isPlaceholder(v: string): boolean {\n return v.startsWith(\"0.0.0\");\n}\n\n/** Nearest package.json walking up from `dir`, or null. */\nfunction nearestPackageVersion(dir: string): string | null {\n const { root } = parse(dir);\n for (let d = dir; ; d = dirname(d)) {\n try {\n const pkg = JSON.parse(readFileSync(join(d, \"package.json\"), \"utf8\")) as { version?: string };\n if (typeof pkg.version === \"string\" && pkg.version) return pkg.version;\n } catch {\n /* no package.json here (or unreadable) — keep walking */\n }\n if (d === root) return null;\n }\n}\n\n/**\n * `git describe` in `cwd`. Returns null when git is missing, this is not a work tree, or no tag is\n * reachable — all of which are normal, not errors. `--always` still yields a bare SHA when the only\n * problem is a missing tag (shallow clone), which is more useful than nothing.\n */\nfunction gitDescribe(cwd: string): string | null {\n try {\n const r = spawnSync(\"git\", [\"describe\", \"--tags\", \"--always\", \"--dirty\"], {\n cwd,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n });\n if (r.status !== 0) return null;\n const out = r.stdout?.trim();\n return out ? out : null;\n } catch {\n return null; // git binary absent\n }\n}\n\n/** Uncached resolution — exported for tests, which need to drive each branch independently. */\nexport function computeVersion(fromDir: string = moduleDir): ResolvedVersion {\n const pkg = nearestPackageVersion(fromDir);\n if (pkg && !isPlaceholder(pkg)) return { version: pkg, source: \"npm\" };\n\n const described = gitDescribe(fromDir);\n if (described) return { version: described, source: \"git\" };\n\n return { version: \"unknown\", source: \"unknown\" };\n}\n\nlet cached: ResolvedVersion | null = null;\n\n/**\n * The running build's version, resolved once per process. Memoized because step 2 spawns `git`, and\n * this is read by /api/health, which the SPA polls — it must never spawn per request.\n */\nexport function resolveVersion(): ResolvedVersion {\n if (!cached) cached = computeVersion();\n return cached;\n}\n\n/** Test seam: drop the memo so a test can drive resolution against a different tree. */\nexport function resetVersionCache(): void {\n cached = null;\n}\n","/**\n * Canonical source resolver for Agent Lens (shared by the collector, ingester, server, CLI, and\n * the dev-only scripts/sources.mjs shim).\n *\n * A \"source\" is a labeled agent instance: { label, agent, configDir }. Multiple local accounts\n * (each with its own config folder) are each a source — e.g. \"personal\" → ~/.claude.\n *\n * Resolution order (see resolveConfigFile in paths.ts):\n * 1. $CLAUDE_DIR set → single legacy source { label: $AGENT_LENS_LABEL|\"default\", ~/.claude }\n * 2. $AGENT_LENS_CONFIG → that JSON file\n * 3. <dataDir>/agent-lens.config.json (installed user's config, next to their data)\n * 4. <repo>/agent-lens.config.json → <repo>/agent-lens.config.example.json\n * 5. built-in default → { label: \"default\", agent: \"claude-code\", configDir: \"$HOME/.claude\" }\n *\n * Excluded projects (playgrounds, personal, dummy tests, …) are resolved here too, from the config's\n * top-level `exclude: [<realProjectPath>, …]` plus the `AGENT_LENS_EXCLUDE` env (comma-separated).\n * They are honored at every stage — collect (not mirrored), ingest (not stored / pruned), corpus.\n */\nimport { expandPath } from \"./paths.js\";\nimport { readConfigFile } from \"./config.js\";\nimport { UserError } from \"./user-error.js\";\n\nexport interface Source {\n label: string;\n agent: string;\n configDir: string;\n}\n\nexport function loadSources(): Source[] {\n // 1. legacy single-source override (used by tests and ad-hoc runs)\n if (process.env.CLAUDE_DIR) {\n return [\n {\n label: process.env.AGENT_LENS_LABEL || \"default\",\n agent: \"claude-code\",\n configDir: expandPath(process.env.CLAUDE_DIR),\n },\n ];\n }\n\n const cfg = readConfigFile() ?? { sources: [{ label: \"default\", agent: \"claude-code\", configDir: \"$HOME/.claude\" }] };\n\n const seen = new Set<string>();\n const out: Source[] = [];\n for (const raw of cfg.sources || []) {\n const s = raw as Partial<Source> | null;\n if (!s || !s.label || !s.configDir) continue;\n if (seen.has(s.label)) throw new UserError(`duplicate source label: ${s.label}`);\n seen.add(s.label);\n out.push({ label: s.label, agent: s.agent || \"claude-code\", configDir: expandPath(s.configDir) });\n }\n if (!out.length) throw new UserError(\"no valid sources configured\");\n return out;\n}\n\n/**\n * Resolve the list of projects to EXCLUDE everywhere (collect, ingest, corpus). Union of the config's\n * `exclude` array and the `AGENT_LENS_EXCLUDE` env (comma-separated). Returns expanded real paths\n * (no trailing slash). The legacy CLAUDE_DIR mode still honors the env var.\n */\nexport function loadExcludes(): string[] {\n const fromEnv = (process.env.AGENT_LENS_EXCLUDE || \"\").split(\",\");\n let fromCfg: unknown[] = [];\n if (!process.env.CLAUDE_DIR) {\n const cfg = readConfigFile();\n if (cfg && Array.isArray(cfg.exclude)) fromCfg = cfg.exclude;\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const p of [...fromCfg, ...fromEnv]) {\n const e = expandPath(String(p ?? \"\").trim());\n if (e && !seen.has(e)) {\n seen.add(e);\n out.push(e);\n }\n }\n return out;\n}\n","/**\n * Encode a real project path the way Claude Code names its `projects/<encodedDir>` folder: every\n * `/` and `.` becomes `-` (e.g. `/home/u/git/agent-lens` → `-home-u-git-agent-lens`). Trailing\n * separators are dropped so a path with or without a trailing slash encodes identically.\n *\n * Canonical here in core so collect (Stage 1) and ingest/redact (Stage 2 / corpus) agree exactly.\n */\nexport function encodeProjectPath(p: string): string {\n return p.replace(/[/.]/g, \"-\").replace(/-+$/, \"\");\n}\n","/**\n * Agent Lens — Stage 1 collection, in portable Node (ADR-001, ADR-002, ADR-005).\n *\n * Passively copies agent session traces out of each configured source into the local archive before\n * the agent prunes them. Never deletes, never sends data off the machine, never copies secrets. This\n * replaced the former bash+rsync collector with the same semantics, minus the Linux-only dependencies.\n *\n * Per file (mirror of rsync --append-verify + the .versions pre-pass):\n * - no archive copy yet → copy whole\n * - unchanged since last collect (size + mtime) → skip\n * - source ≥ archive AND archive is a byte-prefix→ append the new tail (pure append)\n * - source shorter than archive (compaction) → snapshot the SOURCE to .versions/<ts>/, keep the\n * longer archive (no shrink → retains history)\n * - same-or-longer but prefix differs (divergence) → snapshot the OLD archive, overwrite with source\n * Stage 2 ingests the mirror AND .versions/, deduping events by uuid → maximal history.\n *\n * Secrets (.credentials.json, *.lock) are never copied; a post-pass also removes any stray\n * .credentials.json from the archive. Excluded projects (loadExcludes) are never mirrored.\n */\nimport {\n appendFileSync,\n closeSync,\n copyFileSync,\n existsSync,\n mkdirSync,\n openSync,\n readSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n writeSync,\n chmodSync,\n type Stats,\n} from \"node:fs\";\nimport { basename, dirname, join, relative, sep } from \"node:path\";\nimport { platform } from \"node:os\";\nimport { loadExcludes, loadSources, type Source } from \"./sources.js\";\nimport { resolveArchiveDir } from \"./paths.js\";\nimport { encodeProjectPath } from \"./projects.js\";\nimport { UserError } from \"./user-error.js\";\n\nconst POSIX = platform() !== \"win32\";\nconst DIR_MODE = 0o700; // archive is as sensitive as the originals (bash used umask 077)\nconst FILE_MODE = 0o600;\nconst CHUNK = 1 << 16;\n\nexport interface CollectOptions {\n /** Archive base dir. Default: the fixed `<dataDir>/archive` (ADR-021). Tests pass an explicit one. */\n archiveBase?: string;\n /** Sources to collect. Default: loadSources(). */\n sources?: Source[];\n /** Real project paths to exclude. Default: loadExcludes(). */\n excludes?: string[];\n /** Progress sink. Default: console.log. */\n log?: (msg: string) => void;\n}\n\nexport interface CollectStats {\n sources: number;\n scanned: number;\n copied: number;\n appended: number;\n diverged: number;\n compacted: number;\n snapshots: number;\n}\n\n/** `YYYYMMDDTHHMMSSmmm` in local time (matches `date +%Y%m%dT%H%M%S%3N`). */\nfunction collectTimestamp(d: Date = new Date()): string {\n const p = (n: number, w = 2) => String(n).padStart(w, \"0\");\n return (\n `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}T` +\n `${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`\n );\n}\n\nfunction ensureDir(dir: string): void {\n mkdirSync(dir, { recursive: true, mode: DIR_MODE });\n}\n\n/** Set archive file perms + mtime to mirror the source (like rsync -a / cp -p). */\nfunction syncMeta(dst: string, src: Stats): void {\n if (POSIX) {\n try {\n chmodSync(dst, FILE_MODE);\n } catch {\n /* best effort */\n }\n }\n try {\n utimesSync(dst, src.atime, src.mtime);\n } catch {\n /* best effort */\n }\n}\n\nfunction copyWhole(src: string, dst: string, srcStat: Stats): void {\n ensureDir(dirname(dst));\n copyFileSync(src, dst);\n syncMeta(dst, srcStat);\n}\n\n/** True if the first `len` bytes of `src` byte-equal the whole of `arc` (`arc` is `len` bytes). */\nfunction prefixEquals(src: string, arc: string, len: number): boolean {\n const sf = openSync(src, \"r\");\n const af = openSync(arc, \"r\");\n try {\n const sb = Buffer.allocUnsafe(CHUNK);\n const ab = Buffer.allocUnsafe(CHUNK);\n let remaining = len;\n while (remaining > 0) {\n const want = Math.min(CHUNK, remaining);\n const sr = readSync(sf, sb, 0, want, null);\n const ar = readSync(af, ab, 0, want, null);\n if (sr !== ar || sr === 0) return false;\n if (Buffer.compare(sb.subarray(0, sr), ab.subarray(0, sr)) !== 0) return false;\n remaining -= sr;\n }\n return remaining === 0;\n } finally {\n closeSync(sf);\n closeSync(af);\n }\n}\n\n/** Append `src[from..to)` to the end of `arc`. */\nfunction appendRange(src: string, arc: string, from: number, to: number): void {\n const sf = openSync(src, \"r\");\n const af = openSync(arc, \"a\");\n try {\n const b = Buffer.allocUnsafe(CHUNK);\n let pos = from;\n while (pos < to) {\n const want = Math.min(CHUNK, to - pos);\n const r = readSync(sf, b, 0, want, pos);\n if (r === 0) break;\n writeSync(af, b, 0, r);\n pos += r;\n }\n } finally {\n closeSync(sf);\n closeSync(af);\n }\n}\n\nfunction filesEqual(a: string, b: string): boolean {\n const sa = statSync(a);\n const sb = statSync(b);\n if (sa.size !== sb.size) return false;\n return prefixEquals(a, b, sa.size);\n}\n\n/** Most recent `.versions/<ts>/<rel>` snapshot for `rel`, or null. Timestamps sort chronologically. */\nfunction latestSnapshot(archive: string, relParts: string[]): string | null {\n const versRoot = join(archive, \".versions\");\n if (!existsSync(versRoot)) return null;\n const stamps = readdirSync(versRoot).sort();\n for (let i = stamps.length - 1; i >= 0; i--) {\n const p = join(versRoot, stamps[i]!, ...relParts);\n if (existsSync(p)) return p;\n }\n return null;\n}\n\nfunction snapshot(absPath: string, relParts: string[], versionsDir: string, stats: CollectStats): void {\n const dst = join(versionsDir, ...relParts);\n ensureDir(dirname(dst));\n copyFileSync(absPath, dst);\n syncMeta(dst, statSync(absPath));\n stats.snapshots++;\n}\n\n/**\n * Recursively collect the relevant transcript + sidecar files under `dir`:\n * - `*.jsonl` — session/subagent transcripts and journals (the core traces)\n * - anything under `workflows/` — the Workflow-tool run result sidecars (wf_<id>.json) + scripts\n * - `*.meta.json` — per-subagent metadata (agentType, description, spawnDepth, toolUseId)\n * - anything under `tool-results/` — full un-truncated tool outputs spilled from the transcript\n * Operational/state files (memory, cache, todos, shell-snapshots, …) are intentionally left behind.\n * Secrets (.credentials.json, *.lock) are filtered by the caller. `inWorkflows`/`inToolResults` track\n * whether recursion is inside one of those directories so every file within is captured.\n */\nfunction walkCollectable(dir: string, out: string[], inWorkflows = false, inToolResults = false): void {\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, e.name);\n if (e.isDirectory()) {\n walkCollectable(p, out, inWorkflows || e.name === \"workflows\", inToolResults || e.name === \"tool-results\");\n } else if (\n e.isFile() &&\n (e.name.endsWith(\".jsonl\") || inWorkflows || inToolResults || e.name.endsWith(\".meta.json\"))\n ) {\n out.push(p);\n }\n }\n}\n\n/** The single-file sync decision (the heart of the append-verify port). `appendable` is true only for\n * append-only logs (*.jsonl); for rewritten files (JSON/txt sidecars) a same-prefix growth is a\n * rewrite, not an append, so it's handled as a divergence (snapshot old, overwrite) — never appended. */\nfunction syncFile(src: string, dst: string, relParts: string[], archive: string, versionsDir: string, stats: CollectStats, appendable: boolean): void {\n const srcStat = statSync(src);\n if (!existsSync(dst)) {\n copyWhole(src, dst, srcStat);\n stats.copied++;\n return;\n }\n const arcStat = statSync(dst);\n // Unchanged since last collect (we preserve source mtime on write, like rsync -a) → skip, no read.\n if (srcStat.size === arcStat.size && Math.trunc(srcStat.mtimeMs) === Math.trunc(arcStat.mtimeMs)) return;\n\n const ssize = srcStat.size;\n const asize = arcStat.size;\n if (ssize === asize && prefixEquals(src, dst, asize)) {\n // Identical content, mtime moved → just realign metadata (no append, appendable or not).\n syncMeta(dst, srcStat);\n } else if (appendable && ssize > asize && prefixEquals(src, dst, asize)) {\n // Pure append onto an append-only log. Verify the full prefix before appending; never trust size.\n appendRange(src, dst, asize, ssize);\n stats.appended++;\n syncMeta(dst, srcStat);\n } else if (ssize < asize) {\n // Compaction: keep the longer archive; snapshot the compacted source (skip if an identical\n // snapshot already exists, so a stuck-compacted file doesn't re-snapshot every run).\n const last = latestSnapshot(archive, relParts);\n if (!last || !filesEqual(last, src)) snapshot(src, relParts, versionsDir, stats);\n stats.compacted++;\n } else {\n // Divergence: capture the old archive, then overwrite with the source.\n snapshot(dst, relParts, versionsDir, stats);\n copyWhole(src, dst, srcStat);\n stats.diverged++;\n }\n}\n\nfunction removeCredentials(dir: string): void {\n if (!existsSync(dir)) return;\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, e.name);\n if (e.isDirectory()) removeCredentials(p);\n else if (e.name === \".credentials.json\") {\n try {\n unlinkSync(p);\n } catch {\n /* ignore */\n }\n }\n }\n}\n\nfunction collectSource(\n source: Source,\n archiveBase: string,\n excludedDirs: string[],\n runTs: string,\n stats: CollectStats,\n log: (m: string) => void,\n): void {\n const { label, configDir } = source;\n const archive = join(archiveBase, label);\n const versionsDir = join(archive, \".versions\", runTs);\n if (!existsSync(configDir)) {\n log(`agent-lens: [${label}] source not found: ${configDir} (skipping)`);\n return;\n }\n ensureDir(join(archive, \"projects\"));\n ensureDir(join(archive, \"settings\"));\n\n const files: string[] = [];\n const projectsDir = join(configDir, \"projects\");\n if (existsSync(projectsDir)) walkCollectable(projectsDir, files);\n const historySrc = join(configDir, \"history.jsonl\");\n if (existsSync(historySrc)) files.push(historySrc);\n\n const before = { scanned: stats.scanned, snapshots: stats.snapshots };\n for (const src of files) {\n const base = basename(src);\n if (base === \".credentials.json\" || base.endsWith(\".lock\")) continue; // never copy secrets\n const relParts = relative(configDir, src).split(sep);\n const relPosix = relParts.join(\"/\");\n if (excludedDirs.some((enc) => relPosix.startsWith(`projects/${enc}/`))) continue; // excluded project\n stats.scanned++;\n syncFile(src, join(archive, ...relParts), relParts, archive, versionsDir, stats, base.endsWith(\".jsonl\"));\n }\n\n // Settings: latest-wins, snapshot old on change.\n for (const f of [\"settings.json\", \"settings.local.json\"]) {\n const s = join(configDir, f);\n if (!existsSync(s)) continue;\n const a = join(archive, \"settings\", f);\n if (existsSync(a) && !filesEqual(s, a)) snapshot(a, [\"settings\", f], versionsDir, stats);\n ensureDir(dirname(a));\n copyFileSync(s, a);\n syncMeta(a, statSync(s));\n }\n\n removeCredentials(archive);\n\n const scanned = stats.scanned - before.scanned;\n const snapshots = stats.snapshots - before.snapshots;\n const line = `${new Date().toISOString()} run=${runTs} source=${label} scanned=${scanned} snapshots=${snapshots} archive=${archive}`;\n log(`agent-lens: ${line}`);\n try {\n appendFileSync(join(archiveBase, \".collect.log\"), line + \"\\n\");\n } catch {\n /* log is best-effort */\n }\n}\n\n/** Collect every configured source into the archive. Returns aggregate counters. */\nexport function collectAll(opts: CollectOptions = {}): CollectStats {\n const log = opts.log ?? ((m: string) => console.log(m));\n const archiveBase = opts.archiveBase ?? resolveArchiveDir();\n const sources = opts.sources ?? loadSources();\n const excludedDirs = (opts.excludes ?? loadExcludes()).map(encodeProjectPath);\n const runTs = collectTimestamp();\n\n ensureDir(archiveBase);\n const stats: CollectStats = { sources: 0, scanned: 0, copied: 0, appended: 0, diverged: 0, compacted: 0, snapshots: 0 };\n if (!sources.length) throw new UserError(\"no sources configured\");\n for (const source of sources) {\n stats.sources++;\n collectSource(source, archiveBase, excludedDirs, runTs, stats, log);\n }\n return stats;\n}\n","/**\n * Cross-platform single-instance lock. collect+ingest take this so a too-frequent schedule (or an\n * overlapping watch trigger) can never run two collectors against the same archive or two writers\n * against the same SQLite DB. systemd already serializes its oneshot; this makes launchd/schtasks/\n * watch safe too. Not a hard mutex (a local, single-user tool doesn't need one) — a stale-aware\n * PID file: a run whose holder is dead or older than `staleMs` is reclaimed.\n */\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { hostname } from \"node:os\";\n\nexport interface Lock {\n /** Remove the lock file. Idempotent; also runs automatically on process exit. */\n release(): void;\n}\n\nfunction isAlive(pid: number): boolean {\n if (!pid) return false;\n try {\n process.kill(pid, 0); // signal 0 = existence check, doesn't actually signal\n return true;\n } catch (e) {\n // ESRCH → no such process (dead); EPERM → exists but not ours (alive).\n return (e as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * Try to acquire `lockPath`. Returns a Lock on success, or null if another live, non-stale process\n * currently holds it (caller should log \"run in progress\" and exit 0).\n */\nexport function acquireLock(lockPath: string, staleMs = 6 * 60 * 60 * 1000): Lock | null {\n mkdirSync(dirname(lockPath), { recursive: true });\n if (existsSync(lockPath)) {\n try {\n const held = JSON.parse(readFileSync(lockPath, \"utf8\")) as { pid?: number; ts?: number };\n const fresh = typeof held.ts === \"number\" && Date.now() - held.ts < staleMs;\n if (fresh && isAlive(held.pid ?? 0)) return null; // held by a live, recent process\n // else: dead PID or too old → reclaim below\n } catch {\n // corrupt lock file → reclaim\n }\n }\n writeFileSync(lockPath, JSON.stringify({ pid: process.pid, ts: Date.now(), host: hostname() }), { mode: 0o600 });\n let released = false;\n const release = () => {\n if (released) return;\n released = true;\n try {\n // Only remove if it's still ours (avoid deleting a lock a reclaimer took over).\n const cur = JSON.parse(readFileSync(lockPath, \"utf8\")) as { pid?: number };\n if (cur.pid === process.pid) unlinkSync(lockPath);\n } catch {\n /* already gone / unreadable */\n }\n };\n process.once(\"exit\", release);\n return { release };\n}\n","/**\n * Cross-platform OS-service integration for `agent-lens service`. Two targets:\n * - collector → a *periodic* `collect --then-ingest` job (Node can time work via `watch`, but\n * can't survive a reboot alone, so we register it with the OS scheduler)\n * - server → the *long-running* web UI + read-only API daemon (`serve`)\n *\n * Per platform:\n * - Linux → systemd user units (collector: oneshot service + timer; server: Type=simple + Restart) + linger\n * - macOS → launchd LaunchAgents (collector: StartCalendarInterval; server: KeepAlive)\n * - Windows → Task Scheduler (collector: one DAILY task per hour; server: ONLOGON task)\n *\n * The absolute `node` binary (process.execPath) and CLI entry path are baked into every unit/plist/\n * task, because schedulers don't inherit the interactive shell's PATH (the mise problem, generalized).\n * The same reasoning covers the environment — see serverEnv/pathEnv below: a scheduler inherits none\n * of the installing shell's env either, so anything the user explicitly set has to be written in.\n * The unit/plist/command text is generated by the pure functions below (unit-tested); runService\n * wraps them with the OS commands.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir, platform, userInfo } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { findRepoRoot, resolveDataDir } from \"./paths.js\";\nimport { resolveServerConfig } from \"./config.js\";\nimport { UserError } from \"./user-error.js\";\n\nexport const DEFAULT_HOURS = [9, 13, 17, 21];\n\n/**\n * The login name for `loginctl enable-linger`. Asks the OS (passwd entry) rather than reading $USER:\n * schedulers and `env -i` don't set it, which is exactly when this runs, and under sudo it names the\n * invoking user while the units belong to the effective one. Falls back to the env only if the\n * passwd lookup fails (rare — a uid with no entry, seen in some containers); \"\" means \"unknown\", and\n * callers skip linger rather than shelling out with an empty argument.\n */\nexport function currentUser(): string {\n try {\n return userInfo().username;\n } catch {\n return process.env.USER || process.env.LOGNAME || \"\";\n }\n}\n\nconst SYSTEMD_COLLECTOR_TIMER = \"agent-lens-collect.timer\";\nconst SYSTEMD_COLLECTOR_SERVICE = \"agent-lens-collect.service\";\nconst SYSTEMD_SERVER_SERVICE = \"agent-lens-server.service\";\nconst LAUNCHD_COLLECTOR_LABEL = \"org.agent-lens.collect\";\nconst LAUNCHD_SERVER_LABEL = \"org.agent-lens.server\";\nconst WINDOWS_COLLECTOR_PREFIX = \"AgentLens\\\\Collect\";\nconst WINDOWS_SERVER_TASK = \"AgentLens\\\\Server\";\n\nexport type ServiceTarget = \"collector\" | \"server\";\nexport type ServiceAction = \"install\" | \"uninstall\" | \"status\";\n\nexport interface ServiceParams {\n /** Absolute path to the CLI entry (the bundled agent-lens.js). */\n cliEntry: string;\n /** Which OS integrations to act on. */\n targets: ServiceTarget[];\n /** Hours (0–23) the collector runs at. Default: DEFAULT_HOURS. */\n hours?: number[];\n /** Absolute node binary. Default: process.execPath. */\n node?: string;\n}\n\n/** Parse the CLI target arg: undefined/\"all\" → both; \"collector\"/\"server\" → that one; else throw. */\nexport function parseTargets(spec: string | undefined): ServiceTarget[] {\n if (!spec || spec === \"all\") return [\"collector\", \"server\"];\n if (spec === \"collector\" || spec === \"server\") return [spec];\n throw new UserError(`invalid target '${spec}' (expected: collector | server | all)`);\n}\n\nexport function parseHours(spec: string | undefined): number[] {\n if (!spec) return DEFAULT_HOURS;\n const hours = spec\n .split(\",\")\n .map((s) => Number.parseInt(s.trim(), 10))\n .filter((n) => Number.isInteger(n) && n >= 0 && n <= 23);\n if (!hours.length) throw new UserError(`invalid hours '${spec}' (expected comma-separated 0-23, e.g. 9,13,17,21)`);\n return [...new Set(hours)].sort((a, b) => a - b);\n}\n\n/**\n * Effective port/host to bake into the server unit. Resolves via the shared config precedence\n * (env > config file > default) and bakes any value the user explicitly set — from the shell env OR\n * `server.{port,host}` in agent-lens.config.json — so the scheduled server matches what they run\n * interactively (schedulers don't inherit the shell env, hence the baking). Defaults stay unbaked so\n * a future default change still applies to already-installed units.\n */\nfunction serverEnv(): Record<string, string> {\n const env: Record<string, string> = {};\n const { port, host, portOrigin, hostOrigin } = resolveServerConfig();\n if (portOrigin !== \"default\") env.AGENT_LENS_PORT = String(port);\n if (hostOrigin !== \"default\") env.AGENT_LENS_HOST = host;\n return env;\n}\n\n/**\n * The path env, baked into *every* unit rather than just the server's — the collector writes the\n * archive, so pointing it at the wrong data dir is the more damaging half of getting this wrong.\n *\n * Unlike port/host these have no config-file equivalent to fall back on: the data dir is where the\n * config file is looked up, so it can only ever come from the environment (ADR-021). A scheduler\n * inherits none of the installing shell's environment, so without baking, `AGENT_LENS_DATA=… service\n * install` yields services that silently read a different store than every interactive command.\n *\n * Absolute, because the unit's working directory is not the one the user installed from.\n */\nfunction pathEnv(): Record<string, string> {\n const env: Record<string, string> = {};\n // resolveDataDir() rather than the raw variable: bake the path the CLI itself derives from it.\n if (process.env.AGENT_LENS_DATA) env.AGENT_LENS_DATA = resolveDataDir();\n if (process.env.AGENT_LENS_CONFIG) env.AGENT_LENS_CONFIG = resolve(process.env.AGENT_LENS_CONFIG);\n return env;\n}\n\n/** Report what got baked — the failure mode here is silence, so make the values visible at install. */\nfunction reportBakedEnv(env: Record<string, string>): void {\n for (const [k, v] of Object.entries(env)) console.log(`agent-lens service: ${k}=${v}`);\n}\n\n// ---- pure generators (unit-tested) --------------------------------------------------------------\n\n/** systemd `OnCalendar` hour list, e.g. [9,13] → \"09,13:00\". */\nexport function onCalendarHours(hours: number[]): string {\n return hours.map((h) => String(h).padStart(2, \"0\")).join(\",\") + \":00\";\n}\n\n/** `Environment=` lines for a systemd unit, one per variable (empty string when there are none). */\nfunction systemdEnvLines(env: Record<string, string>): string {\n return Object.entries(env)\n .map(([k, v]) => `Environment=${k}=${v}\\n`)\n .join(\"\");\n}\n\n/** Collector: oneshot unit that runs one `collect --then-ingest` pass (driven by the timer). */\nexport function systemdCollectorService(node: string, cli: string, env: Record<string, string> = {}): string {\n return `[Unit]\nDescription=Agent Lens — collect + ingest Claude Code session traces\n\n[Service]\nType=oneshot\n# Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.\nExecStart=${node} ${cli} collect --then-ingest\n${systemdEnvLines(env)}Nice=10\nIOSchedulingClass=idle\n`;\n}\n\nexport function systemdCollectorTimer(hours: number[]): string {\n return `[Unit]\nDescription=Agent Lens — periodic Claude Code trace collection\n\n[Timer]\n# A few times a day. Persistent catches runs missed while the machine was off.\nOnCalendar=*-*-* ${onCalendarHours(hours)}\nPersistent=true\nRandomizedDelaySec=300\n\n[Install]\nWantedBy=timers.target\n`;\n}\n\n/** Server: long-running daemon that runs `serve`, restarted on failure. */\nexport function systemdServerService(node: string, cli: string, env: Record<string, string> = {}): string {\n const envLines = systemdEnvLines(env);\n return `[Unit]\nDescription=Agent Lens — local web UI + read-only API server\nAfter=network.target\n\n[Service]\nType=simple\n# Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.\nExecStart=${node} ${cli} serve\n${envLines}# Long-running: always come back, including after a bare SIGTERM. systemd counts SIGTERM as a\n# clean exit, so Restart=on-failure would leave the server dead after a stray \"pkill -f ... serve\"\n# aimed at an ad-hoc test server — the unit's ExecStart matches that pattern too. Matches the\n# launchd KeepAlive=true behaviour. An explicit \"systemctl --user stop\" still stops it for good.\nRestart=always\nRestartSec=5\nNice=10\n\n[Install]\nWantedBy=default.target\n`;\n}\n\n/** An `EnvironmentVariables` dict for a launchd plist (empty string when there are none). */\nfunction launchdEnvDict(env: Record<string, string>): string {\n if (!Object.keys(env).length) return \"\";\n const entries = Object.entries(env)\n .map(([k, v]) => ` <key>${k}</key><string>${v}</string>`)\n .join(\"\\n\");\n return ` <key>EnvironmentVariables</key>\\n <dict>\\n${entries}\\n </dict>\\n`;\n}\n\nexport function launchdCollectorPlist(\n node: string,\n cli: string,\n hours: number[],\n logPath: string,\n env: Record<string, string> = {},\n): string {\n const cal = hours\n .map((h) => ` <dict><key>Hour</key><integer>${h}</integer><key>Minute</key><integer>0</integer></dict>`)\n .join(\"\\n\");\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key><string>${LAUNCHD_COLLECTOR_LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${node}</string>\n <string>${cli}</string>\n <string>collect</string>\n <string>--then-ingest</string>\n </array>\n <key>StartCalendarInterval</key>\n <array>\n${cal}\n </array>\n${launchdEnvDict(env)} <key>StandardOutPath</key><string>${logPath}</string>\n <key>StandardErrorPath</key><string>${logPath}</string>\n</dict>\n</plist>\n`;\n}\n\n/** Server: launchd agent kept alive (relaunched if it exits) and started at load. */\nexport function launchdServerPlist(node: string, cli: string, logPath: string, env: Record<string, string> = {}): string {\n const envDict = launchdEnvDict(env);\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key><string>${LAUNCHD_SERVER_LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${node}</string>\n <string>${cli}</string>\n <string>serve</string>\n </array>\n${envDict} <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><true/>\n <key>StandardOutPath</key><string>${logPath}</string>\n <key>StandardErrorPath</key><string>${logPath}</string>\n</dict>\n</plist>\n`;\n}\n\n// ---- OS helpers ---------------------------------------------------------------------------------\n\nfunction sh(cmd: string, args: string[], quiet = false): boolean {\n const r = spawnSync(cmd, args, { stdio: quiet ? \"ignore\" : \"inherit\", encoding: \"utf8\" });\n return r.status === 0;\n}\n\nfunction have(cmd: string): boolean {\n const r = spawnSync(cmd, [\"--version\"], { stdio: \"ignore\" });\n return r.status === 0 || r.error === undefined;\n}\n\nfunction systemdUnitDir(): string {\n return join(process.env.XDG_CONFIG_HOME || join(homedir(), \".config\"), \"systemd\", \"user\");\n}\n\n// ---- Linux (systemd user) -----------------------------------------------------------------------\n\nfunction linuxInstall(targets: ServiceTarget[], node: string, cli: string, hours: number[]): void {\n if (!have(\"systemctl\")) {\n console.error(\"agent-lens service: systemctl not found — is this a systemd system?\");\n process.exitCode = 1;\n return;\n }\n const dir = systemdUnitDir();\n mkdirSync(dir, { recursive: true });\n const paths = pathEnv();\n if (targets.includes(\"collector\")) {\n writeFileSync(join(dir, SYSTEMD_COLLECTOR_SERVICE), systemdCollectorService(node, cli, paths));\n writeFileSync(join(dir, SYSTEMD_COLLECTOR_TIMER), systemdCollectorTimer(hours));\n }\n if (targets.includes(\"server\")) {\n writeFileSync(join(dir, SYSTEMD_SERVER_SERVICE), systemdServerService(node, cli, { ...paths, ...serverEnv() }));\n }\n sh(\"systemctl\", [\"--user\", \"daemon-reload\"]);\n if (targets.includes(\"collector\")) {\n sh(\"systemctl\", [\"--user\", \"enable\", \"--now\", SYSTEMD_COLLECTOR_TIMER]);\n console.log(`agent-lens service: installed collector timer (${onCalendarHours(hours)})`);\n reportBakedEnv(paths);\n sh(\"systemctl\", [\"--user\", \"list-timers\", SYSTEMD_COLLECTOR_TIMER, \"--no-pager\"]);\n }\n if (targets.includes(\"server\")) {\n sh(\"systemctl\", [\"--user\", \"enable\", \"--now\", SYSTEMD_SERVER_SERVICE]);\n console.log(\"agent-lens service: installed server daemon\");\n reportBakedEnv({ ...paths, ...serverEnv() });\n sh(\"systemctl\", [\"--user\", \"status\", SYSTEMD_SERVER_SERVICE, \"--no-pager\"]);\n }\n // Run while logged out (best-effort) — needed for both the timer and the daemon.\n const user = currentUser();\n if (user && have(\"loginctl\") && !sh(\"loginctl\", [\"enable-linger\", user], true)) {\n console.log(`agent-lens: could not enable linger automatically; run: sudo loginctl enable-linger ${user}`);\n }\n}\n\nfunction linuxUninstall(targets: ServiceTarget[]): void {\n if (!have(\"systemctl\")) return;\n const dir = systemdUnitDir();\n if (targets.includes(\"collector\")) {\n sh(\"systemctl\", [\"--user\", \"disable\", \"--now\", SYSTEMD_COLLECTOR_TIMER], true);\n for (const u of [SYSTEMD_COLLECTOR_TIMER, SYSTEMD_COLLECTOR_SERVICE]) rmSync(join(dir, u), { force: true });\n sh(\"systemctl\", [\"--user\", \"reset-failed\", SYSTEMD_COLLECTOR_SERVICE], true);\n }\n if (targets.includes(\"server\")) {\n sh(\"systemctl\", [\"--user\", \"disable\", \"--now\", SYSTEMD_SERVER_SERVICE], true);\n rmSync(join(dir, SYSTEMD_SERVER_SERVICE), { force: true });\n sh(\"systemctl\", [\"--user\", \"reset-failed\", SYSTEMD_SERVER_SERVICE], true);\n }\n sh(\"systemctl\", [\"--user\", \"daemon-reload\"]);\n console.log(\"agent-lens service: uninstalled (archive untouched)\");\n}\n\nfunction linuxStatus(targets: ServiceTarget[]): void {\n if (!have(\"systemctl\")) return;\n if (targets.includes(\"collector\")) sh(\"systemctl\", [\"--user\", \"list-timers\", SYSTEMD_COLLECTOR_TIMER, \"--no-pager\"]);\n if (targets.includes(\"server\")) sh(\"systemctl\", [\"--user\", \"status\", SYSTEMD_SERVER_SERVICE, \"--no-pager\"]);\n}\n\n// ---- macOS (launchd) ----------------------------------------------------------------------------\n\nfunction launchdPlistPath(label: string): string {\n return join(homedir(), \"Library\", \"LaunchAgents\", `${label}.plist`);\n}\n\nfunction macBootstrap(plist: string): void {\n const uid = typeof process.getuid === \"function\" ? process.getuid() : 0;\n // Prefer the modern bootstrap API; fall back to legacy load -w.\n if (!sh(\"launchctl\", [\"bootstrap\", `gui/${uid}`, plist], true)) sh(\"launchctl\", [\"load\", \"-w\", plist]);\n}\n\nfunction macBootout(label: string, plist: string): void {\n const uid = typeof process.getuid === \"function\" ? process.getuid() : 0;\n if (!sh(\"launchctl\", [\"bootout\", `gui/${uid}/${label}`], true)) sh(\"launchctl\", [\"unload\", \"-w\", plist], true);\n rmSync(plist, { force: true });\n}\n\nfunction macInstall(targets: ServiceTarget[], node: string, cli: string, hours: number[]): void {\n const agents = join(homedir(), \"Library\", \"LaunchAgents\");\n mkdirSync(agents, { recursive: true });\n const dataDir = resolveDataDir(findRepoRoot());\n const paths = pathEnv();\n if (targets.includes(\"collector\")) {\n const plist = launchdPlistPath(LAUNCHD_COLLECTOR_LABEL);\n writeFileSync(plist, launchdCollectorPlist(node, cli, hours, join(dataDir, \"schedule.log\"), paths));\n macBootstrap(plist);\n console.log(`agent-lens service: installed collector agent ${LAUNCHD_COLLECTOR_LABEL} (hours ${hours.join(\",\")})`);\n reportBakedEnv(paths);\n }\n if (targets.includes(\"server\")) {\n const plist = launchdPlistPath(LAUNCHD_SERVER_LABEL);\n const env = { ...paths, ...serverEnv() };\n writeFileSync(plist, launchdServerPlist(node, cli, join(dataDir, \"server.log\"), env));\n macBootstrap(plist);\n console.log(`agent-lens service: installed server agent ${LAUNCHD_SERVER_LABEL}`);\n reportBakedEnv(env);\n }\n}\n\nfunction macUninstall(targets: ServiceTarget[]): void {\n if (targets.includes(\"collector\")) macBootout(LAUNCHD_COLLECTOR_LABEL, launchdPlistPath(LAUNCHD_COLLECTOR_LABEL));\n if (targets.includes(\"server\")) macBootout(LAUNCHD_SERVER_LABEL, launchdPlistPath(LAUNCHD_SERVER_LABEL));\n console.log(\"agent-lens service: uninstalled (archive untouched)\");\n}\n\nfunction macStatus(targets: ServiceTarget[]): void {\n const uid = typeof process.getuid === \"function\" ? process.getuid() : 0;\n if (targets.includes(\"collector\")) sh(\"launchctl\", [\"print\", `gui/${uid}/${LAUNCHD_COLLECTOR_LABEL}`]);\n if (targets.includes(\"server\")) sh(\"launchctl\", [\"print\", `gui/${uid}/${LAUNCHD_SERVER_LABEL}`]);\n}\n\n// ---- Windows (schtasks) -------------------------------------------------------------------------\n\nfunction winCollectorTaskName(hour: number): string {\n return `${WINDOWS_COLLECTOR_PREFIX}_${String(hour).padStart(2, \"0\")}`;\n}\n\nfunction windowsInstall(targets: ServiceTarget[], node: string, cli: string, hours: number[]): void {\n // Task Scheduler has no environment field — /TR is a command line and nothing else — so the path\n // env can't ride along the way it does in a systemd unit or a launchd plist. Say so rather than\n // installing a task that quietly reads a different data dir than the shell that installed it.\n const unbakeable = Object.entries({ ...pathEnv(), ...serverEnv() });\n if (unbakeable.length) {\n console.log(\"agent-lens service: Task Scheduler cannot carry environment variables. Set these\");\n console.log(\"agent-lens service: for your user account so the task sees them, then reinstall:\");\n for (const [k, v] of unbakeable) console.log(`agent-lens service: setx ${k} \"${v}\"`);\n }\n if (targets.includes(\"collector\")) {\n const tr = `\"${node}\" \"${cli}\" collect --then-ingest`;\n for (const h of hours) {\n sh(\"schtasks\", [\"/Create\", \"/F\", \"/TN\", winCollectorTaskName(h), \"/TR\", tr, \"/SC\", \"DAILY\", \"/ST\", `${String(h).padStart(2, \"0\")}:00`]);\n }\n console.log(`agent-lens service: installed ${hours.length} collector task(s) (hours ${hours.join(\",\")})`);\n }\n if (targets.includes(\"server\")) {\n // ONLOGON runs in the user session, so the per-user data dir (%LOCALAPPDATA%) resolves.\n // Note: schtasks has no crash-restart (systemd/launchd do); a crashed server returns on next logon.\n const tr = `\"${node}\" \"${cli}\" serve`;\n sh(\"schtasks\", [\"/Create\", \"/F\", \"/TN\", WINDOWS_SERVER_TASK, \"/TR\", tr, \"/SC\", \"ONLOGON\", \"/RL\", \"LIMITED\"]);\n console.log(\"agent-lens service: installed server task (starts at logon)\");\n }\n}\n\nfunction windowsUninstall(targets: ServiceTarget[]): void {\n if (targets.includes(\"collector\")) {\n // Delete every task under our prefix (we don't need to know the original hours).\n for (let h = 0; h < 24; h++) sh(\"schtasks\", [\"/Delete\", \"/F\", \"/TN\", winCollectorTaskName(h)], true);\n }\n if (targets.includes(\"server\")) sh(\"schtasks\", [\"/Delete\", \"/F\", \"/TN\", WINDOWS_SERVER_TASK], true);\n console.log(\"agent-lens service: uninstalled (archive untouched)\");\n}\n\nfunction windowsStatus(targets: ServiceTarget[]): void {\n // Status must print the query output on success, so run loud (quiet=false) like linux/macStatus — the\n // `true` here was a copy-paste from the uninstall path and silently swallowed the schtasks listing.\n if (targets.includes(\"collector\")) {\n sh(\"schtasks\", [\"/Query\", \"/FO\", \"LIST\", \"/TN\", WINDOWS_COLLECTOR_PREFIX], false) ||\n console.log(\"agent-lens service: no collector tasks found under \" + WINDOWS_COLLECTOR_PREFIX);\n }\n if (targets.includes(\"server\")) {\n sh(\"schtasks\", [\"/Query\", \"/FO\", \"LIST\", \"/TN\", WINDOWS_SERVER_TASK], false) ||\n console.log(\"agent-lens service: no server task found (\" + WINDOWS_SERVER_TASK + \")\");\n }\n}\n\n// ---- dispatch -----------------------------------------------------------------------------------\n\nexport function runService(action: string, params: ServiceParams): void {\n const node = params.node ?? process.execPath;\n const cli = params.cliEntry;\n const hours = params.hours ?? DEFAULT_HOURS;\n const targets = params.targets;\n const plat = platform();\n const impl =\n plat === \"linux\"\n ? {\n install: () => linuxInstall(targets, node, cli, hours),\n uninstall: () => linuxUninstall(targets),\n status: () => linuxStatus(targets),\n }\n : plat === \"darwin\"\n ? {\n install: () => macInstall(targets, node, cli, hours),\n uninstall: () => macUninstall(targets),\n status: () => macStatus(targets),\n }\n : plat === \"win32\"\n ? {\n install: () => windowsInstall(targets, node, cli, hours),\n uninstall: () => windowsUninstall(targets),\n status: () => windowsStatus(targets),\n }\n : null;\n if (!impl) {\n console.error(`agent-lens service: unsupported platform '${plat}'`);\n process.exitCode = 1;\n return;\n }\n if (action === \"install\") impl.install();\n else if (action === \"uninstall\") impl.uninstall();\n else if (action === \"status\") impl.status();\n else {\n console.error(`agent-lens service: unknown action '${action}' (use: install | uninstall | status)`);\n process.exitCode = 1;\n }\n}\n","/**\n * Agent Lens — Stage 2 ingest, as importable functions (ADR-001, ADR-003).\n *\n * `runIngest` and `runMetrics` are the library entrypoints so the unified `agent-lens` CLI can\n * bundle and call them in-process; the thin bins in index.ts / metrics-cli.ts just forward argv.\n * The DB-writing engine lives in pipeline.ts (unit-tested in-process).\n */\nimport { readFileSync, statSync, mkdirSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n costForUsage,\n installPricing,\n loadExcludes,\n loadSources,\n rateForModel,\n resolveArchiveDir,\n resolveDbPath,\n SCHEMA_VERSION,\n SYNTHETIC_MODEL,\n type SourceAdapter,\n} from \"@agent-lens/core\";\nimport { openDb, openRaw, readSchemaVersion, resetSchema, type DB } from \"./db.js\";\nimport { ClaudeCodeAdapter } from \"./adapters/claude-code.js\";\nimport { classify, CLASSIFIER_VERSION } from \"./classify.js\";\nimport { detect, DETECTOR_VERSION } from \"./detect.js\";\nimport { classifyErrors } from \"./errors.js\";\nimport { deriveFileChanges, FILECHANGES_VERSION } from \"./filechanges.js\";\nimport { canonicalizeProjects } from \"./canonicalize.js\";\nimport { ingestFile, newStats, prepareStatements, pruneExcluded, rebuildDerived, type IngestStats } from \"./pipeline.js\";\nimport { ingestWorkflowResults, newWorkflowStats } from \"./workflows.js\";\nimport { ingestSubagentMeta, newMetaStats } from \"./meta.js\";\nimport { ingestToolResults, newToolResultStats } from \"./toolResults.js\";\nimport { parseExcludes, isExcludedArchivePath } from \"./redact.js\";\nimport { sha256, sha256File, streamLines, STREAM_THRESHOLD } from \"./fileread.js\";\n\nfunction parseIngestArgs(argv: string[]) {\n const a = { full: false, db: \"\", archive: \"\" };\n for (let i = 0; i < argv.length; i++) {\n if (argv[i] === \"--full\") a.full = true;\n else if (argv[i] === \"--db\") a.db = argv[++i] ?? \"\";\n else if (argv[i] === \"--archive\") a.archive = argv[++i] ?? \"\";\n }\n return a;\n}\n\n/** Skip-state for one archive file, as recorded by the previous run. */\ntype FileState = { size: number; mtime_ms: number; sha256: string; events_ingested: number } | undefined;\n\n/** What to do with one archive file this run.\n * - `skip-stat`: unchanged size+mtime means unchanged content — skip without ever reading or hashing\n * the file. This is what restores true incrementality across the mirror + every .versions snapshot\n * (ADR-010, impact 1).\n * - `skip-hash`: content is unchanged though mtime moved (e.g. an rsync --append-verify re-stat) —\n * skip the ingest but refresh size/mtime so the next run short-circuits on stat alone.\n * - `ingest`: genuinely new/changed. `buf` is the whole file for the common case, null for a large\n * file that must be streamed instead. */\ntype FileDecision =\n | { action: \"skip-stat\" }\n | { action: \"skip-hash\"; hash: string }\n | { action: \"ingest\"; hash: string; buf: Buffer | null };\n\nfunction shouldReingest(path: string, size: number, mtimeMs: number, prev: FileState): FileDecision {\n if (prev && prev.size === size && prev.mtime_ms === mtimeMs) return { action: \"skip-stat\" };\n\n // Size/mtime moved: read + hash to decide. Whole-file for the common case; stream large files.\n const small = size <= STREAM_THRESHOLD;\n const buf = small ? readFileSync(path) : null;\n const hash = small ? sha256(buf!) : sha256File(path);\n\n if (prev && prev.sha256 === hash) return { action: \"skip-hash\", hash };\n return { action: \"ingest\", hash, buf };\n}\n\n/** Everything the end-of-run report counts, gathered from the callers that produced it. */\ninterface IngestReport {\n stats: IngestStats;\n pruned: number;\n classified: { count: number };\n detected: { count: number };\n fileChanges: { count: number };\n wfStats: { upserted: number; skipped: number; malformed: number };\n metaStats: { upserted: number; skipped: number; malformed: number };\n trStats: { upserted: number; skipped: number; malformed: number };\n}\n\n/** Tally the final table counts, close the DB, and print the one-block run summary. */\nfunction printIngestReport(db: DB, dbPath: string, r: IngestReport): void {\n const count = (sql: string) => (db.prepare(sql).get() as any).n as number;\n const sessions = count(\"SELECT COUNT(*) n FROM sessions\");\n const turns = count(\"SELECT COUNT(*) n FROM turns\");\n const events = count(\"SELECT COUNT(*) n FROM events\");\n const tools = count(\"SELECT COUNT(*) n FROM tool_calls\");\n const usageRows = db\n .prepare(\"SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage\")\n .all() as any[];\n let cost = 0;\n let totalTokens = 0;\n // A model with no rate contributes $0 silently (ADR-028) — that is how a whole week of sessions\n // once read as free. Tally it here, where a human is watching the run, instead of letting the\n // estimate quietly understate itself.\n const unpriced = new Map<string, number>();\n for (const u of usageRows) {\n cost += costForUsage(u.model, u);\n totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;\n if (u.model && u.model !== SYNTHETIC_MODEL && !rateForModel(u.model)) {\n unpriced.set(u.model, (unpriced.get(u.model) ?? 0) + 1);\n }\n }\n const unpricedLine = unpriced.size\n ? `\\n unpriced=${unpriced.size} model(s): ` +\n [...unpriced].map(([m, n]) => `${m} (${n} records)`).join(\", \") +\n ` — cost above excludes them; add rates under \"pricing\" in agent-lens.config.json`\n : \"\";\n\n const wfRuns = count(\"SELECT COUNT(*) n FROM workflow_results\");\n const metaRows = count(\"SELECT COUNT(*) n FROM session_meta\");\n const trRows = count(\"SELECT COUNT(*) n FROM tool_results\");\n const sevCounts = db\n .prepare(\"SELECT severity, COUNT(*) n FROM findings GROUP BY severity\")\n .all() as Array<{ severity: string; n: number }>;\n const sevMap = Object.fromEntries(sevCounts.map((s) => [s.severity, s.n]));\n const sevLine = [\"critical\", \"high\", \"medium\", \"low\", \"info\"].map((s) => `${s}=${sevMap[s] ?? 0}`).join(\" \");\n\n db.close();\n console.log(\n `agent-lens-ingest: files=${r.stats.files} skipped=${r.stats.skipped} new_events=${r.stats.newEvents} malformed=${r.stats.malformed}${r.pruned ? ` excluded_pruned=${r.pruned}` : \"\"}\\n` +\n ` sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${r.classified.count}\\n` +\n ` workflow_results=${wfRuns} (sidecar upserted=${r.wfStats.upserted} skipped=${r.wfStats.skipped} malformed=${r.wfStats.malformed})\\n` +\n ` session_meta=${metaRows} (upserted=${r.metaStats.upserted} skipped=${r.metaStats.skipped} malformed=${r.metaStats.malformed})\\n` +\n ` tool_results=${trRows} (upserted=${r.trStats.upserted} skipped=${r.trStats.skipped} malformed=${r.trStats.malformed})\\n` +\n ` findings=${r.detected.count} (${sevLine})\\n` +\n ` file_changes=${r.fileChanges.count}\\n` +\n ` tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}` +\n unpricedLine,\n );\n}\n\n/** Stage 2: (re)build the derived SQLite store from the local archive. Idempotent. */\n// This module is the package entry (`exports: ./dist/run.js`), so re-export the stamped engine\n// versions here: the server's /api/about reports what a re-run would produce against what is\n// actually stamped in the stored rows (ADR-027). Importing them from ./detect.js or ./classify.js\n// across the package boundary would reach past the public surface.\nexport { CLASSIFIER_VERSION } from \"./classify.js\";\nexport { DETECTOR_VERSION } from \"./detect.js\";\n\nexport function runIngest(argv: string[] = process.argv.slice(2)): void {\n const args = parseIngestArgs(argv);\n // --archive stays as an explicit per-run override (ingesting a copied archive); there is no\n // env/config equivalent — the location is fixed so collect and ingest can never disagree (ADR-021).\n const archiveRoot = args.archive || resolveArchiveDir();\n const { path: dbPath } = resolveDbPath(args.db);\n // Cost in the end-of-run report is derived through the effective table (ADR-028), so install any\n // config overrides before the report — and warn here about malformed ones, where a human is looking.\n installPricing();\n\n if (!existsSync(archiveRoot)) {\n console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);\n process.exit(1);\n }\n mkdirSync(dirname(dbPath), { recursive: true });\n\n // --full: open WITHOUT applying schema (the on-disk schema may be a stale version), then\n // drop+recreate from the archive (source of truth). This is also the migration path — a\n // SCHEMA_VERSION bump's new columns take effect here without a separate migration step.\n const db = args.full ? openRaw(dbPath) : openDb(dbPath);\n if (args.full) {\n resetSchema(db);\n } else {\n // Schema guard: an incremental ingest only runs CREATE IF NOT EXISTS — it adds new tables but\n // cannot migrate altered columns on existing ones, and never advances the stamp (applySchema is\n // stamp-if-absent). So a DB stamped by an older build is stale: refuse rather than silently ingest\n // into a half-migrated schema. `ingest --full` (drop+recreate) is the migration path.\n const dbVer = readSchemaVersion(db);\n if (dbVer != null && dbVer !== SCHEMA_VERSION) {\n console.error(\n `agent-lens-ingest: schema mismatch — DB is v${dbVer}, this build expects v${SCHEMA_VERSION}. ` +\n `Run 'agent-lens ingest --full' to rebuild.`,\n );\n db.close();\n process.exit(1);\n }\n }\n\n // Adapter registry keyed by agent type; configured sources resolved by the shared resolver.\n const adapterList: SourceAdapter[] = [new ClaudeCodeAdapter()];\n const adapterById = new Map(adapterList.map((a) => [a.agentId, a]));\n const sources = loadSources();\n const now = new Date().toISOString();\n\n // Excluded projects (config `exclude` + AGENT_LENS_EXCLUDE): drop any already-ingested ones now\n // (incremental; --full already reset the DB), then filter them out of discovery below.\n const excludePaths = loadExcludes();\n const excludedDirs = parseExcludes(excludePaths.join(\",\"));\n const pruned = pruneExcluded(db, excludePaths);\n\n const stmts = prepareStatements(db);\n const stats = newStats();\n const wfStats = newWorkflowStats();\n const metaStats = newMetaStats();\n const trStats = newToolResultStats();\n // Sessions touched this run; drives the incremental derived rebuild (ADR-010, impacts 2/3).\n const dirty = new Set<string>();\n\n for (const source of sources) {\n const adapter = adapterById.get(source.agent);\n if (!adapter) {\n console.warn(`agent-lens-ingest: no adapter for agent '${source.agent}' (source '${source.label}') — skipping`);\n continue;\n }\n stmts.insAgent.run(adapter.agentId, adapter.agentName);\n stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });\n\n let files = adapter.discover(join(archiveRoot, source.label), source.label);\n // Drop excluded projects (matches /projects/<encodedDir>/ so nested subagent files go too).\n if (excludedDirs.length) files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));\n // Mirror before versions so the mirror copy wins canonical fields (ON CONFLICT DO NOTHING).\n files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));\n\n for (const file of files) {\n stats.files++;\n const st = statSync(file.path);\n const mtimeMs = Math.trunc(st.mtimeMs);\n const prev = args.full ? undefined : (stmts.getState.get(file.path) as FileState);\n\n const decision = shouldReingest(file.path, st.size, mtimeMs, prev);\n if (decision.action === \"skip-stat\") {\n stats.skipped++;\n continue;\n }\n if (decision.action === \"skip-hash\") {\n stmts.setState.run({\n file_path: file.path,\n size: st.size,\n mtime_ms: mtimeMs,\n sha256: decision.hash,\n events_ingested: prev!.events_ingested,\n ingested_at: now,\n });\n stats.skipped++;\n continue;\n }\n\n const lines = decision.buf ? decision.buf.toString(\"utf8\").split(\"\\n\") : streamLines(file.path);\n ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: decision.hash }, now, stats);\n dirty.add(file.sessionId);\n }\n\n // Workflow-tool result sidecars (wf_<id>.json) — the authoritative record of how each run\n // finished; not *.jsonl, so the transcript walk above never sees them. Own table, own skip-state.\n ingestWorkflowResults(db, join(archiveRoot, source.label), source.label, excludedDirs, now, wfStats, args.full);\n\n // Per-subagent metadata (subagents/agent-<id>.meta.json) — the authoritative agentType/description/\n // spawnDepth; joined onto sessions at read time. Own table (session_meta), own skip-state.\n ingestSubagentMeta(db, join(archiveRoot, source.label), source.label, excludedDirs, now, metaStats, args.full);\n\n // Spilled full tool outputs (tool-results/<name>.txt) — the un-truncated result the transcript only\n // summarized; joined onto tool_calls at read time via the summary's marker. Own table, own skip-state.\n ingestToolResults(db, join(archiveRoot, source.label), excludedDirs, now, trStats, args.full);\n }\n\n // Incremental derived rebuild over only the touched sessions (+ their linkage neighborhood); --full\n // rebuilds everything. classify reuses the expanded set rebuildDerived returns.\n const expanded = rebuildDerived(db, args.full ? null : dirty);\n\n // Canonical project roots (ADR-023): fold cwd-grained project rows into their git root (or\n // observed ancestor) BEFORE the passes below — classify/detect/file-changes all consume the\n // session's project path. Global + re-runnable (the table is a few dozen rows), so an existing\n // DB heals on its next ingest without --full.\n canonicalizeProjects(db);\n\n // Heuristic classification (ADR-004) over the now-stable derived tables. Deterministic +\n // re-runnable; also exposed standalone as `agent-lens metrics` (see runMetrics).\n const classified = classify(db, args.full ? null : expanded);\n\n // Security findings (ADR-017) over the same derived tool_calls. Deterministic + re-runnable; the\n // detector reuses the expanded dirty set and delete-then-inserts the touched sessions' findings.\n const detected = detect(db, args.full ? null : expanded);\n\n // Tool-error classification (error_type on errored tool_calls). Deterministic + re-runnable; reuses\n // the same expanded dirty set. Powers the sessions error-type filter + dashboard/detail breakdowns.\n classifyErrors(db, args.full ? null : expanded);\n\n // File-modification provenance (ADR-022) from Edit/Write/NotebookEdit inputs. Deterministic +\n // re-runnable; reuses the same expanded dirty set and delete-then-inserts per touched session.\n const fileChanges = deriveFileChanges(db, args.full ? null : expanded);\n\n printIngestReport(db, dbPath, { stats, pruned, classified, detected, fileChanges, wfStats, metaStats, trStats });\n}\n\n/** Standalone reclassification over an already-ingested DB (no archive re-read). */\nexport function runMetrics(argv: string[] = process.argv.slice(2)): void {\n let dbArg = \"\";\n for (let i = 0; i < argv.length; i++) if (argv[i] === \"--db\") dbArg = argv[++i] ?? \"\";\n const { path: dbPath } = resolveDbPath(dbArg);\n\n if (!existsSync(dbPath)) {\n console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);\n process.exit(1);\n }\n\n const db = openDb(dbPath);\n canonicalizeProjects(db); // ADR-023 — heal cwd-grained projects before the passes read them\n const r = classify(db);\n const d = detect(db);\n const e = classifyErrors(db);\n const fc = deriveFileChanges(db);\n db.close();\n console.log(\n `agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} ` +\n `findings=${d.count} detector_version=${DETECTOR_VERSION} errors_classified=${e.count} error_classifier_version=${e.version} ` +\n `file_changes=${fc.count} filechanges_version=${FILECHANGES_VERSION} db=${dbPath}`,\n );\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport { SCHEMA_SQL, SCHEMA_VERSION } from \"@agent-lens/core\";\n\nexport type DB = DatabaseSync;\n\n/** Open (creating if needed) the DB and set pragmas, WITHOUT applying the schema. */\nexport function openRaw(file: string): DB {\n const db = new DatabaseSync(file);\n db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(\"PRAGMA foreign_keys = ON\");\n return db;\n}\n\n/** The schema version stamped in an existing DB, or null if unstamped/brand-new. */\nexport function readSchemaVersion(db: DB): number | null {\n const row = db\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined;\n if (!row) return null;\n const n = Number(row.value);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Apply the current schema (idempotent CREATE IF NOT EXISTS) and stamp the version ONLY when absent.\n * Stamping if-absent (not overwrite) is deliberate: an incremental `ingest` runs CREATE IF NOT EXISTS,\n * which adds new *tables* but cannot migrate altered columns on existing ones — so overwriting the stamp\n * would let a stale DB claim the current version and lie. A stale DB keeps its old stamp; the caller\n * (`runIngest`) guards on it. `resetSchema` drops `meta`, so its applySchema re-stamps the current\n * version from scratch — the only path that advances the stamp.\n */\nfunction applySchema(db: DB): void {\n db.exec(SCHEMA_SQL);\n db.prepare(\n \"INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO NOTHING\",\n ).run(String(SCHEMA_VERSION));\n}\n\n/**\n * Open (creating if needed) the Agent Lens database and ensure the schema is applied.\n * NOTE: on a stale on-disk schema this only adds missing objects (CREATE IF NOT EXISTS) — it does\n * not migrate changed/added columns. A SCHEMA_VERSION bump is applied via `ingest --full`\n * (openRaw + resetSchema), the canonical rebuild path.\n */\nexport function openDb(file: string): DB {\n const db = openRaw(file);\n applySchema(db);\n return db;\n}\n\n/**\n * Drop and recreate every table from the current SCHEMA_SQL. Used by `ingest --full`: because the\n * archive is the source of truth and the DB is a derived projection, a full rebuild is also the\n * migration path — dropping the tables (vs. DELETE FROM) lets a SCHEMA_VERSION bump's new columns\n * take effect on an existing DB without a separate migration mechanism (ADR-001/009).\n */\nexport function resetSchema(db: DB): void {\n // Disable FK enforcement during teardown: the v3 schema has cross/self references\n // (sessions.parent_turn_id → turns, parent_session_id → sessions) that otherwise make DROP order\n // matter and trip \"FOREIGN KEY constraint failed\". applySchema re-enables FKs (PRAGMA in SCHEMA_SQL).\n // `finally` so a failed drop/apply can't leave the connection unguarded for the rest of the process.\n db.exec(\"PRAGMA foreign_keys = OFF\");\n try {\n db.exec(`\n DROP TRIGGER IF EXISTS events_au;\n DROP TRIGGER IF EXISTS events_ad;\n DROP TRIGGER IF EXISTS events_ai;\n DROP TABLE IF EXISTS events_fts;\n DROP TABLE IF EXISTS file_changes;\n DROP TABLE IF EXISTS findings;\n DROP TABLE IF EXISTS classifications;\n DROP TABLE IF EXISTS token_usage;\n DROP TABLE IF EXISTS tool_calls;\n DROP TABLE IF EXISTS skills;\n DROP TABLE IF EXISTS events;\n DROP TABLE IF EXISTS turns;\n DROP TABLE IF EXISTS sessions;\n DROP TABLE IF EXISTS projects;\n DROP TABLE IF EXISTS sources;\n DROP TABLE IF EXISTS agents;\n DROP TABLE IF EXISTS workflow_results;\n DROP TABLE IF EXISTS session_meta;\n DROP TABLE IF EXISTS tool_results;\n DROP TABLE IF EXISTS ingest_state;\n DROP TABLE IF EXISTS meta;\n `);\n applySchema(db);\n } finally {\n db.exec(\"PRAGMA foreign_keys = ON\");\n }\n}\n","import { readdirSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport type {\n ParsedLine,\n SourceAdapter,\n SourceFile,\n ToolCallRow,\n ToolResultPatch,\n} from \"@agent-lens/core\";\n\n/** Flatten a message `content` (string or block array) to searchable natural-language text. */\nfunction flattenText(content: unknown): string | null {\n if (content == null) return null;\n if (typeof content === \"string\") return content.trim() || null;\n if (!Array.isArray(content)) return null;\n const parts: string[] = [];\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, unknown>;\n if (b.type === \"text\" && typeof b.text === \"string\") parts.push(b.text);\n else if (b.type === \"thinking\" && typeof b.thinking === \"string\") parts.push(b.thinking);\n }\n const text = parts.join(\"\\n\").trim();\n return text || null;\n}\n\n/** Compact projection of a tool result for the transcript view. Preserves line structure — so console\n * output (build logs, git output, …) renders as lines instead of one run-on line — while staying\n * bounded: trailing per-line whitespace and runs of blank lines are trimmed, and the text is capped by\n * both line count and characters so a large result can't be dumped wholesale into the DB. Internal\n * spacing is kept intact so column-aligned output survives. The full untruncated output, when big\n * enough, is spilled by Claude Code to tool-results/<name>.txt and surfaced separately (full_result). */\nconst RESULT_SUMMARY_MAX_LINES = 40;\nconst RESULT_SUMMARY_MAX_CHARS = 2000;\nfunction summarizeResult(content: unknown): string | null {\n let s: string | null = null;\n if (typeof content === \"string\") s = content;\n else if (Array.isArray(content)) {\n const parts: string[] = [];\n for (const block of content) {\n const b = block as Record<string, unknown>;\n if (b && b.type === \"text\" && typeof b.text === \"string\") parts.push(b.text);\n }\n s = parts.join(\"\\n\");\n } else if (content != null) {\n s = JSON.stringify(content);\n }\n if (!s) return null;\n // Normalize newlines, drop trailing per-line whitespace, and collapse 3+ blank lines — but keep\n // internal spacing and single blank lines so the structure reads like the original console output.\n s = s.replace(/\\r\\n/g, \"\\n\").replace(/[ \\t]+\\n/g, \"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n if (!s) return null;\n let truncated = false;\n const lines = s.split(\"\\n\");\n if (lines.length > RESULT_SUMMARY_MAX_LINES) {\n s = lines.slice(0, RESULT_SUMMARY_MAX_LINES).join(\"\\n\");\n truncated = true;\n }\n if (s.length > RESULT_SUMMARY_MAX_CHARS) {\n s = s.slice(0, RESULT_SUMMARY_MAX_CHARS);\n const nl = s.lastIndexOf(\"\\n\");\n if (nl > RESULT_SUMMARY_MAX_CHARS - 200) s = s.slice(0, nl); // prefer a clean line boundary\n truncated = true;\n }\n s = s.trimEnd();\n return (truncated ? s + \"\\n… (truncated)\" : s) || null;\n}\n\nfunction asString(v: unknown): string | null {\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\n\nfunction asInt(v: unknown): number {\n return typeof v === \"number\" && Number.isFinite(v) ? Math.trunc(v) : 0;\n}\n\n/**\n * Derive the parent session id for a subagent transcript from its on-disk path. Claude Code nests\n * subagent transcripts under the spawning session's directory:\n * Task/Agent: <...>/projects/<enc>/<PARENT_SESSION_ID>/subagents/agent-<id>.jsonl\n * Workflow: <...>/projects/<enc>/<PARENT_SESSION_ID>/subagents/workflows/wf_<runId>/agent-<id>.jsonl\n * In both layouts the parent session id is the path segment immediately before `subagents/`. This is\n * the deterministic, redaction-surviving link (the Workflow tool emits no toolUseResult.agentId, so\n * the tool_calls.spawned_session_id path never fires for workflow fan-out). Returns null for a main\n * session (no `subagents/` segment).\n */\nexport function parentSessionFromPath(path: string): string | null {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"subagents\");\n return i > 0 ? parts[i - 1] : null;\n}\n\n/**\n * For a Workflow-tool subagent, the run id from its path. Workflow fan-out nests under\n * `…/subagents/workflows/<runId>/agent-<id>.jsonl`, so the run id is the segment after `workflows/`\n * (e.g. `wf_ab787f1c-ff7`). Returns null for Task/Agent subagents (no `workflows/` segment) and main\n * sessions. The same id appears in the launching Workflow tool_call's result (toolUseResult.runId),\n * which is how a run's agents group and link back to the turn that started them.\n */\nexport function workflowRunFromPath(path: string): string | null {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"workflows\");\n return i >= 0 && i + 1 < parts.length ? parts[i + 1] : null;\n}\n\nfunction recurseJsonl(dir: string, out: string[]): void {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n const p = join(dir, e.name);\n if (e.isDirectory()) recurseJsonl(p, out);\n else if (e.isFile() && e.name.endsWith(\".jsonl\")) out.push(p);\n }\n}\n\nexport class ClaudeCodeAdapter implements SourceAdapter {\n agentId = \"claude-code\";\n agentName = \"Claude Code CLI\";\n\n discover(sourceArchiveDir: string, sourceId: string): SourceFile[] {\n const files: SourceFile[] = [];\n const archiveRoot = sourceArchiveDir;\n\n const addFrom = (projectsDir: string, isVersion: boolean) => {\n const found: string[] = [];\n recurseJsonl(projectsDir, found);\n for (const path of found) {\n // <...>/projects/<encodedDir>/<sessionId>.jsonl\n const parts = path.split(\"/\");\n const sessionId = basename(path, \".jsonl\");\n const encodedDir = parts[parts.length - 2] ?? \"\";\n files.push({\n path,\n sessionId,\n encodedDir,\n isVersion,\n sourceId,\n parentSessionId: parentSessionFromPath(path),\n workflowRunId: workflowRunFromPath(path),\n });\n }\n };\n\n addFrom(join(archiveRoot, \"projects\"), false);\n\n // .versions/<ts>/projects/<encodedDir>/<sessionId>.jsonl\n const versionsRoot = join(archiveRoot, \".versions\");\n try {\n for (const ts of readdirSync(versionsRoot, { withFileTypes: true })) {\n if (ts.isDirectory()) addFrom(join(versionsRoot, ts.name, \"projects\"), true);\n }\n } catch {\n /* no versions yet */\n }\n return files;\n }\n\n parseLine(raw: unknown, file: SourceFile, seq: number): ParsedLine {\n if (!raw || typeof raw !== \"object\") return {};\n const r = raw as Record<string, any>;\n\n // Session-level metadata from the common envelope + pointer lines.\n const meta: ParsedLine[\"meta\"] = {};\n if (asString(r.cwd)) meta.cwd = r.cwd;\n if (asString(r.slug)) meta.slug = r.slug;\n if (asString(r.version)) meta.cli_version = r.version;\n if (asString(r.entrypoint)) meta.entrypoint = r.entrypoint;\n if (asString(r.gitBranch)) meta.git_branch = r.gitBranch;\n if (typeof r.isSidechain === \"boolean\") meta.is_sidechain = r.isSidechain;\n if (r.type === \"ai-title\" && asString(r.aiTitle)) meta.ai_title = r.aiTitle;\n\n // Lines without a uuid (pointer/meta lines) contribute metadata only.\n if (!asString(r.uuid)) return { meta };\n\n const message = (r.message ?? {}) as Record<string, any>;\n const text = flattenText(message.content);\n\n const result: ParsedLine = {\n meta,\n event: {\n uuid: r.uuid,\n session_id: file.sessionId,\n turn_id: null,\n parent_uuid: asString(r.parentUuid),\n seq,\n type: asString(r.type) ?? \"unknown\",\n role: asString(message.role),\n timestamp: asString(r.timestamp),\n model: asString(message.model),\n is_sidechain: r.isSidechain ? 1 : 0,\n is_meta: r.isMeta ? 1 : 0,\n text,\n raw_json: JSON.stringify(raw),\n source_file: file.path,\n },\n };\n\n // Token usage at the assistant-message grain.\n const usage = message.usage as Record<string, any> | undefined;\n if (usage) {\n result.tokenUsage = {\n event_uuid: r.uuid,\n session_id: file.sessionId,\n turn_id: null,\n message_id: asString(message.id),\n model: asString(message.model),\n input_tokens: asInt(usage.input_tokens),\n output_tokens: asInt(usage.output_tokens),\n cache_creation_input_tokens: asInt(usage.cache_creation_input_tokens),\n cache_read_input_tokens: asInt(usage.cache_read_input_tokens),\n service_tier: asString(usage.service_tier),\n };\n }\n\n // Tool calls (tool_use blocks) and tool results (tool_result blocks + toolUseResult).\n const content = message.content;\n if (Array.isArray(content)) {\n const toolCalls: ToolCallRow[] = [];\n const toolResults: ToolResultPatch[] = [];\n const tur = r.toolUseResult as Record<string, any> | undefined;\n\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, any>;\n\n if (b.type === \"tool_use\" && asString(b.id)) {\n const name = asString(b.name) ?? \"unknown\";\n const input = b.input ?? {};\n toolCalls.push({\n id: b.id,\n event_uuid: r.uuid,\n session_id: file.sessionId,\n turn_id: null,\n tool_name: name,\n caller: asString(b.caller),\n skill_name: name === \"Skill\" ? asString(input.skill) ?? asString(input.command) : null,\n skill_id: null, // content-addressed skill version; linked in rebuildDerived from the injected body\n agent_type:\n name === \"Task\" || name === \"Agent\" ? asString(input.subagent_type) : null,\n spawned_session_id: null, // patched in once the tool_result carries toolUseResult.agentId\n workflow_run_id: null, // patched in from the Workflow tool_result's runId\n workflow_name: name === \"Workflow\" ? asString(input.name) : null, // refined from the result\n resolved_model: null,\n status: null,\n total_duration_ms: null,\n total_tokens: null,\n total_tool_use_count: null,\n input_json: JSON.stringify(input),\n result_summary: null,\n });\n } else if (b.type === \"tool_result\" && asString(b.tool_use_id)) {\n // AskUserQuestion's result is structured (which option the user picked per question, plus any\n // free-text notes) and lives on toolUseResult, not in the block content. Capture it verbatim\n // as JSON so the UI can render the Q&A with the selection marked — the generic 280-char prose\n // summary would truncate multi-question prompts and drop the notes entirely. The questions +\n // options themselves are already in the tool_use input_json, so only answers/annotations are\n // stored here.\n const isAskUser = tur && tur.questions && tur.answers && typeof tur.answers === \"object\";\n toolResults.push({\n tool_use_id: b.tool_use_id,\n result_summary: isAskUser\n ? JSON.stringify({ answers: tur!.answers, annotations: tur!.annotations ?? {} })\n : summarizeResult(b.content),\n // Tool failure is signalled by `is_error` on the tool_result block itself — NOT by\n // toolUseResult.status (whose values are task/lifecycle states like completed/pending/\n // async_launched and never \"error\"). Map is_error → status=\"error\" so the error roll-ups,\n // the transcript `tool-err` styling, and detect.ts's failed-attempt de-escalation all fire.\n // Non-error results fall back to the toolUseResult lifecycle status when present.\n status: b.is_error === true ? \"error\" : tur ? asString(tur.status) : null,\n ...(tur\n ? {\n agent_type: asString(tur.agentType),\n // Deterministic link: a Task/Agent subagent's transcript is keyed off its agentId\n // ('agent-'||agentId is the session id assigned by discover()'s filename stem).\n spawned_session_id: asString(tur.agentId) ? `agent-${tur.agentId}` : null,\n // Workflow fan-out: the result carries the run id (wf_<id>) + workflow name; this is\n // how a run's subagents (nested under subagents/workflows/<runId>/) link to this turn.\n workflow_run_id: asString(tur.runId),\n workflow_name: asString(tur.workflowName),\n resolved_model: asString(tur.resolvedModel),\n total_duration_ms: typeof tur.totalDurationMs === \"number\" ? tur.totalDurationMs : null,\n total_tokens: typeof tur.totalTokens === \"number\" ? tur.totalTokens : null,\n total_tool_use_count:\n typeof tur.totalToolUseCount === \"number\" ? tur.totalToolUseCount : null,\n }\n : {}),\n });\n }\n }\n if (toolCalls.length) result.toolCalls = toolCalls;\n if (toolResults.length) result.toolResults = toolResults;\n }\n\n return result;\n }\n}\n","/**\n * Agent Lens — incremental \"dirty set\" temp table.\n *\n * The derived passes (classify / detect / errors / prune / linkage) each scope their work to the\n * sessions touched this run by materializing the id set into a TEMP table and joining against it\n * (ADR-010). The DROP → CREATE TEMP → batched INSERT prologue lives here once, rather than at each\n * of the 5 call sites. `name` is an internal constant table name (`_dirty`, `_dirty_sec`, …), never\n * user input — safe to interpolate. Callers drop the table themselves when the pass is done.\n */\nimport { transaction } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\n\n/** (Re)create a single-column TEMP id table `name` and populate it from `ids`, in one transaction. */\nexport function createDirtySet(db: DB, name: string, ids: Iterable<string>): void {\n db.exec(`DROP TABLE IF EXISTS ${name}`);\n db.exec(`CREATE TEMP TABLE ${name} (id TEXT PRIMARY KEY)`);\n const ins = db.prepare(`INSERT OR IGNORE INTO ${name} (id) VALUES (?)`);\n transaction(db, (xs: Iterable<string>) => {\n for (const id of xs) ins.run(id);\n })(ids);\n}\n","/**\n * Reading back JSON that ingest itself wrote (`tool_calls.input_json`). A parse failure here is not a\n * malformed-input case — the transcript reader already counts those — it means a row in our own DB is\n * corrupt or truncated, which the derivation passes used to swallow silently: they returned \"no data\"\n * and the affected tool call quietly stopped being classified or scanned for findings.\n *\n * Swallowing is still right (one bad row must not abort a rescan of thousands), but it is now\n * counted, and the first failure per site is reported so a corrupt DB is visible rather than showing\n * up as findings that mysteriously don't fire.\n */\nlet malformed = 0;\nconst reported = new Set<string>();\n\n/** Parse a JSON column, or null. `where` names the call site in the one-time warning. */\nexport function parseStored<T = unknown>(json: string | null | undefined, where: string): T | null {\n if (!json) return null;\n try {\n return JSON.parse(json) as T;\n } catch {\n malformed++;\n if (!reported.has(where)) {\n reported.add(where);\n console.warn(`agent-lens: malformed stored JSON in ${where} — the affected rows are skipped; re-ingest with --full to rebuild`);\n }\n return null;\n }\n}\n\n/** How many stored-JSON parses have failed in this process. Zero on a healthy DB. */\nexport function malformedStoredJson(): number {\n return malformed;\n}\n\n/** Test helper: forget the counter and the one-time warnings. */\nexport function resetMalformedStoredJson(): void {\n malformed = 0;\n reported.clear();\n}\n","/**\n * Agent Lens — heuristic classification (ADR-004). No AI: category + complexity are derived\n * deterministically from signals already in the DB (tool mix, LoC from Edit/Write inputs,\n * turns, tokens, duration, subagent fan-out, prompt keywords). Re-runnable and idempotent —\n * the same DB always yields identical rows. Every input and sub-score is recorded in\n * `signals_json` so a result can be explained and the bands retuned without re-deriving meaning.\n *\n * Scope v1 = sessions (both main and sidechain). The PK (scope, target_id) leaves turn-level\n * for later. `classifier_version` lets a future (e.g. local-LLM) classifier supersede these rows.\n */\nimport type { DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\nimport { transaction } from \"@agent-lens/core\";\nimport { parseStored } from \"./storedjson.js\";\n\n// v2 (ADR-004): realistic complexity ceilings so real (long, substantial) main sessions spread\n// across bands instead of pegging in \"large\"; subagent sessions categorized by their spawner role\n// rather than keyword heuristics on a read-only exploration transcript.\n// v3: keyword scoring matches whole words instead of substrings — \"review\" no longer\n// fires inside \"preview\", nor \"fix\" inside \"prefix\" — so stored categories from v2 are superseded.\nexport const CLASSIFIER_VERSION = 3;\n\nexport type Category = \"feature\" | \"bugfix\" | \"refactor\" | \"docs\" | \"ops\" | \"review\" | \"chore\";\n\n/** Fixed order = deterministic tie-break when scores are equal. */\nconst CATEGORIES: Category[] = [\"bugfix\", \"refactor\", \"docs\", \"ops\", \"review\", \"feature\", \"chore\"];\n\n// Cutoffs tuned to the real main-session score distribution (v2 ceilings) so the five bands\n// partition main work meaningfully (~9/19/32/32/8%). Subagents score low and land \"trivial\".\nconst BANDS: Array<{ max: number; band: string }> = [\n { max: 22, band: \"trivial\" },\n { max: 40, band: \"small\" },\n { max: 55, band: \"medium\" },\n { max: 68, band: \"large\" },\n { max: Infinity, band: \"xl\" },\n];\n\n/**\n * Subagent roles whose work is inherently investigative/planning ⇒ \"review\". A general-purpose (or\n * unknown) subagent does varied work, so fall back to the heuristic on its own transcript.\n */\nconst REVIEW_SUBAGENT_ROLES = new Set([\"Explore\", \"Plan\", \"code-reviewer\", \"claude-code-guide\"]);\nfunction categoryForSubagentRole(role: string | undefined): Category | null {\n if (!role) return null;\n return REVIEW_SUBAGENT_ROLES.has(role) ? \"review\" : null;\n}\n\n/** Number of lines in a string (0 for empty/undefined). Trailing newline doesn't add a line. */\nfunction countLines(s: unknown): number {\n if (typeof s !== \"string\" || s.length === 0) return 0;\n const trimmed = s.endsWith(\"\\n\") ? s.slice(0, -1) : s;\n return trimmed.length === 0 ? 0 : trimmed.split(\"\\n\").length;\n}\n\nconst DOC_EXT = /\\.(md|markdown|rst|txt|adoc)$/i;\nconst OPS_FILE = /(^|\\/)(dockerfile|docker-compose|\\.github\\/|\\.gitlab-ci|makefile)|\\.(ya?ml|tf|sh|toml|ini|cfg|service|timer)$/i;\n\n/** LoC contribution of a single Edit/Write tool call, parsed from its verbatim input_json. */\nexport function locDelta(toolName: string, inputJson: string | null): { added: number; removed: number; file: string | null } {\n if (!inputJson) return { added: 0, removed: 0, file: null };\n const input = parseStored<Record<string, unknown>>(inputJson, \"classify: tool_calls.input_json\");\n if (!input) return { added: 0, removed: 0, file: null };\n const file = typeof input?.file_path === \"string\" ? input.file_path : null;\n if (toolName === \"Write\") return { added: countLines(input?.content), removed: 0, file };\n if (toolName === \"Edit\") return { added: countLines(input?.new_string), removed: countLines(input?.old_string), file };\n return { added: 0, removed: 0, file };\n}\n\nfunction band(score: number): string {\n for (const b of BANDS) if (score < b.max) return b.band;\n return \"xl\";\n}\n\nconst clamp01 = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x);\n\ninterface Signals {\n toolCounts: Record<string, number>;\n skills: Record<string, number>;\n loc: { added: number; removed: number; net: number; churn: number; files: number };\n turn_count: number;\n event_count: number;\n work_tokens: number; // input + output + cache_creation (cache-read excluded — replay ≠ work)\n cache_read_tokens: number;\n duration_ms: number;\n subagent_count: number;\n is_sidechain: number;\n}\n\n/** Compiled `\\b<phrase>\\b` matchers, keyed by phrase. The keyword lists are fixed and `hits` runs once\n * per category per session, so compiling each pattern once matters at corpus scale. */\nconst WORD_RE = new Map<string, RegExp>();\nfunction wordRe(phrase: string): RegExp {\n let re = WORD_RE.get(phrase);\n if (!re) {\n // Escape regex metacharacters — phrases include \"ci/cd\" and could later include \"c++\".\n const escaped = phrase.trim().replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n re = new RegExp(`\\\\b${escaped}\\\\b`, \"g\");\n WORD_RE.set(phrase, re);\n }\n return re;\n}\n\n/**\n * Count whole-word occurrences of any phrase in `words` within `text`.\n *\n * This used to scan with `indexOf`, which is substring matching: \"review\" hit inside \"preview\",\n * \"fix\" inside \"prefix\", \"add\" inside \"address\". Four keywords compensated by carrying a trailing\n * space in the list (`\"add \"`, `\"build \"`, `\"create \"`, `\"ci \"`), which only worked when the next\n * character happened to be a space — a line-ending \"add\", or \"add.\" mid-sentence, still missed.\n * Real word boundaries do the job properly, so those four are now written bare.\n */\nfunction hits(text: string, words: string[]): number {\n let n = 0;\n for (const w of words) {\n const re = wordRe(w);\n re.lastIndex = 0; // shared compiled regex: /g keeps state between calls\n while (re.exec(text) !== null) n++;\n }\n return n;\n}\n\nfunction scoreCategories(s: Signals, text: string, files: string[]): Record<Category, number> {\n const t = text.toLowerCase();\n const editN = s.toolCounts[\"Edit\"] ?? 0;\n const writeN = s.toolCounts[\"Write\"] ?? 0;\n const readN = s.toolCounts[\"Read\"] ?? 0;\n const writes = editN + writeN;\n\n const scores: Record<Category, number> = {\n bugfix: 0,\n refactor: 0,\n docs: 0,\n ops: 0,\n review: 0,\n feature: 0,\n chore: 0,\n };\n\n // Keyword evidence (prompt text).\n scores.bugfix += hits(t, [\"bug\", \"fix\", \"broken\", \"crash\", \"regression\", \"traceback\", \"stack trace\", \"failing\", \"error\", \"exception\", \"not working\"]) * 1.0;\n scores.refactor += hits(t, [\"refactor\", \"clean up\", \"cleanup\", \"rename\", \"extract\", \"simplify\", \"dead code\", \"tidy\", \"reorganize\", \"restructure\"]) * 1.0;\n scores.docs += hits(t, [\"readme\", \"document\", \"documentation\", \"changelog\", \"docstring\", \"comment\", \"write docs\", \"usage guide\"]) * 1.0;\n scores.ops += hits(t, [\"deploy\", \"docker\", \"pipeline\", \"ci/cd\", \"ci\", \"systemd\", \"infra\", \"kubernetes\", \"terraform\", \"release\", \"rollback\", \"container\"]) * 1.0;\n scores.review += hits(t, [\"review\", \"audit\", \"inspect\", \"look over\", \"feedback on\"]) * 1.0;\n // \"new \" dropped — too noisy (\"the new X\", \"new file\"); \"new feature\" stays as a strong signal.\n scores.feature += hits(t, [\"add\", \"implement\", \"build\", \"create\", \"feature\", \"support for\", \"new feature\"]) * 0.8;\n\n // Structural evidence (tools + files), scaled so it complements but doesn't drown keywords.\n const docFiles = s.loc.files > 0 ? countMatch(files, DOC_EXT) : 0;\n const opsFiles = s.loc.files > 0 ? countMatch(files, OPS_FILE) : 0;\n if (s.loc.files > 0) {\n scores.docs += 3 * (docFiles / s.loc.files);\n scores.ops += 3 * (opsFiles / s.loc.files);\n }\n // Edit-heavy + low net churn relative to gross churn ⇒ rework, not new code.\n if (writes >= 3 && s.loc.churn > 0 && Math.abs(s.loc.net) / s.loc.churn < 0.34) scores.refactor += 1.5;\n // Edit-dominated (lots of Edits, few/no new Writes) ⇒ reworking existing code, not adding it.\n if (editN >= 4 && editN >= writeN * 2) scores.refactor += 1.0;\n // Read-dominated, little writing ⇒ review/exploration.\n if (readN >= 5 && writes <= 2) scores.review += 1.5;\n // New files written with net-positive LoC ⇒ feature work.\n if (writeN >= 1 && s.loc.net > 20) scores.feature += 1.5;\n\n // No signal at all ⇒ record the default choice so the argmax (in classify) picks it.\n const maxScore = Math.max(...CATEGORIES.map((c) => scores[c]));\n if (maxScore === 0) scores[writes > 0 ? \"feature\" : \"chore\"] += 0.01;\n return scores;\n}\n\n/** How many of the session's touched files match a pattern. */\nfunction countMatch(files: string[], re: RegExp): number {\n let n = 0;\n for (const f of files) if (re.test(f)) n++;\n return n;\n}\n\nfunction complexity(s: Signals): { score: number; subscores: Record<string, number>; weights: Record<string, number> } {\n // v2 ceilings: tuned to the real p90 of substantial main sessions (churn ~2.6k, files ~29,\n // work-tokens in the tens of millions, multi-hour durations) so a median session sits mid-range\n // instead of pegging every subscore at 1.0. See ADR-004.\n const weights = { loc: 0.25, files: 0.15, turns: 0.2, tokens: 0.2, duration: 0.1, subagents: 0.1 };\n const subscores = {\n loc: clamp01(Math.log1p(s.loc.churn) / Math.log1p(6000)),\n files: clamp01(s.loc.files / 40),\n turns: clamp01(s.turn_count / 40),\n tokens: clamp01(Math.log1p(s.work_tokens) / Math.log1p(40_000_000)),\n duration: clamp01(s.duration_ms / (600 * 60_000)),\n subagents: clamp01(s.subagent_count / 10),\n };\n let acc = 0;\n for (const k of Object.keys(weights) as Array<keyof typeof weights>) acc += weights[k] * subscores[k];\n return { score: Math.round(acc * 1000) / 10, subscores, weights };\n}\n\ninterface SessionAgg {\n id: string;\n is_sidechain: number;\n turn_count: number;\n event_count: number;\n duration_ms: number | null;\n}\n\n/**\n * (Re)classify every session into the `classifications` table. Returns the row count written.\n * One transaction; deterministic; safe to run repeatedly (ON CONFLICT upsert).\n */\nexport function classify(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n // Incremental: classify only the sessions touched this run (the expanded set rebuildDerived returns).\n // Every signal bulk-load is scoped to the same set so memory and work track the delta, not the whole\n // DB (ADR-010). `null`/undefined → classify all sessions (the `--full` / standalone metrics path).\n const incremental = dirty != null;\n if (dirty != null) createDirtySet(db, \"_dirty\", dirty);\n const wSess = incremental ? \" WHERE session_id IN (SELECT id FROM _dirty)\" : \"\";\n const aSess = incremental ? \" AND session_id IN (SELECT id FROM _dirty)\" : \"\";\n\n const sessions = db\n .prepare(`SELECT id, is_sidechain, turn_count, event_count, duration_ms FROM sessions${incremental ? \" WHERE id IN (SELECT id FROM _dirty)\" : \"\"}`)\n .all() as unknown as SessionAgg[];\n\n // Bulk-load per-session signals once, then assemble in JS (scales to many sessions).\n const toolMix = new Map<string, Record<string, number>>();\n for (const r of db.prepare(`SELECT session_id, tool_name, COUNT(*) c FROM tool_calls${wSess} GROUP BY session_id, tool_name`).all() as Array<{ session_id: string; tool_name: string; c: number }>) {\n const m = toolMix.get(r.session_id) ?? {};\n m[r.tool_name] = r.c;\n toolMix.set(r.session_id, m);\n }\n const skillMix = new Map<string, Record<string, number>>();\n 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() as Array<{ session_id: string; skill_name: string; c: number }>) {\n const m = skillMix.get(r.session_id) ?? {};\n m[r.skill_name] = r.c;\n skillMix.set(r.session_id, m);\n }\n const tokens = new Map<string, { work: number; cacheRead: number }>();\n for (const r of db\n .prepare(\n `SELECT session_id, SUM(input_tokens) i, SUM(output_tokens) o,\n SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr\n FROM token_usage${wSess} GROUP BY session_id`,\n )\n .all() as Array<{ session_id: string; i: number | null; o: number | null; cw: number | null; cr: number | null }>) {\n tokens.set(r.session_id, { work: (r.i ?? 0) + (r.o ?? 0) + (r.cw ?? 0), cacheRead: r.cr ?? 0 });\n }\n // LoC + files from Edit/Write inputs.\n const loc = new Map<string, { added: number; removed: number; files: Set<string> }>();\n for (const r of db.prepare(`SELECT session_id, tool_name, input_json FROM tool_calls WHERE tool_name IN ('Edit','Write')${aSess}`).all() as Array<{ session_id: string; tool_name: string; input_json: string | null }>) {\n const d = locDelta(r.tool_name, r.input_json);\n const acc = loc.get(r.session_id) ?? { added: 0, removed: 0, files: new Set<string>() };\n acc.added += d.added;\n acc.removed += d.removed;\n if (d.file) acc.files.add(d.file);\n loc.set(r.session_id, acc);\n }\n // Prompt text for keyword signals.\n const promptText = new Map<string, string>();\n for (const r of db.prepare(`SELECT session_id, prompt_preview FROM turns WHERE prompt_preview IS NOT NULL${aSess}`).all() as Array<{ session_id: string; prompt_preview: string }>) {\n promptText.set(r.session_id, (promptText.get(r.session_id) ?? \"\") + \" \" + r.prompt_preview);\n }\n // Subagent role from the spawning Task/Agent tool_call (schema-v3 linkage). Lets us categorize a\n // sidechain session by what it was spawned to do, rather than keyword-matching its transcript.\n const subagentRole = new Map<string, string>();\n for (const r of db\n .prepare(\n `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)\" : \"\"}`,\n )\n .all() as Array<{ id: string; role: string }>) {\n if (!subagentRole.has(r.id)) subagentRole.set(r.id, r.role);\n }\n\n const upsert = db.prepare(\n `INSERT INTO classifications (scope, target_id, category, complexity_score, complexity_band, signals_json, classifier_version)\n VALUES ('session', @target_id, @category, @complexity_score, @complexity_band, @signals_json, @classifier_version)\n ON CONFLICT(scope, target_id) DO UPDATE SET\n category=excluded.category, complexity_score=excluded.complexity_score,\n complexity_band=excluded.complexity_band, signals_json=excluded.signals_json,\n classifier_version=excluded.classifier_version`,\n );\n\n const tx = transaction(db, () => {\n for (const sess of sessions) {\n const toolCounts = toolMix.get(sess.id) ?? {};\n const skills = skillMix.get(sess.id) ?? {};\n const l = loc.get(sess.id) ?? { added: 0, removed: 0, files: new Set<string>() };\n const tok = tokens.get(sess.id) ?? { work: 0, cacheRead: 0 };\n const filesArr = [...l.files].sort();\n const subagentCount = (toolCounts[\"Agent\"] ?? 0) + (toolCounts[\"Task\"] ?? 0);\n\n const signals: Signals = {\n toolCounts,\n skills,\n loc: { added: l.added, removed: l.removed, net: l.added - l.removed, churn: l.added + l.removed, files: l.files.size },\n turn_count: sess.turn_count,\n event_count: sess.event_count,\n work_tokens: tok.work,\n cache_read_tokens: tok.cacheRead,\n duration_ms: sess.duration_ms ?? 0,\n subagent_count: subagentCount,\n is_sidechain: sess.is_sidechain,\n };\n\n const catScores = scoreCategories(signals, promptText.get(sess.id) ?? \"\", filesArr);\n let category: Category = \"chore\";\n let bestScore = -1;\n for (const c of CATEGORIES) {\n if (catScores[c] > bestScore) {\n bestScore = catScores[c];\n category = c;\n }\n }\n // For subagents, prefer the spawner's role (Explore/Plan/… ⇒ review) over keyword heuristics.\n const role = subagentRole.get(sess.id);\n if (sess.is_sidechain) {\n const roleCat = categoryForSubagentRole(role);\n if (roleCat) category = roleCat;\n }\n const cx = complexity(signals);\n\n // Deterministic, explainable signal blob (stable key order; the file list rides as `files`).\n const signalsJson = JSON.stringify({\n tool_counts: toolCounts,\n skills,\n loc: signals.loc,\n files: filesArr,\n turn_count: signals.turn_count,\n event_count: signals.event_count,\n work_tokens: signals.work_tokens,\n cache_read_tokens: signals.cache_read_tokens,\n duration_ms: signals.duration_ms,\n subagent_count: signals.subagent_count,\n is_sidechain: signals.is_sidechain,\n subagent_role: role ?? null,\n category_scores: catScores,\n complexity_subscores: cx.subscores,\n complexity_weights: cx.weights,\n classifier_version: CLASSIFIER_VERSION,\n });\n\n upsert.run({\n target_id: sess.id,\n category,\n complexity_score: cx.score,\n complexity_band: band(cx.score),\n signals_json: signalsJson,\n classifier_version: CLASSIFIER_VERSION,\n });\n }\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty\");\n return { count: sessions.length, version: CLASSIFIER_VERSION };\n}\n","/**\n * Agent Lens — security findings (ADR-017). No AI: risky operations are flagged deterministically\n * by a rule engine over signals already in the DB (each tool call's verbatim `input_json` /\n * `result_summary` / `status`, plus the session's project path). Re-runnable and idempotent — the\n * same DB always yields identical rows. Every matched pattern and context modifier is recorded in\n * `signals_json` so a finding can be explained and rules retuned without re-deriving meaning.\n *\n * agent-lens is a *retrospective* analyzer, so this is forensic awareness — surfacing that an agent\n * *did* something risky after the fact — not runtime prevention. Findings are 0..N per tool call, so\n * an incremental re-run DELETEs the touched sessions' rows then re-INSERTs (delete-then-insert),\n * unlike the 1-per-session `classifications` upsert. `detector_version` lets a future (e.g. local-LLM)\n * engine supersede these rows.\n */\nimport { createHash } from \"node:crypto\";\nimport { posix as path } from \"node:path\";\nimport { transaction, bumpSeverity, type Severity, type SecurityCategoryKey } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\nimport { parseStored } from \"./storedjson.js\";\n\n// Bump on any rule/severity change so a re-run is attributable to an engine version (mirrors\n// CLASSIFIER_VERSION). Recorded on every row + in signals_json. Finding ids are independent of this\n// version, so a bump doesn't invalidate user triage state. The per-version rationale (what each bump\n// fixed/added and why) lives in docs/DETECTOR-CHANGELOG.md — keep the history there, not inline.\nexport const DETECTOR_VERSION = 8;\n\n// Severity model + category taxonomy are shared from core (packages/core/src/security.ts) so the\n// server and web agree on ordering and keys. Re-exported for tests that drive this module directly.\nexport { bumpSeverity, type Severity };\ntype Category = SecurityCategoryKey;\n\n/** Truncate an evidence snippet so we never dump wholesale secrets/output into the DB. */\nfunction evidence(s: string, max = 200): string {\n const one = s.replace(/\\s+/g, \" \").trim();\n return one.length > max ? one.slice(0, max - 1) + \"…\" : one;\n}\n\n/** Deterministic finding id — stable across runs so re-detection reproduces identical rows. */\nfunction findingId(toolCallId: string, ruleId: string): string {\n return createHash(\"sha1\").update(toolCallId).update(\"\\0\").update(ruleId).digest(\"hex\").slice(0, 16);\n}\n\nexport interface RuleContext {\n toolName: string;\n command: string | null; // Bash command string, verbatim (for evidence + value scanning)\n commandCode: string | null; // executed view, quotes INTACT (comments/heredoc/echo-output neutralized) — for sql_drop\n commandBare: string | null; // commandCode + quoted-string contents blanked — for command-verb rules (sudo, rm, …)\n filePath: string | null; // Read/Write/Edit file_path, if any\n input: any; // parsed input_json (may be null)\n resultSummary: string | null;\n status: string | null; // success | error | ...\n projectPath: string | null; // the session's cwd, for outside-project checks\n ownedConfigDirs: string[]; // agent config roots (from the `sources` table) the agent legitimately owns\n}\n\nexport interface RuleMatch {\n evidence: string; // the offending snippet\n severity?: Severity; // override the rule's baseSeverity (context-driven escalation)\n mods?: Record<string, unknown>; // extra explainability recorded in signals_json\n}\n\ninterface Rule {\n id: string;\n category: Category;\n framework_ref: string;\n title: string;\n tools: string[] | \"*\"; // tool_names this rule applies to\n baseSeverity: Severity;\n test(ctx: RuleContext): RuleMatch | null;\n}\n\n// ---- Shared matchers ------------------------------------------------------\n\n// A path we consider outside the agent's working project (home dotfiles, system dirs, root).\nconst SYSTEM_PATH = /(^|\\s)(\\/etc\\/|\\/usr\\/|\\/bin\\/|\\/sbin\\/|\\/boot\\/|\\/var\\/|\\/root\\/|~\\/\\.ssh\\/|~\\/\\.aws\\/)/;\nconst HOME_OR_ROOT_TARGET = /(\\s|^)(~\\/?|\\/|\\/\\*|\\$HOME\\/?|\\*)(\\s|$)/;\n\n/** Secret-bearing file locations (matched against a file path or a shell command). */\nconst SECRET_FILE =\n /(^|\\/|\\s)(\\.env(\\.[\\w.-]+)?|id_rsa|id_ed25519|id_ecdsa|\\.ssh\\/[\\w.-]+|\\.aws\\/credentials|\\.gcp\\/|gcloud\\/|\\.npmrc|\\.pypirc|\\.netrc|\\.pem|\\.p12|\\.pfx|secrets?\\.(ya?ml|json|txt)|credentials(\\.json)?)\\b/i;\n\n// Non-secret config *templates* (e.g. .env.example, secrets.sample.yaml) — reading these is expected\n// and carries no real secret, so exclude them from credential-access even though they match SECRET_FILE.\n// The trailing (?![\\w-]) matches the end of the \".example\" token whether the token ends the string\n// (a Read filePath) or is followed by more shell text (`… .env.example | head`) — a plain `$` anchor\n// only handled the former and let the Bash-command case slip through.\nconst EXAMPLE_FILE = /\\.(example|sample|template|dist)(?![\\w-])/i;\n\n// Commands that read file *contents* — the actual credential-access signal. Listing/stat commands\n// (ls, file, stat, find, …) are deliberately absent: naming a secret path in an `ls`/`file` doesn't\n// read it, so they must not flag (only a content read does).\nconst CONTENT_READ_CMD = /\\b(cat|less|more|head|tail|bat|xxd|strings|base64|grep|egrep|rg|awk|sed|nl|tac|od|hexdump|cp|scp|sort)\\b/i;\n\n/** Secret-looking values (in a command or a captured result). */\nconst SECRET_VALUE_PATTERNS: Array<{ re: RegExp; label: string; critical?: boolean }> = [\n { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, label: \"private-key\", critical: true },\n { re: /\\bAKIA[0-9A-Z]{16}\\b/, label: \"aws-access-key-id\" },\n { re: /\\bASIA[0-9A-Z]{16}\\b/, label: \"aws-temp-key-id\" },\n { re: /\\bgh[pousr]_[A-Za-z0-9]{30,}\\b/, label: \"github-token\" },\n { re: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/, label: \"slack-token\" },\n { re: /\\bsk-[A-Za-z0-9]{20,}\\b/, label: \"api-secret-key\" },\n];\n\nconst isBash = (t: string) => t === \"Bash\";\n\n// SQL keywords that irreversibly drop/empty a database object.\nconst SQL_DESTRUCTIVE = /\\b(DROP\\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\\s+TABLE)?)\\b/i;\n// Database-client command words. A DROP/TRUNCATE in a *Bash* command is only executed SQL when one of\n// these runs it; without a client the keyword is inert text (a `grep \"…\\|truncate\"` search pattern, an\n// echoed help string, a log line), so it must not flag. The structured input.query path needs no client.\nconst SQL_CLIENT = /\\b(psql|pgcli|mysql|mysqladmin|mariadb|sqlite3?|sqlcmd|usql|mongo|mongosh|clickhouse-client|cockroach|snowsql|bq)\\b/i;\n\n// ---- Command sanitization ------------------------------------------------\n// A Bash command is matched in three views, because \"the agent ran X\" and \"the text X appears\" are\n// different questions. Rules pick the view their question needs (RuleContext documents which):\n//\n// raw — verbatim. Value scanning (secret_in_data) and host classification (netTargetScope) use it:\n// a secret is exposed, and a URL is a destination, whether or not the text was \"executed\".\n// code — the EXECUTED view, quotes INTACT. Comments, heredoc bodies and echo/printf output are\n// neutralized, but quoted strings survive because `psql -c \"DROP …\"` and `sh -c \"…\"` carry\n// real executed code inside quotes.\n// bare — code, plus the CONTENTS of quoted strings blanked. Command-VERB rules (sudo, rm -rf, …)\n// use it: a dangerous token inside a quoted *argument* (`grep \"sudo\"`, `git commit -m \"…\n// rm -rf …\"`) is data passed to a program, not a command the shell runs.\n//\n// Each stage below is one regex pass; `commandViews` is the only place their ORDER is decided, and\n// the order matters (see the stage comments). packages/ingest/test/sanitize.test.ts pins every stage\n// individually, including the holes we knowingly accept — this is regex-as-tokenizer and a real\n// parser is out of scope, so the tests are the contract.\n\n/** `# …` never runs. (Hole: a `#` inside a quoted string reads as a comment.) */\nexport const stripComments = (c: string): string => c.replace(/(^|\\s)#[^\\n]*/g, \"$1\");\n\n/** The print builtins' arguments are data sent to stdout, so `sudo`/`rm -rf`/… inside them is inert.\n * Quoted arguments are consumed as whole units, so a separator (`;` `|`) inside printed text never\n * leaks out as a fake command break. */\nexport const neutralizeEcho = (c: string): string =>\n c.replace(/((?:^|[;&|\\n(])\\s*)(?:echo|printf)\\b(?:\"(?:[^\"\\\\]|\\\\.)*\"|'[^']*'|[^;&|\\n])*/gi, \"$1echo\");\n\n/** A heredoc body (`cmd <<EOF … EOF`) is data written to a file/stdin, not commands the shell runs —\n * unless the resulting file is then executed, which execGeneratedScript catches separately. Runs\n * FIRST, so a `#` inside a body can't turn the terminator line into a comment. */\nexport const blankHeredoc = (c: string): string => c.replace(/<<-?\\s*(['\"]?)(\\w+)\\1[\\s\\S]*?\\n\\s*\\2\\b/g, \"<<HEREDOC\");\n\n/** Empty every quoted string literal, keeping the quotes. Runs LAST, on `code`, so the in-string\n * separators are already gone from the parts being kept. (Hole: a command truly executed via\n * `sh -c \"sudo …\"` disappears from this view; that inline form is rare and worth the far fewer\n * false positives.) */\nexport const blankQuoted = (c: string): string => c.replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"').replace(/'[^']*'/g, \"''\");\n\nexport const codeOf = (c: string): string => neutralizeEcho(stripComments(blankHeredoc(c)));\nexport const bareOf = (c: string): string => blankQuoted(codeOf(c));\n\n/** The two derived views of one command, built together — the single place the pipeline is assembled. */\nexport function commandViews(raw: string): { code: string; bare: string } {\n const code = codeOf(raw);\n return { code, bare: blankQuoted(code) };\n}\n\n// A file is \"executed\" when passed to an interpreter (sh/bash/…), sourced (`source`/`.`), or invoked\n// as a path (`./f`, `/abs/f`). Detects the write-a-script-then-run-it pattern (`echo … > f.sh; sh f.sh`)\n// that turns otherwise-inert printed text into live code. Returns the offending file, or null.\nfunction execGeneratedScript(cmd: string): string | null {\n const targets = new Set<string>();\n for (const m of cmd.matchAll(/(?:>>?|\\btee\\b\\s+(?:-a\\s+)?)\\s*[\"']?([A-Za-z0-9._/~-]+)/g)) {\n if (m[1] && !m[1].startsWith(\"/dev/\")) targets.add(m[1]);\n }\n for (const t of targets) {\n const e = t.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Interpreter/source invocation: `sh FILE`, `bash FILE`, `source FILE`, `. FILE` (the interpreter is\n // the command word, so FILE may sit anywhere after it).\n const interp = new RegExp(`\\\\b(?:sh|bash|zsh|ksh|dash|source)\\\\s+[\"']?${e}\\\\b|(?:^|[\\\\n;|&(])\\\\s*\\\\.\\\\s+[\"']?${e}\\\\b`);\n // FILE run directly — must be at COMMAND POSITION (start or after a separator), NOT an argument slot,\n // otherwise the redirect/`tee FILE` target matches its own occurrence (`… | tee /etc/hosts`).\n const hasPath = /^(\\.\\/|\\/|~\\/)/.test(t);\n const path = hasPath\n ? new RegExp(`(?:^|[\\\\n;|&(])\\\\s*${e}(?:\\\\s|$|[;&|<>])`)\n : new RegExp(`(?:^|[\\\\n;|&(])\\\\s*\\\\.\\\\/${e}\\\\b`);\n if (interp.test(cmd) || path.test(cmd)) return t;\n }\n return null;\n}\n\n// Temp dirs (including the per-session scratchpad under /tmp/…) are always agent-owned regardless of\n// which agent/config produced them.\nconst TEMP_PATH = /^(\\/tmp\\/|\\/var\\/folders\\/|\\/private\\/var\\/folders\\/)/;\n\n/**\n * Paths the agent legitimately owns, so writing there is expected, not a finding: its own config/work\n * dir (e.g. plans, memory, todos, projects, settings, skills) and temp dirs. The config roots are the\n * `config_dir`s of the configured sources (the `sources` table, seeded from the project config file),\n * NOT a hardcoded `.claude` pattern — that way side-by-side installs (~/.claude, ~/.claude-isf, …) and\n * any relocated config dir are covered from a single source of truth. Neutralizes the biggest\n * write_outside_project false-positive source.\n */\nfunction isAgentOwnedPath(p: string, configDirs: string[]): boolean {\n if (TEMP_PATH.test(p)) return true;\n if (configDirs.some((d) => p === d || p.startsWith(d + \"/\"))) return true;\n const tmp = process.env.TMPDIR;\n return !!tmp && p.startsWith(tmp.replace(/\\/$/, \"\") + \"/\");\n}\n\n/**\n * If a write target lands outside the session's project directory, return its resolved absolute path;\n * else null. Handles two escapes: an absolute path already outside the project, and a RELATIVE path that\n * climbs out via `../` (resolved against the project root and posix-normalized — `../../../etc/x` →\n * `/etc/x`). A relative path with no `../` is project-local; a `../` that normalizes back inside the\n * project is not an escape. Agent-owned dirs (config/work + temp) and an unknown project never flag.\n * Returning the resolved path lets the caller run SYSTEM_PATH severity on where the write actually lands.\n */\nfunction outsideProjectPath(filePath: string | null, projectPath: string | null, configDirs: string[]): string | null {\n if (!filePath) return null;\n let abs = filePath;\n if (!filePath.startsWith(\"/\")) {\n if (!projectPath || !filePath.includes(\"../\")) return null; // relative & no traversal → project-local\n abs = path.normalize(projectPath.replace(/\\/$/, \"\") + \"/\" + filePath);\n }\n if (isAgentOwnedPath(abs, configDirs)) return null; // agent's own config/work dir + temp → expected\n if (!projectPath) return null; // unknown project → don't guess\n const root = projectPath.replace(/\\/$/, \"\");\n return abs !== root && !abs.startsWith(root + \"/\") ? abs : null;\n}\n\n// Classify the destination host(s) of a curl/wget command for exfil scoring. Runs on the VERBATIM\n// command (not commandBare) so quoted URLs are visible, and strips -H/--header args so a URL sitting\n// in an Origin/Referer header is never mistaken for the request target. Returns the most-severe scope\n// present: external > internal > loopback; \"unknown\" when no host is recognizable (e.g. the target is\n// a shell variable) — treated as external-ish so we don't under-report an unprovable destination.\ntype NetScope = \"external\" | \"internal\" | \"loopback\" | \"unknown\";\nconst SCOPE_RANK: Record<NetScope, number> = { unknown: 0, loopback: 1, internal: 2, external: 3 };\nfunction netTargetScope(rawCommand: string): NetScope {\n const s = rawCommand\n .replace(/(?:-H|--header)\\s+(['\"]).*?\\1/gi, \" \") // quoted header value (Origin/Referer/…)\n .replace(/(?:-H|--header)\\s+[^\\s'\"]+/gi, \" \"); // bare header token\n const authorities: string[] = [];\n // scheme URLs: http(s)://host (host may be a bracketed IPv6 literal)\n for (const m of s.matchAll(/\\bhttps?:\\/\\/(\\[[0-9a-fA-F:]+\\]|[^/\\s\"'<>|]+)/gi)) authorities.push(m[1]);\n // bare host:port targets (curl localhost:4477, curl 10.0.0.5:8080) — require a numeric port so we\n // never mistake a file path or a `key:value` token for a network destination.\n for (const m of s.matchAll(/(?:^|[\\s\"'=(])(\\[[0-9a-fA-F:]+\\]|[A-Za-z0-9.-]+):\\d{2,5}(?=[/\\s\"']|$)/g)) authorities.push(m[1]);\n let scope: NetScope = \"unknown\";\n for (let host of authorities) {\n host = host.replace(/^\\[|\\]$/g, \"\").replace(/^[^@/]*@/, \"\").toLowerCase(); // drop brackets + userinfo\n if (!host.includes(\"::\")) host = host.replace(/:\\d+$/, \"\"); // drop :port (but not the ::1 in IPv6)\n let hs: NetScope;\n if (host === \"localhost\" || /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host) || host === \"0.0.0.0\" || host === \"::1\")\n hs = \"loopback\";\n else if (\n /^10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host) || // RFC1918\n /^192\\.168\\.\\d{1,3}\\.\\d{1,3}$/.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}$/.test(host) ||\n /^169\\.254\\.\\d{1,3}\\.\\d{1,3}$/.test(host) || // link-local\n host.endsWith(\".local\") ||\n host.endsWith(\".internal\") ||\n !host.includes(\".\") // single-label host (a service name, not a public domain)\n )\n hs = \"internal\";\n else hs = \"external\";\n if (SCOPE_RANK[hs] > SCOPE_RANK[scope]) scope = hs;\n }\n return scope;\n}\n\n/**\n * The commonest rule shape, as a factory: match ONE regex against the command-word view\n * (`bare` — quoted arguments blanked, comments/heredocs/echo output neutralized) and, on a hit, report\n * the VERBATIM command as evidence at the rule's base severity. Nothing context-dependent: a rule that\n * escalates on its target, inspects `input`, or splits the command into segments stays a full `test`.\n *\n * `tools` defaults to Bash, the only tool that carries a command; the few rules that declare \"*\" keep\n * doing so (they self-limit by requiring `commandBare`).\n */\nfunction simpleCommandRule(spec: {\n id: string;\n category: Category;\n framework_ref: string;\n title: string;\n baseSeverity: Severity;\n /** Matched against ctx.commandBare. */\n match: RegExp;\n tools?: string[] | \"*\";\n}): Rule {\n const { match, tools = [\"Bash\"], ...meta } = spec;\n return {\n ...meta,\n tools,\n test(ctx) {\n const c = ctx.commandBare;\n if (!c || !match.test(c)) return null;\n return { evidence: evidence(ctx.command!) };\n },\n };\n}\n\n// ---- The rule set (v1) ----------------------------------------------------\n// Representative, not exhaustive; grouped by framework-anchored category. New rules just append here.\n\nconst RULES: Rule[] = [\n // --- Destructive / data-loss (OWASP ASI02 Tool Misuse) -------------------\n {\n id: \"destructive.rm_rf\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Recursive force delete (rm -rf)\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n if (!ctx.commandBare) return null;\n // `git rm` (even `git rm -rf`) removes *tracked* files and is recoverable from git history — it's\n // not a filesystem `rm -rf`. Neutralize it (glued to `git` so the `rm` word boundary is gone) so it\n // never matches, while a real `rm -rf` elsewhere in the same command still does.\n const c = ctx.commandBare.replace(/\\bgit\\s+rm\\b/gi, \"gitrm\");\n // Isolate the `rm` command's own segment (up to the next |;&\\n) so flags/targets from a later\n // command don't leak in.\n const rmSeg = c.match(/\\brm\\b[^|;&\\n]*/i)?.[0];\n if (!rmSeg) return null;\n // rm needs both a recursive and a force flag. Flag tokens are matched as whitespace-bounded\n // letter-only clusters (`-rf`, `-fr`, `-r -f`, `--recursive --force`) so a path token that merely\n // contains an f before an r — e.g. `reports/…-software-engineer…` — isn't mistaken for `-fr`.\n const combined = /(?:^|\\s)-(?=[a-zA-Z]*[rR])(?=[a-zA-Z]*f)[a-zA-Z]+(?=\\s|$)/.test(rmSeg);\n const recursive = /(?:^|\\s)(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)(?=\\s|$)/.test(rmSeg);\n const force = /(?:^|\\s)(?:-[a-zA-Z]*f[a-zA-Z]*|--force)(?=\\s|$)/.test(rmSeg);\n if (!combined && !(recursive && force)) return null;\n const dangerousTarget = HOME_OR_ROOT_TARGET.test(c) || /\\s\\/(?:\\s|$)/.test(c) || /\\brm\\b[^|;&\\n]*\\*/.test(c);\n // Every non-flag target under a temp dir → routine scratch cleanup, demote to low.\n const targets = rmSeg.split(/\\s+/).slice(1).filter((a) => a && !a.startsWith(\"-\"));\n const tempOnly = targets.length > 0 && targets.every((t) => TEMP_PATH.test(t));\n const severity: Severity = dangerousTarget ? \"critical\" : tempOnly ? \"low\" : \"high\";\n return { evidence: evidence(ctx.command!), severity, mods: { dangerous_target: dangerousTarget, temp_target: tempOnly } };\n },\n },\n simpleCommandRule({\n id: \"destructive.git_reset_hard\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Discards local changes (git reset --hard)\",\n baseSeverity: \"low\", // routine dev op — only uncommitted working-tree changes are lost; commits survive in reflog\n match: /\\bgit\\s+reset\\s+--hard\\b/i,\n }),\n {\n id: \"destructive.git_force_push\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Force-push rewrites remote history\",\n tools: [\"Bash\"],\n baseSeverity: \"low\", // feature-branch force-push is routine; only a protected branch (below) is high\n test(ctx) {\n const c = ctx.commandBare;\n if (!c || !/\\bgit\\s+push\\b[^|;&\\n]*(--force\\b|--force-with-lease\\b|\\s-f\\b)/i.test(c)) return null;\n const mainBranch = /\\b(main|master|release)\\b/.test(c);\n return { evidence: evidence(ctx.command!), severity: mainBranch ? \"high\" : \"low\", mods: { protected_branch: mainBranch } };\n },\n },\n {\n id: \"destructive.sql_drop\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Drops or truncates a database object\",\n tools: \"*\",\n baseSeverity: \"high\",\n test(ctx) {\n const query = typeof ctx.input?.query === \"string\" ? ctx.input.query : null;\n // A structured DB tool call (input.query) is SQL by definition — flag on the keyword alone.\n if (query && SQL_DESTRUCTIVE.test(query)) return { evidence: evidence(ctx.command ?? query) };\n // A Bash command only executes SQL when a database client runs it; otherwise DROP/TRUNCATE is just\n // text (e.g. a `grep \"…\\|truncate\"` search pattern) and inert, so require both a client and the keyword.\n const code = ctx.commandCode;\n if (code && SQL_CLIENT.test(code) && SQL_DESTRUCTIVE.test(code)) return { evidence: evidence(ctx.command!) };\n return null;\n },\n },\n simpleCommandRule({\n id: \"destructive.disk_wipe\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Low-level disk write / wipe (dd, mkfs, shred)\",\n baseSeverity: \"high\",\n match: /(\\bdd\\s+if=|\\bmkfs(\\.\\w+)?\\b|\\bshred\\b)/i,\n }),\n {\n id: \"destructive.overwrite_critical\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Overwrites a critical project file\",\n tools: [\"Write\", \"Edit\", \"MultiEdit\"],\n baseSeverity: \"low\",\n test(ctx) {\n const f = ctx.filePath;\n if (!f) return null;\n const lock = /(^|\\/)(package-lock\\.json|pnpm-lock\\.ya?ml|yarn\\.lock|Cargo\\.lock|go\\.sum)\\b/.test(f);\n // A CI-pipeline file is more sensitive than a lockfile: modifying it can run arbitrary code or\n // exfiltrate secrets in CI (a supply-chain risk), so it stays medium while lockfile churn is low.\n const ci = /(^|\\/)(\\.github\\/workflows\\/|\\.gitlab-ci\\.ya?ml)\\b/.test(f);\n if (!lock && !ci) return null;\n return { evidence: evidence(f), severity: ci ? \"medium\" : \"low\", mods: { kind: ci ? \"ci-config\" : \"lockfile\" } };\n },\n },\n\n // --- Credential & secret access ------------------------------------------\n {\n id: \"credential.secret_file_access\",\n category: \"credential-access\",\n framework_ref: \"MITRE ATLAS: Credential Access\",\n title: \"Reads a secret/credential file\",\n tools: [\"Read\", \"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n if (isBash(ctx.toolName)) {\n const cmd = ctx.commandBare; // executed view: a secret named only in a comment / echo / quoted arg isn't read\n if (!cmd) return null;\n // Evaluate each pipeline/command segment independently: a secret file is only *read* when a\n // content-read command (cat/grep/…) and the secret path occur in the SAME segment. A bare\n // `ls`/`file`/`stat` that merely names the path lists metadata; piping its OUTPUT into a\n // `grep`/`head` in a later segment (e.g. `file ~/.ssh/id_* | grep -v .pub`) exposes no secret\n // content — the read command never touches the file. Splitting first avoids matching the\n // read command against the wrong segment.\n const segments = cmd.split(/\\|\\||&&|[|;\\n]/);\n const reads = segments.some(\n (seg) => SECRET_FILE.test(seg) && !EXAMPLE_FILE.test(seg) && CONTENT_READ_CMD.test(seg),\n );\n return reads ? { evidence: evidence(ctx.command!) } : null;\n }\n // Read tool: the filePath itself is the target (no command to segment).\n const f = ctx.filePath;\n if (!f || !SECRET_FILE.test(f) || EXAMPLE_FILE.test(f)) return null; // skip .env.example & templates\n return { evidence: evidence(f) };\n },\n },\n {\n id: \"credential.secret_in_data\",\n category: \"credential-access\",\n framework_ref: \"MITRE ATLAS: Credential Access\",\n title: \"Secret-looking value in command or output\",\n tools: \"*\",\n baseSeverity: \"high\",\n test(ctx) {\n const hay = [ctx.command, ctx.resultSummary].filter(Boolean).join(\"\\n\");\n if (!hay) return null;\n for (const p of SECRET_VALUE_PATTERNS) {\n const m = p.re.exec(hay);\n if (m) return { evidence: `${p.label}: ${evidence(m[0], 40)}`, severity: p.critical ? \"critical\" : \"high\", mods: { secret_kind: p.label } };\n }\n return null;\n },\n },\n\n // --- Data exfiltration (MITRE ATLAS AML.T0086) ---------------------------\n {\n id: \"exfil.pipe_to_network\",\n category: \"exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n title: \"Pipes local data into a network command\",\n tools: [\"Bash\"],\n baseSeverity: \"critical\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n // e.g. `cat secrets | curl …`, `tar … | nc host port`, `… | curl -d @-`\n if (!/(\\||<)\\s*(sudo\\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\\b/i.test(c)) return null;\n // A data-reading command (or a `<` stdin redirect) must be present on the source side. `<` sits\n // outside the \\b group because a word boundary against a non-word char never matches it there.\n if (!/(?:\\b(?:cat|tar|zip|gzip|base64|env|printenv)\\b|<)/i.test(c)) return null;\n return { evidence: evidence(ctx.command!) };\n },\n },\n {\n id: \"exfil.network_upload\",\n category: \"exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n title: \"Uploads data to a network host\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n // Upload-shaped curl/wget. NOTE: no /i flag — the short flags here are case-SENSITIVE (curl's -d\n // is data but -D is dump-header, -F is form but -f is --fail, -T is upload but -t is unrelated), so\n // matching case-insensitively turned plain GET smoke tests (`curl -sf`, `curl -D -`) into uploads.\n const upload = /\\b(curl|wget)\\b[^|;&\\n]*(-X\\s*POST|--data\\b|--data-\\w+\\b|\\s-d\\b|-T\\b|--upload-file\\b|-F\\b|--form\\b)/.test(c);\n if (!upload) return null;\n // `@file` / --upload-file / -T means a real file is being sent (matched on commandBare so an `@`\n // inside a quoted arg — e.g. an email in a header value — doesn't count as a file).\n const withFile = /(@[^\\s'\"]+|--upload-file|\\s-T\\b)/.test(c);\n // Score by destination: external host = high (critical with a file); a private/internal host is a\n // lesser concern (low); a loopback call is local IPC / a dev server (info). Sending an actual file\n // bumps the internal/loopback tiers one step. An unrecognizable target (shell var) can't be shown\n // local, so it stays high. Classified on the verbatim command so quoted URLs are visible.\n const scope = netTargetScope(ctx.command!);\n let severity: Severity;\n if (scope === \"external\") severity = withFile ? \"critical\" : \"high\";\n else if (scope === \"unknown\") severity = \"high\";\n else severity = withFile ? bumpSeverity(scope === \"loopback\" ? \"info\" : \"low\", 1) : scope === \"loopback\" ? \"info\" : \"low\";\n return {\n evidence: evidence(ctx.command!),\n severity,\n mods: { target_scope: scope, with_file: withFile },\n };\n },\n },\n simpleCommandRule({\n id: \"exfil.reverse_shell\",\n category: \"exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n title: \"Netcat / reverse-shell style connection\",\n baseSeverity: \"critical\",\n match: /\\b(nc|ncat|netcat)\\b[^|;&\\n]*(-e\\b|-c\\b|\\d{2,5})|\\/dev\\/tcp\\/|bash\\s+-i\\b/i,\n }),\n\n // --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)\n simpleCommandRule({\n id: \"privilege.skip_permissions\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Bypasses the permission guardrail\",\n tools: \"*\",\n baseSeverity: \"high\",\n match: /--dangerously-skip-permissions\\b|--yolo\\b|--dangerously-skip\\b/i,\n }),\n {\n id: \"privilege.curl_pipe_shell\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Executes a remote script (curl | sh)\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n const m = /\\b(?:curl|wget)\\b[^|]*\\|\\s*(?:sudo\\s+)?(sh|bash|zsh|python\\d?|node)\\b([^|;&]*)/i.exec(c);\n if (!m) return null;\n // The danger is the interpreter executing the *downloaded body* piped to its stdin. A shell always\n // does. node/python execute piped stdin as a script only when NOT handed an inline program or\n // module — `curl … | node -e '…'`, `python -c '…'`, `python -m json.tool` just consume the output\n // as DATA (e.g. parsing an API response), which is not remote-script execution and must not flag.\n const interp = m[1].toLowerCase();\n const args = m[2] || \"\";\n if (/^(?:node|python)/.test(interp) && /(?:^|\\s)-(?:e|c|m|p)\\b|(?:^|\\s)--(?:eval|command|module|print)\\b/.test(args))\n return null;\n return { evidence: evidence(ctx.command!) };\n },\n },\n {\n id: \"obfuscated.decode_exec\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Decodes and executes obfuscated content (base64 -d | sh)\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n // A decoder turns opaque/encoded text into live code; piping that into an interpreter runs it — a\n // decode-then-execute evasion primitive. Matched on the executed view so a decode string that is\n // only echoed/quoted/commented is already inert.\n const decoder = /\\b(base64\\s+(?:-d|-D|--decode)|xxd\\s+-r|openssl\\s+(?:base64\\s+-d|enc\\b[^|]*\\s-d\\b)|uudecode)\\b/i.exec(c);\n if (!decoder) return null;\n // The decoder must be UPSTREAM of a pipe INTO the interpreter (so its decoded stdout is executed).\n // Decoding to a file (`base64 -d f > out`) or a later, separately-run script (`; sh x`) has no pipe\n // from the decoder to the shell and must not flag — so we search only the text AFTER the decoder.\n const downstream = c.slice(decoder.index + decoder[0].length);\n const piped = /\\|\\s*(?:sudo\\s+)?(sh|bash|zsh|dash|ksh|python\\d?|node|perl|ruby)\\b([^|;&\\n]*)/i.exec(downstream);\n if (!piped) return null;\n const interp = piped[1].toLowerCase();\n const args = piped[2] || \"\";\n // node/python/perl/ruby handed an inline program (-e/-c/-m/-p) consume the piped output as DATA,\n // not as a script to execute — mirror curl_pipe_shell's carve-out.\n if (/^(?:node|python|perl|ruby)/.test(interp) && /(?:^|\\s)-(?:e|c|m|p)\\b|(?:^|\\s)--(?:eval|command|module|print)\\b/.test(args))\n return null;\n const sudo = /\\|\\s*sudo\\b/i.test(downstream);\n const d = decoder[0].toLowerCase();\n const decoderName = d.startsWith(\"base64\") ? \"base64\" : d.startsWith(\"xxd\") ? \"xxd\" : d.startsWith(\"openssl\") ? \"openssl\" : \"uudecode\";\n return { evidence: evidence(ctx.command!), severity: sudo ? \"critical\" : \"high\", mods: { decoder: decoderName, interpreter: interp } };\n },\n },\n simpleCommandRule({\n id: \"privilege.sudo\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Runs a command as root (sudo)\",\n baseSeverity: \"high\", // root escalation removes a real safety barrier — higher than an in-project op\n // sudo must be in *command position* — start of the command or right after a separator (; | && ( ).\n // This ignores `sudo` as an argument (e.g. `apt install sudo`) and, via codeOf, inside echo/comments.\n match: /(?:^|[\\n;|&(])\\s*sudo\\b/i,\n }),\n {\n id: \"privilege.exec_generated_script\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Writes a shell script from inline output and executes it\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const raw = ctx.command;\n if (!raw) return null;\n // Echoing a command into a file that is then run/sourced turns otherwise-inert printed text into\n // live code (e.g. `echo \"sudo rm -rf /\" > f.sh; sh f.sh`) — the reason we don't just ignore\n // dangerous tokens inside echo strings. Detect on the comment-stripped raw command.\n const file = execGeneratedScript(stripComments(raw));\n if (!file) return null;\n // Escalate when the generated script itself carries a destructive/privileged payload.\n const critical = /(?:^|[\"'\\s;|&(])sudo\\b|\\brm\\s+[^|;&\\n]*-[a-z]*r[a-z]*f|\\bchmod\\s+0?777\\b|\\bmkfs|\\bdd\\s+if=/i.test(raw);\n return { evidence: evidence(raw), severity: critical ? \"critical\" : \"high\", mods: { script_file: file } };\n },\n },\n simpleCommandRule({\n id: \"privilege.chmod_777\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Overly permissive permissions (chmod 777)\",\n baseSeverity: \"medium\",\n match: /\\bchmod\\s+(-[a-zA-Z]+\\s+)*0?777\\b/i,\n }),\n simpleCommandRule({\n id: \"privilege.persistence\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Installs a persistence mechanism\",\n baseSeverity: \"high\",\n match: /\\bcrontab\\s+-|\\bsystemctl\\s+enable\\b|\\blaunchctl\\s+load\\b|>>\\s*~\\/\\.(bashrc|zshrc|profile|bash_profile)\\b|\\/etc\\/(cron|systemd|rc\\.local)/i,\n }),\n {\n id: \"privilege.write_outside_project\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Writes a file outside the working project\",\n tools: [\"Write\", \"Edit\", \"MultiEdit\"],\n baseSeverity: \"low\", // writing to /tmp, scratch dirs, adjacent repos is routine; only a system path (below) is high\n test(ctx) {\n const f = ctx.filePath;\n const abs = outsideProjectPath(f, ctx.projectPath, ctx.ownedConfigDirs);\n if (!abs) return null;\n const system = SYSTEM_PATH.test(abs); // severity keys off where the write LANDS (resolved), not the raw arg\n const traversal = !!f && !f.startsWith(\"/\") && f.includes(\"../\"); // escaped via a relative `../` climb\n return {\n evidence: evidence(f!),\n severity: system ? \"high\" : \"low\",\n mods: { project: ctx.projectPath, system_path: system, ...(traversal ? { traversal: true } : {}) },\n };\n },\n },\n];\n\n/** Rule → tool-name applicability. */\nfunction appliesTo(rule: Rule, toolName: string): boolean {\n return rule.tools === \"*\" || rule.tools.includes(toolName);\n}\n\n/** Parse a tool call's verbatim input_json into a RuleContext (mirrors classify.ts's locDelta parse). */\nfunction buildContext(\n row: { tool_name: string; input_json: string | null; result_summary: string | null; status: string | null },\n projectPath: string | null,\n ownedConfigDirs: string[],\n): RuleContext {\n const input: any = parseStored(row.input_json, \"detect: tool_calls.input_json\");\n const command = typeof input?.command === \"string\" ? input.command : null;\n const views = command != null ? commandViews(command) : null;\n const commandCode = views?.code ?? null;\n const commandBare = views?.bare ?? null;\n const filePath = typeof input?.file_path === \"string\" ? input.file_path : null;\n return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };\n}\n\ninterface ToolRow {\n id: string;\n session_id: string;\n event_uuid: string | null;\n turn_id: string | null;\n tool_name: string;\n input_json: string | null;\n result_summary: string | null;\n status: string | null;\n}\n\n/**\n * (Re)scan tool calls into the `findings` table. Returns the total finding count + engine version.\n * Deterministic; safe to run repeatedly. `dirty` (the expanded id set rebuildDerived returns) scopes an\n * incremental run to the touched sessions via delete-then-insert; null/undefined → scan everything.\n */\nexport function detect(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n const incremental = dirty != null;\n if (dirty != null) createDirtySet(db, \"_dirty_sec\", dirty);\n const scope = incremental ? \" WHERE tc.session_id IN (SELECT id FROM _dirty_sec)\" : \"\";\n\n // Agent-owned config roots — the configured sources' `config_dir`s (seeded from the project config\n // file at ingest). Single source of truth for the write_outside_project allowlist, so relocated or\n // side-by-side installs (~/.claude, ~/.claude-isf, …) are covered without a hardcoded path pattern.\n const ownedConfigDirs = (db.prepare(\"SELECT config_dir FROM sources WHERE config_dir IS NOT NULL\").all() as Array<{ config_dir: string }>)\n .map((r) => r.config_dir.replace(/\\/$/, \"\"))\n .filter(Boolean);\n\n // Project path per (scoped) session — for outside-project checks.\n const projPath = new Map<string, string>();\n for (const r of db\n .prepare(\n `SELECT s.id id, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${\n incremental ? \" WHERE s.id IN (SELECT id FROM _dirty_sec)\" : \"\"\n }`,\n )\n .all() as Array<{ id: string; path: string }>) {\n projPath.set(r.id, r.path);\n }\n\n const rows = db\n .prepare(\n `SELECT tc.id, tc.session_id, tc.event_uuid, tc.turn_id, tc.tool_name, tc.input_json, tc.result_summary, tc.status\n FROM tool_calls tc${scope}`,\n )\n .all() as unknown as ToolRow[];\n\n const insert = db.prepare(\n `INSERT INTO findings (id, session_id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json, detector_version)\n VALUES (@id, @session_id, @tool_call_id, @event_uuid, @turn_id, @rule_id, @category, @framework_ref, @severity, @title, @evidence, @signals_json, @detector_version)\n ON CONFLICT(id) DO NOTHING`,\n );\n const delScope = incremental\n ? \"DELETE FROM findings WHERE session_id IN (SELECT id FROM _dirty_sec)\"\n : \"DELETE FROM findings\";\n\n const tx = transaction(db, () => {\n db.exec(delScope);\n for (const row of rows) {\n const ctx = buildContext(row, projPath.get(row.session_id) ?? null, ownedConfigDirs);\n for (const rule of RULES) {\n if (!appliesTo(rule, ctx.toolName)) continue;\n const m = rule.test(ctx);\n if (!m) continue;\n let severity: Severity = m.severity ?? rule.baseSeverity;\n const mods: Record<string, unknown> = { ...(m.mods ?? {}) };\n // A failed/blocked tool call is an *attempt*, not an accomplished action — de-escalate one band.\n if (row.status === \"error\") {\n severity = bumpSeverity(severity, -1);\n mods.attempted = true;\n }\n const signals = {\n rule: rule.id,\n category: rule.category,\n framework_ref: rule.framework_ref,\n tool_name: ctx.toolName,\n base_severity: rule.baseSeverity,\n severity,\n status: row.status,\n modifiers: mods,\n detector_version: DETECTOR_VERSION,\n };\n insert.run({\n id: findingId(row.id, rule.id),\n session_id: row.session_id,\n tool_call_id: row.id,\n event_uuid: row.event_uuid,\n turn_id: row.turn_id,\n rule_id: rule.id,\n category: rule.category,\n framework_ref: rule.framework_ref,\n severity,\n title: rule.title,\n evidence: m.evidence,\n signals_json: JSON.stringify(signals),\n detector_version: DETECTOR_VERSION,\n });\n }\n }\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty_sec\");\n const count = (db.prepare(\"SELECT COUNT(*) n FROM findings\").get() as { n: number }).n;\n return { count, version: DETECTOR_VERSION };\n}\n","/**\n * Agent Lens — tool-error classification pass. Stamps `tool_calls.error_type` for every errored tool\n * call (`status='error'`) by running the deterministic core classifier (classifyToolError) over its\n * verbatim `result_summary`. Re-runnable and idempotent — like classify()/detect(), it re-derives from\n * signals already in the DB and yields identical rows every run.\n *\n * Why a stored column (not read-time): the sessions list filters by error type in SQL, and the\n * dashboard/detail aggregate on it — a column keeps one source of truth (the core classifier) while\n * letting SQL do the filtering/grouping. The raw `status='error'` count stays the authoritative signal;\n * `error_type` is the heuristic bucket (see packages/core/src/errors.ts for the authority boundary).\n */\nimport { transaction, classifyToolError, ERROR_CLASSIFIER_VERSION } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\n\n/**\n * (Re)classify errored tool calls into `error_type`. `dirty` (the expanded id set rebuildDerived\n * returns) scopes an incremental run to the touched sessions; null/undefined → reclassify everything.\n * Returns the number of rows stamped + the engine version.\n */\nexport function classifyErrors(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n const incremental = dirty != null;\n const scope = incremental ? \" AND session_id IN (SELECT id FROM _dirty_err)\" : \"\";\n\n if (dirty != null) createDirtySet(db, \"_dirty_err\", dirty);\n\n const rows = db\n .prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`)\n .all() as Array<{ id: string; result_summary: string | null }>;\n\n const update = db.prepare(\"UPDATE tool_calls SET error_type = ? WHERE id = ?\");\n const tx = transaction(db, () => {\n // Reset the scoped rows' error_type first so a row that stops being an error (or changes bucket) on\n // re-derive doesn't keep a stale label. Non-error rows always have NULL.\n db.exec(`UPDATE tool_calls SET error_type = NULL WHERE error_type IS NOT NULL${scope}`);\n for (const r of rows) update.run(classifyToolError(r.result_summary).type, r.id);\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty_err\");\n const count = (db.prepare(\"SELECT COUNT(*) n FROM tool_calls WHERE error_type IS NOT NULL\").get() as { n: number }).n;\n return { count, version: ERROR_CLASSIFIER_VERSION };\n}\n","/**\n * File-modification provenance (ADR-022, level 1) — derive `file_changes` rows from successful\n * Edit/Write/NotebookEdit tool calls' verbatim input_json.\n *\n * Deterministic and re-runnable, modeled on detect.ts: 0..N rows per tool call ⇒ incremental runs\n * delete-then-insert the dirty sessions' rows; null/undefined dirty ⇒ full rescan. Failed calls\n * (status='error') are skipped — a failed edit did not change the file. Only these tools are\n * covered: Bash-driven writes, deletions, and renames are deliberately out of scope here (they are\n * a lower confidence class; tracked-file deletions/renames arrive with the git-composed level 2 —\n * see the ADR).\n */\nimport { createHash } from \"node:crypto\";\nimport { posix as path } from \"node:path\";\nimport { transaction } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\n\nexport const FILECHANGES_VERSION = 1;\n\nconst FILE_TOOLS = [\"Edit\", \"Write\", \"NotebookEdit\"] as const;\n\n/** Deterministic row id — stable across runs so re-derivation reproduces identical rows. */\nfunction fileChangeId(toolCallId: string, filePath: string): string {\n return createHash(\"sha1\").update(toolCallId).update(\"\\0\").update(filePath).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Resolve a tool call's target path to a normalized absolute path, or null when unusable.\n * Absolute paths are posix-normalized (`/a/b/../c` → `/a/c`); relative paths resolve against the\n * session's project root when known (the same stance as detect.ts's outsideProjectPath) and are\n * skipped otherwise — a path we can't anchor would poison per-file grouping with false identities.\n */\nexport function normalizeFilePath(filePath: unknown, projectPath: string | null): string | null {\n if (typeof filePath !== \"string\" || !filePath) return null;\n if (filePath.startsWith(\"/\")) return path.normalize(filePath);\n if (!projectPath) return null;\n return path.normalize(projectPath.replace(/\\/$/, \"\") + \"/\" + filePath);\n}\n\n/** Count the lines a string spans (1 + newlines); 0 for the empty string. */\nfunction lineCount(s: unknown): number | null {\n if (typeof s !== \"string\") return null;\n if (s === \"\") return 0;\n let n = 1;\n for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) n++;\n return n;\n}\n\ninterface ToolRow {\n id: string;\n session_id: string;\n turn_id: string | null;\n event_uuid: string | null;\n tool_name: string;\n input_json: string | null;\n status: string | null;\n timestamp: string | null;\n}\n\n/**\n * (Re)derive tool-call file modifications into `file_changes`. Returns row count + version.\n * `dirty` (the expanded id set rebuildDerived returns) scopes an incremental run to the touched\n * sessions via delete-then-insert; null/undefined → derive everything.\n */\nexport function deriveFileChanges(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n const incremental = dirty != null;\n if (incremental) {\n db.exec(\"DROP TABLE IF EXISTS _dirty_fc\");\n db.exec(\"CREATE TEMP TABLE _dirty_fc (id TEXT PRIMARY KEY)\");\n const ins = db.prepare(\"INSERT OR IGNORE INTO _dirty_fc (id) VALUES (?)\");\n transaction(db, (ids: Iterable<string>) => {\n for (const id of ids) ins.run(id);\n })(dirty);\n }\n const scope = incremental ? \" AND tc.session_id IN (SELECT id FROM _dirty_fc)\" : \"\";\n\n // Project path + id per (scoped) session — to resolve relative paths and stamp project_id.\n const proj = new Map<string, { id: string; path: string }>();\n for (const r of db\n .prepare(\n `SELECT s.id id, p.id pid, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${\n incremental ? \" WHERE s.id IN (SELECT id FROM _dirty_fc)\" : \"\"\n }`,\n )\n .all() as Array<{ id: string; pid: string; path: string }>) {\n proj.set(r.id, { id: r.pid, path: r.path });\n }\n\n const rows = db\n .prepare(\n `SELECT tc.id, tc.session_id, tc.turn_id, tc.event_uuid, tc.tool_name, tc.input_json, tc.status, e.timestamp\n FROM tool_calls tc LEFT JOIN events e ON e.uuid = tc.event_uuid\n WHERE tc.tool_name IN (${FILE_TOOLS.map(() => \"?\").join(\",\")})${scope}`,\n )\n .all(...FILE_TOOLS) as unknown as ToolRow[];\n\n const insert = db.prepare(\n `INSERT INTO file_changes (id, tool_call_id, session_id, turn_id, event_uuid, project_id, file_path, tool_name, lines_added, lines_removed, timestamp, derive_version)\n VALUES (@id, @tool_call_id, @session_id, @turn_id, @event_uuid, @project_id, @file_path, @tool_name, @lines_added, @lines_removed, @timestamp, @derive_version)\n ON CONFLICT(id) DO NOTHING`,\n );\n const delScope = incremental\n ? \"DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _dirty_fc)\"\n : \"DELETE FROM file_changes\";\n\n const tx = transaction(db, () => {\n db.exec(delScope);\n for (const row of rows) {\n if (row.status === \"error\") continue; // a failed call did not change the file\n let input: any = null;\n try {\n input = row.input_json ? JSON.parse(row.input_json) : null;\n } catch {\n input = null;\n }\n if (!input) continue;\n const p = proj.get(row.session_id);\n const filePath = normalizeFilePath(input.file_path ?? input.notebook_path, p?.path ?? null);\n if (!filePath) continue;\n\n // Magnitude signal only, not a diff: newline-count deltas of the verbatim strings. Edit with\n // replace_all counts the strings once (occurrence count is unknowable from the input alone);\n // Write's prior content is unseen, so removed stays NULL; NotebookEdit carries no line info.\n let added: number | null = null;\n let removed: number | null = null;\n if (row.tool_name === \"Edit\") {\n added = lineCount(input.new_string);\n removed = lineCount(input.old_string);\n } else if (row.tool_name === \"Write\") {\n added = lineCount(input.content);\n }\n\n insert.run({\n id: fileChangeId(row.id, filePath),\n tool_call_id: row.id,\n session_id: row.session_id,\n turn_id: row.turn_id,\n event_uuid: row.event_uuid,\n project_id: p?.id ?? null,\n file_path: filePath,\n tool_name: row.tool_name,\n lines_added: added,\n lines_removed: removed,\n timestamp: row.timestamp,\n derive_version: FILECHANGES_VERSION,\n });\n }\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty_fc\");\n const count = (db.prepare(\"SELECT COUNT(*) n FROM file_changes\").get() as { n: number }).n;\n return { count, version: FILECHANGES_VERSION };\n}\n","/**\n * Canonical project roots (ADR-023) — fold cwd-grained project rows into real project roots.\n *\n * Claude Code stamps one project per distinct cwd, so a session (or a spawned subagent) run from a\n * repo SUBDIRECTORY mints a phantom \"project\" (`repo/packages/x`, `repo/evals`, …) that pollutes\n * every project filter. This re-runnable, global step resolves each project path to its canonical\n * root and merges the rows:\n *\n * 1. Nearest git root wins — walk up from the cwd (inclusive) looking for `.git` (dir OR file, so\n * worktrees count), never testing $HOME or above (a dotfiles repo in ~ must not swallow every\n * non-repo path). Nearest match keeps nested repos distinct: `ws/repo/sub` folds to `ws/repo`,\n * not to `ws`.\n * 2. Fallback: nearest OBSERVED ancestor project — for non-git workspace dirs (`ws/.local` → `ws`\n * when `ws` was itself a session cwd), resolved to that ancestor's own canonical root. Applies\n * when the path still exists on disk, OR when the project holds no main session (sidechain\n * only): for a vanished directory we cannot distinguish a deleted independent repo from a\n * workspace subdir, so a project a human actually opened (≥1 main session) keeps its identity —\n * while subagent-only landing spots (spawn cwds, deleted eval/worktree dirs) always fold.\n * 3. Neither → keep the raw cwd (includes $HOME itself and deleted human-opened projects).\n *\n * Merging repoints sessions.project_id (+ file_changes.project_id), refreshes the canonical row's\n * first/last_seen from its merged sessions, and deletes session-less project rows — which also\n * sweeps any true orphans out of the filter dropdowns. Deterministic given the host filesystem;\n * paths that don't exist on this machine simply never match rule 1.\n */\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { createHash } from \"node:crypto\";\nimport { dirname, join } from \"node:path\";\nimport { transaction } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\n\n/** Same derivation ingestFile uses (pipeline.ts), so a created canonical row is identical to one\n * that ingest would have minted for that cwd. */\nfunction projectId(agentId: string, path: string): string {\n return createHash(\"sha1\").update(`${agentId}\\0${path}`).digest(\"hex\").slice(0, 16);\n}\n\nconst strip = (p: string) => (p.length > 1 ? p.replace(/\\/$/, \"\") : p);\n\n/** Nearest ancestor (inclusive) containing `.git`, walking up to — but never testing — `home`.\n * Null when none is found before hitting home or the filesystem root. */\nfunction nearestGitRoot(path: string, home: string): string | null {\n let cur = strip(path);\n while (cur !== home && cur !== \"/\" && cur.length > 0) {\n if (existsSync(join(cur, \".git\"))) return cur;\n const next = dirname(cur);\n if (next === cur) break;\n cur = next;\n }\n return null;\n}\n\nexport interface CanonicalizeResult {\n merged: number; // project rows folded into another\n removed: number; // session-less project rows deleted\n}\n\n/**\n * Fold every project row into its canonical root (see module doc). Global and re-runnable on every\n * ingest — the whole table is a few dozen rows — so an already-ingested DB heals on its next run,\n * no --full needed.\n */\nexport function canonicalizeProjects(db: DB, opts: { homeDir?: string } = {}): CanonicalizeResult {\n const home = strip(opts.homeDir ?? homedir());\n const rows = db.prepare(\"SELECT id, agent_id, path FROM projects\").all() as Array<{\n id: string;\n agent_id: string;\n path: string;\n }>;\n const byPath = new Map<string, { id: string; agent_id: string }>();\n for (const r of rows) byPath.set(strip(r.path), { id: r.id, agent_id: r.agent_id });\n\n // Projects with at least one MAIN session — a human opened a session there, so a deleted dir\n // keeps its identity (see rule 2 in the module doc). Sidechain-only projects never block folding.\n const withMains = new Set<string>(\n (db.prepare(\"SELECT DISTINCT project_id FROM sessions WHERE is_sidechain = 0 AND project_id IS NOT NULL\").all() as Array<{ project_id: string }>).map(\n (r) => r.project_id,\n ),\n );\n\n // Resolve a path to its canonical root. Memoized; the ancestor fallback recurses on a strictly\n // shorter path, so it terminates.\n const memo = new Map<string, string>();\n function canon(path: string): string {\n const p = strip(path);\n const hit = memo.get(p);\n if (hit) return hit;\n memo.set(p, p); // provisional (self) — guards pathological cycles\n let out = p;\n if (p !== home) {\n const git = nearestGitRoot(p, home);\n if (git) {\n out = git;\n } else if (existsSync(p) || !withMains.has(byPath.get(p)?.id ?? \"\")) {\n // Rule 2: nearest observed ancestor project, resolved to ITS canonical root. Gated so a\n // DELETED dir folds only when no human ever opened a session there (sidechain-only) —\n // a deleted real repo must keep its identity, not fold into its parent.\n for (let cur = dirname(p); cur !== \"/\" && cur !== home && cur.length > 0; cur = dirname(cur)) {\n if (byPath.has(cur)) {\n out = canon(cur);\n break;\n }\n if (dirname(cur) === cur) break;\n }\n }\n }\n memo.set(p, out);\n return out;\n }\n\n let merged = 0;\n const upsertProject = db.prepare(\n `INSERT INTO projects (id, agent_id, path, encoded_dir, first_seen, last_seen)\n VALUES (?, ?, ?, NULL, NULL, NULL) ON CONFLICT(agent_id, path) DO NOTHING`,\n );\n const repointSessions = db.prepare(\"UPDATE sessions SET project_id = ? WHERE project_id = ?\");\n const repointFileChanges = db.prepare(\"UPDATE file_changes SET project_id = ? WHERE project_id = ?\");\n const touched = new Set<string>();\n\n transaction(db, () => {\n for (const r of rows) {\n const target = canon(r.path);\n if (target === strip(r.path)) continue;\n const targetId = byPath.get(target)?.id ?? projectId(r.agent_id, target);\n if (targetId === r.id) continue;\n // The canonical root may never have been a cwd itself (sessions only ever ran in subdirs) —\n // mint its row exactly as ingest would have.\n if (!byPath.has(target)) {\n upsertProject.run(targetId, r.agent_id, target);\n byPath.set(target, { id: targetId, agent_id: r.agent_id });\n }\n repointSessions.run(targetId, r.id);\n repointFileChanges.run(targetId, r.id);\n touched.add(targetId);\n merged++;\n }\n // Refresh merged roots' seen-range from their (now larger) session set.\n const refresh = db.prepare(\n `UPDATE projects SET\n first_seen = (SELECT MIN(started_at) FROM sessions WHERE project_id = projects.id),\n last_seen = (SELECT MAX(COALESCE(ended_at, started_at)) FROM sessions WHERE project_id = projects.id)\n WHERE id = ?`,\n );\n for (const id of touched) refresh.run(id);\n })();\n\n // Session-less rows — both the just-merged sources and any pre-existing orphans — leave the\n // filter dropdowns for good.\n const removed = Number(\n db\n .prepare(\"DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)\")\n .run().changes,\n );\n\n return { merged, removed };\n}\n","/**\n * Agent Lens — ingest engine (the DB-writing core of Stage 2), separated from the CLI bootstrap in\n * index.ts so it can be driven in-process by tests against an :memory: DB. index.ts handles argv/env,\n * the disk walk, and the incremental-skip check; everything here operates on an open DB + file content.\n */\nimport { createHash } from \"node:crypto\";\nimport type { StatementSync } from \"node:sqlite\";\nimport { packRaw, runNamed, transaction, type SourceAdapter, type SourceFile, type TurnRow } from \"@agent-lens/core\";\nimport { type DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\n\ntype Stmt = StatementSync;\n\nexport interface IngestStats {\n files: number;\n skipped: number;\n malformed: number;\n newEvents: number;\n}\n\nexport function newStats(): IngestStats {\n return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };\n}\n\nexport function preview(text: string): string {\n return text.replace(/\\s+/g, \" \").trim().slice(0, 140);\n}\n\nconst SKILL_INJECT_PREFIX = \"Base directory for this skill:\";\n\n/** Final name token of a skill id/dir, ignoring path and plugin prefix: \"plugin:foo\" / \".../foo\" → \"foo\". */\nfunction skillNameKey(name: string): string {\n const afterSlash = name.split(\"/\").pop() ?? name;\n const afterColon = afterSlash.split(\":\").pop() ?? afterSlash;\n return afterColon.trim();\n}\n\n/**\n * Parse a skill-body injection event. A Skill firing injects an isMeta user message of the shape\n * `Base directory for this skill: <abs path>/<name>\\n\\n<SKILL.md body>\\n\\nARGUMENTS: <args>`\n * into the transcript — the only place the real skill content appears. We split off the\n * Base-directory line (path differs per install) and the trailing ARGUMENTS block (per-call) so the\n * remaining body is stable across installs/args and can be content-hashed into a version.\n */\nexport function parseSkillInjection(\n text: string,\n): { baseDir: string | null; nameKey: string; body: string; args: string | null } | null {\n if (!text.startsWith(SKILL_INJECT_PREFIX)) return null;\n const firstNl = text.indexOf(\"\\n\");\n const firstLine = (firstNl >= 0 ? text.slice(0, firstNl) : text).slice(SKILL_INJECT_PREFIX.length).trim();\n const baseDir = firstLine || null;\n let body = firstNl >= 0 ? text.slice(firstNl + 1) : \"\";\n // Strip the trailing per-call ARGUMENTS block (use lastIndexOf — the body may mention \"ARGUMENTS\").\n let args: string | null = null;\n const argIdx = body.lastIndexOf(\"\\nARGUMENTS:\");\n if (argIdx >= 0) {\n args = body.slice(argIdx + \"\\nARGUMENTS:\".length).trim() || null;\n body = body.slice(0, argIdx);\n }\n return { baseDir, nameKey: baseDir ? skillNameKey(baseDir) : \"\", body: body.trim(), args };\n}\n\n/** First markdown heading (or first non-empty line) of a body, for list/detail display. */\nfunction skillSummary(body: string): string | null {\n for (const line of body.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n return (t.startsWith(\"#\") ? t.replace(/^#+\\s*/, \"\").trim() : t).slice(0, 200) || null;\n }\n return null;\n}\n\n/** Content-addressed skill version id: hash of name + normalized body (matches the project's sha1 id convention). */\nfunction skillVersionId(name: string, body: string): string {\n return createHash(\"sha1\").update(name).update(\"\\0\").update(body).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Remove every session whose project path is in `excludedPaths` — plus its subagent descendants —\n * along with all dependent rows (events → FTS via trigger, token_usage, tool_calls, turns,\n * classifications, findings) and the ingest_state for its files. This is what makes the global exclude list\n * take effect on the NEXT ingest, incremental or full: add a project and its data leaves the DB.\n * Matches projects.path exactly or by path-prefix. Returns the number of sessions pruned.\n */\nexport function pruneExcluded(db: DB, excludedPaths: string[]): number {\n if (!excludedPaths.length) return 0;\n const projects = db.prepare(\"SELECT id, path FROM projects\").all() as Array<{ id: string; path: string }>;\n const projIds = projects.filter((p) => excludedPaths.some((e) => p.path === e || p.path.startsWith(e + \"/\"))).map((p) => p.id);\n if (!projIds.length) return 0;\n\n const seed = db.prepare(`SELECT id FROM sessions WHERE project_id IN (${projIds.map(() => \"?\").join(\",\")})`).all(...projIds) as Array<{ id: string }>;\n createDirtySet(db, \"_prune\", seed.map((s) => s.id));\n // Transitive: subagents whose parent is being pruned (covers cross-project linkage). Fixpoint.\n const expand = db.prepare(\"INSERT OR IGNORE INTO _prune (id) SELECT id FROM sessions WHERE parent_session_id IN (SELECT id FROM _prune)\");\n const cnt = db.prepare(\"SELECT COUNT(*) n FROM _prune\");\n for (let prev = -1; ; ) {\n const n = (cnt.get() as { n: number }).n;\n if (n === prev) break;\n prev = n;\n expand.run();\n }\n const total = (cnt.get() as { n: number }).n;\n if (!total) {\n db.exec(\"DROP TABLE IF EXISTS _prune\");\n return 0;\n }\n 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() as Array<{ f: string }>).map((r) => r.f);\n\n // The toggle must stay OUTSIDE the transaction: `PRAGMA foreign_keys` is a silent no-op inside one,\n // so moving it in would disable nothing and raise no error. `finally` so a failed delete can't leave\n // the connection unguarded for every later pass in the process.\n db.exec(\"PRAGMA foreign_keys = OFF\");\n try {\n transaction(db, () => {\n db.exec(\"DELETE FROM findings WHERE session_id IN (SELECT id FROM _prune)\");\n db.exec(\"DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _prune)\");\n for (const t of [\"token_usage\", \"tool_calls\", \"turns\", \"events\"]) db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);\n db.exec(\"DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)\");\n db.exec(\"DELETE FROM sessions WHERE id IN (SELECT id FROM _prune)\");\n const delState = db.prepare(\"DELETE FROM ingest_state WHERE file_path = ?\");\n for (const f of files) delState.run(f);\n // Drop now-empty projects (an excluded project leaves no sessions behind).\n db.exec(\"DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)\");\n })();\n } finally {\n db.exec(\"PRAGMA foreign_keys = ON\");\n }\n db.exec(\"DROP TABLE IF EXISTS _prune\");\n return total;\n}\n\nfunction durationMs(start: string | null, end: string | null): number | null {\n if (!start || !end) return null;\n const a = Date.parse(start);\n const b = Date.parse(end);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.max(0, b - a);\n}\n\n/** Group a session's events into turns and return the turns + a uuid→turn_id map. */\nexport function buildTurns(\n sessionId: string,\n rows: Array<{ uuid: string; type: string; is_sidechain: number; is_meta: number; timestamp: string | null; model: string | null; text: string | null }>,\n): { turns: TurnRow[]; eventTurn: Map<string, string> } {\n rows.sort((x, y) => (x.timestamp ?? \"\").localeCompare(y.timestamp ?? \"\") || x.uuid.localeCompare(y.uuid));\n const turns: TurnRow[] = [];\n const eventTurn = new Map<string, string>();\n let cur: TurnRow | null = null;\n let seq = 0;\n\n for (const e of rows) {\n // A turn starts at a user prompt with real text. Sidechain (subagent) sessions are entirely\n // sidechain — verified no file mixes main+sidechain events — so their task prompt starts a turn\n // too; we don't filter on is_sidechain here. Tool-result carrier messages have empty text.\n const isPrompt = e.type === \"user\" && e.is_meta === 0 && !!e.text && e.text.trim().length > 0;\n if (isPrompt) {\n cur = {\n id: `${sessionId}:${seq}`,\n session_id: sessionId,\n seq,\n user_event_uuid: e.uuid,\n prompt_preview: preview(e.text!),\n model: null,\n started_at: e.timestamp,\n ended_at: e.timestamp,\n duration_ms: null,\n };\n turns.push(cur);\n seq++;\n }\n if (cur) {\n eventTurn.set(e.uuid, cur.id);\n if (e.model) cur.model = e.model;\n if (e.timestamp && (!cur.ended_at || e.timestamp > cur.ended_at)) cur.ended_at = e.timestamp;\n }\n }\n for (const t of turns) t.duration_ms = durationMs(t.started_at, t.ended_at);\n return { turns, eventTurn };\n}\n\n/**\n * Rebuild derived tables (turns, turn_id linkage, subagent parent linkage, session aggregates) from\n * the idempotent events/tool_calls tables.\n *\n * `dirty` is the set of session ids touched by this ingest run (incremental). When provided, only the\n * affected sessions are rebuilt — but the set is first expanded to its **linkage neighborhood** (the\n * subagent children spawned by a dirty session, and the parent session that spawned a dirty sidechain)\n * via a fixpoint, so cross-session parent/child links stay correct even when parent and child\n * transcripts arrive in different runs (ADR-010). When `dirty` is null/undefined (`--full`), every\n * session is rebuilt — the migration/reset path. Returns the expanded id set (or null for the full\n * path) so `classify()` can reuse it without re-expanding.\n */\n/** SQL fragments that scope every derived-data statement to the rebuild's session set: empty strings\n * for a full rebuild, else a restriction to the materialized `_dirty` temp table. */\ninterface Scope {\n bySession: string;\n byId: string;\n andId: string;\n andSelfId: string;\n}\n\n/** Materialize the dirty set, then grow it to the whole linkage neighborhood: a dirty parent pulls in\n * the children it spawned, and a dirty child pulls in its spawner parent. Loops to a fixpoint (covers\n * nested subagents), bounded by the session count. */\nfunction expandDirtyScope(db: DB, dirty: Set<string>): void {\n createDirtySet(db, \"_dirty\", dirty);\n const expand = db.prepare(`\n INSERT OR IGNORE INTO _dirty (id)\n SELECT spawned_session_id FROM tool_calls\n WHERE session_id IN (SELECT id FROM _dirty) AND spawned_session_id IS NOT NULL\n UNION\n SELECT session_id FROM tool_calls\n WHERE spawned_session_id IN (SELECT id FROM _dirty)\n UNION\n -- structural (path-based) workflow linkage: a dirty parent pulls in its subagent children,\n -- and a dirty child pulls in its structural parent, so the fallback link stays correct when\n -- parent and child transcripts arrive in different runs (ADR-010).\n SELECT id FROM sessions\n WHERE spawn_parent_id IN (SELECT id FROM _dirty)\n UNION\n SELECT spawn_parent_id FROM sessions\n WHERE id IN (SELECT id FROM _dirty) AND spawn_parent_id IS NOT NULL\n `);\n const count = db.prepare(\"SELECT COUNT(*) n FROM _dirty\");\n let prev = -1;\n for (;;) {\n const n = (count.get() as { n: number }).n;\n if (n === prev) break;\n prev = n;\n expand.run();\n }\n}\n\n/** Rebuild turns from the (idempotent) events table for the scoped sessions, then propagate the new\n * turn_ids to the dependent tables. Referencing turn_ids are nulled BEFORE the turns are deleted\n * (FK: events/token_usage/tool_calls/sessions -> turns), and skill-version links are cleared for the\n * scope so a changed/removed body can't leave a stale link. */\nfunction rebuildTurns(db: DB, scope: Scope, sessionIds: Array<{ id: string }>): void {\n const selEvents = db.prepare(\n \"SELECT uuid, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?\",\n );\n const insTurn = db.prepare(\n `INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)\n VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`,\n );\n const updEventTurn = db.prepare(\"UPDATE events SET turn_id = ? WHERE uuid = ?\");\n\n const tx = transaction(db, () => {\n for (const { id } of sessionIds) {\n const rows = selEvents.all(id) as any[];\n const { turns, eventTurn } = buildTurns(id, rows);\n for (const t of turns) runNamed(insTurn, t);\n for (const [uuid, turnId] of eventTurn) updEventTurn.run(turnId, uuid);\n }\n });\n tx();\n\n db.exec(`UPDATE token_usage SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = token_usage.event_uuid)${scope.bySession}`);\n db.exec(`UPDATE tool_calls SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = tool_calls.event_uuid)${scope.bySession}`);\n}\n\n/** Link each Skill tool_call to a content-addressed skill *version*. A firing injects the full\n * SKILL.md body as an isMeta user event right after the launch; we pair each Skill tool_call with\n * the following injection (by skill-name token, ARGUMENTS as tiebreak), normalize + hash the body,\n * UPSERT the version, and stamp tool_calls.skill_id. Firings without a captured body keep skill_id\n * NULL (skill_name stays set). Runs over the same scoped session set as the turn rebuild. */\nfunction linkSkillVersions(db: DB, sessionIds: Array<{ id: string }>): void {\n const selEventsOrdered = db.prepare(\n \"SELECT uuid, type, is_meta, timestamp, text FROM events WHERE session_id = ?\",\n );\n const selSkillCalls = db.prepare(\n \"SELECT id, event_uuid, skill_name, input_json FROM tool_calls WHERE session_id = ? AND tool_name = 'Skill'\",\n );\n const upsertSkill = db.prepare(\n `INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)\n VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @ts, @ts)\n ON CONFLICT(id) DO UPDATE SET\n last_seen = MAX(last_seen, excluded.last_seen),\n first_seen = MIN(first_seen, excluded.first_seen),\n base_dir = excluded.base_dir`,\n );\n const linkCall = db.prepare(\"UPDATE tool_calls SET skill_id = ? WHERE id = ?\");\n\n const linkTx = transaction(db, () => {\n for (const { id: sid } of sessionIds) {\n const calls = selSkillCalls.all(sid) as Array<{ id: string; event_uuid: string | null; skill_name: string | null; input_json: string | null }>;\n if (!calls.length) continue;\n const events = (selEventsOrdered.all(sid) as Array<{ uuid: string; type: string; is_meta: number; timestamp: string | null; text: string | null }>).sort(\n (x, y) => (x.timestamp ?? \"\").localeCompare(y.timestamp ?? \"\") || x.uuid.localeCompare(y.uuid),\n );\n const callByEvent = new Map<string, typeof calls>();\n for (const c of calls) {\n if (!c.event_uuid) continue;\n const arr = callByEvent.get(c.event_uuid) ?? [];\n arr.push(c);\n callByEvent.set(c.event_uuid, arr);\n }\n // Queue of pending Skill calls awaiting their body injection, keyed by skill-name token.\n const pending = new Map<string, Array<{ id: string; args: string | null }>>();\n for (const e of events) {\n const here = callByEvent.get(e.uuid);\n if (here) {\n for (const c of here) {\n if (!c.skill_name) continue;\n const key = skillNameKey(c.skill_name);\n let args: string | null = null;\n try {\n args = c.input_json ? (JSON.parse(c.input_json).args ?? null) : null;\n if (typeof args === \"string\") args = args.trim() || null;\n else if (args != null) args = JSON.stringify(args);\n } catch {\n /* keep null */\n }\n const arr = pending.get(key) ?? [];\n arr.push({ id: c.id, args });\n pending.set(key, arr);\n }\n }\n if (e.is_meta !== 1 || e.type !== \"user\" || !e.text) continue;\n const inj = parseSkillInjection(e.text);\n if (!inj) continue;\n const queue = pending.get(inj.nameKey);\n if (!queue || !queue.length) continue;\n // Prefer a call whose args match the injection's ARGUMENTS; else take the earliest pending.\n let idx = inj.args != null ? queue.findIndex((q) => q.args === inj.args) : -1;\n if (idx < 0) idx = 0;\n const [match] = queue.splice(idx, 1);\n const c = calls.find((x) => x.id === match.id)!;\n const name = c.skill_name!;\n const vid = skillVersionId(name, inj.body);\n upsertSkill.run({\n id: vid,\n name,\n base_dir: inj.baseDir,\n body: inj.body,\n summary: skillSummary(inj.body),\n body_bytes: Buffer.byteLength(inj.body, \"utf8\"),\n ts: e.timestamp,\n });\n linkCall.run(vid, match.id);\n }\n }\n });\n linkTx();\n}\n\n/** Attribute every subagent (sidechain) session to the parent turn/session that spawned it, in\n * three passes of decreasing precision — each later pass only fills what the earlier one left NULL. */\nfunction linkParents(db: DB, scope: Scope): void {\n // 1. The deterministic key: the spawning Task/Agent tool_call's spawned_session_id (== this session id).\n db.exec(`\n UPDATE sessions SET\n parent_session_id = (SELECT tc.session_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id),\n parent_turn_id = (SELECT tc.turn_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)\n WHERE EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)${scope.andSelfId}\n `);\n\n // 2. Workflow fan-out carries no toolUseResult.agentId, so the link above never fires for it\n // (finding #1). But the launching Workflow tool_call's result DOES carry a run id (wf_<id>), and the\n // run's subagents nest under …/subagents/workflows/<runId>/ — so sessions.workflow_run_id matches\n // tool_calls.workflow_run_id. That gives workflow agents BOTH their parent session and the exact\n // launching turn. (LIMIT 1: a run id maps to a single Workflow tool_call.)\n db.exec(`\n UPDATE sessions SET\n 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)),\n 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))\n WHERE sessions.workflow_run_id IS NOT NULL\n AND EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id)${scope.andSelfId}\n `);\n\n // 3. Final fallback for any sidechain still unlinked (e.g. its Workflow tool_call/runId wasn't\n // captured, or a non-workflow nested agent): attribute to the structural parent captured at ingest\n // from the transcript's directory location (<parent>/subagents/…) — the deterministic,\n // redaction-surviving signal. parent_turn_id stays NULL here. Tokens stay siloed → no double-count.\n db.exec(`\n UPDATE sessions SET parent_session_id = spawn_parent_id\n WHERE parent_session_id IS NULL\n AND spawn_parent_id IS NOT NULL\n AND spawn_parent_id IN (SELECT id FROM sessions)${scope.andSelfId}\n `);\n}\n\n/** Recompute the per-session aggregates (counts, wall-clock span, sidechain flag) from events/turns. */\nfunction recomputeSessionAggregates(db: DB, scope: Scope): void {\n db.exec(`\n UPDATE sessions SET\n event_count = (SELECT COUNT(*) FROM events e WHERE e.session_id = sessions.id),\n started_at = (SELECT MIN(timestamp) FROM events e WHERE e.session_id = sessions.id),\n ended_at = (SELECT MAX(timestamp) FROM events e WHERE e.session_id = sessions.id),\n turn_count = (SELECT COUNT(*) FROM turns t WHERE t.session_id = sessions.id),\n is_sidechain = (SELECT CASE WHEN MIN(is_sidechain) = 1 THEN 1 ELSE 0 END FROM events e WHERE e.session_id = sessions.id)${scope.byId}\n `);\n db.exec(`\n UPDATE sessions SET duration_ms =\n CAST((julianday(ended_at) - julianday(started_at)) * 86400000 AS INTEGER)\n WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${scope.andId}\n `);\n}\n\n/** Prune phantom sessions: any with zero events is not a real transcript. discover() walks every\n * *.jsonl under projects/, which sweeps up non-transcript files that happen to live there — e.g.\n * a Workflow tool's `journal.jsonl` (lines carry no `uuid`, so they yield no events). insSessionStub\n * still created a row (and they all share the basename → one phantom \"journal\" session). A zero-event\n * session has no events/turns/token_usage/tool_calls referencing it, so the delete is FK-safe.\n *\n * The dependent sweeps stay GLOBAL rather than scoped: classifications has no FK and is one row per\n * session, while detect() and deriveFileChanges run after this rebuild over the dirty scope only — so\n * a vanished non-dirty session's rows must be cleared here or they leak. */\nfunction pruneOrphans(db: DB, scope: Scope, incremental: boolean): void {\n db.exec(`DELETE FROM sessions WHERE event_count = 0${scope.andId}`);\n db.exec(\"DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)\");\n db.exec(\"DELETE FROM findings WHERE session_id NOT IN (SELECT id FROM sessions)\");\n db.exec(\"DELETE FROM file_changes WHERE session_id NOT IN (SELECT id FROM sessions)\");\n // Sweep skill versions no longer referenced by any tool_call. Full path only: an incremental run\n // only re-derives dirty sessions, so a version could still be referenced by a non-dirty session.\n if (!incremental) {\n db.exec(\"DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)\");\n }\n}\n\nexport function rebuildDerived(db: DB, dirty?: Set<string> | null): Set<string> | null {\n const incremental = dirty != null;\n const scope: Scope = {\n bySession: incremental ? \" WHERE session_id IN (SELECT id FROM _dirty)\" : \"\",\n byId: incremental ? \" WHERE id IN (SELECT id FROM _dirty)\" : \"\",\n andId: incremental ? \" AND id IN (SELECT id FROM _dirty)\" : \"\",\n andSelfId: incremental ? \" AND sessions.id IN (SELECT id FROM _dirty)\" : \"\",\n };\n\n if (dirty != null) expandDirtyScope(db, dirty);\n\n // Null referencing turn_ids BEFORE deleting turns (FK: events/token_usage/tool_calls/sessions -> turns).\n db.exec(`UPDATE events SET turn_id = NULL${scope.bySession}`);\n db.exec(`UPDATE token_usage SET turn_id = NULL${scope.bySession}`);\n db.exec(`UPDATE tool_calls SET turn_id = NULL${scope.bySession}`);\n // Clear skill-version links for the scope so changed/removed bodies don't leave a stale link.\n db.exec(`UPDATE tool_calls SET skill_id = NULL${scope.bySession}`);\n db.exec(`UPDATE sessions SET parent_turn_id = NULL${scope.byId}`);\n db.exec(`DELETE FROM turns${scope.bySession}`);\n\n const sessionIds = db.prepare(`SELECT id FROM sessions${scope.byId}`).all() as Array<{ id: string }>;\n\n rebuildTurns(db, scope, sessionIds);\n linkSkillVersions(db, sessionIds);\n linkParents(db, scope);\n recomputeSessionAggregates(db, scope);\n pruneOrphans(db, scope, incremental);\n\n if (incremental) {\n const expanded = new Set(\n (db.prepare(\"SELECT id FROM _dirty\").all() as Array<{ id: string }>).map((r) => r.id),\n );\n db.exec(\"DROP TABLE IF EXISTS _dirty\");\n return expanded;\n }\n return null;\n}\n\nexport interface IngestStatements {\n insAgent: Stmt;\n insSource: Stmt;\n getState: Stmt;\n setState: Stmt;\n insEvent: Stmt;\n insTokens: Stmt;\n insTool: Stmt;\n patchTool: Stmt;\n upsertProject: Stmt;\n insSessionStub: Stmt;\n upsertSession: Stmt;\n}\n\nexport function prepareStatements(db: DB): IngestStatements {\n return {\n insAgent: db.prepare(\"INSERT OR IGNORE INTO agents (id, name, kind) VALUES (?, ?, 'cli')\"),\n insSource: db.prepare(\n `INSERT INTO sources (id, label, agent_id, config_dir) VALUES (@id, @label, @agent_id, @config_dir)\n ON CONFLICT(id) DO UPDATE SET label=excluded.label, agent_id=excluded.agent_id, config_dir=excluded.config_dir`,\n ),\n getState: db.prepare(\"SELECT size, mtime_ms, sha256, events_ingested FROM ingest_state WHERE file_path = ?\"),\n setState: db.prepare(\n `INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)\n VALUES (@file_path, @size, @mtime_ms, @sha256, @events_ingested, @ingested_at)\n ON CONFLICT(file_path) DO UPDATE SET\n size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256,\n events_ingested=excluded.events_ingested, ingested_at=excluded.ingested_at`,\n ),\n insEvent: db.prepare(\n `INSERT INTO events (uuid, session_id, turn_id, parent_uuid, seq, type, role, timestamp, model, is_sidechain, is_meta, text, raw_json, source_file)\n VALUES (@uuid, @session_id, @turn_id, @parent_uuid, @seq, @type, @role, @timestamp, @model, @is_sidechain, @is_meta, @text, @raw_json, @source_file)\n ON CONFLICT(uuid) DO NOTHING`,\n ),\n insTokens: db.prepare(\n // Bare ON CONFLICT (no target) so the insert is skipped on EITHER the event_uuid PK or the\n // unique (session_id, message_id) index — the latter collapses a response's repeated\n // per-content-block usage lines into a single row.\n `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)\n VALUES (@event_uuid, @session_id, @turn_id, @message_id, @model, @input_tokens, @output_tokens, @cache_creation_input_tokens, @cache_read_input_tokens, @service_tier)\n ON CONFLICT DO NOTHING`,\n ),\n insTool: db.prepare(\n `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)\n 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)\n ON CONFLICT(id) DO NOTHING`,\n ),\n patchTool: db.prepare(\n `UPDATE tool_calls SET\n status = COALESCE(@status, status),\n agent_type = COALESCE(@agent_type, agent_type),\n spawned_session_id = COALESCE(@spawned_session_id, spawned_session_id),\n workflow_run_id = COALESCE(@workflow_run_id, workflow_run_id),\n workflow_name = COALESCE(@workflow_name, workflow_name),\n resolved_model = COALESCE(@resolved_model, resolved_model),\n total_duration_ms = COALESCE(@total_duration_ms, total_duration_ms),\n total_tokens = COALESCE(@total_tokens, total_tokens),\n total_tool_use_count = COALESCE(@total_tool_use_count, total_tool_use_count),\n result_summary = COALESCE(@result_summary, result_summary)\n WHERE id = @tool_use_id`,\n ),\n upsertProject: db.prepare(\n `INSERT INTO projects (id, agent_id, path, encoded_dir, first_seen, last_seen)\n VALUES (@id, @agent_id, @path, @encoded_dir, @now, @now)\n ON CONFLICT(id) DO UPDATE SET last_seen = @now, encoded_dir = COALESCE(excluded.encoded_dir, encoded_dir)`,\n ),\n insSessionStub: db.prepare(\"INSERT OR IGNORE INTO sessions (id, agent_id, source_id) VALUES (?, ?, ?)\"),\n upsertSession: db.prepare(\n `INSERT INTO sessions (id, agent_id, source_id, project_id, slug, ai_title, cli_version, entrypoint, git_branch, spawn_parent_id, workflow_run_id)\n VALUES (@id, @agent_id, @source_id, @project_id, @slug, @ai_title, @cli_version, @entrypoint, @git_branch, @spawn_parent_id, @workflow_run_id)\n ON CONFLICT(id) DO UPDATE SET\n source_id = COALESCE(excluded.source_id, source_id),\n project_id = COALESCE(excluded.project_id, project_id),\n slug = COALESCE(excluded.slug, slug),\n ai_title = COALESCE(excluded.ai_title, ai_title),\n cli_version = COALESCE(excluded.cli_version, cli_version),\n entrypoint = COALESCE(excluded.entrypoint, entrypoint),\n git_branch = COALESCE(excluded.git_branch, git_branch),\n spawn_parent_id = COALESCE(excluded.spawn_parent_id, spawn_parent_id),\n workflow_run_id = COALESCE(excluded.workflow_run_id, workflow_run_id)`,\n ),\n };\n}\n\n/**\n * Ingest one transcript file's lines in a single transaction: parse each line via the adapter,\n * insert events/token_usage/tool_calls (deduped by their keys), patch tool results, and upsert the\n * session + project metadata. `lines` is an iterable of newline-stripped lines — the caller chooses\n * whole-file read (small files) or a streaming reader (large files), so this engine never holds the\n * full file as one string. `meta` carries the disk stats recorded into ingest_state (tests pass\n * synthetic values). Derived tables (turns, linkage, aggregates) are rebuilt separately afterwards.\n */\nexport function ingestFile(\n db: DB,\n stmts: IngestStatements,\n adapter: SourceAdapter,\n file: SourceFile,\n lines: Iterable<string>,\n meta: { size: number; mtimeMs: number; hash: string },\n now: string,\n stats: IngestStats,\n): void {\n const sessionMeta: Record<string, any> = {};\n let eventsInFile = 0;\n\n const tx = transaction(db, () => {\n // Ensure the session row exists before any event references it (FK).\n stmts.insSessionStub.run(file.sessionId, adapter.agentId, file.sourceId);\n let seq = 0;\n for (const line of lines) {\n if (!line.trim()) continue;\n let raw: unknown;\n try {\n raw = JSON.parse(line);\n } catch {\n stats.malformed++;\n continue;\n }\n const parsed = adapter.parseLine(raw, file, seq++);\n if (parsed.meta) Object.assign(sessionMeta, parsed.meta);\n if (parsed.event) {\n // raw_json is stored gzip-compressed (ADR-011); compress at this single write chokepoint so\n // adapters stay agent-agnostic and emit a plain string.\n const info = stmts.insEvent.run({ ...parsed.event, raw_json: packRaw(parsed.event.raw_json) });\n if (info.changes > 0) {\n stats.newEvents++;\n eventsInFile++;\n }\n }\n if (parsed.tokenUsage) runNamed(stmts.insTokens, parsed.tokenUsage);\n if (parsed.toolCalls) for (const tc of parsed.toolCalls) runNamed(stmts.insTool, tc);\n if (parsed.toolResults)\n for (const tr of parsed.toolResults)\n stmts.patchTool.run({\n tool_use_id: tr.tool_use_id,\n status: tr.status ?? null,\n agent_type: tr.agent_type ?? null,\n spawned_session_id: tr.spawned_session_id ?? null,\n workflow_run_id: tr.workflow_run_id ?? null,\n workflow_name: tr.workflow_name ?? null,\n resolved_model: tr.resolved_model ?? null,\n total_duration_ms: tr.total_duration_ms ?? null,\n total_tokens: tr.total_tokens ?? null,\n total_tool_use_count: tr.total_tool_use_count ?? null,\n result_summary: tr.result_summary ?? null,\n });\n }\n\n // Persist session + project metadata (survives later skip runs).\n let projectId: string | null = null;\n if (sessionMeta.cwd) {\n projectId = createHash(\"sha1\").update(`${adapter.agentId}\\0${sessionMeta.cwd}`).digest(\"hex\").slice(0, 16);\n stmts.upsertProject.run({\n id: projectId,\n agent_id: adapter.agentId,\n path: sessionMeta.cwd,\n encoded_dir: file.encodedDir || null,\n now,\n });\n }\n stmts.upsertSession.run({\n id: file.sessionId,\n agent_id: adapter.agentId,\n source_id: file.sourceId,\n project_id: projectId,\n slug: sessionMeta.slug ?? null,\n ai_title: sessionMeta.ai_title ?? null,\n cli_version: sessionMeta.cli_version ?? null,\n entrypoint: sessionMeta.entrypoint ?? null,\n git_branch: sessionMeta.git_branch ?? null,\n spawn_parent_id: file.parentSessionId ?? null,\n workflow_run_id: file.workflowRunId ?? null,\n });\n\n stmts.setState.run({\n file_path: file.path,\n size: meta.size,\n mtime_ms: Math.trunc(meta.mtimeMs),\n sha256: meta.hash,\n events_ingested: eventsInFile,\n ingested_at: now,\n });\n });\n tx();\n}\n","/**\n * Agent Lens — Stage 2 ingest of Workflow-tool result sidecars.\n *\n * The Workflow tool writes a JSON sidecar next to the launching session when a run finishes:\n * <archive>/<source>/projects/<enc>/<sessionId>/workflows/wf_<id>.json\n * The transcript only carries the \"launched in background\" ack for an async run, so for those runs\n * this file is the ONLY record of how the run finished — status (completed/failed), summary, the\n * returned result payload, the model, phase structure, per-item logs, and roll-up\n * tokens/tool-calls/duration/agent-count. We project those fields into `workflow_results` (verbatim,\n * like tool_calls.input_json — the DB is local; redaction is an export concern). The big `script` and\n * `args` are intentionally NOT re-stored: they already live on the launching Workflow tool_call.\n *\n * Idempotent: keyed by run id, UPSERT on re-ingest. The discover/skip/state machinery is shared with\n * the other sidecar ingesters in `sidecar.ts`; only the parse + upsert below is workflow-specific.\n */\nimport { basename } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { ingestSidecars, newSidecarStats, sessionBefore, intOrNull, strOrNull, type SidecarStats } from \"./sidecar.js\";\n\nexport type WorkflowIngestStats = SidecarStats;\nexport const newWorkflowStats = newSidecarStats;\n\n/** Epoch-ms number or ISO/date string → ISO string (null when unparseable/absent). */\nfunction toIso(x: unknown): string | null {\n if (typeof x === \"number\" && Number.isFinite(x)) return new Date(x).toISOString();\n if (typeof x === \"string\" && x) {\n const t = Date.parse(x);\n return Number.isNaN(t) ? null : new Date(t).toISOString();\n }\n return null;\n}\n\nconst jsonOrNull = (v: unknown): string | null => (v == null ? null : JSON.stringify(v));\n\n/** Strip terminal styling the runner sometimes bakes into defaultModel — both real ANSI escapes\n * (`\\x1b[1m`) and the de-escaped literal form (`claude-opus-4-8[1m]`) — so the stored model is clean. */\nfunction cleanStr(v: unknown): string | null {\n const s = strOrNull(v);\n if (!s) return null;\n return s\n .replace(/\\x1b\\[[0-9;]*[A-Za-z]/g, \"\") // real ANSI escape sequences\n .replace(/\\[[0-9;]+m\\]?/g, \"\") // de-escaped leftovers: \"[1m\", \"[1m]\"\n .trim() || null;\n}\n\n/**\n * Ingest all workflow result sidecars for one source into `workflow_results`. `excludedDirs` are the\n * parsed exclude tokens (same filter the transcript walk uses). Returns nothing; mutates `stats`.\n */\nexport function ingestWorkflowResults(\n db: DB,\n sourceArchiveDir: string,\n sourceId: string,\n excludedDirs: string[],\n now: string,\n stats: WorkflowIngestStats,\n full: boolean,\n): void {\n const upsert = db.prepare(\n `INSERT INTO workflow_results\n (run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,\n result_json, phases_json, logs_json, progress_json,\n agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)\n VALUES\n (@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,\n @result_json, @phases_json, @logs_json, @progress_json,\n @agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)\n ON CONFLICT(run_id) DO UPDATE SET\n source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,\n workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,\n default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,\n logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,\n total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,\n duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,\n ingested_at=excluded.ingested_at`,\n );\n\n ingestSidecars(db, sourceArchiveDir, excludedDirs, now, stats, full, {\n subdir: \"workflows\",\n // Only the run's own result file (wf_<id>.json); agent-*.meta.json / scripts are handled elsewhere.\n matchName: (name) => /^wf_.+\\.json$/.test(name),\n handle: (path, buf) => {\n let d: Record<string, unknown>;\n try {\n const parsed = JSON.parse(buf.toString(\"utf8\"));\n if (!parsed || typeof parsed !== \"object\") return false;\n d = parsed as Record<string, unknown>;\n } catch {\n return false;\n }\n const runId = strOrNull(d.runId) ?? basename(path, \".json\");\n const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);\n const durationMs = intOrNull(d.durationMs);\n const endedAt = startedAt && durationMs != null ? new Date(Date.parse(startedAt) + durationMs).toISOString() : null;\n upsert.run({\n run_id: runId,\n source_id: sourceId,\n session_id: sessionBefore(path, \"workflows\"),\n task_id: strOrNull(d.taskId),\n workflow_name: strOrNull(d.workflowName),\n status: strOrNull(d.status),\n summary: strOrNull(d.summary),\n default_model: cleanStr(d.defaultModel),\n result_json: jsonOrNull(d.result),\n phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,\n logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,\n progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,\n agent_count: intOrNull(d.agentCount),\n total_tokens: intOrNull(d.totalTokens),\n total_tool_calls: intOrNull(d.totalToolCalls),\n duration_ms: durationMs,\n started_at: startedAt,\n ended_at: endedAt,\n ingested_at: now,\n });\n return true;\n },\n });\n}\n","/**\n * Agent Lens — shared Stage 2 sidecar-ingest machinery.\n *\n * The three sidecar ingesters (workflow results, subagent meta, spilled tool results) share the same\n * skeleton: recursively find files that live directly inside a named subdir (mirror first, then each\n * `.versions/<ts>/` snapshot), sort so the mirror wins on UPSERT, and run an incremental stat→hash\n * skip against `ingest_state` before handing each *changed* file to a per-sidecar handler. That\n * skeleton lives here once; each sidecar keeps only its own config: the subdir name, the filename\n * predicate, and the handler that parses + upserts one file.\n */\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { isExcludedArchivePath } from \"./redact.js\";\nimport { sha256 } from \"./fileread.js\";\n\n/** Counters every sidecar ingester reports (files seen / rows upserted / unchanged / unparseable). */\nexport interface SidecarStats {\n files: number;\n upserted: number;\n skipped: number;\n malformed: number;\n}\n\nexport function newSidecarStats(): SidecarStats {\n return { files: 0, upserted: 0, skipped: 0, malformed: 0 };\n}\n\nexport const intOrNull = (v: unknown): number | null => (typeof v === \"number\" && Number.isFinite(v) ? Math.trunc(v) : null);\nexport const strOrNull = (v: unknown): string | null => (typeof v === \"string\" && v ? v : null);\n\n/** The path segment immediately before `<subdir>/` (the launching/owning session id), or null. */\nexport function sessionBefore(path: string, subdir: string): string | null {\n const parts = path.split(\"/\");\n const i = parts.indexOf(subdir);\n return i > 0 ? parts[i - 1] : null;\n}\n\n/** Recursively collect files that sit directly inside a `<subdir>/` directory and match `matchName`. */\nfunction walk(dir: string, subdir: string, inSubdir: boolean, matchName: (name: string) => boolean, out: string[]): void {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n const p = join(dir, e.name);\n if (e.isDirectory()) walk(p, subdir, inSubdir || e.name === subdir, matchName, out);\n else if (e.isFile() && inSubdir && matchName(e.name)) out.push(p);\n }\n}\n\n/** All matching sidecar paths under a source archive (mirror first, then each `.versions` snapshot). */\nfunction discover(sourceArchiveDir: string, subdir: string, matchName: (name: string) => boolean): string[] {\n const out: string[] = [];\n walk(join(sourceArchiveDir, \"projects\"), subdir, false, matchName, out);\n try {\n for (const ts of readdirSync(join(sourceArchiveDir, \".versions\"), { withFileTypes: true }))\n if (ts.isDirectory()) walk(join(sourceArchiveDir, ts.name, \"projects\"), subdir, false, matchName, out);\n } catch {\n /* no versions yet */\n }\n return out;\n}\n\n/** Per-sidecar configuration for {@link ingestSidecars}. */\nexport interface SidecarConfig {\n /** The directory name whose direct children are sidecars (`workflows` / `subagents` / `tool-results`). */\n subdir: string;\n /** Filename predicate for a sidecar file. */\n matchName: (name: string) => boolean;\n /**\n * Optional pre-stat guard: return false to skip a discovered path entirely (no `files++`) — e.g. a\n * file outside the expected `…/<sessionId>/<subdir>/` layout.\n */\n include?: (path: string) => boolean;\n /**\n * Handle one *changed* file's bytes (`size` is the stat size, for a `bytes` column): parse + upsert.\n * Return true when a row was written (→ counted `upserted` and the file is marked ingested), or false\n * to count it `malformed` and leave it unmarked (so a persistently-bad file is retried, never\n * silently accepted).\n */\n handle: (path: string, buf: Buffer, size: number) => boolean;\n}\n\ninterface StateRow {\n size: number;\n mtime_ms: number;\n sha256: string;\n}\n\n/**\n * Discover, filter, sort, and incrementally skip sidecar files for one source, handing each changed\n * file to `cfg.handle`. Mutates `stats`. `full` forces a re-read (ignores the stat/hash short-circuit).\n */\nexport function ingestSidecars(\n db: DB,\n sourceArchiveDir: string,\n excludedDirs: string[],\n now: string,\n stats: SidecarStats,\n full: boolean,\n cfg: SidecarConfig,\n): void {\n let paths = discover(sourceArchiveDir, cfg.subdir, cfg.matchName);\n if (excludedDirs.length) paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));\n // Mirror last so it wins over any older .versions snapshot on UPSERT (a run that went\n // running→completed diverges: the completed copy is in the mirror).\n paths.sort((a, b) => Number(a.includes(\"/.versions/\")) - Number(b.includes(\"/.versions/\")) || (a < b ? -1 : 1));\n\n const getState = db.prepare(\"SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?\");\n const setState = db.prepare(\n `INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)\n VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)\n ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`,\n );\n\n for (const path of paths) {\n if (cfg.include && !cfg.include(path)) continue;\n stats.files++;\n const st = statSync(path);\n const mtimeMs = Math.trunc(st.mtimeMs);\n const prev = full ? undefined : (getState.get(path) as StateRow | undefined);\n if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {\n stats.skipped++;\n continue;\n }\n let buf: Buffer;\n try {\n buf = readFileSync(path);\n } catch {\n stats.malformed++;\n continue;\n }\n const hash = sha256(buf);\n if (prev && prev.sha256 === hash) {\n setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });\n stats.skipped++;\n continue;\n }\n if (cfg.handle(path, buf, st.size)) {\n setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });\n stats.upserted++;\n } else {\n stats.malformed++;\n }\n }\n}\n","/**\n * Agent Lens — metric-preserving transcript redactor (validation Layer 4).\n *\n * Produces a privacy-safe corpus that is realistic enough to validate the whole compute pipeline\n * end-to-end, by an explicit contract:\n *\n * PRESERVE (so metrics are identical to the raw input): uuid, parentUuid, type, timestamp,\n * isSidechain, isMeta, agentId (opaque hex, drives subagent linkage), message.{role,id,model,\n * usage,stop_reason}, toolUseResult numeric/linkage fields, tool_use {id,name}, and — crucially —\n * the *line count* of Edit/Write payloads + the *distinctness & extension* of file paths (these\n * feed the LoC / files complexity sub-scores).\n *\n * REDACT (deny-by-default): all message text/thinking, tool_result content, cwd & file paths\n * (pseudonymized, usernames stripped), slug/aiTitle/gitBranch, and any other free text. Stable\n * irreversible pseudonyms keep joins/counts intact (same input → same pseudonym).\n *\n * Token accounting, cost, durations, counts, subagent linkage, and the complexity *score* are\n * therefore identical between raw and redacted. The classifier *category* is text-derived and is\n * NOT preserved (validated separately by golden fixtures) — callers must exclude it from oracle\n * equality checks.\n *\n * HARD RULES (see memory test-corpus-redaction): never emit data from the agent-lens project\n * itself; scrub project names, paths, users, hosts, secrets. The CLI filters agent-lens sessions\n * and runs a final leak-scan before writing.\n */\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { parseSkillInjection } from \"./pipeline.js\";\n\nconst PLACEHOLDER = \"[redacted]\";\n\n/** Lines in a string, matching classify.ts/countLines exactly (trailing newline doesn't add a line). */\nfunction countLines(s: unknown): number {\n if (typeof s !== \"string\" || s.length === 0) return 0;\n const trimmed = s.endsWith(\"\\n\") ? s.slice(0, -1) : s;\n return trimmed.length === 0 ? 0 : trimmed.split(\"\\n\").length;\n}\n/** A dummy string whose countLines() == n (so LoC sub-scores are preserved). */\nfunction dummyLines(n: number): string {\n return n <= 0 ? \"\" : Array(n).fill(\"x\").join(\"\\n\");\n}\n\nexport class Redactor {\n private cache = new Map<string, string>();\n constructor(private salt: string = randomBytes(8).toString(\"hex\")) {}\n\n private hash(s: string, n = 6): string {\n return createHash(\"sha256\").update(this.salt).update(s).digest(\"hex\").slice(0, n);\n }\n private stable(kind: string, s: string): string {\n const key = `${kind}:${s}`;\n let v = this.cache.get(key);\n if (!v) {\n v = `${kind}-${this.hash(key)}`;\n this.cache.set(key, v);\n }\n return v;\n }\n\n /** Pseudonymize a path: strip the user's home, pseudonymize each segment, keep depth + extension. */\n path(p: unknown): string | null {\n if (typeof p !== \"string\" || !p) return typeof p === \"string\" ? p : null;\n const abs = p.startsWith(\"/\");\n const segs = p.split(\"/\").filter(Boolean);\n const out: string[] = [];\n for (let i = 0; i < segs.length; i++) {\n const seg = segs[i]!;\n // Drop the \"<user>\" right after a home root so usernames never survive.\n if ((segs[i - 1] === \"home\" || segs[i - 1] === \"Users\") && i >= 1) {\n out.push(\"user\");\n continue;\n }\n if (seg === \"home\" || seg === \"Users\") {\n out.push(seg);\n continue;\n }\n const last = i === segs.length - 1;\n const dot = last ? seg.lastIndexOf(\".\") : -1;\n if (dot > 0) out.push(`file-${this.hash(seg)}${seg.slice(dot)}`); // keep extension\n else out.push(this.stable(\"dir\", seg));\n }\n return (abs ? \"/\" : \"\") + out.join(\"/\");\n }\n\n /** Pseudonymize a free-form identifier (slug, label, branch). */\n ident(kind: string, s: unknown): string {\n return typeof s === \"string\" && s ? this.stable(kind, s) : `${kind}-none`;\n }\n\n /** Replace free text with a placeholder, PRESERVING emptiness (turn detection depends on it). */\n private text(s: unknown): string {\n return typeof s === \"string\" && s.length > 0 ? PLACEHOLDER : \"\";\n }\n\n /**\n * Preserve a skill-body injection (\"Base directory for this skill: …\\n\\n<SKILL.md body>\\n\\nARGUMENTS:…\")\n * instead of redacting it — the body is the only real skill content and drives version tracking.\n * We strip the per-call ARGUMENTS block, replace the home-path base-dir line with a non-leaking\n * `/skills/<name>` (keeping the skill-name tail so ingest still links it), keep the body verbatim,\n * and fail closed: if the result trips the leak scan, return null so the caller fully redacts it.\n * Returns null when `s` is not a skill injection.\n */\n private skillText(s: unknown): string | null {\n if (typeof s !== \"string\") return null;\n const inj = parseSkillInjection(s);\n if (!inj) return null;\n const nameTail = (inj.baseDir ?? \"\").split(\"/\").filter(Boolean).pop() ?? \"\";\n const rebuilt = `Base directory for this skill: /skills/${nameTail}\\n\\n${inj.body}\\n`;\n return findLeak(rebuilt) ? null : rebuilt;\n }\n\n private toolInput(name: string, input: unknown): Record<string, unknown> {\n if (!input || typeof input !== \"object\") return {};\n const inp = input as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n // Skill name (skill_name metric); only the Skill tool's command is non-sensitive enough to keep.\n if (typeof inp.skill === \"string\") out.skill = inp.skill;\n if (name === \"Skill\" && typeof inp.command === \"string\") out.command = inp.command;\n // Subagent type (agent_type metric).\n if ((name === \"Task\" || name === \"Agent\") && typeof inp.subagent_type === \"string\") out.subagent_type = inp.subagent_type;\n // LoC-bearing payloads: keep line counts + a pseudonymized path with its extension.\n if (name === \"Write\") {\n out.file_path = this.path(inp.file_path);\n out.content = dummyLines(countLines(inp.content));\n } else if (name === \"Edit\") {\n out.file_path = this.path(inp.file_path);\n out.old_string = dummyLines(countLines(inp.old_string));\n out.new_string = dummyLines(countLines(inp.new_string));\n }\n return out;\n }\n\n private block(b: unknown): unknown {\n if (!b || typeof b !== \"object\") return b;\n const blk = b as Record<string, unknown>;\n switch (blk.type) {\n case \"text\":\n return { type: \"text\", text: this.skillText(blk.text) ?? this.text(blk.text) };\n case \"thinking\":\n return { type: \"thinking\", thinking: this.text(blk.thinking) };\n case \"tool_use\":\n return { type: \"tool_use\", id: blk.id, name: blk.name, input: this.toolInput(String(blk.name), blk.input), ...(blk.caller ? { caller: blk.caller } : {}) };\n case \"tool_result\":\n return { type: \"tool_result\", tool_use_id: blk.tool_use_id, content: PLACEHOLDER, ...(blk.is_error ? { is_error: true } : {}) };\n default:\n return { type: blk.type };\n }\n }\n\n private message(m: unknown): unknown {\n if (!m || typeof m !== \"object\") return m;\n const msg = m as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const k of [\"role\", \"id\", \"model\", \"type\", \"usage\", \"stop_reason\", \"stop_sequence\"]) if (k in msg) out[k] = msg[k];\n if (typeof msg.content === \"string\") out.content = this.skillText(msg.content) ?? this.text(msg.content);\n else if (Array.isArray(msg.content)) out.content = msg.content.map((b: unknown) => this.block(b));\n return out;\n }\n\n /** Redact one parsed transcript line (object). */\n line(r: unknown): unknown {\n if (!r || typeof r !== \"object\") return r;\n const rec = r as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n // Structural / metric-bearing scalars kept verbatim.\n for (const k of [\"uuid\", \"parentUuid\", \"type\", \"timestamp\", \"isSidechain\", \"isMeta\", \"agentId\", \"userType\", \"version\"]) if (k in rec) out[k] = rec[k];\n if (typeof rec.cwd === \"string\") out.cwd = this.path(rec.cwd);\n if (typeof rec.gitBranch === \"string\") out.gitBranch = this.ident(\"branch\", rec.gitBranch);\n if (typeof rec.slug === \"string\") out.slug = this.ident(\"slug\", rec.slug);\n if (rec.type === \"ai-title\") out.aiTitle = \"Redacted session\";\n if (rec.toolUseResult && typeof rec.toolUseResult === \"object\") {\n const t = rec.toolUseResult as Record<string, unknown>;\n const tu: Record<string, unknown> = {};\n for (const k of [\"status\", \"agentType\", \"agentId\", \"resolvedModel\", \"totalDurationMs\", \"totalTokens\", \"totalToolUseCount\"]) if (k in t) tu[k] = t[k];\n out.toolUseResult = tu;\n }\n if (\"message\" in rec) out.message = this.message(rec.message);\n return out;\n }\n\n /** Redact a whole JSONL transcript (string in, string out). Malformed lines are dropped. */\n transcript(content: string): string {\n const lines: string[] = [];\n for (const raw of content.split(\"\\n\")) {\n if (!raw.trim()) continue;\n let obj: unknown;\n try {\n obj = JSON.parse(raw);\n } catch {\n continue;\n }\n lines.push(JSON.stringify(this.line(obj)));\n }\n return lines.join(\"\\n\") + \"\\n\";\n }\n}\n\n// Canonical Claude Code project-dir encoder now lives in core so collect + ingest agree exactly.\nexport { encodeProjectPath } from \"@agent-lens/core\";\nimport { encodeProjectPath } from \"@agent-lens/core\";\n// The fail-closed leak scan now lives in core (shared with the redacted-export sanitizer). Re-export\n// so this module's consumers (redact-cli.ts, tests) keep their `./redact.js` import unchanged.\nexport { LEAK_PATTERNS, findLeak } from \"@agent-lens/core\";\nimport { findLeak } from \"@agent-lens/core\";\n\n/** Parse the AGENT_LENS_REDACT_EXCLUDE CSV of real project paths into encoded-dir prefixes. */\nexport function parseExcludes(csv: string | undefined | null): string[] {\n return (csv ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n .map(encodeProjectPath);\n}\n\n/** True if `encodedDir` is (or sits under) any excluded project path — used to keep data out of the corpus. */\nexport function isExcludedDir(encodedDir: string, excluded: string[]): boolean {\n return excluded.some((e) => encodedDir === e || encodedDir.startsWith(e + \"-\"));\n}\n\n/**\n * True if an archive file path belongs to an excluded project. Matches the `/projects/<encodedDir>/`\n * segment, so it catches both the main transcript AND its nested `<UUID>/subagents/agent-*.jsonl`.\n * `excluded` is the encoded-dir list from parseExcludes().\n */\nexport function isExcludedArchivePath(filePath: string, excluded: string[]): boolean {\n return excluded.some((e) => filePath.includes(`/projects/${e}/`));\n}\n","/**\n * Agent Lens — transcript file reading (ADR-010, impact 4).\n *\n * Small files are read whole (fast path); files larger than STREAM_THRESHOLD are streamed in fixed\n * chunks so ingest memory stays bounded regardless of transcript size. The streaming SHA-256 is\n * byte-identical to hashing a whole-file `readFileSync` buffer, so the incremental-skip check is\n * unaffected by which path a file takes. Kept separate from the CLI bootstrap so it is unit-testable.\n */\nimport { createHash } from \"node:crypto\";\nimport { openSync, readSync, closeSync } from \"node:fs\";\nimport { StringDecoder } from \"node:string_decoder\";\n\nexport const STREAM_THRESHOLD = 8 * 1024 * 1024;\nconst CHUNK = 64 * 1024;\n\n/** SHA-256 hex of a buffer. */\nexport function sha256(buf: Buffer): string {\n return createHash(\"sha256\").update(buf).digest(\"hex\");\n}\n\n/** SHA-256 hex of a file's bytes, computed by streaming. Equals sha256(readFileSync(path)). */\nexport function sha256File(path: string): string {\n const fd = openSync(path, \"r\");\n const h = createHash(\"sha256\");\n const buf = Buffer.allocUnsafe(CHUNK);\n try {\n let n: number;\n while ((n = readSync(fd, buf, 0, CHUNK, null)) > 0) h.update(buf.subarray(0, n));\n } finally {\n closeSync(fd);\n }\n return h.digest(\"hex\");\n}\n\n/**\n * Lazily yield a file's lines (newline-stripped) by streaming. A StringDecoder handles multibyte\n * characters split across chunk boundaries. Holds at most one line at a time, never the whole file.\n * Yields the same sequence as `readFileSync(path).toString(\"utf8\").split(\"\\n\")` minus the trailing\n * empty element a final newline would produce (ingest skips blank lines anyway).\n */\nexport function* streamLines(path: string): Generator<string> {\n const fd = openSync(path, \"r\");\n const decoder = new StringDecoder(\"utf8\");\n const buf = Buffer.allocUnsafe(CHUNK);\n let leftover = \"\";\n try {\n let n: number;\n while ((n = readSync(fd, buf, 0, CHUNK, null)) > 0) {\n leftover += decoder.write(buf.subarray(0, n));\n let idx: number;\n while ((idx = leftover.indexOf(\"\\n\")) !== -1) {\n yield leftover.slice(0, idx);\n leftover = leftover.slice(idx + 1);\n }\n }\n leftover += decoder.end();\n if (leftover.length > 0) yield leftover;\n } finally {\n closeSync(fd);\n }\n}\n","/**\n * Agent Lens — Stage 2 ingest of per-subagent metadata sidecars.\n *\n * When a subagent (Task/Agent, or a Workflow fan-out agent) runs, Claude Code writes a metadata\n * sidecar next to its transcript:\n * <archive>/<source>/projects/<enc>/<sessionId>/subagents/agent-<id>.meta.json\n * carrying { agentType, description, spawnDepth?, toolUseId }. This is the authoritative source of a\n * subagent's type and human title — Workflow fan-out agents carry no subagent_type on the launching\n * tool_call, so ~700 of them are otherwise untyped. The filename stem `agent-<id>` IS the subagent's\n * session id (the same id `discover()` assigns the transcript), so we key `session_meta` on it and\n * LEFT JOIN at read time (no dependency on the session row existing yet).\n *\n * Idempotent: keyed by session id, UPSERT on re-ingest. The discover/skip/state machinery is shared\n * with the other sidecar ingesters in `sidecar.ts`; only the parse + upsert below is meta-specific.\n */\nimport { basename } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { ingestSidecars, newSidecarStats, intOrNull, strOrNull, type SidecarStats } from \"./sidecar.js\";\n\nexport type MetaIngestStats = SidecarStats;\nexport const newMetaStats = newSidecarStats;\n\n/**\n * Ingest all subagent meta sidecars for one source into `session_meta`. `excludedDirs` are the parsed\n * exclude tokens (same filter the transcript walk uses). Returns nothing; mutates `stats`.\n */\nexport function ingestSubagentMeta(\n db: DB,\n sourceArchiveDir: string,\n sourceId: string,\n excludedDirs: string[],\n now: string,\n stats: MetaIngestStats,\n full: boolean,\n): void {\n const upsert = db.prepare(\n `INSERT INTO session_meta\n (session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)\n VALUES\n (@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)\n ON CONFLICT(session_id) DO UPDATE SET\n source_id=excluded.source_id, agent_type=excluded.agent_type,\n agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,\n tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`,\n );\n\n ingestSidecars(db, sourceArchiveDir, excludedDirs, now, stats, full, {\n subdir: \"subagents\",\n matchName: (name) => /^agent-.+\\.meta\\.json$/.test(name),\n handle: (path, buf) => {\n let d: Record<string, unknown>;\n try {\n const parsed = JSON.parse(buf.toString(\"utf8\"));\n if (!parsed || typeof parsed !== \"object\") return false;\n d = parsed as Record<string, unknown>;\n } catch {\n return false;\n }\n // Filename stem is the subagent's session id: `agent-<id>.meta.json` → `agent-<id>`.\n upsert.run({\n session_id: basename(path, \".meta.json\"),\n source_id: sourceId,\n agent_type: strOrNull(d.agentType),\n agent_description: strOrNull(d.description),\n spawn_depth: intOrNull(d.spawnDepth),\n tool_use_id: strOrNull(d.toolUseId),\n ingested_at: now,\n });\n return true;\n },\n });\n}\n","/**\n * Agent Lens — Stage 2 ingest of spilled full tool-result files.\n *\n * When a tool's output is too large for the transcript, Claude Code writes the full output to disk and\n * keeps only a short summary in the transcript naming the file:\n * <archive>/<source>/projects/<enc>/<sessionId>/tool-results/<name>.txt\n * (the summary reads \"Output too large (…). Full output saved to: …/tool-results/<name>.txt\"). The\n * basename <name> is NOT the tool_use id — it's a short random token — so we key `tool_results` on\n * (session_id, name) and join at read time by parsing that marker out of tool_calls.result_summary.\n *\n * Idempotent: keyed by (session_id, name), UPSERT on re-ingest. The discover/skip/state machinery is\n * shared with the other sidecar ingesters in `sidecar.ts`; only the upsert below is result-specific.\n */\nimport { basename } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { ingestSidecars, newSidecarStats, sessionBefore, type SidecarStats } from \"./sidecar.js\";\n\nexport type ToolResultIngestStats = SidecarStats;\nexport const newToolResultStats = newSidecarStats;\n\n/**\n * Ingest all spilled tool-result files for one source into `tool_results`. `excludedDirs` are the\n * parsed exclude tokens (same filter the transcript walk uses). Returns nothing; mutates `stats`.\n */\nexport function ingestToolResults(\n db: DB,\n sourceArchiveDir: string,\n excludedDirs: string[],\n now: string,\n stats: ToolResultIngestStats,\n full: boolean,\n): void {\n const upsert = db.prepare(\n `INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)\n VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)\n ON CONFLICT(session_id, name) DO UPDATE SET\n path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`,\n );\n\n ingestSidecars(db, sourceArchiveDir, excludedDirs, now, stats, full, {\n subdir: \"tool-results\",\n matchName: (name) => name.endsWith(\".txt\"),\n // Skip a file outside the expected …/<sessionId>/tool-results/ layout (no files++, matching the\n // original early-continue before statting).\n include: (path) => sessionBefore(path, \"tool-results\") !== null,\n handle: (path, buf, size) => {\n const sessionId = sessionBefore(path, \"tool-results\")!; // include() guaranteed non-null\n upsert.run({\n session_id: sessionId,\n name: basename(path, \".txt\"),\n path,\n bytes: size,\n text: buf.toString(\"utf8\"),\n ingested_at: now,\n });\n return true;\n },\n });\n}\n","/**\n * Agent Lens — Stage 3 local server, as an importable function (ADR-005: 127.0.0.1 only, no egress).\n *\n * `startServer` is the library entrypoint so the unified `agent-lens` CLI can bundle and call it\n * in-process; the thin bin in index.ts just forwards to it. Read-only REST over the SQLite store +\n * Markdown export, and serves the built web SPA. (env: AGENT_LENS_DB, AGENT_LENS_PORT, AGENT_LENS_HOST)\n */\nimport { existsSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { findRepoRoot, installPricing, resolveDbPath, resolveServerConfig, resolveWebDist, triageDbFor } from \"@agent-lens/core\";\nimport { openReadonly } from \"./db.js\";\nimport { createApp } from \"./app.js\";\n\n// Re-exported for the unified CLI (bundles this package): the read-only DB open + the shared\n// session-export helpers back the `agent-lens export` command without a second server process.\nexport { openReadonly } from \"./db.js\";\nexport { renderSessionExport, parseRedactionLevel } from \"./export.js\";\n\n/** Per-invocation overrides (from `serve --port/--host/--db`); each takes precedence over env + file. */\nexport interface StartServerOverrides {\n port?: number | string;\n host?: string;\n db?: string;\n}\n\nexport async function startServer(overrides: StartServerOverrides = {}): Promise<void> {\n // Resolve relative to this module: in dev it's packages/server/dist; bundled into the CLI it's the\n // published package root (web SPA sits at ../web, data falls back to the per-user dir).\n const here = dirname(fileURLToPath(import.meta.url));\n const repoRoot = findRepoRoot(here);\n // db/port/host all resolve with precedence flag > env > file > default (bad port fails fast).\n const { path: dbPath, origin: dbOrigin } = resolveDbPath(overrides.db);\n const { host, port } = resolveServerConfig({ port: overrides.port, host: overrides.host });\n // Install the effective price table before anything derives a cost (ADR-028). Malformed overrides\n // warn here and fall back to the built-in rate; the resolved facts go to /api/about.\n const pricing = installPricing();\n const webDist = resolveWebDist(here, repoRoot);\n\n if (host !== \"127.0.0.1\" && host !== \"localhost\" && !process.env.AGENT_LENS_ALLOW_NONLOCAL) {\n console.error(\n `agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). ` +\n `Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`,\n );\n process.exit(1);\n }\n if (!existsSync(dbPath)) {\n console.error(`agent-lens-server: db not found: ${dbPath} (run ingest first)`);\n process.exit(1);\n }\n\n // Triage store (ADR-018) sits beside the analytics DB and is never touched by ingest, so user triage\n // survives `ingest --full`. Opened read-write by createApp; the analytics handle stays read-only.\n const triageDbPath = triageDbFor(dbPath);\n\n const db = openReadonly(dbPath);\n // Enforce a loopback Host allowlist (DNS-rebinding defense) whenever we're bound to loopback. An\n // intentional non-loopback bind (host set + AGENT_LENS_ALLOW_NONLOCAL, checked above) opts out.\n const enforceLoopbackHost = host === \"127.0.0.1\" || host === \"localhost\";\n // /api/about reports what this process actually resolved (ADR-027) — pass the facts down rather\n // than letting it re-resolve, or `serve --db /custom.db` would report the default path instead.\n const app = await createApp(db, {\n webDist,\n triageDbPath,\n enforceLoopbackHost,\n about: { db: { path: dbPath, origin: dbOrigin }, host, port, loopbackOnly: enforceLoopbackHost, repoRoot, pricing },\n });\n\n try {\n await app.listen({ host, port });\n console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);\n if (!existsSync(webDist)) console.log(\" note: web SPA not built — run `pnpm --filter @agent-lens/web build`\");\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport { SCHEMA_VERSION, unpackRaw, severityRank, activePricing, SECURITY_CATEGORIES, errorKind, type ToolErrorType, type Rate } from \"@agent-lens/core\";\nimport type {\n Finding,\n FindingListRow,\n FileChangeRow,\n FileSummary,\n FileTimeline,\n FileTimelineSession,\n FilesPage,\n EventNode,\n Classification,\n ClassificationSignals,\n Project,\n SecuritySummary,\n SessionChild,\n SessionDetail,\n SessionDetailData,\n SessionParent,\n SessionRow,\n SessionSummary,\n SessionsPage,\n SkillDetail,\n SkillSummary,\n SkillSession,\n SkillVersion,\n Source,\n ToolCall,\n TurnRow,\n WorkflowAgent,\n WorkflowCompletion,\n WorkflowDetail,\n WorkflowRun,\n WorkflowRunResult,\n FindingsPage,\n} from \"@agent-lens/contracts\";\nimport { tableExists, queryAll, queryGet, orderBy, pushGrouped, pushDateRange, metaJoin, metaProjection } from \"./sql-util.js\";\nimport type {\n CategoryRow,\n ToolCallProjection,\n ClassificationProjectionRow,\n CountRow,\n EventProjectionRow,\n FindingProjectionRow,\n FileTimelineRow,\n ModelRow,\n RawJsonRow,\n SessionChildRow,\n SessionListRow,\n SessionParentRow,\n SkillListRow,\n SkillSessionRow,\n ToolResultRow,\n WorkflowAgentRow,\n WorkflowLaunchRow,\n WorkflowResultRow,\n} from \"./rows.js\";\nimport { sessionUsage, splitOf, tokensOf, costOf, unpricedOf, USAGE_SUMS, type UsageRow } from \"./usage.js\";\n\nexport type DB = DatabaseSync;\n\nexport function openReadonly(file: string): DB {\n // `readOnly` also supplies the old `fileMustExist`: node:sqlite refuses to create a DB it may not\n // write. `query_only` is belt-and-braces on top — it also blocks writes through an ATTACHed handle.\n const db = new DatabaseSync(file, { readOnly: true });\n db.exec(\"PRAGMA query_only = ON\");\n return db;\n}\n\n/**\n * Compare the DB's stamped schema version against the version this server build expects. `stale` means\n * the on-disk DB was written by an older schema (a bump requires `agent-lens ingest --full` to rebuild)\n * — the UI surfaces this so a mismatch doesn't silently show missing/empty columns. `db_version` is null\n * on an unstamped/never-ingested DB (treated as not-stale: nothing to warn about yet).\n */\nexport function schemaStatus(db: DB): { db_version: number | null; expected: number; stale: boolean } {\n const row = db.prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\").get() as\n | { value: string }\n | undefined;\n const dbVersion = row ? Number(row.value) : null;\n const db_version = dbVersion != null && Number.isFinite(dbVersion) ? dbVersion : null;\n return { db_version, expected: SCHEMA_VERSION, stale: db_version != null && db_version !== SCHEMA_VERSION };\n}\n\n/**\n * Wall-clock time of the most recent ingest run that wrote anything, as an ISO8601 string (or null on\n * a never-ingested DB). Every archive file touched by a run gets the same `ingested_at` stamp, so the\n * max across `ingest_state` is exactly \"data last refreshed at\". Powers the header freshness readout.\n */\nexport function lastIngested(db: DB): string | null {\n const row = db.prepare(\"SELECT MAX(ingested_at) AS last FROM ingest_state\").get() as { last: string | null };\n return row?.last ?? null;\n}\n\n/**\n * Split a raw transcript line's message content into natural text vs thinking. Accepts the stored\n * `events.raw_json` value, which is a gzip BLOB (Uint8Array) post-ADR-011 but may be a legacy plain string;\n * `unpackRaw` normalizes both.\n */\nexport function extractParts(rawJson: string | Uint8Array): { text: string | null; thinking: string | null } {\n let text = \"\";\n let thinking = \"\";\n try {\n const content = JSON.parse(unpackRaw(rawJson))?.message?.content;\n if (typeof content === \"string\") text = content;\n else if (Array.isArray(content)) {\n for (const b of content) {\n if (b?.type === \"text\" && typeof b.text === \"string\") text += (text ? \"\\n\" : \"\") + b.text;\n else if (b?.type === \"thinking\" && typeof b.thinking === \"string\")\n thinking += (thinking ? \"\\n\" : \"\") + b.thinking;\n }\n }\n } catch {\n /* ignore */\n }\n return { text: text || null, thinking: thinking || null };\n}\n\nexport interface SessionFilters {\n source?: string;\n project?: string;\n model?: string;\n q?: string;\n from?: string;\n to?: string;\n kind?: \"main\" | \"subagent\";\n /** Keep only sessions that have ≥1 finding of one of these severities. */\n severity?: string[];\n /** Keep only sessions that have ≥1 errored tool call of one of these error types. */\n errorType?: string[];\n /** Column to sort the whole filtered list by (before pagination). Defaults to \"started\". */\n sort?: \"started\" | \"title\" | \"turns\" | \"tokens\" | \"cost\" | \"duration\" | \"errors\" | \"security\";\n dir?: \"asc\" | \"desc\";\n limit: number;\n offset: number;\n}\n\nexport function listSources(db: DB): Source[] {\n // Count MAIN sessions only (is_sidechain = 0): the dropdown filters the session list, which\n // defaults to main-only, so a source's count must match what you'd see — not be inflated by the\n // many subagent sidechains each task spawns.\n return queryAll<Source>(\n db,\n `SELECT s.id, s.label,\n (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count\n FROM sources s ORDER BY s.label`,\n );\n}\n\nexport function listProjects(db: DB): Project[] {\n return queryAll<Project>(\n db,\n `SELECT p.id, p.path,\n (SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count\n FROM projects p ORDER BY session_count DESC, p.path`,\n );\n}\n\nexport function listModels(db: DB): string[] {\n return queryAll<ModelRow>(db, `SELECT DISTINCT model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).map(\n (r) => r.model,\n );\n}\n\n/**\n * Turn a user's search box input into a safe FTS5 MATCH expression. The raw string is otherwise\n * parsed as FTS5 *query syntax*, so a hyphen, colon (column filter), or bareword operator throws\n * (e.g. \"swe-workflow\" → `SQLITE_ERROR: no such column: workflow`). We treat the input as literal\n * terms: split on whitespace and wrap each token in double quotes (doubling embedded quotes per\n * FTS5 escaping), joined by space (implicit AND). Phrasing also lets the tokenizer split intra-token\n * punctuation, so \"swe-workflow\" matches the adjacent tokens \"swe workflow\". Returns \"\" for input\n * with no usable tokens (caller guards on a non-empty trimmed query). */\nfunction toFtsQuery(raw: string): string {\n return raw\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n .join(\" \");\n}\n\n/** Total tokens for one session (all four classes), matching `tokensOf(splitOf(…))`. */\nconst TOKENS_SORT_SQL = `(SELECT COALESCE(SUM(t.input_tokens + t.output_tokens\n + t.cache_creation_input_tokens + t.cache_read_input_tokens), 0)\n FROM token_usage t WHERE t.session_id = s.id)`;\n\n/**\n * Security ordering: worst severity first, finding count as the tiebreak — the same metric the JS\n * sort computed as `(severityRank(worst) + 1) * 1e6 + finding_count`. The `+ 1` shift is why an\n * unranked/absent severity is 0 here (`severityRank` returns -1 for it), matching SEVERITY_ORDER.\n */\nconst SECURITY_SORT_SQL = `(CASE worst_severity\n WHEN 'critical' THEN 5 WHEN 'high' THEN 4 WHEN 'medium' THEN 3 WHEN 'low' THEN 2 WHEN 'info' THEN 1\n ELSE 0 END) * 1000000 + finding_count`;\n\n/**\n * Cost for one session, in SQL — a transcription of `costForUsage` (rate per 1M tokens, per class)\n * that lets the session list ORDER BY cost without costing every matching session in JS first.\n *\n * The rates it joins come from {@link ratesCte}, never from a literal table: pricing is\n * config-overridable (ADR-028), so the active table is the only correct source. The join reproduces\n * `rateForModel`'s **longest-prefix** match — an equality join would price a dated id like\n * `claude-haiku-4-5-20251001` at zero, which is precisely the silent-$0 failure ADR-028 was cut to\n * fix. `substr` rather than `LIKE` because `_` is a LIKE wildcard and model ids may contain one.\n *\n * This duplicates the cost formula (the other copy is `costForUsage`); `pricing-sql.test.ts` asserts\n * the two agree, including under an override.\n */\nconst COST_SORT_SQL = `(SELECT COALESCE(SUM((tu.input_tokens * mr.i_rate + tu.output_tokens * mr.o_rate\n + tu.cache_creation_input_tokens * mr.cw_rate + tu.cache_read_input_tokens * mr.cr_rate) / 1000000.0), 0)\n FROM token_usage tu LEFT JOIN model_rate mr ON mr.model IS tu.model\n WHERE tu.session_id = s.id)`;\n\n/**\n * A `WITH rates(…)` clause carrying the *active* price table (built-in defaults merged with any\n * config overrides) as bound parameters. Built per query, never cached at module scope — `usePricing`\n * can swap the table at startup and tests install their own.\n */\nexport function ratesCte(): { sql: string; params: unknown[] } {\n const entries = Object.entries(activePricing()) as Array<[string, Rate]>;\n return {\n // `model_rate` resolves the prefix match ONCE per distinct model id, not once per token_usage row.\n // Inlining the lookup into the cost expression instead re-ran it for every usage row of every\n // candidate session — ~35× slower on a real corpus, which would have made this \"fix\" a regression.\n sql: `WITH rates(prefix, i_rate, o_rate, cw_rate, cr_rate) AS (VALUES ${entries.map(() => \"(?,?,?,?,?)\").join(\",\")}),\n model_rate AS (\n SELECT m.model, r.i_rate, r.o_rate, r.cw_rate, r.cr_rate\n FROM (SELECT DISTINCT model FROM token_usage) m\n LEFT JOIN rates r ON r.prefix = (\n SELECT p.prefix FROM rates p WHERE substr(m.model, 1, length(p.prefix)) = p.prefix\n ORDER BY length(p.prefix) DESC LIMIT 1)) `,\n params: entries.flatMap(([prefix, r]) => [prefix, r.input, r.output, r.cacheWrite, r.cacheRead]),\n };\n}\n\nexport function listSessions(db: DB, f: SessionFilters): SessionsPage {\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.source) (where.push(\"s.source_id = ?\"), params.push(f.source));\n if (f.project) (where.push(\"s.project_id = ?\"), params.push(f.project));\n // Date-inclusive on both ends (see findingWhere): a picked `to` day must include that day's sessions.\n pushDateRange(where, params, \"s.started_at\", f.from, f.to);\n if (f.kind === \"main\") where.push(\"s.is_sidechain = 0\");\n if (f.kind === \"subagent\") where.push(\"s.is_sidechain = 1\");\n // Multi-select: session has ≥1 finding of any listed severity / ≥1 errored tool call of any listed type.\n if (f.severity?.length) {\n where.push(`EXISTS (SELECT 1 FROM findings fd WHERE fd.session_id = s.id AND fd.severity IN (${f.severity.map(() => \"?\").join(\",\")}))`);\n params.push(...f.severity);\n }\n if (f.errorType?.length) {\n where.push(`EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.session_id = s.id AND tc.error_type IN (${f.errorType.map(() => \"?\").join(\",\")}))`);\n params.push(...f.errorType);\n }\n if (f.model) {\n where.push(\"EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND t.model = ?)\");\n params.push(f.model);\n }\n if (f.q && f.q.trim()) {\n // Match transcript text (FTS) OR the session's own name (slug/ai_title) OR its project path, so a\n // query like \"fix-youtube-embed-csp-policy\" finds the session by slug even when that string never\n // appears verbatim in an event. A project-path *subquery* (not a join on p.path) keeps the\n // un-joined COUNT(*) query above valid. SQLite LIKE is case-insensitive for ASCII.\n const like = `%${f.q.trim()}%`;\n where.push(\n `(s.id IN (SELECT DISTINCT e.session_id FROM events e JOIN events_fts f ON f.rowid = e.rowid WHERE events_fts MATCH ?)\n OR s.slug LIKE ? OR s.ai_title LIKE ?\n OR s.project_id IN (SELECT id FROM projects WHERE path LIKE ?))`,\n );\n params.push(toFtsQuery(f.q), like, like, like);\n }\n const whereSql = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n\n const total = queryGet<CountRow>(db, `SELECT COUNT(*) n FROM sessions s ${whereSql}`, ...params)!.n;\n\n const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,\n s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,\n (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,\n (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,\n (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id AND tc.status = 'error') AS tool_error_count,\n (SELECT COUNT(*) FROM findings fd WHERE fd.session_id = s.id) AS finding_count,\n (SELECT fd.severity FROM findings fd WHERE fd.session_id = s.id\n ORDER BY CASE fd.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 WHEN 'info' THEN 0 ELSE -1 END DESC\n LIMIT 1) AS worst_severity\n FROM sessions s LEFT JOIN projects p ON p.id = s.project_id\n ${whereSql}`;\n\n // Every sortable column resolves to a SQL expression, so ORDER BY + LIMIT do the paging. Stored\n // columns are direct; the derived ones (tokens/cost/errors/security) are the subquery or CASE the\n // projection above already computes, referenced by output alias where one exists. These four used\n // to sort in JS over the WHOLE matching set — O(corpus) on every request, per the comment this\n // replaces — which is what made the session list the worst-scaling query in the server.\n const ORDER = {\n started: \"s.started_at\",\n title: \"COALESCE(s.ai_title, s.slug)\",\n turns: \"s.turn_count\",\n duration: \"s.duration_ms\",\n errors: \"tool_error_count\",\n tokens: TOKENS_SORT_SQL,\n cost: COST_SORT_SQL,\n security: SECURITY_SORT_SQL,\n };\n const sortKey = f.sort ?? \"started\";\n\n // The cost expression joins a rates CTE, so that clause has to lead the statement — and its bound\n // parameters therefore come before the WHERE's. Only built when actually sorting by cost.\n const rates = sortKey === \"cost\" ? ratesCte() : null;\n const sessions = attachSessionCost(\n db,\n queryAll<SessionListRow>(\n db,\n `${rates?.sql ?? \"\"}${baseSelect} ORDER BY ${orderBy(ORDER, sortKey, \"started\", f.dir)}, s.id ASC LIMIT ? OFFSET ?`,\n ...(rates?.params ?? []),\n ...params,\n f.limit,\n f.offset,\n ),\n );\n\n return { total, sessions };\n}\n\n/** Add per-session `tokens`, `token_split`, `cost` (USD, cache-aware) and `title` to each list row,\n * turning the projection into the response shape. Costs are grouped by model so per-model rates apply.\n * IDs are chunked so the whole-list sort path (which can pass thousands of sessions) stays under\n * SQLite's bound-parameter limit. */\nfunction attachSessionCost(db: DB, rows: SessionListRow[]): SessionSummary[] {\n // Group each session's per-model usage rows, then reuse the shared split/cost helpers. IDs are\n // chunked so the whole-list sort path (which can pass thousands of sessions) stays under SQLite's\n // bound-parameter limit.\n const usageBySession = new Map<string, UsageRow[]>();\n const ids = rows.map((r) => r.id);\n const CHUNK = 800;\n for (let i = 0; i < ids.length; i += CHUNK) {\n const batch = ids.slice(i, i + CHUNK);\n const ph = batch.map(() => \"?\").join(\",\");\n const usage = queryAll<UsageRow & { session_id: string }>(\n db,\n `SELECT session_id, model, ${USAGE_SUMS} FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`,\n ...batch,\n );\n for (const u of usage) {\n const list = usageBySession.get(u.session_id) ?? [];\n list.push(u);\n usageBySession.set(u.session_id, list);\n }\n }\n return rows.map(({ ai_title, slug, ...r }) => {\n const u = usageBySession.get(r.id) ?? [];\n const split = splitOf(u);\n return {\n ...r,\n tokens: tokensOf(split),\n token_split: split,\n cost: costOf(u),\n unpriced_models: unpricedOf(u),\n title: ai_title || slug || null,\n };\n });\n}\n\n/** The session's tool calls, with any spilled full output attached.\n *\n * When a result_summary is the \"Full output saved to: …/tool-results/<name>.txt\" marker (the\n * transcript's 280-char stand-in), the un-truncated text is pulled from tool_results so the UI can\n * expand it. Keyed by (session_id, name); guarded for a pre-ingest DB. */\nfunction loadToolCalls(db: DB, id: string): ToolCallProjection[] {\n const toolRows = queryAll<ToolCallProjection>(\n db,\n `SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,\n workflow_run_id, workflow_name, status, error_type,\n total_duration_ms, input_json, result_summary,\n (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count\n FROM tool_calls WHERE session_id = ?`,\n id,\n );\n\n if (tableExists(db, \"tool_results\")) {\n const getFull = db.prepare(\"SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?\");\n for (const t of toolRows) {\n const m = typeof t.result_summary === \"string\" ? t.result_summary.match(/tool-results\\/([A-Za-z0-9_-]+)\\.txt/) : null;\n if (!m) continue;\n const full = getFull.get(id, m[1]) as ToolResultRow | undefined;\n if (full) t.full_result = { text: full.text, bytes: full.bytes };\n }\n }\n return toolRows;\n}\n\n/** Security findings (ADR-017): attaches each tool call's findings inline (for the transcript severity\n * badge + \"why\" panel) and returns the session-level list for the header summary, most-severe first.\n * Guarded for a read-only pre-ingest DB whose schema predates the findings table. */\nfunction loadFindings(db: DB, id: string, toolRows: ToolCallProjection[]): Finding[] {\n if (!tableExists(db, \"findings\")) return [];\n const sessionFindings: Finding[] = [];\n const findingRows = queryAll<FindingProjectionRow>(\n db,\n `SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json\n FROM findings WHERE session_id = ?`,\n id,\n );\n const byToolCall = new Map<string, Finding[]>();\n for (const fr of findingRows) {\n const f: Finding = {\n id: fr.id,\n tool_call_id: fr.tool_call_id,\n event_uuid: fr.event_uuid,\n turn_id: fr.turn_id,\n rule_id: fr.rule_id,\n category: fr.category,\n framework_ref: fr.framework_ref,\n severity: fr.severity,\n title: fr.title,\n evidence: fr.evidence,\n // Asserted, not validated: safeJson returns unknown and this is the assertion (ADR-026).\n signals: safeJson(fr.signals_json) as Finding[\"signals\"],\n };\n sessionFindings.push(f);\n if (fr.tool_call_id) pushGrouped(byToolCall, fr.tool_call_id, f);\n }\n for (const t of toolRows) t.findings = t.id ? (byToolCall.get(t.id) ?? []) : [];\n // Most-severe first so a session banner can lead with the worst.\n sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));\n return sessionFindings;\n}\n\n/** The session's transcript events in order, each with its flattened text/thinking and its tool calls. */\n/** Drop the columns the query needed but the response does not ship (ADR-026): `event_uuid` has just\n * been used to nest this call under its event, and `error_type` feeds the server-side\n * failure-vs-rejection split and reaches the UI only as a dashboard breakdown. */\nfunction toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }: ToolCallProjection): ToolCall {\n return call;\n}\n\nfunction loadEvents(db: DB, id: string, toolRows: ToolCallProjection[]): EventNode[] {\n const eventRows = queryAll<EventProjectionRow>(\n db,\n `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json\n FROM events WHERE session_id = ? ORDER BY timestamp, seq`,\n id,\n );\n\n const toolsByEvent = new Map<string, ToolCallProjection[]>();\n for (const t of toolRows) {\n if (!t.event_uuid) continue;\n pushGrouped(toolsByEvent, t.event_uuid, t);\n }\n\n return eventRows.map((e) => {\n const { text, thinking } = extractParts(e.raw_json);\n return {\n uuid: e.uuid,\n type: e.type,\n role: e.role,\n timestamp: e.timestamp,\n model: e.model,\n is_sidechain: e.is_sidechain,\n turn_id: e.turn_id,\n text,\n thinking,\n toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall),\n };\n });\n}\n\n/** Heuristic classification: category + complexity + the signals that produced them\n * (tool/skill mix, LoC, files, subagent count) — already captured in signals_json by the classifier. */\nfunction loadClassification(db: DB, id: string): Classification | null {\n const cls = queryGet<ClassificationProjectionRow>(\n db,\n `SELECT category, complexity_score, complexity_band, signals_json, classifier_version\n FROM classifications WHERE scope = 'session' AND target_id = ?`,\n id,\n );\n if (!cls) return null;\n // Asserted, not validated (ADR-026) — a malformed blob degrades to null, a well-formed one of the\n // wrong shape reaches the UI as declared.\n let signals: ClassificationSignals | null = null;\n try {\n signals = cls.signals_json ? (JSON.parse(cls.signals_json) as ClassificationSignals) : null;\n } catch {\n /* ignore malformed */\n }\n return {\n category: cls.category,\n complexity_score: cls.complexity_score,\n complexity_band: cls.complexity_band,\n classifier_version: cls.classifier_version,\n signals,\n };\n}\n\n/** Subagent linkage (schema v3): if this is a sidechain session, resolve the parent turn/session that\n * spawned it so the UI can offer a \"spawned by\" crumb back to the originating turn. */\nfunction loadParent(db: DB, session: SessionRow): SessionParent | null {\n if (!session.parent_session_id) return null;\n const p = queryGet<SessionParentRow>(\n db,\n `SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq\n FROM sessions ps LEFT JOIN turns t ON t.id = ?\n WHERE ps.id = ?`,\n session.parent_turn_id,\n session.parent_session_id,\n );\n return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;\n}\n\n/** Spawned subagents (schema v3): sessions whose parent_session_id points back here. Nesting them\n * under the parent is why the flat session list defaults to main-only — a task with N subagents is\n * one row with N children, not N+1 sibling rows sharing the same slug.\n * Subagent metadata sidecars (session_meta) supply the authoritative agentType/description/spawnDepth\n * — LEFT JOIN so a subagent still lists when its meta hasn't been ingested (or the table is absent on\n * a pre-ingest read-only DB). */\nfunction loadChildren(db: DB, id: string): SessionChild[] {\n const hasMeta = tableExists(db, \"session_meta\");\n const children = queryAll<SessionChildRow>(\n db,\n `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,\n (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models\n ${metaProjection(hasMeta)}\n FROM sessions s ${metaJoin(hasMeta)}\n WHERE s.parent_session_id = ? ORDER BY s.started_at`,\n id,\n );\n return children.map(({ ai_title, slug, ...ch }) => {\n const u = sessionUsage(db, ch.id);\n return { ...ch, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };\n });\n}\n\n/** Workflow runs launched from THIS session: each Workflow tool_call carries a run id + name and sits\n * on a turn. This lets the UI group the spawned subagents by run and link each group back to the\n * exact launching turn (turn_seq) — instead of one flat, unattributed fan-out list.\n * Run status rides along from the result sidecar (workflow_results) when ingested, so the session's\n * workflow-run groups can flag a failed/completed run at a glance. NULL when no sidecar (async, still\n * pending) or the table isn't present yet (read-only pre-ingest DB). */\nfunction loadWorkflowRuns(db: DB, id: string): WorkflowRun[] {\n const hasWR = tableExists(db, \"workflow_results\");\n return queryAll<WorkflowRun>(\n db,\n `SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,\n (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count\n ${hasWR ? \", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status\" : \", NULL AS status\"}\n FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id\n WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL\n ORDER BY t.seq`,\n id,\n );\n}\n\n/** File-modification provenance (ADR-022): the session's derived Edit/Write/NotebookEdit file\n * changes, for the \"Files changed\" header roll-up. Guarded for a pre-v14 read-only DB. */\nfunction loadFileChanges(db: DB, id: string): FileChangeRow[] {\n if (!tableExists(db, \"file_changes\")) return [];\n return queryAll<FileChangeRow>(\n db,\n `SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,\n fc.lines_added, fc.lines_removed, fc.timestamp\n FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`,\n id,\n );\n}\n\n/** Roll-ups written onto the session row for the detail header: token split + cost, display title, and\n * the tool-call error counts (computed from the already-fetched toolRows, so no extra query).\n *\n * Tokens keep their categories split so the UI can show input/output/cache-write/cache-read separately.\n * Cache stays IN the total and IS cost-attributed (at its discounted rate) — differentiated, never\n * dropped. The raw error count is authoritative; the failure-vs-rejection split uses the stored\n * heuristic error_type (rejection = the user/guardrail declined the call, not an agent failure). */\nfunction withSessionTotals(\n db: DB,\n session: SessionRow & { project_path: string | null },\n id: string,\n toolRows: ToolCallProjection[],\n): SessionDetailData {\n const usage = sessionUsage(db, id);\n const split = splitOf(usage);\n\n const tool_error_count = toolRows.filter((t) => t.status === \"error\").length;\n // `error_type` is a plain column read, so nothing proves it is still one of core's ToolErrorType\n // literals — an older DB could hold a bucket name that has since been renamed. errorKind is a set\n // membership test that answers \"failure\" for anything unrecognized, so the cast is safe rather than\n // merely convenient; it replaces an `any` that was making the same assumption invisibly.\n const tool_rejection_count = toolRows.filter(\n (t) => t.status === \"error\" && t.error_type && errorKind(t.error_type as ToolErrorType) === \"rejection\",\n ).length;\n\n return {\n ...session,\n tokens: tokensOf(split),\n token_split: split,\n cost: costOf(usage),\n unpriced_models: unpricedOf(usage),\n title: session.ai_title || session.slug || null,\n tool_call_count: toolRows.length,\n tool_error_count,\n tool_rejection_count,\n tool_failure_count: tool_error_count - tool_rejection_count,\n };\n}\n\nexport function getSession(db: DB, id: string): SessionDetail | null {\n const session = queryGet<SessionRow & { project_path: string | null }>(\n db,\n `SELECT s.*, p.path AS project_path FROM sessions s\n LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`,\n id,\n );\n if (!session) return null;\n\n const turns = queryAll<TurnRow>(db, \"SELECT * FROM turns WHERE session_id = ? ORDER BY seq\", id);\n const toolRows = loadToolCalls(db, id);\n const findings = loadFindings(db, id, toolRows);\n const events = loadEvents(db, id, toolRows);\n\n return {\n session: withSessionTotals(db, session, id, toolRows),\n turns,\n events,\n classification: loadClassification(db, id),\n parent: loadParent(db, session),\n children: loadChildren(db, id),\n workflow_runs: loadWorkflowRuns(db, id),\n findings,\n file_changes: loadFileChanges(db, id),\n };\n}\n\n/**\n * A Workflow-tool run's detail: the launching tool_call (name, status, returned result) + the turn\n * and session it was launched from, every subagent session fanned out under its `workflow_run_id`,\n * and roll-up stats (agent count, tokens, cost, wall-clock span). Powers the /workflow/:run_id page.\n * Returns null when no Workflow tool_call carries this run id (→ 404).\n */\n/** Pull the inner text of a single `<tag>…</tag>` from a flattened message (non-greedy). */\nfunction xmlTag(text: string, tag: string): string | null {\n return text.match(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;\n}\n\n\n/** Parse a stored JSON column back to a value, or null when absent/malformed. */\nexport function safeJson(s: string | null): unknown {\n if (!s) return null;\n try {\n return JSON.parse(s);\n } catch {\n return null;\n }\n}\n\n/** Subagent sessions in this run (same projection as getSession's children, plus the wall-clock fields\n * so the page can show each agent's span and a run-level min/max), with the run's token/cost/span\n * roll-up computed over them. LEFT JOIN session_meta for the authoritative type/description/depth —\n * these workflow agents carry none in-transcript. */\nfunction loadAgents(db: DB, runId: string): { agents: WorkflowAgent[]; stats: WorkflowDetail[\"stats\"] } {\n const hasMeta = tableExists(db, \"session_meta\");\n const agentRows = queryAll<WorkflowAgentRow>(\n db,\n `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,\n (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models\n ${metaProjection(hasMeta)}\n FROM sessions s ${metaJoin(hasMeta)}\n WHERE s.workflow_run_id = ? ORDER BY s.started_at`,\n runId,\n );\n\n let total_tokens = 0;\n let total_cost = 0;\n let started_at: string | null = null;\n let ended_at: string | null = null;\n const agents: WorkflowAgent[] = agentRows.map(({ ai_title, slug, ...r }) => {\n const u = sessionUsage(db, r.id);\n const a = { ...r, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };\n total_tokens += a.tokens;\n total_cost += a.cost;\n if (a.started_at && (!started_at || a.started_at < started_at)) started_at = a.started_at;\n if (a.ended_at && (!ended_at || a.ended_at > ended_at)) ended_at = a.ended_at;\n return a;\n });\n const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;\n\n return {\n agents,\n stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms },\n };\n}\n\n/** The workflow's ACTUAL result, which arrives later as a `<task-notification>` user message in the\n * launching session — tc.result_summary is only the \"launched in background\" ack. Finds that message by\n * the Workflow tool-use id and surfaces its completion status, summary, result, and failures. (The same\n * task can notify more than once; takes the most recent.) */\nfunction parseCompletion(db: DB, wf: WorkflowLaunchRow): WorkflowCompletion | null {\n if (!wf.tool_use_id || !wf.parent_session_id) return null;\n const ev = queryGet<RawJsonRow>(\n db,\n `SELECT raw_json FROM events\n WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?\n ORDER BY timestamp DESC LIMIT 1`,\n wf.parent_session_id,\n `%${wf.tool_use_id}%`,\n );\n if (!ev) return null;\n const { text } = extractParts(ev.raw_json);\n const t = text ?? \"\";\n return { status: xmlTag(t, \"status\"), summary: xmlTag(t, \"summary\"), result: xmlTag(t, \"result\"), failures: xmlTag(t, \"failures\") };\n}\n\n/** The runner's own result sidecar (workflow_results), ingested from wf_<id>.json. It's the\n * authoritative record of how the run finished — especially for async runs that never posted a\n * task-notification (`completion` stays null) or that failed before fanning out any agents.\n * Sidecar-preferred: it supplies status + the returned result and fills the completion when the\n * transcript has none. Its self-reported roll-up (model, tokens, tool calls, phases, per-item logs,\n * duration, agent count) rides along as `run` so the page can show it even with zero ingested agents. */\nfunction loadRunSidecar(\n db: DB,\n runId: string,\n completion: WorkflowCompletion | null,\n): { wr: WorkflowResultRow | null; run: WorkflowRunResult | null; completion: WorkflowCompletion | null } {\n const wr = tableExists(db, \"workflow_results\")\n ? (queryGet<WorkflowResultRow>(\n db,\n `SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,\n agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at\n FROM workflow_results WHERE run_id = ?`,\n runId,\n ) ?? null)\n : null;\n if (!wr) return { wr: null, run: null, completion };\n\n const run: WorkflowRunResult = {\n status: wr.status ?? null,\n summary: wr.summary ?? null,\n default_model: wr.default_model ?? null,\n agent_count: wr.agent_count ?? null,\n total_tokens: wr.total_tokens ?? null,\n total_tool_calls: wr.total_tool_calls ?? null,\n duration_ms: wr.duration_ms ?? null,\n started_at: wr.started_at ?? null,\n ended_at: wr.ended_at ?? null,\n // The three JSON blobs are asserted, not validated (ADR-026): safeJson yields null for malformed\n // stored text, and these casts are what claims the shape of everything else.\n phases: safeJson(wr.phases_json) as WorkflowRunResult[\"phases\"],\n logs: safeJson(wr.logs_json) as WorkflowRunResult[\"logs\"],\n // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph\n // with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.\n progress: safeJson(wr.progress_json) as WorkflowRunResult[\"progress\"],\n };\n\n if (wr.result_json != null || wr.status) {\n completion = {\n status: wr.status ?? completion?.status ?? null,\n summary: wr.summary ?? completion?.summary ?? null,\n result: wr.result_json ?? completion?.result ?? null,\n failures: completion?.failures ?? null,\n };\n }\n return { wr, run, completion };\n}\n\nexport function getWorkflow(db: DB, runId: string): WorkflowDetail | null {\n const wf = queryGet<WorkflowLaunchRow>(\n db,\n `SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,\n tc.input_json AS input_json,\n tc.session_id AS parent_session_id, t.seq AS turn_seq,\n ps.ai_title AS parent_ai_title, ps.slug AS parent_slug\n FROM tool_calls tc\n LEFT JOIN turns t ON t.id = tc.turn_id\n LEFT JOIN sessions ps ON ps.id = tc.session_id\n WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'\n ORDER BY t.seq\n LIMIT 1`,\n runId,\n );\n if (!wf) return null;\n\n const { agents, stats } = loadAgents(db, runId);\n const { wr, run, completion } = loadRunSidecar(db, wf.run_id, parseCompletion(db, wf));\n\n return {\n run_id: wf.run_id,\n name: wf.name ?? null,\n status: wr?.status ?? wf.status ?? null,\n result_summary: wf.result_summary ?? null,\n input_json: wf.input_json ?? null,\n completion,\n run,\n parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },\n agents,\n stats,\n };\n}\n\nexport interface SkillFilters {\n q?: string;\n source?: string;\n project?: string;\n}\n\n/**\n * One row per fired skill (grouped by name), for the Skills list. `version_count` is the number of\n * distinct captured content versions (COUNT(DISTINCT skill_id) ignores firings with no body), while\n * `call_count` counts every firing — so a skill whose body was never captured still appears. Filters\n * (name search, source, project) mirror the sessions list and apply to the firing session.\n */\nexport function listSkills(db: DB, f: SkillFilters = {}): SkillSummary[] {\n const where = [\"tc.tool_name = 'Skill'\", \"tc.skill_name IS NOT NULL\"];\n const params: unknown[] = [];\n // Search matches the skill name OR any captured version body, so a query for a phrase that only\n // appears inside a SKILL.md still surfaces the skill. (skills.body is the normalized body; joined by\n // name rather than skill_id so firings with no captured body still match on name.)\n if (f.q && f.q.trim()) {\n const like = `%${f.q.trim()}%`;\n where.push(\"(tc.skill_name LIKE ? OR EXISTS (SELECT 1 FROM skills sk WHERE sk.name = tc.skill_name AND sk.body LIKE ?))\");\n params.push(like, like);\n }\n if (f.source) (where.push(\"s.source_id = ?\"), params.push(f.source));\n if (f.project) (where.push(\"s.project_id = ?\"), params.push(f.project));\n const rows = queryAll<SkillListRow>(\n db,\n `SELECT tc.skill_name AS name,\n COUNT(*) AS call_count,\n COUNT(DISTINCT tc.skill_id) AS version_count,\n MAX(e.timestamp) AS last_fired,\n GROUP_CONCAT(DISTINCT s.source_id) AS sources\n FROM tool_calls tc\n JOIN sessions s ON s.id = tc.session_id\n LEFT JOIN events e ON e.uuid = tc.event_uuid\n WHERE ${where.join(\" AND \")}\n GROUP BY tc.skill_name\n ORDER BY call_count DESC, name`,\n ...params,\n );\n // GROUP_CONCAT hands back a comma-joined string; the response declares an array.\n return rows.map((r) => ({ ...r, sources: r.sources ? String(r.sources).split(\",\").filter(Boolean) : [] }));\n}\n\nexport interface FileFilters {\n q?: string; // path substring\n source?: string;\n project?: string;\n sort?: \"last_ts\" | \"first_ts\" | \"changes\" | \"sessions\" | \"path\";\n dir?: \"asc\" | \"desc\";\n limit?: number;\n offset?: number;\n}\n\n/**\n * The /files list (ADR-022): one row per (project, file) aggregated from `file_changes` — session\n * and change counts, line totals, first/last touched. Filters mirror the sessions list (source via\n * the changing session; q is a path substring). Sort whitelist is native SQL; count-then-page like\n * listSessions. Empty result (not an error) on a pre-v14 DB missing the table.\n */\nexport function listFiles(db: DB, f: FileFilters = {}): FilesPage {\n if (!tableExists(db, \"file_changes\")) return { total: 0, files: [] };\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.q && f.q.trim()) (where.push(\"fc.file_path LIKE ?\"), params.push(`%${f.q.trim()}%`));\n if (f.project) (where.push(\"fc.project_id = ?\"), params.push(f.project));\n if (f.source) (where.push(\"EXISTS (SELECT 1 FROM sessions s WHERE s.id = fc.session_id AND s.source_id = ?)\"), params.push(f.source));\n const whereSql = where.length ? ` WHERE ${where.join(\" AND \")}` : \"\";\n\n const grouped = `SELECT fc.project_id, fc.file_path,\n COUNT(DISTINCT fc.session_id) AS sessions,\n COUNT(*) AS changes,\n SUM(fc.lines_added) AS lines_added,\n SUM(fc.lines_removed) AS lines_removed,\n MIN(fc.timestamp) AS first_ts,\n MAX(fc.timestamp) AS last_ts\n FROM file_changes fc${whereSql}\n GROUP BY fc.project_id, fc.file_path`;\n\n const total = queryGet<CountRow>(db, `SELECT COUNT(*) n FROM (${grouped})`, ...params)!.n;\n\n const ORDER = {\n last_ts: \"last_ts\",\n first_ts: \"first_ts\",\n changes: \"changes\",\n sessions: \"sessions\",\n path: \"file_path\",\n };\n const limit = Math.min(f.limit ?? 50, 200);\n const offset = f.offset ?? 0;\n\n const files = queryAll<FileSummary>(\n db,\n `SELECT g.*, p.path AS project_path FROM (${grouped}) g\n LEFT JOIN projects p ON p.id = g.project_id\n ORDER BY ${orderBy(ORDER, f.sort, \"last_ts\", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`,\n ...params,\n limit,\n offset,\n );\n return { total, files };\n}\n\n/**\n * One file's provenance timeline (ADR-022): every `file_changes` row for the path (optionally\n * scoped to a project), grouped by changing session (newest-first) with per-turn detail — each\n * change deep-links to its tool call's transcript event (#ev-<event_uuid>). Session cards carry\n * title/date/category (classification join) like the skills detail. Returns null (→ 404) when the\n * path has no recorded changes.\n */\nexport function getFileTimeline(db: DB, filePath: string, projectId?: string): FileTimeline | null {\n if (!tableExists(db, \"file_changes\")) return null;\n const params: unknown[] = [filePath];\n let scope = \"fc.file_path = ?\";\n if (projectId) (scope += \" AND fc.project_id = ?\"), params.push(projectId);\n const rows = queryAll<FileTimelineRow>(\n db,\n `SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,\n fc.lines_removed, fc.timestamp, fc.project_id,\n s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,\n p.path AS project_path\n FROM file_changes fc\n JOIN sessions s ON s.id = fc.session_id\n LEFT JOIN turns t ON t.id = fc.turn_id\n LEFT JOIN projects p ON p.id = fc.project_id\n WHERE ${scope} ORDER BY fc.timestamp`,\n ...params,\n );\n if (!rows.length) return null;\n\n const cat = tableExists(db, \"classifications\")\n ? db.prepare(\"SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?\")\n : null;\n\n const bySession = new Map<string, FileTimelineSession>();\n for (const r of rows) {\n let g = bySession.get(r.session_id);\n if (!g) {\n g = {\n session_id: r.session_id,\n title: r.ai_title || r.slug || null,\n source_id: r.source_id,\n started_at: r.started_at,\n category: cat ? ((cat.get(r.session_id) as CategoryRow | undefined)?.category ?? null) : null,\n changes: [],\n };\n bySession.set(r.session_id, g);\n }\n g.changes.push({\n id: r.id,\n turn_id: r.turn_id,\n turn_seq: r.turn_seq,\n prompt_preview: r.prompt_preview,\n event_uuid: r.event_uuid,\n tool_name: r.tool_name,\n lines_added: r.lines_added,\n lines_removed: r.lines_removed,\n timestamp: r.timestamp,\n });\n }\n // Newest-changing session first; changes within a session stay chronological.\n const sessions = [...bySession.values()].sort((a, b) => {\n const la = a.changes[a.changes.length - 1].timestamp ?? \"\";\n const lb = b.changes[b.changes.length - 1].timestamp ?? \"\";\n return la < lb ? 1 : la > lb ? -1 : 0;\n });\n\n return {\n file_path: filePath,\n project_id: rows[0].project_id ?? null,\n project_path: rows[0].project_path ?? null,\n sessions_count: sessions.length,\n changes_count: rows.length,\n lines_added: rows.reduce((a, r) => a + (r.lines_added ?? 0), 0),\n lines_removed: rows.reduce((a, r) => a + (r.lines_removed ?? 0), 0),\n first_ts: rows[0].timestamp ?? null,\n last_ts: rows[rows.length - 1].timestamp ?? null,\n sessions,\n };\n}\n\n/**\n * All data for one skill (by name): its content versions (most-recent first, each with body +\n * firing count) and the sessions that fired it, tagged with which version (`version_id`) so the UI\n * can map the session list to the selected version. Returns null (→ 404) when the name never fired.\n */\nexport function getSkill(db: DB, name: string): SkillDetail | null {\n const versions = queryAll<SkillVersion>(\n db,\n `SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,\n (SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count\n FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`,\n name,\n );\n\n const sessionRows = queryAll<SkillSessionRow>(\n db,\n `SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,\n tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count\n FROM tool_calls tc\n JOIN sessions s ON s.id = tc.session_id\n LEFT JOIN projects p ON p.id = s.project_id\n LEFT JOIN events e ON e.uuid = tc.event_uuid\n WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?\n GROUP BY s.id, tc.skill_id\n ORDER BY fired_at DESC`,\n name,\n );\n\n if (!versions.length && !sessionRows.length) return null;\n const sessions: SkillSession[] = sessionRows.map(({ ai_title, slug, ...s }) => ({ ...s, title: ai_title || slug || null }));\n const call_count = sessions.reduce((a, s) => a + s.fire_count, 0);\n return { name, versions, sessions, call_count };\n}\n\n// ---- Security findings (ADR-017) + triage (ADR-018) ----------------------\n\nexport interface FindingFilters {\n severity?: string;\n category?: string;\n rule?: string;\n session?: string;\n source?: string;\n project?: string;\n from?: string;\n to?: string;\n /** open (default; excludes dismissed + muted) | dismissed | muted | all. */\n status?: \"open\" | \"dismissed\" | \"muted\" | \"all\";\n /** Sort the whole filtered list before pagination. Defaults to \"severity\" (most-severe first). */\n sort?: \"severity\" | \"session\" | \"rule\" | \"category\" | \"time\";\n dir?: \"asc\" | \"desc\";\n limit: number;\n offset: number;\n}\n\n// Rank severities inside SQL so the list can be severity-ordered and paginated without materializing\n// everything (mirrors SEVERITY_ORDER in core: info=1 … critical=5).\nconst SEVERITY_RANK_SQL =\n \"CASE f.severity WHEN 'critical' THEN 5 WHEN 'high' THEN 4 WHEN 'medium' THEN 3 WHEN 'low' THEN 2 WHEN 'info' THEN 1 ELSE 0 END\";\n\n// A finding is muted when a muted_rules row matches its rule at global/project/source scope. Only valid\n// when the triage DB is ATTACHed (createApp does this); guarded by triageAttached() everywhere it's used.\nconst MUTED_SQL = `EXISTS (SELECT 1 FROM triage.muted_rules m WHERE m.rule_id = f.rule_id\n AND (m.scope='global' OR (m.scope='project' AND m.scope_id = s.project_id) OR (m.scope='source' AND m.scope_id = s.source_id)))`;\n\n/** Whether the writable triage store is ATTACHed to this (read) connection — gates all triage SQL. */\nexport function triageAttached(db: DB): boolean {\n return (db.prepare(\"PRAGMA database_list\").all() as Array<{ name: string }>).some((r) => r.name === \"triage\");\n}\n\n/** The filter half of FindingFilters — what narrows the set, without status/sort/paging. */\nexport type FindingScope = Omit<FindingFilters, \"status\" | \"sort\" | \"dir\" | \"limit\" | \"offset\">;\n\n/** WHERE fragments shared by list + summary: user filters (no status) over findings f / sessions s. */\nfunction findingWhere(f: FindingScope): { sql: string[]; params: unknown[] } {\n const sql: string[] = [];\n const params: unknown[] = [];\n if (f.severity) (sql.push(\"f.severity = ?\"), params.push(f.severity));\n if (f.category) (sql.push(\"f.category = ?\"), params.push(f.category));\n if (f.rule) (sql.push(\"f.rule_id = ?\"), params.push(f.rule));\n if (f.session) (sql.push(\"f.session_id = ?\"), params.push(f.session));\n if (f.source) (sql.push(\"s.source_id = ?\"), params.push(f.source));\n if (f.project) (sql.push(\"s.project_id = ?\"), params.push(f.project));\n // Compare on the DATE part so a picked day is inclusive on both ends — `to = 2026-07-14` must include\n // 2026-07-14 events (a plain `started_at <= '2026-07-14'` would exclude that whole day's timestamps).\n pushDateRange(sql, params, \"s.started_at\", f.from, f.to);\n return { sql, params };\n}\n\n/** Status → WHERE fragment over the dismissed LEFT JOIN (`d`) + MUTED_SQL. Empty when triage absent. */\nfunction statusWhere(status: FindingFilters[\"status\"], attached: boolean): string {\n if (!attached) return status === \"dismissed\" || status === \"muted\" ? \"1 = 0\" : \"\"; // no triage → nothing dismissed/muted\n switch (status) {\n case \"dismissed\":\n return \"d.finding_id IS NOT NULL\";\n case \"muted\":\n return `d.finding_id IS NULL AND ${MUTED_SQL}`;\n case \"all\":\n return \"\";\n default: // open\n return `d.finding_id IS NULL AND NOT ${MUTED_SQL}`;\n }\n}\n\n/**\n * The browsable findings list (GET /api/security/findings), filtered/sorted/paged. Each row carries\n * enough session context (title, project, source, when) to render and link without a second fetch, plus\n * its triage state (dismissed flag/note/when) when the triage store is attached. Default status hides\n * dismissed + muted so real, un-triaged findings surface. Guarded for a pre-ingest read-only DB.\n */\nexport function listFindings(db: DB, f: FindingFilters): FindingsPage {\n if (!tableExists(db, \"findings\")) return { total: 0, findings: [] };\n const attached = triageAttached(db);\n\n const w = findingWhere(f);\n const st = statusWhere(f.status ?? \"open\", attached);\n const where = [...w.sql, ...(st ? [st] : [])];\n const whereSql = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n const dismissJoin = attached ? \"LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id\" : \"\";\n const triageCols = attached\n ? `, (d.finding_id IS NOT NULL) AS dismissed, d.note AS dismiss_note, d.dismissed_at, ${MUTED_SQL} AS muted`\n : `, 0 AS dismissed, NULL AS dismiss_note, NULL AS dismissed_at, 0 AS muted`;\n\n const total = queryGet<CountRow>(\n db,\n `SELECT COUNT(*) n FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin} ${whereSql}`,\n ...w.params,\n )!.n;\n\n const ORDER = {\n severity: SEVERITY_RANK_SQL,\n session: \"f.session_id\",\n rule: \"f.rule_id\",\n category: \"f.category\",\n time: \"s.started_at\",\n };\n\n const findings = queryAll<FindingListRow>(\n db,\n `SELECT f.id, f.session_id, f.tool_call_id, f.event_uuid, f.turn_id, f.rule_id, f.category,\n f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,\n COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,\n s.started_at, p.path AS project_path, s.project_id ${triageCols}\n FROM findings f\n JOIN sessions s ON s.id = f.session_id\n LEFT JOIN projects p ON p.id = s.project_id\n LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id\n ${dismissJoin}\n ${whereSql}\n ORDER BY ${orderBy(ORDER, f.sort, \"severity\", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC\n LIMIT ? OFFSET ?`,\n ...w.params,\n f.limit,\n f.offset,\n );\n\n return { total, findings };\n}\n\n/**\n * Ids of every OPEN finding matching `f` — the id-only half of listFindings, for bulk dismiss\n * (POST /api/security/dismiss-matching). Shares findingWhere + statusWhere with the list so \"what\n * matches\" cannot drift between browsing and bulk-dismissing; only the projection differs, so this\n * skips the list's COUNT(*) pass, session/tool context columns, and ORDER BY. Status is fixed to\n * open: a bulk dismiss acts on what the user can currently see, never on dismissed or muted rows.\n */\nexport function openFindingIds(db: DB, f: FindingScope): string[] {\n if (!tableExists(db, \"findings\")) return [];\n const attached = triageAttached(db);\n\n const w = findingWhere(f);\n const st = statusWhere(\"open\", attached);\n const where = [...w.sql, ...(st ? [st] : [])];\n\n return queryAll<{ id: string }>(\n db,\n `SELECT f.id\n FROM findings f\n JOIN sessions s ON s.id = f.session_id\n ${attached ? \"LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id\" : \"\"}\n ${where.length ? `WHERE ${where.join(\" AND \")}` : \"\"}`,\n ...w.params,\n ).map((r) => r.id);\n}\n\n/**\n * Roll-up for the /security page header + the Dashboard KPI. Counts (by severity/category/rule,\n * sessions flagged) are computed over **open** findings — dismissed + muted excluded — so the KPIs show\n * what still needs review; `dismissed` and `muted` totals ride along for the triage affordances. The\n * framework reference content (core) powers the \"what & why\" explainers. Guarded for a pre-ingest DB.\n */\nexport function securitySummary(db: DB): SecuritySummary {\n const categories = SECURITY_CATEGORIES;\n if (!tableExists(db, \"findings\")) {\n return { total: 0, sessions_flagged: 0, dismissed: 0, muted: 0, by_severity: [], by_category: [], by_rule: [], categories };\n }\n const attached = triageAttached(db);\n const dismissJoin = attached ? \"LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id\" : \"\";\n const base = `FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin}`;\n const openWhere = attached ? `WHERE d.finding_id IS NULL AND NOT ${MUTED_SQL}` : \"\";\n\n const scalar = (sql: string): number => queryGet<CountRow>(db, sql)!.n;\n const total = scalar(`SELECT COUNT(*) n ${base} ${openWhere}`);\n const sessions_flagged = scalar(`SELECT COUNT(DISTINCT f.session_id) n ${base} ${openWhere}`);\n const dismissed = attached ? scalar(`SELECT COUNT(*) n FROM findings f LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id WHERE d.finding_id IS NOT NULL`) : 0;\n const muted = attached ? scalar(`SELECT COUNT(*) n ${base} WHERE d.finding_id IS NULL AND ${MUTED_SQL}`) : 0;\n\n const by_severity = queryAll<SecuritySummary[\"by_severity\"][number]>(\n db,\n `SELECT f.severity AS severity, COUNT(*) n ${base} ${openWhere} GROUP BY f.severity ORDER BY ${SEVERITY_RANK_SQL} DESC`,\n );\n const by_category = queryAll<SecuritySummary[\"by_category\"][number]>(\n db,\n `SELECT f.category AS category, COUNT(*) n ${base} ${openWhere} GROUP BY f.category ORDER BY n DESC`,\n );\n const by_rule = queryAll<SecuritySummary[\"by_rule\"][number]>(\n db,\n `SELECT f.rule_id AS rule_id, f.category AS category, f.title AS title, COUNT(*) n, MAX(${SEVERITY_RANK_SQL}) AS rank\n ${base} ${openWhere} GROUP BY f.rule_id ORDER BY rank DESC, n DESC`,\n );\n return { total, sessions_flagged, dismissed, muted, by_severity, by_category, by_rule, categories };\n}\n","/** Shared read-only SQL helpers for the server. */\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DB } from \"./db.js\";\n\n/** Bound parameters, as `node:sqlite` wants them. Call sites pass `unknown` and cast through here. */\nconst bind = (params: unknown[]): SQLInputValue[] => params as SQLInputValue[];\n\n/** Whether a table exists — the server opens the DB read-only, so a not-yet-ingested schema (e.g. no\n * workflow_results table) must degrade gracefully rather than throw. */\nexport function tableExists(db: DB, name: string): boolean {\n return !!db.prepare(\"SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?\").get(name);\n}\n\n/**\n * Typed `prepare(sql).all(...params)`. The caller names the row shape `T` once (a `@agent-lens/contracts`\n * row, a server-local derived shape from `rows.ts`, or an inline shape) and the cast lives here instead\n * of at every call site — so a column the query doesn't actually select is a compile error, not a silent\n * `undefined` at runtime. SQLite has no static knowledge of a query's columns, so the single cast\n * here is the unavoidable boundary; keep it the only one.\n */\nexport function queryAll<T>(db: DB, sql: string, ...params: unknown[]): T[] {\n return db.prepare(sql).all(...bind(params)) as unknown as T[];\n}\n\n/** Typed `prepare(sql).get(...params)` — returns the row or `undefined`. See {@link queryAll}. */\nexport function queryGet<T>(db: DB, sql: string, ...params: unknown[]): T | undefined {\n return db.prepare(sql).get(...bind(params)) as T | undefined;\n}\n\n/**\n * Append an inclusive `from`/`to` date-range filter on `col` to a WHERE-clause accumulator. Both ends\n * compare the DATE part (`date(col) >= date(?)`) so a picked `to` day includes that whole day — a plain\n * `col <= '2026-07-14'` would drop everything after midnight. `col` is an internal column expression\n * (e.g. `s.started_at`), never user input. Mutates `clauses`/`params` in place.\n */\nexport function pushDateRange(clauses: string[], params: unknown[], col: string, from?: string, to?: string): void {\n if (from) {\n clauses.push(`date(${col}) >= date(?)`);\n params.push(from);\n }\n if (to) {\n clauses.push(`date(${col}) <= date(?)`);\n params.push(to);\n }\n}\n\n/** The subagent-meta LEFT JOIN, present only when the `session_meta` table exists (pre-ingest DBs). */\nexport function metaJoin(hasMeta: boolean): string {\n return hasMeta ? \"LEFT JOIN session_meta sm ON sm.session_id = s.id\" : \"\";\n}\n\n/** The subagent-meta column projection — real columns when `session_meta` exists, else NULL stand-ins\n * so the row shape is stable regardless of whether the table has been ingested yet. Pairs with\n * {@link metaJoin}; leads with a comma to append to a SELECT list. */\nexport function metaProjection(hasMeta: boolean): string {\n return hasMeta\n ? \", sm.agent_type, sm.agent_description, sm.spawn_depth\"\n : \", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth\";\n}\n\n/**\n * Resolve a request's `sort`/`dir` into an `ORDER BY` fragment. SQLite can't parameterize an ORDER BY\n * expression, so it has to be interpolated — which makes this the one place a request value could\n * reach the SQL text. It can't: the expression is always a value from the caller's literal `columns`\n * map (an unknown key falls back to `fallback`, which must itself be a key of that map), and the\n * direction collapses to one of two literals. Route the interpolation through here, never build it\n * inline, and the invariant stays next to the string it protects.\n */\nexport function orderBy<K extends string>(columns: Record<K, string>, sort: string | undefined, fallback: K, dir?: string): string {\n // Object.hasOwn, not `in` / `map[key] ?? fallback`: both walk the prototype chain, so `?sort=toString`\n // resolves to Function.prototype.toString and its SOURCE TEXT lands in the query (a 500, not an\n // injection — but exactly what this helper exists to prevent).\n const col = sort !== undefined && Object.hasOwn(columns, sort) ? columns[sort as K] : columns[fallback];\n return `${col} ${dir === \"asc\" ? \"ASC\" : \"DESC\"}`;\n}\n\n/**\n * Resolve a request's `?limit=` into a row count within `[1, max]`.\n *\n * `Math.min(Number(q.limit) || 50, max)` looks equivalent and is not: it guards only the UPPER bound,\n * so `?limit=-1` yields -1, and SQLite reads a negative LIMIT as \"no limit\" — the page cap is gone and\n * the whole table comes back in one response. Non-integers are floored for the same reason `offset`\n * needs it (see {@link pageOffset}).\n */\nexport function pageLimit(raw: unknown, fallback: number, max: number): number {\n const n = Math.floor(Number(raw));\n if (!Number.isFinite(n) || n < 1) return Math.min(fallback, max);\n return Math.min(n, max);\n}\n\n/**\n * Resolve a request's `?offset=` into a non-negative integer.\n *\n * `Number(q.offset) || 0` passes a non-integer straight through — `?offset=1.5` is truthy, reaches\n * SQLite, and throws (a 500) because it will not bind a float to an integer parameter.\n */\nexport function pageOffset(raw: unknown): number {\n const n = Math.floor(Number(raw));\n return Number.isFinite(n) && n > 0 ? n : 0;\n}\n\n/** Append `value` to the list stored at `key`, creating the list on first use. Group-by-key is the\n * shape half this module's loaders end in (findings by tool call, tool calls by event). */\nexport function pushGrouped<K, V>(map: Map<K, V[]>, key: K, value: V): void {\n const list = map.get(key);\n if (list) list.push(value);\n else map.set(key, [value]);\n}\n","/**\n * Per-model token-usage aggregation + cost, shared by the session detail, workflow detail, session\n * list, and dashboard queries. Keeping the `SELECT model, SUM(...) …` shape, the column aliases\n * (`i/o/cw/cr`) and the cache-aware cost roll-up in one place makes a pricing or column change a\n * single edit rather than one per call site.\n */\nimport { costForUsage, unpricedModels } from \"@agent-lens/core\";\nimport { type DB } from \"./db.js\";\nimport { queryAll } from \"./sql-util.js\";\n\n/** A per-model token-usage aggregate row: input / output / cache-write / cache-read sums. */\nexport interface UsageRow {\n model: string | null;\n i: number;\n o: number;\n cw: number;\n cr: number;\n}\n\n/** The token categories, kept split so the UI can show input/output/cache-write/cache-read separately. */\nexport interface TokenSplit {\n input: number;\n output: number;\n cache_creation: number;\n cache_read: number;\n}\n\n/** The `SUM(...)` column list every usage query shares (aliased i/o/cw/cr). */\nexport const USAGE_SUMS =\n \"SUM(input_tokens) i, SUM(output_tokens) o, SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr\";\n\n/** Per-model token usage for one session. */\nexport function sessionUsage(db: DB, sessionId: string): UsageRow[] {\n return queryAll<UsageRow>(db, `SELECT model, ${USAGE_SUMS} FROM token_usage WHERE session_id = ? GROUP BY model`, sessionId);\n}\n\n/** Sum the split categories of a set of usage rows into a fresh {@link TokenSplit}. */\nexport function splitOf(rows: UsageRow[]): TokenSplit {\n const split: TokenSplit = { input: 0, output: 0, cache_creation: 0, cache_read: 0 };\n for (const u of rows) {\n split.input += u.i;\n split.output += u.o;\n split.cache_creation += u.cw;\n split.cache_read += u.cr;\n }\n return split;\n}\n\n/** Total tokens (all categories, cache included) for a set of usage rows. */\nexport function tokensOf(split: TokenSplit): number {\n return split.input + split.output + split.cache_creation + split.cache_read;\n}\n\n/**\n * The models in these rows that have real token usage but no rate, so {@link costOf} understates the\n * total. Empty for a fully-priced scope. `<synthetic>` is excluded — it never had an API cost.\n */\nexport function unpricedOf(rows: UsageRow[]): string[] {\n return unpricedModels(rows.filter((u) => u.i || u.o || u.cw || u.cr).map((u) => u.model));\n}\n\n/** Cache-aware USD cost (per-model rates) for a set of usage rows, rounded to 4dp. */\nexport function costOf(rows: UsageRow[]): number {\n let cost = 0;\n for (const u of rows) {\n cost += costForUsage(u.model, {\n input_tokens: u.i,\n output_tokens: u.o,\n cache_creation_input_tokens: u.cw,\n cache_read_input_tokens: u.cr,\n });\n }\n return Number(cost.toFixed(4));\n}\n","/**\n * Agent Lens — Fastify app factory (ADR-005: read-only over the SQLite store).\n *\n * `createApp(db, opts)` builds the route tree against an already-open DB handle so the same app can\n * be driven by the CLI entry (index.ts, with .listen) and by tests (app.inject, no socket).\n */\nimport { existsSync } from \"node:fs\";\nimport Fastify, { type FastifyInstance, type FastifyReply } from \"fastify\";\nimport fastifyStatic from \"@fastify/static\";\nimport { resolveVersion } from \"@agent-lens/core\";\nimport { renderSessionExport, parseRedactionLevel } from \"./export.js\";\nimport { type DB, lastIngested, schemaStatus, listSources, listProjects, listModels, listSessions, getSession, getWorkflow, listSkills, getSkill, listFindings, openFindingIds, securitySummary, listFiles, getFileTimeline, safeJson } from \"./db.js\";\nimport { dashboardOverview, dashboardTimeseries, dashboardBreakdowns, type DashFilters } from \"./dashboard.js\";\nimport { writeBlocked, runRefresh, LOOPBACK_HOSTS } from \"./refresh.js\";\nimport { openTriage, dismiss, reopen, muteRule, unmute, listMutes, type TriageDB, type MuteScope } from \"./triage.js\";\nimport { about, type AboutContext } from \"./about.js\";\nimport { PREFS_SCHEMA_SQL, getPref, setPref } from \"./prefs.js\";\nimport { pageLimit, pageOffset } from \"./sql-util.js\";\n\nexport interface CreateAppOpts {\n /** Absolute path to the built web SPA; when present it is served with a history fallback. */\n webDist?: string | null;\n /** Path to the writable security-triage store (ADR-018). When set, it is opened read-write for\n * triage writes and ATTACHed to the read handle so the findings list can JOIN triage state. */\n triageDbPath?: string | null;\n /** Reject any request whose Host authority is not loopback (DNS-rebinding defense, HIGH-001).\n * Defaults to true; the intentional non-local bind (AGENT_LENS_ALLOW_NONLOCAL) passes false. */\n enforceLoopbackHost?: boolean;\n /** Startup facts backing GET /api/about (ADR-027). Omit and the route is not registered — an\n * embedder that never resolved a db path has nothing truthful to report. */\n about?: AboutContext;\n}\n\n/** Structured 404 envelope. Every not-found response shares this shape so a client can uniformly read\n * `err.error.code` — the rest of the API already uses `{ error: { code, message } }`. */\nfunction notFound(reply: FastifyReply, message = \"not found\") {\n return reply.code(404).send({ error: { code: \"NOT_FOUND\", message } });\n}\n\nexport async function createApp(db: DB, opts: CreateAppOpts = {}): Promise<FastifyInstance> {\n const app = Fastify({ logger: false });\n\n // HIGH-001: binding 127.0.0.1 stops direct remote connections but NOT DNS rebinding — a page on\n // evil.com re-resolved to 127.0.0.1 makes same-origin requests the browser will let it read. Reads\n // are unauthenticated, so without a Host check the whole transcript store is exfiltratable. Reject\n // any non-loopback Host up front. This also removes the residual write ambiguity (Origin-absent).\n if (opts.enforceLoopbackHost !== false) {\n app.addHook(\"onRequest\", async (req, reply) => {\n // Host is case-insensitive (RFC 3986) and LOOPBACK_HOSTS is lowercase, so `Host: LOCALHOST`\n // would otherwise be rejected — over-strict, but a needless 403 all the same.\n const host = (req.headers.host ?? \"\").replace(/:\\d+$/, \"\").toLowerCase();\n if (!LOOPBACK_HOSTS.has(host)) {\n return reply.code(403).send({ error: { code: \"FORBIDDEN_HOST\", message: \"non-loopback Host rejected\" } });\n }\n });\n }\n\n // Baseline security headers on every response (incl. static SPA + errors). Anti-clickjacking\n // (`X-Frame-Options`/`frame-ancestors`) matters here because framing the loopback UI could drive\n // the Origin-passing write routes. A full script/style CSP is deliberately omitted — it risks\n // breaking the SPA and adds little on a loopback-only, Host-guarded server (tracked as follow-up).\n app.addHook(\"onSend\", async (_req, reply, payload) => {\n reply.header(\"X-Frame-Options\", \"DENY\");\n reply.header(\"X-Content-Type-Options\", \"nosniff\");\n reply.header(\"Referrer-Policy\", \"no-referrer\");\n reply.header(\"Content-Security-Policy\", \"frame-ancestors 'none'\");\n return payload;\n });\n\n // Security triage store (ADR-018): a separate writable SQLite, ATTACHed to the read handle so the\n // findings queries can JOIN dismissed/muted state, while writes go through `triageDb`. The analytics\n // handle stays read-only. Absent → the security endpoints degrade to the un-triaged (all-open) view.\n let triageDb: TriageDB | null = null;\n if (opts.triageDbPath) {\n triageDb = openTriage(opts.triageDbPath); // creates the file + schema before we ATTACH it\n triageDb.exec(PREFS_SCHEMA_SQL); // UI-prefs table rides the same writable sidecar (see prefs.ts)\n // ATTACH takes a literal, not a bound parameter, so the path must be interpolated. It is an\n // OPERATOR-supplied path (config/CLI), never request data, and single quotes are doubled per SQL\n // string-literal escaping — the standard workaround, called out here so it doesn't read as an\n // oversight.\n db.exec(`ATTACH DATABASE '${opts.triageDbPath.replace(/'/g, \"''\")}' AS triage`);\n app.addHook(\"onClose\", async () => triageDb?.close());\n }\n\n // Shared CSRF + availability guard for the triage write routes (same posture as /api/refresh).\n const guardWrite = (req: any, reply: any): boolean => {\n if (writeBlocked(req.headers)) {\n reply.code(403).send({ error: { code: \"FORBIDDEN_ORIGIN\", message: \"cross-origin write blocked\" } });\n return false;\n }\n if (!triageDb) {\n reply.code(503).send({ error: { code: \"TRIAGE_UNAVAILABLE\", message: \"triage store not configured\" } });\n return false;\n }\n return true;\n };\n\n app.get(\"/api/health\", async () => {\n const s = schemaStatus(db);\n // `version` rides on health rather than /api/about because it is the one diagnostic safe to\n // publish: health IS exported to the static Pages snapshot, so the badge works there too\n // (ADR-027). resolveVersion() is memoized — health is polled and must not spawn git per request.\n const v = resolveVersion();\n return {\n ok: true,\n last_ingested: lastIngested(db),\n schema_version: s.db_version,\n schema_stale: s.stale,\n version: v.version,\n version_source: v.source,\n };\n });\n\n // Diagnostics (ADR-027). Read-only, and deliberately NOT in the static snapshot: it carries\n // absolute filesystem paths. Registered only when startServer passed the context it resolved —\n // re-resolving here would ignore `serve --db` and report a path nobody is reading.\n if (opts.about) {\n const ctx = opts.about;\n app.get(\"/api/about\", async () => about(db, ctx));\n }\n\n // The one write-action on this read-only server: run a collect + ingest pass on the host so the UI\n // can pull in new transcripts on demand (ADR-015). Guarded against cross-site CSRF (Origin) and\n // concurrent runs (single-instance lock → 409). See refresh.ts.\n app.post(\"/api/refresh\", async (req, reply) => {\n if (writeBlocked(req.headers)) {\n return reply.code(403).send({ error: { code: \"FORBIDDEN_ORIGIN\", message: \"cross-origin refresh blocked\" } });\n }\n let result;\n try {\n result = runRefresh();\n } catch (e: any) {\n // Don't leak the raw exception (may include host filesystem paths, LOW-003) to the client; the\n // recipient is the local user but a generic message keeps this safe if ever exposed. Detail → log.\n console.error(\"agent-lens-server: refresh failed:\", e);\n return reply.code(500).send({ error: { code: \"REFRESH_FAILED\", message: \"refresh failed; see server logs\" } });\n }\n if (!result) {\n return reply.code(409).send({ error: { code: \"REFRESH_IN_PROGRESS\", message: \"a collect/ingest run is already in progress\" } });\n }\n return { ok: true, collected: result.collected, last_ingested: lastIngested(db) };\n });\n app.get(\"/api/sources\", async () => listSources(db));\n app.get(\"/api/projects\", async () => listProjects(db));\n app.get(\"/api/models\", async () => listModels(db));\n\n // Dashboard aggregates. All read-only; filters: source, from, to.\n const dashFilters = (req: any): DashFilters => {\n const q = req.query as Record<string, string>;\n return { source: q.source, from: q.from, to: q.to };\n };\n app.get(\"/api/dashboard/overview\", async (req) => dashboardOverview(db, dashFilters(req)));\n app.get(\"/api/dashboard/timeseries\", async (req) => {\n const q = req.query as Record<string, string>;\n return dashboardTimeseries(db, dashFilters(req), q.bucket);\n });\n app.get(\"/api/dashboard/breakdowns\", async (req) => dashboardBreakdowns(db, dashFilters(req)));\n\n // UI preferences (chart/column visibility, per-chart toggles). Stored in the writable sidecar\n // (prefs.ts); localStorage on the client is an optimistic cache. GET degrades to `{value:null}` when\n // no writable store is configured so the client simply keeps its local value. PUT reuses the same\n // CSRF+availability guard as the other writes.\n const PREF_KEY = /^[a-z0-9._-]{1,64}$/i;\n app.get(\"/api/prefs/:key\", async (req, reply) => {\n const { key } = req.params as { key: string };\n if (!PREF_KEY.test(key)) return reply.code(400).send({ error: { code: \"BAD_KEY\", message: \"invalid pref key\" } });\n if (!triageDb) return { value: null };\n const raw = getPref(triageDb, key);\n // safeJson tolerates a malformed stored value (returns null) rather than throwing a 500.\n return { value: safeJson(raw) };\n });\n app.put(\"/api/prefs/:key\", async (req, reply) => {\n const { key } = req.params as { key: string };\n if (!PREF_KEY.test(key)) return reply.code(400).send({ error: { code: \"BAD_KEY\", message: \"invalid pref key\" } });\n if (!guardWrite(req, reply)) return;\n const body = req.body as { value?: unknown };\n setPref(triageDb!, key, JSON.stringify(body?.value ?? null));\n return { ok: true };\n });\n\n app.get(\"/api/sessions\", async (req) => {\n const q = req.query as Record<string, string>;\n return listSessions(db, {\n source: q.source,\n project: q.project,\n model: q.model,\n q: q.q,\n from: q.from,\n to: q.to,\n kind: q.kind === \"main\" || q.kind === \"subagent\" ? q.kind : undefined,\n severity: q.severity ? q.severity.split(\",\").filter(Boolean) : undefined,\n errorType: q.error_type ? q.error_type.split(\",\").filter(Boolean) : undefined,\n sort: ([\"started\", \"title\", \"turns\", \"tokens\", \"cost\", \"duration\", \"errors\", \"security\"] as const).find((s) => s === q.sort),\n dir: q.dir === \"asc\" ? \"asc\" : q.dir === \"desc\" ? \"desc\" : undefined,\n limit: pageLimit(q.limit, 50, 200),\n offset: pageOffset(q.offset),\n });\n });\n\n app.get(\"/api/sessions/:id\", async (req, reply) => {\n const { id } = req.params as { id: string };\n const result = getSession(db, id);\n if (!result) return notFound(reply);\n return result;\n });\n\n app.get(\"/api/workflows/:run_id\", async (req, reply) => {\n const { run_id } = req.params as { run_id: string };\n const result = getWorkflow(db, run_id);\n if (!result) return notFound(reply);\n return result;\n });\n\n // Security findings (ADR-017). Read-only; browsable list + roll-up summary for the /security page,\n // the transcript inline badges (served via /api/sessions/:id), and the Dashboard KPI.\n app.get(\"/api/security/summary\", async () => securitySummary(db));\n const findingFilters = (q: Record<string, string>) => ({\n severity: q.severity,\n category: q.category,\n rule: q.rule,\n session: q.session,\n source: q.source,\n project: q.project,\n from: q.from,\n to: q.to,\n status: ([\"open\", \"dismissed\", \"muted\", \"all\"] as const).find((s) => s === q.status),\n });\n app.get(\"/api/security/findings\", async (req) => {\n const q = req.query as Record<string, string>;\n return listFindings(db, {\n ...findingFilters(q),\n sort: ([\"severity\", \"session\", \"rule\", \"category\", \"time\"] as const).find((s) => s === q.sort),\n dir: q.dir === \"asc\" ? \"asc\" : q.dir === \"desc\" ? \"desc\" : undefined,\n limit: pageLimit(q.limit, 50, 5000),\n offset: pageOffset(q.offset),\n });\n });\n\n // Triage writes (ADR-018): CSRF-guarded, loopback-only, same posture as /api/refresh.\n app.get(\"/api/security/mutes\", async () => (triageDb ? listMutes(triageDb) : []));\n app.post(\"/api/security/dismiss\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { ids?: string[]; note?: string };\n const n = dismiss(triageDb!, Array.isArray(b.ids) ? b.ids : [], b.note ?? null);\n return { ok: true, dismissed: n };\n });\n app.post(\"/api/security/reopen\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { ids?: string[] };\n const n = reopen(triageDb!, Array.isArray(b.ids) ? b.ids : []);\n return { ok: true, reopened: n };\n });\n // Dismiss every OPEN finding matching a filter (bulk cleanup) — collects ids server-side, then marks.\n app.post(\"/api/security/dismiss-matching\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { filter?: Record<string, string>; note?: string };\n const ids = openFindingIds(db, findingFilters(b.filter ?? {}));\n const n = dismiss(triageDb!, ids, b.note ?? null);\n return { ok: true, dismissed: n };\n });\n app.post(\"/api/security/mute\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { rule_id?: string; scope?: string; scope_id?: string; note?: string };\n if (!b.rule_id) return reply.code(400).send({ error: { code: \"BAD_REQUEST\", message: \"rule_id required\" } });\n const scope = ([\"global\", \"project\", \"source\"] as const).find((s) => s === b.scope) ?? \"global\";\n muteRule(triageDb!, b.rule_id, scope as MuteScope, b.scope_id ?? \"\", b.note ?? null);\n return { ok: true };\n });\n app.post(\"/api/security/unmute\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { rule_id?: string; scope?: string; scope_id?: string };\n if (!b.rule_id) return reply.code(400).send({ error: { code: \"BAD_REQUEST\", message: \"rule_id required\" } });\n const scope = ([\"global\", \"project\", \"source\"] as const).find((s) => s === b.scope) ?? \"global\";\n const n = unmute(triageDb!, b.rule_id, scope as MuteScope, b.scope_id ?? \"\");\n return { ok: true, unmuted: n };\n });\n\n app.get(\"/api/skills\", async (req) => {\n const q = req.query as Record<string, string>;\n return listSkills(db, { q: q.q, source: q.source, project: q.project });\n });\n\n app.get(\"/api/skills/:name\", async (req, reply) => {\n const { name } = req.params as { name: string };\n let decoded: string;\n try {\n decoded = decodeURIComponent(name);\n } catch {\n // A malformed percent-escape (e.g. `%zz`) throws URIError — that's a bad request, not a 500.\n return reply.code(400).send({ error: { code: \"BAD_NAME\", message: \"invalid skill name encoding\" } });\n }\n const result = getSkill(db, decoded);\n if (!result) return notFound(reply);\n return result;\n });\n\n // File-modification provenance (ADR-022): the /files list and the per-file timeline. The file\n // path travels as a query param (it contains slashes), not a route segment.\n app.get(\"/api/files\", async (req) => {\n const q = req.query as Record<string, string>;\n const sort = ([\"last_ts\", \"first_ts\", \"changes\", \"sessions\", \"path\"] as const).find((s) => s === q.sort);\n return listFiles(db, {\n q: q.q,\n source: q.source,\n project: q.project,\n sort,\n dir: q.dir === \"asc\" ? \"asc\" : \"desc\",\n limit: pageLimit(q.limit, 50, 200),\n offset: pageOffset(q.offset),\n });\n });\n\n app.get(\"/api/file\", async (req, reply) => {\n const q = req.query as Record<string, string>;\n if (!q.path) return reply.code(400).send({ error: { code: \"MISSING_PATH\", message: \"missing path\" } });\n const result = getFileTimeline(db, q.path, q.project || undefined);\n if (!result) return notFound(reply);\n return result;\n });\n\n app.get(\"/api/sessions/:id/export.md\", async (req, reply) => {\n const { id } = req.params as { id: string };\n // Redaction defaults ON (selective secret/PII masking). `structure` = aggressive scrub;\n // `off` = explicit verbatim opt-out. Anything unrecognized falls back to the safe default.\n const level = parseRedactionLevel((req.query as { redact?: string }).redact);\n const out = renderSessionExport(db, id, level);\n if (!out) return notFound(reply);\n reply\n .header(\"content-type\", \"text/markdown; charset=utf-8\")\n .header(\"content-disposition\", `attachment; filename=\"${out.filename}\"`)\n .send(out.markdown);\n });\n\n // Serve the built SPA (if present) with a history fallback for client routes.\n if (opts.webDist && existsSync(opts.webDist)) {\n await app.register(fastifyStatic, { root: opts.webDist });\n app.setNotFoundHandler((req, reply) => {\n if (req.url.startsWith(\"/api/\")) return notFound(reply);\n // A missing *file* must 404 honestly. The tempting blanket fallback breaks the case where an\n // open tab outlives a rebuild: its index.html names hashed chunks the new dist no longer has\n // (`emptyOutDir`), so the lazy `import()` asks for /assets/SessionView-<old>.js and gets HTML\n // back with `content-type: text/html`. The browser refuses it on MIME grounds, and the error\n // the SPA sees is a module-load failure rather than a plain 404 — harder to recognize as\n // \"your build is stale\". Only extensionless paths are client routes worth falling back for.\n if (/\\.[^/.]+$/.test(req.url.split(\"?\")[0])) return notFound(reply);\n return reply.sendFile(\"index.html\");\n });\n }\n\n return app;\n}\n","/**\n * Session → shareable Markdown, shared by the HTTP route and the CLI `export` command so the\n * DB-row → MarkdownEvent mapping and filename convention live in exactly one place.\n * Redaction defaults ON (selective secret/PII masking); see @agent-lens/core exportMarkdown.\n */\nimport { exportMarkdown, type MarkdownEvent, type RedactionLevel } from \"@agent-lens/core\";\nimport { type DB, getSession } from \"./db.js\";\n\n/** Normalize a raw ?redact= / --level value to a valid level, defaulting to the safe `secrets`. */\nexport function parseRedactionLevel(raw: string | undefined): RedactionLevel | \"off\" {\n return raw === \"off\" ? \"off\" : raw === \"structure\" ? \"structure\" : \"secrets\";\n}\n\n/** Render session `id` to Markdown at the given level, or null if the session doesn't exist. */\nexport function renderSessionExport(\n db: DB,\n id: string,\n level: RedactionLevel | \"off\",\n): { markdown: string; filename: string } | null {\n const result = getSession(db, id);\n if (!result) return null;\n const s = result.session;\n const events: MarkdownEvent[] = result.events.map((e) => ({\n type: e.type,\n role: e.role,\n timestamp: e.timestamp,\n text: e.text,\n thinking: e.thinking,\n toolCalls: e.toolCalls.map((t: any) => ({\n tool_name: t.tool_name,\n skill_name: t.skill_name,\n agent_type: t.agent_type,\n input_json: t.input_json,\n status: t.status,\n })),\n }));\n const { markdown } = exportMarkdown(\n { id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at },\n events,\n { level },\n );\n const suffix = level === \"off\" ? \"\" : \".redacted\";\n return { markdown, filename: `session-${filenameSafe(id.slice(0, 8))}${suffix}.md` };\n}\n\n/**\n * Reduce a session id to characters that are safe in a `content-disposition` filename.\n *\n * The header is built by interpolation — `attachment; filename=\"<name>\"` (app.ts) — so a `\"` in the\n * id would close the quoted-string early and the rest would be parsed as header parameters. Ids are\n * ours today (uuids and slugs), which makes this defence-in-depth rather than a live hole, but the id\n * is a path segment the caller supplies and nothing else validates it before it reaches the header.\n * Sanitizing at the point the filename is BUILT keeps the guarantee with the naming convention\n * instead of leaving it to every future caller of this function.\n *\n * Anything outside `[A-Za-z0-9._-]` collapses to `_`, including the CR/LF that would otherwise permit\n * header injection. An id that is entirely unsafe still yields a usable name rather than an empty one.\n */\nfunction filenameSafe(s: string): string {\n const cleaned = s.replace(/[^A-Za-z0-9._-]/g, \"_\");\n return cleaned === \"\" ? \"session\" : cleaned;\n}\n","/**\n * Agent Lens — dashboard aggregation queries. Read-only; everything is computed as\n * server-side aggregates (GROUP BY over indexed columns) so the charts plot bounded series, not\n * raw rows — this is what lets the UI scale to years of data. Token series are always kept split\n * (input / output / cache-creation / cache-read); cache-read is never folded into a single\n * \"tokens\" number because it dominates and misleads. Cost is derived via the shared pricing table.\n */\nimport { costForUsage, rateForModel, errorKind, type ToolErrorType } from \"@agent-lens/core\";\nimport type { DashBreakdowns, DashOverview, DashTimeseries, TimeseriesPoint } from \"@agent-lens/contracts\";\nimport type { DB } from \"./db.js\";\nimport { tableExists, pushDateRange, queryAll, queryGet } from \"./sql-util.js\";\nimport type {\n BucketCountRow,\n BucketErrorRow,\n BucketUsageAggRow,\n CountRow,\n ModelBreakdownRow,\n OverviewCountsRow,\n SpanRow,\n UsageAggRow,\n WorkflowStatusAggRow,\n} from \"./rows.js\";\n\nexport interface DashFilters {\n source?: string;\n from?: string;\n to?: string;\n}\n\n/** A WHERE-clause fragment and its bound parameters, threaded through every aggregate below. */\ntype Where = { sql: string; params: unknown[] };\n\n/** WHERE clause + params over the `sessions` alias `s` (started_at / source_id). */\nfunction sessionWhere(f: DashFilters): Where {\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.source) (where.push(\"s.source_id = ?\"), params.push(f.source));\n // Date-inclusive on both ends (compare the DATE part) so a picked `to` day includes that day's\n // events — a plain `started_at <= '2026-07-14'` would drop everything after 2026-07-14T00:00.\n pushDateRange(where, params, \"s.started_at\", f.from, f.to);\n return { sql: where.length ? `WHERE ${where.join(\" AND \")}` : \"\", params };\n}\n\ninterface Split {\n input: number;\n output: number;\n cache_creation: number;\n cache_read: number;\n}\nconst zeroSplit = (): Split => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });\n\n/** Sum a grouped usage row into a split accumulator. */\nfunction addUsage(acc: Split, r: UsageAggRow) {\n acc.input += r.i ?? 0;\n acc.output += r.o ?? 0;\n acc.cache_creation += r.cw ?? 0;\n acc.cache_read += r.cr ?? 0;\n}\n\n/** The per-model cost arguments, unpacked from a usage aggregate's short SQL aliases. */\nconst usageForCost = (u: UsageAggRow) => ({\n input_tokens: u.i ?? 0,\n output_tokens: u.o ?? 0,\n cache_creation_input_tokens: u.cw ?? 0,\n cache_read_input_tokens: u.cr ?? 0,\n});\n\n/**\n * p50/p95 (nearest-rank) plus the sample size, computed **in SQL**.\n *\n * `values` is a `SELECT <expr> v FROM …` returning one row per observation, unordered; this wraps it\n * in a CTE and picks the ranked element with `LIMIT 1 OFFSET`. It used to pull every duration into JS\n * and sort there — one row per turn, per request, growing with the corpus forever.\n *\n * The offset is `ceil(p/100 × n) - 1` floored at 0, which is exactly the rank the previous JS\n * `pct()` selected, so stored percentiles do not shift. An empty set yields 0 (the CTE returns NULL,\n * coalesced), also matching the old behaviour.\n */\nfunction percentiles(db: DB, values: string, params: unknown[]): { p50: number; p95: number; count: number } {\n const at = (p: number) =>\n `(SELECT COALESCE((SELECT v FROM vals ORDER BY v\n LIMIT 1 OFFSET MAX(0, CAST(ceil(${p} / 100.0 * (SELECT c FROM n)) AS INTEGER) - 1)), 0))`;\n const row = queryGet<{ p50: number; p95: number; count: number }>(\n db,\n // MATERIALIZED matters: the three selects below each reference `vals`, and without the hint\n // SQLite inlines the CTE and re-runs the underlying scan once per percentile.\n `WITH vals AS MATERIALIZED (${values}), n AS (SELECT COUNT(*) c FROM vals)\n SELECT ${at(50)} AS p50, ${at(95)} AS p95, (SELECT c FROM n) AS count`,\n ...params,\n );\n return row ?? { p50: 0, p95: 0, count: 0 };\n}\n\n/** WHERE over `workflow_results` own columns (its own source_id / started_at, not the sessions alias). */\nfunction workflowWhere(f: DashFilters): Where {\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.source) (where.push(\"source_id = ?\"), params.push(f.source));\n pushDateRange(where, params, \"started_at\", f.from, f.to);\n return { sql: where.length ? `WHERE ${where.join(\" AND \")}` : \"\", params };\n}\n\n/** Aggregate the async-workflow runs (schema v9+): counts by status, success rate over decided runs\n * (completed vs failed; in-flight `running` excluded from the denominator), and token/duration rollups.\n * Guarded so a pre-v9 DB (no `workflow_results` table) returns an all-zero shape instead of throwing. */\nfunction workflowAgg(db: DB, f: DashFilters): DashOverview[\"workflows\"] {\n const empty = { total: 0, by_status: [], completed: 0, failed: 0, success_rate: 0, total_tokens: 0, avg_duration_ms: 0 };\n if (!tableExists(db, \"workflow_results\")) return empty;\n const ww = workflowWhere(f);\n const rows = queryAll<WorkflowStatusAggRow>(\n db,\n `SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,\n SUM(COALESCE(total_tokens, 0)) tokens,\n SUM(COALESCE(duration_ms, 0)) dur,\n SUM(CASE WHEN duration_ms IS NOT NULL THEN 1 ELSE 0 END) dur_n\n FROM workflow_results ${ww.sql} GROUP BY status ORDER BY n DESC`,\n ...ww.params,\n );\n let total = 0, completed = 0, failed = 0, tokens = 0, durSum = 0, durN = 0;\n const by_status = rows.map((r) => {\n total += r.n;\n if (r.status === \"completed\") completed += r.n;\n else if (r.status === \"failed\") failed += r.n;\n tokens += r.tokens ?? 0;\n durSum += r.dur ?? 0;\n durN += r.dur_n ?? 0;\n return { status: r.status, n: r.n };\n });\n const decided = completed + failed;\n return {\n total,\n by_status,\n completed,\n failed,\n success_rate: decided ? completed / decided : 0,\n total_tokens: tokens,\n avg_duration_ms: durN ? Math.round(durSum / durN) : 0,\n };\n}\n\nexport function dashboardOverview(db: DB, f: DashFilters): DashOverview {\n const w = sessionWhere(f);\n\n const counts = queryGet<OverviewCountsRow>(\n db,\n `SELECT\n COUNT(*) sessions,\n SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,\n SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,\n SUM(turn_count) turns,\n COUNT(DISTINCT project_id) projects\n FROM sessions s ${w.sql}`,\n ...w.params,\n )!;\n\n const toolCount = queryGet<CountRow>(\n db,\n `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id ${w.sql}`,\n ...w.params,\n )!.n;\n\n // Per-model usage → token split + cache-aware cost; track unpriced models honestly.\n const usage = queryAll<UsageAggRow>(\n db,\n `SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,\n SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr\n FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}\n GROUP BY t.model`,\n ...w.params,\n );\n const tokens = zeroSplit();\n let cost = 0;\n const unpriced: string[] = [];\n for (const u of usage) {\n addUsage(tokens, u);\n cost += costForUsage(u.model, usageForCost(u));\n if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr)) unpriced.push(u.model);\n }\n const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;\n\n // Turn-duration percentiles (work cadence), excluding null durations.\n const turnDur = percentiles(\n db,\n `SELECT tn.duration_ms v FROM turns tn JOIN sessions s ON s.id = tn.session_id\n ${w.sql ? w.sql + \" AND\" : \"WHERE\"} tn.duration_ms IS NOT NULL`,\n w.params,\n );\n\n // Session-length percentiles over MAIN sessions only (subagents share the parent's wall clock),\n // excluding null durations. Complements the per-turn cadence with an end-to-end task-length view.\n const sessDur = percentiles(\n db,\n `SELECT s.duration_ms v FROM sessions s\n ${w.sql ? w.sql + \" AND\" : \"WHERE\"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL`,\n w.params,\n );\n\n return {\n range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },\n sessions: counts.sessions ?? 0,\n sessions_main: counts.main ?? 0,\n sessions_subagent: counts.subagent ?? 0,\n turns: counts.turns ?? 0,\n projects: counts.projects ?? 0,\n tool_calls: toolCount ?? 0,\n tokens, // {input, output, cache_creation, cache_read}\n total_tokens: totalTokens,\n cache_read_ratio: totalTokens ? tokens.cache_read / totalTokens : 0,\n cost: Number(cost.toFixed(4)),\n unpriced_models: [...new Set(unpriced)].sort(),\n turn_duration_ms: turnDur,\n session_duration_ms: sessDur,\n workflows: workflowAgg(db, f),\n };\n}\n\ntype Bucket = \"day\" | \"week\" | \"month\";\n\n/** Pick a bucket so the series stays small regardless of how wide the range is. */\nfunction chooseBucket(db: DB, f: DashFilters): Bucket {\n const w = sessionWhere(f);\n const r = queryGet<SpanRow>(db, `SELECT MIN(started_at) mn, MAX(started_at) mx FROM sessions s ${w.sql}`, ...w.params);\n if (!r?.mn || !r?.mx) return \"day\";\n const spanDays = (Date.parse(r.mx) - Date.parse(r.mn)) / 86_400_000;\n if (spanDays <= 92) return \"day\";\n if (spanDays <= 730) return \"week\";\n return \"month\";\n}\n\nconst BUCKET_EXPR: Record<Bucket, string> = {\n day: \"strftime('%Y-%m-%d', s.started_at)\",\n week: \"strftime('%Y-W%W', s.started_at)\",\n month: \"strftime('%Y-%m', s.started_at)\",\n};\n\nexport function dashboardTimeseries(db: DB, f: DashFilters, bucketParam?: string): DashTimeseries {\n const bucket: Bucket = bucketParam === \"day\" || bucketParam === \"week\" || bucketParam === \"month\" ? bucketParam : chooseBucket(db, f);\n const w = sessionWhere(f);\n const expr = BUCKET_EXPR[bucket];\n\n const byBucket = new Map<string, TimeseriesPoint>();\n const get = (b: string): TimeseriesPoint => {\n let r = byBucket.get(b);\n if (!r) {\n r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0, failures: 0, rejections: 0 };\n byBucket.set(b, r);\n }\n return r;\n };\n\n for (const u of queryAll<BucketUsageAggRow>(\n db,\n `SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,\n SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr\n FROM token_usage t JOIN sessions s ON s.id = t.session_id\n ${w.sql} GROUP BY b, t.model`,\n ...w.params,\n )) {\n if (!u.b) continue;\n const r = get(u.b);\n addUsage(r, u);\n r.cost += costForUsage(u.model, usageForCost(u));\n }\n for (const s of queryAll<BucketCountRow>(db, `SELECT ${expr} b, COUNT(*) n FROM sessions s ${w.sql} GROUP BY b`, ...w.params)) {\n if (s.b) get(s.b).sessions = s.n;\n }\n for (const t of queryAll<BucketCountRow>(\n db,\n `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${w.sql} GROUP BY b`,\n ...w.params,\n )) {\n if (t.b) get(t.b).turns = t.n;\n }\n // Errored tool calls per bucket, split into genuine failures vs user/guardrail rejections (kind\n // derived from the stored error_type). Bucketed by the session's date, same as every other series.\n for (const e of queryAll<BucketErrorRow>(\n db,\n `SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${w.sql ? w.sql + \" AND\" : \"WHERE\"} tc.status = 'error' GROUP BY b, tc.error_type`,\n ...w.params,\n )) {\n if (!e.b) continue;\n const r = get(e.b);\n if (e.et && errorKind(e.et as ToolErrorType) === \"rejection\") r.rejections += e.n;\n else r.failures += e.n;\n }\n\n const series = [...byBucket.values()].sort((a, b) => a.bucket.localeCompare(b.bucket));\n for (const r of series) r.cost = Number(r.cost.toFixed(4));\n return { bucket, series };\n}\n\n/** Extend a session WHERE clause with one more condition, opening the clause when it is empty. */\nconst andWhere = (w: Where) => (w.sql ? w.sql + \" AND\" : \"WHERE\");\n\nfunction modelBreakdown(db: DB, w: Where): DashBreakdowns[\"by_model\"] {\n const modelRows = queryAll<ModelBreakdownRow>(\n db,\n // COALESCE the bucket label, exactly as by_source does for a null source_id: token_usage.model is\n // nullable and this GROUP BY — unlike the model FILTER list — does not exclude nulls, so a\n // null-model bucket reaches the chart. It used to render as a nameless bar.\n `SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,\n SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,\n COUNT(DISTINCT t.session_id) sessions\n FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}\n 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`,\n ...w.params,\n );\n return modelRows.map((u) => ({\n model: u.model,\n tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },\n total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),\n cost: Number(costForUsage(u.model, usageForCost(u)).toFixed(4)),\n sessions: u.sessions ?? 0,\n priced: !!rateForModel(u.model),\n }));\n}\n\nfunction sourceBreakdown(db: DB, w: Where): DashBreakdowns[\"by_source\"] {\n return queryAll<DashBreakdowns[\"by_source\"][number]>(\n db,\n `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns\n FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`,\n ...w.params,\n );\n}\n\n/** Category + complexity over MAIN sessions only (subagents inherit the parent's task). */\nfunction classificationBreakdowns(db: DB, w: Where): {\n byCategory: DashBreakdowns[\"by_category\"];\n byComplexity: DashBreakdowns[\"by_complexity\"];\n} {\n const mainW: Where = { sql: w.sql ? w.sql + \" AND s.is_sidechain = 0\" : \"WHERE s.is_sidechain = 0\", params: w.params };\n const byCategory = queryAll<DashBreakdowns[\"by_category\"][number]>(\n db,\n `SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id\n ${mainW.sql} GROUP BY c.category ORDER BY n DESC`,\n ...mainW.params,\n );\n const byComplexity = queryAll<DashBreakdowns[\"by_complexity\"][number]>(\n db,\n `SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id\n ${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`,\n ...mainW.params,\n );\n return { byCategory, byComplexity };\n}\n\nfunction toolBreakdown(db: DB, w: Where): DashBreakdowns[\"tools\"] {\n return queryAll<DashBreakdowns[\"tools\"][number]>(\n db,\n `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`,\n ...w.params,\n );\n}\n\n/** Skill firings by name, plus the per-version breakdown for the grouped skill bar chart: each\n * captured version's firing count, with the version id + its last-seen time so the chart can\n * stack/hover and deep-link to /skill/:name?v=<id>. Both share the same source/from/to filter. */\nfunction skillBreakdowns(db: DB, w: Where): {\n skills: DashBreakdowns[\"skills\"];\n skillVersions: DashBreakdowns[\"skill_versions\"];\n} {\n const skills = queryAll<DashBreakdowns[\"skills\"][number]>(\n db,\n `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`,\n ...w.params,\n );\n\n const skillVersions = queryAll<DashBreakdowns[\"skill_versions\"][number]>(\n db,\n `SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n\n FROM tool_calls tc\n JOIN skills sk ON sk.id = tc.skill_id\n JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.skill_id IS NOT NULL\n GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`,\n ...w.params,\n );\n\n return { skills, skillVersions };\n}\n\n/** Subagent fan-out: spawns by type, plus a per-(main-)session histogram of subagent calls. */\nfunction subagentFanoutBreakdown(db: DB, w: Where): DashBreakdowns[\"subagent_fanout\"] {\n const subagentByType = queryAll<DashBreakdowns[\"subagent_fanout\"][\"by_type\"][number]>(\n db,\n `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`,\n ...w.params,\n );\n const perSession = queryAll<CountRow>(\n db,\n `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')\n GROUP BY tc.session_id`,\n ...w.params,\n );\n const counts = perSession.map((r) => r.n);\n return {\n by_type: subagentByType,\n sessions_with_subagents: counts.length,\n total_spawns: counts.reduce((a, b) => a + b, 0),\n max_per_session: counts.length ? Math.max(...counts) : 0,\n avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0,\n };\n}\n\n/** Errored tool calls by heuristic error_type, with failures-vs-rejections totals. error_type is\n * populated only for status='error' rows (see errors.ts); the raw count is authoritative, the bucket\n * is the heuristic. Ordered most-frequent first. */\nfunction errorTypeBreakdown(db: DB, w: Where): DashBreakdowns[\"error_types\"] {\n const errorRows = queryAll<{ type: string; n: number }>(\n db,\n `SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`,\n ...w.params,\n );\n let failures = 0;\n let rejections = 0;\n const byType = errorRows.map((r) => {\n // See the note in db.ts's withSessionTotals: a stored bucket name need not still be one of the\n // ToolErrorType literals, and errorKind answers \"failure\" for anything it doesn't recognize.\n const kind = errorKind(r.type as ToolErrorType);\n if (kind === \"rejection\") rejections += r.n;\n else failures += r.n;\n return { type: r.type, kind, n: r.n };\n });\n return { by_type: byType, failures, rejections };\n}\n\nexport function dashboardBreakdowns(db: DB, f: DashFilters): DashBreakdowns {\n const w = sessionWhere(f);\n const { byCategory, byComplexity } = classificationBreakdowns(db, w);\n const { skills, skillVersions } = skillBreakdowns(db, w);\n\n return {\n by_model: modelBreakdown(db, w),\n by_source: sourceBreakdown(db, w),\n by_category: byCategory,\n by_complexity: byComplexity,\n tools: toolBreakdown(db, w),\n skills,\n skill_versions: skillVersions,\n subagent_fanout: subagentFanoutBreakdown(db, w),\n error_types: errorTypeBreakdown(db, w),\n };\n}\n","/**\n * `POST /api/refresh` support — the one write-action on an otherwise read-only server (ADR-005).\n * Runs a single collect + ingest pass on the host so the UI can pull in new transcripts on demand,\n * without waiting for the scheduled/`watch` collector. See ADR-015 for the scoped exception.\n *\n * Two guards keep this safe on an unauthenticated loopback server:\n * - `originAllowed` blocks cross-site CSRF (a web page you visit POSTing to 127.0.0.1).\n * - the shared single-instance lock stops it racing a scheduled/`watch`/CLI run.\n * The server's own DB handle stays read-only; ingest opens its own short-lived write connection\n * (WAL permits one writer alongside readers), so the read-only invariant of THIS handle holds.\n */\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { acquireLock, collectAll, findRepoRoot, resolveArchiveDir, resolveDataDir, type CollectStats } from \"@agent-lens/core\";\nimport { runIngest } from \"@agent-lens/ingest\";\n\nexport interface RefreshResult {\n ok: true;\n collected: CollectStats;\n}\n\n/** Loopback host authorities (hostname only; any port), shared by the Host guard (app.ts) and the\n * Origin/CSRF guard below. Note the bracketed IPv6 form: `new URL(...).hostname` and a stripped Host\n * header both yield `[::1]` (not `::1`) for the IPv6 loopback, so the set must carry the brackets. */\nexport const LOOPBACK_HOSTS = new Set([\"127.0.0.1\", \"localhost\", \"[::1]\"]);\n\n/**\n * CSRF guard for the loopback server. Allow requests with no `Origin` (non-browser callers like curl\n * or the CLI — not a CSRF vector) and same-machine origins; reject anything cross-site. The server\n * already binds loopback only (ADR-005); this stops a page you visit from driving localhost.\n */\nexport function originAllowed(origin: string | undefined): boolean {\n if (!origin) return true;\n try {\n return LOOPBACK_HOSTS.has(new URL(origin).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether a state-changing request should be blocked (defense-in-depth CSRF, LOW-001). Combines the\n * `Origin` allowlist with `Sec-Fetch-Site`: browsers stamp the latter on every request, so a\n * cross-site/same-site fetch is rejected even if `Origin` is somehow absent — closing the\n * `originAllowed(undefined) === true` gap. Non-browser callers (curl/CLI) send neither header and\n * stay allowed. Paired with the loopback-Host guard (HIGH-001), this covers the rebinding case too.\n */\nexport function writeBlocked(headers: Record<string, string | string[] | undefined>): boolean {\n const site = headers[\"sec-fetch-site\"];\n const s = Array.isArray(site) ? site[0] : site;\n if (typeof s === \"string\" && s !== \"same-origin\" && s !== \"none\") return true;\n const origin = headers[\"origin\"];\n return !originAllowed(Array.isArray(origin) ? origin[0] : origin);\n}\n\n/**\n * Run one collect + ingest pass under the shared single-instance lock. Returns `null` if a run is\n * already in progress (caller → 409). Throws on collect/ingest failure (caller → 500). Synchronous\n * and blocking: on a large archive this briefly stalls the server, acceptable for a local\n * single-user tool (the clicking user is waiting on it anyway).\n */\nexport function runRefresh(): RefreshResult | null {\n const dataDir = resolveDataDir(findRepoRoot());\n const lock = acquireLock(join(dataDir, \".agent-lens.lock\"));\n if (!lock) return null;\n try {\n const collected = collectAll();\n // runIngest process.exit(1)s if the archive dir is missing — which would kill the server. Ensure\n // it exists first (collectAll creates it when there's anything to copy; this covers the empty case).\n const archiveRoot = resolveArchiveDir();\n if (!existsSync(archiveRoot)) mkdirSync(archiveRoot, { recursive: true });\n runIngest([]);\n return { ok: true, collected };\n } finally {\n lock.release();\n }\n}\n","/**\n * Agent Lens — security-finding triage store (ADR-018). User-authored triage state (which findings are\n * dismissed as safe, which rules are muted) is NOT derivable from the archive, so it can't live in the\n * rebuildable analytics DB — `detect()` wipes and re-inserts `findings` on every ingest, and\n * `ingest --full` drops every analytics table. Instead it lives in a **separate, writable `triage.db`**\n * that ingest never touches, keyed by the *stable* finding id (`sha1(tool_call_id, rule_id)`), so triage\n * survives re-detection and full rebuilds automatically.\n *\n * The analytics server handle stays read-only (ADR-005): it only ATTACHes this file to JOIN triage\n * state into the findings list. Writes go through this dedicated handle, guarded by the same CSRF check\n * as `POST /api/refresh`. WAL lets the writer run alongside the read handle's queries.\n */\nimport { DatabaseSync } from \"node:sqlite\";\nimport { transaction } from \"@agent-lens/core\";\n\nexport type TriageDB = DatabaseSync;\n\nexport const TRIAGE_SCHEMA_SQL = /* sql */ `\nPRAGMA journal_mode = WAL;\n\n-- Findings the user reviewed and marked safe. Keyed by the stable finding id so it survives re-ingest.\nCREATE TABLE IF NOT EXISTS dismissed_findings (\n finding_id TEXT PRIMARY KEY, -- findings.id = sha1(tool_call_id, rule_id)\n tool_call_id TEXT, -- reference only (may be null)\n rule_id TEXT, -- reference only (may be null)\n note TEXT,\n dismissed_at TEXT NOT NULL\n);\n\n-- Rules the user muted (suppressed as noise), optionally scoped to a project or source. A muted rule\n-- hides its current AND future findings from the open view — the scalable lever for a noisy rule.\nCREATE TABLE IF NOT EXISTS muted_rules (\n rule_id TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT 'global', -- 'global' | 'project' | 'source'\n scope_id TEXT NOT NULL DEFAULT '', -- project_id / source_id; '' for global (NOT NULL keeps it in the PK)\n note TEXT,\n muted_at TEXT NOT NULL,\n PRIMARY KEY (rule_id, scope, scope_id)\n);\n`;\n\n/** Open (creating if needed) the writable triage DB and ensure its schema. */\nexport function openTriage(file: string): TriageDB {\n const db = new DatabaseSync(file);\n db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(TRIAGE_SCHEMA_SQL);\n return db;\n}\n\nconst now = () => new Date().toISOString();\n\n/** Mark findings safe (idempotent upsert). Returns the number of ids processed. */\nexport function dismiss(db: TriageDB, ids: string[], note?: string | null): number {\n const stmt = db.prepare(\n `INSERT INTO dismissed_findings (finding_id, note, dismissed_at) VALUES (?, ?, ?)\n ON CONFLICT(finding_id) DO UPDATE SET note = excluded.note, dismissed_at = excluded.dismissed_at`,\n );\n const ts = now();\n const tx = transaction(db, (list: string[]) => {\n for (const id of list) stmt.run(id, note ?? null, ts);\n });\n tx(ids);\n return ids.length;\n}\n\n/** Reopen (un-dismiss) findings. Returns the number of rows removed. */\nexport function reopen(db: TriageDB, ids: string[]): number {\n const stmt = db.prepare(\"DELETE FROM dismissed_findings WHERE finding_id = ?\");\n let n = 0;\n const tx = transaction(db, (list: string[]) => {\n for (const id of list) n += Number(stmt.run(id).changes);\n });\n tx(ids);\n return n;\n}\n\nexport type MuteScope = \"global\" | \"project\" | \"source\";\n\n/** Mute a rule (optionally scoped). Idempotent. */\nexport function muteRule(db: TriageDB, ruleId: string, scope: MuteScope = \"global\", scopeId = \"\", note?: string | null): void {\n db.prepare(\n `INSERT INTO muted_rules (rule_id, scope, scope_id, note, muted_at) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(rule_id, scope, scope_id) DO UPDATE SET note = excluded.note, muted_at = excluded.muted_at`,\n ).run(ruleId, scope, scope === \"global\" ? \"\" : scopeId, note ?? null, now());\n}\n\n/** Unmute a rule. Returns rows removed (0 if it wasn't muted at that scope). */\nexport function unmute(db: TriageDB, ruleId: string, scope: MuteScope = \"global\", scopeId = \"\"): number {\n return Number(\n db\n .prepare(\"DELETE FROM muted_rules WHERE rule_id = ? AND scope = ? AND scope_id = ?\")\n .run(ruleId, scope, scope === \"global\" ? \"\" : scopeId).changes,\n );\n}\n\nexport interface MuteRow {\n rule_id: string;\n scope: string;\n scope_id: string;\n note: string | null;\n muted_at: string;\n}\n\n/** All muted rules, newest first — for the management panel. */\nexport function listMutes(db: TriageDB): MuteRow[] {\n return db\n .prepare(\"SELECT rule_id, scope, scope_id, note, muted_at FROM muted_rules ORDER BY muted_at DESC\")\n .all() as unknown as MuteRow[];\n}\n","/**\n * GET /api/about — the read-only diagnostics payload (ADR-027).\n *\n * This is the web mirror of `agent-lens config`, plus versions and storage. Two rules make it\n * trustworthy, and both are easy to break by accident:\n *\n * 1. **Report what the server is actually using, not a fresh resolution.** The overridable facts\n * (db path + its origin, host, port) are passed in from `run.ts`, which resolved them with\n * precedence flag > env > file > default. Re-resolving them here would silently ignore\n * `serve --db /custom.db` and report a path nobody is reading.\n * 2. **Never let this reach the static snapshot.** It carries absolute paths and\n * `export-snapshot.mjs` publishes to a public GitHub Pages demo. The exporter deliberately does\n * not fetch it; the SPA hides the page under `SNAPSHOT`.\n *\n * Read-only on purpose: config resolves flag > env > file > default, so an editor could only write\n * the third layer and would do nothing at all for anyone using env. Values carry their `origin`\n * instead — the same idiom `agent-lens config` prints.\n */\nimport { statSync } from \"node:fs\";\nimport type { AboutResponse, EngineVersion } from \"@agent-lens/contracts\";\nimport {\n loadSources,\n resolveArchiveDir,\n resolveConfigFile,\n resolveDataDir,\n resolveRetention,\n resolveVersion,\n triageDbFor,\n unpricedModels,\n type ConfigOrigin,\n type ResolvedPricing,\n} from \"@agent-lens/core\";\nimport { CLASSIFIER_VERSION, DETECTOR_VERSION } from \"@agent-lens/ingest\";\nimport { type DB, schemaStatus } from \"./db.js\";\nimport { queryAll, queryGet, tableExists } from \"./sql-util.js\";\n\n/** The facts `run.ts` resolved at startup — the ones a flag or env can change. */\nexport interface AboutContext {\n db: { path: string; origin: ConfigOrigin };\n host: string;\n port: number;\n loopbackOnly: boolean;\n repoRoot: string | null;\n /** The price table this process installed at startup (ADR-028) — reported, never re-resolved. */\n pricing: ResolvedPricing;\n}\n\ninterface StorageRow {\n files: number;\n bytes: number | null;\n last: string | null;\n}\n\n/**\n * Ingested archive bytes, straight from the per-file bookkeeping ingest already maintains\n * (`ingest_state.size`, written by both pipeline.ts and sidecar.ts). No filesystem walk, no cached\n * copy to go stale: measured at ~0.5ms over 7.5k rows, and it is \"as of the last ingest\" by\n * construction because ingest_state *is* the ingest bookkeeping.\n *\n * This is deliberately NOT `du` of the archive dir — anything present but not ingested (notably\n * `.versions/` retention snapshots) is excluded. The UI says \"ingested\" for that reason.\n */\nfunction storage(db: DB, dbPath: string): AboutResponse[\"storage\"] {\n const row = tableExists(db, \"ingest_state\")\n ? queryGet<StorageRow>(db, \"SELECT COUNT(*) files, SUM(size) bytes, MAX(ingested_at) last FROM ingest_state\")\n : undefined;\n\n let db_bytes: number | null = null;\n try {\n db_bytes = statSync(dbPath).size;\n } catch {\n db_bytes = null; // path moved out from under a running server — report unknown, don't throw\n }\n\n return {\n db_bytes,\n archive_bytes: row?.bytes ?? 0, // SUM over zero rows is NULL, not 0\n archive_files: row?.files ?? 0,\n last_ingested: row?.last ?? null,\n };\n}\n\n/**\n * Models this store has real token usage for but no rate — the set whose cost silently reads as $0.\n * Computed from the store rather than from the price table alone, because \"which rates are missing\"\n * only means something relative to the models actually ingested.\n */\nfunction unpriced(db: DB): string[] {\n if (!tableExists(db, \"token_usage\")) return [];\n const rows = queryAll<{ model: string | null }>(\n db,\n `SELECT DISTINCT model FROM token_usage\n WHERE input_tokens + output_tokens + cache_creation_input_tokens + cache_read_input_tokens > 0`,\n );\n return unpricedModels(rows.map((r) => r.model));\n}\n\n/**\n * What a stamped engine produces now vs what is actually stored. The stored side is the point: it\n * answers \"were these findings produced by the rules this build has?\", which is invisible otherwise\n * and is what tells you a re-run (`agent-lens ingest --full`, `agent-lens metrics`) is pending.\n *\n * Rows stamped NEWER than this build are possible too — an older server reading a DB a newer ingest\n * wrote — and are deliberately not flagged `stale`: re-running would *downgrade* them.\n */\nfunction engineVersion(db: DB, table: string, sql: string, expected: number): EngineVersion {\n // `sql` is a literal at each call site — no identifier is ever interpolated here.\n const in_data = tableExists(db, table)\n ? queryAll<{ v: number }>(db, sql)\n .map((r) => r.v)\n .filter((v) => Number.isFinite(v))\n : [];\n return { expected, in_data, stale: in_data.some((v) => v < expected) };\n}\n\nexport function about(db: DB, ctx: AboutContext): AboutResponse {\n const version = resolveVersion();\n const schema = schemaStatus(db);\n const dataDir = resolveDataDir(ctx.repoRoot);\n const retention = resolveRetention();\n\n return {\n versions: {\n app: version.version,\n app_source: version.source,\n schema: schema.db_version,\n schema_expected: schema.expected,\n schema_stale: schema.stale,\n detector: engineVersion(\n db,\n \"findings\",\n \"SELECT DISTINCT detector_version v FROM findings ORDER BY v\",\n DETECTOR_VERSION,\n ),\n classifier: engineVersion(\n db,\n \"classifications\",\n \"SELECT DISTINCT classifier_version v FROM classifications ORDER BY v\",\n CLASSIFIER_VERSION,\n ),\n },\n paths: {\n config_file: resolveConfigFile(ctx.repoRoot, dataDir),\n data_dir: { path: dataDir, origin: process.env.AGENT_LENS_DATA ? \"env\" : \"default\" },\n // archive and triage_db are \"fixed\": ADR-021 makes them non-relocatable on their own, so they\n // are derived here with the same core helpers the CLI uses rather than read from config.\n archive: { path: resolveArchiveDir(ctx.repoRoot), origin: \"fixed\" },\n db: { path: ctx.db.path, origin: ctx.db.origin },\n triage_db: { path: triageDbFor(ctx.db.path), origin: \"fixed\" },\n },\n server: { host: ctx.host, port: ctx.port, loopback_only: ctx.loopbackOnly },\n retention: { versions_keep_days: retention.keepDays, origin: retention.origin },\n pricing: {\n origin: ctx.pricing.origin,\n models: Object.keys(ctx.pricing.table).length,\n applied: ctx.pricing.applied,\n invalid: ctx.pricing.invalid,\n unpriced: unpriced(db),\n },\n sources: loadSources().map((s) => ({ label: s.label, agent: s.agent, config_dir: s.configDir })),\n // storage() already carries last_ingested from the same MAX(ingested_at) it aggregates over —\n // reusing it avoids a second query AND keeps the tableExists guard that lastIngested() lacks.\n storage: storage(db, ctx.db.path),\n };\n}\n","/**\n * Agent Lens — UI preferences store. Like security triage (ADR-018), which charts/columns a user\n * shows is user-authored state that is NOT derivable from the archive, so it can't live in the\n * rebuildable read-only analytics DB. It rides the same **writable sidecar** handle (the triage DB):\n * a generic key→JSON table the server treats as opaque. The client keeps localStorage as an optimistic\n * cache and writes through to here; when no writable store is configured the endpoints degrade so the\n * client simply keeps its local value. Single-user local tool → prefs are global (no per-user key).\n */\nimport type { TriageDB } from \"./triage.js\";\n\nexport const PREFS_SCHEMA_SQL = /* sql */ `\nCREATE TABLE IF NOT EXISTS ui_prefs (\n key TEXT PRIMARY KEY, -- e.g. 'dashboard.charts', 'sessions.columns'\n value TEXT NOT NULL, -- JSON blob, opaque to the server\n updated_at TEXT NOT NULL\n);\n`;\n\n/** Raw stored JSON string for a key, or null if unset. */\nexport function getPref(db: TriageDB, key: string): string | null {\n const row = db.prepare(\"SELECT value FROM ui_prefs WHERE key = ?\").get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\n/** Upsert a key's JSON string (idempotent). */\nexport function setPref(db: TriageDB, key: string, value: string): void {\n db.prepare(\n `INSERT INTO ui_prefs (key, value, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n ).run(key, value, new Date().toISOString());\n}\n","/**\n * `agent-lens watch` — a resident process that collects + ingests whenever a source's transcripts\n * change (debounced), and optionally on a fixed interval. This is the Node-native periodic option;\n * `agent-lens service install collector` layers OS-level persistence (reboot survival) on top.\n *\n * Overlap safety: an in-process guard collapses a burst of file events into one cycle and never runs\n * two cycles at once; the shared single-instance lock additionally guards against a scheduled run\n * firing concurrently.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { acquireLock, collectAll, findRepoRoot, loadSources, resolveDataDir, type Source } from \"@agent-lens/core\";\nimport { runIngest } from \"@agent-lens/ingest\";\n\nexport interface WatchOptions {\n intervalSec?: number;\n poll?: boolean;\n}\n\nexport async function runWatch(opts: WatchOptions = {}): Promise<void> {\n const sources: Source[] = loadSources();\n const lockPath = join(resolveDataDir(findRepoRoot()), \".agent-lens.lock\");\n\n let running = false;\n let pending = false;\n const cycle = () => {\n if (running) {\n pending = true; // a change arrived mid-cycle — run once more when this finishes\n return;\n }\n running = true;\n const lock = acquireLock(lockPath);\n if (!lock) {\n running = false;\n console.error(\"agent-lens watch: another collect/ingest run is in progress — skipping this cycle\");\n return;\n }\n try {\n const stats = collectAll({ sources });\n console.log(\n `agent-lens watch: collected ${stats.scanned} files (${stats.appended} appended, ${stats.snapshots} snapshots) — ingesting`,\n );\n runIngest([]);\n } catch (e) {\n console.error(\"agent-lens watch: cycle failed:\", e);\n } finally {\n lock.release();\n running = false;\n if (pending) {\n pending = false;\n setTimeout(cycle, 0);\n }\n }\n };\n\n cycle(); // initial catch-up\n\n const watchPaths = sources.map((s) => join(s.configDir, \"projects\")).filter((p) => existsSync(p));\n if (watchPaths.length === 0) {\n console.error(\"agent-lens watch: no source 'projects' directories found — nothing to watch\");\n }\n const watcher = chokidar.watch(watchPaths, {\n ignoreInitial: true,\n usePolling: !!opts.poll,\n awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },\n });\n let debounce: NodeJS.Timeout | null = null;\n const trigger = () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(cycle, 3000); // coalesce bursts of writes into one cycle\n };\n watcher.on(\"add\", trigger).on(\"change\", trigger);\n\n if (opts.intervalSec && opts.intervalSec > 0) {\n setInterval(cycle, opts.intervalSec * 1000);\n }\n\n console.log(\n `agent-lens watch: watching ${watchPaths.length} source dir(s)` +\n (opts.intervalSec ? ` + every ${opts.intervalSec}s` : \"\") +\n \" — Ctrl-C to stop\",\n );\n // Keep the process alive indefinitely (chokidar + timers hold it open, but be explicit).\n return new Promise<void>(() => {});\n}\n"],"mappings":"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","names":["existsSync","writeFileSync","join","fileURLToPath","parse","cli","version","readFileSync","join","readFileSync","join","readFileSync","dirname","join","fileURLToPath","moduleDir","existsSync","dirname","join","platform","platform","dirname","join","existsSync","existsSync","mkdirSync","readFileSync","unlinkSync","dirname","spawnSync","mkdirSync","writeFileSync","homedir","platform","join","resolve","resolve","cli","spawnSync","join","homedir","mkdirSync","writeFileSync","platform","readFileSync","statSync","mkdirSync","existsSync","dirname","join","readdirSync","basename","join","path","createHash","createHash","path","createHash","path","createHash","path","existsSync","homedir","createHash","dirname","join","path","createHash","existsSync","join","dirname","homedir","createHash","createHash","c","now","projectId","basename","readdirSync","readFileSync","statSync","join","createHash","openSync","readSync","closeSync","CHUNK","path","path","readdirSync","join","now","statSync","readFileSync","hash","now","path","basename","durationMs","basename","now","path","basename","basename","now","path","basename","path","readFileSync","hash","unpriced","existsSync","mkdirSync","dirname","now","join","statSync","existsSync","dirname","fileURLToPath","DatabaseSync","DatabaseSync","CHUNK","projectId","existsSync","unpriced","existsSync","mkdirSync","join","join","existsSync","mkdirSync","DatabaseSync","DatabaseSync","statSync","statSync","version","existsSync","dirname","fileURLToPath","existsSync","existsSync","join","join","existsSync","join","existsSync","writeFileSync","fileURLToPath"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs","../../core/src/schema.ts","../../core/src/sqlite.ts","../../core/src/security.ts","../../core/src/secrets.ts","../../core/src/redact-export.ts","../../core/src/markdown.ts","../../core/src/errors.ts","../../core/src/user-error.ts","../../core/src/pricing.ts","../../core/src/rawjson.ts","../../core/src/paths.ts","../../core/src/config.ts","../../core/src/version.ts","../../core/src/sources.ts","../../core/src/projects.ts","../../core/src/collect.ts","../../core/src/lock.ts","../../core/src/service.ts","../../ingest/src/run.ts","../../ingest/src/db.ts","../../ingest/src/adapters/claude-code.ts","../../ingest/src/dirtyset.ts","../../ingest/src/storedjson.ts","../../ingest/src/classify.ts","../../ingest/src/detect.ts","../../ingest/src/errors.ts","../../ingest/src/filechanges.ts","../../ingest/src/canonicalize.ts","../../ingest/src/pipeline.ts","../../transcript-format/src/index.ts","../../ingest/src/workflows.ts","../../ingest/src/sidecar.ts","../../ingest/src/redact.ts","../../ingest/src/fileread.ts","../../ingest/src/meta.ts","../../ingest/src/toolResults.ts","../../server/src/run.ts","../../server/src/db.ts","../../server/src/sql-util.ts","../../server/src/usage.ts","../../server/src/app.ts","../../server/src/export.ts","../../server/src/dashboard.ts","../../server/src/refresh.ts","../../server/src/triage.ts","../../server/src/about.ts","../../server/src/prefs.ts","../src/watch.ts"],"sourcesContent":["/**\n * `agent-lens` — the unified CLI. One binary dispatching the pipeline: collect → ingest → serve,\n * plus watch (resident collect+ingest) and service (install collect+ingest and/or serve as OS\n * services). Bundled into a single file by tsup so it installs as one npm package (ADR-010).\n */\nimport { existsSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { cac } from \"cac\";\nimport {\n acquireLock,\n collectAll,\n findRepoRoot,\n loadSources,\n parseHours,\n parseTargets,\n resolveArchiveDir,\n resolveConfigFile,\n resolveDataDir,\n resolveDbPath,\n resolvePricing,\n resolveRetention,\n resolveServerConfig,\n resolveVersion,\n runService,\n triageDbFor,\n UserError,\n} from \"@agent-lens/core\";\nimport { runIngest, runMetrics } from \"@agent-lens/ingest\";\nimport { startServer, openReadonly, renderSessionExport, parseRedactionLevel } from \"@agent-lens/server\";\nimport { runWatch } from \"./watch.js\";\n\n// `agent-lens --version`. Shared with the server's /api/health so the CLI and the web UI can never\n// disagree about what build this is (ADR-027). A published install reports \"0.9.6\"; a clone reports\n// the richer \"v0.9.6-3-gabc1234\", which is the more useful answer for a dev build.\nfunction version(): string {\n return resolveVersion().version;\n}\n\n/**\n * The process's error boundary. A `UserError` is something the user typed or configured, so it\n * prints as one line and exits 1 — a stack trace there is noise the user can't act on. Anything\n * else is a bug, and is re-thrown with its stack intact rather than flattened into a message that\n * would read like their mistake.\n */\nfunction die(e: unknown): never {\n if (e instanceof UserError) {\n console.error(`agent-lens: ${e.message}`);\n process.exit(1);\n }\n throw e;\n}\n\n/** Run `fn` under the single-instance lock so collect/ingest never overlap another run. */\nfunction withLock(fn: () => void): void {\n const lockPath = join(resolveDataDir(findRepoRoot()), \".agent-lens.lock\");\n const lock = acquireLock(lockPath);\n if (!lock) {\n console.error(\"agent-lens: another collect/ingest run is in progress — skipping\");\n return;\n }\n try {\n fn();\n } finally {\n lock.release();\n }\n}\n\nconst cli = cac(\"agent-lens\");\n\ncli\n .command(\"collect\", \"Copy new session traces into the local archive\")\n .option(\"--then-ingest\", \"Run ingest immediately after collecting\")\n .action((opts: { thenIngest?: boolean }) => {\n withLock(() => {\n const s = collectAll();\n console.log(\n `agent-lens: collect done — ${s.sources} source(s), ${s.scanned} scanned, ${s.copied} copied, ` +\n `${s.appended} appended, ${s.diverged} diverged, ${s.compacted} compacted, ${s.snapshots} snapshots`,\n );\n if (opts.thenIngest) runIngest([]);\n });\n });\n\ncli\n .command(\"ingest\", \"Build/update the SQLite projection from the archive\")\n .option(\"--full\", \"Ignore incremental state and re-read every file\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .option(\"--archive <path>\", \"Archive directory\")\n .action((opts: { full?: boolean; db?: string; archive?: string }) => {\n const argv: string[] = [];\n if (opts.full) argv.push(\"--full\");\n if (opts.db) argv.push(\"--db\", opts.db);\n if (opts.archive) argv.push(\"--archive\", opts.archive);\n withLock(() => runIngest(argv));\n });\n\ncli\n .command(\"serve\", \"Serve the web UI + read-only API on 127.0.0.1\")\n .option(\"--port <n>\", \"HTTP port (overrides AGENT_LENS_PORT / config server.port; default 4477)\")\n .option(\"--host <host>\", \"Bind host (overrides AGENT_LENS_HOST / config server.host; loopback only unless AGENT_LENS_ALLOW_NONLOCAL=1)\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .action(async (opts: { port?: string; host?: string; db?: string }) => {\n await startServer({ port: opts.port, host: opts.host, db: opts.db });\n });\n\ncli\n .command(\"watch\", \"Watch sources and collect + ingest on change (resident process)\")\n .option(\"--interval <sec>\", \"Also run on a fixed interval (seconds)\")\n .option(\"--poll\", \"Use polling — for network filesystems where fs events don't fire\")\n .action(async (opts: { interval?: string; poll?: boolean }) => {\n await runWatch({ intervalSec: opts.interval ? Number(opts.interval) : undefined, poll: !!opts.poll });\n });\n\ncli\n .command(\"metrics\", \"Re-run classification over an already-ingested DB\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .action((opts: { db?: string }) => {\n runMetrics(opts.db ? [\"--db\", opts.db] : []);\n });\n\ncli\n .command(\"export <sessionId>\", \"Write a session to a shareable Markdown file (redacted by default)\")\n .option(\"--out <file>\", \"Write to this file (default: stdout)\")\n .option(\"--level <level>\", \"Redaction level: secrets (default) | structure\")\n .option(\"--no-redact\", \"Verbatim, UNREDACTED output (opt-out)\")\n .option(\"--db <path>\", \"SQLite DB path (overrides AGENT_LENS_DB / config db)\")\n .action((sessionId: string, opts: { out?: string; level?: string; redact?: boolean; db?: string }) => {\n const { path: dbPath } = resolveDbPath(opts.db);\n if (!existsSync(dbPath)) {\n console.error(`agent-lens: db not found: ${dbPath} (run ingest first)`);\n process.exit(1);\n }\n // cac sets `redact:false` for --no-redact. Otherwise the level flag (default: secrets).\n const level = opts.redact === false ? \"off\" : parseRedactionLevel(opts.level);\n const out = renderSessionExport(openReadonly(dbPath), sessionId, level);\n if (!out) {\n console.error(`agent-lens: session not found: ${sessionId}`);\n process.exit(1);\n }\n if (opts.out) {\n writeFileSync(opts.out, out.markdown);\n console.error(`agent-lens: wrote ${out.filename} → ${opts.out} (redaction: ${level})`);\n } else {\n process.stdout.write(out.markdown);\n }\n });\n\ncli\n .command(\n \"service <action> [target]\",\n \"Install/uninstall/status OS services — action: install|uninstall|status; target: collector|server|all (default all)\",\n )\n .option(\"--times <hours>\", \"Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)\")\n .action((action: string, target: string | undefined, opts: { times?: string }) => {\n const targets = parseTargets(target);\n const hours = action === \"install\" ? parseHours(opts.times) : undefined;\n // The bundled CLI file is this module; bake its absolute path + node into every unit/plist/task.\n runService(action, { cliEntry: fileURLToPath(import.meta.url), hours, targets });\n });\n\ncli\n .command(\"config\", \"Show the effective configuration (sources, paths, server) and where each value came from\")\n .action(() => {\n const repoRoot = findRepoRoot();\n const dataDir = resolveDataDir(repoRoot);\n const configFile = resolveConfigFile(repoRoot, dataDir);\n const { port, host, portOrigin, hostOrigin } = resolveServerConfig();\n const { path: dbPath, origin: dbOrigin } = resolveDbPath();\n const archive = resolveArchiveDir(repoRoot);\n const triageDb = triageDbFor(dbPath);\n const { keepDays, origin: keepOrigin } = resolveRetention();\n // resolvePricing, not installPricing: this command reports the table, and the Pricing block below\n // already lists what was rejected — the stderr warning would just say it twice.\n const pricing = resolvePricing();\n\n console.log(\"agent-lens config (precedence: flag > env > config file > default)\\n\");\n console.log(\"Paths\");\n console.log(` config file ${configFile ?? \"(none — using built-in default source)\"}`);\n console.log(` data dir ${dataDir}${process.env.AGENT_LENS_DATA ? \" [env]\" : \"\"}`);\n console.log(` archive ${archive} [fixed: <data dir>/archive]`);\n console.log(` db ${dbPath} [${dbOrigin}]`);\n console.log(` triage db ${triageDb} [fixed: beside the db]`);\n console.log(\"\\nServer\");\n console.log(` host ${host} [${hostOrigin}]`);\n console.log(` port ${port} [${portOrigin}]`);\n console.log(` non-local bind ${process.env.AGENT_LENS_ALLOW_NONLOCAL ? \"allowed (AGENT_LENS_ALLOW_NONLOCAL)\" : \"blocked (loopback only)\"}`);\n console.log(\"\\nRetention\");\n console.log(` .versions keep ${keepDays} day(s) [${keepOrigin}]`);\n console.log(\"\\nPricing\");\n console.log(` models priced ${Object.keys(pricing.table).length} [${pricing.origin}]`);\n if (pricing.applied.length) console.log(` overrides ${pricing.applied.join(\", \")}`);\n if (pricing.invalid.length) console.log(` ignored (bad) ${pricing.invalid.join(\", \")}`);\n console.log(\"\\nSources\");\n for (const s of loadSources()) console.log(` ${s.label.padEnd(14)} ${s.configDir} (${s.agent})`);\n });\n\ncli.help();\ncli.version(version());\n// `serve` and `watch` are async, so their failures surface as a rejected promise cac never returns\n// to us — the synchronous catch below would miss them entirely.\nprocess.on(\"unhandledRejection\", die);\ntry {\n cli.parse();\n} catch (e) {\n die(e);\n}\n","import { EventEmitter } from 'events';\n\nfunction toArr(any) {\n\treturn any == null ? [] : Array.isArray(any) ? any : [any];\n}\n\nfunction toVal(out, key, val, opts) {\n\tvar x, old=out[key], nxt=(\n\t\t!!~opts.string.indexOf(key) ? (val == null || val === true ? '' : String(val))\n\t\t: typeof val === 'boolean' ? val\n\t\t: !!~opts.boolean.indexOf(key) ? (val === 'false' ? false : val === 'true' || (out._.push((x = +val,x * 0 === 0) ? x : val),!!val))\n\t\t: (x = +val,x * 0 === 0) ? x : val\n\t);\n\tout[key] = old == null ? nxt : (Array.isArray(old) ? old.concat(nxt) : [old, nxt]);\n}\n\nfunction mri2 (args, opts) {\n\targs = args || [];\n\topts = opts || {};\n\n\tvar k, arr, arg, name, val, out={ _:[] };\n\tvar i=0, j=0, idx=0, len=args.length;\n\n\tconst alibi = opts.alias !== void 0;\n\tconst strict = opts.unknown !== void 0;\n\tconst defaults = opts.default !== void 0;\n\n\topts.alias = opts.alias || {};\n\topts.string = toArr(opts.string);\n\topts.boolean = toArr(opts.boolean);\n\n\tif (alibi) {\n\t\tfor (k in opts.alias) {\n\t\t\tarr = opts.alias[k] = toArr(opts.alias[k]);\n\t\t\tfor (i=0; i < arr.length; i++) {\n\t\t\t\t(opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (i=opts.boolean.length; i-- > 0;) {\n\t\tarr = opts.alias[opts.boolean[i]] || [];\n\t\tfor (j=arr.length; j-- > 0;) opts.boolean.push(arr[j]);\n\t}\n\n\tfor (i=opts.string.length; i-- > 0;) {\n\t\tarr = opts.alias[opts.string[i]] || [];\n\t\tfor (j=arr.length; j-- > 0;) opts.string.push(arr[j]);\n\t}\n\n\tif (defaults) {\n\t\tfor (k in opts.default) {\n\t\t\tname = typeof opts.default[k];\n\t\t\tarr = opts.alias[k] = opts.alias[k] || [];\n\t\t\tif (opts[name] !== void 0) {\n\t\t\t\topts[name].push(k);\n\t\t\t\tfor (i=0; i < arr.length; i++) {\n\t\t\t\t\topts[name].push(arr[i]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst keys = strict ? Object.keys(opts.alias) : [];\n\n\tfor (i=0; i < len; i++) {\n\t\targ = args[i];\n\n\t\tif (arg === '--') {\n\t\t\tout._ = out._.concat(args.slice(++i));\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (j=0; j < arg.length; j++) {\n\t\t\tif (arg.charCodeAt(j) !== 45) break; // \"-\"\n\t\t}\n\n\t\tif (j === 0) {\n\t\t\tout._.push(arg);\n\t\t} else if (arg.substring(j, j + 3) === 'no-') {\n\t\t\tname = arg.substring(j + 3);\n\t\t\tif (strict && !~keys.indexOf(name)) {\n\t\t\t\treturn opts.unknown(arg);\n\t\t\t}\n\t\t\tout[name] = false;\n\t\t} else {\n\t\t\tfor (idx=j+1; idx < arg.length; idx++) {\n\t\t\t\tif (arg.charCodeAt(idx) === 61) break; // \"=\"\n\t\t\t}\n\n\t\t\tname = arg.substring(j, idx);\n\t\t\tval = arg.substring(++idx) || (i+1 === len || (''+args[i+1]).charCodeAt(0) === 45 || args[++i]);\n\t\t\tarr = (j === 2 ? [name] : name);\n\n\t\t\tfor (idx=0; idx < arr.length; idx++) {\n\t\t\t\tname = arr[idx];\n\t\t\t\tif (strict && !~keys.indexOf(name)) return opts.unknown('-'.repeat(j) + name);\n\t\t\t\ttoVal(out, name, (idx + 1 < arr.length) || val, opts);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (defaults) {\n\t\tfor (k in opts.default) {\n\t\t\tif (out[k] === void 0) {\n\t\t\t\tout[k] = opts.default[k];\n\t\t\t}\n\t\t}\n\t}\n\n\tif (alibi) {\n\t\tfor (k in out) {\n\t\t\tarr = opts.alias[k] || [];\n\t\t\twhile (arr.length > 0) {\n\t\t\t\tout[arr.shift()] = out[k];\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out;\n}\n\nconst removeBrackets = (v) => v.replace(/[<[].+/, \"\").trim();\nconst findAllBrackets = (v) => {\n const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;\n const SQUARE_BRACKET_RE_GLOBAL = /\\[([^\\]]+)\\]/g;\n const res = [];\n const parse = (match) => {\n let variadic = false;\n let value = match[1];\n if (value.startsWith(\"...\")) {\n value = value.slice(3);\n variadic = true;\n }\n return {\n required: match[0].startsWith(\"<\"),\n value,\n variadic\n };\n };\n let angledMatch;\n while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {\n res.push(parse(angledMatch));\n }\n let squareMatch;\n while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {\n res.push(parse(squareMatch));\n }\n return res;\n};\nconst getMriOptions = (options) => {\n const result = {alias: {}, boolean: []};\n for (const [index, option] of options.entries()) {\n if (option.names.length > 1) {\n result.alias[option.names[0]] = option.names.slice(1);\n }\n if (option.isBoolean) {\n if (option.negated) {\n const hasStringTypeOption = options.some((o, i) => {\n return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === \"boolean\";\n });\n if (!hasStringTypeOption) {\n result.boolean.push(option.names[0]);\n }\n } else {\n result.boolean.push(option.names[0]);\n }\n }\n }\n return result;\n};\nconst findLongest = (arr) => {\n return arr.sort((a, b) => {\n return a.length > b.length ? -1 : 1;\n })[0];\n};\nconst padRight = (str, length) => {\n return str.length >= length ? str : `${str}${\" \".repeat(length - str.length)}`;\n};\nconst camelcase = (input) => {\n return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {\n return p1 + p2.toUpperCase();\n });\n};\nconst setDotProp = (obj, keys, val) => {\n let i = 0;\n let length = keys.length;\n let t = obj;\n let x;\n for (; i < length; ++i) {\n x = t[keys[i]];\n t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(\".\") || !(+keys[i + 1] > -1) ? {} : [];\n }\n};\nconst setByType = (obj, transforms) => {\n for (const key of Object.keys(transforms)) {\n const transform = transforms[key];\n if (transform.shouldTransform) {\n obj[key] = Array.prototype.concat.call([], obj[key]);\n if (typeof transform.transformFunction === \"function\") {\n obj[key] = obj[key].map(transform.transformFunction);\n }\n }\n }\n};\nconst getFileName = (input) => {\n const m = /([^\\\\\\/]+)$/.exec(input);\n return m ? m[1] : \"\";\n};\nconst camelcaseOptionName = (name) => {\n return name.split(\".\").map((v, i) => {\n return i === 0 ? camelcase(v) : v;\n }).join(\".\");\n};\nclass CACError extends Error {\n constructor(message) {\n super(message);\n this.name = this.constructor.name;\n if (typeof Error.captureStackTrace === \"function\") {\n Error.captureStackTrace(this, this.constructor);\n } else {\n this.stack = new Error(message).stack;\n }\n }\n}\n\nclass Option {\n constructor(rawName, description, config) {\n this.rawName = rawName;\n this.description = description;\n this.config = Object.assign({}, config);\n rawName = rawName.replace(/\\.\\*/g, \"\");\n this.negated = false;\n this.names = removeBrackets(rawName).split(\",\").map((v) => {\n let name = v.trim().replace(/^-{1,2}/, \"\");\n if (name.startsWith(\"no-\")) {\n this.negated = true;\n name = name.replace(/^no-/, \"\");\n }\n return camelcaseOptionName(name);\n }).sort((a, b) => a.length > b.length ? 1 : -1);\n this.name = this.names[this.names.length - 1];\n if (this.negated && this.config.default == null) {\n this.config.default = true;\n }\n if (rawName.includes(\"<\")) {\n this.required = true;\n } else if (rawName.includes(\"[\")) {\n this.required = false;\n } else {\n this.isBoolean = true;\n }\n }\n}\n\nconst processArgs = process.argv;\nconst platformInfo = `${process.platform}-${process.arch} node-${process.version}`;\n\nclass Command {\n constructor(rawName, description, config = {}, cli) {\n this.rawName = rawName;\n this.description = description;\n this.config = config;\n this.cli = cli;\n this.options = [];\n this.aliasNames = [];\n this.name = removeBrackets(rawName);\n this.args = findAllBrackets(rawName);\n this.examples = [];\n }\n usage(text) {\n this.usageText = text;\n return this;\n }\n allowUnknownOptions() {\n this.config.allowUnknownOptions = true;\n return this;\n }\n ignoreOptionDefaultValue() {\n this.config.ignoreOptionDefaultValue = true;\n return this;\n }\n version(version, customFlags = \"-v, --version\") {\n this.versionNumber = version;\n this.option(customFlags, \"Display version number\");\n return this;\n }\n example(example) {\n this.examples.push(example);\n return this;\n }\n option(rawName, description, config) {\n const option = new Option(rawName, description, config);\n this.options.push(option);\n return this;\n }\n alias(name) {\n this.aliasNames.push(name);\n return this;\n }\n action(callback) {\n this.commandAction = callback;\n return this;\n }\n isMatched(name) {\n return this.name === name || this.aliasNames.includes(name);\n }\n get isDefaultCommand() {\n return this.name === \"\" || this.aliasNames.includes(\"!\");\n }\n get isGlobalCommand() {\n return this instanceof GlobalCommand;\n }\n hasOption(name) {\n name = name.split(\".\")[0];\n return this.options.find((option) => {\n return option.names.includes(name);\n });\n }\n outputHelp() {\n const {name, commands} = this.cli;\n const {\n versionNumber,\n options: globalOptions,\n helpCallback\n } = this.cli.globalCommand;\n let sections = [\n {\n body: `${name}${versionNumber ? `/${versionNumber}` : \"\"}`\n }\n ];\n sections.push({\n title: \"Usage\",\n body: ` $ ${name} ${this.usageText || this.rawName}`\n });\n const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;\n if (showCommands) {\n const longestCommandName = findLongest(commands.map((command) => command.rawName));\n sections.push({\n title: \"Commands\",\n body: commands.map((command) => {\n return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;\n }).join(\"\\n\")\n });\n sections.push({\n title: `For more info, run any command with the \\`--help\\` flag`,\n body: commands.map((command) => ` $ ${name}${command.name === \"\" ? \"\" : ` ${command.name}`} --help`).join(\"\\n\")\n });\n }\n let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];\n if (!this.isGlobalCommand && !this.isDefaultCommand) {\n options = options.filter((option) => option.name !== \"version\");\n }\n if (options.length > 0) {\n const longestOptionName = findLongest(options.map((option) => option.rawName));\n sections.push({\n title: \"Options\",\n body: options.map((option) => {\n return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === void 0 ? \"\" : `(default: ${option.config.default})`}`;\n }).join(\"\\n\")\n });\n }\n if (this.examples.length > 0) {\n sections.push({\n title: \"Examples\",\n body: this.examples.map((example) => {\n if (typeof example === \"function\") {\n return example(name);\n }\n return example;\n }).join(\"\\n\")\n });\n }\n if (helpCallback) {\n sections = helpCallback(sections) || sections;\n }\n console.log(sections.map((section) => {\n return section.title ? `${section.title}:\n${section.body}` : section.body;\n }).join(\"\\n\\n\"));\n }\n outputVersion() {\n const {name} = this.cli;\n const {versionNumber} = this.cli.globalCommand;\n if (versionNumber) {\n console.log(`${name}/${versionNumber} ${platformInfo}`);\n }\n }\n checkRequiredArgs() {\n const minimalArgsCount = this.args.filter((arg) => arg.required).length;\n if (this.cli.args.length < minimalArgsCount) {\n throw new CACError(`missing required args for command \\`${this.rawName}\\``);\n }\n }\n checkUnknownOptions() {\n const {options, globalCommand} = this.cli;\n if (!this.config.allowUnknownOptions) {\n for (const name of Object.keys(options)) {\n if (name !== \"--\" && !this.hasOption(name) && !globalCommand.hasOption(name)) {\n throw new CACError(`Unknown option \\`${name.length > 1 ? `--${name}` : `-${name}`}\\``);\n }\n }\n }\n }\n checkOptionValue() {\n const {options: parsedOptions, globalCommand} = this.cli;\n const options = [...globalCommand.options, ...this.options];\n for (const option of options) {\n const value = parsedOptions[option.name.split(\".\")[0]];\n if (option.required) {\n const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));\n if (value === true || value === false && !hasNegated) {\n throw new CACError(`option \\`${option.rawName}\\` value is missing`);\n }\n }\n }\n }\n}\nclass GlobalCommand extends Command {\n constructor(cli) {\n super(\"@@global@@\", \"\", {}, cli);\n }\n}\n\nvar __assign = Object.assign;\nclass CAC extends EventEmitter {\n constructor(name = \"\") {\n super();\n this.name = name;\n this.commands = [];\n this.rawArgs = [];\n this.args = [];\n this.options = {};\n this.globalCommand = new GlobalCommand(this);\n this.globalCommand.usage(\"<command> [options]\");\n }\n usage(text) {\n this.globalCommand.usage(text);\n return this;\n }\n command(rawName, description, config) {\n const command = new Command(rawName, description || \"\", config, this);\n command.globalCommand = this.globalCommand;\n this.commands.push(command);\n return command;\n }\n option(rawName, description, config) {\n this.globalCommand.option(rawName, description, config);\n return this;\n }\n help(callback) {\n this.globalCommand.option(\"-h, --help\", \"Display this message\");\n this.globalCommand.helpCallback = callback;\n this.showHelpOnExit = true;\n return this;\n }\n version(version, customFlags = \"-v, --version\") {\n this.globalCommand.version(version, customFlags);\n this.showVersionOnExit = true;\n return this;\n }\n example(example) {\n this.globalCommand.example(example);\n return this;\n }\n outputHelp() {\n if (this.matchedCommand) {\n this.matchedCommand.outputHelp();\n } else {\n this.globalCommand.outputHelp();\n }\n }\n outputVersion() {\n this.globalCommand.outputVersion();\n }\n setParsedInfo({args, options}, matchedCommand, matchedCommandName) {\n this.args = args;\n this.options = options;\n if (matchedCommand) {\n this.matchedCommand = matchedCommand;\n }\n if (matchedCommandName) {\n this.matchedCommandName = matchedCommandName;\n }\n return this;\n }\n unsetMatchedCommand() {\n this.matchedCommand = void 0;\n this.matchedCommandName = void 0;\n }\n parse(argv = processArgs, {\n run = true\n } = {}) {\n this.rawArgs = argv;\n if (!this.name) {\n this.name = argv[1] ? getFileName(argv[1]) : \"cli\";\n }\n let shouldParse = true;\n for (const command of this.commands) {\n const parsed = this.mri(argv.slice(2), command);\n const commandName = parsed.args[0];\n if (command.isMatched(commandName)) {\n shouldParse = false;\n const parsedInfo = __assign(__assign({}, parsed), {\n args: parsed.args.slice(1)\n });\n this.setParsedInfo(parsedInfo, command, commandName);\n this.emit(`command:${commandName}`, command);\n }\n }\n if (shouldParse) {\n for (const command of this.commands) {\n if (command.name === \"\") {\n shouldParse = false;\n const parsed = this.mri(argv.slice(2), command);\n this.setParsedInfo(parsed, command);\n this.emit(`command:!`, command);\n }\n }\n }\n if (shouldParse) {\n const parsed = this.mri(argv.slice(2));\n this.setParsedInfo(parsed);\n }\n if (this.options.help && this.showHelpOnExit) {\n this.outputHelp();\n run = false;\n this.unsetMatchedCommand();\n }\n if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {\n this.outputVersion();\n run = false;\n this.unsetMatchedCommand();\n }\n const parsedArgv = {args: this.args, options: this.options};\n if (run) {\n this.runMatchedCommand();\n }\n if (!this.matchedCommand && this.args[0]) {\n this.emit(\"command:*\");\n }\n return parsedArgv;\n }\n mri(argv, command) {\n const cliOptions = [\n ...this.globalCommand.options,\n ...command ? command.options : []\n ];\n const mriOptions = getMriOptions(cliOptions);\n let argsAfterDoubleDashes = [];\n const doubleDashesIndex = argv.indexOf(\"--\");\n if (doubleDashesIndex > -1) {\n argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);\n argv = argv.slice(0, doubleDashesIndex);\n }\n let parsed = mri2(argv, mriOptions);\n parsed = Object.keys(parsed).reduce((res, name) => {\n return __assign(__assign({}, res), {\n [camelcaseOptionName(name)]: parsed[name]\n });\n }, {_: []});\n const args = parsed._;\n const options = {\n \"--\": argsAfterDoubleDashes\n };\n const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;\n let transforms = Object.create(null);\n for (const cliOption of cliOptions) {\n if (!ignoreDefault && cliOption.config.default !== void 0) {\n for (const name of cliOption.names) {\n options[name] = cliOption.config.default;\n }\n }\n if (Array.isArray(cliOption.config.type)) {\n if (transforms[cliOption.name] === void 0) {\n transforms[cliOption.name] = Object.create(null);\n transforms[cliOption.name][\"shouldTransform\"] = true;\n transforms[cliOption.name][\"transformFunction\"] = cliOption.config.type[0];\n }\n }\n }\n for (const key of Object.keys(parsed)) {\n if (key !== \"_\") {\n const keys = key.split(\".\");\n setDotProp(options, keys, parsed[key]);\n setByType(options, transforms);\n }\n }\n return {\n args,\n options\n };\n }\n runMatchedCommand() {\n const {args, options, matchedCommand: command} = this;\n if (!command || !command.commandAction)\n return;\n command.checkUnknownOptions();\n command.checkOptionValue();\n command.checkRequiredArgs();\n const actionArgs = [];\n command.args.forEach((arg, index) => {\n if (arg.variadic) {\n actionArgs.push(args.slice(index));\n } else {\n actionArgs.push(args[index]);\n }\n });\n actionArgs.push(options);\n return command.commandAction.apply(this, actionArgs);\n }\n}\n\nconst cac = (name = \"\") => new CAC(name);\n\nexport default cac;\nexport { CAC, Command, cac };\n","/**\n * Agent Lens — normalized, agent-agnostic data model (SQLite DDL).\n *\n * Design notes\n * - Agent-agnostic: Claude Code is one *source adapter*. Adding another agent later means a new\n * adapter that emits rows for these same tables — no schema change (see ADR-003).\n * - Event-grained source of truth: every transcript line becomes a row in `events`, keyed by its\n * stable `uuid`. Re-ingesting the same event is an UPSERT, so ingesting the mirror plus all\n * `.versions/` divergence backups deduplicates to the *maximal* history and recovers\n * compaction-dropped events (see ADR-001 / ADR-002).\n * - Hierarchy: session › turn › event. A \"turn\" = one user prompt → assistant completion.\n * - `raw_json` keeps the original line verbatim for lossless re-derivation; structured columns are\n * projections for querying/dashboards. It is stored gzip-compressed as a BLOB (ADR-011) — write via\n * `packRaw`, read via `unpackRaw` (both in `rawjson.ts`).\n * - An event's displayable halves are *stored*, never re-derived: `text` and `thinking` are separate\n * columns so readers never parse a transcript record (ADR-031). Only the adapter knows that shape.\n *\n * This module only declares the schema. Connection handling and migrations live in `db.ts`.\n * Bump SCHEMA_VERSION on any DDL change.\n */\n\nexport const SCHEMA_VERSION = 15;\n\nexport const SCHEMA_SQL = /* sql */ `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\n-- Schema/version bookkeeping ------------------------------------------------\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- An agent *type* (e.g. 'claude-code'). Extensible to other agents. ---------\nCREATE TABLE IF NOT EXISTS agents (\n id TEXT PRIMARY KEY, -- 'claude-code'\n name TEXT NOT NULL, -- 'Claude Code CLI'\n kind TEXT NOT NULL DEFAULT 'cli'\n);\n\n-- A configured source = a labeled agent *instance* (e.g. account 'personal' --\n-- of agent 'claude-code' rooted at ~/.claude). Sessions belong to a source. --\nCREATE TABLE IF NOT EXISTS sources (\n id TEXT PRIMARY KEY, -- the (unique) label, e.g. 'personal'\n label TEXT NOT NULL,\n agent_id TEXT NOT NULL REFERENCES agents(id),\n config_dir TEXT\n);\n\n-- A project = a working directory a session ran in. ------------------------\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY, -- stable hash of (agent_id, path)\n agent_id TEXT NOT NULL REFERENCES agents(id),\n path TEXT NOT NULL, -- decoded absolute cwd\n encoded_dir TEXT, -- original '-home-...' archive dir name\n first_seen TEXT, -- ISO8601\n last_seen TEXT,\n UNIQUE (agent_id, path)\n);\n\n-- A session = one transcript file (sessionId). -----------------------------\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY, -- sessionId (UUID)\n agent_id TEXT NOT NULL REFERENCES agents(id),\n source_id TEXT REFERENCES sources(id),\n project_id TEXT REFERENCES projects(id),\n slug TEXT,\n ai_title TEXT,\n cli_version TEXT,\n entrypoint TEXT,\n git_branch TEXT,\n is_sidechain INTEGER NOT NULL DEFAULT 0, -- 1 = subagent thread\n spawn_parent_id TEXT, -- structural parent from path (<parent>/subagents/…); raw link hint, set at ingest\n workflow_run_id TEXT, -- for a Workflow-tool subagent: its run id (wf_<id>) from the path; groups fan-out by run\n parent_session_id TEXT REFERENCES sessions(id), -- spawning session (subagents only); derived in rebuildDerived\n parent_turn_id TEXT REFERENCES turns(id), -- the turn that spawned this subagent\n started_at TEXT, -- ISO8601 of first event\n ended_at TEXT, -- ISO8601 of last event\n duration_ms INTEGER,\n event_count INTEGER NOT NULL DEFAULT 0,\n turn_count INTEGER NOT NULL DEFAULT 0\n);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_source ON sessions(source_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);\nCREATE INDEX IF NOT EXISTS idx_sessions_parent_session ON sessions(parent_session_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_workflow_run ON sessions(workflow_run_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_parent_turn ON sessions(parent_turn_id);\n\n-- A turn = user prompt -> assistant completion within a session. ------------\nCREATE TABLE IF NOT EXISTS turns (\n id TEXT PRIMARY KEY, -- hash(session_id, seq)\n session_id TEXT NOT NULL REFERENCES sessions(id),\n seq INTEGER NOT NULL, -- 0-based order within session\n user_event_uuid TEXT, -- the originating user event\n prompt_preview TEXT, -- short, for lists (full text in events)\n model TEXT, -- model that answered this turn\n started_at TEXT,\n ended_at TEXT,\n duration_ms INTEGER,\n UNIQUE (session_id, seq)\n);\nCREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);\n\n-- Every transcript line. uuid is the dedup key across archive versions. -----\nCREATE TABLE IF NOT EXISTS events (\n uuid TEXT PRIMARY KEY,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT REFERENCES turns(id),\n parent_uuid TEXT,\n seq INTEGER, -- line order as first observed\n type TEXT NOT NULL, -- user | assistant | system | attachment | ...\n role TEXT, -- user | assistant (message.role)\n timestamp TEXT, -- ISO8601\n model TEXT,\n is_sidechain INTEGER NOT NULL DEFAULT 0,\n is_meta INTEGER NOT NULL DEFAULT 0,\n text TEXT, -- visible message text, for search + preview\n thinking TEXT, -- reasoning blocks, kept apart so the read path needs no parsing\n raw_json BLOB NOT NULL, -- original line, verbatim, gzip-compressed (ADR-011); read via unpackRaw()\n source_file TEXT -- archive path the canonical copy came from\n);\nCREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);\nCREATE INDEX IF NOT EXISTS idx_events_turn ON events(turn_id);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(type);\nCREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp);\n\n-- Tool invocations (incl. subagents / Task calls). -------------------------\nCREATE TABLE IF NOT EXISTS tool_calls (\n id TEXT PRIMARY KEY, -- tool_use id, or hash\n event_uuid TEXT REFERENCES events(uuid),\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT REFERENCES turns(id),\n tool_name TEXT NOT NULL, -- Edit, Write, Bash, Skill, Task, ...\n caller TEXT,\n skill_name TEXT, -- when tool_name = Skill\n skill_id TEXT REFERENCES skills(id), -- the specific skill *version* this call fired (content hash); set in rebuildDerived\n agent_type TEXT, -- subagent type (toolUseResult.agentType)\n spawned_session_id TEXT, -- for Task/Agent: the subagent session id ('agent-'||agentId)\n workflow_run_id TEXT, -- for Workflow: the run id (toolUseResult.runId, e.g. wf_<id>) — ties the run to its subagents\n workflow_name TEXT, -- for Workflow: the workflow name (toolUseResult.workflowName)\n resolved_model TEXT,\n status TEXT, -- success | error | ... ('error' when the tool_result had is_error)\n error_type TEXT, -- heuristic bucket for status='error' rows (see errors.ts); NULL otherwise\n total_duration_ms INTEGER,\n total_tokens INTEGER,\n total_tool_use_count INTEGER,\n input_json TEXT, -- tool input, verbatim\n result_summary TEXT -- short result projection (no secrets dumped wholesale)\n);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_skill ON tool_calls(skill_name);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_skill_id ON tool_calls(skill_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_spawned ON tool_calls(spawned_session_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_workflow_run ON tool_calls(workflow_run_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_error_type ON tool_calls(error_type);\n\n-- Skill *versions* (content-addressed). A skill firing injects the full SKILL.md body into the\n-- transcript as an isMeta user event (begins \"Base directory for this skill: …\", then the body,\n-- then a trailing \"ARGUMENTS: …\" block). That body is the only real skill content available (the\n-- trace carries no description/created_at). We identify a version by hashing the *normalized* body\n-- (Base-directory line + per-call ARGUMENTS block stripped, trailing whitespace trimmed) so the same\n-- content from either install / any args is one version, and a content change shows as a new row.\n-- Linked from tool_calls.skill_id in rebuildDerived. See plan: content-addressed skill versioning.\nCREATE TABLE IF NOT EXISTS skills (\n id TEXT PRIMARY KEY, -- sha1 of name + NUL + normalized_body\n name TEXT NOT NULL,\n base_dir TEXT, -- last-seen base directory (informational; NOT part of the hash)\n body TEXT NOT NULL, -- normalized SKILL.md body (Base-dir line + ARGUMENTS block stripped)\n summary TEXT, -- first heading/intro line, for list + detail display\n body_bytes INTEGER,\n first_seen TEXT, -- earliest firing timestamp (MIN across firings)\n last_seen TEXT -- latest firing timestamp (MAX across firings)\n);\nCREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);\n\n-- Token usage at the assistant-response grain. Cost derived later from model. --\n-- One API response is logged across multiple JSONL lines (one per content block: text + each\n-- tool_use), and Claude Code stamps the SAME usage object on every line. We dedup at the response\n-- grain via the unique (session_id, message_id) index so a response's tokens are counted ONCE, not\n-- once per content block (which over-counted tokens ~2.3x and cost ~3x). event_uuid stays the PK\n-- (the row is attached to whichever content-block event won the insert). --\nCREATE TABLE IF NOT EXISTS token_usage (\n event_uuid TEXT PRIMARY KEY REFERENCES events(uuid),\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT REFERENCES turns(id),\n message_id TEXT, -- Anthropic response id (msg_…); dedup key\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n cache_creation_input_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read_input_tokens INTEGER NOT NULL DEFAULT 0,\n service_tier TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_token_usage_session ON token_usage(session_id);\n-- Partial so rare usage rows lacking a message_id (e.g. <synthetic>) don't collide on NULL. --\nCREATE UNIQUE INDEX IF NOT EXISTS idx_token_usage_msg\n ON token_usage(session_id, message_id) WHERE message_id IS NOT NULL;\n\n-- Heuristic classification (category + complexity), re-runnable. -----------\n-- scope: 'session' | 'turn'; target_id references that entity. -------------\nCREATE TABLE IF NOT EXISTS classifications (\n scope TEXT NOT NULL, -- 'session' | 'turn'\n target_id TEXT NOT NULL,\n category TEXT, -- feature | bugfix | refactor | docs | ops | review | ...\n complexity_score REAL,\n complexity_band TEXT, -- trivial | small | medium | large | xl\n signals_json TEXT, -- inputs used, for transparency/repro\n classifier_version INTEGER NOT NULL DEFAULT 1,\n PRIMARY KEY (scope, target_id)\n);\n\n-- Security findings (ADR-017), re-runnable. A deterministic rule engine (detect.ts) scans each\n-- tool call's verbatim input/result for risky operations — destructive/data-loss, credential access,\n-- data exfiltration, privilege/guardrail bypass — and records one row per (tool_call, rule) match.\n-- Findings are 0..N per tool call (unlike classifications, which are 1-per-session), so an incremental\n-- re-run DELETEs the touched sessions' rows and re-INSERTs (delete-then-insert) rather than upserting.\n-- Every input/modifier is captured in signals_json so a finding is fully explainable; framework_ref\n-- anchors it to OWASP Agentic / MITRE ATLAS. detector_version lets a future engine supersede rows.\nCREATE TABLE IF NOT EXISTS findings (\n id TEXT PRIMARY KEY, -- deterministic hash(tool_call_id, rule_id)\n session_id TEXT NOT NULL REFERENCES sessions(id),\n tool_call_id TEXT REFERENCES tool_calls(id),\n event_uuid TEXT, -- the tool call's event, for jump-to-transcript\n turn_id TEXT,\n rule_id TEXT NOT NULL, -- 'destructive.rm_rf'\n category TEXT NOT NULL, -- destructive | credential-access | exfiltration | privilege-bypass\n framework_ref TEXT, -- 'OWASP ASI02' | 'MITRE ATLAS AML.T0086' | ...\n severity TEXT NOT NULL, -- info | low | medium | high | critical\n title TEXT,\n evidence TEXT, -- matched snippet, truncated (no wholesale secret dump)\n signals_json TEXT, -- why: rule, matched pattern, context modifiers\n detector_version INTEGER NOT NULL DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_findings_session ON findings(session_id);\nCREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);\nCREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);\nCREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule_id);\nCREATE INDEX IF NOT EXISTS idx_findings_tool_call ON findings(tool_call_id);\n\n-- File modifications derived from tool calls (ADR-022). One row per (tool_call, file): today\n-- Edit/Write/NotebookEdit are 1:1, but the hash id leaves room for multi-file sources (e.g. a\n-- future Bash-write heuristic) without a schema break. Failed calls (status='error') are excluded\n-- at derivation — they didn't change the file. Re-runnable like findings: incremental runs DELETE\n-- the dirty sessions' rows and re-INSERT (delete-then-insert). Populated by ingest's\n-- deriveFileChanges (filechanges.ts).\nCREATE TABLE IF NOT EXISTS file_changes (\n id TEXT PRIMARY KEY, -- sha1(tool_call_id + NUL + file_path), 16 hex\n tool_call_id TEXT NOT NULL REFERENCES tool_calls(id),\n session_id TEXT NOT NULL REFERENCES sessions(id),\n turn_id TEXT, -- no FK: rebuildDerived recreates turns (same stance as findings)\n event_uuid TEXT, -- jump-to-transcript anchor (#ev-<uuid>)\n project_id TEXT REFERENCES projects(id), -- from the session, for per-project grouping\n file_path TEXT NOT NULL, -- normalized absolute path\n tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit\n lines_added INTEGER, -- newline-count delta (NULL when unknowable)\n lines_removed INTEGER,\n timestamp TEXT, -- ISO8601, from the tool call's event\n derive_version INTEGER NOT NULL DEFAULT 1\n);\nCREATE INDEX IF NOT EXISTS idx_file_changes_session ON file_changes(session_id);\nCREATE INDEX IF NOT EXISTS idx_file_changes_file ON file_changes(project_id, file_path, timestamp);\n\n-- Workflow-tool run results, ingested from the sidecar JSON the runner writes next to the launching\n-- session (…/projects/<enc>/<sessionId>/workflows/wf_<id>.json). The transcript only carries the\n-- \"launched in background\" ack for an async run; this sidecar is the authoritative record of how the\n-- run finished — status (completed/failed), summary, the returned result payload, and roll-up\n-- tokens/tool-calls/duration/agent-count. One row per run id; UPSERT on re-ingest. Stored verbatim\n-- (like tool_calls.input_json) — the DB is local; redaction is an export-time concern. --------------\nCREATE TABLE IF NOT EXISTS workflow_results (\n run_id TEXT PRIMARY KEY, -- wf_<id> (matches tool_calls.workflow_run_id / sessions.workflow_run_id)\n source_id TEXT, -- which configured source (account) it came from\n session_id TEXT, -- launching session (path segment before /workflows/)\n task_id TEXT, -- async task id (the \"Task ID: …\" in the launch ack)\n workflow_name TEXT,\n status TEXT, -- completed | failed | running | ...\n summary TEXT,\n default_model TEXT, -- model the run executed on (e.g. claude-fable-5)\n result_json TEXT, -- the returned result object, verbatim JSON (null when none)\n phases_json TEXT, -- [{title}] the run's phase structure (e.g. Generate → Judge)\n logs_json TEXT, -- concise per-item outcome lines the runner emitted\n progress_json TEXT, -- workflowProgress: the phase/agent event timeline\n agent_count INTEGER, -- agents the run reports having fanned out to\n total_tokens INTEGER,\n total_tool_calls INTEGER,\n duration_ms INTEGER,\n started_at TEXT, -- ISO, from the sidecar startTime/timestamp\n ended_at TEXT, -- ISO, started_at + duration\n ingested_at TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_workflow_results_session ON workflow_results(session_id);\n\n-- Per-subagent metadata sidecar (subagents/agent-<id>.meta.json). One row per subagent session,\n-- joined at read time onto sessions (LEFT JOIN, may precede the session row's arrival). Supplies the\n-- authoritative agentType/description/spawnDepth — Workflow fan-out subagents carry none in-transcript.\nCREATE TABLE IF NOT EXISTS session_meta (\n session_id TEXT PRIMARY KEY, -- 'agent-<id>' = the subagent session id (meta filename stem)\n source_id TEXT,\n agent_type TEXT, -- meta.agentType (e.g. 'Explore', 'claude-code-guide')\n agent_description TEXT, -- meta.description (a real human title)\n spawn_depth INTEGER, -- meta.spawnDepth (optional; absent ⇒ top-level)\n tool_use_id TEXT, -- meta.toolUseId (the spawning tool_call)\n ingested_at TEXT\n);\n\n-- Full un-truncated tool outputs spilled to disk (tool-results/<name>.txt) when a result was too large\n-- for the transcript, which keeps only a 280-char summary naming the file. Joined at read time by\n-- parsing that summary's '…/tool-results/<name>.txt' marker back to (session_id, name).\nCREATE TABLE IF NOT EXISTS tool_results (\n session_id TEXT, -- session the tool ran in (from the archive path)\n name TEXT, -- file basename without .txt (matches the marker in tool_calls.result_summary)\n path TEXT, -- archive path (provenance)\n bytes INTEGER, -- original size, for the UI\n text TEXT, -- full un-truncated output, stored inline (self-contained DB)\n ingested_at TEXT,\n PRIMARY KEY (session_id, name)\n);\n\n-- Idempotent ingest bookkeeping per archive file. --------------------------\nCREATE TABLE IF NOT EXISTS ingest_state (\n file_path TEXT PRIMARY KEY,\n size INTEGER,\n mtime_ms INTEGER,\n sha256 TEXT,\n events_ingested INTEGER NOT NULL DEFAULT 0,\n ingested_at TEXT\n);\n\n-- Full-text search over event text (contentless external-content FTS). ------\n-- Both columns are indexed: search covered reasoning back when text carried it inline, and the\n-- queries are unqualified MATCHes, which span every column — so indexing thinking here is what makes\n-- splitting the columns invisible to search.\nCREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(\n text,\n thinking,\n content='events',\n content_rowid='rowid'\n);\n\n-- Keep FTS in sync with events. --------------------------------------------\nCREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN\n INSERT INTO events_fts(rowid, text, thinking) VALUES (new.rowid, new.text, new.thinking);\nEND;\nCREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN\n INSERT INTO events_fts(events_fts, rowid, text, thinking) VALUES ('delete', old.rowid, old.text, old.thinking);\nEND;\nCREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events\nWHEN old.text IS NOT new.text OR old.thinking IS NOT new.thinking BEGIN\n INSERT INTO events_fts(events_fts, rowid, text, thinking) VALUES ('delete', old.rowid, old.text, old.thinking);\n INSERT INTO events_fts(rowid, text, thinking) VALUES (new.rowid, new.text, new.thinking);\nEND;\n`;\n","/**\n * SQLite access helpers over Node's built-in `node:sqlite` (ADR-029).\n *\n * `DatabaseSync` covers everything the store needs except `db.transaction(fn)`, which better-sqlite3\n * provided and `node:sqlite` does not. {@link transaction} is that one gap, kept here so `ingest` and\n * `server` share a single implementation of the rollback semantics.\n *\n * One behavioural difference worth knowing when reading rows back: `.all()` / `.get()` return\n * **null-prototype** objects. Spread, `Object.keys`, `JSON.stringify` and vitest's `toEqual` all\n * behave normally, but `row.hasOwnProperty(…)` and `toStrictEqual` do not.\n */\nimport type { DatabaseSync, StatementResultingChanges, StatementSync } from \"node:sqlite\";\n\n/** The open-database handle. Aliased so call sites don't each import from `node:sqlite`. */\nexport type SqliteDB = DatabaseSync;\n\n/**\n * Bind a declared row object to a named-parameter statement.\n *\n * `node:sqlite` types named parameters as `Record<string, SQLInputValue>`, which a declared interface\n * (`TurnRow`, `ToolCallRow`, …) never satisfies — an interface has no index signature even when every\n * field is bindable. The cast lives here rather than at each call site, mirroring the single read-side\n * boundary in the server's `queryAll`.\n */\nexport function runNamed(stmt: StatementSync, row: object): StatementResultingChanges {\n return stmt.run(row as Record<string, never>);\n}\n\n/** Distinguishes nested savepoints from each other; module-scoped so names never collide. */\nlet savepointSeq = 0;\n\n/**\n * Run `fn` inside a transaction, committing on return and rolling back on throw.\n *\n * Returns a *callable* rather than running immediately, matching the better-sqlite3 shape the call\n * sites were written against (`const tx = transaction(db, fn); tx(args)`) — arguments and the return\n * value pass straight through.\n *\n * Uses `SAVEPOINT` when a transaction is already open, because SQLite rejects a nested `BEGIN`.\n * Nothing nests today, but `createDirtySet` runs immediately before several of these and would\n * otherwise fail at runtime the first time one moved inside another.\n */\nexport function transaction<A extends unknown[], R>(db: DatabaseSync, fn: (...args: A) => R): (...args: A) => R {\n return (...args: A): R => {\n const savepoint = db.isTransaction ? `agent_lens_sp_${savepointSeq++}` : \"\";\n db.exec(savepoint ? `SAVEPOINT ${savepoint}` : \"BEGIN\");\n try {\n const result = fn(...args);\n db.exec(savepoint ? `RELEASE ${savepoint}` : \"COMMIT\");\n return result;\n } catch (err) {\n // The unwind can itself fail (a failed COMMIT may already have closed the transaction), and\n // that secondary error would mask the real one. Drop it and rethrow the original cause.\n try {\n db.exec(savepoint ? `ROLLBACK TO ${savepoint}; RELEASE ${savepoint}` : \"ROLLBACK\");\n } catch {\n /* ignore — `err` below is the useful failure */\n }\n throw err;\n }\n };\n}\n","/**\n * Agent Lens — security findings: shared severity model + framework-anchored reference content\n * (ADR-017). This is the low-level, dependency-free source of truth so the ingest detector\n * (packages/ingest/src/detect.ts), the server queries, and the web UI all agree on the severity\n * ordering and the category taxonomy. The reference content also powers the \"what & why\" explainer\n * blocks on the /security page.\n */\n\n// Severity is defined once in @agent-lens/contracts (the node-free shared-types leaf) so the detector,\n// server, and web browser bundle all agree; re-exported here so existing `@agent-lens/core` importers\n// (e.g. the ingest detector) keep their import path.\nexport type { Severity } from \"@agent-lens/contracts\";\nimport type { Severity } from \"@agent-lens/contracts\";\n\n/** Ascending order of concern; index doubles as the numeric rank (info=0 … critical=4). */\nexport const SEVERITY_ORDER: Severity[] = [\"info\", \"low\", \"medium\", \"high\", \"critical\"];\n\n/** Numeric rank of a severity (info=0 … critical=4); -1 for an unknown value. */\nexport function severityRank(s: string): number {\n return SEVERITY_ORDER.indexOf(s as Severity);\n}\n\n/** Shift a severity by `delta` bands, clamped to the [info, critical] range. */\nexport function bumpSeverity(s: Severity, delta: number): Severity {\n const i = SEVERITY_ORDER.indexOf(s);\n const j = Math.max(0, Math.min(SEVERITY_ORDER.length - 1, i + delta));\n return SEVERITY_ORDER[j];\n}\n\n/** The four v1 risk families a finding is categorized into. */\nexport type SecurityCategoryKey = \"destructive\" | \"credential-access\" | \"exfiltration\" | \"privilege-bypass\";\n\nexport interface SecurityCategoryRef {\n key: SecurityCategoryKey;\n title: string;\n /** Short framework anchor shown as a chip (matches findings.framework_ref). */\n framework_ref: string;\n framework_url: string;\n /** What the detector looks for. */\n what: string;\n /** Why it matters for a host running an autonomous agent. */\n why: string;\n /** What to do when you see one. */\n remediation: string;\n}\n\n/**\n * Reference content for each category — the browsable \"security page with info\" the feature was scoped\n * around. Anchored to OWASP Top 10 for Agentic Apps 2026 and MITRE ATLAS. Kept deliberately concise;\n * the point is orientation, not a full framework reproduction.\n */\nexport const SECURITY_CATEGORIES: SecurityCategoryRef[] = [\n {\n key: \"destructive\",\n title: \"Destructive & data-loss operations\",\n framework_ref: \"OWASP ASI02\",\n framework_url: \"https://genai.owasp.org/\",\n what: \"Commands or edits that irreversibly delete, overwrite, or truncate data — rm -rf, git reset --hard, force-pushes, DROP/TRUNCATE, disk writes (dd/mkfs/shred), and overwrites of critical project files (lockfiles, CI config).\",\n why: \"An agent acting on a bad plan or a poisoned instruction can wipe work, history, or a database with a single tool call. Because agent-lens sees sessions after the fact, this is how you notice the damage — and the blast radius — even when nobody was watching live.\",\n remediation: \"Open the flagged session, confirm the target was intended, and check the tool result for whether it succeeded. Restore from git/backups if needed, and consider a PreToolUse hook to block the pattern going forward.\",\n },\n {\n key: \"credential-access\",\n title: \"Secret & credential access\",\n framework_ref: \"MITRE ATLAS: Credential Access\",\n framework_url: \"https://atlas.mitre.org/\",\n what: \"Reads of secret-bearing files (.env, ~/.ssh keys, ~/.aws/credentials, .npmrc, .pem) or secret-looking values (private keys, AWS keys, GitHub/Slack tokens) appearing in a command or a captured tool result.\",\n why: \"Credentials the agent touched may have been exposed to the model context, logs, or a downstream tool — and could be leaked or reused. Retrospective visibility tells you which secrets to rotate.\",\n remediation: \"Treat any accessed secret as potentially exposed: rotate it. Verify the access was purposeful, and keep secrets out of the working tree the agent operates on.\",\n },\n {\n key: \"exfiltration\",\n title: \"Data exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n framework_url: \"https://atlas.mitre.org/techniques/AML.T0086\",\n what: \"Local data flowing outbound: curl/wget uploads to external hosts, piping files into a network command, netcat/reverse-shell patterns, and other ways an agent's write-capable tools become an exfiltration channel.\",\n why: \"Exfiltration via the agent's own tool calls (AML.T0086) is a primary agentic attack path — a prompt-injected agent can POST your files or secrets to an attacker with one command. These are the highest-signal findings to review.\",\n remediation: \"Inspect the destination host and the data referenced in the command. If it was not you, treat it as an incident: rotate anything referenced, review what left the host, and audit the session for the injection source.\",\n },\n {\n key: \"privilege-bypass\",\n title: \"Privilege escalation & guardrail bypass\",\n framework_ref: \"OWASP LLM06\",\n framework_url: \"https://genai.owasp.org/\",\n what: \"Actions that widen the agent's authority or sidestep safety: sudo, chmod 777, curl | sh, --dangerously-skip-permissions, writes outside the working project, and persistence mechanisms (cron, systemd, shell rc files).\",\n why: \"Excessive agency (LLM06) is the root cause behind most agent incidents — every disabled guardrail or out-of-scope write removes a barrier the design relied on. Persistence in particular outlives the session that created it.\",\n remediation: \"Confirm the escalation was necessary and scoped. Remove any persistence the agent added, re-tighten permissions, and avoid running agents with skipped permission prompts on a trusted host.\",\n },\n];\n","/**\n * Canonical secret / PII patterns — single source of truth for masking and leak-scanning.\n *\n * Two consumers:\n * • the corpus redactor's fail-closed scan (`LEAK_PATTERNS` / `findLeak`, relocated here from\n * packages/ingest/src/redact.ts and re-exported there so its API is unchanged), and\n * • the redacted-export sanitizer (`maskSecrets`, `findShareLeak`) — see redact-export.ts.\n *\n * NOTE: packages/ingest/src/detect.ts keeps its own SECRET_VALUE_PATTERNS today (its v8 rules are\n * FP-tuned around them); a future cleanup can have it import SECRET_PATTERNS from here to dedupe.\n */\n\nexport interface LeakPattern {\n name: string;\n re: RegExp;\n}\n\n// Base (non-global) patterns reused across the leak-scan sets and the /g masking sets below, so a\n// tightening happens once (the module's \"single source of truth\" promise). The masking sets derive a\n// global copy via `new RegExp(base.source, \"g\")`; the leak-scan sets (findLeak → `re.exec`, non-global,\n// always searches from 0) can share the object directly.\nconst EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/;\nconst IPV4 = /\\b\\d{1,3}(?:\\.\\d{1,3}){3}\\b/;\nconst HOME_PATH_WITH_USER = /\\/(?:home|Users)\\/(?!user\\b)[A-Za-z0-9._-]+/;\n\n/** Patterns that must NEVER appear in redacted CORPUS output (strict, forbids URLs/IPs entirely). */\nexport const LEAK_PATTERNS: LeakPattern[] = [\n { name: \"email\", re: EMAIL },\n { name: \"home-path-with-user\", re: HOME_PATH_WITH_USER },\n { name: \"ipv4\", re: IPV4 },\n { name: \"url\", re: /https?:\\/\\/[^\\s\"']+/ },\n { name: \"aws-key\", re: /AKIA[0-9A-Z]{16}/ },\n { name: \"bearer/token\", re: /\\b(?:sk|pk|ghp|gho|xox[abp])[-_][A-Za-z0-9]{8,}/ },\n];\n\n/** Return the first leak found by `patterns` (default: strict corpus set), or null. */\nexport function findLeak(text: string, patterns: LeakPattern[] = LEAK_PATTERNS): { name: string; sample: string } | null {\n for (const { name, re } of patterns) {\n const m = re.exec(text);\n if (m) return { name, sample: m[0].slice(0, 40) };\n }\n return null;\n}\n\n/** Secret VALUE patterns → a human-readable placeholder label. Mirrors detect.ts's stronger set,\n * extended (2026-07-18 audit) with formats seen in the wild: Google/Stripe/OpenAI/Anthropic keys,\n * GitHub fine-grained PATs, npm tokens, Slack app tokens, and JWTs. Hyphen/underscore-bearing keys\n * (sk-proj-…, sk-ant-…, sk_live_…) need their own entries — the generic `sk-…{20,}` stops at the\n * first separator and would miss them. */\nexport const SECRET_PATTERNS: Array<{ re: RegExp; label: string }> = [\n { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g, label: \"PRIVATE-KEY\" },\n { re: /\\bAKIA[0-9A-Z]{16}\\b/g, label: \"AWS-KEY\" },\n { re: /\\bASIA[0-9A-Z]{16}\\b/g, label: \"AWS-KEY\" },\n { re: /\\bgithub_pat_[A-Za-z0-9_]{20,}/g, label: \"GITHUB-TOKEN\" },\n { re: /\\bgh[pousr]_[A-Za-z0-9]{30,}\\b/g, label: \"GITHUB-TOKEN\" },\n { re: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/g, label: \"SLACK-TOKEN\" },\n { re: /\\bxapp-[0-9A-Za-z-]{10,}/g, label: \"SLACK-TOKEN\" },\n { re: /\\bnpm_[A-Za-z0-9]{30,}/g, label: \"NPM-TOKEN\" },\n { re: /\\bAIza[0-9A-Za-z_-]{35}\\b/g, label: \"GOOGLE-API-KEY\" },\n { re: /\\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,}\\b/g, label: \"STRIPE-KEY\" },\n { re: /\\bsk-(?:proj|ant|or|svcacct)-[A-Za-z0-9_-]{20,}/g, label: \"API-KEY\" },\n { re: /\\bsk-[A-Za-z0-9]{20,}\\b/g, label: \"API-KEY\" },\n { re: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{6,}/g, label: \"JWT\" },\n];\n\n/** URL-embedded credentials survive the kept-readable-URLs policy, so mask them explicitly:\n * basic-auth userinfo (`scheme://user:pass@host` → keep the host) and secret query-param values. */\nfunction maskUrlCredentials(text: string): string {\n return text\n .replace(/([a-z][a-z0-9+.-]*:\\/\\/)[^/\\s:@\"'`]+:[^/\\s@\"'`]+@/gi, \"$1[REDACTED-AUTH]@\")\n .replace(\n /([?&](?:access_?token|api[_-]?key|apikey|token|secret|password|pwd|client[_-]?secret|auth|signature|sig)=)[^&\\s\"'`<>]+/gi,\n \"$1[REDACTED]\",\n );\n}\n\n/** Conservative env-var secret assignment: mask a LITERAL value of NAME=… when NAME looks secret\n * (…_TOKEN/_SECRET/_KEY/_PASSWORD/… or a bare PASSWORD/SECRET/TOKEN) and the value is not a `$VAR`\n * / `${VAR}` reference (those are safe and common, so kept). Known key formats are already handled\n * by SECRET_PATTERNS; this catches opaque literals like `DB_PASSWORD=hunter2`. */\nconst ENV_ASSIGN =\n /\\b([A-Za-z][A-Za-z0-9_]*_(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PWD|PW|CREDENTIALS?)|PASSWORD|PASSWD|SECRET|TOKEN)=([\"']?)(?!\\$)[^\\s\"'`&;|<>()]+/gi;\nfunction maskEnvAssignments(text: string): string {\n return text.replace(ENV_ASSIGN, (_m, name, q) => `${name}=${q}[REDACTED]`);\n}\n\n/** PII value patterns → placeholder label (home paths are handled separately, username-strip only). */\nconst PII_PATTERNS: Array<{ re: RegExp; label: string }> = [\n { re: new RegExp(EMAIL.source, \"g\"), label: \"EMAIL\" },\n { re: new RegExp(IPV4.source, \"g\"), label: \"IP\" },\n];\n\n/** Strip only the `<user>` segment of a home path so `/home/alice/x` → `/home/user/x` (rest kept). */\nconst HOME_USER = /\\/(home|Users)\\/[^/\\s\"'`]+/g;\n\n/** Claude Code encodes a project path by replacing \"/\" with \"-\", so `/home/alice/proj` becomes\n * `-home-alice-proj`. That form appears verbatim in scratchpad/task paths in transcript text and\n * leaks the username past the slash-form strip. Match `-home-<user>-` (not preceded by an\n * alphanumeric, so `flex-home-x` is left alone) and drop only the username segment. */\nconst ENCODED_HOME_USER = /(?<![A-Za-z0-9])(-(?:home|Users)-)[A-Za-z0-9._]+(?=-)/g;\n\n/** Windows profile path: `C:\\Users\\Alice\\…` → `C:\\Users\\user\\…`. */\nconst WINDOWS_USER = /([A-Za-z]:\\\\Users\\\\)[^\\\\/\\s\"'`]+/gi;\n\n/**\n * Selective share masking: replace secret values and PII with labels, and strip home usernames,\n * while leaving the surrounding narrative (and ordinary URLs) readable. Idempotent.\n */\nexport function maskSecrets(text: string): string {\n let out = maskUrlCredentials(text);\n out = maskEnvAssignments(out);\n for (const { re, label } of SECRET_PATTERNS) out = out.replace(re, `[${label}]`);\n for (const { re, label } of PII_PATTERNS) out = out.replace(re, `[${label}]`);\n out = out.replace(HOME_USER, (_m, root) => `/${root}/user`);\n out = out.replace(ENCODED_HOME_USER, (_m, root) => `${root}user`);\n out = out.replace(WINDOWS_USER, (_m, root) => `${root}user`);\n return out;\n}\n\n/**\n * Fail-closed scan set for the selective `secrets` share level: secret values + email +\n * home-path-with-user. Deliberately EXCLUDES generic url/ipv4 so legitimate doc links stay\n * readable (IPs are already masked by maskSecrets).\n */\nexport const SHARE_LEAK: LeakPattern[] = [\n ...SECRET_PATTERNS.map(({ re, label }) => ({ name: label, re: new RegExp(re.source) })),\n { name: \"email\", re: EMAIL },\n { name: \"home-path-with-user\", re: HOME_PATH_WITH_USER },\n { name: \"encoded-home-with-user\", re: /(?<![A-Za-z0-9])-(?:home|Users)-(?!user-)[A-Za-z0-9._]+-/ },\n { name: \"url-credentials\", re: /:\\/\\/[^/\\s:@\"'`]+:[^/\\s@\"'`]+@/ },\n];\n\n/** First leak under the selective share set, or null. */\nexport function findShareLeak(text: string): { name: string; sample: string } | null {\n return findLeak(text, SHARE_LEAK);\n}\n\n// Common / non-identifying home-dir owners — masking these would mangle prose (e.g. the OS name\n// \"Ubuntu\") without protecting an individual's identity, so they are never scrubbed as a username.\nconst USER_STOPLIST = new Set([\n \"user\", \"users\", \"home\", \"root\", \"admin\", \"ubuntu\", \"runner\", \"node\", \"app\", \"apps\",\n \"data\", \"tmp\", \"shared\", \"public\", \"guest\", \"vagrant\", \"ec2-user\", \"docker\",\n]);\n// A home-dir owner: the segment right after a home root, in slash, encoded, or Windows form.\n// The username capture excludes \"-\" so the encoded form (-home-u-proj) stops at the next segment.\nconst HOME_OWNER = /(?:\\/(?:home|Users)\\/|-(?:home|Users)-|[A-Za-z]:\\\\Users\\\\)([A-Za-z0-9._]{3,})/g;\n\n/** Derive the username(s) that own a home directory anywhere in the given texts. These are provably\n * PII (they own a home dir in the transcript), so they can be scrubbed everywhere — including bare\n * occurrences in URLs/prose that no path pattern would catch. Common/OS owners are excluded. */\nexport function deriveHomeUsers(texts: Array<string | null | undefined>): string[] {\n const found = new Set<string>();\n for (const t of texts) {\n if (!t) continue;\n const re = new RegExp(HOME_OWNER.source, \"g\");\n let m: RegExpExecArray | null;\n while ((m = re.exec(t))) {\n const u = m[1]!;\n if (u.length >= 3 && !USER_STOPLIST.has(u.toLowerCase())) found.add(u);\n }\n }\n return [...found];\n}\n\n/** Replace whole-token occurrences of each derived username with `[USER]`. */\nexport function scrubUsernames(text: string, users: string[]): string {\n let out = text;\n for (const u of users) {\n const esc = u.replace(/[.*+?^${}()|[\\]\\\\-]/g, \"\\\\$&\");\n out = out.replace(new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, \"g\"), \"[USER]\");\n }\n return out;\n}\n","/**\n * Redacted, export-only sharing (backlog #3). Produces a sanitized Markdown artifact the user\n * shares MANUALLY — no network, no upload, no server-side link. Distinct from the corpus Redactor\n * (packages/ingest/src/redact.ts), which is deny-by-default for a metric-preserving test corpus and\n * would scrub a share into a wall of \"[redacted]\". Here the default keeps the narrative readable and\n * masks only secrets/PII, with a fail-closed post-render scan; `structure` is the aggressive scrub;\n * `off` is the explicit verbatim opt-out.\n *\n * Redaction is BEST-EFFORT / pattern-based, not a guarantee — the emitted header says so.\n */\nimport { createHash } from \"node:crypto\";\nimport { sessionToMarkdown, type MarkdownSession, type MarkdownEvent } from \"./markdown.js\";\nimport { maskSecrets, deriveHomeUsers, scrubUsernames, LEAK_PATTERNS, SHARE_LEAK, type LeakPattern } from \"./secrets.js\";\n\nexport type RedactionLevel = \"secrets\" | \"structure\";\nexport interface ExportOptions {\n level?: RedactionLevel | \"off\";\n}\nexport interface ExportResult {\n markdown: string;\n /** True if the fail-closed post-render scan had to catch a leak the field pass missed. */\n residualLeak: boolean;\n level: RedactionLevel | \"off\";\n}\n\nfunction hash(s: string): string {\n return createHash(\"sha256\").update(s).digest(\"hex\").slice(0, 4);\n}\n\n/** Aggressive path pseudonymization for the `structure` level (home username still stripped). */\nfunction structurePath(p: string | null): string | null {\n if (!p) return p;\n const abs = p.startsWith(\"/\");\n const segs = p.split(\"/\").filter(Boolean);\n const out = segs.map((seg, i) => {\n if (segs[i - 1] === \"home\" || segs[i - 1] === \"Users\") return \"user\";\n if (seg === \"home\" || seg === \"Users\") return seg;\n const last = i === segs.length - 1;\n const dot = last ? seg.lastIndexOf(\".\") : -1;\n return dot > 0 ? `file-${hash(seg)}${seg.slice(dot)}` : `dir-${hash(seg)}`;\n });\n return (abs ? \"/\" : \"\") + out.join(\"/\");\n}\n\n/** Replace narrative text with a placeholder, preserving null/empty. */\nfunction scrub(s: string | null): string | null {\n return s && s.length > 0 ? \"[redacted]\" : s;\n}\n\n/** Apply the chosen redaction policy to the export data model, returning fresh copies. */\nfunction redactSession(\n session: MarkdownSession,\n events: MarkdownEvent[],\n level: RedactionLevel,\n): { session: MarkdownSession; events: MarkdownEvent[] } {\n // Derive the session's home-dir owner(s) from ALL raw fields, so a bare username in a github URL\n // or prose (no path form) is scrubbed too. Done pre-masking, before /home/<u>/ becomes /home/user/.\n const users = deriveHomeUsers([\n session.title, session.project, session.source,\n ...events.flatMap((e) => [e.text, e.thinking, ...e.toolCalls.map((t) => t.input_json)]),\n ]);\n const unuser = (s: string | null) => (s == null ? s : scrubUsernames(s, users));\n\n if (level === \"structure\") {\n return {\n session: { ...session, title: session.title ? \"Redacted session\" : session.title, project: structurePath(session.project), source: unuser(session.source) },\n events: events.map((e) => ({\n ...e,\n text: scrub(e.text),\n thinking: scrub(e.thinking),\n toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: t.input_json ? \"{}\" : t.input_json })),\n })),\n };\n }\n // \"secrets\": selective masking, narrative preserved.\n const mask = (s: string | null) => (s == null ? s : scrubUsernames(maskSecrets(s), users));\n return {\n session: { ...session, title: mask(session.title), project: mask(session.project), source: mask(session.source) },\n events: events.map((e) => ({\n ...e,\n text: mask(e.text),\n thinking: mask(e.thinking),\n toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: mask(t.input_json) })),\n })),\n };\n}\n\n/** Global replace of any scan-set match with [REDACTED]; reports whether anything was caught. */\nfunction sweep(md: string, patterns: LeakPattern[]): { md: string; caught: boolean } {\n let out = md;\n let caught = false;\n for (const { re } of patterns) {\n const g = new RegExp(re.source, re.flags.includes(\"g\") ? re.flags : re.flags + \"g\");\n if (g.test(out)) {\n caught = true;\n out = out.replace(g, \"[REDACTED]\");\n }\n }\n return { md: out, caught };\n}\n\nfunction disclaimer(level: RedactionLevel): string {\n return (\n `> 🔒 **Redacted export** (level: ${level}) — best-effort automatic masking of secrets, tokens, ` +\n `emails, IPs, and usernames. Not a guarantee; review before sharing. Generated locally by ` +\n `Agent Lens — never uploaded.`\n );\n}\n\n/**\n * Render a session to a shareable Markdown artifact. The single entry point used by the server\n * endpoint and the CLI. For redacted levels: redact fields → render → fail-closed sweep →\n * prepend the disclaimer.\n */\nexport function exportMarkdown(session: MarkdownSession, events: MarkdownEvent[], opts: ExportOptions = {}): ExportResult {\n const level = opts.level ?? \"secrets\";\n if (level === \"off\") {\n return { markdown: sessionToMarkdown(session, events), residualLeak: false, level: \"off\" };\n }\n const redacted = redactSession(session, events, level);\n const rendered = sessionToMarkdown(redacted.session, redacted.events);\n const scanSet = level === \"structure\" ? LEAK_PATTERNS : SHARE_LEAK;\n const { md, caught } = sweep(rendered, scanSet);\n return { markdown: `${disclaimer(level)}\\n\\n${md}`, residualLeak: caught, level };\n}\n","/** Render a session to Markdown — shared by the export endpoint (ADR: data exportable as markdown). */\n\nexport interface MarkdownSession {\n id: string;\n title: string | null;\n source: string | null;\n project: string | null;\n model: string | null;\n started_at: string | null;\n ended_at: string | null;\n}\n\nexport interface MarkdownToolCall {\n tool_name: string;\n skill_name: string | null;\n agent_type: string | null;\n input_json: string | null;\n status: string | null;\n}\n\nexport interface MarkdownEvent {\n type: string;\n role: string | null;\n timestamp: string | null;\n text: string | null;\n thinking: string | null;\n toolCalls: MarkdownToolCall[];\n}\n\n/** Cap long prose/JSON at 2000 chars so an export stays readable — this truncates, it does not fence. */\nfunction truncate(s: string): string {\n return s.length > 2000 ? s.slice(0, 2000) + \"\\n… (truncated)\" : s;\n}\n\nexport function sessionToMarkdown(s: MarkdownSession, events: MarkdownEvent[]): string {\n const out: string[] = [];\n out.push(`# ${s.title || \"Session \" + s.id.slice(0, 8)}`);\n out.push(\"\");\n const meta = [\n [\"Session\", s.id],\n [\"Source\", s.source],\n [\"Project\", s.project],\n [\"Model\", s.model],\n [\"Started\", s.started_at],\n [\"Ended\", s.ended_at],\n ].filter(([, v]) => v);\n for (const [k, v] of meta) out.push(`- **${k}:** ${v}`);\n out.push(\"\");\n out.push(\"---\");\n out.push(\"\");\n\n for (const e of events) {\n const who = e.role || e.type;\n const ts = e.timestamp ? ` _(${e.timestamp})_` : \"\";\n if (e.thinking) {\n out.push(`### 🧠 thinking${ts}`);\n out.push(\"\");\n out.push(truncate(e.thinking));\n out.push(\"\");\n }\n if (e.text) {\n const icon = who === \"user\" ? \"👤\" : who === \"assistant\" ? \"🤖\" : \"⚙️\";\n out.push(`### ${icon} ${who}${ts}`);\n out.push(\"\");\n out.push(truncate(e.text));\n out.push(\"\");\n }\n for (const tc of e.toolCalls) {\n const label = tc.skill_name\n ? `Skill: ${tc.skill_name}`\n : tc.agent_type\n ? `${tc.tool_name} → ${tc.agent_type}`\n : tc.tool_name;\n const status = tc.status ? ` _(${tc.status})_` : \"\";\n out.push(`> 🔧 **${label}**${status}`);\n if (tc.input_json && tc.input_json !== \"{}\") {\n out.push(\"```json\");\n out.push(truncate(tc.input_json));\n out.push(\"```\");\n }\n out.push(\"\");\n }\n }\n return out.join(\"\\n\");\n}\n","/**\n * Agent Lens — tool-error classification. No AI: an errored tool call is bucketed deterministically\n * by pattern-matching its verbatim `result_summary`. Re-runnable and idempotent — the same text always\n * yields the same bucket. `ERROR_CLASSIFIER_VERSION` lets a future engine supersede prior labels.\n *\n * ── The authority boundary (why this is careful) ────────────────────────────────────────────────\n * The Messages API's tool_result carries a single boolean `is_error` and **no field for *why*** a\n * result is an error — the API does not distinguish a tool that genuinely failed from one the user or\n * a permission/guardrail system rejected (see platform.claude.com tool-use docs). Claude Code sets\n * `is_error: true` for *both* real failures and permission denials / guardrail blocks; the only signal\n * of which is the human-readable result text (see code.claude.com/docs/en/errors + the auto-mode post).\n *\n * Therefore: the raw `is_error` / `tool_calls.status='error'` **count** is the authoritative, documented\n * signal. The `type` bucket and the failure-vs-rejection `kind` below are a **heuristic over Claude\n * Code's result wording** — best-effort, and liable to drift if that wording changes. Surface them as\n * such (not as an API-blessed fact), and bump ERROR_CLASSIFIER_VERSION when the patterns change.\n */\n\n// Bump on any pattern/bucket change so a reclassification is attributable to an engine version\n// (mirrors CLASSIFIER_VERSION / DETECTOR_VERSION).\nexport const ERROR_CLASSIFIER_VERSION = 1;\n\n/** The error bucket. `other` = errored but unmatched by the patterns below. */\nexport type ToolErrorType =\n | \"string-not-found\" // Edit: the old_string wasn't found in the file\n | \"command-failed\" // Bash: non-zero exit code\n | \"file-state\" // Read/Write/Edit: file missing / not-read-first / changed-since-read / is-a-directory\n | \"token-limit\" // Read: file content exceeds the tool's token cap\n | \"user-rejected\" // the human declined / interrupted / cancelled the tool use\n | \"guardrail-blocked\" // Claude Code's own guardrail blocked the command\n | \"other\";\n\n/**\n * Whether the error reflects the agent's tool genuinely failing (`failure`) or a human/harness\n * declining to run it (`rejection`). Rejections are NOT agent failures — separating them keeps the\n * \"how often did the agent's tools fail\" signal honest.\n */\nexport type ToolErrorKind = \"failure\" | \"rejection\";\n\nexport interface ToolErrorClass {\n type: ToolErrorType;\n kind: ToolErrorKind;\n}\n\n/** Error types that represent a human/harness declining the tool use — NOT the agent's tool failing. */\nconst REJECTION_TYPES: ReadonlySet<ToolErrorType> = new Set<ToolErrorType>([\"user-rejected\", \"guardrail-blocked\"]);\n\n/** The kind (failure vs rejection) implied by an error type — the single mapping used everywhere. */\nexport function errorKind(type: ToolErrorType): ToolErrorKind {\n return REJECTION_TYPES.has(type) ? \"rejection\" : \"failure\";\n}\n\n// Ordered most-specific → fallback; first match wins. Rejection patterns are checked first because a\n// rejection message can also mention a command that would otherwise look like a real failure. `kind` is\n// derived from `type` via errorKind() — the single source of that mapping — not re-encoded per row.\nconst PATTERNS: Array<{ re: RegExp; type: ToolErrorType }> = [\n // — Rejections / blocks: is_error, but the agent's tool didn't fail — a human or guardrail stopped it.\n { re: /interrupted by user|tool use was rejected|does(?:n['’]| not)t want to proceed|\\bCancelled:/i, type: \"user-rejected\" },\n { re: /\\bBlocked:/i, type: \"guardrail-blocked\" },\n // — Real tool failures.\n { re: /String to replace not found/i, type: \"string-not-found\" },\n { re: /exceeds maximum allowed tokens/i, type: \"token-limit\" },\n // A non-zero Bash exit code is a command failure regardless of what its stderr text mentions — check\n // it before the file-state fs-error patterns, since ENOENT/EISDIR also appear inside command stderr\n // (e.g. `Exit code 2 npm error ENOENT`). The file-state patterns then catch the Read/Write/Edit\n // tool_use_errors, which carry no exit code.\n { re: /\\bExit code\\b/i, type: \"command-failed\" },\n { re: /has not been read yet|modified since (?:you |it was )?read|File (?:content )?does not exist|\\bEISDIR\\b|\\bENOENT\\b/i, type: \"file-state\" },\n];\n\n/**\n * Classify an errored tool call from its `result_summary` text. Only call this on tool calls already\n * known to be errors (`status='error'` / `is_error: true`) — it always returns a class, defaulting to\n * `{ type: \"other\", kind: \"failure\" }` for an errored result whose text matches no pattern.\n */\nexport function classifyToolError(resultSummary: string | null | undefined): ToolErrorClass {\n const s = resultSummary ?? \"\";\n for (const p of PATTERNS) {\n if (p.re.test(s)) return { type: p.type, kind: errorKind(p.type) };\n }\n return { type: \"other\", kind: errorKind(\"other\") };\n}\n","/**\n * `UserError` — an error caused by what the user typed or configured, as opposed to a bug.\n *\n * The CLI's top-level boundary (packages/cli/src/index.ts) prints these as a single\n * `agent-lens: <message>` line and exits 1, and lets everything else through with its stack intact.\n * That split is the whole point: a bad `--times` value is not a crash and shouldn't read like one,\n * while a genuine TypeError is worthless without the trace.\n *\n * So throw this only where the message is already written *for the user* — it becomes the entire\n * output they see, with no stack to fall back on.\n *\n * (Unrelated to `errors.ts`, which classifies the tool-call errors found inside collected\n * transcripts. This one is about the CLI process itself.)\n */\nexport class UserError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"UserError\";\n }\n}\n","/**\n * Model pricing for deriving cost from token counts (ADR-003: no cost is stored in traces).\n * USD per 1M tokens. Cache-read is the discounted rate; cache-write the premium rate.\n *\n * Rates are matched by prefix so dated/variant model ids resolve (e.g. 'claude-opus-4-8[1m]').\n * Update as pricing changes; unknown models cost 0 and are reported separately.\n *\n * PRICE_TABLE is the built-in default. A `pricing` block in agent-lens.config.json can override or\n * extend it (ADR-028) — `resolvePricing()` in config.ts builds the merged table and the process\n * entry point installs it with {@link usePricing}, so a model that launches between releases can be\n * priced without one. Cost is still derived at read time, never stored.\n */\n\nexport interface Rate {\n input: number;\n output: number;\n cacheWrite: number;\n cacheRead: number;\n}\n\n/**\n * Claude Code stamps this on locally-generated messages — no API call was made, so it is correctly\n * unpriced and must never be reported as a missing rate. Excluded wherever unpriced models are\n * surfaced (ingest report, /api/about, per-session markers).\n */\nexport const SYNTHETIC_MODEL = \"<synthetic>\";\n\n/** Model ids with real token usage but no rate in the active table — the \"cost is understated\" set. */\nexport function unpricedModels(models: Iterable<string | null | undefined>): string[] {\n const out = new Set<string>();\n for (const m of models) {\n if (m && m !== SYNTHETIC_MODEL && !rateForModel(m)) out.add(m);\n }\n return [...out].sort();\n}\n\nexport interface UsageTokens {\n input_tokens: number;\n output_tokens: number;\n cache_creation_input_tokens: number;\n cache_read_input_tokens: number;\n}\n\n/**\n * Prefix → rate. Longest matching prefix wins, so dated/variant ids resolve to the most specific\n * entry (e.g. 'claude-opus-4-8' beats the legacy 'claude-opus-4' fallback).\n * USD per 1M tokens. Convention: cacheWrite (5-minute) = 1.25× input, cacheRead = 0.1× input.\n */\nexport const PRICE_TABLE: Record<string, Rate> = {\n // 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.\n // The bare 'claude-opus-4' fallback stays at the legacy rate for 4.0/4.1; 4.5+ override below.\n \"claude-opus-4\": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },\n \"claude-opus-4-5\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-4-6\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-4-7\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-4-8\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-opus-5\": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },\n \"claude-fable-5\": { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1.0 },\n \"claude-sonnet-4\": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },\n // Sonnet 5 list price. An introductory $2/$10 rate runs through 2026-08-31; we bill the list\n // price so the estimate doesn't silently change under a date, matching the \"list price\" label.\n \"claude-sonnet-5\": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },\n \"claude-haiku-4\": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },\n \"claude-3-5-haiku\": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },\n \"claude-3-5-sonnet\": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },\n};\n\n/** Cache-rate convention when an override gives only input/output — mirrors the PRICE_TABLE entries. */\nconst CACHE_WRITE_MULTIPLIER = 1.25;\nconst CACHE_READ_MULTIPLIER = 0.1;\n\nexport interface MergedPricing {\n table: Record<string, Rate>;\n /** Keys rejected as malformed. Reported, never applied — a typo must not silently reprice a model. */\n invalid: string[];\n /** Keys that were accepted, so callers can report \"N overrides in force\" without diffing tables. */\n applied: string[];\n}\n\nfunction toRate(v: unknown): Rate | null {\n if (v === null || typeof v !== \"object\" || Array.isArray(v)) return null;\n const o = v as Record<string, unknown>;\n const num = (x: unknown): number | null =>\n typeof x === \"number\" && Number.isFinite(x) && x >= 0 ? x : null;\n const input = num(o.input);\n const output = num(o.output);\n if (input === null || output === null) return null;\n // Optional keys must be valid *if present* — a mistyped cacheRead falls back to the convention\n // silently otherwise, which is the same class of bug as the missing entry this all came from.\n if (o.cacheWrite !== undefined && num(o.cacheWrite) === null) return null;\n if (o.cacheRead !== undefined && num(o.cacheRead) === null) return null;\n return {\n input,\n output,\n cacheWrite: num(o.cacheWrite) ?? input * CACHE_WRITE_MULTIPLIER,\n cacheRead: num(o.cacheRead) ?? input * CACHE_READ_MULTIPLIER,\n };\n}\n\n/**\n * Overlay user-supplied rates on a base table, by exact prefix key. Pure: the caller decides what to\n * do with `invalid` (we warn and keep the built-in rate — cost is a labelled estimate, so a typo in\n * an optional block must not stop the tool from running).\n */\nexport function mergePricing(base: Record<string, Rate>, overrides: unknown): MergedPricing {\n const table = { ...base };\n const invalid: string[] = [];\n const applied: string[] = [];\n if (overrides === null || typeof overrides !== \"object\" || Array.isArray(overrides)) {\n return { table, invalid, applied };\n }\n for (const [prefix, value] of Object.entries(overrides as Record<string, unknown>)) {\n const rate = toRate(value);\n if (!prefix.trim() || !rate) invalid.push(prefix);\n else {\n table[prefix] = rate;\n applied.push(prefix);\n }\n }\n return { table, invalid: invalid.sort(), applied: applied.sort() };\n}\n\n/**\n * The table `rateForModel` reads. Process-wide and set once at startup by the CLI entry points, so\n * the ~6 cost call sites keep their signatures and no table has to be threaded through the server's\n * query layer. Tests that install a table must restore PRICE_TABLE afterwards.\n */\nlet activeTable: Record<string, Rate> = PRICE_TABLE;\n\n/** Install the effective price table (built-in defaults merged with config overrides). */\nexport function usePricing(table: Record<string, Rate>): void {\n activeTable = table;\n}\n\n/** The table currently in force — for diagnostics (`agent-lens config`, /api/about). */\nexport function activePricing(): Record<string, Rate> {\n return activeTable;\n}\n\nexport function rateForModel(model: string | null | undefined): Rate | null {\n if (!model) return null;\n let best: { len: number; rate: Rate } | null = null;\n for (const [prefix, rate] of Object.entries(activeTable)) {\n if (model.startsWith(prefix) && (!best || prefix.length > best.len)) {\n best = { len: prefix.length, rate };\n }\n }\n return best?.rate ?? null;\n}\n\n/** Cost in USD for one usage record. Returns 0 for unknown models. */\nexport function costForUsage(model: string | null | undefined, u: UsageTokens): number {\n const r = rateForModel(model);\n if (!r) return 0;\n const M = 1_000_000;\n return (\n (u.input_tokens * r.input +\n u.output_tokens * r.output +\n u.cache_creation_input_tokens * r.cacheWrite +\n u.cache_read_input_tokens * r.cacheRead) /\n M\n );\n}\n","/**\n * Agent Lens — `events.raw_json` storage codec (ADR-011).\n *\n * Every transcript line is kept verbatim in `events.raw_json` for lossless re-derivation. That column\n * dominates DB growth, so it is stored gzip-compressed as a BLOB. This module is the single chokepoint:\n * the ingest writer compresses with `packRaw`, the server reader decompresses with `unpackRaw`.\n *\n * `unpackRaw` sniffs the gzip magic bytes (0x1f 0x8b) so it transparently handles a mix of compressed\n * BLOBs and legacy plain-text rows (a DB written before the migration, or read before `--full` runs).\n * JSON always begins with `{`/`[`/`\"`/whitespace, never 0x1f, so the sniff is unambiguous.\n */\nimport { gzipSync, gunzipSync } from \"node:zlib\";\n\n/** Compress a raw transcript line for storage in `events.raw_json` (BLOB). */\nexport function packRaw(json: string): Buffer {\n return gzipSync(Buffer.from(json, \"utf8\"));\n}\n\n/**\n * Decompress a stored `raw_json` value. Accepts gzip BLOBs and legacy plain text/strings.\n *\n * Takes `Uint8Array`, not `Buffer`: `node:sqlite` hands BLOBs back as plain `Uint8Array` (ADR-029),\n * and `Buffer.isBuffer` is false for those. Gating on `Buffer` here would silently skip the gunzip\n * and return the compressed bytes as if they were legacy plain text. `Buffer` still satisfies this —\n * it is a `Uint8Array` subclass — so the write path is unchanged.\n */\nexport function unpackRaw(value: string | Uint8Array): string {\n if (typeof value === \"string\") return value;\n if (value.length >= 2 && value[0] === 0x1f && value[1] === 0x8b) {\n return gunzipSync(value).toString(\"utf8\");\n }\n return Buffer.from(value).toString(\"utf8\");\n}\n","/**\n * Path resolution shared by ingest, server, and the CLI. The goal is that agent-lens works both\n * from the dev monorepo (data under <repo>/data, config at the repo root) AND when installed as a\n * standalone package under node_modules (per-user OS data dir, no repo layout to assume).\n *\n * Nothing here reads process state beyond env vars + the filesystem, so it is safe to call from any\n * entrypoint. `import.meta.url`-relative resolution replaces the old hardcoded \"../../..\" repo anchor.\n */\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { homedir, platform } from \"node:os\";\n\n/** Expand a leading `~` or `$HOME` and resolve to an absolute path. Empty input passes through. */\nexport function expandPath(p: string): string {\n if (!p) return p;\n let out = p;\n if (out === \"~\") out = homedir();\n else if (out.startsWith(\"~/\")) out = join(homedir(), out.slice(2));\n out = out.replace(/\\$HOME/g, homedir());\n return resolve(out);\n}\n\nconst moduleDir = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Find the agent-lens monorepo root by walking up from `from` (default: this module's location).\n * A directory qualifies only if it has BOTH a `pnpm-workspace.yaml` AND a `package.json` named\n * \"agent-lens\" — so an unrelated pnpm workspace never matches, and the published/bundled package\n * (which ships neither) resolves to `null`, sending data to the per-user dir instead.\n */\nexport function findRepoRoot(from: string = moduleDir): string | null {\n let dir = resolve(from);\n for (;;) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) {\n try {\n const pkg = JSON.parse(readFileSync(join(dir, \"package.json\"), \"utf8\"));\n if (pkg?.name === \"agent-lens\") return dir;\n } catch {\n /* not the file we want — keep walking */\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/** Per-user data directory used when not running from the repo (installed CLI). */\nfunction userDataDir(): string {\n const home = homedir();\n if (platform() === \"win32\") {\n return join(process.env.LOCALAPPDATA || join(home, \"AppData\", \"Local\"), \"agent-lens\");\n }\n if (platform() === \"darwin\") {\n return join(home, \"Library\", \"Application Support\", \"agent-lens\");\n }\n return join(process.env.XDG_DATA_HOME || join(home, \".local\", \"share\"), \"agent-lens\");\n}\n\n/** The data dir: `AGENT_LENS_DATA` → `<repo>/data` (dev) → per-user OS dir (installed). */\nexport function resolveDataDir(repoRoot: string | null = findRepoRoot()): string {\n if (process.env.AGENT_LENS_DATA) return resolve(process.env.AGENT_LENS_DATA);\n if (repoRoot) return join(repoRoot, \"data\");\n return userDataDir();\n}\n\n/**\n * The raw transcript mirror, always `<dataDir>/archive` (ADR-021). Deliberately not independently\n * configurable: it is the source of truth, so a stray override desyncs the writer (collect) from the\n * readers (ingest, refresh) and silently strands previously collected data. Move the data dir instead.\n */\nexport function resolveArchiveDir(repoRoot: string | null = findRepoRoot()): string {\n return join(resolveDataDir(repoRoot), \"archive\");\n}\n\n/**\n * The writable triage/prefs sidecar (ADR-018) for a given store: `triage.db` beside it. Also fixed\n * (ADR-021) — it holds hand-authored state that nothing can rebuild, so it travels with the db it\n * annotates rather than taking an override of its own.\n */\nexport function triageDbFor(dbPath: string): string {\n return join(dirname(dbPath), \"triage.db\");\n}\n\n/**\n * First existing config file, in precedence order:\n * `AGENT_LENS_CONFIG` → `<dataDir>/agent-lens.config.json` → repo config → repo example.\n * Returns `null` when none exist (callers fall back to the built-in default source).\n */\nexport function resolveConfigFile(repoRoot: string | null, dataDir: string): string | null {\n const candidates = [\n process.env.AGENT_LENS_CONFIG,\n join(dataDir, \"agent-lens.config.json\"),\n repoRoot ? join(repoRoot, \"agent-lens.config.json\") : null,\n repoRoot ? join(repoRoot, \"agent-lens.config.example.json\") : null,\n ].filter((x): x is string => Boolean(x));\n for (const f of candidates) if (existsSync(f)) return f;\n return null;\n}\n\n/**\n * The built web SPA directory. Tries, in order: the bundled layout (`<pkgDir>/web`, a sibling of the\n * CLI's `dist/`), the dev sibling (`packages/server/dist` → `packages/web/dist`), then the repo path.\n * `fromDir` is the directory of the calling module (`dirname(fileURLToPath(import.meta.url))`).\n */\nexport function resolveWebDist(fromDir: string, repoRoot: string | null = findRepoRoot(fromDir)): string {\n const installed = resolve(fromDir, \"../web\"); // bundled CLI: dist/ and web/ are siblings\n if (existsSync(installed)) return installed;\n const devSibling = resolve(fromDir, \"../../web/dist\"); // packages/server/dist → packages/web/dist\n if (existsSync(devSibling)) return devSibling;\n if (repoRoot) return join(repoRoot, \"packages/web/dist\");\n return installed; // best-effort default; caller guards with existsSync\n}\n","/**\n * Runtime configuration resolver — server settings (port/host) and the store path (db) layered over\n * the SAME agent-lens.config.json that carries `sources`/`exclude`. Kept here (not in sources.ts) so\n * the file is parsed in one place and both the source resolver and the runtime knobs share it.\n *\n * Precedence, highest wins: CLI flag (override arg) > env var > config file > built-in default.\n *\n * Deliberately NOT resolved here:\n * - The non-loopback bind guard (`AGENT_LENS_ALLOW_NONLOCAL`) — a privacy guardrail (ADR-005) that\n * stays an explicit env-only override enforced by the server, never a convenient flag/config key.\n * - The data dir (`AGENT_LENS_DATA`) — it is where the config file is *found* (see resolveConfigFile),\n * so reading it back out of that file would be circular. Env-only by construction.\n */\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { expandPath, findRepoRoot, resolveConfigFile, resolveDataDir } from \"./paths.js\";\nimport { mergePricing, PRICE_TABLE, usePricing, type Rate } from \"./pricing.js\";\nimport { UserError } from \"./user-error.js\";\n\n/** Shape of agent-lens.config.json (only the fields this module and sources.ts read). */\nexport interface AgentLensConfigFile {\n sources?: unknown[];\n exclude?: unknown[];\n /** SQLite store path. Top-level, not under `server`: ingest and export read it too. */\n db?: string;\n server?: { port?: number; host?: string };\n /** Model-price overrides, keyed by model-id prefix (ADR-028). Validated by mergePricing, not here. */\n pricing?: unknown;\n}\n\nexport const DEFAULT_PORT = 4477;\nexport const DEFAULT_HOST = \"127.0.0.1\"; // loopback only\nexport const DEFAULT_DB_NAME = \"agent-lens.db\";\n\n/** `.versions/` retention window. NOTE: `scripts/prune.sh` carries its own copy (bash can't import\n * this) — change both together. The TS side reads it here so the CLI and the server cannot drift. */\nexport const DEFAULT_VERSIONS_KEEP_DAYS = 90;\n\n/** The retention window in force. Narrower than ConfigOrigin on purpose: retention has no flag or\n * config-file layer, so \"env\" and \"default\" are the only outcomes this can ever produce. */\nexport function resolveRetention(): { keepDays: number; origin: \"env\" | \"default\" } {\n const raw = process.env.AGENT_LENS_VERSIONS_KEEP_DAYS;\n const parsed = raw ? Number(raw) : NaN;\n return Number.isFinite(parsed) && parsed >= 0\n ? { keepDays: parsed, origin: \"env\" }\n : { keepDays: DEFAULT_VERSIONS_KEEP_DAYS, origin: \"default\" };\n}\n\nexport interface ResolvedPricing {\n /** Built-in PRICE_TABLE with any valid config overrides applied. */\n table: Record<string, Rate>;\n /** \"file\" once any override is in force, else \"default\". No flag or env layer exists (see below). */\n origin: Extract<ConfigOrigin, \"file\" | \"default\">;\n /** Override keys applied, and those rejected as malformed — both surfaced by the diagnostics. */\n applied: string[];\n invalid: string[];\n}\n\n/**\n * Resolve the effective model-price table: built-in defaults overlaid with the config file's\n * `pricing` block (ADR-028). Same shape as the other resolvers here — pure over an injected config,\n * tagged with where the value came from.\n *\n * Narrower than ConfigOrigin on purpose, like resolveRetention: prices are a table, not a scalar, so\n * there is no sensible flag or env layer — \"file\" and \"default\" are the only reachable origins.\n *\n * Malformed entries are reported in `invalid` and dropped, leaving the built-in rate in place. That\n * is deliberately unlike validatePort, which throws: cost is a labelled estimate, so a typo in an\n * optional block should degrade the estimate, not stop the tool from starting.\n */\nexport function resolvePricing(cfg: AgentLensConfigFile | null = readConfigFile()): ResolvedPricing {\n const { table, invalid, applied } = mergePricing(PRICE_TABLE, cfg?.pricing);\n return { table, origin: applied.length ? \"file\" : \"default\", applied, invalid };\n}\n\n/**\n * Resolve the price table, install it process-wide, and warn about anything rejected. Called once at\n * startup by each entry point that derives cost (ingest, server) — the returned facts are what the\n * diagnostics report, so they describe the table actually in force rather than a fresh resolution.\n */\nexport function installPricing(cfg?: AgentLensConfigFile | null): ResolvedPricing {\n const resolved = cfg === undefined ? resolvePricing() : resolvePricing(cfg);\n usePricing(resolved.table);\n if (resolved.invalid.length) {\n console.error(\n `agent-lens: ignoring ${resolved.invalid.length} malformed pricing override(s): ` +\n `${resolved.invalid.join(\", \")} (each entry needs numeric input + output; built-in rates kept)`,\n );\n }\n return resolved;\n}\n\n/**\n * Read + parse the resolved config file once, or null when none exists. Shared by `sources.ts`\n * (sources/exclude) and this module (server settings) so the JSON is read a single time per call.\n */\nexport function readConfigFile(): AgentLensConfigFile | null {\n const repoRoot = findRepoRoot();\n const dataDir = resolveDataDir(repoRoot);\n const file = resolveConfigFile(repoRoot, dataDir);\n return file ? (JSON.parse(readFileSync(file, \"utf8\")) as AgentLensConfigFile) : null;\n}\n\n/** Where a resolved value came from — surfaced by `agent-lens config` for support/discoverability. */\nexport type ConfigOrigin = \"flag\" | \"env\" | \"file\" | \"default\";\n\nexport interface ResolvedDb {\n path: string;\n origin: ConfigOrigin;\n}\n\n/**\n * Resolve the SQLite store path with the same precedence as the server settings:\n * `--db` flag > AGENT_LENS_DB > config `db` > `<dataDir>/agent-lens.db`.\n *\n * The flag and env layers keep the pre-existing semantics EXACTLY — the raw string, any non-empty\n * value wins — so upgrading can never repoint the store of an install that already sets them. In\n * particular a literal `~`/`$HOME` in AGENT_LENS_DB keeps resolving the way it always did (as a\n * directory of that name); expanding it here would silently move such an install to a different,\n * empty database. Only the `db` config key — which no existing install can have — gets expandPath,\n * matching how `sources[].configDir` has always been treated.\n *\n * The triage store is derived from this path's directory by the server (ADR-018), so pointing `db`\n * elsewhere moves the pair together.\n */\nexport function resolveDbPath(override?: string, cfg: AgentLensConfigFile | null = readConfigFile()): ResolvedDb {\n if (override) return { path: override, origin: \"flag\" };\n if (process.env.AGENT_LENS_DB) return { path: process.env.AGENT_LENS_DB, origin: \"env\" };\n if (isSet(cfg?.db)) return { path: expandPath(String(cfg!.db).trim()), origin: \"file\" };\n return { path: join(resolveDataDir(findRepoRoot()), DEFAULT_DB_NAME), origin: \"default\" };\n}\n\nexport interface ServerOverrides {\n /** From a CLI flag (`serve --port/--host`); highest precedence. Empty/undefined means \"not set\". */\n port?: number | string;\n host?: string;\n}\n\nexport interface ResolvedServer {\n port: number;\n host: string;\n portOrigin: ConfigOrigin;\n hostOrigin: ConfigOrigin;\n}\n\n/** Validate a port from any source into an integer 1-65535, or throw a clear, source-tagged error. */\nexport function validatePort(value: number | string, source: string): number {\n const n = typeof value === \"number\" ? value : Number(String(value).trim());\n if (!Number.isInteger(n) || n < 1 || n > 65535) {\n // No \"agent-lens:\" prefix — the CLI's error boundary adds it, and both would read twice.\n throw new UserError(`invalid port from ${source}: '${value}' (expected an integer 1-65535)`);\n }\n return n;\n}\n\nfunction isSet(v: unknown): boolean {\n return v !== undefined && v !== null && String(v).trim() !== \"\";\n}\n\n/**\n * Resolve the effective server port + host with precedence flag > env > file > default. Validates the\n * port (range) and host (non-empty) at every layer so a bad value fails fast at startup rather than\n * silently coercing to NaN. Does NOT enforce the loopback guard — the server still does that.\n */\nexport function resolveServerConfig(\n overrides: ServerOverrides = {},\n cfg: AgentLensConfigFile | null = readConfigFile(),\n): ResolvedServer {\n let port: number;\n let portOrigin: ConfigOrigin;\n if (isSet(overrides.port)) {\n port = validatePort(overrides.port as number | string, \"--port\");\n portOrigin = \"flag\";\n } else if (isSet(process.env.AGENT_LENS_PORT)) {\n port = validatePort(process.env.AGENT_LENS_PORT!, \"AGENT_LENS_PORT\");\n portOrigin = \"env\";\n } else if (isSet(cfg?.server?.port)) {\n port = validatePort(cfg!.server!.port!, \"server.port (config file)\");\n portOrigin = \"file\";\n } else {\n port = DEFAULT_PORT;\n portOrigin = \"default\";\n }\n\n let host: string;\n let hostOrigin: ConfigOrigin;\n if (isSet(overrides.host)) {\n host = String(overrides.host).trim();\n hostOrigin = \"flag\";\n } else if (isSet(process.env.AGENT_LENS_HOST)) {\n host = process.env.AGENT_LENS_HOST!.trim();\n hostOrigin = \"env\";\n } else if (isSet(cfg?.server?.host)) {\n host = String(cfg!.server!.host).trim();\n hostOrigin = \"file\";\n } else {\n host = DEFAULT_HOST;\n hostOrigin = \"default\";\n }\n\n return { port, host, portOrigin, hostOrigin };\n}\n","/**\n * Resolve the running build's version (ADR-027).\n *\n * There is no version recorded in the repository: `@semantic-release/git` was dropped in PR #6\n * because its `git push HEAD:main` is rejected by the `protect-main` ruleset, so the committed\n * manifests are permanent placeholders (`0.0.0`, `0.0.0-development`). The real version lives in a\n * different place depending on how the app was installed, so resolution is a chain that reports\n * which link answered — the same \"say where the value came from\" idiom `agent-lens config` uses for\n * paths (see ConfigOrigin in config.ts).\n *\n * 1. package.json, when it is NOT a 0.0.0* placeholder → \"npm\" (published tarball:\n * @semantic-release/npm stamps the real version before packing)\n * 2. git describe --tags --always --dirty → \"git\" (a clone: a normal clone\n * fetches tags, and the extra detail — v0.9.6-3-gabc1234 — is what you want from a dev build)\n * 3. neither available → \"unknown\"\n *\n * \"unknown\" is reachable only from undocumented install paths (a shallow clone with no tags, or a\n * GitHub source zip, which has neither npm metadata nor .git). That is deliberate: reporting\n * \"unknown\" beats reporting the `0.0.0` placeholder as if it were real.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join, parse } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type VersionSource = \"npm\" | \"git\" | \"unknown\";\n\nexport interface ResolvedVersion {\n /** \"0.9.6\" (npm) · \"v0.9.6-3-gabc1234-dirty\" (git) · \"unknown\". Not necessarily semver — see source. */\n version: string;\n source: VersionSource;\n}\n\nconst moduleDir = dirname(fileURLToPath(import.meta.url));\n\n/** A committed manifest never carries a real version, only the release placeholders. */\nfunction isPlaceholder(v: string): boolean {\n return v.startsWith(\"0.0.0\");\n}\n\n/** Nearest package.json walking up from `dir`, or null. */\nfunction nearestPackageVersion(dir: string): string | null {\n const { root } = parse(dir);\n for (let d = dir; ; d = dirname(d)) {\n try {\n const pkg = JSON.parse(readFileSync(join(d, \"package.json\"), \"utf8\")) as { version?: string };\n if (typeof pkg.version === \"string\" && pkg.version) return pkg.version;\n } catch {\n /* no package.json here (or unreadable) — keep walking */\n }\n if (d === root) return null;\n }\n}\n\n/**\n * `git describe` in `cwd`. Returns null when git is missing, this is not a work tree, or no tag is\n * reachable — all of which are normal, not errors. `--always` still yields a bare SHA when the only\n * problem is a missing tag (shallow clone), which is more useful than nothing.\n */\nfunction gitDescribe(cwd: string): string | null {\n try {\n const r = spawnSync(\"git\", [\"describe\", \"--tags\", \"--always\", \"--dirty\"], {\n cwd,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n });\n if (r.status !== 0) return null;\n const out = r.stdout?.trim();\n return out ? out : null;\n } catch {\n return null; // git binary absent\n }\n}\n\n/** Uncached resolution — exported for tests, which need to drive each branch independently. */\nexport function computeVersion(fromDir: string = moduleDir): ResolvedVersion {\n const pkg = nearestPackageVersion(fromDir);\n if (pkg && !isPlaceholder(pkg)) return { version: pkg, source: \"npm\" };\n\n const described = gitDescribe(fromDir);\n if (described) return { version: described, source: \"git\" };\n\n return { version: \"unknown\", source: \"unknown\" };\n}\n\nlet cached: ResolvedVersion | null = null;\n\n/**\n * The running build's version, resolved once per process. Memoized because step 2 spawns `git`, and\n * this is read by /api/health, which the SPA polls — it must never spawn per request.\n */\nexport function resolveVersion(): ResolvedVersion {\n if (!cached) cached = computeVersion();\n return cached;\n}\n\n/** Test seam: drop the memo so a test can drive resolution against a different tree. */\nexport function resetVersionCache(): void {\n cached = null;\n}\n","/**\n * Canonical source resolver for Agent Lens (shared by the collector, ingester, server, CLI, and\n * the dev-only scripts/sources.mjs shim).\n *\n * A \"source\" is a labeled agent instance: { label, agent, configDir }. Multiple local accounts\n * (each with its own config folder) are each a source — e.g. \"personal\" → ~/.claude.\n *\n * Resolution order (see resolveConfigFile in paths.ts):\n * 1. $CLAUDE_DIR set → single legacy source { label: $AGENT_LENS_LABEL|\"default\", ~/.claude }\n * 2. $AGENT_LENS_CONFIG → that JSON file\n * 3. <dataDir>/agent-lens.config.json (installed user's config, next to their data)\n * 4. <repo>/agent-lens.config.json → <repo>/agent-lens.config.example.json\n * 5. built-in default → { label: \"default\", agent: \"claude-code\", configDir: \"$HOME/.claude\" }\n *\n * Excluded projects (playgrounds, personal, dummy tests, …) are resolved here too, from the config's\n * top-level `exclude: [<realProjectPath>, …]` plus the `AGENT_LENS_EXCLUDE` env (comma-separated).\n * They are honored at every stage — collect (not mirrored), ingest (not stored / pruned), corpus.\n */\nimport { expandPath } from \"./paths.js\";\nimport { readConfigFile } from \"./config.js\";\nimport { UserError } from \"./user-error.js\";\n\nexport interface Source {\n label: string;\n agent: string;\n configDir: string;\n}\n\nexport function loadSources(): Source[] {\n // 1. legacy single-source override (used by tests and ad-hoc runs)\n if (process.env.CLAUDE_DIR) {\n return [\n {\n label: process.env.AGENT_LENS_LABEL || \"default\",\n agent: \"claude-code\",\n configDir: expandPath(process.env.CLAUDE_DIR),\n },\n ];\n }\n\n const cfg = readConfigFile() ?? { sources: [{ label: \"default\", agent: \"claude-code\", configDir: \"$HOME/.claude\" }] };\n\n const seen = new Set<string>();\n const out: Source[] = [];\n for (const raw of cfg.sources || []) {\n const s = raw as Partial<Source> | null;\n if (!s || !s.label || !s.configDir) continue;\n if (seen.has(s.label)) throw new UserError(`duplicate source label: ${s.label}`);\n seen.add(s.label);\n out.push({ label: s.label, agent: s.agent || \"claude-code\", configDir: expandPath(s.configDir) });\n }\n if (!out.length) throw new UserError(\"no valid sources configured\");\n return out;\n}\n\n/**\n * Resolve the list of projects to EXCLUDE everywhere (collect, ingest, corpus). Union of the config's\n * `exclude` array and the `AGENT_LENS_EXCLUDE` env (comma-separated). Returns expanded real paths\n * (no trailing slash). The legacy CLAUDE_DIR mode still honors the env var.\n */\nexport function loadExcludes(): string[] {\n const fromEnv = (process.env.AGENT_LENS_EXCLUDE || \"\").split(\",\");\n let fromCfg: unknown[] = [];\n if (!process.env.CLAUDE_DIR) {\n const cfg = readConfigFile();\n if (cfg && Array.isArray(cfg.exclude)) fromCfg = cfg.exclude;\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const p of [...fromCfg, ...fromEnv]) {\n const e = expandPath(String(p ?? \"\").trim());\n if (e && !seen.has(e)) {\n seen.add(e);\n out.push(e);\n }\n }\n return out;\n}\n","/**\n * Encode a real project path the way Claude Code names its `projects/<encodedDir>` folder: every\n * `/` and `.` becomes `-` (e.g. `/home/u/git/agent-lens` → `-home-u-git-agent-lens`). Trailing\n * separators are dropped so a path with or without a trailing slash encodes identically.\n *\n * Canonical here in core so collect (Stage 1) and ingest/redact (Stage 2 / corpus) agree exactly.\n */\nexport function encodeProjectPath(p: string): string {\n return p.replace(/[/.]/g, \"-\").replace(/-+$/, \"\");\n}\n","/**\n * Agent Lens — Stage 1 collection, in portable Node (ADR-001, ADR-002, ADR-005).\n *\n * Passively copies agent session traces out of each configured source into the local archive before\n * the agent prunes them. Never deletes, never sends data off the machine, never copies secrets. This\n * replaced the former bash+rsync collector with the same semantics, minus the Linux-only dependencies.\n *\n * Per file (mirror of rsync --append-verify + the .versions pre-pass):\n * - no archive copy yet → copy whole\n * - unchanged since last collect (size + mtime) → skip\n * - source ≥ archive AND archive is a byte-prefix→ append the new tail (pure append)\n * - source shorter than archive (compaction) → snapshot the SOURCE to .versions/<ts>/, keep the\n * longer archive (no shrink → retains history)\n * - same-or-longer but prefix differs (divergence) → snapshot the OLD archive, overwrite with source\n * Stage 2 ingests the mirror AND .versions/, deduping events by uuid → maximal history.\n *\n * Secrets (.credentials.json, *.lock) are never copied; a post-pass also removes any stray\n * .credentials.json from the archive. Excluded projects (loadExcludes) are never mirrored.\n */\nimport {\n appendFileSync,\n closeSync,\n copyFileSync,\n existsSync,\n mkdirSync,\n openSync,\n readSync,\n readdirSync,\n statSync,\n unlinkSync,\n utimesSync,\n writeSync,\n chmodSync,\n type Stats,\n} from \"node:fs\";\nimport { basename, dirname, join, relative, sep } from \"node:path\";\nimport { platform } from \"node:os\";\nimport { loadExcludes, loadSources, type Source } from \"./sources.js\";\nimport { resolveArchiveDir } from \"./paths.js\";\nimport { encodeProjectPath } from \"./projects.js\";\nimport { UserError } from \"./user-error.js\";\n\nconst POSIX = platform() !== \"win32\";\nconst DIR_MODE = 0o700; // archive is as sensitive as the originals (bash used umask 077)\nconst FILE_MODE = 0o600;\nconst CHUNK = 1 << 16;\n\nexport interface CollectOptions {\n /** Archive base dir. Default: the fixed `<dataDir>/archive` (ADR-021). Tests pass an explicit one. */\n archiveBase?: string;\n /** Sources to collect. Default: loadSources(). */\n sources?: Source[];\n /** Real project paths to exclude. Default: loadExcludes(). */\n excludes?: string[];\n /** Progress sink. Default: console.log. */\n log?: (msg: string) => void;\n}\n\nexport interface CollectStats {\n sources: number;\n scanned: number;\n copied: number;\n appended: number;\n diverged: number;\n compacted: number;\n snapshots: number;\n}\n\n/** `YYYYMMDDTHHMMSSmmm` in local time (matches `date +%Y%m%dT%H%M%S%3N`). */\nfunction collectTimestamp(d: Date = new Date()): string {\n const p = (n: number, w = 2) => String(n).padStart(w, \"0\");\n return (\n `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}T` +\n `${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`\n );\n}\n\nfunction ensureDir(dir: string): void {\n mkdirSync(dir, { recursive: true, mode: DIR_MODE });\n}\n\n/** Set archive file perms + mtime to mirror the source (like rsync -a / cp -p). */\nfunction syncMeta(dst: string, src: Stats): void {\n if (POSIX) {\n try {\n chmodSync(dst, FILE_MODE);\n } catch {\n /* best effort */\n }\n }\n try {\n utimesSync(dst, src.atime, src.mtime);\n } catch {\n /* best effort */\n }\n}\n\nfunction copyWhole(src: string, dst: string, srcStat: Stats): void {\n ensureDir(dirname(dst));\n copyFileSync(src, dst);\n syncMeta(dst, srcStat);\n}\n\n/** True if the first `len` bytes of `src` byte-equal the whole of `arc` (`arc` is `len` bytes). */\nfunction prefixEquals(src: string, arc: string, len: number): boolean {\n const sf = openSync(src, \"r\");\n const af = openSync(arc, \"r\");\n try {\n const sb = Buffer.allocUnsafe(CHUNK);\n const ab = Buffer.allocUnsafe(CHUNK);\n let remaining = len;\n while (remaining > 0) {\n const want = Math.min(CHUNK, remaining);\n const sr = readSync(sf, sb, 0, want, null);\n const ar = readSync(af, ab, 0, want, null);\n if (sr !== ar || sr === 0) return false;\n if (Buffer.compare(sb.subarray(0, sr), ab.subarray(0, sr)) !== 0) return false;\n remaining -= sr;\n }\n return remaining === 0;\n } finally {\n closeSync(sf);\n closeSync(af);\n }\n}\n\n/** Append `src[from..to)` to the end of `arc`. */\nfunction appendRange(src: string, arc: string, from: number, to: number): void {\n const sf = openSync(src, \"r\");\n const af = openSync(arc, \"a\");\n try {\n const b = Buffer.allocUnsafe(CHUNK);\n let pos = from;\n while (pos < to) {\n const want = Math.min(CHUNK, to - pos);\n const r = readSync(sf, b, 0, want, pos);\n if (r === 0) break;\n writeSync(af, b, 0, r);\n pos += r;\n }\n } finally {\n closeSync(sf);\n closeSync(af);\n }\n}\n\nfunction filesEqual(a: string, b: string): boolean {\n const sa = statSync(a);\n const sb = statSync(b);\n if (sa.size !== sb.size) return false;\n return prefixEquals(a, b, sa.size);\n}\n\n/** Most recent `.versions/<ts>/<rel>` snapshot for `rel`, or null. Timestamps sort chronologically. */\nfunction latestSnapshot(archive: string, relParts: string[]): string | null {\n const versRoot = join(archive, \".versions\");\n if (!existsSync(versRoot)) return null;\n const stamps = readdirSync(versRoot).sort();\n for (let i = stamps.length - 1; i >= 0; i--) {\n const p = join(versRoot, stamps[i]!, ...relParts);\n if (existsSync(p)) return p;\n }\n return null;\n}\n\nfunction snapshot(absPath: string, relParts: string[], versionsDir: string, stats: CollectStats): void {\n const dst = join(versionsDir, ...relParts);\n ensureDir(dirname(dst));\n copyFileSync(absPath, dst);\n syncMeta(dst, statSync(absPath));\n stats.snapshots++;\n}\n\n/**\n * Recursively collect the relevant transcript + sidecar files under `dir`:\n * - `*.jsonl` — session/subagent transcripts and journals (the core traces)\n * - anything under `workflows/` — the Workflow-tool run result sidecars (wf_<id>.json) + scripts\n * - `*.meta.json` — per-subagent metadata (agentType, description, spawnDepth, toolUseId)\n * - anything under `tool-results/` — full un-truncated tool outputs spilled from the transcript\n * Operational/state files (memory, cache, todos, shell-snapshots, …) are intentionally left behind.\n * Secrets (.credentials.json, *.lock) are filtered by the caller. `inWorkflows`/`inToolResults` track\n * whether recursion is inside one of those directories so every file within is captured.\n */\nfunction walkCollectable(dir: string, out: string[], inWorkflows = false, inToolResults = false): void {\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, e.name);\n if (e.isDirectory()) {\n walkCollectable(p, out, inWorkflows || e.name === \"workflows\", inToolResults || e.name === \"tool-results\");\n } else if (\n e.isFile() &&\n (e.name.endsWith(\".jsonl\") || inWorkflows || inToolResults || e.name.endsWith(\".meta.json\"))\n ) {\n out.push(p);\n }\n }\n}\n\n/** The single-file sync decision (the heart of the append-verify port). `appendable` is true only for\n * append-only logs (*.jsonl); for rewritten files (JSON/txt sidecars) a same-prefix growth is a\n * rewrite, not an append, so it's handled as a divergence (snapshot old, overwrite) — never appended. */\nfunction syncFile(src: string, dst: string, relParts: string[], archive: string, versionsDir: string, stats: CollectStats, appendable: boolean): void {\n const srcStat = statSync(src);\n if (!existsSync(dst)) {\n copyWhole(src, dst, srcStat);\n stats.copied++;\n return;\n }\n const arcStat = statSync(dst);\n // Unchanged since last collect (we preserve source mtime on write, like rsync -a) → skip, no read.\n if (srcStat.size === arcStat.size && Math.trunc(srcStat.mtimeMs) === Math.trunc(arcStat.mtimeMs)) return;\n\n const ssize = srcStat.size;\n const asize = arcStat.size;\n if (ssize === asize && prefixEquals(src, dst, asize)) {\n // Identical content, mtime moved → just realign metadata (no append, appendable or not).\n syncMeta(dst, srcStat);\n } else if (appendable && ssize > asize && prefixEquals(src, dst, asize)) {\n // Pure append onto an append-only log. Verify the full prefix before appending; never trust size.\n appendRange(src, dst, asize, ssize);\n stats.appended++;\n syncMeta(dst, srcStat);\n } else if (ssize < asize) {\n // Compaction: keep the longer archive; snapshot the compacted source (skip if an identical\n // snapshot already exists, so a stuck-compacted file doesn't re-snapshot every run).\n const last = latestSnapshot(archive, relParts);\n if (!last || !filesEqual(last, src)) snapshot(src, relParts, versionsDir, stats);\n stats.compacted++;\n } else {\n // Divergence: capture the old archive, then overwrite with the source.\n snapshot(dst, relParts, versionsDir, stats);\n copyWhole(src, dst, srcStat);\n stats.diverged++;\n }\n}\n\nfunction removeCredentials(dir: string): void {\n if (!existsSync(dir)) return;\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n const p = join(dir, e.name);\n if (e.isDirectory()) removeCredentials(p);\n else if (e.name === \".credentials.json\") {\n try {\n unlinkSync(p);\n } catch {\n /* ignore */\n }\n }\n }\n}\n\nfunction collectSource(\n source: Source,\n archiveBase: string,\n excludedDirs: string[],\n runTs: string,\n stats: CollectStats,\n log: (m: string) => void,\n): void {\n const { label, configDir } = source;\n const archive = join(archiveBase, label);\n const versionsDir = join(archive, \".versions\", runTs);\n if (!existsSync(configDir)) {\n log(`agent-lens: [${label}] source not found: ${configDir} (skipping)`);\n return;\n }\n ensureDir(join(archive, \"projects\"));\n ensureDir(join(archive, \"settings\"));\n\n const files: string[] = [];\n const projectsDir = join(configDir, \"projects\");\n if (existsSync(projectsDir)) walkCollectable(projectsDir, files);\n const historySrc = join(configDir, \"history.jsonl\");\n if (existsSync(historySrc)) files.push(historySrc);\n\n const before = { scanned: stats.scanned, snapshots: stats.snapshots };\n for (const src of files) {\n const base = basename(src);\n if (base === \".credentials.json\" || base.endsWith(\".lock\")) continue; // never copy secrets\n const relParts = relative(configDir, src).split(sep);\n const relPosix = relParts.join(\"/\");\n if (excludedDirs.some((enc) => relPosix.startsWith(`projects/${enc}/`))) continue; // excluded project\n stats.scanned++;\n syncFile(src, join(archive, ...relParts), relParts, archive, versionsDir, stats, base.endsWith(\".jsonl\"));\n }\n\n // Settings: latest-wins, snapshot old on change.\n for (const f of [\"settings.json\", \"settings.local.json\"]) {\n const s = join(configDir, f);\n if (!existsSync(s)) continue;\n const a = join(archive, \"settings\", f);\n if (existsSync(a) && !filesEqual(s, a)) snapshot(a, [\"settings\", f], versionsDir, stats);\n ensureDir(dirname(a));\n copyFileSync(s, a);\n syncMeta(a, statSync(s));\n }\n\n removeCredentials(archive);\n\n const scanned = stats.scanned - before.scanned;\n const snapshots = stats.snapshots - before.snapshots;\n const line = `${new Date().toISOString()} run=${runTs} source=${label} scanned=${scanned} snapshots=${snapshots} archive=${archive}`;\n log(`agent-lens: ${line}`);\n try {\n appendFileSync(join(archiveBase, \".collect.log\"), line + \"\\n\");\n } catch {\n /* log is best-effort */\n }\n}\n\n/** Collect every configured source into the archive. Returns aggregate counters. */\nexport function collectAll(opts: CollectOptions = {}): CollectStats {\n const log = opts.log ?? ((m: string) => console.log(m));\n const archiveBase = opts.archiveBase ?? resolveArchiveDir();\n const sources = opts.sources ?? loadSources();\n const excludedDirs = (opts.excludes ?? loadExcludes()).map(encodeProjectPath);\n const runTs = collectTimestamp();\n\n ensureDir(archiveBase);\n const stats: CollectStats = { sources: 0, scanned: 0, copied: 0, appended: 0, diverged: 0, compacted: 0, snapshots: 0 };\n if (!sources.length) throw new UserError(\"no sources configured\");\n for (const source of sources) {\n stats.sources++;\n collectSource(source, archiveBase, excludedDirs, runTs, stats, log);\n }\n return stats;\n}\n","/**\n * Cross-platform single-instance lock. collect+ingest take this so a too-frequent schedule (or an\n * overlapping watch trigger) can never run two collectors against the same archive or two writers\n * against the same SQLite DB. systemd already serializes its oneshot; this makes launchd/schtasks/\n * watch safe too. Not a hard mutex (a local, single-user tool doesn't need one) — a stale-aware\n * PID file: a run whose holder is dead or older than `staleMs` is reclaimed.\n */\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { hostname } from \"node:os\";\n\nexport interface Lock {\n /** Remove the lock file. Idempotent; also runs automatically on process exit. */\n release(): void;\n}\n\nfunction isAlive(pid: number): boolean {\n if (!pid) return false;\n try {\n process.kill(pid, 0); // signal 0 = existence check, doesn't actually signal\n return true;\n } catch (e) {\n // ESRCH → no such process (dead); EPERM → exists but not ours (alive).\n return (e as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * Try to acquire `lockPath`. Returns a Lock on success, or null if another live, non-stale process\n * currently holds it (caller should log \"run in progress\" and exit 0).\n */\nexport function acquireLock(lockPath: string, staleMs = 6 * 60 * 60 * 1000): Lock | null {\n mkdirSync(dirname(lockPath), { recursive: true });\n if (existsSync(lockPath)) {\n try {\n const held = JSON.parse(readFileSync(lockPath, \"utf8\")) as { pid?: number; ts?: number };\n const fresh = typeof held.ts === \"number\" && Date.now() - held.ts < staleMs;\n if (fresh && isAlive(held.pid ?? 0)) return null; // held by a live, recent process\n // else: dead PID or too old → reclaim below\n } catch {\n // corrupt lock file → reclaim\n }\n }\n writeFileSync(lockPath, JSON.stringify({ pid: process.pid, ts: Date.now(), host: hostname() }), { mode: 0o600 });\n let released = false;\n const release = () => {\n if (released) return;\n released = true;\n try {\n // Only remove if it's still ours (avoid deleting a lock a reclaimer took over).\n const cur = JSON.parse(readFileSync(lockPath, \"utf8\")) as { pid?: number };\n if (cur.pid === process.pid) unlinkSync(lockPath);\n } catch {\n /* already gone / unreadable */\n }\n };\n process.once(\"exit\", release);\n return { release };\n}\n","/**\n * Cross-platform OS-service integration for `agent-lens service`. Two targets:\n * - collector → a *periodic* `collect --then-ingest` job (Node can time work via `watch`, but\n * can't survive a reboot alone, so we register it with the OS scheduler)\n * - server → the *long-running* web UI + read-only API daemon (`serve`)\n *\n * Per platform:\n * - Linux → systemd user units (collector: oneshot service + timer; server: Type=simple + Restart) + linger\n * - macOS → launchd LaunchAgents (collector: StartCalendarInterval; server: KeepAlive)\n * - Windows → Task Scheduler (collector: one DAILY task per hour; server: ONLOGON task)\n *\n * The absolute `node` binary (process.execPath) and CLI entry path are baked into every unit/plist/\n * task, because schedulers don't inherit the interactive shell's PATH (the mise problem, generalized).\n * The same reasoning covers the environment — see serverEnv/pathEnv below: a scheduler inherits none\n * of the installing shell's env either, so anything the user explicitly set has to be written in.\n * The unit/plist/command text is generated by the pure functions below (unit-tested); runService\n * wraps them with the OS commands.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir, platform, userInfo } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { findRepoRoot, resolveDataDir } from \"./paths.js\";\nimport { resolveServerConfig } from \"./config.js\";\nimport { UserError } from \"./user-error.js\";\n\nexport const DEFAULT_HOURS = [9, 13, 17, 21];\n\n/**\n * The login name for `loginctl enable-linger`. Asks the OS (passwd entry) rather than reading $USER:\n * schedulers and `env -i` don't set it, which is exactly when this runs, and under sudo it names the\n * invoking user while the units belong to the effective one. Falls back to the env only if the\n * passwd lookup fails (rare — a uid with no entry, seen in some containers); \"\" means \"unknown\", and\n * callers skip linger rather than shelling out with an empty argument.\n */\nexport function currentUser(): string {\n try {\n return userInfo().username;\n } catch {\n return process.env.USER || process.env.LOGNAME || \"\";\n }\n}\n\nconst SYSTEMD_COLLECTOR_TIMER = \"agent-lens-collect.timer\";\nconst SYSTEMD_COLLECTOR_SERVICE = \"agent-lens-collect.service\";\nconst SYSTEMD_SERVER_SERVICE = \"agent-lens-server.service\";\nconst LAUNCHD_COLLECTOR_LABEL = \"org.agent-lens.collect\";\nconst LAUNCHD_SERVER_LABEL = \"org.agent-lens.server\";\nconst WINDOWS_COLLECTOR_PREFIX = \"AgentLens\\\\Collect\";\nconst WINDOWS_SERVER_TASK = \"AgentLens\\\\Server\";\n\nexport type ServiceTarget = \"collector\" | \"server\";\nexport type ServiceAction = \"install\" | \"uninstall\" | \"status\";\n\nexport interface ServiceParams {\n /** Absolute path to the CLI entry (the bundled agent-lens.js). */\n cliEntry: string;\n /** Which OS integrations to act on. */\n targets: ServiceTarget[];\n /** Hours (0–23) the collector runs at. Default: DEFAULT_HOURS. */\n hours?: number[];\n /** Absolute node binary. Default: process.execPath. */\n node?: string;\n}\n\n/** Parse the CLI target arg: undefined/\"all\" → both; \"collector\"/\"server\" → that one; else throw. */\nexport function parseTargets(spec: string | undefined): ServiceTarget[] {\n if (!spec || spec === \"all\") return [\"collector\", \"server\"];\n if (spec === \"collector\" || spec === \"server\") return [spec];\n throw new UserError(`invalid target '${spec}' (expected: collector | server | all)`);\n}\n\nexport function parseHours(spec: string | undefined): number[] {\n if (!spec) return DEFAULT_HOURS;\n const hours = spec\n .split(\",\")\n .map((s) => Number.parseInt(s.trim(), 10))\n .filter((n) => Number.isInteger(n) && n >= 0 && n <= 23);\n if (!hours.length) throw new UserError(`invalid hours '${spec}' (expected comma-separated 0-23, e.g. 9,13,17,21)`);\n return [...new Set(hours)].sort((a, b) => a - b);\n}\n\n/**\n * Effective port/host to bake into the server unit. Resolves via the shared config precedence\n * (env > config file > default) and bakes any value the user explicitly set — from the shell env OR\n * `server.{port,host}` in agent-lens.config.json — so the scheduled server matches what they run\n * interactively (schedulers don't inherit the shell env, hence the baking). Defaults stay unbaked so\n * a future default change still applies to already-installed units.\n */\nfunction serverEnv(): Record<string, string> {\n const env: Record<string, string> = {};\n const { port, host, portOrigin, hostOrigin } = resolveServerConfig();\n if (portOrigin !== \"default\") env.AGENT_LENS_PORT = String(port);\n if (hostOrigin !== \"default\") env.AGENT_LENS_HOST = host;\n return env;\n}\n\n/**\n * The path env, baked into *every* unit rather than just the server's — the collector writes the\n * archive, so pointing it at the wrong data dir is the more damaging half of getting this wrong.\n *\n * Unlike port/host these have no config-file equivalent to fall back on: the data dir is where the\n * config file is looked up, so it can only ever come from the environment (ADR-021). A scheduler\n * inherits none of the installing shell's environment, so without baking, `AGENT_LENS_DATA=… service\n * install` yields services that silently read a different store than every interactive command.\n *\n * Absolute, because the unit's working directory is not the one the user installed from.\n */\nfunction pathEnv(): Record<string, string> {\n const env: Record<string, string> = {};\n // resolveDataDir() rather than the raw variable: bake the path the CLI itself derives from it.\n if (process.env.AGENT_LENS_DATA) env.AGENT_LENS_DATA = resolveDataDir();\n if (process.env.AGENT_LENS_CONFIG) env.AGENT_LENS_CONFIG = resolve(process.env.AGENT_LENS_CONFIG);\n return env;\n}\n\n/** Report what got baked — the failure mode here is silence, so make the values visible at install. */\nfunction reportBakedEnv(env: Record<string, string>): void {\n for (const [k, v] of Object.entries(env)) console.log(`agent-lens service: ${k}=${v}`);\n}\n\n// ---- pure generators (unit-tested) --------------------------------------------------------------\n\n/** systemd `OnCalendar` hour list, e.g. [9,13] → \"09,13:00\". */\nexport function onCalendarHours(hours: number[]): string {\n return hours.map((h) => String(h).padStart(2, \"0\")).join(\",\") + \":00\";\n}\n\n/** `Environment=` lines for a systemd unit, one per variable (empty string when there are none). */\nfunction systemdEnvLines(env: Record<string, string>): string {\n return Object.entries(env)\n .map(([k, v]) => `Environment=${k}=${v}\\n`)\n .join(\"\");\n}\n\n/** Collector: oneshot unit that runs one `collect --then-ingest` pass (driven by the timer). */\nexport function systemdCollectorService(node: string, cli: string, env: Record<string, string> = {}): string {\n return `[Unit]\nDescription=Agent Lens — collect + ingest Claude Code session traces\n\n[Service]\nType=oneshot\n# Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.\nExecStart=${node} ${cli} collect --then-ingest\n${systemdEnvLines(env)}Nice=10\nIOSchedulingClass=idle\n`;\n}\n\nexport function systemdCollectorTimer(hours: number[]): string {\n return `[Unit]\nDescription=Agent Lens — periodic Claude Code trace collection\n\n[Timer]\n# A few times a day. Persistent catches runs missed while the machine was off.\nOnCalendar=*-*-* ${onCalendarHours(hours)}\nPersistent=true\nRandomizedDelaySec=300\n\n[Install]\nWantedBy=timers.target\n`;\n}\n\n/** Server: long-running daemon that runs `serve`, restarted on failure. */\nexport function systemdServerService(node: string, cli: string, env: Record<string, string> = {}): string {\n const envLines = systemdEnvLines(env);\n return `[Unit]\nDescription=Agent Lens — local web UI + read-only API server\nAfter=network.target\n\n[Service]\nType=simple\n# Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.\nExecStart=${node} ${cli} serve\n${envLines}# Long-running: always come back, including after a bare SIGTERM. systemd counts SIGTERM as a\n# clean exit, so Restart=on-failure would leave the server dead after a stray \"pkill -f ... serve\"\n# aimed at an ad-hoc test server — the unit's ExecStart matches that pattern too. Matches the\n# launchd KeepAlive=true behaviour. An explicit \"systemctl --user stop\" still stops it for good.\nRestart=always\nRestartSec=5\nNice=10\n\n[Install]\nWantedBy=default.target\n`;\n}\n\n/** An `EnvironmentVariables` dict for a launchd plist (empty string when there are none). */\nfunction launchdEnvDict(env: Record<string, string>): string {\n if (!Object.keys(env).length) return \"\";\n const entries = Object.entries(env)\n .map(([k, v]) => ` <key>${k}</key><string>${v}</string>`)\n .join(\"\\n\");\n return ` <key>EnvironmentVariables</key>\\n <dict>\\n${entries}\\n </dict>\\n`;\n}\n\nexport function launchdCollectorPlist(\n node: string,\n cli: string,\n hours: number[],\n logPath: string,\n env: Record<string, string> = {},\n): string {\n const cal = hours\n .map((h) => ` <dict><key>Hour</key><integer>${h}</integer><key>Minute</key><integer>0</integer></dict>`)\n .join(\"\\n\");\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key><string>${LAUNCHD_COLLECTOR_LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${node}</string>\n <string>${cli}</string>\n <string>collect</string>\n <string>--then-ingest</string>\n </array>\n <key>StartCalendarInterval</key>\n <array>\n${cal}\n </array>\n${launchdEnvDict(env)} <key>StandardOutPath</key><string>${logPath}</string>\n <key>StandardErrorPath</key><string>${logPath}</string>\n</dict>\n</plist>\n`;\n}\n\n/** Server: launchd agent kept alive (relaunched if it exits) and started at load. */\nexport function launchdServerPlist(node: string, cli: string, logPath: string, env: Record<string, string> = {}): string {\n const envDict = launchdEnvDict(env);\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key><string>${LAUNCHD_SERVER_LABEL}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${node}</string>\n <string>${cli}</string>\n <string>serve</string>\n </array>\n${envDict} <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><true/>\n <key>StandardOutPath</key><string>${logPath}</string>\n <key>StandardErrorPath</key><string>${logPath}</string>\n</dict>\n</plist>\n`;\n}\n\n// ---- OS helpers ---------------------------------------------------------------------------------\n\nfunction sh(cmd: string, args: string[], quiet = false): boolean {\n const r = spawnSync(cmd, args, { stdio: quiet ? \"ignore\" : \"inherit\", encoding: \"utf8\" });\n return r.status === 0;\n}\n\nfunction have(cmd: string): boolean {\n const r = spawnSync(cmd, [\"--version\"], { stdio: \"ignore\" });\n return r.status === 0 || r.error === undefined;\n}\n\nfunction systemdUnitDir(): string {\n return join(process.env.XDG_CONFIG_HOME || join(homedir(), \".config\"), \"systemd\", \"user\");\n}\n\n// ---- Linux (systemd user) -----------------------------------------------------------------------\n\nfunction linuxInstall(targets: ServiceTarget[], node: string, cli: string, hours: number[]): void {\n if (!have(\"systemctl\")) {\n console.error(\"agent-lens service: systemctl not found — is this a systemd system?\");\n process.exitCode = 1;\n return;\n }\n const dir = systemdUnitDir();\n mkdirSync(dir, { recursive: true });\n const paths = pathEnv();\n if (targets.includes(\"collector\")) {\n writeFileSync(join(dir, SYSTEMD_COLLECTOR_SERVICE), systemdCollectorService(node, cli, paths));\n writeFileSync(join(dir, SYSTEMD_COLLECTOR_TIMER), systemdCollectorTimer(hours));\n }\n if (targets.includes(\"server\")) {\n writeFileSync(join(dir, SYSTEMD_SERVER_SERVICE), systemdServerService(node, cli, { ...paths, ...serverEnv() }));\n }\n sh(\"systemctl\", [\"--user\", \"daemon-reload\"]);\n if (targets.includes(\"collector\")) {\n sh(\"systemctl\", [\"--user\", \"enable\", \"--now\", SYSTEMD_COLLECTOR_TIMER]);\n console.log(`agent-lens service: installed collector timer (${onCalendarHours(hours)})`);\n reportBakedEnv(paths);\n sh(\"systemctl\", [\"--user\", \"list-timers\", SYSTEMD_COLLECTOR_TIMER, \"--no-pager\"]);\n }\n if (targets.includes(\"server\")) {\n sh(\"systemctl\", [\"--user\", \"enable\", \"--now\", SYSTEMD_SERVER_SERVICE]);\n console.log(\"agent-lens service: installed server daemon\");\n reportBakedEnv({ ...paths, ...serverEnv() });\n sh(\"systemctl\", [\"--user\", \"status\", SYSTEMD_SERVER_SERVICE, \"--no-pager\"]);\n }\n // Run while logged out (best-effort) — needed for both the timer and the daemon.\n const user = currentUser();\n if (user && have(\"loginctl\") && !sh(\"loginctl\", [\"enable-linger\", user], true)) {\n console.log(`agent-lens: could not enable linger automatically; run: sudo loginctl enable-linger ${user}`);\n }\n}\n\nfunction linuxUninstall(targets: ServiceTarget[]): void {\n if (!have(\"systemctl\")) return;\n const dir = systemdUnitDir();\n if (targets.includes(\"collector\")) {\n sh(\"systemctl\", [\"--user\", \"disable\", \"--now\", SYSTEMD_COLLECTOR_TIMER], true);\n for (const u of [SYSTEMD_COLLECTOR_TIMER, SYSTEMD_COLLECTOR_SERVICE]) rmSync(join(dir, u), { force: true });\n sh(\"systemctl\", [\"--user\", \"reset-failed\", SYSTEMD_COLLECTOR_SERVICE], true);\n }\n if (targets.includes(\"server\")) {\n sh(\"systemctl\", [\"--user\", \"disable\", \"--now\", SYSTEMD_SERVER_SERVICE], true);\n rmSync(join(dir, SYSTEMD_SERVER_SERVICE), { force: true });\n sh(\"systemctl\", [\"--user\", \"reset-failed\", SYSTEMD_SERVER_SERVICE], true);\n }\n sh(\"systemctl\", [\"--user\", \"daemon-reload\"]);\n console.log(\"agent-lens service: uninstalled (archive untouched)\");\n}\n\nfunction linuxStatus(targets: ServiceTarget[]): void {\n if (!have(\"systemctl\")) return;\n if (targets.includes(\"collector\")) sh(\"systemctl\", [\"--user\", \"list-timers\", SYSTEMD_COLLECTOR_TIMER, \"--no-pager\"]);\n if (targets.includes(\"server\")) sh(\"systemctl\", [\"--user\", \"status\", SYSTEMD_SERVER_SERVICE, \"--no-pager\"]);\n}\n\n// ---- macOS (launchd) ----------------------------------------------------------------------------\n\nfunction launchdPlistPath(label: string): string {\n return join(homedir(), \"Library\", \"LaunchAgents\", `${label}.plist`);\n}\n\nfunction macBootstrap(plist: string): void {\n const uid = typeof process.getuid === \"function\" ? process.getuid() : 0;\n // Prefer the modern bootstrap API; fall back to legacy load -w.\n if (!sh(\"launchctl\", [\"bootstrap\", `gui/${uid}`, plist], true)) sh(\"launchctl\", [\"load\", \"-w\", plist]);\n}\n\nfunction macBootout(label: string, plist: string): void {\n const uid = typeof process.getuid === \"function\" ? process.getuid() : 0;\n if (!sh(\"launchctl\", [\"bootout\", `gui/${uid}/${label}`], true)) sh(\"launchctl\", [\"unload\", \"-w\", plist], true);\n rmSync(plist, { force: true });\n}\n\nfunction macInstall(targets: ServiceTarget[], node: string, cli: string, hours: number[]): void {\n const agents = join(homedir(), \"Library\", \"LaunchAgents\");\n mkdirSync(agents, { recursive: true });\n const dataDir = resolveDataDir(findRepoRoot());\n const paths = pathEnv();\n if (targets.includes(\"collector\")) {\n const plist = launchdPlistPath(LAUNCHD_COLLECTOR_LABEL);\n writeFileSync(plist, launchdCollectorPlist(node, cli, hours, join(dataDir, \"schedule.log\"), paths));\n macBootstrap(plist);\n console.log(`agent-lens service: installed collector agent ${LAUNCHD_COLLECTOR_LABEL} (hours ${hours.join(\",\")})`);\n reportBakedEnv(paths);\n }\n if (targets.includes(\"server\")) {\n const plist = launchdPlistPath(LAUNCHD_SERVER_LABEL);\n const env = { ...paths, ...serverEnv() };\n writeFileSync(plist, launchdServerPlist(node, cli, join(dataDir, \"server.log\"), env));\n macBootstrap(plist);\n console.log(`agent-lens service: installed server agent ${LAUNCHD_SERVER_LABEL}`);\n reportBakedEnv(env);\n }\n}\n\nfunction macUninstall(targets: ServiceTarget[]): void {\n if (targets.includes(\"collector\")) macBootout(LAUNCHD_COLLECTOR_LABEL, launchdPlistPath(LAUNCHD_COLLECTOR_LABEL));\n if (targets.includes(\"server\")) macBootout(LAUNCHD_SERVER_LABEL, launchdPlistPath(LAUNCHD_SERVER_LABEL));\n console.log(\"agent-lens service: uninstalled (archive untouched)\");\n}\n\nfunction macStatus(targets: ServiceTarget[]): void {\n const uid = typeof process.getuid === \"function\" ? process.getuid() : 0;\n if (targets.includes(\"collector\")) sh(\"launchctl\", [\"print\", `gui/${uid}/${LAUNCHD_COLLECTOR_LABEL}`]);\n if (targets.includes(\"server\")) sh(\"launchctl\", [\"print\", `gui/${uid}/${LAUNCHD_SERVER_LABEL}`]);\n}\n\n// ---- Windows (schtasks) -------------------------------------------------------------------------\n\nfunction winCollectorTaskName(hour: number): string {\n return `${WINDOWS_COLLECTOR_PREFIX}_${String(hour).padStart(2, \"0\")}`;\n}\n\nfunction windowsInstall(targets: ServiceTarget[], node: string, cli: string, hours: number[]): void {\n // Task Scheduler has no environment field — /TR is a command line and nothing else — so the path\n // env can't ride along the way it does in a systemd unit or a launchd plist. Say so rather than\n // installing a task that quietly reads a different data dir than the shell that installed it.\n const unbakeable = Object.entries({ ...pathEnv(), ...serverEnv() });\n if (unbakeable.length) {\n console.log(\"agent-lens service: Task Scheduler cannot carry environment variables. Set these\");\n console.log(\"agent-lens service: for your user account so the task sees them, then reinstall:\");\n for (const [k, v] of unbakeable) console.log(`agent-lens service: setx ${k} \"${v}\"`);\n }\n if (targets.includes(\"collector\")) {\n const tr = `\"${node}\" \"${cli}\" collect --then-ingest`;\n for (const h of hours) {\n sh(\"schtasks\", [\"/Create\", \"/F\", \"/TN\", winCollectorTaskName(h), \"/TR\", tr, \"/SC\", \"DAILY\", \"/ST\", `${String(h).padStart(2, \"0\")}:00`]);\n }\n console.log(`agent-lens service: installed ${hours.length} collector task(s) (hours ${hours.join(\",\")})`);\n }\n if (targets.includes(\"server\")) {\n // ONLOGON runs in the user session, so the per-user data dir (%LOCALAPPDATA%) resolves.\n // Note: schtasks has no crash-restart (systemd/launchd do); a crashed server returns on next logon.\n const tr = `\"${node}\" \"${cli}\" serve`;\n sh(\"schtasks\", [\"/Create\", \"/F\", \"/TN\", WINDOWS_SERVER_TASK, \"/TR\", tr, \"/SC\", \"ONLOGON\", \"/RL\", \"LIMITED\"]);\n console.log(\"agent-lens service: installed server task (starts at logon)\");\n }\n}\n\nfunction windowsUninstall(targets: ServiceTarget[]): void {\n if (targets.includes(\"collector\")) {\n // Delete every task under our prefix (we don't need to know the original hours).\n for (let h = 0; h < 24; h++) sh(\"schtasks\", [\"/Delete\", \"/F\", \"/TN\", winCollectorTaskName(h)], true);\n }\n if (targets.includes(\"server\")) sh(\"schtasks\", [\"/Delete\", \"/F\", \"/TN\", WINDOWS_SERVER_TASK], true);\n console.log(\"agent-lens service: uninstalled (archive untouched)\");\n}\n\nfunction windowsStatus(targets: ServiceTarget[]): void {\n // Status must print the query output on success, so run loud (quiet=false) like linux/macStatus — the\n // `true` here was a copy-paste from the uninstall path and silently swallowed the schtasks listing.\n if (targets.includes(\"collector\")) {\n sh(\"schtasks\", [\"/Query\", \"/FO\", \"LIST\", \"/TN\", WINDOWS_COLLECTOR_PREFIX], false) ||\n console.log(\"agent-lens service: no collector tasks found under \" + WINDOWS_COLLECTOR_PREFIX);\n }\n if (targets.includes(\"server\")) {\n sh(\"schtasks\", [\"/Query\", \"/FO\", \"LIST\", \"/TN\", WINDOWS_SERVER_TASK], false) ||\n console.log(\"agent-lens service: no server task found (\" + WINDOWS_SERVER_TASK + \")\");\n }\n}\n\n// ---- dispatch -----------------------------------------------------------------------------------\n\nexport function runService(action: string, params: ServiceParams): void {\n const node = params.node ?? process.execPath;\n const cli = params.cliEntry;\n const hours = params.hours ?? DEFAULT_HOURS;\n const targets = params.targets;\n const plat = platform();\n const impl =\n plat === \"linux\"\n ? {\n install: () => linuxInstall(targets, node, cli, hours),\n uninstall: () => linuxUninstall(targets),\n status: () => linuxStatus(targets),\n }\n : plat === \"darwin\"\n ? {\n install: () => macInstall(targets, node, cli, hours),\n uninstall: () => macUninstall(targets),\n status: () => macStatus(targets),\n }\n : plat === \"win32\"\n ? {\n install: () => windowsInstall(targets, node, cli, hours),\n uninstall: () => windowsUninstall(targets),\n status: () => windowsStatus(targets),\n }\n : null;\n if (!impl) {\n console.error(`agent-lens service: unsupported platform '${plat}'`);\n process.exitCode = 1;\n return;\n }\n if (action === \"install\") impl.install();\n else if (action === \"uninstall\") impl.uninstall();\n else if (action === \"status\") impl.status();\n else {\n console.error(`agent-lens service: unknown action '${action}' (use: install | uninstall | status)`);\n process.exitCode = 1;\n }\n}\n","/**\n * Agent Lens — Stage 2 ingest, as importable functions (ADR-001, ADR-003).\n *\n * `runIngest` and `runMetrics` are the library entrypoints so the unified `agent-lens` CLI can\n * bundle and call them in-process; the thin bins in index.ts / metrics-cli.ts just forward argv.\n * The DB-writing engine lives in pipeline.ts (unit-tested in-process).\n */\nimport { readFileSync, statSync, mkdirSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n costForUsage,\n installPricing,\n loadExcludes,\n loadSources,\n rateForModel,\n resolveArchiveDir,\n resolveDbPath,\n SCHEMA_VERSION,\n SYNTHETIC_MODEL,\n type SourceAdapter,\n} from \"@agent-lens/core\";\nimport { openDb, openRaw, readSchemaVersion, resetSchema, type DB } from \"./db.js\";\nimport { ClaudeCodeAdapter } from \"./adapters/claude-code.js\";\nimport { classify, CLASSIFIER_VERSION } from \"./classify.js\";\nimport { detect, DETECTOR_VERSION } from \"./detect.js\";\nimport { classifyErrors } from \"./errors.js\";\nimport { deriveFileChanges, FILECHANGES_VERSION } from \"./filechanges.js\";\nimport { canonicalizeProjects } from \"./canonicalize.js\";\nimport { ingestFile, newStats, prepareStatements, pruneExcluded, rebuildDerived, type IngestStats } from \"./pipeline.js\";\nimport { ingestWorkflowResults, newWorkflowStats } from \"./workflows.js\";\nimport { ingestSubagentMeta, newMetaStats } from \"./meta.js\";\nimport { ingestToolResults, newToolResultStats } from \"./toolResults.js\";\nimport { parseExcludes, isExcludedArchivePath } from \"./redact.js\";\nimport { sha256, sha256File, streamLines, STREAM_THRESHOLD } from \"./fileread.js\";\n\nfunction parseIngestArgs(argv: string[]) {\n const a = { full: false, db: \"\", archive: \"\" };\n for (let i = 0; i < argv.length; i++) {\n if (argv[i] === \"--full\") a.full = true;\n else if (argv[i] === \"--db\") a.db = argv[++i] ?? \"\";\n else if (argv[i] === \"--archive\") a.archive = argv[++i] ?? \"\";\n }\n return a;\n}\n\n/** Skip-state for one archive file, as recorded by the previous run. */\ntype FileState = { size: number; mtime_ms: number; sha256: string; events_ingested: number } | undefined;\n\n/** What to do with one archive file this run.\n * - `skip-stat`: unchanged size+mtime means unchanged content — skip without ever reading or hashing\n * the file. This is what restores true incrementality across the mirror + every .versions snapshot\n * (ADR-010, impact 1).\n * - `skip-hash`: content is unchanged though mtime moved (e.g. an rsync --append-verify re-stat) —\n * skip the ingest but refresh size/mtime so the next run short-circuits on stat alone.\n * - `ingest`: genuinely new/changed. `buf` is the whole file for the common case, null for a large\n * file that must be streamed instead. */\ntype FileDecision =\n | { action: \"skip-stat\" }\n | { action: \"skip-hash\"; hash: string }\n | { action: \"ingest\"; hash: string; buf: Buffer | null };\n\nfunction shouldReingest(path: string, size: number, mtimeMs: number, prev: FileState): FileDecision {\n if (prev && prev.size === size && prev.mtime_ms === mtimeMs) return { action: \"skip-stat\" };\n\n // Size/mtime moved: read + hash to decide. Whole-file for the common case; stream large files.\n const small = size <= STREAM_THRESHOLD;\n const buf = small ? readFileSync(path) : null;\n const hash = small ? sha256(buf!) : sha256File(path);\n\n if (prev && prev.sha256 === hash) return { action: \"skip-hash\", hash };\n return { action: \"ingest\", hash, buf };\n}\n\n/** Everything the end-of-run report counts, gathered from the callers that produced it. */\ninterface IngestReport {\n stats: IngestStats;\n pruned: number;\n classified: { count: number };\n detected: { count: number };\n fileChanges: { count: number };\n wfStats: { upserted: number; skipped: number; malformed: number };\n metaStats: { upserted: number; skipped: number; malformed: number };\n trStats: { upserted: number; skipped: number; malformed: number };\n}\n\n/** Tally the final table counts, close the DB, and print the one-block run summary. */\nfunction printIngestReport(db: DB, dbPath: string, r: IngestReport): void {\n const count = (sql: string) => (db.prepare(sql).get() as any).n as number;\n const sessions = count(\"SELECT COUNT(*) n FROM sessions\");\n const turns = count(\"SELECT COUNT(*) n FROM turns\");\n const events = count(\"SELECT COUNT(*) n FROM events\");\n const tools = count(\"SELECT COUNT(*) n FROM tool_calls\");\n const usageRows = db\n .prepare(\"SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage\")\n .all() as any[];\n let cost = 0;\n let totalTokens = 0;\n // A model with no rate contributes $0 silently (ADR-028) — that is how a whole week of sessions\n // once read as free. Tally it here, where a human is watching the run, instead of letting the\n // estimate quietly understate itself.\n const unpriced = new Map<string, number>();\n for (const u of usageRows) {\n cost += costForUsage(u.model, u);\n totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;\n if (u.model && u.model !== SYNTHETIC_MODEL && !rateForModel(u.model)) {\n unpriced.set(u.model, (unpriced.get(u.model) ?? 0) + 1);\n }\n }\n const unpricedLine = unpriced.size\n ? `\\n unpriced=${unpriced.size} model(s): ` +\n [...unpriced].map(([m, n]) => `${m} (${n} records)`).join(\", \") +\n ` — cost above excludes them; add rates under \"pricing\" in agent-lens.config.json`\n : \"\";\n\n const wfRuns = count(\"SELECT COUNT(*) n FROM workflow_results\");\n const metaRows = count(\"SELECT COUNT(*) n FROM session_meta\");\n const trRows = count(\"SELECT COUNT(*) n FROM tool_results\");\n const sevCounts = db\n .prepare(\"SELECT severity, COUNT(*) n FROM findings GROUP BY severity\")\n .all() as Array<{ severity: string; n: number }>;\n const sevMap = Object.fromEntries(sevCounts.map((s) => [s.severity, s.n]));\n const sevLine = [\"critical\", \"high\", \"medium\", \"low\", \"info\"].map((s) => `${s}=${sevMap[s] ?? 0}`).join(\" \");\n\n db.close();\n console.log(\n `agent-lens-ingest: files=${r.stats.files} skipped=${r.stats.skipped} new_events=${r.stats.newEvents} malformed=${r.stats.malformed}${r.pruned ? ` excluded_pruned=${r.pruned}` : \"\"}\\n` +\n ` sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${r.classified.count}\\n` +\n ` workflow_results=${wfRuns} (sidecar upserted=${r.wfStats.upserted} skipped=${r.wfStats.skipped} malformed=${r.wfStats.malformed})\\n` +\n ` session_meta=${metaRows} (upserted=${r.metaStats.upserted} skipped=${r.metaStats.skipped} malformed=${r.metaStats.malformed})\\n` +\n ` tool_results=${trRows} (upserted=${r.trStats.upserted} skipped=${r.trStats.skipped} malformed=${r.trStats.malformed})\\n` +\n ` findings=${r.detected.count} (${sevLine})\\n` +\n ` file_changes=${r.fileChanges.count}\\n` +\n ` tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}` +\n unpricedLine,\n );\n}\n\n/** Stage 2: (re)build the derived SQLite store from the local archive. Idempotent. */\n// This module is the package entry (`exports: ./dist/run.js`), so re-export the stamped engine\n// versions here: the server's /api/about reports what a re-run would produce against what is\n// actually stamped in the stored rows (ADR-027). Importing them from ./detect.js or ./classify.js\n// across the package boundary would reach past the public surface.\nexport { CLASSIFIER_VERSION } from \"./classify.js\";\nexport { DETECTOR_VERSION } from \"./detect.js\";\n\nexport function runIngest(argv: string[] = process.argv.slice(2)): void {\n const args = parseIngestArgs(argv);\n // --archive stays as an explicit per-run override (ingesting a copied archive); there is no\n // env/config equivalent — the location is fixed so collect and ingest can never disagree (ADR-021).\n const archiveRoot = args.archive || resolveArchiveDir();\n const { path: dbPath } = resolveDbPath(args.db);\n // Cost in the end-of-run report is derived through the effective table (ADR-028), so install any\n // config overrides before the report — and warn here about malformed ones, where a human is looking.\n installPricing();\n\n if (!existsSync(archiveRoot)) {\n console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);\n process.exit(1);\n }\n mkdirSync(dirname(dbPath), { recursive: true });\n\n // --full: open WITHOUT applying schema (the on-disk schema may be a stale version), then\n // drop+recreate from the archive (source of truth). This is also the migration path — a\n // SCHEMA_VERSION bump's new columns take effect here without a separate migration step.\n const db = args.full ? openRaw(dbPath) : openDb(dbPath);\n if (args.full) {\n resetSchema(db);\n } else {\n // Schema guard: an incremental ingest only runs CREATE IF NOT EXISTS — it adds new tables but\n // cannot migrate altered columns on existing ones, and never advances the stamp (applySchema is\n // stamp-if-absent). So a DB stamped by an older build is stale: refuse rather than silently ingest\n // into a half-migrated schema. `ingest --full` (drop+recreate) is the migration path.\n const dbVer = readSchemaVersion(db);\n if (dbVer != null && dbVer !== SCHEMA_VERSION) {\n console.error(\n `agent-lens-ingest: schema mismatch — DB is v${dbVer}, this build expects v${SCHEMA_VERSION}. ` +\n `Run 'agent-lens ingest --full' to rebuild.`,\n );\n db.close();\n process.exit(1);\n }\n }\n\n // Adapter registry keyed by agent type; configured sources resolved by the shared resolver.\n const adapterList: SourceAdapter[] = [new ClaudeCodeAdapter()];\n const adapterById = new Map(adapterList.map((a) => [a.agentId, a]));\n const sources = loadSources();\n const now = new Date().toISOString();\n\n // Excluded projects (config `exclude` + AGENT_LENS_EXCLUDE): drop any already-ingested ones now\n // (incremental; --full already reset the DB), then filter them out of discovery below.\n const excludePaths = loadExcludes();\n const excludedDirs = parseExcludes(excludePaths.join(\",\"));\n const pruned = pruneExcluded(db, excludePaths);\n\n const stmts = prepareStatements(db);\n const stats = newStats();\n const wfStats = newWorkflowStats();\n const metaStats = newMetaStats();\n const trStats = newToolResultStats();\n // Sessions touched this run; drives the incremental derived rebuild (ADR-010, impacts 2/3).\n const dirty = new Set<string>();\n\n for (const source of sources) {\n const adapter = adapterById.get(source.agent);\n if (!adapter) {\n console.warn(`agent-lens-ingest: no adapter for agent '${source.agent}' (source '${source.label}') — skipping`);\n continue;\n }\n stmts.insAgent.run(adapter.agentId, adapter.agentName);\n stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });\n\n let files = adapter.discover(join(archiveRoot, source.label), source.label);\n // Drop excluded projects (matches /projects/<encodedDir>/ so nested subagent files go too).\n if (excludedDirs.length) files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));\n // Mirror before versions so the mirror copy wins canonical fields (ON CONFLICT DO NOTHING).\n files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));\n\n for (const file of files) {\n stats.files++;\n const st = statSync(file.path);\n const mtimeMs = Math.trunc(st.mtimeMs);\n const prev = args.full ? undefined : (stmts.getState.get(file.path) as FileState);\n\n const decision = shouldReingest(file.path, st.size, mtimeMs, prev);\n if (decision.action === \"skip-stat\") {\n stats.skipped++;\n continue;\n }\n if (decision.action === \"skip-hash\") {\n stmts.setState.run({\n file_path: file.path,\n size: st.size,\n mtime_ms: mtimeMs,\n sha256: decision.hash,\n events_ingested: prev!.events_ingested,\n ingested_at: now,\n });\n stats.skipped++;\n continue;\n }\n\n const lines = decision.buf ? decision.buf.toString(\"utf8\").split(\"\\n\") : streamLines(file.path);\n ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: decision.hash }, now, stats);\n dirty.add(file.sessionId);\n }\n\n // Workflow-tool result sidecars (wf_<id>.json) — the authoritative record of how each run\n // finished; not *.jsonl, so the transcript walk above never sees them. Own table, own skip-state.\n ingestWorkflowResults(db, join(archiveRoot, source.label), source.label, excludedDirs, now, wfStats, args.full);\n\n // Per-subagent metadata (subagents/agent-<id>.meta.json) — the authoritative agentType/description/\n // spawnDepth; joined onto sessions at read time. Own table (session_meta), own skip-state.\n ingestSubagentMeta(db, join(archiveRoot, source.label), source.label, excludedDirs, now, metaStats, args.full);\n\n // Spilled full tool outputs (tool-results/<name>.txt) — the un-truncated result the transcript only\n // summarized; joined onto tool_calls at read time via the summary's marker. Own table, own skip-state.\n ingestToolResults(db, join(archiveRoot, source.label), excludedDirs, now, trStats, args.full);\n }\n\n // Incremental derived rebuild over only the touched sessions (+ their linkage neighborhood); --full\n // rebuilds everything. classify reuses the expanded set rebuildDerived returns.\n const expanded = rebuildDerived(db, args.full ? null : dirty);\n\n // Canonical project roots (ADR-023): fold cwd-grained project rows into their git root (or\n // observed ancestor) BEFORE the passes below — classify/detect/file-changes all consume the\n // session's project path. Global + re-runnable (the table is a few dozen rows), so an existing\n // DB heals on its next ingest without --full.\n canonicalizeProjects(db);\n\n // Heuristic classification (ADR-004) over the now-stable derived tables. Deterministic +\n // re-runnable; also exposed standalone as `agent-lens metrics` (see runMetrics).\n const classified = classify(db, args.full ? null : expanded);\n\n // Security findings (ADR-017) over the same derived tool_calls. Deterministic + re-runnable; the\n // detector reuses the expanded dirty set and delete-then-inserts the touched sessions' findings.\n const detected = detect(db, args.full ? null : expanded);\n\n // Tool-error classification (error_type on errored tool_calls). Deterministic + re-runnable; reuses\n // the same expanded dirty set. Powers the sessions error-type filter + dashboard/detail breakdowns.\n classifyErrors(db, args.full ? null : expanded);\n\n // File-modification provenance (ADR-022) from Edit/Write/NotebookEdit inputs. Deterministic +\n // re-runnable; reuses the same expanded dirty set and delete-then-inserts per touched session.\n const fileChanges = deriveFileChanges(db, args.full ? null : expanded);\n\n printIngestReport(db, dbPath, { stats, pruned, classified, detected, fileChanges, wfStats, metaStats, trStats });\n}\n\n/** Standalone reclassification over an already-ingested DB (no archive re-read). */\nexport function runMetrics(argv: string[] = process.argv.slice(2)): void {\n let dbArg = \"\";\n for (let i = 0; i < argv.length; i++) if (argv[i] === \"--db\") dbArg = argv[++i] ?? \"\";\n const { path: dbPath } = resolveDbPath(dbArg);\n\n if (!existsSync(dbPath)) {\n console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);\n process.exit(1);\n }\n\n const db = openDb(dbPath);\n canonicalizeProjects(db); // ADR-023 — heal cwd-grained projects before the passes read them\n const r = classify(db);\n const d = detect(db);\n const e = classifyErrors(db);\n const fc = deriveFileChanges(db);\n db.close();\n console.log(\n `agent-lens-metrics: classified=${r.count} classifier_version=${CLASSIFIER_VERSION} ` +\n `findings=${d.count} detector_version=${DETECTOR_VERSION} errors_classified=${e.count} error_classifier_version=${e.version} ` +\n `file_changes=${fc.count} filechanges_version=${FILECHANGES_VERSION} db=${dbPath}`,\n );\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport { SCHEMA_SQL, SCHEMA_VERSION } from \"@agent-lens/core\";\n\nexport type DB = DatabaseSync;\n\n/** Open (creating if needed) the DB and set pragmas, WITHOUT applying the schema. */\nexport function openRaw(file: string): DB {\n const db = new DatabaseSync(file);\n db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(\"PRAGMA foreign_keys = ON\");\n return db;\n}\n\n/** The schema version stamped in an existing DB, or null if unstamped/brand-new. */\nexport function readSchemaVersion(db: DB): number | null {\n const row = db\n .prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\")\n .get() as { value: string } | undefined;\n if (!row) return null;\n const n = Number(row.value);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Apply the current schema (idempotent CREATE IF NOT EXISTS) and stamp the version ONLY when absent.\n * Stamping if-absent (not overwrite) is deliberate: an incremental `ingest` runs CREATE IF NOT EXISTS,\n * which adds new *tables* but cannot migrate altered columns on existing ones — so overwriting the stamp\n * would let a stale DB claim the current version and lie. A stale DB keeps its old stamp; the caller\n * (`runIngest`) guards on it. `resetSchema` drops `meta`, so its applySchema re-stamps the current\n * version from scratch — the only path that advances the stamp.\n */\nfunction applySchema(db: DB): void {\n db.exec(SCHEMA_SQL);\n db.prepare(\n \"INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO NOTHING\",\n ).run(String(SCHEMA_VERSION));\n}\n\n/**\n * Open (creating if needed) the Agent Lens database and ensure the schema is applied.\n * NOTE: on a stale on-disk schema this only adds missing objects (CREATE IF NOT EXISTS) — it does\n * not migrate changed/added columns. A SCHEMA_VERSION bump is applied via `ingest --full`\n * (openRaw + resetSchema), the canonical rebuild path.\n */\nexport function openDb(file: string): DB {\n const db = openRaw(file);\n applySchema(db);\n return db;\n}\n\n/**\n * Drop and recreate every table from the current SCHEMA_SQL. Used by `ingest --full`: because the\n * archive is the source of truth and the DB is a derived projection, a full rebuild is also the\n * migration path — dropping the tables (vs. DELETE FROM) lets a SCHEMA_VERSION bump's new columns\n * take effect on an existing DB without a separate migration mechanism (ADR-001/009).\n */\nexport function resetSchema(db: DB): void {\n // Disable FK enforcement during teardown: the v3 schema has cross/self references\n // (sessions.parent_turn_id → turns, parent_session_id → sessions) that otherwise make DROP order\n // matter and trip \"FOREIGN KEY constraint failed\". applySchema re-enables FKs (PRAGMA in SCHEMA_SQL).\n // `finally` so a failed drop/apply can't leave the connection unguarded for the rest of the process.\n db.exec(\"PRAGMA foreign_keys = OFF\");\n try {\n db.exec(`\n DROP TRIGGER IF EXISTS events_au;\n DROP TRIGGER IF EXISTS events_ad;\n DROP TRIGGER IF EXISTS events_ai;\n DROP TABLE IF EXISTS events_fts;\n DROP TABLE IF EXISTS file_changes;\n DROP TABLE IF EXISTS findings;\n DROP TABLE IF EXISTS classifications;\n DROP TABLE IF EXISTS token_usage;\n DROP TABLE IF EXISTS tool_calls;\n DROP TABLE IF EXISTS skills;\n DROP TABLE IF EXISTS events;\n DROP TABLE IF EXISTS turns;\n DROP TABLE IF EXISTS sessions;\n DROP TABLE IF EXISTS projects;\n DROP TABLE IF EXISTS sources;\n DROP TABLE IF EXISTS agents;\n DROP TABLE IF EXISTS workflow_results;\n DROP TABLE IF EXISTS session_meta;\n DROP TABLE IF EXISTS tool_results;\n DROP TABLE IF EXISTS ingest_state;\n DROP TABLE IF EXISTS meta;\n `);\n applySchema(db);\n } finally {\n db.exec(\"PRAGMA foreign_keys = ON\");\n }\n}\n","import { readdirSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport type {\n ParsedLine,\n SourceAdapter,\n SourceFile,\n ToolCallRow,\n ToolResultPatch,\n} from \"@agent-lens/core\";\n\n/** The displayable halves of a message `content`, kept apart. Stored as two columns so nothing\n * downstream has to re-parse a transcript record to tell prose from reasoning — this adapter is the\n * only place that knows the record's shape (ADR-008). */\nexport interface RecordParts {\n text: string | null;\n thinking: string | null;\n}\n\n/** Split a message `content` (string or block array) into visible text and reasoning. */\nfunction extractParts(content: unknown): RecordParts {\n if (content == null) return { text: null, thinking: null };\n if (typeof content === \"string\") return { text: content.trim() || null, thinking: null };\n if (!Array.isArray(content)) return { text: null, thinking: null };\n const texts: string[] = [];\n const thoughts: string[] = [];\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, unknown>;\n if (b.type === \"text\" && typeof b.text === \"string\") texts.push(b.text);\n else if (b.type === \"thinking\" && typeof b.thinking === \"string\") thoughts.push(b.thinking);\n }\n return { text: texts.join(\"\\n\").trim() || null, thinking: thoughts.join(\"\\n\").trim() || null };\n}\n\n/** Just the visible text — for the places that only ever carry prose (a queued prompt, a tool result). */\nfunction flattenText(content: unknown): string | null {\n return extractParts(content).text;\n}\n\n/** Recover the text of a message the user typed while a turn was still running. Claude Code doesn't\n * log it as a `user` event; it appends an `attachment` whose `prompt` holds what was typed (a string,\n * or content blocks when something was pasted alongside it). Without this the words survive only in\n * raw_json — the transcript renders nothing for a body-less event, so the message just disappears.\n *\n * Scoped to `commandMode: \"prompt\"`. The other mode, \"task-notification\", is Claude Code queuing its\n * own notice, and that one IS replayed afterwards as a real user event — reading it here would show\n * it twice. */\nfunction queuedPromptText(attachment: unknown): string | null {\n if (!attachment || typeof attachment !== \"object\") return null;\n const a = attachment as Record<string, unknown>;\n if (a.type !== \"queued_command\" || a.commandMode !== \"prompt\") return null;\n return flattenText(a.prompt);\n}\n\n/** Compact projection of a tool result for the transcript view. Preserves line structure — so console\n * output (build logs, git output, …) renders as lines instead of one run-on line — while staying\n * bounded: trailing per-line whitespace and runs of blank lines are trimmed, and the text is capped by\n * both line count and characters so a large result can't be dumped wholesale into the DB. Internal\n * spacing is kept intact so column-aligned output survives. The full untruncated output, when big\n * enough, is spilled by Claude Code to tool-results/<name>.txt and surfaced separately (full_result). */\nconst RESULT_SUMMARY_MAX_LINES = 40;\nconst RESULT_SUMMARY_MAX_CHARS = 2000;\nfunction summarizeResult(content: unknown): string | null {\n let s: string | null = null;\n if (typeof content === \"string\") s = content;\n else if (Array.isArray(content)) {\n const parts: string[] = [];\n for (const block of content) {\n const b = block as Record<string, unknown>;\n if (b && b.type === \"text\" && typeof b.text === \"string\") parts.push(b.text);\n }\n s = parts.join(\"\\n\");\n } else if (content != null) {\n s = JSON.stringify(content);\n }\n if (!s) return null;\n // Normalize newlines, drop trailing per-line whitespace, and collapse 3+ blank lines — but keep\n // internal spacing and single blank lines so the structure reads like the original console output.\n s = s.replace(/\\r\\n/g, \"\\n\").replace(/[ \\t]+\\n/g, \"\\n\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n if (!s) return null;\n let truncated = false;\n const lines = s.split(\"\\n\");\n if (lines.length > RESULT_SUMMARY_MAX_LINES) {\n s = lines.slice(0, RESULT_SUMMARY_MAX_LINES).join(\"\\n\");\n truncated = true;\n }\n if (s.length > RESULT_SUMMARY_MAX_CHARS) {\n s = s.slice(0, RESULT_SUMMARY_MAX_CHARS);\n const nl = s.lastIndexOf(\"\\n\");\n if (nl > RESULT_SUMMARY_MAX_CHARS - 200) s = s.slice(0, nl); // prefer a clean line boundary\n truncated = true;\n }\n s = s.trimEnd();\n return (truncated ? s + \"\\n… (truncated)\" : s) || null;\n}\n\nfunction asString(v: unknown): string | null {\n return typeof v === \"string\" && v.length > 0 ? v : null;\n}\n\nfunction asInt(v: unknown): number {\n return typeof v === \"number\" && Number.isFinite(v) ? Math.trunc(v) : 0;\n}\n\n/**\n * Derive the parent session id for a subagent transcript from its on-disk path. Claude Code nests\n * subagent transcripts under the spawning session's directory:\n * Task/Agent: <...>/projects/<enc>/<PARENT_SESSION_ID>/subagents/agent-<id>.jsonl\n * Workflow: <...>/projects/<enc>/<PARENT_SESSION_ID>/subagents/workflows/wf_<runId>/agent-<id>.jsonl\n * In both layouts the parent session id is the path segment immediately before `subagents/`. This is\n * the deterministic, redaction-surviving link (the Workflow tool emits no toolUseResult.agentId, so\n * the tool_calls.spawned_session_id path never fires for workflow fan-out). Returns null for a main\n * session (no `subagents/` segment).\n */\nexport function parentSessionFromPath(path: string): string | null {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"subagents\");\n return i > 0 ? parts[i - 1] : null;\n}\n\n/**\n * For a Workflow-tool subagent, the run id from its path. Workflow fan-out nests under\n * `…/subagents/workflows/<runId>/agent-<id>.jsonl`, so the run id is the segment after `workflows/`\n * (e.g. `wf_ab787f1c-ff7`). Returns null for Task/Agent subagents (no `workflows/` segment) and main\n * sessions. The same id appears in the launching Workflow tool_call's result (toolUseResult.runId),\n * which is how a run's agents group and link back to the turn that started them.\n */\nexport function workflowRunFromPath(path: string): string | null {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"workflows\");\n return i >= 0 && i + 1 < parts.length ? parts[i + 1] : null;\n}\n\nfunction recurseJsonl(dir: string, out: string[]): void {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n const p = join(dir, e.name);\n if (e.isDirectory()) recurseJsonl(p, out);\n else if (e.isFile() && e.name.endsWith(\".jsonl\")) out.push(p);\n }\n}\n\nexport class ClaudeCodeAdapter implements SourceAdapter {\n agentId = \"claude-code\";\n agentName = \"Claude Code CLI\";\n\n discover(sourceArchiveDir: string, sourceId: string): SourceFile[] {\n const files: SourceFile[] = [];\n const archiveRoot = sourceArchiveDir;\n\n const addFrom = (projectsDir: string, isVersion: boolean) => {\n const found: string[] = [];\n recurseJsonl(projectsDir, found);\n for (const path of found) {\n // <...>/projects/<encodedDir>/<sessionId>.jsonl\n const parts = path.split(\"/\");\n const sessionId = basename(path, \".jsonl\");\n const encodedDir = parts[parts.length - 2] ?? \"\";\n files.push({\n path,\n sessionId,\n encodedDir,\n isVersion,\n sourceId,\n parentSessionId: parentSessionFromPath(path),\n workflowRunId: workflowRunFromPath(path),\n });\n }\n };\n\n addFrom(join(archiveRoot, \"projects\"), false);\n\n // .versions/<ts>/projects/<encodedDir>/<sessionId>.jsonl\n const versionsRoot = join(archiveRoot, \".versions\");\n try {\n for (const ts of readdirSync(versionsRoot, { withFileTypes: true })) {\n if (ts.isDirectory()) addFrom(join(versionsRoot, ts.name, \"projects\"), true);\n }\n } catch {\n /* no versions yet */\n }\n return files;\n }\n\n parseLine(raw: unknown, file: SourceFile, seq: number): ParsedLine {\n if (!raw || typeof raw !== \"object\") return {};\n const r = raw as Record<string, any>;\n\n // Session-level metadata from the common envelope + pointer lines.\n const meta: ParsedLine[\"meta\"] = {};\n if (asString(r.cwd)) meta.cwd = r.cwd;\n if (asString(r.slug)) meta.slug = r.slug;\n if (asString(r.version)) meta.cli_version = r.version;\n if (asString(r.entrypoint)) meta.entrypoint = r.entrypoint;\n if (asString(r.gitBranch)) meta.git_branch = r.gitBranch;\n if (typeof r.isSidechain === \"boolean\") meta.is_sidechain = r.isSidechain;\n if (r.type === \"ai-title\" && asString(r.aiTitle)) meta.ai_title = r.aiTitle;\n\n // Lines without a uuid (pointer/meta lines) contribute metadata only.\n if (!asString(r.uuid)) return { meta };\n\n const message = (r.message ?? {}) as Record<string, any>;\n const queued = queuedPromptText(r.attachment);\n const parts = extractParts(message.content);\n const text = parts.text ?? queued;\n\n const result: ParsedLine = {\n meta,\n event: {\n uuid: r.uuid,\n session_id: file.sessionId,\n turn_id: null,\n parent_uuid: asString(r.parentUuid),\n seq,\n type: asString(r.type) ?? \"unknown\",\n // A recovered queued prompt has no `message` to take a role from, but it *is* the user\n // speaking — without this the transcript labels their own words as a generic attachment.\n role: asString(message.role) ?? (queued ? \"user\" : null),\n timestamp: asString(r.timestamp),\n model: asString(message.model),\n is_sidechain: r.isSidechain ? 1 : 0,\n is_meta: r.isMeta ? 1 : 0,\n text,\n thinking: parts.thinking,\n raw_json: JSON.stringify(raw),\n source_file: file.path,\n },\n };\n\n // Token usage at the assistant-message grain.\n const usage = message.usage as Record<string, any> | undefined;\n if (usage) {\n result.tokenUsage = {\n event_uuid: r.uuid,\n session_id: file.sessionId,\n turn_id: null,\n message_id: asString(message.id),\n model: asString(message.model),\n input_tokens: asInt(usage.input_tokens),\n output_tokens: asInt(usage.output_tokens),\n cache_creation_input_tokens: asInt(usage.cache_creation_input_tokens),\n cache_read_input_tokens: asInt(usage.cache_read_input_tokens),\n service_tier: asString(usage.service_tier),\n };\n }\n\n // Tool calls (tool_use blocks) and tool results (tool_result blocks + toolUseResult).\n const content = message.content;\n if (Array.isArray(content)) {\n const toolCalls: ToolCallRow[] = [];\n const toolResults: ToolResultPatch[] = [];\n const tur = r.toolUseResult as Record<string, any> | undefined;\n\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const b = block as Record<string, any>;\n\n if (b.type === \"tool_use\" && asString(b.id)) {\n const name = asString(b.name) ?? \"unknown\";\n const input = b.input ?? {};\n toolCalls.push({\n id: b.id,\n event_uuid: r.uuid,\n session_id: file.sessionId,\n turn_id: null,\n tool_name: name,\n caller: asString(b.caller),\n skill_name: name === \"Skill\" ? asString(input.skill) ?? asString(input.command) : null,\n skill_id: null, // content-addressed skill version; linked in rebuildDerived from the injected body\n agent_type:\n name === \"Task\" || name === \"Agent\" ? asString(input.subagent_type) : null,\n spawned_session_id: null, // patched in once the tool_result carries toolUseResult.agentId\n workflow_run_id: null, // patched in from the Workflow tool_result's runId\n workflow_name: name === \"Workflow\" ? asString(input.name) : null, // refined from the result\n resolved_model: null,\n status: null,\n total_duration_ms: null,\n total_tokens: null,\n total_tool_use_count: null,\n input_json: JSON.stringify(input),\n result_summary: null,\n });\n } else if (b.type === \"tool_result\" && asString(b.tool_use_id)) {\n // AskUserQuestion's result is structured (which option the user picked per question, plus any\n // free-text notes) and lives on toolUseResult, not in the block content. Capture it verbatim\n // as JSON so the UI can render the Q&A with the selection marked — the generic 280-char prose\n // summary would truncate multi-question prompts and drop the notes entirely. The questions +\n // options themselves are already in the tool_use input_json, so only answers/annotations are\n // stored here.\n const isAskUser = tur && tur.questions && tur.answers && typeof tur.answers === \"object\";\n toolResults.push({\n tool_use_id: b.tool_use_id,\n result_summary: isAskUser\n ? JSON.stringify({ answers: tur!.answers, annotations: tur!.annotations ?? {} })\n : summarizeResult(b.content),\n // Tool failure is signalled by `is_error` on the tool_result block itself — NOT by\n // toolUseResult.status (whose values are task/lifecycle states like completed/pending/\n // async_launched and never \"error\"). Map is_error → status=\"error\" so the error roll-ups,\n // the transcript `tool-err` styling, and detect.ts's failed-attempt de-escalation all fire.\n // Non-error results fall back to the toolUseResult lifecycle status when present.\n status: b.is_error === true ? \"error\" : tur ? asString(tur.status) : null,\n ...(tur\n ? {\n agent_type: asString(tur.agentType),\n // Deterministic link: a Task/Agent subagent's transcript is keyed off its agentId\n // ('agent-'||agentId is the session id assigned by discover()'s filename stem).\n spawned_session_id: asString(tur.agentId) ? `agent-${tur.agentId}` : null,\n // Workflow fan-out: the result carries the run id (wf_<id>) + workflow name; this is\n // how a run's subagents (nested under subagents/workflows/<runId>/) link to this turn.\n workflow_run_id: asString(tur.runId),\n workflow_name: asString(tur.workflowName),\n resolved_model: asString(tur.resolvedModel),\n total_duration_ms: typeof tur.totalDurationMs === \"number\" ? tur.totalDurationMs : null,\n total_tokens: typeof tur.totalTokens === \"number\" ? tur.totalTokens : null,\n total_tool_use_count:\n typeof tur.totalToolUseCount === \"number\" ? tur.totalToolUseCount : null,\n }\n : {}),\n });\n }\n }\n if (toolCalls.length) result.toolCalls = toolCalls;\n if (toolResults.length) result.toolResults = toolResults;\n }\n\n return result;\n }\n}\n","/**\n * Agent Lens — incremental \"dirty set\" temp table.\n *\n * The derived passes (classify / detect / errors / prune / linkage) each scope their work to the\n * sessions touched this run by materializing the id set into a TEMP table and joining against it\n * (ADR-010). The DROP → CREATE TEMP → batched INSERT prologue lives here once, rather than at each\n * of the 5 call sites. `name` is an internal constant table name (`_dirty`, `_dirty_sec`, …), never\n * user input — safe to interpolate. Callers drop the table themselves when the pass is done.\n */\nimport { transaction } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\n\n/** (Re)create a single-column TEMP id table `name` and populate it from `ids`, in one transaction. */\nexport function createDirtySet(db: DB, name: string, ids: Iterable<string>): void {\n db.exec(`DROP TABLE IF EXISTS ${name}`);\n db.exec(`CREATE TEMP TABLE ${name} (id TEXT PRIMARY KEY)`);\n const ins = db.prepare(`INSERT OR IGNORE INTO ${name} (id) VALUES (?)`);\n transaction(db, (xs: Iterable<string>) => {\n for (const id of xs) ins.run(id);\n })(ids);\n}\n","/**\n * Reading back JSON that ingest itself wrote (`tool_calls.input_json`). A parse failure here is not a\n * malformed-input case — the transcript reader already counts those — it means a row in our own DB is\n * corrupt or truncated, which the derivation passes used to swallow silently: they returned \"no data\"\n * and the affected tool call quietly stopped being classified or scanned for findings.\n *\n * Swallowing is still right (one bad row must not abort a rescan of thousands), but it is now\n * counted, and the first failure per site is reported so a corrupt DB is visible rather than showing\n * up as findings that mysteriously don't fire.\n */\nlet malformed = 0;\nconst reported = new Set<string>();\n\n/** Parse a JSON column, or null. `where` names the call site in the one-time warning. */\nexport function parseStored<T = unknown>(json: string | null | undefined, where: string): T | null {\n if (!json) return null;\n try {\n return JSON.parse(json) as T;\n } catch {\n malformed++;\n if (!reported.has(where)) {\n reported.add(where);\n console.warn(`agent-lens: malformed stored JSON in ${where} — the affected rows are skipped; re-ingest with --full to rebuild`);\n }\n return null;\n }\n}\n\n/** How many stored-JSON parses have failed in this process. Zero on a healthy DB. */\nexport function malformedStoredJson(): number {\n return malformed;\n}\n\n/** Test helper: forget the counter and the one-time warnings. */\nexport function resetMalformedStoredJson(): void {\n malformed = 0;\n reported.clear();\n}\n","/**\n * Agent Lens — heuristic classification (ADR-004). No AI: category + complexity are derived\n * deterministically from signals already in the DB (tool mix, LoC from Edit/Write inputs,\n * turns, tokens, duration, subagent fan-out, prompt keywords). Re-runnable and idempotent —\n * the same DB always yields identical rows. Every input and sub-score is recorded in\n * `signals_json` so a result can be explained and the bands retuned without re-deriving meaning.\n *\n * Scope v1 = sessions (both main and sidechain). The PK (scope, target_id) leaves turn-level\n * for later. `classifier_version` lets a future (e.g. local-LLM) classifier supersede these rows.\n */\nimport type { DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\nimport { transaction } from \"@agent-lens/core\";\nimport { parseStored } from \"./storedjson.js\";\n\n// v2 (ADR-004): realistic complexity ceilings so real (long, substantial) main sessions spread\n// across bands instead of pegging in \"large\"; subagent sessions categorized by their spawner role\n// rather than keyword heuristics on a read-only exploration transcript.\n// v3: keyword scoring matches whole words instead of substrings — \"review\" no longer\n// fires inside \"preview\", nor \"fix\" inside \"prefix\" — so stored categories from v2 are superseded.\nexport const CLASSIFIER_VERSION = 3;\n\nexport type Category = \"feature\" | \"bugfix\" | \"refactor\" | \"docs\" | \"ops\" | \"review\" | \"chore\";\n\n/** Fixed order = deterministic tie-break when scores are equal. */\nconst CATEGORIES: Category[] = [\"bugfix\", \"refactor\", \"docs\", \"ops\", \"review\", \"feature\", \"chore\"];\n\n// Cutoffs tuned to the real main-session score distribution (v2 ceilings) so the five bands\n// partition main work meaningfully (~9/19/32/32/8%). Subagents score low and land \"trivial\".\nconst BANDS: Array<{ max: number; band: string }> = [\n { max: 22, band: \"trivial\" },\n { max: 40, band: \"small\" },\n { max: 55, band: \"medium\" },\n { max: 68, band: \"large\" },\n { max: Infinity, band: \"xl\" },\n];\n\n/**\n * Subagent roles whose work is inherently investigative/planning ⇒ \"review\". A general-purpose (or\n * unknown) subagent does varied work, so fall back to the heuristic on its own transcript.\n */\nconst REVIEW_SUBAGENT_ROLES = new Set([\"Explore\", \"Plan\", \"code-reviewer\", \"claude-code-guide\"]);\nfunction categoryForSubagentRole(role: string | undefined): Category | null {\n if (!role) return null;\n return REVIEW_SUBAGENT_ROLES.has(role) ? \"review\" : null;\n}\n\n/** Number of lines in a string (0 for empty/undefined). Trailing newline doesn't add a line. */\nfunction countLines(s: unknown): number {\n if (typeof s !== \"string\" || s.length === 0) return 0;\n const trimmed = s.endsWith(\"\\n\") ? s.slice(0, -1) : s;\n return trimmed.length === 0 ? 0 : trimmed.split(\"\\n\").length;\n}\n\nconst DOC_EXT = /\\.(md|markdown|rst|txt|adoc)$/i;\nconst OPS_FILE = /(^|\\/)(dockerfile|docker-compose|\\.github\\/|\\.gitlab-ci|makefile)|\\.(ya?ml|tf|sh|toml|ini|cfg|service|timer)$/i;\n\n/** LoC contribution of a single Edit/Write tool call, parsed from its verbatim input_json. */\nexport function locDelta(toolName: string, inputJson: string | null): { added: number; removed: number; file: string | null } {\n if (!inputJson) return { added: 0, removed: 0, file: null };\n const input = parseStored<Record<string, unknown>>(inputJson, \"classify: tool_calls.input_json\");\n if (!input) return { added: 0, removed: 0, file: null };\n const file = typeof input?.file_path === \"string\" ? input.file_path : null;\n if (toolName === \"Write\") return { added: countLines(input?.content), removed: 0, file };\n if (toolName === \"Edit\") return { added: countLines(input?.new_string), removed: countLines(input?.old_string), file };\n return { added: 0, removed: 0, file };\n}\n\nfunction band(score: number): string {\n for (const b of BANDS) if (score < b.max) return b.band;\n return \"xl\";\n}\n\nconst clamp01 = (x: number) => (x < 0 ? 0 : x > 1 ? 1 : x);\n\ninterface Signals {\n toolCounts: Record<string, number>;\n skills: Record<string, number>;\n loc: { added: number; removed: number; net: number; churn: number; files: number };\n turn_count: number;\n event_count: number;\n work_tokens: number; // input + output + cache_creation (cache-read excluded — replay ≠ work)\n cache_read_tokens: number;\n duration_ms: number;\n subagent_count: number;\n is_sidechain: number;\n}\n\n/** Compiled `\\b<phrase>\\b` matchers, keyed by phrase. The keyword lists are fixed and `hits` runs once\n * per category per session, so compiling each pattern once matters at corpus scale. */\nconst WORD_RE = new Map<string, RegExp>();\nfunction wordRe(phrase: string): RegExp {\n let re = WORD_RE.get(phrase);\n if (!re) {\n // Escape regex metacharacters — phrases include \"ci/cd\" and could later include \"c++\".\n const escaped = phrase.trim().replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n re = new RegExp(`\\\\b${escaped}\\\\b`, \"g\");\n WORD_RE.set(phrase, re);\n }\n return re;\n}\n\n/**\n * Count whole-word occurrences of any phrase in `words` within `text`.\n *\n * This used to scan with `indexOf`, which is substring matching: \"review\" hit inside \"preview\",\n * \"fix\" inside \"prefix\", \"add\" inside \"address\". Four keywords compensated by carrying a trailing\n * space in the list (`\"add \"`, `\"build \"`, `\"create \"`, `\"ci \"`), which only worked when the next\n * character happened to be a space — a line-ending \"add\", or \"add.\" mid-sentence, still missed.\n * Real word boundaries do the job properly, so those four are now written bare.\n */\nfunction hits(text: string, words: string[]): number {\n let n = 0;\n for (const w of words) {\n const re = wordRe(w);\n re.lastIndex = 0; // shared compiled regex: /g keeps state between calls\n while (re.exec(text) !== null) n++;\n }\n return n;\n}\n\nfunction scoreCategories(s: Signals, text: string, files: string[]): Record<Category, number> {\n const t = text.toLowerCase();\n const editN = s.toolCounts[\"Edit\"] ?? 0;\n const writeN = s.toolCounts[\"Write\"] ?? 0;\n const readN = s.toolCounts[\"Read\"] ?? 0;\n const writes = editN + writeN;\n\n const scores: Record<Category, number> = {\n bugfix: 0,\n refactor: 0,\n docs: 0,\n ops: 0,\n review: 0,\n feature: 0,\n chore: 0,\n };\n\n // Keyword evidence (prompt text).\n scores.bugfix += hits(t, [\"bug\", \"fix\", \"broken\", \"crash\", \"regression\", \"traceback\", \"stack trace\", \"failing\", \"error\", \"exception\", \"not working\"]) * 1.0;\n scores.refactor += hits(t, [\"refactor\", \"clean up\", \"cleanup\", \"rename\", \"extract\", \"simplify\", \"dead code\", \"tidy\", \"reorganize\", \"restructure\"]) * 1.0;\n scores.docs += hits(t, [\"readme\", \"document\", \"documentation\", \"changelog\", \"docstring\", \"comment\", \"write docs\", \"usage guide\"]) * 1.0;\n scores.ops += hits(t, [\"deploy\", \"docker\", \"pipeline\", \"ci/cd\", \"ci\", \"systemd\", \"infra\", \"kubernetes\", \"terraform\", \"release\", \"rollback\", \"container\"]) * 1.0;\n scores.review += hits(t, [\"review\", \"audit\", \"inspect\", \"look over\", \"feedback on\"]) * 1.0;\n // \"new \" dropped — too noisy (\"the new X\", \"new file\"); \"new feature\" stays as a strong signal.\n scores.feature += hits(t, [\"add\", \"implement\", \"build\", \"create\", \"feature\", \"support for\", \"new feature\"]) * 0.8;\n\n // Structural evidence (tools + files), scaled so it complements but doesn't drown keywords.\n const docFiles = s.loc.files > 0 ? countMatch(files, DOC_EXT) : 0;\n const opsFiles = s.loc.files > 0 ? countMatch(files, OPS_FILE) : 0;\n if (s.loc.files > 0) {\n scores.docs += 3 * (docFiles / s.loc.files);\n scores.ops += 3 * (opsFiles / s.loc.files);\n }\n // Edit-heavy + low net churn relative to gross churn ⇒ rework, not new code.\n if (writes >= 3 && s.loc.churn > 0 && Math.abs(s.loc.net) / s.loc.churn < 0.34) scores.refactor += 1.5;\n // Edit-dominated (lots of Edits, few/no new Writes) ⇒ reworking existing code, not adding it.\n if (editN >= 4 && editN >= writeN * 2) scores.refactor += 1.0;\n // Read-dominated, little writing ⇒ review/exploration.\n if (readN >= 5 && writes <= 2) scores.review += 1.5;\n // New files written with net-positive LoC ⇒ feature work.\n if (writeN >= 1 && s.loc.net > 20) scores.feature += 1.5;\n\n // No signal at all ⇒ record the default choice so the argmax (in classify) picks it.\n const maxScore = Math.max(...CATEGORIES.map((c) => scores[c]));\n if (maxScore === 0) scores[writes > 0 ? \"feature\" : \"chore\"] += 0.01;\n return scores;\n}\n\n/** How many of the session's touched files match a pattern. */\nfunction countMatch(files: string[], re: RegExp): number {\n let n = 0;\n for (const f of files) if (re.test(f)) n++;\n return n;\n}\n\nfunction complexity(s: Signals): { score: number; subscores: Record<string, number>; weights: Record<string, number> } {\n // v2 ceilings: tuned to the real p90 of substantial main sessions (churn ~2.6k, files ~29,\n // work-tokens in the tens of millions, multi-hour durations) so a median session sits mid-range\n // instead of pegging every subscore at 1.0. See ADR-004.\n const weights = { loc: 0.25, files: 0.15, turns: 0.2, tokens: 0.2, duration: 0.1, subagents: 0.1 };\n const subscores = {\n loc: clamp01(Math.log1p(s.loc.churn) / Math.log1p(6000)),\n files: clamp01(s.loc.files / 40),\n turns: clamp01(s.turn_count / 40),\n tokens: clamp01(Math.log1p(s.work_tokens) / Math.log1p(40_000_000)),\n duration: clamp01(s.duration_ms / (600 * 60_000)),\n subagents: clamp01(s.subagent_count / 10),\n };\n let acc = 0;\n for (const k of Object.keys(weights) as Array<keyof typeof weights>) acc += weights[k] * subscores[k];\n return { score: Math.round(acc * 1000) / 10, subscores, weights };\n}\n\ninterface SessionAgg {\n id: string;\n is_sidechain: number;\n turn_count: number;\n event_count: number;\n duration_ms: number | null;\n}\n\n/**\n * (Re)classify every session into the `classifications` table. Returns the row count written.\n * One transaction; deterministic; safe to run repeatedly (ON CONFLICT upsert).\n */\nexport function classify(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n // Incremental: classify only the sessions touched this run (the expanded set rebuildDerived returns).\n // Every signal bulk-load is scoped to the same set so memory and work track the delta, not the whole\n // DB (ADR-010). `null`/undefined → classify all sessions (the `--full` / standalone metrics path).\n const incremental = dirty != null;\n if (dirty != null) createDirtySet(db, \"_dirty\", dirty);\n const wSess = incremental ? \" WHERE session_id IN (SELECT id FROM _dirty)\" : \"\";\n const aSess = incremental ? \" AND session_id IN (SELECT id FROM _dirty)\" : \"\";\n\n const sessions = db\n .prepare(`SELECT id, is_sidechain, turn_count, event_count, duration_ms FROM sessions${incremental ? \" WHERE id IN (SELECT id FROM _dirty)\" : \"\"}`)\n .all() as unknown as SessionAgg[];\n\n // Bulk-load per-session signals once, then assemble in JS (scales to many sessions).\n const toolMix = new Map<string, Record<string, number>>();\n for (const r of db.prepare(`SELECT session_id, tool_name, COUNT(*) c FROM tool_calls${wSess} GROUP BY session_id, tool_name`).all() as Array<{ session_id: string; tool_name: string; c: number }>) {\n const m = toolMix.get(r.session_id) ?? {};\n m[r.tool_name] = r.c;\n toolMix.set(r.session_id, m);\n }\n const skillMix = new Map<string, Record<string, number>>();\n 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() as Array<{ session_id: string; skill_name: string; c: number }>) {\n const m = skillMix.get(r.session_id) ?? {};\n m[r.skill_name] = r.c;\n skillMix.set(r.session_id, m);\n }\n const tokens = new Map<string, { work: number; cacheRead: number }>();\n for (const r of db\n .prepare(\n `SELECT session_id, SUM(input_tokens) i, SUM(output_tokens) o,\n SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr\n FROM token_usage${wSess} GROUP BY session_id`,\n )\n .all() as Array<{ session_id: string; i: number | null; o: number | null; cw: number | null; cr: number | null }>) {\n tokens.set(r.session_id, { work: (r.i ?? 0) + (r.o ?? 0) + (r.cw ?? 0), cacheRead: r.cr ?? 0 });\n }\n // LoC + files from Edit/Write inputs.\n const loc = new Map<string, { added: number; removed: number; files: Set<string> }>();\n for (const r of db.prepare(`SELECT session_id, tool_name, input_json FROM tool_calls WHERE tool_name IN ('Edit','Write')${aSess}`).all() as Array<{ session_id: string; tool_name: string; input_json: string | null }>) {\n const d = locDelta(r.tool_name, r.input_json);\n const acc = loc.get(r.session_id) ?? { added: 0, removed: 0, files: new Set<string>() };\n acc.added += d.added;\n acc.removed += d.removed;\n if (d.file) acc.files.add(d.file);\n loc.set(r.session_id, acc);\n }\n // Prompt text for keyword signals.\n const promptText = new Map<string, string>();\n for (const r of db.prepare(`SELECT session_id, prompt_preview FROM turns WHERE prompt_preview IS NOT NULL${aSess}`).all() as Array<{ session_id: string; prompt_preview: string }>) {\n promptText.set(r.session_id, (promptText.get(r.session_id) ?? \"\") + \" \" + r.prompt_preview);\n }\n // Subagent role from the spawning Task/Agent tool_call (schema-v3 linkage). Lets us categorize a\n // sidechain session by what it was spawned to do, rather than keyword-matching its transcript.\n const subagentRole = new Map<string, string>();\n for (const r of db\n .prepare(\n `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)\" : \"\"}`,\n )\n .all() as Array<{ id: string; role: string }>) {\n if (!subagentRole.has(r.id)) subagentRole.set(r.id, r.role);\n }\n\n const upsert = db.prepare(\n `INSERT INTO classifications (scope, target_id, category, complexity_score, complexity_band, signals_json, classifier_version)\n VALUES ('session', @target_id, @category, @complexity_score, @complexity_band, @signals_json, @classifier_version)\n ON CONFLICT(scope, target_id) DO UPDATE SET\n category=excluded.category, complexity_score=excluded.complexity_score,\n complexity_band=excluded.complexity_band, signals_json=excluded.signals_json,\n classifier_version=excluded.classifier_version`,\n );\n\n const tx = transaction(db, () => {\n for (const sess of sessions) {\n const toolCounts = toolMix.get(sess.id) ?? {};\n const skills = skillMix.get(sess.id) ?? {};\n const l = loc.get(sess.id) ?? { added: 0, removed: 0, files: new Set<string>() };\n const tok = tokens.get(sess.id) ?? { work: 0, cacheRead: 0 };\n const filesArr = [...l.files].sort();\n const subagentCount = (toolCounts[\"Agent\"] ?? 0) + (toolCounts[\"Task\"] ?? 0);\n\n const signals: Signals = {\n toolCounts,\n skills,\n loc: { added: l.added, removed: l.removed, net: l.added - l.removed, churn: l.added + l.removed, files: l.files.size },\n turn_count: sess.turn_count,\n event_count: sess.event_count,\n work_tokens: tok.work,\n cache_read_tokens: tok.cacheRead,\n duration_ms: sess.duration_ms ?? 0,\n subagent_count: subagentCount,\n is_sidechain: sess.is_sidechain,\n };\n\n const catScores = scoreCategories(signals, promptText.get(sess.id) ?? \"\", filesArr);\n let category: Category = \"chore\";\n let bestScore = -1;\n for (const c of CATEGORIES) {\n if (catScores[c] > bestScore) {\n bestScore = catScores[c];\n category = c;\n }\n }\n // For subagents, prefer the spawner's role (Explore/Plan/… ⇒ review) over keyword heuristics.\n const role = subagentRole.get(sess.id);\n if (sess.is_sidechain) {\n const roleCat = categoryForSubagentRole(role);\n if (roleCat) category = roleCat;\n }\n const cx = complexity(signals);\n\n // Deterministic, explainable signal blob (stable key order; the file list rides as `files`).\n const signalsJson = JSON.stringify({\n tool_counts: toolCounts,\n skills,\n loc: signals.loc,\n files: filesArr,\n turn_count: signals.turn_count,\n event_count: signals.event_count,\n work_tokens: signals.work_tokens,\n cache_read_tokens: signals.cache_read_tokens,\n duration_ms: signals.duration_ms,\n subagent_count: signals.subagent_count,\n is_sidechain: signals.is_sidechain,\n subagent_role: role ?? null,\n category_scores: catScores,\n complexity_subscores: cx.subscores,\n complexity_weights: cx.weights,\n classifier_version: CLASSIFIER_VERSION,\n });\n\n upsert.run({\n target_id: sess.id,\n category,\n complexity_score: cx.score,\n complexity_band: band(cx.score),\n signals_json: signalsJson,\n classifier_version: CLASSIFIER_VERSION,\n });\n }\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty\");\n return { count: sessions.length, version: CLASSIFIER_VERSION };\n}\n","/**\n * Agent Lens — security findings (ADR-017). No AI: risky operations are flagged deterministically\n * by a rule engine over signals already in the DB (each tool call's verbatim `input_json` /\n * `result_summary` / `status`, plus the session's project path). Re-runnable and idempotent — the\n * same DB always yields identical rows. Every matched pattern and context modifier is recorded in\n * `signals_json` so a finding can be explained and rules retuned without re-deriving meaning.\n *\n * agent-lens is a *retrospective* analyzer, so this is forensic awareness — surfacing that an agent\n * *did* something risky after the fact — not runtime prevention. Findings are 0..N per tool call, so\n * an incremental re-run DELETEs the touched sessions' rows then re-INSERTs (delete-then-insert),\n * unlike the 1-per-session `classifications` upsert. `detector_version` lets a future (e.g. local-LLM)\n * engine supersede these rows.\n */\nimport { createHash } from \"node:crypto\";\nimport { posix as path } from \"node:path\";\nimport { transaction, bumpSeverity, type Severity, type SecurityCategoryKey } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\nimport { parseStored } from \"./storedjson.js\";\n\n// Bump on any rule/severity change so a re-run is attributable to an engine version (mirrors\n// CLASSIFIER_VERSION). Recorded on every row + in signals_json. Finding ids are independent of this\n// version, so a bump doesn't invalidate user triage state. The per-version rationale (what each bump\n// fixed/added and why) lives in docs/DETECTOR-CHANGELOG.md — keep the history there, not inline.\nexport const DETECTOR_VERSION = 8;\n\n// Severity model + category taxonomy are shared from core (packages/core/src/security.ts) so the\n// server and web agree on ordering and keys. Re-exported for tests that drive this module directly.\nexport { bumpSeverity, type Severity };\ntype Category = SecurityCategoryKey;\n\n/** Truncate an evidence snippet so we never dump wholesale secrets/output into the DB. */\nfunction evidence(s: string, max = 200): string {\n const one = s.replace(/\\s+/g, \" \").trim();\n return one.length > max ? one.slice(0, max - 1) + \"…\" : one;\n}\n\n/** Deterministic finding id — stable across runs so re-detection reproduces identical rows. */\nfunction findingId(toolCallId: string, ruleId: string): string {\n return createHash(\"sha1\").update(toolCallId).update(\"\\0\").update(ruleId).digest(\"hex\").slice(0, 16);\n}\n\nexport interface RuleContext {\n toolName: string;\n command: string | null; // Bash command string, verbatim (for evidence + value scanning)\n commandCode: string | null; // executed view, quotes INTACT (comments/heredoc/echo-output neutralized) — for sql_drop\n commandBare: string | null; // commandCode + quoted-string contents blanked — for command-verb rules (sudo, rm, …)\n filePath: string | null; // Read/Write/Edit file_path, if any\n input: any; // parsed input_json (may be null)\n resultSummary: string | null;\n status: string | null; // success | error | ...\n projectPath: string | null; // the session's cwd, for outside-project checks\n ownedConfigDirs: string[]; // agent config roots (from the `sources` table) the agent legitimately owns\n}\n\nexport interface RuleMatch {\n evidence: string; // the offending snippet\n severity?: Severity; // override the rule's baseSeverity (context-driven escalation)\n mods?: Record<string, unknown>; // extra explainability recorded in signals_json\n}\n\ninterface Rule {\n id: string;\n category: Category;\n framework_ref: string;\n title: string;\n tools: string[] | \"*\"; // tool_names this rule applies to\n baseSeverity: Severity;\n test(ctx: RuleContext): RuleMatch | null;\n}\n\n// ---- Shared matchers ------------------------------------------------------\n\n// A path we consider outside the agent's working project (home dotfiles, system dirs, root).\nconst SYSTEM_PATH = /(^|\\s)(\\/etc\\/|\\/usr\\/|\\/bin\\/|\\/sbin\\/|\\/boot\\/|\\/var\\/|\\/root\\/|~\\/\\.ssh\\/|~\\/\\.aws\\/)/;\nconst HOME_OR_ROOT_TARGET = /(\\s|^)(~\\/?|\\/|\\/\\*|\\$HOME\\/?|\\*)(\\s|$)/;\n\n/** Secret-bearing file locations (matched against a file path or a shell command). */\nconst SECRET_FILE =\n /(^|\\/|\\s)(\\.env(\\.[\\w.-]+)?|id_rsa|id_ed25519|id_ecdsa|\\.ssh\\/[\\w.-]+|\\.aws\\/credentials|\\.gcp\\/|gcloud\\/|\\.npmrc|\\.pypirc|\\.netrc|\\.pem|\\.p12|\\.pfx|secrets?\\.(ya?ml|json|txt)|credentials(\\.json)?)\\b/i;\n\n// Non-secret config *templates* (e.g. .env.example, secrets.sample.yaml) — reading these is expected\n// and carries no real secret, so exclude them from credential-access even though they match SECRET_FILE.\n// The trailing (?![\\w-]) matches the end of the \".example\" token whether the token ends the string\n// (a Read filePath) or is followed by more shell text (`… .env.example | head`) — a plain `$` anchor\n// only handled the former and let the Bash-command case slip through.\nconst EXAMPLE_FILE = /\\.(example|sample|template|dist)(?![\\w-])/i;\n\n// Commands that read file *contents* — the actual credential-access signal. Listing/stat commands\n// (ls, file, stat, find, …) are deliberately absent: naming a secret path in an `ls`/`file` doesn't\n// read it, so they must not flag (only a content read does).\nconst CONTENT_READ_CMD = /\\b(cat|less|more|head|tail|bat|xxd|strings|base64|grep|egrep|rg|awk|sed|nl|tac|od|hexdump|cp|scp|sort)\\b/i;\n\n/** Secret-looking values (in a command or a captured result). */\nconst SECRET_VALUE_PATTERNS: Array<{ re: RegExp; label: string; critical?: boolean }> = [\n { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, label: \"private-key\", critical: true },\n { re: /\\bAKIA[0-9A-Z]{16}\\b/, label: \"aws-access-key-id\" },\n { re: /\\bASIA[0-9A-Z]{16}\\b/, label: \"aws-temp-key-id\" },\n { re: /\\bgh[pousr]_[A-Za-z0-9]{30,}\\b/, label: \"github-token\" },\n { re: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/, label: \"slack-token\" },\n { re: /\\bsk-[A-Za-z0-9]{20,}\\b/, label: \"api-secret-key\" },\n];\n\nconst isBash = (t: string) => t === \"Bash\";\n\n// SQL keywords that irreversibly drop/empty a database object.\nconst SQL_DESTRUCTIVE = /\\b(DROP\\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\\s+TABLE)?)\\b/i;\n// Database-client command words. A DROP/TRUNCATE in a *Bash* command is only executed SQL when one of\n// these runs it; without a client the keyword is inert text (a `grep \"…\\|truncate\"` search pattern, an\n// echoed help string, a log line), so it must not flag. The structured input.query path needs no client.\nconst SQL_CLIENT = /\\b(psql|pgcli|mysql|mysqladmin|mariadb|sqlite3?|sqlcmd|usql|mongo|mongosh|clickhouse-client|cockroach|snowsql|bq)\\b/i;\n\n// ---- Command sanitization ------------------------------------------------\n// A Bash command is matched in three views, because \"the agent ran X\" and \"the text X appears\" are\n// different questions. Rules pick the view their question needs (RuleContext documents which):\n//\n// raw — verbatim. Value scanning (secret_in_data) and host classification (netTargetScope) use it:\n// a secret is exposed, and a URL is a destination, whether or not the text was \"executed\".\n// code — the EXECUTED view, quotes INTACT. Comments, heredoc bodies and echo/printf output are\n// neutralized, but quoted strings survive because `psql -c \"DROP …\"` and `sh -c \"…\"` carry\n// real executed code inside quotes.\n// bare — code, plus the CONTENTS of quoted strings blanked. Command-VERB rules (sudo, rm -rf, …)\n// use it: a dangerous token inside a quoted *argument* (`grep \"sudo\"`, `git commit -m \"…\n// rm -rf …\"`) is data passed to a program, not a command the shell runs.\n//\n// Each stage below is one regex pass; `commandViews` is the only place their ORDER is decided, and\n// the order matters (see the stage comments). packages/ingest/test/sanitize.test.ts pins every stage\n// individually, including the holes we knowingly accept — this is regex-as-tokenizer and a real\n// parser is out of scope, so the tests are the contract.\n\n/** `# …` never runs. (Hole: a `#` inside a quoted string reads as a comment.) */\nexport const stripComments = (c: string): string => c.replace(/(^|\\s)#[^\\n]*/g, \"$1\");\n\n/** The print builtins' arguments are data sent to stdout, so `sudo`/`rm -rf`/… inside them is inert.\n * Quoted arguments are consumed as whole units, so a separator (`;` `|`) inside printed text never\n * leaks out as a fake command break. */\nexport const neutralizeEcho = (c: string): string =>\n c.replace(/((?:^|[;&|\\n(])\\s*)(?:echo|printf)\\b(?:\"(?:[^\"\\\\]|\\\\.)*\"|'[^']*'|[^;&|\\n])*/gi, \"$1echo\");\n\n/** A heredoc body (`cmd <<EOF … EOF`) is data written to a file/stdin, not commands the shell runs —\n * unless the resulting file is then executed, which execGeneratedScript catches separately. Runs\n * FIRST, so a `#` inside a body can't turn the terminator line into a comment. */\nexport const blankHeredoc = (c: string): string => c.replace(/<<-?\\s*(['\"]?)(\\w+)\\1[\\s\\S]*?\\n\\s*\\2\\b/g, \"<<HEREDOC\");\n\n/** Empty every quoted string literal, keeping the quotes. Runs LAST, on `code`, so the in-string\n * separators are already gone from the parts being kept. (Hole: a command truly executed via\n * `sh -c \"sudo …\"` disappears from this view; that inline form is rare and worth the far fewer\n * false positives.) */\nexport const blankQuoted = (c: string): string => c.replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"').replace(/'[^']*'/g, \"''\");\n\nexport const codeOf = (c: string): string => neutralizeEcho(stripComments(blankHeredoc(c)));\nexport const bareOf = (c: string): string => blankQuoted(codeOf(c));\n\n/** The two derived views of one command, built together — the single place the pipeline is assembled. */\nexport function commandViews(raw: string): { code: string; bare: string } {\n const code = codeOf(raw);\n return { code, bare: blankQuoted(code) };\n}\n\n// A file is \"executed\" when passed to an interpreter (sh/bash/…), sourced (`source`/`.`), or invoked\n// as a path (`./f`, `/abs/f`). Detects the write-a-script-then-run-it pattern (`echo … > f.sh; sh f.sh`)\n// that turns otherwise-inert printed text into live code. Returns the offending file, or null.\nfunction execGeneratedScript(cmd: string): string | null {\n const targets = new Set<string>();\n for (const m of cmd.matchAll(/(?:>>?|\\btee\\b\\s+(?:-a\\s+)?)\\s*[\"']?([A-Za-z0-9._/~-]+)/g)) {\n if (m[1] && !m[1].startsWith(\"/dev/\")) targets.add(m[1]);\n }\n for (const t of targets) {\n const e = t.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n // Interpreter/source invocation: `sh FILE`, `bash FILE`, `source FILE`, `. FILE` (the interpreter is\n // the command word, so FILE may sit anywhere after it).\n const interp = new RegExp(`\\\\b(?:sh|bash|zsh|ksh|dash|source)\\\\s+[\"']?${e}\\\\b|(?:^|[\\\\n;|&(])\\\\s*\\\\.\\\\s+[\"']?${e}\\\\b`);\n // FILE run directly — must be at COMMAND POSITION (start or after a separator), NOT an argument slot,\n // otherwise the redirect/`tee FILE` target matches its own occurrence (`… | tee /etc/hosts`).\n const hasPath = /^(\\.\\/|\\/|~\\/)/.test(t);\n const path = hasPath\n ? new RegExp(`(?:^|[\\\\n;|&(])\\\\s*${e}(?:\\\\s|$|[;&|<>])`)\n : new RegExp(`(?:^|[\\\\n;|&(])\\\\s*\\\\.\\\\/${e}\\\\b`);\n if (interp.test(cmd) || path.test(cmd)) return t;\n }\n return null;\n}\n\n// Temp dirs (including the per-session scratchpad under /tmp/…) are always agent-owned regardless of\n// which agent/config produced them.\nconst TEMP_PATH = /^(\\/tmp\\/|\\/var\\/folders\\/|\\/private\\/var\\/folders\\/)/;\n\n/**\n * Paths the agent legitimately owns, so writing there is expected, not a finding: its own config/work\n * dir (e.g. plans, memory, todos, projects, settings, skills) and temp dirs. The config roots are the\n * `config_dir`s of the configured sources (the `sources` table, seeded from the project config file),\n * NOT a hardcoded `.claude` pattern — that way side-by-side installs (~/.claude, ~/.claude-isf, …) and\n * any relocated config dir are covered from a single source of truth. Neutralizes the biggest\n * write_outside_project false-positive source.\n */\nfunction isAgentOwnedPath(p: string, configDirs: string[]): boolean {\n if (TEMP_PATH.test(p)) return true;\n if (configDirs.some((d) => p === d || p.startsWith(d + \"/\"))) return true;\n const tmp = process.env.TMPDIR;\n return !!tmp && p.startsWith(tmp.replace(/\\/$/, \"\") + \"/\");\n}\n\n/**\n * If a write target lands outside the session's project directory, return its resolved absolute path;\n * else null. Handles two escapes: an absolute path already outside the project, and a RELATIVE path that\n * climbs out via `../` (resolved against the project root and posix-normalized — `../../../etc/x` →\n * `/etc/x`). A relative path with no `../` is project-local; a `../` that normalizes back inside the\n * project is not an escape. Agent-owned dirs (config/work + temp) and an unknown project never flag.\n * Returning the resolved path lets the caller run SYSTEM_PATH severity on where the write actually lands.\n */\nfunction outsideProjectPath(filePath: string | null, projectPath: string | null, configDirs: string[]): string | null {\n if (!filePath) return null;\n let abs = filePath;\n if (!filePath.startsWith(\"/\")) {\n if (!projectPath || !filePath.includes(\"../\")) return null; // relative & no traversal → project-local\n abs = path.normalize(projectPath.replace(/\\/$/, \"\") + \"/\" + filePath);\n }\n if (isAgentOwnedPath(abs, configDirs)) return null; // agent's own config/work dir + temp → expected\n if (!projectPath) return null; // unknown project → don't guess\n const root = projectPath.replace(/\\/$/, \"\");\n return abs !== root && !abs.startsWith(root + \"/\") ? abs : null;\n}\n\n// Classify the destination host(s) of a curl/wget command for exfil scoring. Runs on the VERBATIM\n// command (not commandBare) so quoted URLs are visible, and strips -H/--header args so a URL sitting\n// in an Origin/Referer header is never mistaken for the request target. Returns the most-severe scope\n// present: external > internal > loopback; \"unknown\" when no host is recognizable (e.g. the target is\n// a shell variable) — treated as external-ish so we don't under-report an unprovable destination.\ntype NetScope = \"external\" | \"internal\" | \"loopback\" | \"unknown\";\nconst SCOPE_RANK: Record<NetScope, number> = { unknown: 0, loopback: 1, internal: 2, external: 3 };\nfunction netTargetScope(rawCommand: string): NetScope {\n const s = rawCommand\n .replace(/(?:-H|--header)\\s+(['\"]).*?\\1/gi, \" \") // quoted header value (Origin/Referer/…)\n .replace(/(?:-H|--header)\\s+[^\\s'\"]+/gi, \" \"); // bare header token\n const authorities: string[] = [];\n // scheme URLs: http(s)://host (host may be a bracketed IPv6 literal)\n for (const m of s.matchAll(/\\bhttps?:\\/\\/(\\[[0-9a-fA-F:]+\\]|[^/\\s\"'<>|]+)/gi)) authorities.push(m[1]);\n // bare host:port targets (curl localhost:4477, curl 10.0.0.5:8080) — require a numeric port so we\n // never mistake a file path or a `key:value` token for a network destination.\n for (const m of s.matchAll(/(?:^|[\\s\"'=(])(\\[[0-9a-fA-F:]+\\]|[A-Za-z0-9.-]+):\\d{2,5}(?=[/\\s\"']|$)/g)) authorities.push(m[1]);\n let scope: NetScope = \"unknown\";\n for (let host of authorities) {\n host = host.replace(/^\\[|\\]$/g, \"\").replace(/^[^@/]*@/, \"\").toLowerCase(); // drop brackets + userinfo\n if (!host.includes(\"::\")) host = host.replace(/:\\d+$/, \"\"); // drop :port (but not the ::1 in IPv6)\n let hs: NetScope;\n if (host === \"localhost\" || /^127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host) || host === \"0.0.0.0\" || host === \"::1\")\n hs = \"loopback\";\n else if (\n /^10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/.test(host) || // RFC1918\n /^192\\.168\\.\\d{1,3}\\.\\d{1,3}$/.test(host) ||\n /^172\\.(1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}$/.test(host) ||\n /^169\\.254\\.\\d{1,3}\\.\\d{1,3}$/.test(host) || // link-local\n host.endsWith(\".local\") ||\n host.endsWith(\".internal\") ||\n !host.includes(\".\") // single-label host (a service name, not a public domain)\n )\n hs = \"internal\";\n else hs = \"external\";\n if (SCOPE_RANK[hs] > SCOPE_RANK[scope]) scope = hs;\n }\n return scope;\n}\n\n/**\n * The commonest rule shape, as a factory: match ONE regex against the command-word view\n * (`bare` — quoted arguments blanked, comments/heredocs/echo output neutralized) and, on a hit, report\n * the VERBATIM command as evidence at the rule's base severity. Nothing context-dependent: a rule that\n * escalates on its target, inspects `input`, or splits the command into segments stays a full `test`.\n *\n * `tools` defaults to Bash, the only tool that carries a command; the few rules that declare \"*\" keep\n * doing so (they self-limit by requiring `commandBare`).\n */\nfunction simpleCommandRule(spec: {\n id: string;\n category: Category;\n framework_ref: string;\n title: string;\n baseSeverity: Severity;\n /** Matched against ctx.commandBare. */\n match: RegExp;\n tools?: string[] | \"*\";\n}): Rule {\n const { match, tools = [\"Bash\"], ...meta } = spec;\n return {\n ...meta,\n tools,\n test(ctx) {\n const c = ctx.commandBare;\n if (!c || !match.test(c)) return null;\n return { evidence: evidence(ctx.command!) };\n },\n };\n}\n\n// ---- The rule set (v1) ----------------------------------------------------\n// Representative, not exhaustive; grouped by framework-anchored category. New rules just append here.\n\nconst RULES: Rule[] = [\n // --- Destructive / data-loss (OWASP ASI02 Tool Misuse) -------------------\n {\n id: \"destructive.rm_rf\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Recursive force delete (rm -rf)\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n if (!ctx.commandBare) return null;\n // `git rm` (even `git rm -rf`) removes *tracked* files and is recoverable from git history — it's\n // not a filesystem `rm -rf`. Neutralize it (glued to `git` so the `rm` word boundary is gone) so it\n // never matches, while a real `rm -rf` elsewhere in the same command still does.\n const c = ctx.commandBare.replace(/\\bgit\\s+rm\\b/gi, \"gitrm\");\n // Isolate the `rm` command's own segment (up to the next |;&\\n) so flags/targets from a later\n // command don't leak in.\n const rmSeg = c.match(/\\brm\\b[^|;&\\n]*/i)?.[0];\n if (!rmSeg) return null;\n // rm needs both a recursive and a force flag. Flag tokens are matched as whitespace-bounded\n // letter-only clusters (`-rf`, `-fr`, `-r -f`, `--recursive --force`) so a path token that merely\n // contains an f before an r — e.g. `reports/…-software-engineer…` — isn't mistaken for `-fr`.\n const combined = /(?:^|\\s)-(?=[a-zA-Z]*[rR])(?=[a-zA-Z]*f)[a-zA-Z]+(?=\\s|$)/.test(rmSeg);\n const recursive = /(?:^|\\s)(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)(?=\\s|$)/.test(rmSeg);\n const force = /(?:^|\\s)(?:-[a-zA-Z]*f[a-zA-Z]*|--force)(?=\\s|$)/.test(rmSeg);\n if (!combined && !(recursive && force)) return null;\n const dangerousTarget = HOME_OR_ROOT_TARGET.test(c) || /\\s\\/(?:\\s|$)/.test(c) || /\\brm\\b[^|;&\\n]*\\*/.test(c);\n // Every non-flag target under a temp dir → routine scratch cleanup, demote to low.\n const targets = rmSeg.split(/\\s+/).slice(1).filter((a) => a && !a.startsWith(\"-\"));\n const tempOnly = targets.length > 0 && targets.every((t) => TEMP_PATH.test(t));\n const severity: Severity = dangerousTarget ? \"critical\" : tempOnly ? \"low\" : \"high\";\n return { evidence: evidence(ctx.command!), severity, mods: { dangerous_target: dangerousTarget, temp_target: tempOnly } };\n },\n },\n simpleCommandRule({\n id: \"destructive.git_reset_hard\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Discards local changes (git reset --hard)\",\n baseSeverity: \"low\", // routine dev op — only uncommitted working-tree changes are lost; commits survive in reflog\n match: /\\bgit\\s+reset\\s+--hard\\b/i,\n }),\n {\n id: \"destructive.git_force_push\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Force-push rewrites remote history\",\n tools: [\"Bash\"],\n baseSeverity: \"low\", // feature-branch force-push is routine; only a protected branch (below) is high\n test(ctx) {\n const c = ctx.commandBare;\n if (!c || !/\\bgit\\s+push\\b[^|;&\\n]*(--force\\b|--force-with-lease\\b|\\s-f\\b)/i.test(c)) return null;\n const mainBranch = /\\b(main|master|release)\\b/.test(c);\n return { evidence: evidence(ctx.command!), severity: mainBranch ? \"high\" : \"low\", mods: { protected_branch: mainBranch } };\n },\n },\n {\n id: \"destructive.sql_drop\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Drops or truncates a database object\",\n tools: \"*\",\n baseSeverity: \"high\",\n test(ctx) {\n const query = typeof ctx.input?.query === \"string\" ? ctx.input.query : null;\n // A structured DB tool call (input.query) is SQL by definition — flag on the keyword alone.\n if (query && SQL_DESTRUCTIVE.test(query)) return { evidence: evidence(ctx.command ?? query) };\n // A Bash command only executes SQL when a database client runs it; otherwise DROP/TRUNCATE is just\n // text (e.g. a `grep \"…\\|truncate\"` search pattern) and inert, so require both a client and the keyword.\n const code = ctx.commandCode;\n if (code && SQL_CLIENT.test(code) && SQL_DESTRUCTIVE.test(code)) return { evidence: evidence(ctx.command!) };\n return null;\n },\n },\n simpleCommandRule({\n id: \"destructive.disk_wipe\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Low-level disk write / wipe (dd, mkfs, shred)\",\n baseSeverity: \"high\",\n match: /(\\bdd\\s+if=|\\bmkfs(\\.\\w+)?\\b|\\bshred\\b)/i,\n }),\n {\n id: \"destructive.overwrite_critical\",\n category: \"destructive\",\n framework_ref: \"OWASP ASI02\",\n title: \"Overwrites a critical project file\",\n tools: [\"Write\", \"Edit\", \"MultiEdit\"],\n baseSeverity: \"low\",\n test(ctx) {\n const f = ctx.filePath;\n if (!f) return null;\n const lock = /(^|\\/)(package-lock\\.json|pnpm-lock\\.ya?ml|yarn\\.lock|Cargo\\.lock|go\\.sum)\\b/.test(f);\n // A CI-pipeline file is more sensitive than a lockfile: modifying it can run arbitrary code or\n // exfiltrate secrets in CI (a supply-chain risk), so it stays medium while lockfile churn is low.\n const ci = /(^|\\/)(\\.github\\/workflows\\/|\\.gitlab-ci\\.ya?ml)\\b/.test(f);\n if (!lock && !ci) return null;\n return { evidence: evidence(f), severity: ci ? \"medium\" : \"low\", mods: { kind: ci ? \"ci-config\" : \"lockfile\" } };\n },\n },\n\n // --- Credential & secret access ------------------------------------------\n {\n id: \"credential.secret_file_access\",\n category: \"credential-access\",\n framework_ref: \"MITRE ATLAS: Credential Access\",\n title: \"Reads a secret/credential file\",\n tools: [\"Read\", \"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n if (isBash(ctx.toolName)) {\n const cmd = ctx.commandBare; // executed view: a secret named only in a comment / echo / quoted arg isn't read\n if (!cmd) return null;\n // Evaluate each pipeline/command segment independently: a secret file is only *read* when a\n // content-read command (cat/grep/…) and the secret path occur in the SAME segment. A bare\n // `ls`/`file`/`stat` that merely names the path lists metadata; piping its OUTPUT into a\n // `grep`/`head` in a later segment (e.g. `file ~/.ssh/id_* | grep -v .pub`) exposes no secret\n // content — the read command never touches the file. Splitting first avoids matching the\n // read command against the wrong segment.\n const segments = cmd.split(/\\|\\||&&|[|;\\n]/);\n const reads = segments.some(\n (seg) => SECRET_FILE.test(seg) && !EXAMPLE_FILE.test(seg) && CONTENT_READ_CMD.test(seg),\n );\n return reads ? { evidence: evidence(ctx.command!) } : null;\n }\n // Read tool: the filePath itself is the target (no command to segment).\n const f = ctx.filePath;\n if (!f || !SECRET_FILE.test(f) || EXAMPLE_FILE.test(f)) return null; // skip .env.example & templates\n return { evidence: evidence(f) };\n },\n },\n {\n id: \"credential.secret_in_data\",\n category: \"credential-access\",\n framework_ref: \"MITRE ATLAS: Credential Access\",\n title: \"Secret-looking value in command or output\",\n tools: \"*\",\n baseSeverity: \"high\",\n test(ctx) {\n const hay = [ctx.command, ctx.resultSummary].filter(Boolean).join(\"\\n\");\n if (!hay) return null;\n for (const p of SECRET_VALUE_PATTERNS) {\n const m = p.re.exec(hay);\n if (m) return { evidence: `${p.label}: ${evidence(m[0], 40)}`, severity: p.critical ? \"critical\" : \"high\", mods: { secret_kind: p.label } };\n }\n return null;\n },\n },\n\n // --- Data exfiltration (MITRE ATLAS AML.T0086) ---------------------------\n {\n id: \"exfil.pipe_to_network\",\n category: \"exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n title: \"Pipes local data into a network command\",\n tools: [\"Bash\"],\n baseSeverity: \"critical\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n // e.g. `cat secrets | curl …`, `tar … | nc host port`, `… | curl -d @-`\n if (!/(\\||<)\\s*(sudo\\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\\b/i.test(c)) return null;\n // A data-reading command (or a `<` stdin redirect) must be present on the source side. `<` sits\n // outside the \\b group because a word boundary against a non-word char never matches it there.\n if (!/(?:\\b(?:cat|tar|zip|gzip|base64|env|printenv)\\b|<)/i.test(c)) return null;\n return { evidence: evidence(ctx.command!) };\n },\n },\n {\n id: \"exfil.network_upload\",\n category: \"exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n title: \"Uploads data to a network host\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n // Upload-shaped curl/wget. NOTE: no /i flag — the short flags here are case-SENSITIVE (curl's -d\n // is data but -D is dump-header, -F is form but -f is --fail, -T is upload but -t is unrelated), so\n // matching case-insensitively turned plain GET smoke tests (`curl -sf`, `curl -D -`) into uploads.\n const upload = /\\b(curl|wget)\\b[^|;&\\n]*(-X\\s*POST|--data\\b|--data-\\w+\\b|\\s-d\\b|-T\\b|--upload-file\\b|-F\\b|--form\\b)/.test(c);\n if (!upload) return null;\n // `@file` / --upload-file / -T means a real file is being sent (matched on commandBare so an `@`\n // inside a quoted arg — e.g. an email in a header value — doesn't count as a file).\n const withFile = /(@[^\\s'\"]+|--upload-file|\\s-T\\b)/.test(c);\n // Score by destination: external host = high (critical with a file); a private/internal host is a\n // lesser concern (low); a loopback call is local IPC / a dev server (info). Sending an actual file\n // bumps the internal/loopback tiers one step. An unrecognizable target (shell var) can't be shown\n // local, so it stays high. Classified on the verbatim command so quoted URLs are visible.\n const scope = netTargetScope(ctx.command!);\n let severity: Severity;\n if (scope === \"external\") severity = withFile ? \"critical\" : \"high\";\n else if (scope === \"unknown\") severity = \"high\";\n else severity = withFile ? bumpSeverity(scope === \"loopback\" ? \"info\" : \"low\", 1) : scope === \"loopback\" ? \"info\" : \"low\";\n return {\n evidence: evidence(ctx.command!),\n severity,\n mods: { target_scope: scope, with_file: withFile },\n };\n },\n },\n simpleCommandRule({\n id: \"exfil.reverse_shell\",\n category: \"exfiltration\",\n framework_ref: \"MITRE ATLAS AML.T0086\",\n title: \"Netcat / reverse-shell style connection\",\n baseSeverity: \"critical\",\n match: /\\b(nc|ncat|netcat)\\b[^|;&\\n]*(-e\\b|-c\\b|\\d{2,5})|\\/dev\\/tcp\\/|bash\\s+-i\\b/i,\n }),\n\n // --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)\n simpleCommandRule({\n id: \"privilege.skip_permissions\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Bypasses the permission guardrail\",\n tools: \"*\",\n baseSeverity: \"high\",\n match: /--dangerously-skip-permissions\\b|--yolo\\b|--dangerously-skip\\b/i,\n }),\n {\n id: \"privilege.curl_pipe_shell\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Executes a remote script (curl | sh)\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n const m = /\\b(?:curl|wget)\\b[^|]*\\|\\s*(?:sudo\\s+)?(sh|bash|zsh|python\\d?|node)\\b([^|;&]*)/i.exec(c);\n if (!m) return null;\n // The danger is the interpreter executing the *downloaded body* piped to its stdin. A shell always\n // does. node/python execute piped stdin as a script only when NOT handed an inline program or\n // module — `curl … | node -e '…'`, `python -c '…'`, `python -m json.tool` just consume the output\n // as DATA (e.g. parsing an API response), which is not remote-script execution and must not flag.\n const interp = m[1].toLowerCase();\n const args = m[2] || \"\";\n if (/^(?:node|python)/.test(interp) && /(?:^|\\s)-(?:e|c|m|p)\\b|(?:^|\\s)--(?:eval|command|module|print)\\b/.test(args))\n return null;\n return { evidence: evidence(ctx.command!) };\n },\n },\n {\n id: \"obfuscated.decode_exec\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Decodes and executes obfuscated content (base64 -d | sh)\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const c = ctx.commandBare;\n if (!c) return null;\n // A decoder turns opaque/encoded text into live code; piping that into an interpreter runs it — a\n // decode-then-execute evasion primitive. Matched on the executed view so a decode string that is\n // only echoed/quoted/commented is already inert.\n const decoder = /\\b(base64\\s+(?:-d|-D|--decode)|xxd\\s+-r|openssl\\s+(?:base64\\s+-d|enc\\b[^|]*\\s-d\\b)|uudecode)\\b/i.exec(c);\n if (!decoder) return null;\n // The decoder must be UPSTREAM of a pipe INTO the interpreter (so its decoded stdout is executed).\n // Decoding to a file (`base64 -d f > out`) or a later, separately-run script (`; sh x`) has no pipe\n // from the decoder to the shell and must not flag — so we search only the text AFTER the decoder.\n const downstream = c.slice(decoder.index + decoder[0].length);\n const piped = /\\|\\s*(?:sudo\\s+)?(sh|bash|zsh|dash|ksh|python\\d?|node|perl|ruby)\\b([^|;&\\n]*)/i.exec(downstream);\n if (!piped) return null;\n const interp = piped[1].toLowerCase();\n const args = piped[2] || \"\";\n // node/python/perl/ruby handed an inline program (-e/-c/-m/-p) consume the piped output as DATA,\n // not as a script to execute — mirror curl_pipe_shell's carve-out.\n if (/^(?:node|python|perl|ruby)/.test(interp) && /(?:^|\\s)-(?:e|c|m|p)\\b|(?:^|\\s)--(?:eval|command|module|print)\\b/.test(args))\n return null;\n const sudo = /\\|\\s*sudo\\b/i.test(downstream);\n const d = decoder[0].toLowerCase();\n const decoderName = d.startsWith(\"base64\") ? \"base64\" : d.startsWith(\"xxd\") ? \"xxd\" : d.startsWith(\"openssl\") ? \"openssl\" : \"uudecode\";\n return { evidence: evidence(ctx.command!), severity: sudo ? \"critical\" : \"high\", mods: { decoder: decoderName, interpreter: interp } };\n },\n },\n simpleCommandRule({\n id: \"privilege.sudo\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Runs a command as root (sudo)\",\n baseSeverity: \"high\", // root escalation removes a real safety barrier — higher than an in-project op\n // sudo must be in *command position* — start of the command or right after a separator (; | && ( ).\n // This ignores `sudo` as an argument (e.g. `apt install sudo`) and, via codeOf, inside echo/comments.\n match: /(?:^|[\\n;|&(])\\s*sudo\\b/i,\n }),\n {\n id: \"privilege.exec_generated_script\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Writes a shell script from inline output and executes it\",\n tools: [\"Bash\"],\n baseSeverity: \"high\",\n test(ctx) {\n const raw = ctx.command;\n if (!raw) return null;\n // Echoing a command into a file that is then run/sourced turns otherwise-inert printed text into\n // live code (e.g. `echo \"sudo rm -rf /\" > f.sh; sh f.sh`) — the reason we don't just ignore\n // dangerous tokens inside echo strings. Detect on the comment-stripped raw command.\n const file = execGeneratedScript(stripComments(raw));\n if (!file) return null;\n // Escalate when the generated script itself carries a destructive/privileged payload.\n const critical = /(?:^|[\"'\\s;|&(])sudo\\b|\\brm\\s+[^|;&\\n]*-[a-z]*r[a-z]*f|\\bchmod\\s+0?777\\b|\\bmkfs|\\bdd\\s+if=/i.test(raw);\n return { evidence: evidence(raw), severity: critical ? \"critical\" : \"high\", mods: { script_file: file } };\n },\n },\n simpleCommandRule({\n id: \"privilege.chmod_777\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Overly permissive permissions (chmod 777)\",\n baseSeverity: \"medium\",\n match: /\\bchmod\\s+(-[a-zA-Z]+\\s+)*0?777\\b/i,\n }),\n simpleCommandRule({\n id: \"privilege.persistence\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Installs a persistence mechanism\",\n baseSeverity: \"high\",\n match: /\\bcrontab\\s+-|\\bsystemctl\\s+enable\\b|\\blaunchctl\\s+load\\b|>>\\s*~\\/\\.(bashrc|zshrc|profile|bash_profile)\\b|\\/etc\\/(cron|systemd|rc\\.local)/i,\n }),\n {\n id: \"privilege.write_outside_project\",\n category: \"privilege-bypass\",\n framework_ref: \"OWASP LLM06\",\n title: \"Writes a file outside the working project\",\n tools: [\"Write\", \"Edit\", \"MultiEdit\"],\n baseSeverity: \"low\", // writing to /tmp, scratch dirs, adjacent repos is routine; only a system path (below) is high\n test(ctx) {\n const f = ctx.filePath;\n const abs = outsideProjectPath(f, ctx.projectPath, ctx.ownedConfigDirs);\n if (!abs) return null;\n const system = SYSTEM_PATH.test(abs); // severity keys off where the write LANDS (resolved), not the raw arg\n const traversal = !!f && !f.startsWith(\"/\") && f.includes(\"../\"); // escaped via a relative `../` climb\n return {\n evidence: evidence(f!),\n severity: system ? \"high\" : \"low\",\n mods: { project: ctx.projectPath, system_path: system, ...(traversal ? { traversal: true } : {}) },\n };\n },\n },\n];\n\n/** Rule → tool-name applicability. */\nfunction appliesTo(rule: Rule, toolName: string): boolean {\n return rule.tools === \"*\" || rule.tools.includes(toolName);\n}\n\n/** Parse a tool call's verbatim input_json into a RuleContext (mirrors classify.ts's locDelta parse). */\nfunction buildContext(\n row: { tool_name: string; input_json: string | null; result_summary: string | null; status: string | null },\n projectPath: string | null,\n ownedConfigDirs: string[],\n): RuleContext {\n const input: any = parseStored(row.input_json, \"detect: tool_calls.input_json\");\n const command = typeof input?.command === \"string\" ? input.command : null;\n const views = command != null ? commandViews(command) : null;\n const commandCode = views?.code ?? null;\n const commandBare = views?.bare ?? null;\n const filePath = typeof input?.file_path === \"string\" ? input.file_path : null;\n return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };\n}\n\ninterface ToolRow {\n id: string;\n session_id: string;\n event_uuid: string | null;\n turn_id: string | null;\n tool_name: string;\n input_json: string | null;\n result_summary: string | null;\n status: string | null;\n}\n\n/**\n * (Re)scan tool calls into the `findings` table. Returns the total finding count + engine version.\n * Deterministic; safe to run repeatedly. `dirty` (the expanded id set rebuildDerived returns) scopes an\n * incremental run to the touched sessions via delete-then-insert; null/undefined → scan everything.\n */\nexport function detect(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n const incremental = dirty != null;\n if (dirty != null) createDirtySet(db, \"_dirty_sec\", dirty);\n const scope = incremental ? \" WHERE tc.session_id IN (SELECT id FROM _dirty_sec)\" : \"\";\n\n // Agent-owned config roots — the configured sources' `config_dir`s (seeded from the project config\n // file at ingest). Single source of truth for the write_outside_project allowlist, so relocated or\n // side-by-side installs (~/.claude, ~/.claude-isf, …) are covered without a hardcoded path pattern.\n const ownedConfigDirs = (db.prepare(\"SELECT config_dir FROM sources WHERE config_dir IS NOT NULL\").all() as Array<{ config_dir: string }>)\n .map((r) => r.config_dir.replace(/\\/$/, \"\"))\n .filter(Boolean);\n\n // Project path per (scoped) session — for outside-project checks.\n const projPath = new Map<string, string>();\n for (const r of db\n .prepare(\n `SELECT s.id id, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${\n incremental ? \" WHERE s.id IN (SELECT id FROM _dirty_sec)\" : \"\"\n }`,\n )\n .all() as Array<{ id: string; path: string }>) {\n projPath.set(r.id, r.path);\n }\n\n const rows = db\n .prepare(\n `SELECT tc.id, tc.session_id, tc.event_uuid, tc.turn_id, tc.tool_name, tc.input_json, tc.result_summary, tc.status\n FROM tool_calls tc${scope}`,\n )\n .all() as unknown as ToolRow[];\n\n const insert = db.prepare(\n `INSERT INTO findings (id, session_id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json, detector_version)\n VALUES (@id, @session_id, @tool_call_id, @event_uuid, @turn_id, @rule_id, @category, @framework_ref, @severity, @title, @evidence, @signals_json, @detector_version)\n ON CONFLICT(id) DO NOTHING`,\n );\n const delScope = incremental\n ? \"DELETE FROM findings WHERE session_id IN (SELECT id FROM _dirty_sec)\"\n : \"DELETE FROM findings\";\n\n const tx = transaction(db, () => {\n db.exec(delScope);\n for (const row of rows) {\n const ctx = buildContext(row, projPath.get(row.session_id) ?? null, ownedConfigDirs);\n for (const rule of RULES) {\n if (!appliesTo(rule, ctx.toolName)) continue;\n const m = rule.test(ctx);\n if (!m) continue;\n let severity: Severity = m.severity ?? rule.baseSeverity;\n const mods: Record<string, unknown> = { ...(m.mods ?? {}) };\n // A failed/blocked tool call is an *attempt*, not an accomplished action — de-escalate one band.\n if (row.status === \"error\") {\n severity = bumpSeverity(severity, -1);\n mods.attempted = true;\n }\n const signals = {\n rule: rule.id,\n category: rule.category,\n framework_ref: rule.framework_ref,\n tool_name: ctx.toolName,\n base_severity: rule.baseSeverity,\n severity,\n status: row.status,\n modifiers: mods,\n detector_version: DETECTOR_VERSION,\n };\n insert.run({\n id: findingId(row.id, rule.id),\n session_id: row.session_id,\n tool_call_id: row.id,\n event_uuid: row.event_uuid,\n turn_id: row.turn_id,\n rule_id: rule.id,\n category: rule.category,\n framework_ref: rule.framework_ref,\n severity,\n title: rule.title,\n evidence: m.evidence,\n signals_json: JSON.stringify(signals),\n detector_version: DETECTOR_VERSION,\n });\n }\n }\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty_sec\");\n const count = (db.prepare(\"SELECT COUNT(*) n FROM findings\").get() as { n: number }).n;\n return { count, version: DETECTOR_VERSION };\n}\n","/**\n * Agent Lens — tool-error classification pass. Stamps `tool_calls.error_type` for every errored tool\n * call (`status='error'`) by running the deterministic core classifier (classifyToolError) over its\n * verbatim `result_summary`. Re-runnable and idempotent — like classify()/detect(), it re-derives from\n * signals already in the DB and yields identical rows every run.\n *\n * Why a stored column (not read-time): the sessions list filters by error type in SQL, and the\n * dashboard/detail aggregate on it — a column keeps one source of truth (the core classifier) while\n * letting SQL do the filtering/grouping. The raw `status='error'` count stays the authoritative signal;\n * `error_type` is the heuristic bucket (see packages/core/src/errors.ts for the authority boundary).\n */\nimport { transaction, classifyToolError, ERROR_CLASSIFIER_VERSION } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\n\n/**\n * (Re)classify errored tool calls into `error_type`. `dirty` (the expanded id set rebuildDerived\n * returns) scopes an incremental run to the touched sessions; null/undefined → reclassify everything.\n * Returns the number of rows stamped + the engine version.\n */\nexport function classifyErrors(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n const incremental = dirty != null;\n const scope = incremental ? \" AND session_id IN (SELECT id FROM _dirty_err)\" : \"\";\n\n if (dirty != null) createDirtySet(db, \"_dirty_err\", dirty);\n\n const rows = db\n .prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`)\n .all() as Array<{ id: string; result_summary: string | null }>;\n\n const update = db.prepare(\"UPDATE tool_calls SET error_type = ? WHERE id = ?\");\n const tx = transaction(db, () => {\n // Reset the scoped rows' error_type first so a row that stops being an error (or changes bucket) on\n // re-derive doesn't keep a stale label. Non-error rows always have NULL.\n db.exec(`UPDATE tool_calls SET error_type = NULL WHERE error_type IS NOT NULL${scope}`);\n for (const r of rows) update.run(classifyToolError(r.result_summary).type, r.id);\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty_err\");\n const count = (db.prepare(\"SELECT COUNT(*) n FROM tool_calls WHERE error_type IS NOT NULL\").get() as { n: number }).n;\n return { count, version: ERROR_CLASSIFIER_VERSION };\n}\n","/**\n * File-modification provenance (ADR-022, level 1) — derive `file_changes` rows from successful\n * Edit/Write/NotebookEdit tool calls' verbatim input_json.\n *\n * Deterministic and re-runnable, modeled on detect.ts: 0..N rows per tool call ⇒ incremental runs\n * delete-then-insert the dirty sessions' rows; null/undefined dirty ⇒ full rescan. Failed calls\n * (status='error') are skipped — a failed edit did not change the file. Only these tools are\n * covered: Bash-driven writes, deletions, and renames are deliberately out of scope here (they are\n * a lower confidence class; tracked-file deletions/renames arrive with the git-composed level 2 —\n * see the ADR).\n */\nimport { createHash } from \"node:crypto\";\nimport { posix as path } from \"node:path\";\nimport { transaction } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\n\nexport const FILECHANGES_VERSION = 1;\n\nconst FILE_TOOLS = [\"Edit\", \"Write\", \"NotebookEdit\"] as const;\n\n/** Deterministic row id — stable across runs so re-derivation reproduces identical rows. */\nfunction fileChangeId(toolCallId: string, filePath: string): string {\n return createHash(\"sha1\").update(toolCallId).update(\"\\0\").update(filePath).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Resolve a tool call's target path to a normalized absolute path, or null when unusable.\n * Absolute paths are posix-normalized (`/a/b/../c` → `/a/c`); relative paths resolve against the\n * session's project root when known (the same stance as detect.ts's outsideProjectPath) and are\n * skipped otherwise — a path we can't anchor would poison per-file grouping with false identities.\n */\nexport function normalizeFilePath(filePath: unknown, projectPath: string | null): string | null {\n if (typeof filePath !== \"string\" || !filePath) return null;\n if (filePath.startsWith(\"/\")) return path.normalize(filePath);\n if (!projectPath) return null;\n return path.normalize(projectPath.replace(/\\/$/, \"\") + \"/\" + filePath);\n}\n\n/** Count the lines a string spans (1 + newlines); 0 for the empty string. */\nfunction lineCount(s: unknown): number | null {\n if (typeof s !== \"string\") return null;\n if (s === \"\") return 0;\n let n = 1;\n for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) n++;\n return n;\n}\n\ninterface ToolRow {\n id: string;\n session_id: string;\n turn_id: string | null;\n event_uuid: string | null;\n tool_name: string;\n input_json: string | null;\n status: string | null;\n timestamp: string | null;\n}\n\n/**\n * (Re)derive tool-call file modifications into `file_changes`. Returns row count + version.\n * `dirty` (the expanded id set rebuildDerived returns) scopes an incremental run to the touched\n * sessions via delete-then-insert; null/undefined → derive everything.\n */\nexport function deriveFileChanges(db: DB, dirty?: Set<string> | null): { count: number; version: number } {\n const incremental = dirty != null;\n if (incremental) {\n db.exec(\"DROP TABLE IF EXISTS _dirty_fc\");\n db.exec(\"CREATE TEMP TABLE _dirty_fc (id TEXT PRIMARY KEY)\");\n const ins = db.prepare(\"INSERT OR IGNORE INTO _dirty_fc (id) VALUES (?)\");\n transaction(db, (ids: Iterable<string>) => {\n for (const id of ids) ins.run(id);\n })(dirty);\n }\n const scope = incremental ? \" AND tc.session_id IN (SELECT id FROM _dirty_fc)\" : \"\";\n\n // Project path + id per (scoped) session — to resolve relative paths and stamp project_id.\n const proj = new Map<string, { id: string; path: string }>();\n for (const r of db\n .prepare(\n `SELECT s.id id, p.id pid, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${\n incremental ? \" WHERE s.id IN (SELECT id FROM _dirty_fc)\" : \"\"\n }`,\n )\n .all() as Array<{ id: string; pid: string; path: string }>) {\n proj.set(r.id, { id: r.pid, path: r.path });\n }\n\n const rows = db\n .prepare(\n `SELECT tc.id, tc.session_id, tc.turn_id, tc.event_uuid, tc.tool_name, tc.input_json, tc.status, e.timestamp\n FROM tool_calls tc LEFT JOIN events e ON e.uuid = tc.event_uuid\n WHERE tc.tool_name IN (${FILE_TOOLS.map(() => \"?\").join(\",\")})${scope}`,\n )\n .all(...FILE_TOOLS) as unknown as ToolRow[];\n\n const insert = db.prepare(\n `INSERT INTO file_changes (id, tool_call_id, session_id, turn_id, event_uuid, project_id, file_path, tool_name, lines_added, lines_removed, timestamp, derive_version)\n VALUES (@id, @tool_call_id, @session_id, @turn_id, @event_uuid, @project_id, @file_path, @tool_name, @lines_added, @lines_removed, @timestamp, @derive_version)\n ON CONFLICT(id) DO NOTHING`,\n );\n const delScope = incremental\n ? \"DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _dirty_fc)\"\n : \"DELETE FROM file_changes\";\n\n const tx = transaction(db, () => {\n db.exec(delScope);\n for (const row of rows) {\n if (row.status === \"error\") continue; // a failed call did not change the file\n let input: any = null;\n try {\n input = row.input_json ? JSON.parse(row.input_json) : null;\n } catch {\n input = null;\n }\n if (!input) continue;\n const p = proj.get(row.session_id);\n const filePath = normalizeFilePath(input.file_path ?? input.notebook_path, p?.path ?? null);\n if (!filePath) continue;\n\n // Magnitude signal only, not a diff: newline-count deltas of the verbatim strings. Edit with\n // replace_all counts the strings once (occurrence count is unknowable from the input alone);\n // Write's prior content is unseen, so removed stays NULL; NotebookEdit carries no line info.\n let added: number | null = null;\n let removed: number | null = null;\n if (row.tool_name === \"Edit\") {\n added = lineCount(input.new_string);\n removed = lineCount(input.old_string);\n } else if (row.tool_name === \"Write\") {\n added = lineCount(input.content);\n }\n\n insert.run({\n id: fileChangeId(row.id, filePath),\n tool_call_id: row.id,\n session_id: row.session_id,\n turn_id: row.turn_id,\n event_uuid: row.event_uuid,\n project_id: p?.id ?? null,\n file_path: filePath,\n tool_name: row.tool_name,\n lines_added: added,\n lines_removed: removed,\n timestamp: row.timestamp,\n derive_version: FILECHANGES_VERSION,\n });\n }\n });\n tx();\n\n if (incremental) db.exec(\"DROP TABLE IF EXISTS _dirty_fc\");\n const count = (db.prepare(\"SELECT COUNT(*) n FROM file_changes\").get() as { n: number }).n;\n return { count, version: FILECHANGES_VERSION };\n}\n","/**\n * Canonical project roots (ADR-023) — fold cwd-grained project rows into real project roots.\n *\n * Claude Code stamps one project per distinct cwd, so a session (or a spawned subagent) run from a\n * repo SUBDIRECTORY mints a phantom \"project\" (`repo/packages/x`, `repo/evals`, …) that pollutes\n * every project filter. This re-runnable, global step resolves each project path to its canonical\n * root and merges the rows:\n *\n * 1. Nearest git root wins — walk up from the cwd (inclusive) looking for `.git` (dir OR file, so\n * worktrees count), never testing $HOME or above (a dotfiles repo in ~ must not swallow every\n * non-repo path). Nearest match keeps nested repos distinct: `ws/repo/sub` folds to `ws/repo`,\n * not to `ws`.\n * 2. Fallback: nearest OBSERVED ancestor project — for non-git workspace dirs (`ws/.local` → `ws`\n * when `ws` was itself a session cwd), resolved to that ancestor's own canonical root. Applies\n * when the path still exists on disk, OR when the project holds no main session (sidechain\n * only): for a vanished directory we cannot distinguish a deleted independent repo from a\n * workspace subdir, so a project a human actually opened (≥1 main session) keeps its identity —\n * while subagent-only landing spots (spawn cwds, deleted eval/worktree dirs) always fold.\n * 3. Neither → keep the raw cwd (includes $HOME itself and deleted human-opened projects).\n *\n * Merging repoints sessions.project_id (+ file_changes.project_id), refreshes the canonical row's\n * first/last_seen from its merged sessions, and deletes session-less project rows — which also\n * sweeps any true orphans out of the filter dropdowns. Deterministic given the host filesystem;\n * paths that don't exist on this machine simply never match rule 1.\n */\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { createHash } from \"node:crypto\";\nimport { dirname, join } from \"node:path\";\nimport { transaction } from \"@agent-lens/core\";\nimport type { DB } from \"./db.js\";\n\n/** Same derivation ingestFile uses (pipeline.ts), so a created canonical row is identical to one\n * that ingest would have minted for that cwd. */\nfunction projectId(agentId: string, path: string): string {\n return createHash(\"sha1\").update(`${agentId}\\0${path}`).digest(\"hex\").slice(0, 16);\n}\n\nconst strip = (p: string) => (p.length > 1 ? p.replace(/\\/$/, \"\") : p);\n\n/** Nearest ancestor (inclusive) containing `.git`, walking up to — but never testing — `home`.\n * Null when none is found before hitting home or the filesystem root. */\nfunction nearestGitRoot(path: string, home: string): string | null {\n let cur = strip(path);\n while (cur !== home && cur !== \"/\" && cur.length > 0) {\n if (existsSync(join(cur, \".git\"))) return cur;\n const next = dirname(cur);\n if (next === cur) break;\n cur = next;\n }\n return null;\n}\n\nexport interface CanonicalizeResult {\n merged: number; // project rows folded into another\n removed: number; // session-less project rows deleted\n}\n\n/**\n * Fold every project row into its canonical root (see module doc). Global and re-runnable on every\n * ingest — the whole table is a few dozen rows — so an already-ingested DB heals on its next run,\n * no --full needed.\n */\nexport function canonicalizeProjects(db: DB, opts: { homeDir?: string } = {}): CanonicalizeResult {\n const home = strip(opts.homeDir ?? homedir());\n const rows = db.prepare(\"SELECT id, agent_id, path FROM projects\").all() as Array<{\n id: string;\n agent_id: string;\n path: string;\n }>;\n const byPath = new Map<string, { id: string; agent_id: string }>();\n for (const r of rows) byPath.set(strip(r.path), { id: r.id, agent_id: r.agent_id });\n\n // Projects with at least one MAIN session — a human opened a session there, so a deleted dir\n // keeps its identity (see rule 2 in the module doc). Sidechain-only projects never block folding.\n const withMains = new Set<string>(\n (db.prepare(\"SELECT DISTINCT project_id FROM sessions WHERE is_sidechain = 0 AND project_id IS NOT NULL\").all() as Array<{ project_id: string }>).map(\n (r) => r.project_id,\n ),\n );\n\n // Resolve a path to its canonical root. Memoized; the ancestor fallback recurses on a strictly\n // shorter path, so it terminates.\n const memo = new Map<string, string>();\n function canon(path: string): string {\n const p = strip(path);\n const hit = memo.get(p);\n if (hit) return hit;\n memo.set(p, p); // provisional (self) — guards pathological cycles\n let out = p;\n if (p !== home) {\n const git = nearestGitRoot(p, home);\n if (git) {\n out = git;\n } else if (existsSync(p) || !withMains.has(byPath.get(p)?.id ?? \"\")) {\n // Rule 2: nearest observed ancestor project, resolved to ITS canonical root. Gated so a\n // DELETED dir folds only when no human ever opened a session there (sidechain-only) —\n // a deleted real repo must keep its identity, not fold into its parent.\n for (let cur = dirname(p); cur !== \"/\" && cur !== home && cur.length > 0; cur = dirname(cur)) {\n if (byPath.has(cur)) {\n out = canon(cur);\n break;\n }\n if (dirname(cur) === cur) break;\n }\n }\n }\n memo.set(p, out);\n return out;\n }\n\n let merged = 0;\n const upsertProject = db.prepare(\n `INSERT INTO projects (id, agent_id, path, encoded_dir, first_seen, last_seen)\n VALUES (?, ?, ?, NULL, NULL, NULL) ON CONFLICT(agent_id, path) DO NOTHING`,\n );\n const repointSessions = db.prepare(\"UPDATE sessions SET project_id = ? WHERE project_id = ?\");\n const repointFileChanges = db.prepare(\"UPDATE file_changes SET project_id = ? WHERE project_id = ?\");\n const touched = new Set<string>();\n\n transaction(db, () => {\n for (const r of rows) {\n const target = canon(r.path);\n if (target === strip(r.path)) continue;\n const targetId = byPath.get(target)?.id ?? projectId(r.agent_id, target);\n if (targetId === r.id) continue;\n // The canonical root may never have been a cwd itself (sessions only ever ran in subdirs) —\n // mint its row exactly as ingest would have.\n if (!byPath.has(target)) {\n upsertProject.run(targetId, r.agent_id, target);\n byPath.set(target, { id: targetId, agent_id: r.agent_id });\n }\n repointSessions.run(targetId, r.id);\n repointFileChanges.run(targetId, r.id);\n touched.add(targetId);\n merged++;\n }\n // Refresh merged roots' seen-range from their (now larger) session set.\n const refresh = db.prepare(\n `UPDATE projects SET\n first_seen = (SELECT MIN(started_at) FROM sessions WHERE project_id = projects.id),\n last_seen = (SELECT MAX(COALESCE(ended_at, started_at)) FROM sessions WHERE project_id = projects.id)\n WHERE id = ?`,\n );\n for (const id of touched) refresh.run(id);\n })();\n\n // Session-less rows — both the just-merged sources and any pre-existing orphans — leave the\n // filter dropdowns for good.\n const removed = Number(\n db\n .prepare(\"DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)\")\n .run().changes,\n );\n\n return { merged, removed };\n}\n","/**\n * Agent Lens — ingest engine (the DB-writing core of Stage 2), separated from the CLI bootstrap in\n * index.ts so it can be driven in-process by tests against an :memory: DB. index.ts handles argv/env,\n * the disk walk, and the incremental-skip check; everything here operates on an open DB + file content.\n */\nimport { createHash } from \"node:crypto\";\nimport type { StatementSync } from \"node:sqlite\";\nimport { packRaw, runNamed, transaction, type SourceAdapter, type SourceFile, type TurnRow } from \"@agent-lens/core\";\nimport { isCommandResultCarrier } from \"@agent-lens/transcript-format\";\nimport { type DB } from \"./db.js\";\nimport { createDirtySet } from \"./dirtyset.js\";\n\ntype Stmt = StatementSync;\n\nexport interface IngestStats {\n files: number;\n skipped: number;\n malformed: number;\n newEvents: number;\n}\n\nexport function newStats(): IngestStats {\n return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };\n}\n\nexport function preview(text: string): string {\n return text.replace(/\\s+/g, \" \").trim().slice(0, 140);\n}\n\nconst SKILL_INJECT_PREFIX = \"Base directory for this skill:\";\n\n/** Final name token of a skill id/dir, ignoring path and plugin prefix: \"plugin:foo\" / \".../foo\" → \"foo\". */\nfunction skillNameKey(name: string): string {\n const afterSlash = name.split(\"/\").pop() ?? name;\n const afterColon = afterSlash.split(\":\").pop() ?? afterSlash;\n return afterColon.trim();\n}\n\n/**\n * Parse a skill-body injection event. A Skill firing injects an isMeta user message of the shape\n * `Base directory for this skill: <abs path>/<name>\\n\\n<SKILL.md body>\\n\\nARGUMENTS: <args>`\n * into the transcript — the only place the real skill content appears. We split off the\n * Base-directory line (path differs per install) and the trailing ARGUMENTS block (per-call) so the\n * remaining body is stable across installs/args and can be content-hashed into a version.\n */\nexport function parseSkillInjection(\n text: string,\n): { baseDir: string | null; nameKey: string; body: string; args: string | null } | null {\n if (!text.startsWith(SKILL_INJECT_PREFIX)) return null;\n const firstNl = text.indexOf(\"\\n\");\n const firstLine = (firstNl >= 0 ? text.slice(0, firstNl) : text).slice(SKILL_INJECT_PREFIX.length).trim();\n const baseDir = firstLine || null;\n let body = firstNl >= 0 ? text.slice(firstNl + 1) : \"\";\n // Strip the trailing per-call ARGUMENTS block (use lastIndexOf — the body may mention \"ARGUMENTS\").\n let args: string | null = null;\n const argIdx = body.lastIndexOf(\"\\nARGUMENTS:\");\n if (argIdx >= 0) {\n args = body.slice(argIdx + \"\\nARGUMENTS:\".length).trim() || null;\n body = body.slice(0, argIdx);\n }\n return { baseDir, nameKey: baseDir ? skillNameKey(baseDir) : \"\", body: body.trim(), args };\n}\n\n/** First markdown heading (or first non-empty line) of a body, for list/detail display. */\nfunction skillSummary(body: string): string | null {\n for (const line of body.split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n return (t.startsWith(\"#\") ? t.replace(/^#+\\s*/, \"\").trim() : t).slice(0, 200) || null;\n }\n return null;\n}\n\n/** Content-addressed skill version id: hash of name + normalized body (matches the project's sha1 id convention). */\nfunction skillVersionId(name: string, body: string): string {\n return createHash(\"sha1\").update(name).update(\"\\0\").update(body).digest(\"hex\").slice(0, 16);\n}\n\n/**\n * Remove every session whose project path is in `excludedPaths` — plus its subagent descendants —\n * along with all dependent rows (events → FTS via trigger, token_usage, tool_calls, turns,\n * classifications, findings) and the ingest_state for its files. This is what makes the global exclude list\n * take effect on the NEXT ingest, incremental or full: add a project and its data leaves the DB.\n * Matches projects.path exactly or by path-prefix. Returns the number of sessions pruned.\n */\nexport function pruneExcluded(db: DB, excludedPaths: string[]): number {\n if (!excludedPaths.length) return 0;\n const projects = db.prepare(\"SELECT id, path FROM projects\").all() as Array<{ id: string; path: string }>;\n const projIds = projects.filter((p) => excludedPaths.some((e) => p.path === e || p.path.startsWith(e + \"/\"))).map((p) => p.id);\n if (!projIds.length) return 0;\n\n const seed = db.prepare(`SELECT id FROM sessions WHERE project_id IN (${projIds.map(() => \"?\").join(\",\")})`).all(...projIds) as Array<{ id: string }>;\n createDirtySet(db, \"_prune\", seed.map((s) => s.id));\n // Transitive: subagents whose parent is being pruned (covers cross-project linkage). Fixpoint.\n const expand = db.prepare(\"INSERT OR IGNORE INTO _prune (id) SELECT id FROM sessions WHERE parent_session_id IN (SELECT id FROM _prune)\");\n const cnt = db.prepare(\"SELECT COUNT(*) n FROM _prune\");\n for (let prev = -1; ; ) {\n const n = (cnt.get() as { n: number }).n;\n if (n === prev) break;\n prev = n;\n expand.run();\n }\n const total = (cnt.get() as { n: number }).n;\n if (!total) {\n db.exec(\"DROP TABLE IF EXISTS _prune\");\n return 0;\n }\n 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() as Array<{ f: string }>).map((r) => r.f);\n\n // The toggle must stay OUTSIDE the transaction: `PRAGMA foreign_keys` is a silent no-op inside one,\n // so moving it in would disable nothing and raise no error. `finally` so a failed delete can't leave\n // the connection unguarded for every later pass in the process.\n db.exec(\"PRAGMA foreign_keys = OFF\");\n try {\n transaction(db, () => {\n db.exec(\"DELETE FROM findings WHERE session_id IN (SELECT id FROM _prune)\");\n db.exec(\"DELETE FROM file_changes WHERE session_id IN (SELECT id FROM _prune)\");\n for (const t of [\"token_usage\", \"tool_calls\", \"turns\", \"events\"]) db.exec(`DELETE FROM ${t} WHERE session_id IN (SELECT id FROM _prune)`);\n db.exec(\"DELETE FROM classifications WHERE scope = 'session' AND target_id IN (SELECT id FROM _prune)\");\n db.exec(\"DELETE FROM sessions WHERE id IN (SELECT id FROM _prune)\");\n const delState = db.prepare(\"DELETE FROM ingest_state WHERE file_path = ?\");\n for (const f of files) delState.run(f);\n // Drop now-empty projects (an excluded project leaves no sessions behind).\n db.exec(\"DELETE FROM projects WHERE id NOT IN (SELECT DISTINCT project_id FROM sessions WHERE project_id IS NOT NULL)\");\n })();\n } finally {\n db.exec(\"PRAGMA foreign_keys = ON\");\n }\n db.exec(\"DROP TABLE IF EXISTS _prune\");\n return total;\n}\n\nfunction durationMs(start: string | null, end: string | null): number | null {\n if (!start || !end) return null;\n const a = Date.parse(start);\n const b = Date.parse(end);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.max(0, b - a);\n}\n\n/** Group a session's events into turns and return the turns + a uuid→turn_id map. */\nexport function buildTurns(\n sessionId: string,\n rows: Array<{ uuid: string; seq: number | null; type: string; is_sidechain: number; is_meta: number; timestamp: string | null; model: string | null; text: string | null }>,\n): { turns: TurnRow[]; eventTurn: Map<string, string> } {\n // Ties on timestamp are common and meaningful — a slash command and its output share one to the\n // millisecond — so the tiebreak decides which of them opens the turn and which events land in it.\n // `seq` is the line order as first observed, i.e. the order Claude Code actually wrote them; uuid\n // is random and silently reversed such pairs. This matches how the server reads events back.\n rows.sort(\n (x, y) =>\n (x.timestamp ?? \"\").localeCompare(y.timestamp ?? \"\") ||\n (x.seq ?? 0) - (y.seq ?? 0) ||\n x.uuid.localeCompare(y.uuid),\n );\n const turns: TurnRow[] = [];\n const eventTurn = new Map<string, string>();\n let cur: TurnRow | null = null;\n let seq = 0;\n\n for (const e of rows) {\n // A turn starts at a user prompt with real text. Sidechain (subagent) sessions are entirely\n // sidechain — verified no file mixes main+sidechain events — so their task prompt starts a turn\n // too; we don't filter on is_sidechain here. Tool-result carrier messages have empty text, and a\n // slash command's output rides on a user line without being a prompt (`/login` logs its stdout\n // with the same timestamp as the command).\n const body = e.text?.trim() ?? \"\";\n const isPrompt = e.type === \"user\" && e.is_meta === 0 && body.length > 0 && !isCommandResultCarrier(body);\n if (isPrompt) {\n cur = {\n id: `${sessionId}:${seq}`,\n session_id: sessionId,\n seq,\n user_event_uuid: e.uuid,\n prompt_preview: preview(e.text!),\n model: null,\n started_at: e.timestamp,\n ended_at: e.timestamp,\n duration_ms: null,\n };\n turns.push(cur);\n seq++;\n }\n if (cur) {\n eventTurn.set(e.uuid, cur.id);\n if (e.model) cur.model = e.model;\n if (e.timestamp && (!cur.ended_at || e.timestamp > cur.ended_at)) cur.ended_at = e.timestamp;\n }\n }\n for (const t of turns) t.duration_ms = durationMs(t.started_at, t.ended_at);\n return { turns, eventTurn };\n}\n\n/**\n * Rebuild derived tables (turns, turn_id linkage, subagent parent linkage, session aggregates) from\n * the idempotent events/tool_calls tables.\n *\n * `dirty` is the set of session ids touched by this ingest run (incremental). When provided, only the\n * affected sessions are rebuilt — but the set is first expanded to its **linkage neighborhood** (the\n * subagent children spawned by a dirty session, and the parent session that spawned a dirty sidechain)\n * via a fixpoint, so cross-session parent/child links stay correct even when parent and child\n * transcripts arrive in different runs (ADR-010). When `dirty` is null/undefined (`--full`), every\n * session is rebuilt — the migration/reset path. Returns the expanded id set (or null for the full\n * path) so `classify()` can reuse it without re-expanding.\n */\n/** SQL fragments that scope every derived-data statement to the rebuild's session set: empty strings\n * for a full rebuild, else a restriction to the materialized `_dirty` temp table. */\ninterface Scope {\n bySession: string;\n byId: string;\n andId: string;\n andSelfId: string;\n}\n\n/** Materialize the dirty set, then grow it to the whole linkage neighborhood: a dirty parent pulls in\n * the children it spawned, and a dirty child pulls in its spawner parent. Loops to a fixpoint (covers\n * nested subagents), bounded by the session count. */\nfunction expandDirtyScope(db: DB, dirty: Set<string>): void {\n createDirtySet(db, \"_dirty\", dirty);\n const expand = db.prepare(`\n INSERT OR IGNORE INTO _dirty (id)\n SELECT spawned_session_id FROM tool_calls\n WHERE session_id IN (SELECT id FROM _dirty) AND spawned_session_id IS NOT NULL\n UNION\n SELECT session_id FROM tool_calls\n WHERE spawned_session_id IN (SELECT id FROM _dirty)\n UNION\n -- structural (path-based) workflow linkage: a dirty parent pulls in its subagent children,\n -- and a dirty child pulls in its structural parent, so the fallback link stays correct when\n -- parent and child transcripts arrive in different runs (ADR-010).\n SELECT id FROM sessions\n WHERE spawn_parent_id IN (SELECT id FROM _dirty)\n UNION\n SELECT spawn_parent_id FROM sessions\n WHERE id IN (SELECT id FROM _dirty) AND spawn_parent_id IS NOT NULL\n `);\n const count = db.prepare(\"SELECT COUNT(*) n FROM _dirty\");\n let prev = -1;\n for (;;) {\n const n = (count.get() as { n: number }).n;\n if (n === prev) break;\n prev = n;\n expand.run();\n }\n}\n\n/** Rebuild turns from the (idempotent) events table for the scoped sessions, then propagate the new\n * turn_ids to the dependent tables. Referencing turn_ids are nulled BEFORE the turns are deleted\n * (FK: events/token_usage/tool_calls/sessions -> turns), and skill-version links are cleared for the\n * scope so a changed/removed body can't leave a stale link. */\nfunction rebuildTurns(db: DB, scope: Scope, sessionIds: Array<{ id: string }>): void {\n const selEvents = db.prepare(\n \"SELECT uuid, seq, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?\",\n );\n const insTurn = db.prepare(\n `INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)\n VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`,\n );\n const updEventTurn = db.prepare(\"UPDATE events SET turn_id = ? WHERE uuid = ?\");\n\n const tx = transaction(db, () => {\n for (const { id } of sessionIds) {\n const rows = selEvents.all(id) as any[];\n const { turns, eventTurn } = buildTurns(id, rows);\n for (const t of turns) runNamed(insTurn, t);\n for (const [uuid, turnId] of eventTurn) updEventTurn.run(turnId, uuid);\n }\n });\n tx();\n\n db.exec(`UPDATE token_usage SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = token_usage.event_uuid)${scope.bySession}`);\n db.exec(`UPDATE tool_calls SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = tool_calls.event_uuid)${scope.bySession}`);\n}\n\n/** Link each Skill tool_call to a content-addressed skill *version*. A firing injects the full\n * SKILL.md body as an isMeta user event right after the launch; we pair each Skill tool_call with\n * the following injection (by skill-name token, ARGUMENTS as tiebreak), normalize + hash the body,\n * UPSERT the version, and stamp tool_calls.skill_id. Firings without a captured body keep skill_id\n * NULL (skill_name stays set). Runs over the same scoped session set as the turn rebuild. */\nfunction linkSkillVersions(db: DB, sessionIds: Array<{ id: string }>): void {\n const selEventsOrdered = db.prepare(\n \"SELECT uuid, type, is_meta, timestamp, text FROM events WHERE session_id = ?\",\n );\n const selSkillCalls = db.prepare(\n \"SELECT id, event_uuid, skill_name, input_json FROM tool_calls WHERE session_id = ? AND tool_name = 'Skill'\",\n );\n const upsertSkill = db.prepare(\n `INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)\n VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @ts, @ts)\n ON CONFLICT(id) DO UPDATE SET\n last_seen = MAX(last_seen, excluded.last_seen),\n first_seen = MIN(first_seen, excluded.first_seen),\n base_dir = excluded.base_dir`,\n );\n const linkCall = db.prepare(\"UPDATE tool_calls SET skill_id = ? WHERE id = ?\");\n\n const linkTx = transaction(db, () => {\n for (const { id: sid } of sessionIds) {\n const calls = selSkillCalls.all(sid) as Array<{ id: string; event_uuid: string | null; skill_name: string | null; input_json: string | null }>;\n if (!calls.length) continue;\n const events = (selEventsOrdered.all(sid) as Array<{ uuid: string; type: string; is_meta: number; timestamp: string | null; text: string | null }>).sort(\n (x, y) => (x.timestamp ?? \"\").localeCompare(y.timestamp ?? \"\") || x.uuid.localeCompare(y.uuid),\n );\n const callByEvent = new Map<string, typeof calls>();\n for (const c of calls) {\n if (!c.event_uuid) continue;\n const arr = callByEvent.get(c.event_uuid) ?? [];\n arr.push(c);\n callByEvent.set(c.event_uuid, arr);\n }\n // Queue of pending Skill calls awaiting their body injection, keyed by skill-name token.\n const pending = new Map<string, Array<{ id: string; args: string | null }>>();\n for (const e of events) {\n const here = callByEvent.get(e.uuid);\n if (here) {\n for (const c of here) {\n if (!c.skill_name) continue;\n const key = skillNameKey(c.skill_name);\n let args: string | null = null;\n try {\n args = c.input_json ? (JSON.parse(c.input_json).args ?? null) : null;\n if (typeof args === \"string\") args = args.trim() || null;\n else if (args != null) args = JSON.stringify(args);\n } catch {\n /* keep null */\n }\n const arr = pending.get(key) ?? [];\n arr.push({ id: c.id, args });\n pending.set(key, arr);\n }\n }\n if (e.is_meta !== 1 || e.type !== \"user\" || !e.text) continue;\n const inj = parseSkillInjection(e.text);\n if (!inj) continue;\n const queue = pending.get(inj.nameKey);\n if (!queue || !queue.length) continue;\n // Prefer a call whose args match the injection's ARGUMENTS; else take the earliest pending.\n let idx = inj.args != null ? queue.findIndex((q) => q.args === inj.args) : -1;\n if (idx < 0) idx = 0;\n const [match] = queue.splice(idx, 1);\n const c = calls.find((x) => x.id === match.id)!;\n const name = c.skill_name!;\n const vid = skillVersionId(name, inj.body);\n upsertSkill.run({\n id: vid,\n name,\n base_dir: inj.baseDir,\n body: inj.body,\n summary: skillSummary(inj.body),\n body_bytes: Buffer.byteLength(inj.body, \"utf8\"),\n ts: e.timestamp,\n });\n linkCall.run(vid, match.id);\n }\n }\n });\n linkTx();\n}\n\n/** Attribute every subagent (sidechain) session to the parent turn/session that spawned it, in\n * three passes of decreasing precision — each later pass only fills what the earlier one left NULL. */\nfunction linkParents(db: DB, scope: Scope): void {\n // 1. The deterministic key: the spawning Task/Agent tool_call's spawned_session_id (== this session id).\n db.exec(`\n UPDATE sessions SET\n parent_session_id = (SELECT tc.session_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id),\n parent_turn_id = (SELECT tc.turn_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)\n WHERE EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)${scope.andSelfId}\n `);\n\n // 2. Workflow fan-out carries no toolUseResult.agentId, so the link above never fires for it\n // (finding #1). But the launching Workflow tool_call's result DOES carry a run id (wf_<id>), and the\n // run's subagents nest under …/subagents/workflows/<runId>/ — so sessions.workflow_run_id matches\n // tool_calls.workflow_run_id. That gives workflow agents BOTH their parent session and the exact\n // launching turn. (LIMIT 1: a run id maps to a single Workflow tool_call.)\n db.exec(`\n UPDATE sessions SET\n 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)),\n 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))\n WHERE sessions.workflow_run_id IS NOT NULL\n AND EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id)${scope.andSelfId}\n `);\n\n // 3. Final fallback for any sidechain still unlinked (e.g. its Workflow tool_call/runId wasn't\n // captured, or a non-workflow nested agent): attribute to the structural parent captured at ingest\n // from the transcript's directory location (<parent>/subagents/…) — the deterministic,\n // redaction-surviving signal. parent_turn_id stays NULL here. Tokens stay siloed → no double-count.\n db.exec(`\n UPDATE sessions SET parent_session_id = spawn_parent_id\n WHERE parent_session_id IS NULL\n AND spawn_parent_id IS NOT NULL\n AND spawn_parent_id IN (SELECT id FROM sessions)${scope.andSelfId}\n `);\n}\n\n/** Recompute the per-session aggregates (counts, wall-clock span, sidechain flag) from events/turns. */\nfunction recomputeSessionAggregates(db: DB, scope: Scope): void {\n db.exec(`\n UPDATE sessions SET\n event_count = (SELECT COUNT(*) FROM events e WHERE e.session_id = sessions.id),\n started_at = (SELECT MIN(timestamp) FROM events e WHERE e.session_id = sessions.id),\n ended_at = (SELECT MAX(timestamp) FROM events e WHERE e.session_id = sessions.id),\n turn_count = (SELECT COUNT(*) FROM turns t WHERE t.session_id = sessions.id),\n is_sidechain = (SELECT CASE WHEN MIN(is_sidechain) = 1 THEN 1 ELSE 0 END FROM events e WHERE e.session_id = sessions.id)${scope.byId}\n `);\n db.exec(`\n UPDATE sessions SET duration_ms =\n CAST((julianday(ended_at) - julianday(started_at)) * 86400000 AS INTEGER)\n WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${scope.andId}\n `);\n}\n\n/** Prune phantom sessions: any with zero events is not a real transcript. discover() walks every\n * *.jsonl under projects/, which sweeps up non-transcript files that happen to live there — e.g.\n * a Workflow tool's `journal.jsonl` (lines carry no `uuid`, so they yield no events). insSessionStub\n * still created a row (and they all share the basename → one phantom \"journal\" session). A zero-event\n * session has no events/turns/token_usage/tool_calls referencing it, so the delete is FK-safe.\n *\n * The dependent sweeps stay GLOBAL rather than scoped: classifications has no FK and is one row per\n * session, while detect() and deriveFileChanges run after this rebuild over the dirty scope only — so\n * a vanished non-dirty session's rows must be cleared here or they leak. */\nfunction pruneOrphans(db: DB, scope: Scope, incremental: boolean): void {\n db.exec(`DELETE FROM sessions WHERE event_count = 0${scope.andId}`);\n db.exec(\"DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)\");\n db.exec(\"DELETE FROM findings WHERE session_id NOT IN (SELECT id FROM sessions)\");\n db.exec(\"DELETE FROM file_changes WHERE session_id NOT IN (SELECT id FROM sessions)\");\n // Sweep skill versions no longer referenced by any tool_call. Full path only: an incremental run\n // only re-derives dirty sessions, so a version could still be referenced by a non-dirty session.\n if (!incremental) {\n db.exec(\"DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)\");\n }\n}\n\nexport function rebuildDerived(db: DB, dirty?: Set<string> | null): Set<string> | null {\n const incremental = dirty != null;\n const scope: Scope = {\n bySession: incremental ? \" WHERE session_id IN (SELECT id FROM _dirty)\" : \"\",\n byId: incremental ? \" WHERE id IN (SELECT id FROM _dirty)\" : \"\",\n andId: incremental ? \" AND id IN (SELECT id FROM _dirty)\" : \"\",\n andSelfId: incremental ? \" AND sessions.id IN (SELECT id FROM _dirty)\" : \"\",\n };\n\n if (dirty != null) expandDirtyScope(db, dirty);\n\n // Null referencing turn_ids BEFORE deleting turns (FK: events/token_usage/tool_calls/sessions -> turns).\n db.exec(`UPDATE events SET turn_id = NULL${scope.bySession}`);\n db.exec(`UPDATE token_usage SET turn_id = NULL${scope.bySession}`);\n db.exec(`UPDATE tool_calls SET turn_id = NULL${scope.bySession}`);\n // Clear skill-version links for the scope so changed/removed bodies don't leave a stale link.\n db.exec(`UPDATE tool_calls SET skill_id = NULL${scope.bySession}`);\n db.exec(`UPDATE sessions SET parent_turn_id = NULL${scope.byId}`);\n db.exec(`DELETE FROM turns${scope.bySession}`);\n\n const sessionIds = db.prepare(`SELECT id FROM sessions${scope.byId}`).all() as Array<{ id: string }>;\n\n rebuildTurns(db, scope, sessionIds);\n linkSkillVersions(db, sessionIds);\n linkParents(db, scope);\n recomputeSessionAggregates(db, scope);\n pruneOrphans(db, scope, incremental);\n\n if (incremental) {\n const expanded = new Set(\n (db.prepare(\"SELECT id FROM _dirty\").all() as Array<{ id: string }>).map((r) => r.id),\n );\n db.exec(\"DROP TABLE IF EXISTS _dirty\");\n return expanded;\n }\n return null;\n}\n\nexport interface IngestStatements {\n insAgent: Stmt;\n insSource: Stmt;\n getState: Stmt;\n setState: Stmt;\n insEvent: Stmt;\n insTokens: Stmt;\n insTool: Stmt;\n patchTool: Stmt;\n upsertProject: Stmt;\n insSessionStub: Stmt;\n upsertSession: Stmt;\n}\n\nexport function prepareStatements(db: DB): IngestStatements {\n return {\n insAgent: db.prepare(\"INSERT OR IGNORE INTO agents (id, name, kind) VALUES (?, ?, 'cli')\"),\n insSource: db.prepare(\n `INSERT INTO sources (id, label, agent_id, config_dir) VALUES (@id, @label, @agent_id, @config_dir)\n ON CONFLICT(id) DO UPDATE SET label=excluded.label, agent_id=excluded.agent_id, config_dir=excluded.config_dir`,\n ),\n getState: db.prepare(\"SELECT size, mtime_ms, sha256, events_ingested FROM ingest_state WHERE file_path = ?\"),\n setState: db.prepare(\n `INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)\n VALUES (@file_path, @size, @mtime_ms, @sha256, @events_ingested, @ingested_at)\n ON CONFLICT(file_path) DO UPDATE SET\n size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256,\n events_ingested=excluded.events_ingested, ingested_at=excluded.ingested_at`,\n ),\n insEvent: db.prepare(\n `INSERT INTO events (uuid, session_id, turn_id, parent_uuid, seq, type, role, timestamp, model, is_sidechain, is_meta, text, thinking, raw_json, source_file)\n VALUES (@uuid, @session_id, @turn_id, @parent_uuid, @seq, @type, @role, @timestamp, @model, @is_sidechain, @is_meta, @text, @thinking, @raw_json, @source_file)\n ON CONFLICT(uuid) DO NOTHING`,\n ),\n insTokens: db.prepare(\n // Bare ON CONFLICT (no target) so the insert is skipped on EITHER the event_uuid PK or the\n // unique (session_id, message_id) index — the latter collapses a response's repeated\n // per-content-block usage lines into a single row.\n `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)\n VALUES (@event_uuid, @session_id, @turn_id, @message_id, @model, @input_tokens, @output_tokens, @cache_creation_input_tokens, @cache_read_input_tokens, @service_tier)\n ON CONFLICT DO NOTHING`,\n ),\n insTool: db.prepare(\n `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)\n 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)\n ON CONFLICT(id) DO NOTHING`,\n ),\n patchTool: db.prepare(\n `UPDATE tool_calls SET\n status = COALESCE(@status, status),\n agent_type = COALESCE(@agent_type, agent_type),\n spawned_session_id = COALESCE(@spawned_session_id, spawned_session_id),\n workflow_run_id = COALESCE(@workflow_run_id, workflow_run_id),\n workflow_name = COALESCE(@workflow_name, workflow_name),\n resolved_model = COALESCE(@resolved_model, resolved_model),\n total_duration_ms = COALESCE(@total_duration_ms, total_duration_ms),\n total_tokens = COALESCE(@total_tokens, total_tokens),\n total_tool_use_count = COALESCE(@total_tool_use_count, total_tool_use_count),\n result_summary = COALESCE(@result_summary, result_summary)\n WHERE id = @tool_use_id`,\n ),\n upsertProject: db.prepare(\n `INSERT INTO projects (id, agent_id, path, encoded_dir, first_seen, last_seen)\n VALUES (@id, @agent_id, @path, @encoded_dir, @now, @now)\n ON CONFLICT(id) DO UPDATE SET last_seen = @now, encoded_dir = COALESCE(excluded.encoded_dir, encoded_dir)`,\n ),\n insSessionStub: db.prepare(\"INSERT OR IGNORE INTO sessions (id, agent_id, source_id) VALUES (?, ?, ?)\"),\n upsertSession: db.prepare(\n `INSERT INTO sessions (id, agent_id, source_id, project_id, slug, ai_title, cli_version, entrypoint, git_branch, spawn_parent_id, workflow_run_id)\n VALUES (@id, @agent_id, @source_id, @project_id, @slug, @ai_title, @cli_version, @entrypoint, @git_branch, @spawn_parent_id, @workflow_run_id)\n ON CONFLICT(id) DO UPDATE SET\n source_id = COALESCE(excluded.source_id, source_id),\n project_id = COALESCE(excluded.project_id, project_id),\n slug = COALESCE(excluded.slug, slug),\n ai_title = COALESCE(excluded.ai_title, ai_title),\n cli_version = COALESCE(excluded.cli_version, cli_version),\n entrypoint = COALESCE(excluded.entrypoint, entrypoint),\n git_branch = COALESCE(excluded.git_branch, git_branch),\n spawn_parent_id = COALESCE(excluded.spawn_parent_id, spawn_parent_id),\n workflow_run_id = COALESCE(excluded.workflow_run_id, workflow_run_id)`,\n ),\n };\n}\n\n/**\n * Ingest one transcript file's lines in a single transaction: parse each line via the adapter,\n * insert events/token_usage/tool_calls (deduped by their keys), patch tool results, and upsert the\n * session + project metadata. `lines` is an iterable of newline-stripped lines — the caller chooses\n * whole-file read (small files) or a streaming reader (large files), so this engine never holds the\n * full file as one string. `meta` carries the disk stats recorded into ingest_state (tests pass\n * synthetic values). Derived tables (turns, linkage, aggregates) are rebuilt separately afterwards.\n */\nexport function ingestFile(\n db: DB,\n stmts: IngestStatements,\n adapter: SourceAdapter,\n file: SourceFile,\n lines: Iterable<string>,\n meta: { size: number; mtimeMs: number; hash: string },\n now: string,\n stats: IngestStats,\n): void {\n const sessionMeta: Record<string, any> = {};\n let eventsInFile = 0;\n\n const tx = transaction(db, () => {\n // Ensure the session row exists before any event references it (FK).\n stmts.insSessionStub.run(file.sessionId, adapter.agentId, file.sourceId);\n let seq = 0;\n for (const line of lines) {\n if (!line.trim()) continue;\n let raw: unknown;\n try {\n raw = JSON.parse(line);\n } catch {\n stats.malformed++;\n continue;\n }\n const parsed = adapter.parseLine(raw, file, seq++);\n if (parsed.meta) Object.assign(sessionMeta, parsed.meta);\n if (parsed.event) {\n // raw_json is stored gzip-compressed (ADR-011); compress at this single write chokepoint so\n // adapters stay agent-agnostic and emit a plain string.\n const info = stmts.insEvent.run({ ...parsed.event, raw_json: packRaw(parsed.event.raw_json) });\n if (info.changes > 0) {\n stats.newEvents++;\n eventsInFile++;\n }\n }\n if (parsed.tokenUsage) runNamed(stmts.insTokens, parsed.tokenUsage);\n if (parsed.toolCalls) for (const tc of parsed.toolCalls) runNamed(stmts.insTool, tc);\n if (parsed.toolResults)\n for (const tr of parsed.toolResults)\n stmts.patchTool.run({\n tool_use_id: tr.tool_use_id,\n status: tr.status ?? null,\n agent_type: tr.agent_type ?? null,\n spawned_session_id: tr.spawned_session_id ?? null,\n workflow_run_id: tr.workflow_run_id ?? null,\n workflow_name: tr.workflow_name ?? null,\n resolved_model: tr.resolved_model ?? null,\n total_duration_ms: tr.total_duration_ms ?? null,\n total_tokens: tr.total_tokens ?? null,\n total_tool_use_count: tr.total_tool_use_count ?? null,\n result_summary: tr.result_summary ?? null,\n });\n }\n\n // Persist session + project metadata (survives later skip runs).\n let projectId: string | null = null;\n if (sessionMeta.cwd) {\n projectId = createHash(\"sha1\").update(`${adapter.agentId}\\0${sessionMeta.cwd}`).digest(\"hex\").slice(0, 16);\n stmts.upsertProject.run({\n id: projectId,\n agent_id: adapter.agentId,\n path: sessionMeta.cwd,\n encoded_dir: file.encodedDir || null,\n now,\n });\n }\n stmts.upsertSession.run({\n id: file.sessionId,\n agent_id: adapter.agentId,\n source_id: file.sourceId,\n project_id: projectId,\n slug: sessionMeta.slug ?? null,\n ai_title: sessionMeta.ai_title ?? null,\n cli_version: sessionMeta.cli_version ?? null,\n entrypoint: sessionMeta.entrypoint ?? null,\n git_branch: sessionMeta.git_branch ?? null,\n spawn_parent_id: file.parentSessionId ?? null,\n workflow_run_id: file.workflowRunId ?? null,\n });\n\n stmts.setState.run({\n file_path: file.path,\n size: meta.size,\n mtime_ms: Math.trunc(meta.mtimeMs),\n sha256: meta.hash,\n events_ingested: eventsInFile,\n ingested_at: now,\n });\n });\n tx();\n}\n","/**\n * @agent-lens/transcript-format — the markup vocabulary Claude Code embeds inside message text.\n *\n * Claude Code wraps some content in XML-ish tags inside an otherwise ordinary user message: slash\n * commands and their local output (`<command-name>`, `<local-command-stdout>`, …) and async-task\n * completions (`<task-notification>`). Three packages need to recognize that markup — ingest, to tell\n * a command's output from a prompt when grouping turns; the server, to read a workflow's completion;\n * and web, to render commands and notifications as cards rather than raw tags. Before this package\n * each kept its own copy, and the server's and web's tag readers were the same regex character for\n * character.\n *\n * **Scope:** this is *surface vocabulary* — the tag names and how to read one — not transcript\n * parsing. How a transcript record is shaped (where text, reasoning, or a queued prompt live) stays\n * behind the `SourceAdapter` seam in `packages/ingest/src/adapters/`, per ADR-008. The distinction\n * matters: adapters own the record; this owns strings that appear *within* message text and that a\n * browser has to re-recognize at render time.\n *\n * **Constraints:** zero dependencies and no `node:` imports, so the browser bundle can import it.\n * `@agent-lens/contracts` deliberately cannot host this — it is a pure-types leaf whose node-free\n * guarantee is structural precisely because it exports no values.\n *\n * It is named for Claude Code on purpose. A second agent would bring its own vocabulary rather than\n * inherit this one.\n */\n\n/** Tags naming a slash-command invocation the user typed. */\nexport const COMMAND_NAME_TAG = \"command-name\";\nexport const COMMAND_MESSAGE_TAG = \"command-message\";\nexport const COMMAND_ARGS_TAG = \"command-args\";\n\n/** The banner Claude Code prepends to a local slash command's transcript block. */\nexport const COMMAND_CAVEAT_TAG = \"local-command-caveat\";\n\n/**\n * Tags carrying a slash command's *output*. These ride on a `user` line but are a result, not\n * something the user typed — treating one as a prompt opens a spurious turn (`/login` writes its\n * command and its stdout with the same timestamp, so they are otherwise hard to tell apart).\n */\nexport const COMMAND_OUTPUT_TAGS = [\"local-command-stdout\", \"local-command-stderr\", \"command-output\"] as const;\n\n/** Tags inside a `<task-notification>` block, posted when an async Workflow or Agent run finishes. */\nexport const TASK_NOTIFICATION_TAG = \"task-notification\";\nexport const TASK_ID_TAG = \"task-id\";\nexport const TASK_TOOL_USE_ID_TAG = \"tool-use-id\";\nexport const TASK_STATUS_TAG = \"status\";\nexport const TASK_SUMMARY_TAG = \"summary\";\nexport const TASK_RESULT_TAG = \"result\";\nexport const TASK_FAILURES_TAG = \"failures\";\n\nconst COMMAND_OUTPUT_ALTERNATION = COMMAND_OUTPUT_TAGS.join(\"|\");\n\n/** Matches a message whose body *starts* with a command-output tag — i.e. the whole line is a result. */\nconst COMMAND_RESULT_CARRIER = new RegExp(`^<(?:${COMMAND_OUTPUT_ALTERNATION})>`);\n\n/**\n * True when this message text is a slash command's output rather than a prompt. Anchored at the\n * start so a message that merely *quotes* a tag isn't mistaken for one; a `<command-name>` line is\n * the genuine prompt and is never matched here.\n */\nexport function isCommandResultCarrier(text: string): boolean {\n return COMMAND_RESULT_CARRIER.test(text.trimStart());\n}\n\n/** Read one tag's inner content, trimmed, or null when the tag is absent. Non-greedy, so the first\n * `</tag>` closes it, and dot-all via `[\\s\\S]` so multi-line bodies (a command's stdout, a task\n * summary) are captured whole. */\nexport function xmlTag(text: string, tag: string): string | null {\n return text.match(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;\n}\n\n/** Read the first of the command-output tags present, or null when none is. */\nexport function commandOutput(text: string): string | null {\n return text.match(new RegExp(`<(?:${COMMAND_OUTPUT_ALTERNATION})>([\\\\s\\\\S]*?)</(?:${COMMAND_OUTPUT_ALTERNATION})>`))?.[1]?.trim() ?? null;\n}\n","/**\n * Agent Lens — Stage 2 ingest of Workflow-tool result sidecars.\n *\n * The Workflow tool writes a JSON sidecar next to the launching session when a run finishes:\n * <archive>/<source>/projects/<enc>/<sessionId>/workflows/wf_<id>.json\n * The transcript only carries the \"launched in background\" ack for an async run, so for those runs\n * this file is the ONLY record of how the run finished — status (completed/failed), summary, the\n * returned result payload, the model, phase structure, per-item logs, and roll-up\n * tokens/tool-calls/duration/agent-count. We project those fields into `workflow_results` (verbatim,\n * like tool_calls.input_json — the DB is local; redaction is an export concern). The big `script` and\n * `args` are intentionally NOT re-stored: they already live on the launching Workflow tool_call.\n *\n * Idempotent: keyed by run id, UPSERT on re-ingest. The discover/skip/state machinery is shared with\n * the other sidecar ingesters in `sidecar.ts`; only the parse + upsert below is workflow-specific.\n */\nimport { basename } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { ingestSidecars, newSidecarStats, sessionBefore, intOrNull, strOrNull, type SidecarStats } from \"./sidecar.js\";\n\nexport type WorkflowIngestStats = SidecarStats;\nexport const newWorkflowStats = newSidecarStats;\n\n/** Epoch-ms number or ISO/date string → ISO string (null when unparseable/absent). */\nfunction toIso(x: unknown): string | null {\n if (typeof x === \"number\" && Number.isFinite(x)) return new Date(x).toISOString();\n if (typeof x === \"string\" && x) {\n const t = Date.parse(x);\n return Number.isNaN(t) ? null : new Date(t).toISOString();\n }\n return null;\n}\n\nconst jsonOrNull = (v: unknown): string | null => (v == null ? null : JSON.stringify(v));\n\n/** Strip terminal styling the runner sometimes bakes into defaultModel — both real ANSI escapes\n * (`\\x1b[1m`) and the de-escaped literal form (`claude-opus-4-8[1m]`) — so the stored model is clean. */\nfunction cleanStr(v: unknown): string | null {\n const s = strOrNull(v);\n if (!s) return null;\n return s\n .replace(/\\x1b\\[[0-9;]*[A-Za-z]/g, \"\") // real ANSI escape sequences\n .replace(/\\[[0-9;]+m\\]?/g, \"\") // de-escaped leftovers: \"[1m\", \"[1m]\"\n .trim() || null;\n}\n\n/**\n * Ingest all workflow result sidecars for one source into `workflow_results`. `excludedDirs` are the\n * parsed exclude tokens (same filter the transcript walk uses). Returns nothing; mutates `stats`.\n */\nexport function ingestWorkflowResults(\n db: DB,\n sourceArchiveDir: string,\n sourceId: string,\n excludedDirs: string[],\n now: string,\n stats: WorkflowIngestStats,\n full: boolean,\n): void {\n const upsert = db.prepare(\n `INSERT INTO workflow_results\n (run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,\n result_json, phases_json, logs_json, progress_json,\n agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)\n VALUES\n (@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,\n @result_json, @phases_json, @logs_json, @progress_json,\n @agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)\n ON CONFLICT(run_id) DO UPDATE SET\n source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,\n workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,\n default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,\n logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,\n total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,\n duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,\n ingested_at=excluded.ingested_at`,\n );\n\n ingestSidecars(db, sourceArchiveDir, excludedDirs, now, stats, full, {\n subdir: \"workflows\",\n // Only the run's own result file (wf_<id>.json); agent-*.meta.json / scripts are handled elsewhere.\n matchName: (name) => /^wf_.+\\.json$/.test(name),\n handle: (path, buf) => {\n let d: Record<string, unknown>;\n try {\n const parsed = JSON.parse(buf.toString(\"utf8\"));\n if (!parsed || typeof parsed !== \"object\") return false;\n d = parsed as Record<string, unknown>;\n } catch {\n return false;\n }\n const runId = strOrNull(d.runId) ?? basename(path, \".json\");\n const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);\n const durationMs = intOrNull(d.durationMs);\n const endedAt = startedAt && durationMs != null ? new Date(Date.parse(startedAt) + durationMs).toISOString() : null;\n upsert.run({\n run_id: runId,\n source_id: sourceId,\n session_id: sessionBefore(path, \"workflows\"),\n task_id: strOrNull(d.taskId),\n workflow_name: strOrNull(d.workflowName),\n status: strOrNull(d.status),\n summary: strOrNull(d.summary),\n default_model: cleanStr(d.defaultModel),\n result_json: jsonOrNull(d.result),\n phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,\n logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,\n progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,\n agent_count: intOrNull(d.agentCount),\n total_tokens: intOrNull(d.totalTokens),\n total_tool_calls: intOrNull(d.totalToolCalls),\n duration_ms: durationMs,\n started_at: startedAt,\n ended_at: endedAt,\n ingested_at: now,\n });\n return true;\n },\n });\n}\n","/**\n * Agent Lens — shared Stage 2 sidecar-ingest machinery.\n *\n * The three sidecar ingesters (workflow results, subagent meta, spilled tool results) share the same\n * skeleton: recursively find files that live directly inside a named subdir (mirror first, then each\n * `.versions/<ts>/` snapshot), sort so the mirror wins on UPSERT, and run an incremental stat→hash\n * skip against `ingest_state` before handing each *changed* file to a per-sidecar handler. That\n * skeleton lives here once; each sidecar keeps only its own config: the subdir name, the filename\n * predicate, and the handler that parses + upserts one file.\n */\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { isExcludedArchivePath } from \"./redact.js\";\nimport { sha256 } from \"./fileread.js\";\n\n/** Counters every sidecar ingester reports (files seen / rows upserted / unchanged / unparseable). */\nexport interface SidecarStats {\n files: number;\n upserted: number;\n skipped: number;\n malformed: number;\n}\n\nexport function newSidecarStats(): SidecarStats {\n return { files: 0, upserted: 0, skipped: 0, malformed: 0 };\n}\n\nexport const intOrNull = (v: unknown): number | null => (typeof v === \"number\" && Number.isFinite(v) ? Math.trunc(v) : null);\nexport const strOrNull = (v: unknown): string | null => (typeof v === \"string\" && v ? v : null);\n\n/** The path segment immediately before `<subdir>/` (the launching/owning session id), or null. */\nexport function sessionBefore(path: string, subdir: string): string | null {\n const parts = path.split(\"/\");\n const i = parts.indexOf(subdir);\n return i > 0 ? parts[i - 1] : null;\n}\n\n/** Recursively collect files that sit directly inside a `<subdir>/` directory and match `matchName`. */\nfunction walk(dir: string, subdir: string, inSubdir: boolean, matchName: (name: string) => boolean, out: string[]): void {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const e of entries) {\n const p = join(dir, e.name);\n if (e.isDirectory()) walk(p, subdir, inSubdir || e.name === subdir, matchName, out);\n else if (e.isFile() && inSubdir && matchName(e.name)) out.push(p);\n }\n}\n\n/** All matching sidecar paths under a source archive (mirror first, then each `.versions` snapshot). */\nfunction discover(sourceArchiveDir: string, subdir: string, matchName: (name: string) => boolean): string[] {\n const out: string[] = [];\n walk(join(sourceArchiveDir, \"projects\"), subdir, false, matchName, out);\n try {\n for (const ts of readdirSync(join(sourceArchiveDir, \".versions\"), { withFileTypes: true }))\n if (ts.isDirectory()) walk(join(sourceArchiveDir, ts.name, \"projects\"), subdir, false, matchName, out);\n } catch {\n /* no versions yet */\n }\n return out;\n}\n\n/** Per-sidecar configuration for {@link ingestSidecars}. */\nexport interface SidecarConfig {\n /** The directory name whose direct children are sidecars (`workflows` / `subagents` / `tool-results`). */\n subdir: string;\n /** Filename predicate for a sidecar file. */\n matchName: (name: string) => boolean;\n /**\n * Optional pre-stat guard: return false to skip a discovered path entirely (no `files++`) — e.g. a\n * file outside the expected `…/<sessionId>/<subdir>/` layout.\n */\n include?: (path: string) => boolean;\n /**\n * Handle one *changed* file's bytes (`size` is the stat size, for a `bytes` column): parse + upsert.\n * Return true when a row was written (→ counted `upserted` and the file is marked ingested), or false\n * to count it `malformed` and leave it unmarked (so a persistently-bad file is retried, never\n * silently accepted).\n */\n handle: (path: string, buf: Buffer, size: number) => boolean;\n}\n\ninterface StateRow {\n size: number;\n mtime_ms: number;\n sha256: string;\n}\n\n/**\n * Discover, filter, sort, and incrementally skip sidecar files for one source, handing each changed\n * file to `cfg.handle`. Mutates `stats`. `full` forces a re-read (ignores the stat/hash short-circuit).\n */\nexport function ingestSidecars(\n db: DB,\n sourceArchiveDir: string,\n excludedDirs: string[],\n now: string,\n stats: SidecarStats,\n full: boolean,\n cfg: SidecarConfig,\n): void {\n let paths = discover(sourceArchiveDir, cfg.subdir, cfg.matchName);\n if (excludedDirs.length) paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));\n // Mirror last so it wins over any older .versions snapshot on UPSERT (a run that went\n // running→completed diverges: the completed copy is in the mirror).\n paths.sort((a, b) => Number(a.includes(\"/.versions/\")) - Number(b.includes(\"/.versions/\")) || (a < b ? -1 : 1));\n\n const getState = db.prepare(\"SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?\");\n const setState = db.prepare(\n `INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)\n VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)\n ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`,\n );\n\n for (const path of paths) {\n if (cfg.include && !cfg.include(path)) continue;\n stats.files++;\n const st = statSync(path);\n const mtimeMs = Math.trunc(st.mtimeMs);\n const prev = full ? undefined : (getState.get(path) as StateRow | undefined);\n if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {\n stats.skipped++;\n continue;\n }\n let buf: Buffer;\n try {\n buf = readFileSync(path);\n } catch {\n stats.malformed++;\n continue;\n }\n const hash = sha256(buf);\n if (prev && prev.sha256 === hash) {\n setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });\n stats.skipped++;\n continue;\n }\n if (cfg.handle(path, buf, st.size)) {\n setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });\n stats.upserted++;\n } else {\n stats.malformed++;\n }\n }\n}\n","/**\n * Agent Lens — metric-preserving transcript redactor (validation Layer 4).\n *\n * Produces a privacy-safe corpus that is realistic enough to validate the whole compute pipeline\n * end-to-end, by an explicit contract:\n *\n * PRESERVE (so metrics are identical to the raw input): uuid, parentUuid, type, timestamp,\n * isSidechain, isMeta, agentId (opaque hex, drives subagent linkage), message.{role,id,model,\n * usage,stop_reason}, toolUseResult numeric/linkage fields, tool_use {id,name}, and — crucially —\n * the *line count* of Edit/Write payloads + the *distinctness & extension* of file paths (these\n * feed the LoC / files complexity sub-scores).\n *\n * REDACT (deny-by-default): all message text/thinking, tool_result content, cwd & file paths\n * (pseudonymized, usernames stripped), slug/aiTitle/gitBranch, and any other free text. Stable\n * irreversible pseudonyms keep joins/counts intact (same input → same pseudonym).\n *\n * Token accounting, cost, durations, counts, subagent linkage, and the complexity *score* are\n * therefore identical between raw and redacted. The classifier *category* is text-derived and is\n * NOT preserved (validated separately by golden fixtures) — callers must exclude it from oracle\n * equality checks.\n *\n * HARD RULES (see memory test-corpus-redaction): never emit data from the agent-lens project\n * itself; scrub project names, paths, users, hosts, secrets. The CLI filters agent-lens sessions\n * and runs a final leak-scan before writing.\n */\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { parseSkillInjection } from \"./pipeline.js\";\n\nconst PLACEHOLDER = \"[redacted]\";\n\n/** Lines in a string, matching classify.ts/countLines exactly (trailing newline doesn't add a line). */\nfunction countLines(s: unknown): number {\n if (typeof s !== \"string\" || s.length === 0) return 0;\n const trimmed = s.endsWith(\"\\n\") ? s.slice(0, -1) : s;\n return trimmed.length === 0 ? 0 : trimmed.split(\"\\n\").length;\n}\n/** A dummy string whose countLines() == n (so LoC sub-scores are preserved). */\nfunction dummyLines(n: number): string {\n return n <= 0 ? \"\" : Array(n).fill(\"x\").join(\"\\n\");\n}\n\nexport class Redactor {\n private cache = new Map<string, string>();\n constructor(private salt: string = randomBytes(8).toString(\"hex\")) {}\n\n private hash(s: string, n = 6): string {\n return createHash(\"sha256\").update(this.salt).update(s).digest(\"hex\").slice(0, n);\n }\n private stable(kind: string, s: string): string {\n const key = `${kind}:${s}`;\n let v = this.cache.get(key);\n if (!v) {\n v = `${kind}-${this.hash(key)}`;\n this.cache.set(key, v);\n }\n return v;\n }\n\n /** Pseudonymize a path: strip the user's home, pseudonymize each segment, keep depth + extension. */\n path(p: unknown): string | null {\n if (typeof p !== \"string\" || !p) return typeof p === \"string\" ? p : null;\n const abs = p.startsWith(\"/\");\n const segs = p.split(\"/\").filter(Boolean);\n const out: string[] = [];\n for (let i = 0; i < segs.length; i++) {\n const seg = segs[i]!;\n // Drop the \"<user>\" right after a home root so usernames never survive.\n if ((segs[i - 1] === \"home\" || segs[i - 1] === \"Users\") && i >= 1) {\n out.push(\"user\");\n continue;\n }\n if (seg === \"home\" || seg === \"Users\") {\n out.push(seg);\n continue;\n }\n const last = i === segs.length - 1;\n const dot = last ? seg.lastIndexOf(\".\") : -1;\n if (dot > 0) out.push(`file-${this.hash(seg)}${seg.slice(dot)}`); // keep extension\n else out.push(this.stable(\"dir\", seg));\n }\n return (abs ? \"/\" : \"\") + out.join(\"/\");\n }\n\n /** Pseudonymize a free-form identifier (slug, label, branch). */\n ident(kind: string, s: unknown): string {\n return typeof s === \"string\" && s ? this.stable(kind, s) : `${kind}-none`;\n }\n\n /** Replace free text with a placeholder, PRESERVING emptiness (turn detection depends on it). */\n private text(s: unknown): string {\n return typeof s === \"string\" && s.length > 0 ? PLACEHOLDER : \"\";\n }\n\n /**\n * Preserve a skill-body injection (\"Base directory for this skill: …\\n\\n<SKILL.md body>\\n\\nARGUMENTS:…\")\n * instead of redacting it — the body is the only real skill content and drives version tracking.\n * We strip the per-call ARGUMENTS block, replace the home-path base-dir line with a non-leaking\n * `/skills/<name>` (keeping the skill-name tail so ingest still links it), keep the body verbatim,\n * and fail closed: if the result trips the leak scan, return null so the caller fully redacts it.\n * Returns null when `s` is not a skill injection.\n */\n private skillText(s: unknown): string | null {\n if (typeof s !== \"string\") return null;\n const inj = parseSkillInjection(s);\n if (!inj) return null;\n const nameTail = (inj.baseDir ?? \"\").split(\"/\").filter(Boolean).pop() ?? \"\";\n const rebuilt = `Base directory for this skill: /skills/${nameTail}\\n\\n${inj.body}\\n`;\n return findLeak(rebuilt) ? null : rebuilt;\n }\n\n private toolInput(name: string, input: unknown): Record<string, unknown> {\n if (!input || typeof input !== \"object\") return {};\n const inp = input as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n // Skill name (skill_name metric); only the Skill tool's command is non-sensitive enough to keep.\n if (typeof inp.skill === \"string\") out.skill = inp.skill;\n if (name === \"Skill\" && typeof inp.command === \"string\") out.command = inp.command;\n // Subagent type (agent_type metric).\n if ((name === \"Task\" || name === \"Agent\") && typeof inp.subagent_type === \"string\") out.subagent_type = inp.subagent_type;\n // LoC-bearing payloads: keep line counts + a pseudonymized path with its extension.\n if (name === \"Write\") {\n out.file_path = this.path(inp.file_path);\n out.content = dummyLines(countLines(inp.content));\n } else if (name === \"Edit\") {\n out.file_path = this.path(inp.file_path);\n out.old_string = dummyLines(countLines(inp.old_string));\n out.new_string = dummyLines(countLines(inp.new_string));\n }\n return out;\n }\n\n private block(b: unknown): unknown {\n if (!b || typeof b !== \"object\") return b;\n const blk = b as Record<string, unknown>;\n switch (blk.type) {\n case \"text\":\n return { type: \"text\", text: this.skillText(blk.text) ?? this.text(blk.text) };\n case \"thinking\":\n return { type: \"thinking\", thinking: this.text(blk.thinking) };\n case \"tool_use\":\n return { type: \"tool_use\", id: blk.id, name: blk.name, input: this.toolInput(String(blk.name), blk.input), ...(blk.caller ? { caller: blk.caller } : {}) };\n case \"tool_result\":\n return { type: \"tool_result\", tool_use_id: blk.tool_use_id, content: PLACEHOLDER, ...(blk.is_error ? { is_error: true } : {}) };\n default:\n return { type: blk.type };\n }\n }\n\n private message(m: unknown): unknown {\n if (!m || typeof m !== \"object\") return m;\n const msg = m as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const k of [\"role\", \"id\", \"model\", \"type\", \"usage\", \"stop_reason\", \"stop_sequence\"]) if (k in msg) out[k] = msg[k];\n if (typeof msg.content === \"string\") out.content = this.skillText(msg.content) ?? this.text(msg.content);\n else if (Array.isArray(msg.content)) out.content = msg.content.map((b: unknown) => this.block(b));\n return out;\n }\n\n /** Redact one parsed transcript line (object). */\n line(r: unknown): unknown {\n if (!r || typeof r !== \"object\") return r;\n const rec = r as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n // Structural / metric-bearing scalars kept verbatim.\n for (const k of [\"uuid\", \"parentUuid\", \"type\", \"timestamp\", \"isSidechain\", \"isMeta\", \"agentId\", \"userType\", \"version\"]) if (k in rec) out[k] = rec[k];\n if (typeof rec.cwd === \"string\") out.cwd = this.path(rec.cwd);\n if (typeof rec.gitBranch === \"string\") out.gitBranch = this.ident(\"branch\", rec.gitBranch);\n if (typeof rec.slug === \"string\") out.slug = this.ident(\"slug\", rec.slug);\n if (rec.type === \"ai-title\") out.aiTitle = \"Redacted session\";\n if (rec.toolUseResult && typeof rec.toolUseResult === \"object\") {\n const t = rec.toolUseResult as Record<string, unknown>;\n const tu: Record<string, unknown> = {};\n for (const k of [\"status\", \"agentType\", \"agentId\", \"resolvedModel\", \"totalDurationMs\", \"totalTokens\", \"totalToolUseCount\"]) if (k in t) tu[k] = t[k];\n out.toolUseResult = tu;\n }\n if (\"message\" in rec) out.message = this.message(rec.message);\n return out;\n }\n\n /** Redact a whole JSONL transcript (string in, string out). Malformed lines are dropped. */\n transcript(content: string): string {\n const lines: string[] = [];\n for (const raw of content.split(\"\\n\")) {\n if (!raw.trim()) continue;\n let obj: unknown;\n try {\n obj = JSON.parse(raw);\n } catch {\n continue;\n }\n lines.push(JSON.stringify(this.line(obj)));\n }\n return lines.join(\"\\n\") + \"\\n\";\n }\n}\n\n// Canonical Claude Code project-dir encoder now lives in core so collect + ingest agree exactly.\nexport { encodeProjectPath } from \"@agent-lens/core\";\nimport { encodeProjectPath } from \"@agent-lens/core\";\n// The fail-closed leak scan now lives in core (shared with the redacted-export sanitizer). Re-export\n// so this module's consumers (redact-cli.ts, tests) keep their `./redact.js` import unchanged.\nexport { LEAK_PATTERNS, findLeak } from \"@agent-lens/core\";\nimport { findLeak } from \"@agent-lens/core\";\n\n/** Parse the AGENT_LENS_REDACT_EXCLUDE CSV of real project paths into encoded-dir prefixes. */\nexport function parseExcludes(csv: string | undefined | null): string[] {\n return (csv ?? \"\")\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n .map(encodeProjectPath);\n}\n\n/** True if `encodedDir` is (or sits under) any excluded project path — used to keep data out of the corpus. */\nexport function isExcludedDir(encodedDir: string, excluded: string[]): boolean {\n return excluded.some((e) => encodedDir === e || encodedDir.startsWith(e + \"-\"));\n}\n\n/**\n * True if an archive file path belongs to an excluded project. Matches the `/projects/<encodedDir>/`\n * segment, so it catches both the main transcript AND its nested `<UUID>/subagents/agent-*.jsonl`.\n * `excluded` is the encoded-dir list from parseExcludes().\n */\nexport function isExcludedArchivePath(filePath: string, excluded: string[]): boolean {\n return excluded.some((e) => filePath.includes(`/projects/${e}/`));\n}\n","/**\n * Agent Lens — transcript file reading (ADR-010, impact 4).\n *\n * Small files are read whole (fast path); files larger than STREAM_THRESHOLD are streamed in fixed\n * chunks so ingest memory stays bounded regardless of transcript size. The streaming SHA-256 is\n * byte-identical to hashing a whole-file `readFileSync` buffer, so the incremental-skip check is\n * unaffected by which path a file takes. Kept separate from the CLI bootstrap so it is unit-testable.\n */\nimport { createHash } from \"node:crypto\";\nimport { openSync, readSync, closeSync } from \"node:fs\";\nimport { StringDecoder } from \"node:string_decoder\";\n\nexport const STREAM_THRESHOLD = 8 * 1024 * 1024;\nconst CHUNK = 64 * 1024;\n\n/** SHA-256 hex of a buffer. */\nexport function sha256(buf: Buffer): string {\n return createHash(\"sha256\").update(buf).digest(\"hex\");\n}\n\n/** SHA-256 hex of a file's bytes, computed by streaming. Equals sha256(readFileSync(path)). */\nexport function sha256File(path: string): string {\n const fd = openSync(path, \"r\");\n const h = createHash(\"sha256\");\n const buf = Buffer.allocUnsafe(CHUNK);\n try {\n let n: number;\n while ((n = readSync(fd, buf, 0, CHUNK, null)) > 0) h.update(buf.subarray(0, n));\n } finally {\n closeSync(fd);\n }\n return h.digest(\"hex\");\n}\n\n/**\n * Lazily yield a file's lines (newline-stripped) by streaming. A StringDecoder handles multibyte\n * characters split across chunk boundaries. Holds at most one line at a time, never the whole file.\n * Yields the same sequence as `readFileSync(path).toString(\"utf8\").split(\"\\n\")` minus the trailing\n * empty element a final newline would produce (ingest skips blank lines anyway).\n */\nexport function* streamLines(path: string): Generator<string> {\n const fd = openSync(path, \"r\");\n const decoder = new StringDecoder(\"utf8\");\n const buf = Buffer.allocUnsafe(CHUNK);\n let leftover = \"\";\n try {\n let n: number;\n while ((n = readSync(fd, buf, 0, CHUNK, null)) > 0) {\n leftover += decoder.write(buf.subarray(0, n));\n let idx: number;\n while ((idx = leftover.indexOf(\"\\n\")) !== -1) {\n yield leftover.slice(0, idx);\n leftover = leftover.slice(idx + 1);\n }\n }\n leftover += decoder.end();\n if (leftover.length > 0) yield leftover;\n } finally {\n closeSync(fd);\n }\n}\n","/**\n * Agent Lens — Stage 2 ingest of per-subagent metadata sidecars.\n *\n * When a subagent (Task/Agent, or a Workflow fan-out agent) runs, Claude Code writes a metadata\n * sidecar next to its transcript:\n * <archive>/<source>/projects/<enc>/<sessionId>/subagents/agent-<id>.meta.json\n * carrying { agentType, description, spawnDepth?, toolUseId }. This is the authoritative source of a\n * subagent's type and human title — Workflow fan-out agents carry no subagent_type on the launching\n * tool_call, so ~700 of them are otherwise untyped. The filename stem `agent-<id>` IS the subagent's\n * session id (the same id `discover()` assigns the transcript), so we key `session_meta` on it and\n * LEFT JOIN at read time (no dependency on the session row existing yet).\n *\n * Idempotent: keyed by session id, UPSERT on re-ingest. The discover/skip/state machinery is shared\n * with the other sidecar ingesters in `sidecar.ts`; only the parse + upsert below is meta-specific.\n */\nimport { basename } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { ingestSidecars, newSidecarStats, intOrNull, strOrNull, type SidecarStats } from \"./sidecar.js\";\n\nexport type MetaIngestStats = SidecarStats;\nexport const newMetaStats = newSidecarStats;\n\n/**\n * Ingest all subagent meta sidecars for one source into `session_meta`. `excludedDirs` are the parsed\n * exclude tokens (same filter the transcript walk uses). Returns nothing; mutates `stats`.\n */\nexport function ingestSubagentMeta(\n db: DB,\n sourceArchiveDir: string,\n sourceId: string,\n excludedDirs: string[],\n now: string,\n stats: MetaIngestStats,\n full: boolean,\n): void {\n const upsert = db.prepare(\n `INSERT INTO session_meta\n (session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)\n VALUES\n (@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)\n ON CONFLICT(session_id) DO UPDATE SET\n source_id=excluded.source_id, agent_type=excluded.agent_type,\n agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,\n tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`,\n );\n\n ingestSidecars(db, sourceArchiveDir, excludedDirs, now, stats, full, {\n subdir: \"subagents\",\n matchName: (name) => /^agent-.+\\.meta\\.json$/.test(name),\n handle: (path, buf) => {\n let d: Record<string, unknown>;\n try {\n const parsed = JSON.parse(buf.toString(\"utf8\"));\n if (!parsed || typeof parsed !== \"object\") return false;\n d = parsed as Record<string, unknown>;\n } catch {\n return false;\n }\n // Filename stem is the subagent's session id: `agent-<id>.meta.json` → `agent-<id>`.\n upsert.run({\n session_id: basename(path, \".meta.json\"),\n source_id: sourceId,\n agent_type: strOrNull(d.agentType),\n agent_description: strOrNull(d.description),\n spawn_depth: intOrNull(d.spawnDepth),\n tool_use_id: strOrNull(d.toolUseId),\n ingested_at: now,\n });\n return true;\n },\n });\n}\n","/**\n * Agent Lens — Stage 2 ingest of spilled full tool-result files.\n *\n * When a tool's output is too large for the transcript, Claude Code writes the full output to disk and\n * keeps only a short summary in the transcript naming the file:\n * <archive>/<source>/projects/<enc>/<sessionId>/tool-results/<name>.txt\n * (the summary reads \"Output too large (…). Full output saved to: …/tool-results/<name>.txt\"). The\n * basename <name> is NOT the tool_use id — it's a short random token — so we key `tool_results` on\n * (session_id, name) and join at read time by parsing that marker out of tool_calls.result_summary.\n *\n * Idempotent: keyed by (session_id, name), UPSERT on re-ingest. The discover/skip/state machinery is\n * shared with the other sidecar ingesters in `sidecar.ts`; only the upsert below is result-specific.\n */\nimport { basename } from \"node:path\";\nimport type { DB } from \"./db.js\";\nimport { ingestSidecars, newSidecarStats, sessionBefore, type SidecarStats } from \"./sidecar.js\";\n\nexport type ToolResultIngestStats = SidecarStats;\nexport const newToolResultStats = newSidecarStats;\n\n/**\n * Ingest all spilled tool-result files for one source into `tool_results`. `excludedDirs` are the\n * parsed exclude tokens (same filter the transcript walk uses). Returns nothing; mutates `stats`.\n */\nexport function ingestToolResults(\n db: DB,\n sourceArchiveDir: string,\n excludedDirs: string[],\n now: string,\n stats: ToolResultIngestStats,\n full: boolean,\n): void {\n const upsert = db.prepare(\n `INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)\n VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)\n ON CONFLICT(session_id, name) DO UPDATE SET\n path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`,\n );\n\n ingestSidecars(db, sourceArchiveDir, excludedDirs, now, stats, full, {\n subdir: \"tool-results\",\n matchName: (name) => name.endsWith(\".txt\"),\n // Skip a file outside the expected …/<sessionId>/tool-results/ layout (no files++, matching the\n // original early-continue before statting).\n include: (path) => sessionBefore(path, \"tool-results\") !== null,\n handle: (path, buf, size) => {\n const sessionId = sessionBefore(path, \"tool-results\")!; // include() guaranteed non-null\n upsert.run({\n session_id: sessionId,\n name: basename(path, \".txt\"),\n path,\n bytes: size,\n text: buf.toString(\"utf8\"),\n ingested_at: now,\n });\n return true;\n },\n });\n}\n","/**\n * Agent Lens — Stage 3 local server, as an importable function (ADR-005: 127.0.0.1 only, no egress).\n *\n * `startServer` is the library entrypoint so the unified `agent-lens` CLI can bundle and call it\n * in-process; the thin bin in index.ts just forwards to it. Read-only REST over the SQLite store +\n * Markdown export, and serves the built web SPA. (env: AGENT_LENS_DB, AGENT_LENS_PORT, AGENT_LENS_HOST)\n */\nimport { existsSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { findRepoRoot, installPricing, resolveDbPath, resolveServerConfig, resolveWebDist, triageDbFor } from \"@agent-lens/core\";\nimport { openReadonly } from \"./db.js\";\nimport { createApp } from \"./app.js\";\n\n// Re-exported for the unified CLI (bundles this package): the read-only DB open + the shared\n// session-export helpers back the `agent-lens export` command without a second server process.\nexport { openReadonly } from \"./db.js\";\nexport { renderSessionExport, parseRedactionLevel } from \"./export.js\";\n\n/** Per-invocation overrides (from `serve --port/--host/--db`); each takes precedence over env + file. */\nexport interface StartServerOverrides {\n port?: number | string;\n host?: string;\n db?: string;\n}\n\nexport async function startServer(overrides: StartServerOverrides = {}): Promise<void> {\n // Resolve relative to this module: in dev it's packages/server/dist; bundled into the CLI it's the\n // published package root (web SPA sits at ../web, data falls back to the per-user dir).\n const here = dirname(fileURLToPath(import.meta.url));\n const repoRoot = findRepoRoot(here);\n // db/port/host all resolve with precedence flag > env > file > default (bad port fails fast).\n const { path: dbPath, origin: dbOrigin } = resolveDbPath(overrides.db);\n const { host, port } = resolveServerConfig({ port: overrides.port, host: overrides.host });\n // Install the effective price table before anything derives a cost (ADR-028). Malformed overrides\n // warn here and fall back to the built-in rate; the resolved facts go to /api/about.\n const pricing = installPricing();\n const webDist = resolveWebDist(here, repoRoot);\n\n if (host !== \"127.0.0.1\" && host !== \"localhost\" && !process.env.AGENT_LENS_ALLOW_NONLOCAL) {\n console.error(\n `agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). ` +\n `Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`,\n );\n process.exit(1);\n }\n if (!existsSync(dbPath)) {\n console.error(`agent-lens-server: db not found: ${dbPath} (run ingest first)`);\n process.exit(1);\n }\n\n // Triage store (ADR-018) sits beside the analytics DB and is never touched by ingest, so user triage\n // survives `ingest --full`. Opened read-write by createApp; the analytics handle stays read-only.\n const triageDbPath = triageDbFor(dbPath);\n\n const db = openReadonly(dbPath);\n // Enforce a loopback Host allowlist (DNS-rebinding defense) whenever we're bound to loopback. An\n // intentional non-loopback bind (host set + AGENT_LENS_ALLOW_NONLOCAL, checked above) opts out.\n const enforceLoopbackHost = host === \"127.0.0.1\" || host === \"localhost\";\n // /api/about reports what this process actually resolved (ADR-027) — pass the facts down rather\n // than letting it re-resolve, or `serve --db /custom.db` would report the default path instead.\n const app = await createApp(db, {\n webDist,\n triageDbPath,\n enforceLoopbackHost,\n about: { db: { path: dbPath, origin: dbOrigin }, host, port, loopbackOnly: enforceLoopbackHost, repoRoot, pricing },\n });\n\n try {\n await app.listen({ host, port });\n console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);\n if (!existsSync(webDist)) console.log(\" note: web SPA not built — run `pnpm --filter @agent-lens/web build`\");\n } catch (err) {\n console.error(err);\n process.exit(1);\n }\n}\n","import { DatabaseSync } from \"node:sqlite\";\nimport { SCHEMA_VERSION, severityRank, activePricing, SECURITY_CATEGORIES, errorKind, type ToolErrorType, type Rate } from \"@agent-lens/core\";\nimport { TASK_FAILURES_TAG, TASK_NOTIFICATION_TAG, TASK_RESULT_TAG, TASK_STATUS_TAG, TASK_SUMMARY_TAG, xmlTag } from \"@agent-lens/transcript-format\";\nimport type {\n Finding,\n FindingListRow,\n FileChangeRow,\n FileSummary,\n FileTimeline,\n FileTimelineSession,\n FilesPage,\n EventNode,\n Classification,\n ClassificationSignals,\n Project,\n SecuritySummary,\n SessionChild,\n SessionDetail,\n SessionDetailData,\n SessionParent,\n SessionRow,\n SessionSummary,\n SessionsPage,\n SkillDetail,\n SkillSummary,\n SkillSession,\n SkillVersion,\n Source,\n ToolCall,\n TurnRow,\n WorkflowAgent,\n WorkflowCompletion,\n WorkflowDetail,\n WorkflowRun,\n WorkflowRunResult,\n FindingsPage,\n} from \"@agent-lens/contracts\";\nimport { tableExists, queryAll, queryGet, orderBy, pushGrouped, pushDateRange, metaJoin, metaProjection } from \"./sql-util.js\";\nimport type {\n CategoryRow,\n ToolCallProjection,\n ClassificationProjectionRow,\n CountRow,\n EventProjectionRow,\n FindingProjectionRow,\n FileTimelineRow,\n ModelRow,\n SessionChildRow,\n SessionListRow,\n SessionParentRow,\n SkillListRow,\n SkillSessionRow,\n ToolResultRow,\n WorkflowAgentRow,\n WorkflowLaunchRow,\n WorkflowResultRow,\n} from \"./rows.js\";\nimport { sessionUsage, splitOf, tokensOf, costOf, unpricedOf, USAGE_SUMS, type UsageRow } from \"./usage.js\";\n\nexport type DB = DatabaseSync;\n\nexport function openReadonly(file: string): DB {\n // `readOnly` also supplies the old `fileMustExist`: node:sqlite refuses to create a DB it may not\n // write. `query_only` is belt-and-braces on top — it also blocks writes through an ATTACHed handle.\n const db = new DatabaseSync(file, { readOnly: true });\n db.exec(\"PRAGMA query_only = ON\");\n return db;\n}\n\n/**\n * Compare the DB's stamped schema version against the version this server build expects. `stale` means\n * the on-disk DB was written by an older schema (a bump requires `agent-lens ingest --full` to rebuild)\n * — the UI surfaces this so a mismatch doesn't silently show missing/empty columns. `db_version` is null\n * on an unstamped/never-ingested DB (treated as not-stale: nothing to warn about yet).\n */\nexport function schemaStatus(db: DB): { db_version: number | null; expected: number; stale: boolean } {\n const row = db.prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\").get() as\n | { value: string }\n | undefined;\n const dbVersion = row ? Number(row.value) : null;\n const db_version = dbVersion != null && Number.isFinite(dbVersion) ? dbVersion : null;\n return { db_version, expected: SCHEMA_VERSION, stale: db_version != null && db_version !== SCHEMA_VERSION };\n}\n\n/**\n * Wall-clock time of the most recent ingest run that wrote anything, as an ISO8601 string (or null on\n * a never-ingested DB). Every archive file touched by a run gets the same `ingested_at` stamp, so the\n * max across `ingest_state` is exactly \"data last refreshed at\". Powers the header freshness readout.\n */\nexport function lastIngested(db: DB): string | null {\n const row = db.prepare(\"SELECT MAX(ingested_at) AS last FROM ingest_state\").get() as { last: string | null };\n return row?.last ?? null;\n}\n\nexport interface SessionFilters {\n source?: string;\n project?: string;\n model?: string;\n q?: string;\n from?: string;\n to?: string;\n kind?: \"main\" | \"subagent\";\n /** Keep only sessions that have ≥1 finding of one of these severities. */\n severity?: string[];\n /** Keep only sessions that have ≥1 errored tool call of one of these error types. */\n errorType?: string[];\n /** Column to sort the whole filtered list by (before pagination). Defaults to \"started\". */\n sort?: \"started\" | \"title\" | \"turns\" | \"tokens\" | \"cost\" | \"duration\" | \"errors\" | \"security\";\n dir?: \"asc\" | \"desc\";\n limit: number;\n offset: number;\n}\n\nexport function listSources(db: DB): Source[] {\n // Count MAIN sessions only (is_sidechain = 0): the dropdown filters the session list, which\n // defaults to main-only, so a source's count must match what you'd see — not be inflated by the\n // many subagent sidechains each task spawns.\n return queryAll<Source>(\n db,\n `SELECT s.id, s.label,\n (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count\n FROM sources s ORDER BY s.label`,\n );\n}\n\nexport function listProjects(db: DB): Project[] {\n return queryAll<Project>(\n db,\n `SELECT p.id, p.path,\n (SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count\n FROM projects p ORDER BY session_count DESC, p.path`,\n );\n}\n\nexport function listModels(db: DB): string[] {\n return queryAll<ModelRow>(db, `SELECT DISTINCT model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).map(\n (r) => r.model,\n );\n}\n\n/**\n * Turn a user's search box input into a safe FTS5 MATCH expression. The raw string is otherwise\n * parsed as FTS5 *query syntax*, so a hyphen, colon (column filter), or bareword operator throws\n * (e.g. \"swe-workflow\" → `SQLITE_ERROR: no such column: workflow`). We treat the input as literal\n * terms: split on whitespace and wrap each token in double quotes (doubling embedded quotes per\n * FTS5 escaping), joined by space (implicit AND). Phrasing also lets the tokenizer split intra-token\n * punctuation, so \"swe-workflow\" matches the adjacent tokens \"swe workflow\". Returns \"\" for input\n * with no usable tokens (caller guards on a non-empty trimmed query). */\nfunction toFtsQuery(raw: string): string {\n return raw\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n .join(\" \");\n}\n\n/** Total tokens for one session (all four classes), matching `tokensOf(splitOf(…))`. */\nconst TOKENS_SORT_SQL = `(SELECT COALESCE(SUM(t.input_tokens + t.output_tokens\n + t.cache_creation_input_tokens + t.cache_read_input_tokens), 0)\n FROM token_usage t WHERE t.session_id = s.id)`;\n\n/**\n * Security ordering: worst severity first, finding count as the tiebreak — the same metric the JS\n * sort computed as `(severityRank(worst) + 1) * 1e6 + finding_count`. The `+ 1` shift is why an\n * unranked/absent severity is 0 here (`severityRank` returns -1 for it), matching SEVERITY_ORDER.\n */\nconst SECURITY_SORT_SQL = `(CASE worst_severity\n WHEN 'critical' THEN 5 WHEN 'high' THEN 4 WHEN 'medium' THEN 3 WHEN 'low' THEN 2 WHEN 'info' THEN 1\n ELSE 0 END) * 1000000 + finding_count`;\n\n/**\n * Cost for one session, in SQL — a transcription of `costForUsage` (rate per 1M tokens, per class)\n * that lets the session list ORDER BY cost without costing every matching session in JS first.\n *\n * The rates it joins come from {@link ratesCte}, never from a literal table: pricing is\n * config-overridable (ADR-028), so the active table is the only correct source. The join reproduces\n * `rateForModel`'s **longest-prefix** match — an equality join would price a dated id like\n * `claude-haiku-4-5-20251001` at zero, which is precisely the silent-$0 failure ADR-028 was cut to\n * fix. `substr` rather than `LIKE` because `_` is a LIKE wildcard and model ids may contain one.\n *\n * This duplicates the cost formula (the other copy is `costForUsage`); `pricing-sql.test.ts` asserts\n * the two agree, including under an override.\n */\nconst COST_SORT_SQL = `(SELECT COALESCE(SUM((tu.input_tokens * mr.i_rate + tu.output_tokens * mr.o_rate\n + tu.cache_creation_input_tokens * mr.cw_rate + tu.cache_read_input_tokens * mr.cr_rate) / 1000000.0), 0)\n FROM token_usage tu LEFT JOIN model_rate mr ON mr.model IS tu.model\n WHERE tu.session_id = s.id)`;\n\n/**\n * A `WITH rates(…)` clause carrying the *active* price table (built-in defaults merged with any\n * config overrides) as bound parameters. Built per query, never cached at module scope — `usePricing`\n * can swap the table at startup and tests install their own.\n */\nexport function ratesCte(): { sql: string; params: unknown[] } {\n const entries = Object.entries(activePricing()) as Array<[string, Rate]>;\n return {\n // `model_rate` resolves the prefix match ONCE per distinct model id, not once per token_usage row.\n // Inlining the lookup into the cost expression instead re-ran it for every usage row of every\n // candidate session — ~35× slower on a real corpus, which would have made this \"fix\" a regression.\n sql: `WITH rates(prefix, i_rate, o_rate, cw_rate, cr_rate) AS (VALUES ${entries.map(() => \"(?,?,?,?,?)\").join(\",\")}),\n model_rate AS (\n SELECT m.model, r.i_rate, r.o_rate, r.cw_rate, r.cr_rate\n FROM (SELECT DISTINCT model FROM token_usage) m\n LEFT JOIN rates r ON r.prefix = (\n SELECT p.prefix FROM rates p WHERE substr(m.model, 1, length(p.prefix)) = p.prefix\n ORDER BY length(p.prefix) DESC LIMIT 1)) `,\n params: entries.flatMap(([prefix, r]) => [prefix, r.input, r.output, r.cacheWrite, r.cacheRead]),\n };\n}\n\nexport function listSessions(db: DB, f: SessionFilters): SessionsPage {\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.source) (where.push(\"s.source_id = ?\"), params.push(f.source));\n if (f.project) (where.push(\"s.project_id = ?\"), params.push(f.project));\n // Date-inclusive on both ends (see findingWhere): a picked `to` day must include that day's sessions.\n pushDateRange(where, params, \"s.started_at\", f.from, f.to);\n if (f.kind === \"main\") where.push(\"s.is_sidechain = 0\");\n if (f.kind === \"subagent\") where.push(\"s.is_sidechain = 1\");\n // Multi-select: session has ≥1 finding of any listed severity / ≥1 errored tool call of any listed type.\n if (f.severity?.length) {\n where.push(`EXISTS (SELECT 1 FROM findings fd WHERE fd.session_id = s.id AND fd.severity IN (${f.severity.map(() => \"?\").join(\",\")}))`);\n params.push(...f.severity);\n }\n if (f.errorType?.length) {\n where.push(`EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.session_id = s.id AND tc.error_type IN (${f.errorType.map(() => \"?\").join(\",\")}))`);\n params.push(...f.errorType);\n }\n if (f.model) {\n where.push(\"EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND t.model = ?)\");\n params.push(f.model);\n }\n if (f.q && f.q.trim()) {\n // Match transcript text (FTS) OR the session's own name (slug/ai_title) OR its project path, so a\n // query like \"fix-youtube-embed-csp-policy\" finds the session by slug even when that string never\n // appears verbatim in an event. A project-path *subquery* (not a join on p.path) keeps the\n // un-joined COUNT(*) query above valid. SQLite LIKE is case-insensitive for ASCII.\n const like = `%${f.q.trim()}%`;\n where.push(\n `(s.id IN (SELECT DISTINCT e.session_id FROM events e JOIN events_fts f ON f.rowid = e.rowid WHERE events_fts MATCH ?)\n OR s.slug LIKE ? OR s.ai_title LIKE ?\n OR s.project_id IN (SELECT id FROM projects WHERE path LIKE ?))`,\n );\n params.push(toFtsQuery(f.q), like, like, like);\n }\n const whereSql = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n\n const total = queryGet<CountRow>(db, `SELECT COUNT(*) n FROM sessions s ${whereSql}`, ...params)!.n;\n\n const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,\n s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,\n (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,\n (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,\n (SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id AND tc.status = 'error') AS tool_error_count,\n (SELECT COUNT(*) FROM findings fd WHERE fd.session_id = s.id) AS finding_count,\n (SELECT fd.severity FROM findings fd WHERE fd.session_id = s.id\n ORDER BY CASE fd.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 WHEN 'info' THEN 0 ELSE -1 END DESC\n LIMIT 1) AS worst_severity\n FROM sessions s LEFT JOIN projects p ON p.id = s.project_id\n ${whereSql}`;\n\n // Every sortable column resolves to a SQL expression, so ORDER BY + LIMIT do the paging. Stored\n // columns are direct; the derived ones (tokens/cost/errors/security) are the subquery or CASE the\n // projection above already computes, referenced by output alias where one exists. These four used\n // to sort in JS over the WHOLE matching set — O(corpus) on every request, per the comment this\n // replaces — which is what made the session list the worst-scaling query in the server.\n const ORDER = {\n started: \"s.started_at\",\n title: \"COALESCE(s.ai_title, s.slug)\",\n turns: \"s.turn_count\",\n duration: \"s.duration_ms\",\n errors: \"tool_error_count\",\n tokens: TOKENS_SORT_SQL,\n cost: COST_SORT_SQL,\n security: SECURITY_SORT_SQL,\n };\n const sortKey = f.sort ?? \"started\";\n\n // The cost expression joins a rates CTE, so that clause has to lead the statement — and its bound\n // parameters therefore come before the WHERE's. Only built when actually sorting by cost.\n const rates = sortKey === \"cost\" ? ratesCte() : null;\n const sessions = attachSessionCost(\n db,\n queryAll<SessionListRow>(\n db,\n `${rates?.sql ?? \"\"}${baseSelect} ORDER BY ${orderBy(ORDER, sortKey, \"started\", f.dir)}, s.id ASC LIMIT ? OFFSET ?`,\n ...(rates?.params ?? []),\n ...params,\n f.limit,\n f.offset,\n ),\n );\n\n return { total, sessions };\n}\n\n/** Add per-session `tokens`, `token_split`, `cost` (USD, cache-aware) and `title` to each list row,\n * turning the projection into the response shape. Costs are grouped by model so per-model rates apply.\n * IDs are chunked so the whole-list sort path (which can pass thousands of sessions) stays under\n * SQLite's bound-parameter limit. */\nfunction attachSessionCost(db: DB, rows: SessionListRow[]): SessionSummary[] {\n // Group each session's per-model usage rows, then reuse the shared split/cost helpers. IDs are\n // chunked so the whole-list sort path (which can pass thousands of sessions) stays under SQLite's\n // bound-parameter limit.\n const usageBySession = new Map<string, UsageRow[]>();\n const ids = rows.map((r) => r.id);\n const CHUNK = 800;\n for (let i = 0; i < ids.length; i += CHUNK) {\n const batch = ids.slice(i, i + CHUNK);\n const ph = batch.map(() => \"?\").join(\",\");\n const usage = queryAll<UsageRow & { session_id: string }>(\n db,\n `SELECT session_id, model, ${USAGE_SUMS} FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`,\n ...batch,\n );\n for (const u of usage) {\n const list = usageBySession.get(u.session_id) ?? [];\n list.push(u);\n usageBySession.set(u.session_id, list);\n }\n }\n return rows.map(({ ai_title, slug, ...r }) => {\n const u = usageBySession.get(r.id) ?? [];\n const split = splitOf(u);\n return {\n ...r,\n tokens: tokensOf(split),\n token_split: split,\n cost: costOf(u),\n unpriced_models: unpricedOf(u),\n title: ai_title || slug || null,\n };\n });\n}\n\n/** The session's tool calls, with any spilled full output attached.\n *\n * When a result_summary is the \"Full output saved to: …/tool-results/<name>.txt\" marker (the\n * transcript's 280-char stand-in), the un-truncated text is pulled from tool_results so the UI can\n * expand it. Keyed by (session_id, name); guarded for a pre-ingest DB. */\nfunction loadToolCalls(db: DB, id: string): ToolCallProjection[] {\n const toolRows = queryAll<ToolCallProjection>(\n db,\n `SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,\n workflow_run_id, workflow_name, status, error_type,\n total_duration_ms, input_json, result_summary,\n (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count\n FROM tool_calls WHERE session_id = ?`,\n id,\n );\n\n if (tableExists(db, \"tool_results\")) {\n const getFull = db.prepare(\"SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?\");\n for (const t of toolRows) {\n const m = typeof t.result_summary === \"string\" ? t.result_summary.match(/tool-results\\/([A-Za-z0-9_-]+)\\.txt/) : null;\n if (!m) continue;\n const full = getFull.get(id, m[1]) as ToolResultRow | undefined;\n if (full) t.full_result = { text: full.text, bytes: full.bytes };\n }\n }\n return toolRows;\n}\n\n/** Security findings (ADR-017): attaches each tool call's findings inline (for the transcript severity\n * badge + \"why\" panel) and returns the session-level list for the header summary, most-severe first.\n * Guarded for a read-only pre-ingest DB whose schema predates the findings table. */\nfunction loadFindings(db: DB, id: string, toolRows: ToolCallProjection[]): Finding[] {\n if (!tableExists(db, \"findings\")) return [];\n const sessionFindings: Finding[] = [];\n const findingRows = queryAll<FindingProjectionRow>(\n db,\n `SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json\n FROM findings WHERE session_id = ?`,\n id,\n );\n const byToolCall = new Map<string, Finding[]>();\n for (const fr of findingRows) {\n const f: Finding = {\n id: fr.id,\n tool_call_id: fr.tool_call_id,\n event_uuid: fr.event_uuid,\n turn_id: fr.turn_id,\n rule_id: fr.rule_id,\n category: fr.category,\n framework_ref: fr.framework_ref,\n severity: fr.severity,\n title: fr.title,\n evidence: fr.evidence,\n // Asserted, not validated: safeJson returns unknown and this is the assertion (ADR-026).\n signals: safeJson(fr.signals_json) as Finding[\"signals\"],\n };\n sessionFindings.push(f);\n if (fr.tool_call_id) pushGrouped(byToolCall, fr.tool_call_id, f);\n }\n for (const t of toolRows) t.findings = t.id ? (byToolCall.get(t.id) ?? []) : [];\n // Most-severe first so a session banner can lead with the worst.\n sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));\n return sessionFindings;\n}\n\n/** The session's transcript events in order, each with its flattened text/thinking and its tool calls. */\n/** Drop the columns the query needed but the response does not ship (ADR-026): `event_uuid` has just\n * been used to nest this call under its event, and `error_type` feeds the server-side\n * failure-vs-rejection split and reaches the UI only as a dashboard breakdown. */\nfunction toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }: ToolCallProjection): ToolCall {\n return call;\n}\n\nfunction loadEvents(db: DB, id: string, toolRows: ToolCallProjection[]): EventNode[] {\n const eventRows = queryAll<EventProjectionRow>(\n db,\n `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, text, thinking\n FROM events WHERE session_id = ? ORDER BY timestamp, seq`,\n id,\n );\n\n const toolsByEvent = new Map<string, ToolCallProjection[]>();\n for (const t of toolRows) {\n if (!t.event_uuid) continue;\n pushGrouped(toolsByEvent, t.event_uuid, t);\n }\n\n return eventRows.map((e) => ({\n uuid: e.uuid,\n type: e.type,\n role: e.role,\n timestamp: e.timestamp,\n model: e.model,\n is_sidechain: e.is_sidechain,\n turn_id: e.turn_id,\n text: e.text,\n thinking: e.thinking,\n toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall),\n }));\n}\n\n/** Heuristic classification: category + complexity + the signals that produced them\n * (tool/skill mix, LoC, files, subagent count) — already captured in signals_json by the classifier. */\nfunction loadClassification(db: DB, id: string): Classification | null {\n const cls = queryGet<ClassificationProjectionRow>(\n db,\n `SELECT category, complexity_score, complexity_band, signals_json, classifier_version\n FROM classifications WHERE scope = 'session' AND target_id = ?`,\n id,\n );\n if (!cls) return null;\n // Asserted, not validated (ADR-026) — a malformed blob degrades to null, a well-formed one of the\n // wrong shape reaches the UI as declared.\n let signals: ClassificationSignals | null = null;\n try {\n signals = cls.signals_json ? (JSON.parse(cls.signals_json) as ClassificationSignals) : null;\n } catch {\n /* ignore malformed */\n }\n return {\n category: cls.category,\n complexity_score: cls.complexity_score,\n complexity_band: cls.complexity_band,\n classifier_version: cls.classifier_version,\n signals,\n };\n}\n\n/** Subagent linkage (schema v3): if this is a sidechain session, resolve the parent turn/session that\n * spawned it so the UI can offer a \"spawned by\" crumb back to the originating turn. */\nfunction loadParent(db: DB, session: SessionRow): SessionParent | null {\n if (!session.parent_session_id) return null;\n const p = queryGet<SessionParentRow>(\n db,\n `SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq\n FROM sessions ps LEFT JOIN turns t ON t.id = ?\n WHERE ps.id = ?`,\n session.parent_turn_id,\n session.parent_session_id,\n );\n return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;\n}\n\n/** Spawned subagents (schema v3): sessions whose parent_session_id points back here. Nesting them\n * under the parent is why the flat session list defaults to main-only — a task with N subagents is\n * one row with N children, not N+1 sibling rows sharing the same slug.\n * Subagent metadata sidecars (session_meta) supply the authoritative agentType/description/spawnDepth\n * — LEFT JOIN so a subagent still lists when its meta hasn't been ingested (or the table is absent on\n * a pre-ingest read-only DB). */\nfunction loadChildren(db: DB, id: string): SessionChild[] {\n const hasMeta = tableExists(db, \"session_meta\");\n const children = queryAll<SessionChildRow>(\n db,\n `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,\n (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models\n ${metaProjection(hasMeta)}\n FROM sessions s ${metaJoin(hasMeta)}\n WHERE s.parent_session_id = ? ORDER BY s.started_at`,\n id,\n );\n return children.map(({ ai_title, slug, ...ch }) => {\n const u = sessionUsage(db, ch.id);\n return { ...ch, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };\n });\n}\n\n/** Workflow runs launched from THIS session: each Workflow tool_call carries a run id + name and sits\n * on a turn. This lets the UI group the spawned subagents by run and link each group back to the\n * exact launching turn (turn_seq) — instead of one flat, unattributed fan-out list.\n * Run status rides along from the result sidecar (workflow_results) when ingested, so the session's\n * workflow-run groups can flag a failed/completed run at a glance. NULL when no sidecar (async, still\n * pending) or the table isn't present yet (read-only pre-ingest DB). */\nfunction loadWorkflowRuns(db: DB, id: string): WorkflowRun[] {\n const hasWR = tableExists(db, \"workflow_results\");\n return queryAll<WorkflowRun>(\n db,\n `SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,\n (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count\n ${hasWR ? \", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status\" : \", NULL AS status\"}\n FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id\n WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL\n ORDER BY t.seq`,\n id,\n );\n}\n\n/** File-modification provenance (ADR-022): the session's derived Edit/Write/NotebookEdit file\n * changes, for the \"Files changed\" header roll-up. Guarded for a pre-v14 read-only DB. */\nfunction loadFileChanges(db: DB, id: string): FileChangeRow[] {\n if (!tableExists(db, \"file_changes\")) return [];\n return queryAll<FileChangeRow>(\n db,\n `SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,\n fc.lines_added, fc.lines_removed, fc.timestamp\n FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`,\n id,\n );\n}\n\n/** Roll-ups written onto the session row for the detail header: token split + cost, display title, and\n * the tool-call error counts (computed from the already-fetched toolRows, so no extra query).\n *\n * Tokens keep their categories split so the UI can show input/output/cache-write/cache-read separately.\n * Cache stays IN the total and IS cost-attributed (at its discounted rate) — differentiated, never\n * dropped. The raw error count is authoritative; the failure-vs-rejection split uses the stored\n * heuristic error_type (rejection = the user/guardrail declined the call, not an agent failure). */\nfunction withSessionTotals(\n db: DB,\n session: SessionRow & { project_path: string | null },\n id: string,\n toolRows: ToolCallProjection[],\n): SessionDetailData {\n const usage = sessionUsage(db, id);\n const split = splitOf(usage);\n\n const tool_error_count = toolRows.filter((t) => t.status === \"error\").length;\n // `error_type` is a plain column read, so nothing proves it is still one of core's ToolErrorType\n // literals — an older DB could hold a bucket name that has since been renamed. errorKind is a set\n // membership test that answers \"failure\" for anything unrecognized, so the cast is safe rather than\n // merely convenient; it replaces an `any` that was making the same assumption invisibly.\n const tool_rejection_count = toolRows.filter(\n (t) => t.status === \"error\" && t.error_type && errorKind(t.error_type as ToolErrorType) === \"rejection\",\n ).length;\n\n return {\n ...session,\n tokens: tokensOf(split),\n token_split: split,\n cost: costOf(usage),\n unpriced_models: unpricedOf(usage),\n title: session.ai_title || session.slug || null,\n tool_call_count: toolRows.length,\n tool_error_count,\n tool_rejection_count,\n tool_failure_count: tool_error_count - tool_rejection_count,\n };\n}\n\nexport function getSession(db: DB, id: string): SessionDetail | null {\n const session = queryGet<SessionRow & { project_path: string | null }>(\n db,\n `SELECT s.*, p.path AS project_path FROM sessions s\n LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`,\n id,\n );\n if (!session) return null;\n\n const turns = queryAll<TurnRow>(db, \"SELECT * FROM turns WHERE session_id = ? ORDER BY seq\", id);\n const toolRows = loadToolCalls(db, id);\n const findings = loadFindings(db, id, toolRows);\n const events = loadEvents(db, id, toolRows);\n\n return {\n session: withSessionTotals(db, session, id, toolRows),\n turns,\n events,\n classification: loadClassification(db, id),\n parent: loadParent(db, session),\n children: loadChildren(db, id),\n workflow_runs: loadWorkflowRuns(db, id),\n findings,\n file_changes: loadFileChanges(db, id),\n };\n}\n\n/**\n * A Workflow-tool run's detail: the launching tool_call (name, status, returned result) + the turn\n * and session it was launched from, every subagent session fanned out under its `workflow_run_id`,\n * and roll-up stats (agent count, tokens, cost, wall-clock span). Powers the /workflow/:run_id page.\n * Returns null when no Workflow tool_call carries this run id (→ 404).\n */\n\n/** Parse a stored JSON column back to a value, or null when absent/malformed. */\nexport function safeJson(s: string | null): unknown {\n if (!s) return null;\n try {\n return JSON.parse(s);\n } catch {\n return null;\n }\n}\n\n/** Subagent sessions in this run (same projection as getSession's children, plus the wall-clock fields\n * so the page can show each agent's span and a run-level min/max), with the run's token/cost/span\n * roll-up computed over them. LEFT JOIN session_meta for the authoritative type/description/depth —\n * these workflow agents carry none in-transcript. */\nfunction loadAgents(db: DB, runId: string): { agents: WorkflowAgent[]; stats: WorkflowDetail[\"stats\"] } {\n const hasMeta = tableExists(db, \"session_meta\");\n const agentRows = queryAll<WorkflowAgentRow>(\n db,\n `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,\n (SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models\n ${metaProjection(hasMeta)}\n FROM sessions s ${metaJoin(hasMeta)}\n WHERE s.workflow_run_id = ? ORDER BY s.started_at`,\n runId,\n );\n\n let total_tokens = 0;\n let total_cost = 0;\n let started_at: string | null = null;\n let ended_at: string | null = null;\n const agents: WorkflowAgent[] = agentRows.map(({ ai_title, slug, ...r }) => {\n const u = sessionUsage(db, r.id);\n const a = { ...r, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };\n total_tokens += a.tokens;\n total_cost += a.cost;\n if (a.started_at && (!started_at || a.started_at < started_at)) started_at = a.started_at;\n if (a.ended_at && (!ended_at || a.ended_at > ended_at)) ended_at = a.ended_at;\n return a;\n });\n const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;\n\n return {\n agents,\n stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms },\n };\n}\n\n/** The workflow's ACTUAL result, which arrives later as a `<task-notification>` user message in the\n * launching session — tc.result_summary is only the \"launched in background\" ack. Finds that message by\n * the Workflow tool-use id and surfaces its completion status, summary, result, and failures. (The same\n * task can notify more than once; takes the most recent.) */\nfunction parseCompletion(db: DB, wf: WorkflowLaunchRow): WorkflowCompletion | null {\n if (!wf.tool_use_id || !wf.parent_session_id) return null;\n const ev = queryGet<{ text: string | null }>(\n db,\n `SELECT text FROM events\n WHERE session_id = ? AND text LIKE '%<${TASK_NOTIFICATION_TAG}>%' AND text LIKE ?\n ORDER BY timestamp DESC LIMIT 1`,\n wf.parent_session_id,\n `%${wf.tool_use_id}%`,\n );\n if (!ev) return null;\n const t = ev.text ?? \"\";\n return {\n status: xmlTag(t, TASK_STATUS_TAG),\n summary: xmlTag(t, TASK_SUMMARY_TAG),\n result: xmlTag(t, TASK_RESULT_TAG),\n failures: xmlTag(t, TASK_FAILURES_TAG),\n };\n}\n\n/** The runner's own result sidecar (workflow_results), ingested from wf_<id>.json. It's the\n * authoritative record of how the run finished — especially for async runs that never posted a\n * task-notification (`completion` stays null) or that failed before fanning out any agents.\n * Sidecar-preferred: it supplies status + the returned result and fills the completion when the\n * transcript has none. Its self-reported roll-up (model, tokens, tool calls, phases, per-item logs,\n * duration, agent count) rides along as `run` so the page can show it even with zero ingested agents. */\nfunction loadRunSidecar(\n db: DB,\n runId: string,\n completion: WorkflowCompletion | null,\n): { wr: WorkflowResultRow | null; run: WorkflowRunResult | null; completion: WorkflowCompletion | null } {\n const wr = tableExists(db, \"workflow_results\")\n ? (queryGet<WorkflowResultRow>(\n db,\n `SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,\n agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at\n FROM workflow_results WHERE run_id = ?`,\n runId,\n ) ?? null)\n : null;\n if (!wr) return { wr: null, run: null, completion };\n\n const run: WorkflowRunResult = {\n status: wr.status ?? null,\n summary: wr.summary ?? null,\n default_model: wr.default_model ?? null,\n agent_count: wr.agent_count ?? null,\n total_tokens: wr.total_tokens ?? null,\n total_tool_calls: wr.total_tool_calls ?? null,\n duration_ms: wr.duration_ms ?? null,\n started_at: wr.started_at ?? null,\n ended_at: wr.ended_at ?? null,\n // The three JSON blobs are asserted, not validated (ADR-026): safeJson yields null for malformed\n // stored text, and these casts are what claims the shape of everything else.\n phases: safeJson(wr.phases_json) as WorkflowRunResult[\"phases\"],\n logs: safeJson(wr.logs_json) as WorkflowRunResult[\"logs\"],\n // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph\n // with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.\n progress: safeJson(wr.progress_json) as WorkflowRunResult[\"progress\"],\n };\n\n if (wr.result_json != null || wr.status) {\n completion = {\n status: wr.status ?? completion?.status ?? null,\n summary: wr.summary ?? completion?.summary ?? null,\n result: wr.result_json ?? completion?.result ?? null,\n failures: completion?.failures ?? null,\n };\n }\n return { wr, run, completion };\n}\n\nexport function getWorkflow(db: DB, runId: string): WorkflowDetail | null {\n const wf = queryGet<WorkflowLaunchRow>(\n db,\n `SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,\n tc.input_json AS input_json,\n tc.session_id AS parent_session_id, t.seq AS turn_seq,\n ps.ai_title AS parent_ai_title, ps.slug AS parent_slug\n FROM tool_calls tc\n LEFT JOIN turns t ON t.id = tc.turn_id\n LEFT JOIN sessions ps ON ps.id = tc.session_id\n WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'\n ORDER BY t.seq\n LIMIT 1`,\n runId,\n );\n if (!wf) return null;\n\n const { agents, stats } = loadAgents(db, runId);\n const { wr, run, completion } = loadRunSidecar(db, wf.run_id, parseCompletion(db, wf));\n\n return {\n run_id: wf.run_id,\n name: wf.name ?? null,\n status: wr?.status ?? wf.status ?? null,\n result_summary: wf.result_summary ?? null,\n input_json: wf.input_json ?? null,\n completion,\n run,\n parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },\n agents,\n stats,\n };\n}\n\nexport interface SkillFilters {\n q?: string;\n source?: string;\n project?: string;\n}\n\n/**\n * One row per fired skill (grouped by name), for the Skills list. `version_count` is the number of\n * distinct captured content versions (COUNT(DISTINCT skill_id) ignores firings with no body), while\n * `call_count` counts every firing — so a skill whose body was never captured still appears. Filters\n * (name search, source, project) mirror the sessions list and apply to the firing session.\n */\nexport function listSkills(db: DB, f: SkillFilters = {}): SkillSummary[] {\n const where = [\"tc.tool_name = 'Skill'\", \"tc.skill_name IS NOT NULL\"];\n const params: unknown[] = [];\n // Search matches the skill name OR any captured version body, so a query for a phrase that only\n // appears inside a SKILL.md still surfaces the skill. (skills.body is the normalized body; joined by\n // name rather than skill_id so firings with no captured body still match on name.)\n if (f.q && f.q.trim()) {\n const like = `%${f.q.trim()}%`;\n where.push(\"(tc.skill_name LIKE ? OR EXISTS (SELECT 1 FROM skills sk WHERE sk.name = tc.skill_name AND sk.body LIKE ?))\");\n params.push(like, like);\n }\n if (f.source) (where.push(\"s.source_id = ?\"), params.push(f.source));\n if (f.project) (where.push(\"s.project_id = ?\"), params.push(f.project));\n const rows = queryAll<SkillListRow>(\n db,\n `SELECT tc.skill_name AS name,\n COUNT(*) AS call_count,\n COUNT(DISTINCT tc.skill_id) AS version_count,\n MAX(e.timestamp) AS last_fired,\n GROUP_CONCAT(DISTINCT s.source_id) AS sources\n FROM tool_calls tc\n JOIN sessions s ON s.id = tc.session_id\n LEFT JOIN events e ON e.uuid = tc.event_uuid\n WHERE ${where.join(\" AND \")}\n GROUP BY tc.skill_name\n ORDER BY call_count DESC, name`,\n ...params,\n );\n // GROUP_CONCAT hands back a comma-joined string; the response declares an array.\n return rows.map((r) => ({ ...r, sources: r.sources ? String(r.sources).split(\",\").filter(Boolean) : [] }));\n}\n\nexport interface FileFilters {\n q?: string; // path substring\n source?: string;\n project?: string;\n sort?: \"last_ts\" | \"first_ts\" | \"changes\" | \"sessions\" | \"path\";\n dir?: \"asc\" | \"desc\";\n limit?: number;\n offset?: number;\n}\n\n/**\n * The /files list (ADR-022): one row per (project, file) aggregated from `file_changes` — session\n * and change counts, line totals, first/last touched. Filters mirror the sessions list (source via\n * the changing session; q is a path substring). Sort whitelist is native SQL; count-then-page like\n * listSessions. Empty result (not an error) on a pre-v14 DB missing the table.\n */\nexport function listFiles(db: DB, f: FileFilters = {}): FilesPage {\n if (!tableExists(db, \"file_changes\")) return { total: 0, files: [] };\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.q && f.q.trim()) (where.push(\"fc.file_path LIKE ?\"), params.push(`%${f.q.trim()}%`));\n if (f.project) (where.push(\"fc.project_id = ?\"), params.push(f.project));\n if (f.source) (where.push(\"EXISTS (SELECT 1 FROM sessions s WHERE s.id = fc.session_id AND s.source_id = ?)\"), params.push(f.source));\n const whereSql = where.length ? ` WHERE ${where.join(\" AND \")}` : \"\";\n\n const grouped = `SELECT fc.project_id, fc.file_path,\n COUNT(DISTINCT fc.session_id) AS sessions,\n COUNT(*) AS changes,\n SUM(fc.lines_added) AS lines_added,\n SUM(fc.lines_removed) AS lines_removed,\n MIN(fc.timestamp) AS first_ts,\n MAX(fc.timestamp) AS last_ts\n FROM file_changes fc${whereSql}\n GROUP BY fc.project_id, fc.file_path`;\n\n const total = queryGet<CountRow>(db, `SELECT COUNT(*) n FROM (${grouped})`, ...params)!.n;\n\n const ORDER = {\n last_ts: \"last_ts\",\n first_ts: \"first_ts\",\n changes: \"changes\",\n sessions: \"sessions\",\n path: \"file_path\",\n };\n const limit = Math.min(f.limit ?? 50, 200);\n const offset = f.offset ?? 0;\n\n const files = queryAll<FileSummary>(\n db,\n `SELECT g.*, p.path AS project_path FROM (${grouped}) g\n LEFT JOIN projects p ON p.id = g.project_id\n ORDER BY ${orderBy(ORDER, f.sort, \"last_ts\", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`,\n ...params,\n limit,\n offset,\n );\n return { total, files };\n}\n\n/**\n * One file's provenance timeline (ADR-022): every `file_changes` row for the path (optionally\n * scoped to a project), grouped by changing session (newest-first) with per-turn detail — each\n * change deep-links to its tool call's transcript event (#ev-<event_uuid>). Session cards carry\n * title/date/category (classification join) like the skills detail. Returns null (→ 404) when the\n * path has no recorded changes.\n */\nexport function getFileTimeline(db: DB, filePath: string, projectId?: string): FileTimeline | null {\n if (!tableExists(db, \"file_changes\")) return null;\n const params: unknown[] = [filePath];\n let scope = \"fc.file_path = ?\";\n if (projectId) (scope += \" AND fc.project_id = ?\"), params.push(projectId);\n const rows = queryAll<FileTimelineRow>(\n db,\n `SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,\n fc.lines_removed, fc.timestamp, fc.project_id,\n s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,\n p.path AS project_path\n FROM file_changes fc\n JOIN sessions s ON s.id = fc.session_id\n LEFT JOIN turns t ON t.id = fc.turn_id\n LEFT JOIN projects p ON p.id = fc.project_id\n WHERE ${scope} ORDER BY fc.timestamp`,\n ...params,\n );\n if (!rows.length) return null;\n\n const cat = tableExists(db, \"classifications\")\n ? db.prepare(\"SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?\")\n : null;\n\n const bySession = new Map<string, FileTimelineSession>();\n for (const r of rows) {\n let g = bySession.get(r.session_id);\n if (!g) {\n g = {\n session_id: r.session_id,\n title: r.ai_title || r.slug || null,\n source_id: r.source_id,\n started_at: r.started_at,\n category: cat ? ((cat.get(r.session_id) as CategoryRow | undefined)?.category ?? null) : null,\n changes: [],\n };\n bySession.set(r.session_id, g);\n }\n g.changes.push({\n id: r.id,\n turn_id: r.turn_id,\n turn_seq: r.turn_seq,\n prompt_preview: r.prompt_preview,\n event_uuid: r.event_uuid,\n tool_name: r.tool_name,\n lines_added: r.lines_added,\n lines_removed: r.lines_removed,\n timestamp: r.timestamp,\n });\n }\n // Newest-changing session first; changes within a session stay chronological.\n const sessions = [...bySession.values()].sort((a, b) => {\n const la = a.changes[a.changes.length - 1].timestamp ?? \"\";\n const lb = b.changes[b.changes.length - 1].timestamp ?? \"\";\n return la < lb ? 1 : la > lb ? -1 : 0;\n });\n\n return {\n file_path: filePath,\n project_id: rows[0].project_id ?? null,\n project_path: rows[0].project_path ?? null,\n sessions_count: sessions.length,\n changes_count: rows.length,\n lines_added: rows.reduce((a, r) => a + (r.lines_added ?? 0), 0),\n lines_removed: rows.reduce((a, r) => a + (r.lines_removed ?? 0), 0),\n first_ts: rows[0].timestamp ?? null,\n last_ts: rows[rows.length - 1].timestamp ?? null,\n sessions,\n };\n}\n\n/**\n * All data for one skill (by name): its content versions (most-recent first, each with body +\n * firing count) and the sessions that fired it, tagged with which version (`version_id`) so the UI\n * can map the session list to the selected version. Returns null (→ 404) when the name never fired.\n */\nexport function getSkill(db: DB, name: string): SkillDetail | null {\n const versions = queryAll<SkillVersion>(\n db,\n `SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,\n (SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count\n FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`,\n name,\n );\n\n const sessionRows = queryAll<SkillSessionRow>(\n db,\n `SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,\n tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count\n FROM tool_calls tc\n JOIN sessions s ON s.id = tc.session_id\n LEFT JOIN projects p ON p.id = s.project_id\n LEFT JOIN events e ON e.uuid = tc.event_uuid\n WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?\n GROUP BY s.id, tc.skill_id\n ORDER BY fired_at DESC`,\n name,\n );\n\n if (!versions.length && !sessionRows.length) return null;\n const sessions: SkillSession[] = sessionRows.map(({ ai_title, slug, ...s }) => ({ ...s, title: ai_title || slug || null }));\n const call_count = sessions.reduce((a, s) => a + s.fire_count, 0);\n return { name, versions, sessions, call_count };\n}\n\n// ---- Security findings (ADR-017) + triage (ADR-018) ----------------------\n\nexport interface FindingFilters {\n severity?: string;\n category?: string;\n rule?: string;\n session?: string;\n source?: string;\n project?: string;\n from?: string;\n to?: string;\n /** open (default; excludes dismissed + muted) | dismissed | muted | all. */\n status?: \"open\" | \"dismissed\" | \"muted\" | \"all\";\n /** Sort the whole filtered list before pagination. Defaults to \"severity\" (most-severe first). */\n sort?: \"severity\" | \"session\" | \"rule\" | \"category\" | \"time\";\n dir?: \"asc\" | \"desc\";\n limit: number;\n offset: number;\n}\n\n// Rank severities inside SQL so the list can be severity-ordered and paginated without materializing\n// everything (mirrors SEVERITY_ORDER in core: info=1 … critical=5).\nconst SEVERITY_RANK_SQL =\n \"CASE f.severity WHEN 'critical' THEN 5 WHEN 'high' THEN 4 WHEN 'medium' THEN 3 WHEN 'low' THEN 2 WHEN 'info' THEN 1 ELSE 0 END\";\n\n// A finding is muted when a muted_rules row matches its rule at global/project/source scope. Only valid\n// when the triage DB is ATTACHed (createApp does this); guarded by triageAttached() everywhere it's used.\nconst MUTED_SQL = `EXISTS (SELECT 1 FROM triage.muted_rules m WHERE m.rule_id = f.rule_id\n AND (m.scope='global' OR (m.scope='project' AND m.scope_id = s.project_id) OR (m.scope='source' AND m.scope_id = s.source_id)))`;\n\n/** Whether the writable triage store is ATTACHed to this (read) connection — gates all triage SQL. */\nexport function triageAttached(db: DB): boolean {\n return (db.prepare(\"PRAGMA database_list\").all() as Array<{ name: string }>).some((r) => r.name === \"triage\");\n}\n\n/** The filter half of FindingFilters — what narrows the set, without status/sort/paging. */\nexport type FindingScope = Omit<FindingFilters, \"status\" | \"sort\" | \"dir\" | \"limit\" | \"offset\">;\n\n/** WHERE fragments shared by list + summary: user filters (no status) over findings f / sessions s. */\nfunction findingWhere(f: FindingScope): { sql: string[]; params: unknown[] } {\n const sql: string[] = [];\n const params: unknown[] = [];\n if (f.severity) (sql.push(\"f.severity = ?\"), params.push(f.severity));\n if (f.category) (sql.push(\"f.category = ?\"), params.push(f.category));\n if (f.rule) (sql.push(\"f.rule_id = ?\"), params.push(f.rule));\n if (f.session) (sql.push(\"f.session_id = ?\"), params.push(f.session));\n if (f.source) (sql.push(\"s.source_id = ?\"), params.push(f.source));\n if (f.project) (sql.push(\"s.project_id = ?\"), params.push(f.project));\n // Compare on the DATE part so a picked day is inclusive on both ends — `to = 2026-07-14` must include\n // 2026-07-14 events (a plain `started_at <= '2026-07-14'` would exclude that whole day's timestamps).\n pushDateRange(sql, params, \"s.started_at\", f.from, f.to);\n return { sql, params };\n}\n\n/** Status → WHERE fragment over the dismissed LEFT JOIN (`d`) + MUTED_SQL. Empty when triage absent. */\nfunction statusWhere(status: FindingFilters[\"status\"], attached: boolean): string {\n if (!attached) return status === \"dismissed\" || status === \"muted\" ? \"1 = 0\" : \"\"; // no triage → nothing dismissed/muted\n switch (status) {\n case \"dismissed\":\n return \"d.finding_id IS NOT NULL\";\n case \"muted\":\n return `d.finding_id IS NULL AND ${MUTED_SQL}`;\n case \"all\":\n return \"\";\n default: // open\n return `d.finding_id IS NULL AND NOT ${MUTED_SQL}`;\n }\n}\n\n/**\n * The browsable findings list (GET /api/security/findings), filtered/sorted/paged. Each row carries\n * enough session context (title, project, source, when) to render and link without a second fetch, plus\n * its triage state (dismissed flag/note/when) when the triage store is attached. Default status hides\n * dismissed + muted so real, un-triaged findings surface. Guarded for a pre-ingest read-only DB.\n */\nexport function listFindings(db: DB, f: FindingFilters): FindingsPage {\n if (!tableExists(db, \"findings\")) return { total: 0, findings: [] };\n const attached = triageAttached(db);\n\n const w = findingWhere(f);\n const st = statusWhere(f.status ?? \"open\", attached);\n const where = [...w.sql, ...(st ? [st] : [])];\n const whereSql = where.length ? `WHERE ${where.join(\" AND \")}` : \"\";\n const dismissJoin = attached ? \"LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id\" : \"\";\n const triageCols = attached\n ? `, (d.finding_id IS NOT NULL) AS dismissed, d.note AS dismiss_note, d.dismissed_at, ${MUTED_SQL} AS muted`\n : `, 0 AS dismissed, NULL AS dismiss_note, NULL AS dismissed_at, 0 AS muted`;\n\n const total = queryGet<CountRow>(\n db,\n `SELECT COUNT(*) n FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin} ${whereSql}`,\n ...w.params,\n )!.n;\n\n const ORDER = {\n severity: SEVERITY_RANK_SQL,\n session: \"f.session_id\",\n rule: \"f.rule_id\",\n category: \"f.category\",\n time: \"s.started_at\",\n };\n\n const findings = queryAll<FindingListRow>(\n db,\n `SELECT f.id, f.session_id, f.tool_call_id, f.event_uuid, f.turn_id, f.rule_id, f.category,\n f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,\n COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,\n s.started_at, p.path AS project_path, s.project_id ${triageCols}\n FROM findings f\n JOIN sessions s ON s.id = f.session_id\n LEFT JOIN projects p ON p.id = s.project_id\n LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id\n ${dismissJoin}\n ${whereSql}\n ORDER BY ${orderBy(ORDER, f.sort, \"severity\", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC\n LIMIT ? OFFSET ?`,\n ...w.params,\n f.limit,\n f.offset,\n );\n\n return { total, findings };\n}\n\n/**\n * Ids of every OPEN finding matching `f` — the id-only half of listFindings, for bulk dismiss\n * (POST /api/security/dismiss-matching). Shares findingWhere + statusWhere with the list so \"what\n * matches\" cannot drift between browsing and bulk-dismissing; only the projection differs, so this\n * skips the list's COUNT(*) pass, session/tool context columns, and ORDER BY. Status is fixed to\n * open: a bulk dismiss acts on what the user can currently see, never on dismissed or muted rows.\n */\nexport function openFindingIds(db: DB, f: FindingScope): string[] {\n if (!tableExists(db, \"findings\")) return [];\n const attached = triageAttached(db);\n\n const w = findingWhere(f);\n const st = statusWhere(\"open\", attached);\n const where = [...w.sql, ...(st ? [st] : [])];\n\n return queryAll<{ id: string }>(\n db,\n `SELECT f.id\n FROM findings f\n JOIN sessions s ON s.id = f.session_id\n ${attached ? \"LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id\" : \"\"}\n ${where.length ? `WHERE ${where.join(\" AND \")}` : \"\"}`,\n ...w.params,\n ).map((r) => r.id);\n}\n\n/**\n * Roll-up for the /security page header + the Dashboard KPI. Counts (by severity/category/rule,\n * sessions flagged) are computed over **open** findings — dismissed + muted excluded — so the KPIs show\n * what still needs review; `dismissed` and `muted` totals ride along for the triage affordances. The\n * framework reference content (core) powers the \"what & why\" explainers. Guarded for a pre-ingest DB.\n */\nexport function securitySummary(db: DB): SecuritySummary {\n const categories = SECURITY_CATEGORIES;\n if (!tableExists(db, \"findings\")) {\n return { total: 0, sessions_flagged: 0, dismissed: 0, muted: 0, by_severity: [], by_category: [], by_rule: [], categories };\n }\n const attached = triageAttached(db);\n const dismissJoin = attached ? \"LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id\" : \"\";\n const base = `FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin}`;\n const openWhere = attached ? `WHERE d.finding_id IS NULL AND NOT ${MUTED_SQL}` : \"\";\n\n const scalar = (sql: string): number => queryGet<CountRow>(db, sql)!.n;\n const total = scalar(`SELECT COUNT(*) n ${base} ${openWhere}`);\n const sessions_flagged = scalar(`SELECT COUNT(DISTINCT f.session_id) n ${base} ${openWhere}`);\n const dismissed = attached ? scalar(`SELECT COUNT(*) n FROM findings f LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id WHERE d.finding_id IS NOT NULL`) : 0;\n const muted = attached ? scalar(`SELECT COUNT(*) n ${base} WHERE d.finding_id IS NULL AND ${MUTED_SQL}`) : 0;\n\n const by_severity = queryAll<SecuritySummary[\"by_severity\"][number]>(\n db,\n `SELECT f.severity AS severity, COUNT(*) n ${base} ${openWhere} GROUP BY f.severity ORDER BY ${SEVERITY_RANK_SQL} DESC`,\n );\n const by_category = queryAll<SecuritySummary[\"by_category\"][number]>(\n db,\n `SELECT f.category AS category, COUNT(*) n ${base} ${openWhere} GROUP BY f.category ORDER BY n DESC`,\n );\n const by_rule = queryAll<SecuritySummary[\"by_rule\"][number]>(\n db,\n `SELECT f.rule_id AS rule_id, f.category AS category, f.title AS title, COUNT(*) n, MAX(${SEVERITY_RANK_SQL}) AS rank\n ${base} ${openWhere} GROUP BY f.rule_id ORDER BY rank DESC, n DESC`,\n );\n return { total, sessions_flagged, dismissed, muted, by_severity, by_category, by_rule, categories };\n}\n","/** Shared read-only SQL helpers for the server. */\nimport type { SQLInputValue } from \"node:sqlite\";\nimport type { DB } from \"./db.js\";\n\n/** Bound parameters, as `node:sqlite` wants them. Call sites pass `unknown` and cast through here. */\nconst bind = (params: unknown[]): SQLInputValue[] => params as SQLInputValue[];\n\n/** Whether a table exists — the server opens the DB read-only, so a not-yet-ingested schema (e.g. no\n * workflow_results table) must degrade gracefully rather than throw. */\nexport function tableExists(db: DB, name: string): boolean {\n return !!db.prepare(\"SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?\").get(name);\n}\n\n/**\n * Typed `prepare(sql).all(...params)`. The caller names the row shape `T` once (a `@agent-lens/contracts`\n * row, a server-local derived shape from `rows.ts`, or an inline shape) and the cast lives here instead\n * of at every call site — so a column the query doesn't actually select is a compile error, not a silent\n * `undefined` at runtime. SQLite has no static knowledge of a query's columns, so the single cast\n * here is the unavoidable boundary; keep it the only one.\n */\nexport function queryAll<T>(db: DB, sql: string, ...params: unknown[]): T[] {\n return db.prepare(sql).all(...bind(params)) as unknown as T[];\n}\n\n/** Typed `prepare(sql).get(...params)` — returns the row or `undefined`. See {@link queryAll}. */\nexport function queryGet<T>(db: DB, sql: string, ...params: unknown[]): T | undefined {\n return db.prepare(sql).get(...bind(params)) as T | undefined;\n}\n\n/**\n * Append an inclusive `from`/`to` date-range filter on `col` to a WHERE-clause accumulator. Both ends\n * compare the DATE part (`date(col) >= date(?)`) so a picked `to` day includes that whole day — a plain\n * `col <= '2026-07-14'` would drop everything after midnight. `col` is an internal column expression\n * (e.g. `s.started_at`), never user input. Mutates `clauses`/`params` in place.\n */\nexport function pushDateRange(clauses: string[], params: unknown[], col: string, from?: string, to?: string): void {\n if (from) {\n clauses.push(`date(${col}) >= date(?)`);\n params.push(from);\n }\n if (to) {\n clauses.push(`date(${col}) <= date(?)`);\n params.push(to);\n }\n}\n\n/** The subagent-meta LEFT JOIN, present only when the `session_meta` table exists (pre-ingest DBs). */\nexport function metaJoin(hasMeta: boolean): string {\n return hasMeta ? \"LEFT JOIN session_meta sm ON sm.session_id = s.id\" : \"\";\n}\n\n/** The subagent-meta column projection — real columns when `session_meta` exists, else NULL stand-ins\n * so the row shape is stable regardless of whether the table has been ingested yet. Pairs with\n * {@link metaJoin}; leads with a comma to append to a SELECT list. */\nexport function metaProjection(hasMeta: boolean): string {\n return hasMeta\n ? \", sm.agent_type, sm.agent_description, sm.spawn_depth\"\n : \", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth\";\n}\n\n/**\n * Resolve a request's `sort`/`dir` into an `ORDER BY` fragment. SQLite can't parameterize an ORDER BY\n * expression, so it has to be interpolated — which makes this the one place a request value could\n * reach the SQL text. It can't: the expression is always a value from the caller's literal `columns`\n * map (an unknown key falls back to `fallback`, which must itself be a key of that map), and the\n * direction collapses to one of two literals. Route the interpolation through here, never build it\n * inline, and the invariant stays next to the string it protects.\n */\nexport function orderBy<K extends string>(columns: Record<K, string>, sort: string | undefined, fallback: K, dir?: string): string {\n // Object.hasOwn, not `in` / `map[key] ?? fallback`: both walk the prototype chain, so `?sort=toString`\n // resolves to Function.prototype.toString and its SOURCE TEXT lands in the query (a 500, not an\n // injection — but exactly what this helper exists to prevent).\n const col = sort !== undefined && Object.hasOwn(columns, sort) ? columns[sort as K] : columns[fallback];\n return `${col} ${dir === \"asc\" ? \"ASC\" : \"DESC\"}`;\n}\n\n/**\n * Resolve a request's `?limit=` into a row count within `[1, max]`.\n *\n * `Math.min(Number(q.limit) || 50, max)` looks equivalent and is not: it guards only the UPPER bound,\n * so `?limit=-1` yields -1, and SQLite reads a negative LIMIT as \"no limit\" — the page cap is gone and\n * the whole table comes back in one response. Non-integers are floored for the same reason `offset`\n * needs it (see {@link pageOffset}).\n */\nexport function pageLimit(raw: unknown, fallback: number, max: number): number {\n const n = Math.floor(Number(raw));\n if (!Number.isFinite(n) || n < 1) return Math.min(fallback, max);\n return Math.min(n, max);\n}\n\n/**\n * Resolve a request's `?offset=` into a non-negative integer.\n *\n * `Number(q.offset) || 0` passes a non-integer straight through — `?offset=1.5` is truthy, reaches\n * SQLite, and throws (a 500) because it will not bind a float to an integer parameter.\n */\nexport function pageOffset(raw: unknown): number {\n const n = Math.floor(Number(raw));\n return Number.isFinite(n) && n > 0 ? n : 0;\n}\n\n/** Append `value` to the list stored at `key`, creating the list on first use. Group-by-key is the\n * shape half this module's loaders end in (findings by tool call, tool calls by event). */\nexport function pushGrouped<K, V>(map: Map<K, V[]>, key: K, value: V): void {\n const list = map.get(key);\n if (list) list.push(value);\n else map.set(key, [value]);\n}\n","/**\n * Per-model token-usage aggregation + cost, shared by the session detail, workflow detail, session\n * list, and dashboard queries. Keeping the `SELECT model, SUM(...) …` shape, the column aliases\n * (`i/o/cw/cr`) and the cache-aware cost roll-up in one place makes a pricing or column change a\n * single edit rather than one per call site.\n */\nimport { costForUsage, unpricedModels } from \"@agent-lens/core\";\nimport { type DB } from \"./db.js\";\nimport { queryAll } from \"./sql-util.js\";\n\n/** A per-model token-usage aggregate row: input / output / cache-write / cache-read sums. */\nexport interface UsageRow {\n model: string | null;\n i: number;\n o: number;\n cw: number;\n cr: number;\n}\n\n/** The token categories, kept split so the UI can show input/output/cache-write/cache-read separately. */\nexport interface TokenSplit {\n input: number;\n output: number;\n cache_creation: number;\n cache_read: number;\n}\n\n/** The `SUM(...)` column list every usage query shares (aliased i/o/cw/cr). */\nexport const USAGE_SUMS =\n \"SUM(input_tokens) i, SUM(output_tokens) o, SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr\";\n\n/** Per-model token usage for one session. */\nexport function sessionUsage(db: DB, sessionId: string): UsageRow[] {\n return queryAll<UsageRow>(db, `SELECT model, ${USAGE_SUMS} FROM token_usage WHERE session_id = ? GROUP BY model`, sessionId);\n}\n\n/** Sum the split categories of a set of usage rows into a fresh {@link TokenSplit}. */\nexport function splitOf(rows: UsageRow[]): TokenSplit {\n const split: TokenSplit = { input: 0, output: 0, cache_creation: 0, cache_read: 0 };\n for (const u of rows) {\n split.input += u.i;\n split.output += u.o;\n split.cache_creation += u.cw;\n split.cache_read += u.cr;\n }\n return split;\n}\n\n/** Total tokens (all categories, cache included) for a set of usage rows. */\nexport function tokensOf(split: TokenSplit): number {\n return split.input + split.output + split.cache_creation + split.cache_read;\n}\n\n/**\n * The models in these rows that have real token usage but no rate, so {@link costOf} understates the\n * total. Empty for a fully-priced scope. `<synthetic>` is excluded — it never had an API cost.\n */\nexport function unpricedOf(rows: UsageRow[]): string[] {\n return unpricedModels(rows.filter((u) => u.i || u.o || u.cw || u.cr).map((u) => u.model));\n}\n\n/** Cache-aware USD cost (per-model rates) for a set of usage rows, rounded to 4dp. */\nexport function costOf(rows: UsageRow[]): number {\n let cost = 0;\n for (const u of rows) {\n cost += costForUsage(u.model, {\n input_tokens: u.i,\n output_tokens: u.o,\n cache_creation_input_tokens: u.cw,\n cache_read_input_tokens: u.cr,\n });\n }\n return Number(cost.toFixed(4));\n}\n","/**\n * Agent Lens — Fastify app factory (ADR-005: read-only over the SQLite store).\n *\n * `createApp(db, opts)` builds the route tree against an already-open DB handle so the same app can\n * be driven by the CLI entry (index.ts, with .listen) and by tests (app.inject, no socket).\n */\nimport { existsSync } from \"node:fs\";\nimport Fastify, { type FastifyInstance, type FastifyReply } from \"fastify\";\nimport fastifyStatic from \"@fastify/static\";\nimport { resolveVersion } from \"@agent-lens/core\";\nimport { renderSessionExport, parseRedactionLevel } from \"./export.js\";\nimport { type DB, lastIngested, schemaStatus, listSources, listProjects, listModels, listSessions, getSession, getWorkflow, listSkills, getSkill, listFindings, openFindingIds, securitySummary, listFiles, getFileTimeline, safeJson } from \"./db.js\";\nimport { dashboardOverview, dashboardTimeseries, dashboardBreakdowns, type DashFilters } from \"./dashboard.js\";\nimport { writeBlocked, runRefresh, LOOPBACK_HOSTS } from \"./refresh.js\";\nimport { openTriage, dismiss, reopen, muteRule, unmute, listMutes, type TriageDB, type MuteScope } from \"./triage.js\";\nimport { about, type AboutContext } from \"./about.js\";\nimport { PREFS_SCHEMA_SQL, getPref, setPref } from \"./prefs.js\";\nimport { pageLimit, pageOffset } from \"./sql-util.js\";\n\nexport interface CreateAppOpts {\n /** Absolute path to the built web SPA; when present it is served with a history fallback. */\n webDist?: string | null;\n /** Path to the writable security-triage store (ADR-018). When set, it is opened read-write for\n * triage writes and ATTACHed to the read handle so the findings list can JOIN triage state. */\n triageDbPath?: string | null;\n /** Reject any request whose Host authority is not loopback (DNS-rebinding defense, HIGH-001).\n * Defaults to true; the intentional non-local bind (AGENT_LENS_ALLOW_NONLOCAL) passes false. */\n enforceLoopbackHost?: boolean;\n /** Startup facts backing GET /api/about (ADR-027). Omit and the route is not registered — an\n * embedder that never resolved a db path has nothing truthful to report. */\n about?: AboutContext;\n}\n\n/** Structured 404 envelope. Every not-found response shares this shape so a client can uniformly read\n * `err.error.code` — the rest of the API already uses `{ error: { code, message } }`. */\nfunction notFound(reply: FastifyReply, message = \"not found\") {\n return reply.code(404).send({ error: { code: \"NOT_FOUND\", message } });\n}\n\nexport async function createApp(db: DB, opts: CreateAppOpts = {}): Promise<FastifyInstance> {\n const app = Fastify({ logger: false });\n\n // HIGH-001: binding 127.0.0.1 stops direct remote connections but NOT DNS rebinding — a page on\n // evil.com re-resolved to 127.0.0.1 makes same-origin requests the browser will let it read. Reads\n // are unauthenticated, so without a Host check the whole transcript store is exfiltratable. Reject\n // any non-loopback Host up front. This also removes the residual write ambiguity (Origin-absent).\n if (opts.enforceLoopbackHost !== false) {\n app.addHook(\"onRequest\", async (req, reply) => {\n // Host is case-insensitive (RFC 3986) and LOOPBACK_HOSTS is lowercase, so `Host: LOCALHOST`\n // would otherwise be rejected — over-strict, but a needless 403 all the same.\n const host = (req.headers.host ?? \"\").replace(/:\\d+$/, \"\").toLowerCase();\n if (!LOOPBACK_HOSTS.has(host)) {\n return reply.code(403).send({ error: { code: \"FORBIDDEN_HOST\", message: \"non-loopback Host rejected\" } });\n }\n });\n }\n\n // Baseline security headers on every response (incl. static SPA + errors). Anti-clickjacking\n // (`X-Frame-Options`/`frame-ancestors`) matters here because framing the loopback UI could drive\n // the Origin-passing write routes. A full script/style CSP is deliberately omitted — it risks\n // breaking the SPA and adds little on a loopback-only, Host-guarded server (tracked as follow-up).\n app.addHook(\"onSend\", async (_req, reply, payload) => {\n reply.header(\"X-Frame-Options\", \"DENY\");\n reply.header(\"X-Content-Type-Options\", \"nosniff\");\n reply.header(\"Referrer-Policy\", \"no-referrer\");\n reply.header(\"Content-Security-Policy\", \"frame-ancestors 'none'\");\n return payload;\n });\n\n // Security triage store (ADR-018): a separate writable SQLite, ATTACHed to the read handle so the\n // findings queries can JOIN dismissed/muted state, while writes go through `triageDb`. The analytics\n // handle stays read-only. Absent → the security endpoints degrade to the un-triaged (all-open) view.\n let triageDb: TriageDB | null = null;\n if (opts.triageDbPath) {\n triageDb = openTriage(opts.triageDbPath); // creates the file + schema before we ATTACH it\n triageDb.exec(PREFS_SCHEMA_SQL); // UI-prefs table rides the same writable sidecar (see prefs.ts)\n // ATTACH takes a literal, not a bound parameter, so the path must be interpolated. It is an\n // OPERATOR-supplied path (config/CLI), never request data, and single quotes are doubled per SQL\n // string-literal escaping — the standard workaround, called out here so it doesn't read as an\n // oversight.\n db.exec(`ATTACH DATABASE '${opts.triageDbPath.replace(/'/g, \"''\")}' AS triage`);\n app.addHook(\"onClose\", async () => triageDb?.close());\n }\n\n // Shared CSRF + availability guard for the triage write routes (same posture as /api/refresh).\n const guardWrite = (req: any, reply: any): boolean => {\n if (writeBlocked(req.headers)) {\n reply.code(403).send({ error: { code: \"FORBIDDEN_ORIGIN\", message: \"cross-origin write blocked\" } });\n return false;\n }\n if (!triageDb) {\n reply.code(503).send({ error: { code: \"TRIAGE_UNAVAILABLE\", message: \"triage store not configured\" } });\n return false;\n }\n return true;\n };\n\n app.get(\"/api/health\", async () => {\n const s = schemaStatus(db);\n // `version` rides on health rather than /api/about because it is the one diagnostic safe to\n // publish: health IS exported to the static Pages snapshot, so the badge works there too\n // (ADR-027). resolveVersion() is memoized — health is polled and must not spawn git per request.\n const v = resolveVersion();\n return {\n ok: true,\n last_ingested: lastIngested(db),\n schema_version: s.db_version,\n schema_stale: s.stale,\n version: v.version,\n version_source: v.source,\n };\n });\n\n // Diagnostics (ADR-027). Read-only, and deliberately NOT in the static snapshot: it carries\n // absolute filesystem paths. Registered only when startServer passed the context it resolved —\n // re-resolving here would ignore `serve --db` and report a path nobody is reading.\n if (opts.about) {\n const ctx = opts.about;\n app.get(\"/api/about\", async () => about(db, ctx));\n }\n\n // The one write-action on this read-only server: run a collect + ingest pass on the host so the UI\n // can pull in new transcripts on demand (ADR-015). Guarded against cross-site CSRF (Origin) and\n // concurrent runs (single-instance lock → 409). See refresh.ts.\n app.post(\"/api/refresh\", async (req, reply) => {\n if (writeBlocked(req.headers)) {\n return reply.code(403).send({ error: { code: \"FORBIDDEN_ORIGIN\", message: \"cross-origin refresh blocked\" } });\n }\n let result;\n try {\n result = runRefresh();\n } catch (e: any) {\n // Don't leak the raw exception (may include host filesystem paths, LOW-003) to the client; the\n // recipient is the local user but a generic message keeps this safe if ever exposed. Detail → log.\n console.error(\"agent-lens-server: refresh failed:\", e);\n return reply.code(500).send({ error: { code: \"REFRESH_FAILED\", message: \"refresh failed; see server logs\" } });\n }\n if (!result) {\n return reply.code(409).send({ error: { code: \"REFRESH_IN_PROGRESS\", message: \"a collect/ingest run is already in progress\" } });\n }\n return { ok: true, collected: result.collected, last_ingested: lastIngested(db) };\n });\n app.get(\"/api/sources\", async () => listSources(db));\n app.get(\"/api/projects\", async () => listProjects(db));\n app.get(\"/api/models\", async () => listModels(db));\n\n // Dashboard aggregates. All read-only; filters: source, from, to.\n const dashFilters = (req: any): DashFilters => {\n const q = req.query as Record<string, string>;\n return { source: q.source, from: q.from, to: q.to };\n };\n app.get(\"/api/dashboard/overview\", async (req) => dashboardOverview(db, dashFilters(req)));\n app.get(\"/api/dashboard/timeseries\", async (req) => {\n const q = req.query as Record<string, string>;\n return dashboardTimeseries(db, dashFilters(req), q.bucket);\n });\n app.get(\"/api/dashboard/breakdowns\", async (req) => dashboardBreakdowns(db, dashFilters(req)));\n\n // UI preferences (chart/column visibility, per-chart toggles). Stored in the writable sidecar\n // (prefs.ts); localStorage on the client is an optimistic cache. GET degrades to `{value:null}` when\n // no writable store is configured so the client simply keeps its local value. PUT reuses the same\n // CSRF+availability guard as the other writes.\n const PREF_KEY = /^[a-z0-9._-]{1,64}$/i;\n app.get(\"/api/prefs/:key\", async (req, reply) => {\n const { key } = req.params as { key: string };\n if (!PREF_KEY.test(key)) return reply.code(400).send({ error: { code: \"BAD_KEY\", message: \"invalid pref key\" } });\n if (!triageDb) return { value: null };\n const raw = getPref(triageDb, key);\n // safeJson tolerates a malformed stored value (returns null) rather than throwing a 500.\n return { value: safeJson(raw) };\n });\n app.put(\"/api/prefs/:key\", async (req, reply) => {\n const { key } = req.params as { key: string };\n if (!PREF_KEY.test(key)) return reply.code(400).send({ error: { code: \"BAD_KEY\", message: \"invalid pref key\" } });\n if (!guardWrite(req, reply)) return;\n const body = req.body as { value?: unknown };\n setPref(triageDb!, key, JSON.stringify(body?.value ?? null));\n return { ok: true };\n });\n\n app.get(\"/api/sessions\", async (req) => {\n const q = req.query as Record<string, string>;\n return listSessions(db, {\n source: q.source,\n project: q.project,\n model: q.model,\n q: q.q,\n from: q.from,\n to: q.to,\n kind: q.kind === \"main\" || q.kind === \"subagent\" ? q.kind : undefined,\n severity: q.severity ? q.severity.split(\",\").filter(Boolean) : undefined,\n errorType: q.error_type ? q.error_type.split(\",\").filter(Boolean) : undefined,\n sort: ([\"started\", \"title\", \"turns\", \"tokens\", \"cost\", \"duration\", \"errors\", \"security\"] as const).find((s) => s === q.sort),\n dir: q.dir === \"asc\" ? \"asc\" : q.dir === \"desc\" ? \"desc\" : undefined,\n limit: pageLimit(q.limit, 50, 200),\n offset: pageOffset(q.offset),\n });\n });\n\n app.get(\"/api/sessions/:id\", async (req, reply) => {\n const { id } = req.params as { id: string };\n const result = getSession(db, id);\n if (!result) return notFound(reply);\n return result;\n });\n\n app.get(\"/api/workflows/:run_id\", async (req, reply) => {\n const { run_id } = req.params as { run_id: string };\n const result = getWorkflow(db, run_id);\n if (!result) return notFound(reply);\n return result;\n });\n\n // Security findings (ADR-017). Read-only; browsable list + roll-up summary for the /security page,\n // the transcript inline badges (served via /api/sessions/:id), and the Dashboard KPI.\n app.get(\"/api/security/summary\", async () => securitySummary(db));\n const findingFilters = (q: Record<string, string>) => ({\n severity: q.severity,\n category: q.category,\n rule: q.rule,\n session: q.session,\n source: q.source,\n project: q.project,\n from: q.from,\n to: q.to,\n status: ([\"open\", \"dismissed\", \"muted\", \"all\"] as const).find((s) => s === q.status),\n });\n app.get(\"/api/security/findings\", async (req) => {\n const q = req.query as Record<string, string>;\n return listFindings(db, {\n ...findingFilters(q),\n sort: ([\"severity\", \"session\", \"rule\", \"category\", \"time\"] as const).find((s) => s === q.sort),\n dir: q.dir === \"asc\" ? \"asc\" : q.dir === \"desc\" ? \"desc\" : undefined,\n limit: pageLimit(q.limit, 50, 5000),\n offset: pageOffset(q.offset),\n });\n });\n\n // Triage writes (ADR-018): CSRF-guarded, loopback-only, same posture as /api/refresh.\n app.get(\"/api/security/mutes\", async () => (triageDb ? listMutes(triageDb) : []));\n app.post(\"/api/security/dismiss\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { ids?: string[]; note?: string };\n const n = dismiss(triageDb!, Array.isArray(b.ids) ? b.ids : [], b.note ?? null);\n return { ok: true, dismissed: n };\n });\n app.post(\"/api/security/reopen\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { ids?: string[] };\n const n = reopen(triageDb!, Array.isArray(b.ids) ? b.ids : []);\n return { ok: true, reopened: n };\n });\n // Dismiss every OPEN finding matching a filter (bulk cleanup) — collects ids server-side, then marks.\n app.post(\"/api/security/dismiss-matching\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { filter?: Record<string, string>; note?: string };\n const ids = openFindingIds(db, findingFilters(b.filter ?? {}));\n const n = dismiss(triageDb!, ids, b.note ?? null);\n return { ok: true, dismissed: n };\n });\n app.post(\"/api/security/mute\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { rule_id?: string; scope?: string; scope_id?: string; note?: string };\n if (!b.rule_id) return reply.code(400).send({ error: { code: \"BAD_REQUEST\", message: \"rule_id required\" } });\n const scope = ([\"global\", \"project\", \"source\"] as const).find((s) => s === b.scope) ?? \"global\";\n muteRule(triageDb!, b.rule_id, scope as MuteScope, b.scope_id ?? \"\", b.note ?? null);\n return { ok: true };\n });\n app.post(\"/api/security/unmute\", async (req, reply) => {\n if (!guardWrite(req, reply)) return reply;\n const b = (req.body ?? {}) as { rule_id?: string; scope?: string; scope_id?: string };\n if (!b.rule_id) return reply.code(400).send({ error: { code: \"BAD_REQUEST\", message: \"rule_id required\" } });\n const scope = ([\"global\", \"project\", \"source\"] as const).find((s) => s === b.scope) ?? \"global\";\n const n = unmute(triageDb!, b.rule_id, scope as MuteScope, b.scope_id ?? \"\");\n return { ok: true, unmuted: n };\n });\n\n app.get(\"/api/skills\", async (req) => {\n const q = req.query as Record<string, string>;\n return listSkills(db, { q: q.q, source: q.source, project: q.project });\n });\n\n app.get(\"/api/skills/:name\", async (req, reply) => {\n const { name } = req.params as { name: string };\n let decoded: string;\n try {\n decoded = decodeURIComponent(name);\n } catch {\n // A malformed percent-escape (e.g. `%zz`) throws URIError — that's a bad request, not a 500.\n return reply.code(400).send({ error: { code: \"BAD_NAME\", message: \"invalid skill name encoding\" } });\n }\n const result = getSkill(db, decoded);\n if (!result) return notFound(reply);\n return result;\n });\n\n // File-modification provenance (ADR-022): the /files list and the per-file timeline. The file\n // path travels as a query param (it contains slashes), not a route segment.\n app.get(\"/api/files\", async (req) => {\n const q = req.query as Record<string, string>;\n const sort = ([\"last_ts\", \"first_ts\", \"changes\", \"sessions\", \"path\"] as const).find((s) => s === q.sort);\n return listFiles(db, {\n q: q.q,\n source: q.source,\n project: q.project,\n sort,\n dir: q.dir === \"asc\" ? \"asc\" : \"desc\",\n limit: pageLimit(q.limit, 50, 200),\n offset: pageOffset(q.offset),\n });\n });\n\n app.get(\"/api/file\", async (req, reply) => {\n const q = req.query as Record<string, string>;\n if (!q.path) return reply.code(400).send({ error: { code: \"MISSING_PATH\", message: \"missing path\" } });\n const result = getFileTimeline(db, q.path, q.project || undefined);\n if (!result) return notFound(reply);\n return result;\n });\n\n app.get(\"/api/sessions/:id/export.md\", async (req, reply) => {\n const { id } = req.params as { id: string };\n // Redaction defaults ON (selective secret/PII masking). `structure` = aggressive scrub;\n // `off` = explicit verbatim opt-out. Anything unrecognized falls back to the safe default.\n const level = parseRedactionLevel((req.query as { redact?: string }).redact);\n const out = renderSessionExport(db, id, level);\n if (!out) return notFound(reply);\n reply\n .header(\"content-type\", \"text/markdown; charset=utf-8\")\n .header(\"content-disposition\", `attachment; filename=\"${out.filename}\"`)\n .send(out.markdown);\n });\n\n // Serve the built SPA (if present) with a history fallback for client routes.\n if (opts.webDist && existsSync(opts.webDist)) {\n await app.register(fastifyStatic, { root: opts.webDist });\n app.setNotFoundHandler((req, reply) => {\n if (req.url.startsWith(\"/api/\")) return notFound(reply);\n // A missing *file* must 404 honestly. The tempting blanket fallback breaks the case where an\n // open tab outlives a rebuild: its index.html names hashed chunks the new dist no longer has\n // (`emptyOutDir`), so the lazy `import()` asks for /assets/SessionView-<old>.js and gets HTML\n // back with `content-type: text/html`. The browser refuses it on MIME grounds, and the error\n // the SPA sees is a module-load failure rather than a plain 404 — harder to recognize as\n // \"your build is stale\". Only extensionless paths are client routes worth falling back for.\n if (/\\.[^/.]+$/.test(req.url.split(\"?\")[0])) return notFound(reply);\n return reply.sendFile(\"index.html\");\n });\n }\n\n return app;\n}\n","/**\n * Session → shareable Markdown, shared by the HTTP route and the CLI `export` command so the\n * DB-row → MarkdownEvent mapping and filename convention live in exactly one place.\n * Redaction defaults ON (selective secret/PII masking); see @agent-lens/core exportMarkdown.\n */\nimport { exportMarkdown, type MarkdownEvent, type RedactionLevel } from \"@agent-lens/core\";\nimport { type DB, getSession } from \"./db.js\";\n\n/** Normalize a raw ?redact= / --level value to a valid level, defaulting to the safe `secrets`. */\nexport function parseRedactionLevel(raw: string | undefined): RedactionLevel | \"off\" {\n return raw === \"off\" ? \"off\" : raw === \"structure\" ? \"structure\" : \"secrets\";\n}\n\n/** Render session `id` to Markdown at the given level, or null if the session doesn't exist. */\nexport function renderSessionExport(\n db: DB,\n id: string,\n level: RedactionLevel | \"off\",\n): { markdown: string; filename: string } | null {\n const result = getSession(db, id);\n if (!result) return null;\n const s = result.session;\n const events: MarkdownEvent[] = result.events.map((e) => ({\n type: e.type,\n role: e.role,\n timestamp: e.timestamp,\n text: e.text,\n thinking: e.thinking,\n toolCalls: e.toolCalls.map((t: any) => ({\n tool_name: t.tool_name,\n skill_name: t.skill_name,\n agent_type: t.agent_type,\n input_json: t.input_json,\n status: t.status,\n })),\n }));\n const { markdown } = exportMarkdown(\n { id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at },\n events,\n { level },\n );\n const suffix = level === \"off\" ? \"\" : \".redacted\";\n return { markdown, filename: `session-${filenameSafe(id.slice(0, 8))}${suffix}.md` };\n}\n\n/**\n * Reduce a session id to characters that are safe in a `content-disposition` filename.\n *\n * The header is built by interpolation — `attachment; filename=\"<name>\"` (app.ts) — so a `\"` in the\n * id would close the quoted-string early and the rest would be parsed as header parameters. Ids are\n * ours today (uuids and slugs), which makes this defence-in-depth rather than a live hole, but the id\n * is a path segment the caller supplies and nothing else validates it before it reaches the header.\n * Sanitizing at the point the filename is BUILT keeps the guarantee with the naming convention\n * instead of leaving it to every future caller of this function.\n *\n * Anything outside `[A-Za-z0-9._-]` collapses to `_`, including the CR/LF that would otherwise permit\n * header injection. An id that is entirely unsafe still yields a usable name rather than an empty one.\n */\nfunction filenameSafe(s: string): string {\n const cleaned = s.replace(/[^A-Za-z0-9._-]/g, \"_\");\n return cleaned === \"\" ? \"session\" : cleaned;\n}\n","/**\n * Agent Lens — dashboard aggregation queries. Read-only; everything is computed as\n * server-side aggregates (GROUP BY over indexed columns) so the charts plot bounded series, not\n * raw rows — this is what lets the UI scale to years of data. Token series are always kept split\n * (input / output / cache-creation / cache-read); cache-read is never folded into a single\n * \"tokens\" number because it dominates and misleads. Cost is derived via the shared pricing table.\n */\nimport { costForUsage, rateForModel, errorKind, type ToolErrorType } from \"@agent-lens/core\";\nimport type { DashBreakdowns, DashOverview, DashTimeseries, TimeseriesPoint } from \"@agent-lens/contracts\";\nimport type { DB } from \"./db.js\";\nimport { tableExists, pushDateRange, queryAll, queryGet } from \"./sql-util.js\";\nimport type {\n BucketCountRow,\n BucketErrorRow,\n BucketUsageAggRow,\n CountRow,\n ModelBreakdownRow,\n OverviewCountsRow,\n SpanRow,\n UsageAggRow,\n WorkflowStatusAggRow,\n} from \"./rows.js\";\n\nexport interface DashFilters {\n source?: string;\n from?: string;\n to?: string;\n}\n\n/** A WHERE-clause fragment and its bound parameters, threaded through every aggregate below. */\ntype Where = { sql: string; params: unknown[] };\n\n/** WHERE clause + params over the `sessions` alias `s` (started_at / source_id). */\nfunction sessionWhere(f: DashFilters): Where {\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.source) (where.push(\"s.source_id = ?\"), params.push(f.source));\n // Date-inclusive on both ends (compare the DATE part) so a picked `to` day includes that day's\n // events — a plain `started_at <= '2026-07-14'` would drop everything after 2026-07-14T00:00.\n pushDateRange(where, params, \"s.started_at\", f.from, f.to);\n return { sql: where.length ? `WHERE ${where.join(\" AND \")}` : \"\", params };\n}\n\ninterface Split {\n input: number;\n output: number;\n cache_creation: number;\n cache_read: number;\n}\nconst zeroSplit = (): Split => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });\n\n/** Sum a grouped usage row into a split accumulator. */\nfunction addUsage(acc: Split, r: UsageAggRow) {\n acc.input += r.i ?? 0;\n acc.output += r.o ?? 0;\n acc.cache_creation += r.cw ?? 0;\n acc.cache_read += r.cr ?? 0;\n}\n\n/** The per-model cost arguments, unpacked from a usage aggregate's short SQL aliases. */\nconst usageForCost = (u: UsageAggRow) => ({\n input_tokens: u.i ?? 0,\n output_tokens: u.o ?? 0,\n cache_creation_input_tokens: u.cw ?? 0,\n cache_read_input_tokens: u.cr ?? 0,\n});\n\n/**\n * p50/p95 (nearest-rank) plus the sample size, computed **in SQL**.\n *\n * `values` is a `SELECT <expr> v FROM …` returning one row per observation, unordered; this wraps it\n * in a CTE and picks the ranked element with `LIMIT 1 OFFSET`. It used to pull every duration into JS\n * and sort there — one row per turn, per request, growing with the corpus forever.\n *\n * The offset is `ceil(p/100 × n) - 1` floored at 0, which is exactly the rank the previous JS\n * `pct()` selected, so stored percentiles do not shift. An empty set yields 0 (the CTE returns NULL,\n * coalesced), also matching the old behaviour.\n */\nfunction percentiles(db: DB, values: string, params: unknown[]): { p50: number; p95: number; count: number } {\n const at = (p: number) =>\n `(SELECT COALESCE((SELECT v FROM vals ORDER BY v\n LIMIT 1 OFFSET MAX(0, CAST(ceil(${p} / 100.0 * (SELECT c FROM n)) AS INTEGER) - 1)), 0))`;\n const row = queryGet<{ p50: number; p95: number; count: number }>(\n db,\n // MATERIALIZED matters: the three selects below each reference `vals`, and without the hint\n // SQLite inlines the CTE and re-runs the underlying scan once per percentile.\n `WITH vals AS MATERIALIZED (${values}), n AS (SELECT COUNT(*) c FROM vals)\n SELECT ${at(50)} AS p50, ${at(95)} AS p95, (SELECT c FROM n) AS count`,\n ...params,\n );\n return row ?? { p50: 0, p95: 0, count: 0 };\n}\n\n/** WHERE over `workflow_results` own columns (its own source_id / started_at, not the sessions alias). */\nfunction workflowWhere(f: DashFilters): Where {\n const where: string[] = [];\n const params: unknown[] = [];\n if (f.source) (where.push(\"source_id = ?\"), params.push(f.source));\n pushDateRange(where, params, \"started_at\", f.from, f.to);\n return { sql: where.length ? `WHERE ${where.join(\" AND \")}` : \"\", params };\n}\n\n/** Aggregate the async-workflow runs (schema v9+): counts by status, success rate over decided runs\n * (completed vs failed; in-flight `running` excluded from the denominator), and token/duration rollups.\n * Guarded so a pre-v9 DB (no `workflow_results` table) returns an all-zero shape instead of throwing. */\nfunction workflowAgg(db: DB, f: DashFilters): DashOverview[\"workflows\"] {\n const empty = { total: 0, by_status: [], completed: 0, failed: 0, success_rate: 0, total_tokens: 0, avg_duration_ms: 0 };\n if (!tableExists(db, \"workflow_results\")) return empty;\n const ww = workflowWhere(f);\n const rows = queryAll<WorkflowStatusAggRow>(\n db,\n `SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,\n SUM(COALESCE(total_tokens, 0)) tokens,\n SUM(COALESCE(duration_ms, 0)) dur,\n SUM(CASE WHEN duration_ms IS NOT NULL THEN 1 ELSE 0 END) dur_n\n FROM workflow_results ${ww.sql} GROUP BY status ORDER BY n DESC`,\n ...ww.params,\n );\n let total = 0, completed = 0, failed = 0, tokens = 0, durSum = 0, durN = 0;\n const by_status = rows.map((r) => {\n total += r.n;\n if (r.status === \"completed\") completed += r.n;\n else if (r.status === \"failed\") failed += r.n;\n tokens += r.tokens ?? 0;\n durSum += r.dur ?? 0;\n durN += r.dur_n ?? 0;\n return { status: r.status, n: r.n };\n });\n const decided = completed + failed;\n return {\n total,\n by_status,\n completed,\n failed,\n success_rate: decided ? completed / decided : 0,\n total_tokens: tokens,\n avg_duration_ms: durN ? Math.round(durSum / durN) : 0,\n };\n}\n\nexport function dashboardOverview(db: DB, f: DashFilters): DashOverview {\n const w = sessionWhere(f);\n\n const counts = queryGet<OverviewCountsRow>(\n db,\n `SELECT\n COUNT(*) sessions,\n SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,\n SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,\n SUM(turn_count) turns,\n COUNT(DISTINCT project_id) projects\n FROM sessions s ${w.sql}`,\n ...w.params,\n )!;\n\n const toolCount = queryGet<CountRow>(\n db,\n `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id ${w.sql}`,\n ...w.params,\n )!.n;\n\n // Per-model usage → token split + cache-aware cost; track unpriced models honestly.\n const usage = queryAll<UsageAggRow>(\n db,\n `SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,\n SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr\n FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}\n GROUP BY t.model`,\n ...w.params,\n );\n const tokens = zeroSplit();\n let cost = 0;\n const unpriced: string[] = [];\n for (const u of usage) {\n addUsage(tokens, u);\n cost += costForUsage(u.model, usageForCost(u));\n if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr)) unpriced.push(u.model);\n }\n const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;\n\n // Turn-duration percentiles (work cadence), excluding null durations.\n const turnDur = percentiles(\n db,\n `SELECT tn.duration_ms v FROM turns tn JOIN sessions s ON s.id = tn.session_id\n ${w.sql ? w.sql + \" AND\" : \"WHERE\"} tn.duration_ms IS NOT NULL`,\n w.params,\n );\n\n // Session-length percentiles over MAIN sessions only (subagents share the parent's wall clock),\n // excluding null durations. Complements the per-turn cadence with an end-to-end task-length view.\n const sessDur = percentiles(\n db,\n `SELECT s.duration_ms v FROM sessions s\n ${w.sql ? w.sql + \" AND\" : \"WHERE\"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL`,\n w.params,\n );\n\n return {\n range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },\n sessions: counts.sessions ?? 0,\n sessions_main: counts.main ?? 0,\n sessions_subagent: counts.subagent ?? 0,\n turns: counts.turns ?? 0,\n projects: counts.projects ?? 0,\n tool_calls: toolCount ?? 0,\n tokens, // {input, output, cache_creation, cache_read}\n total_tokens: totalTokens,\n cache_read_ratio: totalTokens ? tokens.cache_read / totalTokens : 0,\n cost: Number(cost.toFixed(4)),\n unpriced_models: [...new Set(unpriced)].sort(),\n turn_duration_ms: turnDur,\n session_duration_ms: sessDur,\n workflows: workflowAgg(db, f),\n };\n}\n\ntype Bucket = \"day\" | \"week\" | \"month\";\n\n/** Pick a bucket so the series stays small regardless of how wide the range is. */\nfunction chooseBucket(db: DB, f: DashFilters): Bucket {\n const w = sessionWhere(f);\n const r = queryGet<SpanRow>(db, `SELECT MIN(started_at) mn, MAX(started_at) mx FROM sessions s ${w.sql}`, ...w.params);\n if (!r?.mn || !r?.mx) return \"day\";\n const spanDays = (Date.parse(r.mx) - Date.parse(r.mn)) / 86_400_000;\n if (spanDays <= 92) return \"day\";\n if (spanDays <= 730) return \"week\";\n return \"month\";\n}\n\nconst BUCKET_EXPR: Record<Bucket, string> = {\n day: \"strftime('%Y-%m-%d', s.started_at)\",\n week: \"strftime('%Y-W%W', s.started_at)\",\n month: \"strftime('%Y-%m', s.started_at)\",\n};\n\nexport function dashboardTimeseries(db: DB, f: DashFilters, bucketParam?: string): DashTimeseries {\n const bucket: Bucket = bucketParam === \"day\" || bucketParam === \"week\" || bucketParam === \"month\" ? bucketParam : chooseBucket(db, f);\n const w = sessionWhere(f);\n const expr = BUCKET_EXPR[bucket];\n\n const byBucket = new Map<string, TimeseriesPoint>();\n const get = (b: string): TimeseriesPoint => {\n let r = byBucket.get(b);\n if (!r) {\n r = { bucket: b, ...zeroSplit(), cost: 0, sessions: 0, turns: 0, failures: 0, rejections: 0 };\n byBucket.set(b, r);\n }\n return r;\n };\n\n for (const u of queryAll<BucketUsageAggRow>(\n db,\n `SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,\n SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr\n FROM token_usage t JOIN sessions s ON s.id = t.session_id\n ${w.sql} GROUP BY b, t.model`,\n ...w.params,\n )) {\n if (!u.b) continue;\n const r = get(u.b);\n addUsage(r, u);\n r.cost += costForUsage(u.model, usageForCost(u));\n }\n for (const s of queryAll<BucketCountRow>(db, `SELECT ${expr} b, COUNT(*) n FROM sessions s ${w.sql} GROUP BY b`, ...w.params)) {\n if (s.b) get(s.b).sessions = s.n;\n }\n for (const t of queryAll<BucketCountRow>(\n db,\n `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${w.sql} GROUP BY b`,\n ...w.params,\n )) {\n if (t.b) get(t.b).turns = t.n;\n }\n // Errored tool calls per bucket, split into genuine failures vs user/guardrail rejections (kind\n // derived from the stored error_type). Bucketed by the session's date, same as every other series.\n for (const e of queryAll<BucketErrorRow>(\n db,\n `SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${w.sql ? w.sql + \" AND\" : \"WHERE\"} tc.status = 'error' GROUP BY b, tc.error_type`,\n ...w.params,\n )) {\n if (!e.b) continue;\n const r = get(e.b);\n if (e.et && errorKind(e.et as ToolErrorType) === \"rejection\") r.rejections += e.n;\n else r.failures += e.n;\n }\n\n const series = [...byBucket.values()].sort((a, b) => a.bucket.localeCompare(b.bucket));\n for (const r of series) r.cost = Number(r.cost.toFixed(4));\n return { bucket, series };\n}\n\n/** Extend a session WHERE clause with one more condition, opening the clause when it is empty. */\nconst andWhere = (w: Where) => (w.sql ? w.sql + \" AND\" : \"WHERE\");\n\nfunction modelBreakdown(db: DB, w: Where): DashBreakdowns[\"by_model\"] {\n const modelRows = queryAll<ModelBreakdownRow>(\n db,\n // COALESCE the bucket label, exactly as by_source does for a null source_id: token_usage.model is\n // nullable and this GROUP BY — unlike the model FILTER list — does not exclude nulls, so a\n // null-model bucket reaches the chart. It used to render as a nameless bar.\n `SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,\n SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,\n COUNT(DISTINCT t.session_id) sessions\n FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}\n 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`,\n ...w.params,\n );\n return modelRows.map((u) => ({\n model: u.model,\n tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },\n total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),\n cost: Number(costForUsage(u.model, usageForCost(u)).toFixed(4)),\n sessions: u.sessions ?? 0,\n priced: !!rateForModel(u.model),\n }));\n}\n\nfunction sourceBreakdown(db: DB, w: Where): DashBreakdowns[\"by_source\"] {\n return queryAll<DashBreakdowns[\"by_source\"][number]>(\n db,\n `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns\n FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`,\n ...w.params,\n );\n}\n\n/** Category + complexity over MAIN sessions only (subagents inherit the parent's task). */\nfunction classificationBreakdowns(db: DB, w: Where): {\n byCategory: DashBreakdowns[\"by_category\"];\n byComplexity: DashBreakdowns[\"by_complexity\"];\n} {\n const mainW: Where = { sql: w.sql ? w.sql + \" AND s.is_sidechain = 0\" : \"WHERE s.is_sidechain = 0\", params: w.params };\n const byCategory = queryAll<DashBreakdowns[\"by_category\"][number]>(\n db,\n `SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id\n ${mainW.sql} GROUP BY c.category ORDER BY n DESC`,\n ...mainW.params,\n );\n const byComplexity = queryAll<DashBreakdowns[\"by_complexity\"][number]>(\n db,\n `SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id\n ${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`,\n ...mainW.params,\n );\n return { byCategory, byComplexity };\n}\n\nfunction toolBreakdown(db: DB, w: Where): DashBreakdowns[\"tools\"] {\n return queryAll<DashBreakdowns[\"tools\"][number]>(\n db,\n `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`,\n ...w.params,\n );\n}\n\n/** Skill firings by name, plus the per-version breakdown for the grouped skill bar chart: each\n * captured version's firing count, with the version id + its last-seen time so the chart can\n * stack/hover and deep-link to /skill/:name?v=<id>. Both share the same source/from/to filter. */\nfunction skillBreakdowns(db: DB, w: Where): {\n skills: DashBreakdowns[\"skills\"];\n skillVersions: DashBreakdowns[\"skill_versions\"];\n} {\n const skills = queryAll<DashBreakdowns[\"skills\"][number]>(\n db,\n `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`,\n ...w.params,\n );\n\n const skillVersions = queryAll<DashBreakdowns[\"skill_versions\"][number]>(\n db,\n `SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n\n FROM tool_calls tc\n JOIN skills sk ON sk.id = tc.skill_id\n JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.skill_id IS NOT NULL\n GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`,\n ...w.params,\n );\n\n return { skills, skillVersions };\n}\n\n/** Subagent fan-out: spawns by type, plus a per-(main-)session histogram of subagent calls. */\nfunction subagentFanoutBreakdown(db: DB, w: Where): DashBreakdowns[\"subagent_fanout\"] {\n const subagentByType = queryAll<DashBreakdowns[\"subagent_fanout\"][\"by_type\"][number]>(\n db,\n `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`,\n ...w.params,\n );\n const perSession = queryAll<CountRow>(\n db,\n `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')\n GROUP BY tc.session_id`,\n ...w.params,\n );\n const counts = perSession.map((r) => r.n);\n return {\n by_type: subagentByType,\n sessions_with_subagents: counts.length,\n total_spawns: counts.reduce((a, b) => a + b, 0),\n max_per_session: counts.length ? Math.max(...counts) : 0,\n avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0,\n };\n}\n\n/** Errored tool calls by heuristic error_type, with failures-vs-rejections totals. error_type is\n * populated only for status='error' rows (see errors.ts); the raw count is authoritative, the bucket\n * is the heuristic. Ordered most-frequent first. */\nfunction errorTypeBreakdown(db: DB, w: Where): DashBreakdowns[\"error_types\"] {\n const errorRows = queryAll<{ type: string; n: number }>(\n db,\n `SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id\n ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`,\n ...w.params,\n );\n let failures = 0;\n let rejections = 0;\n const byType = errorRows.map((r) => {\n // See the note in db.ts's withSessionTotals: a stored bucket name need not still be one of the\n // ToolErrorType literals, and errorKind answers \"failure\" for anything it doesn't recognize.\n const kind = errorKind(r.type as ToolErrorType);\n if (kind === \"rejection\") rejections += r.n;\n else failures += r.n;\n return { type: r.type, kind, n: r.n };\n });\n return { by_type: byType, failures, rejections };\n}\n\nexport function dashboardBreakdowns(db: DB, f: DashFilters): DashBreakdowns {\n const w = sessionWhere(f);\n const { byCategory, byComplexity } = classificationBreakdowns(db, w);\n const { skills, skillVersions } = skillBreakdowns(db, w);\n\n return {\n by_model: modelBreakdown(db, w),\n by_source: sourceBreakdown(db, w),\n by_category: byCategory,\n by_complexity: byComplexity,\n tools: toolBreakdown(db, w),\n skills,\n skill_versions: skillVersions,\n subagent_fanout: subagentFanoutBreakdown(db, w),\n error_types: errorTypeBreakdown(db, w),\n };\n}\n","/**\n * `POST /api/refresh` support — the one write-action on an otherwise read-only server (ADR-005).\n * Runs a single collect + ingest pass on the host so the UI can pull in new transcripts on demand,\n * without waiting for the scheduled/`watch` collector. See ADR-015 for the scoped exception.\n *\n * Two guards keep this safe on an unauthenticated loopback server:\n * - `originAllowed` blocks cross-site CSRF (a web page you visit POSTing to 127.0.0.1).\n * - the shared single-instance lock stops it racing a scheduled/`watch`/CLI run.\n * The server's own DB handle stays read-only; ingest opens its own short-lived write connection\n * (WAL permits one writer alongside readers), so the read-only invariant of THIS handle holds.\n */\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { acquireLock, collectAll, findRepoRoot, resolveArchiveDir, resolveDataDir, type CollectStats } from \"@agent-lens/core\";\nimport { runIngest } from \"@agent-lens/ingest\";\n\nexport interface RefreshResult {\n ok: true;\n collected: CollectStats;\n}\n\n/** Loopback host authorities (hostname only; any port), shared by the Host guard (app.ts) and the\n * Origin/CSRF guard below. Note the bracketed IPv6 form: `new URL(...).hostname` and a stripped Host\n * header both yield `[::1]` (not `::1`) for the IPv6 loopback, so the set must carry the brackets. */\nexport const LOOPBACK_HOSTS = new Set([\"127.0.0.1\", \"localhost\", \"[::1]\"]);\n\n/**\n * CSRF guard for the loopback server. Allow requests with no `Origin` (non-browser callers like curl\n * or the CLI — not a CSRF vector) and same-machine origins; reject anything cross-site. The server\n * already binds loopback only (ADR-005); this stops a page you visit from driving localhost.\n */\nexport function originAllowed(origin: string | undefined): boolean {\n if (!origin) return true;\n try {\n return LOOPBACK_HOSTS.has(new URL(origin).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether a state-changing request should be blocked (defense-in-depth CSRF, LOW-001). Combines the\n * `Origin` allowlist with `Sec-Fetch-Site`: browsers stamp the latter on every request, so a\n * cross-site/same-site fetch is rejected even if `Origin` is somehow absent — closing the\n * `originAllowed(undefined) === true` gap. Non-browser callers (curl/CLI) send neither header and\n * stay allowed. Paired with the loopback-Host guard (HIGH-001), this covers the rebinding case too.\n */\nexport function writeBlocked(headers: Record<string, string | string[] | undefined>): boolean {\n const site = headers[\"sec-fetch-site\"];\n const s = Array.isArray(site) ? site[0] : site;\n if (typeof s === \"string\" && s !== \"same-origin\" && s !== \"none\") return true;\n const origin = headers[\"origin\"];\n return !originAllowed(Array.isArray(origin) ? origin[0] : origin);\n}\n\n/**\n * Run one collect + ingest pass under the shared single-instance lock. Returns `null` if a run is\n * already in progress (caller → 409). Throws on collect/ingest failure (caller → 500). Synchronous\n * and blocking: on a large archive this briefly stalls the server, acceptable for a local\n * single-user tool (the clicking user is waiting on it anyway).\n */\nexport function runRefresh(): RefreshResult | null {\n const dataDir = resolveDataDir(findRepoRoot());\n const lock = acquireLock(join(dataDir, \".agent-lens.lock\"));\n if (!lock) return null;\n try {\n const collected = collectAll();\n // runIngest process.exit(1)s if the archive dir is missing — which would kill the server. Ensure\n // it exists first (collectAll creates it when there's anything to copy; this covers the empty case).\n const archiveRoot = resolveArchiveDir();\n if (!existsSync(archiveRoot)) mkdirSync(archiveRoot, { recursive: true });\n runIngest([]);\n return { ok: true, collected };\n } finally {\n lock.release();\n }\n}\n","/**\n * Agent Lens — security-finding triage store (ADR-018). User-authored triage state (which findings are\n * dismissed as safe, which rules are muted) is NOT derivable from the archive, so it can't live in the\n * rebuildable analytics DB — `detect()` wipes and re-inserts `findings` on every ingest, and\n * `ingest --full` drops every analytics table. Instead it lives in a **separate, writable `triage.db`**\n * that ingest never touches, keyed by the *stable* finding id (`sha1(tool_call_id, rule_id)`), so triage\n * survives re-detection and full rebuilds automatically.\n *\n * The analytics server handle stays read-only (ADR-005): it only ATTACHes this file to JOIN triage\n * state into the findings list. Writes go through this dedicated handle, guarded by the same CSRF check\n * as `POST /api/refresh`. WAL lets the writer run alongside the read handle's queries.\n */\nimport { DatabaseSync } from \"node:sqlite\";\nimport { transaction } from \"@agent-lens/core\";\n\nexport type TriageDB = DatabaseSync;\n\nexport const TRIAGE_SCHEMA_SQL = /* sql */ `\nPRAGMA journal_mode = WAL;\n\n-- Findings the user reviewed and marked safe. Keyed by the stable finding id so it survives re-ingest.\nCREATE TABLE IF NOT EXISTS dismissed_findings (\n finding_id TEXT PRIMARY KEY, -- findings.id = sha1(tool_call_id, rule_id)\n tool_call_id TEXT, -- reference only (may be null)\n rule_id TEXT, -- reference only (may be null)\n note TEXT,\n dismissed_at TEXT NOT NULL\n);\n\n-- Rules the user muted (suppressed as noise), optionally scoped to a project or source. A muted rule\n-- hides its current AND future findings from the open view — the scalable lever for a noisy rule.\nCREATE TABLE IF NOT EXISTS muted_rules (\n rule_id TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT 'global', -- 'global' | 'project' | 'source'\n scope_id TEXT NOT NULL DEFAULT '', -- project_id / source_id; '' for global (NOT NULL keeps it in the PK)\n note TEXT,\n muted_at TEXT NOT NULL,\n PRIMARY KEY (rule_id, scope, scope_id)\n);\n`;\n\n/** Open (creating if needed) the writable triage DB and ensure its schema. */\nexport function openTriage(file: string): TriageDB {\n const db = new DatabaseSync(file);\n db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(TRIAGE_SCHEMA_SQL);\n return db;\n}\n\nconst now = () => new Date().toISOString();\n\n/** Mark findings safe (idempotent upsert). Returns the number of ids processed. */\nexport function dismiss(db: TriageDB, ids: string[], note?: string | null): number {\n const stmt = db.prepare(\n `INSERT INTO dismissed_findings (finding_id, note, dismissed_at) VALUES (?, ?, ?)\n ON CONFLICT(finding_id) DO UPDATE SET note = excluded.note, dismissed_at = excluded.dismissed_at`,\n );\n const ts = now();\n const tx = transaction(db, (list: string[]) => {\n for (const id of list) stmt.run(id, note ?? null, ts);\n });\n tx(ids);\n return ids.length;\n}\n\n/** Reopen (un-dismiss) findings. Returns the number of rows removed. */\nexport function reopen(db: TriageDB, ids: string[]): number {\n const stmt = db.prepare(\"DELETE FROM dismissed_findings WHERE finding_id = ?\");\n let n = 0;\n const tx = transaction(db, (list: string[]) => {\n for (const id of list) n += Number(stmt.run(id).changes);\n });\n tx(ids);\n return n;\n}\n\nexport type MuteScope = \"global\" | \"project\" | \"source\";\n\n/** Mute a rule (optionally scoped). Idempotent. */\nexport function muteRule(db: TriageDB, ruleId: string, scope: MuteScope = \"global\", scopeId = \"\", note?: string | null): void {\n db.prepare(\n `INSERT INTO muted_rules (rule_id, scope, scope_id, note, muted_at) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(rule_id, scope, scope_id) DO UPDATE SET note = excluded.note, muted_at = excluded.muted_at`,\n ).run(ruleId, scope, scope === \"global\" ? \"\" : scopeId, note ?? null, now());\n}\n\n/** Unmute a rule. Returns rows removed (0 if it wasn't muted at that scope). */\nexport function unmute(db: TriageDB, ruleId: string, scope: MuteScope = \"global\", scopeId = \"\"): number {\n return Number(\n db\n .prepare(\"DELETE FROM muted_rules WHERE rule_id = ? AND scope = ? AND scope_id = ?\")\n .run(ruleId, scope, scope === \"global\" ? \"\" : scopeId).changes,\n );\n}\n\nexport interface MuteRow {\n rule_id: string;\n scope: string;\n scope_id: string;\n note: string | null;\n muted_at: string;\n}\n\n/** All muted rules, newest first — for the management panel. */\nexport function listMutes(db: TriageDB): MuteRow[] {\n return db\n .prepare(\"SELECT rule_id, scope, scope_id, note, muted_at FROM muted_rules ORDER BY muted_at DESC\")\n .all() as unknown as MuteRow[];\n}\n","/**\n * GET /api/about — the read-only diagnostics payload (ADR-027).\n *\n * This is the web mirror of `agent-lens config`, plus versions and storage. Two rules make it\n * trustworthy, and both are easy to break by accident:\n *\n * 1. **Report what the server is actually using, not a fresh resolution.** The overridable facts\n * (db path + its origin, host, port) are passed in from `run.ts`, which resolved them with\n * precedence flag > env > file > default. Re-resolving them here would silently ignore\n * `serve --db /custom.db` and report a path nobody is reading.\n * 2. **Never let this reach the static snapshot.** It carries absolute paths and\n * `export-snapshot.mjs` publishes to a public GitHub Pages demo. The exporter deliberately does\n * not fetch it; the SPA hides the page under `SNAPSHOT`.\n *\n * Read-only on purpose: config resolves flag > env > file > default, so an editor could only write\n * the third layer and would do nothing at all for anyone using env. Values carry their `origin`\n * instead — the same idiom `agent-lens config` prints.\n */\nimport { statSync } from \"node:fs\";\nimport type { AboutResponse, EngineVersion } from \"@agent-lens/contracts\";\nimport {\n loadSources,\n resolveArchiveDir,\n resolveConfigFile,\n resolveDataDir,\n resolveRetention,\n resolveVersion,\n triageDbFor,\n unpricedModels,\n type ConfigOrigin,\n type ResolvedPricing,\n} from \"@agent-lens/core\";\nimport { CLASSIFIER_VERSION, DETECTOR_VERSION } from \"@agent-lens/ingest\";\nimport { type DB, schemaStatus } from \"./db.js\";\nimport { queryAll, queryGet, tableExists } from \"./sql-util.js\";\n\n/** The facts `run.ts` resolved at startup — the ones a flag or env can change. */\nexport interface AboutContext {\n db: { path: string; origin: ConfigOrigin };\n host: string;\n port: number;\n loopbackOnly: boolean;\n repoRoot: string | null;\n /** The price table this process installed at startup (ADR-028) — reported, never re-resolved. */\n pricing: ResolvedPricing;\n}\n\ninterface StorageRow {\n files: number;\n bytes: number | null;\n last: string | null;\n}\n\n/**\n * Ingested archive bytes, straight from the per-file bookkeeping ingest already maintains\n * (`ingest_state.size`, written by both pipeline.ts and sidecar.ts). No filesystem walk, no cached\n * copy to go stale: measured at ~0.5ms over 7.5k rows, and it is \"as of the last ingest\" by\n * construction because ingest_state *is* the ingest bookkeeping.\n *\n * This is deliberately NOT `du` of the archive dir — anything present but not ingested (notably\n * `.versions/` retention snapshots) is excluded. The UI says \"ingested\" for that reason.\n */\nfunction storage(db: DB, dbPath: string): AboutResponse[\"storage\"] {\n const row = tableExists(db, \"ingest_state\")\n ? queryGet<StorageRow>(db, \"SELECT COUNT(*) files, SUM(size) bytes, MAX(ingested_at) last FROM ingest_state\")\n : undefined;\n\n let db_bytes: number | null = null;\n try {\n db_bytes = statSync(dbPath).size;\n } catch {\n db_bytes = null; // path moved out from under a running server — report unknown, don't throw\n }\n\n return {\n db_bytes,\n archive_bytes: row?.bytes ?? 0, // SUM over zero rows is NULL, not 0\n archive_files: row?.files ?? 0,\n last_ingested: row?.last ?? null,\n };\n}\n\n/**\n * Models this store has real token usage for but no rate — the set whose cost silently reads as $0.\n * Computed from the store rather than from the price table alone, because \"which rates are missing\"\n * only means something relative to the models actually ingested.\n */\nfunction unpriced(db: DB): string[] {\n if (!tableExists(db, \"token_usage\")) return [];\n const rows = queryAll<{ model: string | null }>(\n db,\n `SELECT DISTINCT model FROM token_usage\n WHERE input_tokens + output_tokens + cache_creation_input_tokens + cache_read_input_tokens > 0`,\n );\n return unpricedModels(rows.map((r) => r.model));\n}\n\n/**\n * What a stamped engine produces now vs what is actually stored. The stored side is the point: it\n * answers \"were these findings produced by the rules this build has?\", which is invisible otherwise\n * and is what tells you a re-run (`agent-lens ingest --full`, `agent-lens metrics`) is pending.\n *\n * Rows stamped NEWER than this build are possible too — an older server reading a DB a newer ingest\n * wrote — and are deliberately not flagged `stale`: re-running would *downgrade* them.\n */\nfunction engineVersion(db: DB, table: string, sql: string, expected: number): EngineVersion {\n // `sql` is a literal at each call site — no identifier is ever interpolated here.\n const in_data = tableExists(db, table)\n ? queryAll<{ v: number }>(db, sql)\n .map((r) => r.v)\n .filter((v) => Number.isFinite(v))\n : [];\n return { expected, in_data, stale: in_data.some((v) => v < expected) };\n}\n\nexport function about(db: DB, ctx: AboutContext): AboutResponse {\n const version = resolveVersion();\n const schema = schemaStatus(db);\n const dataDir = resolveDataDir(ctx.repoRoot);\n const retention = resolveRetention();\n\n return {\n versions: {\n app: version.version,\n app_source: version.source,\n schema: schema.db_version,\n schema_expected: schema.expected,\n schema_stale: schema.stale,\n detector: engineVersion(\n db,\n \"findings\",\n \"SELECT DISTINCT detector_version v FROM findings ORDER BY v\",\n DETECTOR_VERSION,\n ),\n classifier: engineVersion(\n db,\n \"classifications\",\n \"SELECT DISTINCT classifier_version v FROM classifications ORDER BY v\",\n CLASSIFIER_VERSION,\n ),\n },\n paths: {\n config_file: resolveConfigFile(ctx.repoRoot, dataDir),\n data_dir: { path: dataDir, origin: process.env.AGENT_LENS_DATA ? \"env\" : \"default\" },\n // archive and triage_db are \"fixed\": ADR-021 makes them non-relocatable on their own, so they\n // are derived here with the same core helpers the CLI uses rather than read from config.\n archive: { path: resolveArchiveDir(ctx.repoRoot), origin: \"fixed\" },\n db: { path: ctx.db.path, origin: ctx.db.origin },\n triage_db: { path: triageDbFor(ctx.db.path), origin: \"fixed\" },\n },\n server: { host: ctx.host, port: ctx.port, loopback_only: ctx.loopbackOnly },\n retention: { versions_keep_days: retention.keepDays, origin: retention.origin },\n pricing: {\n origin: ctx.pricing.origin,\n models: Object.keys(ctx.pricing.table).length,\n applied: ctx.pricing.applied,\n invalid: ctx.pricing.invalid,\n unpriced: unpriced(db),\n },\n sources: loadSources().map((s) => ({ label: s.label, agent: s.agent, config_dir: s.configDir })),\n // storage() already carries last_ingested from the same MAX(ingested_at) it aggregates over —\n // reusing it avoids a second query AND keeps the tableExists guard that lastIngested() lacks.\n storage: storage(db, ctx.db.path),\n };\n}\n","/**\n * Agent Lens — UI preferences store. Like security triage (ADR-018), which charts/columns a user\n * shows is user-authored state that is NOT derivable from the archive, so it can't live in the\n * rebuildable read-only analytics DB. It rides the same **writable sidecar** handle (the triage DB):\n * a generic key→JSON table the server treats as opaque. The client keeps localStorage as an optimistic\n * cache and writes through to here; when no writable store is configured the endpoints degrade so the\n * client simply keeps its local value. Single-user local tool → prefs are global (no per-user key).\n */\nimport type { TriageDB } from \"./triage.js\";\n\nexport const PREFS_SCHEMA_SQL = /* sql */ `\nCREATE TABLE IF NOT EXISTS ui_prefs (\n key TEXT PRIMARY KEY, -- e.g. 'dashboard.charts', 'sessions.columns'\n value TEXT NOT NULL, -- JSON blob, opaque to the server\n updated_at TEXT NOT NULL\n);\n`;\n\n/** Raw stored JSON string for a key, or null if unset. */\nexport function getPref(db: TriageDB, key: string): string | null {\n const row = db.prepare(\"SELECT value FROM ui_prefs WHERE key = ?\").get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\n/** Upsert a key's JSON string (idempotent). */\nexport function setPref(db: TriageDB, key: string, value: string): void {\n db.prepare(\n `INSERT INTO ui_prefs (key, value, updated_at) VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n ).run(key, value, new Date().toISOString());\n}\n","/**\n * `agent-lens watch` — a resident process that collects + ingests whenever a source's transcripts\n * change (debounced), and optionally on a fixed interval. This is the Node-native periodic option;\n * `agent-lens service install collector` layers OS-level persistence (reboot survival) on top.\n *\n * Overlap safety: an in-process guard collapses a burst of file events into one cycle and never runs\n * two cycles at once; the shared single-instance lock additionally guards against a scheduled run\n * firing concurrently.\n */\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport { acquireLock, collectAll, findRepoRoot, loadSources, resolveDataDir, type Source } from \"@agent-lens/core\";\nimport { runIngest } from \"@agent-lens/ingest\";\n\nexport interface WatchOptions {\n intervalSec?: number;\n poll?: boolean;\n}\n\nexport async function runWatch(opts: WatchOptions = {}): Promise<void> {\n const sources: Source[] = loadSources();\n const lockPath = join(resolveDataDir(findRepoRoot()), \".agent-lens.lock\");\n\n let running = false;\n let pending = false;\n const cycle = () => {\n if (running) {\n pending = true; // a change arrived mid-cycle — run once more when this finishes\n return;\n }\n running = true;\n const lock = acquireLock(lockPath);\n if (!lock) {\n running = false;\n console.error(\"agent-lens watch: another collect/ingest run is in progress — skipping this cycle\");\n return;\n }\n try {\n const stats = collectAll({ sources });\n console.log(\n `agent-lens watch: collected ${stats.scanned} files (${stats.appended} appended, ${stats.snapshots} snapshots) — ingesting`,\n );\n runIngest([]);\n } catch (e) {\n console.error(\"agent-lens watch: cycle failed:\", e);\n } finally {\n lock.release();\n running = false;\n if (pending) {\n pending = false;\n setTimeout(cycle, 0);\n }\n }\n };\n\n cycle(); // initial catch-up\n\n const watchPaths = sources.map((s) => join(s.configDir, \"projects\")).filter((p) => existsSync(p));\n if (watchPaths.length === 0) {\n console.error(\"agent-lens watch: no source 'projects' directories found — nothing to watch\");\n }\n const watcher = chokidar.watch(watchPaths, {\n ignoreInitial: true,\n usePolling: !!opts.poll,\n awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },\n });\n let debounce: NodeJS.Timeout | null = null;\n const trigger = () => {\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(cycle, 3000); // coalesce bursts of writes into one cycle\n };\n watcher.on(\"add\", trigger).on(\"change\", trigger);\n\n if (opts.intervalSec && opts.intervalSec > 0) {\n setInterval(cycle, opts.intervalSec * 1000);\n }\n\n console.log(\n `agent-lens watch: watching ${watchPaths.length} source dir(s)` +\n (opts.intervalSec ? ` + every ${opts.intervalSec}s` : \"\") +\n \" — Ctrl-C to stop\",\n );\n // Keep the process alive indefinitely (chokidar + timers hold it open, but be explicit).\n return new Promise<void>(() => {});\n}\n"],"mappings":"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","names":["existsSync","writeFileSync","join","fileURLToPath","parse","cli","version","readFileSync","join","readFileSync","join","readFileSync","dirname","join","fileURLToPath","moduleDir","existsSync","dirname","join","platform","platform","dirname","join","existsSync","existsSync","mkdirSync","readFileSync","unlinkSync","dirname","spawnSync","mkdirSync","writeFileSync","homedir","platform","join","resolve","resolve","cli","spawnSync","join","homedir","mkdirSync","writeFileSync","platform","readFileSync","statSync","mkdirSync","existsSync","dirname","join","readdirSync","basename","join","path","createHash","createHash","path","createHash","path","createHash","path","existsSync","homedir","createHash","dirname","join","path","createHash","existsSync","join","dirname","homedir","createHash","createHash","c","now","projectId","basename","readdirSync","readFileSync","statSync","join","createHash","openSync","readSync","closeSync","CHUNK","path","path","readdirSync","join","now","statSync","readFileSync","hash","now","path","basename","durationMs","basename","now","path","basename","basename","now","path","basename","path","readFileSync","hash","unpriced","existsSync","mkdirSync","dirname","now","join","statSync","existsSync","dirname","fileURLToPath","DatabaseSync","DatabaseSync","CHUNK","projectId","existsSync","unpriced","existsSync","mkdirSync","join","join","existsSync","mkdirSync","DatabaseSync","DatabaseSync","statSync","statSync","version","existsSync","dirname","fileURLToPath","existsSync","existsSync","join","join","existsSync","join","existsSync","writeFileSync","fileURLToPath"]}
|