reasonix 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/dashboard/dist/app.js +98 -3
- package/dashboard/dist/app.js.map +1 -1
- package/dist/cli/{acp-64VQZLDJ.js → acp-DAGPCVFZ.js} +33 -28
- package/dist/cli/acp-DAGPCVFZ.js.map +1 -0
- package/dist/cli/chat-7ES4IBNH.js +50 -0
- package/dist/cli/{chunk-E46ECXJD.js → chunk-2425HK6U.js} +2 -1
- package/dist/cli/{chunk-E46ECXJD.js.map → chunk-2425HK6U.js.map} +1 -1
- package/dist/cli/chunk-25T6CVUP.js +172 -0
- package/dist/cli/chunk-25T6CVUP.js.map +1 -0
- package/dist/cli/{chunk-CD4SCQL4.js → chunk-2K65GZBT.js} +11 -2
- package/dist/cli/chunk-2K65GZBT.js.map +1 -0
- package/dist/cli/{chunk-H4OLWRSX.js → chunk-2KDUS647.js} +5 -4
- package/dist/cli/chunk-2KDUS647.js.map +1 -0
- package/dist/cli/chunk-2R4QCDOZ.js +11392 -0
- package/dist/cli/chunk-2R4QCDOZ.js.map +1 -0
- package/dist/cli/{chunk-3Q3C4W66.js → chunk-2UQP6H6T.js} +2 -1
- package/dist/cli/{chunk-3Q3C4W66.js.map → chunk-2UQP6H6T.js.map} +1 -1
- package/dist/cli/chunk-2Z35JOA4.js +96 -0
- package/dist/cli/chunk-2Z35JOA4.js.map +1 -0
- package/dist/cli/chunk-32TIKD5U.js +54 -0
- package/dist/cli/{chunk-L7W3HJZQ.js.map → chunk-32TIKD5U.js.map} +1 -1
- package/dist/cli/{chunk-A3LL4XDV.js → chunk-3BXRZFWS.js} +59 -3
- package/dist/cli/chunk-3BXRZFWS.js.map +1 -0
- package/dist/cli/{chunk-SXLJBFIV.js → chunk-3Z6IBU3D.js} +11 -7
- package/dist/cli/chunk-3Z6IBU3D.js.map +1 -0
- package/dist/cli/{chunk-A7VHMMDE.js → chunk-45U62RI3.js} +12 -5
- package/dist/cli/chunk-45U62RI3.js.map +1 -0
- package/dist/cli/{chunk-7VFNPMKG.js → chunk-4QUNBQQ2.js} +3 -2
- package/dist/cli/{chunk-7VFNPMKG.js.map → chunk-4QUNBQQ2.js.map} +1 -1
- package/dist/cli/{chunk-ARF3N2SY.js → chunk-5JJRUIPA.js} +4 -3
- package/dist/cli/{chunk-ARF3N2SY.js.map → chunk-5JJRUIPA.js.map} +1 -1
- package/dist/cli/{chunk-CFY2XLY6.js → chunk-6AK4EY3D.js} +7 -5
- package/dist/cli/{chunk-CFY2XLY6.js.map → chunk-6AK4EY3D.js.map} +1 -1
- package/dist/cli/chunk-6G3CUUFG.js +34320 -0
- package/dist/cli/chunk-6G3CUUFG.js.map +1 -0
- package/dist/cli/{chunk-YJFKFTAL.js → chunk-6PBZN4VI.js} +15 -3
- package/dist/cli/chunk-6PBZN4VI.js.map +1 -0
- package/dist/cli/{chunk-4W2CICFQ.js → chunk-6PZ3CXBP.js} +71 -60
- package/dist/cli/chunk-6PZ3CXBP.js.map +1 -0
- package/dist/cli/chunk-74EX7SUH.js +25293 -0
- package/dist/cli/chunk-74EX7SUH.js.map +1 -0
- package/dist/cli/{chunk-WE3YZULK.js → chunk-7O5ALB4C.js} +3 -2
- package/dist/cli/{chunk-WE3YZULK.js.map → chunk-7O5ALB4C.js.map} +1 -1
- package/dist/cli/{chunk-2CXPDAWX.js → chunk-DOYHN4KB.js} +3 -2
- package/dist/cli/{chunk-2CXPDAWX.js.map → chunk-DOYHN4KB.js.map} +1 -1
- package/dist/cli/{chunk-VFG4GIT3.js → chunk-F3PXYSNN.js} +3 -2
- package/dist/cli/{chunk-VFG4GIT3.js.map → chunk-F3PXYSNN.js.map} +1 -1
- package/dist/cli/{chunk-7SPOFTMT.js → chunk-FHOGSSCH.js} +4 -3
- package/dist/cli/{chunk-7SPOFTMT.js.map → chunk-FHOGSSCH.js.map} +1 -1
- package/dist/cli/{chunk-LTXADNCO.js → chunk-H6PS7IUE.js} +3 -2
- package/dist/cli/{chunk-LTXADNCO.js.map → chunk-H6PS7IUE.js.map} +1 -1
- package/dist/cli/{chunk-ZTLZO42A.js → chunk-HFEAY5DT.js} +3 -2
- package/dist/cli/{chunk-ZTLZO42A.js.map → chunk-HFEAY5DT.js.map} +1 -1
- package/dist/cli/{chunk-FWGEHRB7.js → chunk-J5XJHLWM.js} +2 -1
- package/dist/cli/{chunk-FWGEHRB7.js.map → chunk-J5XJHLWM.js.map} +1 -1
- package/dist/cli/{chunk-Y5XNV3NX.js → chunk-JMBMLOBP.js} +2 -1
- package/dist/cli/{chunk-Y5XNV3NX.js.map → chunk-JMBMLOBP.js.map} +1 -1
- package/dist/cli/{chunk-BYZGO3BX.js → chunk-O52OLQL3.js} +11 -3
- package/dist/cli/chunk-O52OLQL3.js.map +1 -0
- package/dist/cli/{chunk-4DCHFFEY.js → chunk-OSZC7C6F.js} +3 -2
- package/dist/cli/{chunk-4DCHFFEY.js.map → chunk-OSZC7C6F.js.map} +1 -1
- package/dist/cli/chunk-P7EKE5ZQ.js +60641 -0
- package/dist/cli/chunk-P7EKE5ZQ.js.map +1 -0
- package/dist/cli/{chunk-FM57FNPJ.js → chunk-PLHAZOLZ.js} +2 -1
- package/dist/cli/{chunk-FM57FNPJ.js.map → chunk-PLHAZOLZ.js.map} +1 -1
- package/dist/cli/{chunk-BOFL3T45.js → chunk-PQXPXJBJ.js} +12 -11
- package/dist/cli/chunk-PQXPXJBJ.js.map +1 -0
- package/dist/cli/{chunk-DAEAAVDF.js → chunk-PV55UMTO.js} +2 -1
- package/dist/cli/{chunk-DAEAAVDF.js.map → chunk-PV55UMTO.js.map} +1 -1
- package/dist/cli/{chunk-MHGPBJ2T.js → chunk-RE4RAVFF.js} +45 -10
- package/dist/cli/chunk-RE4RAVFF.js.map +1 -0
- package/dist/cli/chunk-S4XVGLRW.js +499 -0
- package/dist/cli/chunk-S4XVGLRW.js.map +1 -0
- package/dist/cli/{chunk-WJ3YX4PZ.js → chunk-SZ5XES2N.js} +3 -2
- package/dist/cli/{chunk-WJ3YX4PZ.js.map → chunk-SZ5XES2N.js.map} +1 -1
- package/dist/cli/{chunk-UV7XJUJH.js → chunk-TJX6BFZZ.js} +16 -9
- package/dist/cli/{chunk-UV7XJUJH.js.map → chunk-TJX6BFZZ.js.map} +1 -1
- package/dist/cli/chunk-TUK7OWJA.js +51 -0
- package/dist/cli/{chunk-KZYLMMU5.js → chunk-VK5HG73G.js} +13 -12
- package/dist/cli/{chunk-KZYLMMU5.js.map → chunk-VK5HG73G.js.map} +1 -1
- package/dist/cli/{chunk-4H3ZRJ2U.js → chunk-XCGGEJTI.js} +4 -3
- package/dist/cli/{chunk-4H3ZRJ2U.js.map → chunk-XCGGEJTI.js.map} +1 -1
- package/dist/cli/{chunk-SOZE7V7V.js → chunk-XJXDHAES.js} +3 -2
- package/dist/cli/{chunk-SOZE7V7V.js.map → chunk-XJXDHAES.js.map} +1 -1
- package/dist/cli/chunk-XPDVG52A.js +2648 -0
- package/dist/cli/chunk-XPDVG52A.js.map +1 -0
- package/dist/cli/{chunk-CRPQUBP6.js → chunk-XXC2BYTV.js} +2 -1
- package/dist/cli/{chunk-CRPQUBP6.js.map → chunk-XXC2BYTV.js.map} +1 -1
- package/dist/cli/{chunk-AT6GGIBV.js → chunk-YFGF5NKA.js} +17 -14
- package/dist/cli/{chunk-AT6GGIBV.js.map → chunk-YFGF5NKA.js.map} +1 -1
- package/dist/cli/{chunk-ORM6PK57.js → chunk-YQ6NTIIE.js} +2 -1
- package/dist/cli/{chunk-ORM6PK57.js.map → chunk-YQ6NTIIE.js.map} +1 -1
- package/dist/cli/{chunk-RAUPWSYA.js → chunk-YYQAUTTN.js} +3 -2
- package/dist/cli/{chunk-RAUPWSYA.js.map → chunk-YYQAUTTN.js.map} +1 -1
- package/dist/cli/chunk-ZZM6QJ4W.js +109 -0
- package/dist/cli/chunk-ZZM6QJ4W.js.map +1 -0
- package/dist/cli/{code-X3M6ENTQ.js → code-SMKEW6CD.js} +60 -56
- package/dist/cli/code-SMKEW6CD.js.map +1 -0
- package/dist/cli/{commands-QY7MSQG7.js → commands-FVVB5FZF.js} +7 -5
- package/dist/cli/{commands-QY7MSQG7.js.map → commands-FVVB5FZF.js.map} +1 -1
- package/dist/cli/{commit-BRCQ3OQO.js → commit-HE4VSPZ7.js} +7 -4
- package/dist/cli/{commit-BRCQ3OQO.js.map → commit-HE4VSPZ7.js.map} +1 -1
- package/dist/cli/{desktop-ZTMHQR2Y.js → desktop-Q7NDXCON.js} +162 -74
- package/dist/cli/desktop-Q7NDXCON.js.map +1 -0
- package/dist/cli/devtools-YECO25QO.js +3719 -0
- package/dist/cli/devtools-YECO25QO.js.map +1 -0
- package/dist/cli/diff-435UTPC5.js +165 -0
- package/dist/cli/{diff-YASCB7PU.js.map → diff-435UTPC5.js.map} +1 -1
- package/dist/cli/doctor-OT7KH75K.js +27 -0
- package/dist/cli/{events-2AJTXR7I.js → events-XEFAD5VX.js} +6 -4
- package/dist/cli/{events-2AJTXR7I.js.map → events-XEFAD5VX.js.map} +1 -1
- package/dist/cli/index.js +3209 -133
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{mcp-YMWBLRR7.js → mcp-WUL2WO75.js} +6 -4
- package/dist/cli/{mcp-YMWBLRR7.js.map → mcp-WUL2WO75.js.map} +1 -1
- package/dist/cli/{mcp-browse-XLDUE6SB.js → mcp-browse-RR7R4XET.js} +32 -21
- package/dist/cli/{mcp-browse-XLDUE6SB.js.map → mcp-browse-RR7R4XET.js.map} +1 -1
- package/dist/cli/{mcp-inspect-H4D2HSJP.js → mcp-inspect-REGLYBWT.js} +8 -5
- package/dist/cli/{mcp-inspect-H4D2HSJP.js.map → mcp-inspect-REGLYBWT.js.map} +1 -1
- package/dist/cli/package.json +3 -0
- package/dist/cli/prompt-UW6EFLVR.js +16 -0
- package/dist/cli/{prune-sessions-4N3BVST2.js → prune-sessions-3RWUBYRS.js} +4 -2
- package/dist/cli/{prune-sessions-4N3BVST2.js.map → prune-sessions-3RWUBYRS.js.map} +1 -1
- package/dist/cli/{replay-3GTWM75X.js → replay-YOURXV4C.js} +42 -30
- package/dist/cli/{replay-3GTWM75X.js.map → replay-YOURXV4C.js.map} +1 -1
- package/dist/cli/{run-BLZPTRDX.js → run-Q6BUXV66.js} +24 -21
- package/dist/cli/{run-BLZPTRDX.js.map → run-Q6BUXV66.js.map} +1 -1
- package/dist/cli/{server-DRFPXXSH.js → server-XGDBRWMB.js} +40 -43
- package/dist/cli/server-XGDBRWMB.js.map +1 -0
- package/dist/cli/{sessions-BOWFPTXT.js → sessions-FH7QVYSY.js} +22 -19
- package/dist/cli/{sessions-BOWFPTXT.js.map → sessions-FH7QVYSY.js.map} +1 -1
- package/dist/cli/setup-VDS6SVEP.js +618 -0
- package/dist/cli/setup-VDS6SVEP.js.map +1 -0
- package/dist/cli/stats-MQVI2XQH.js +14 -0
- package/dist/cli/update-6ITLPRDV.js +15 -0
- package/dist/cli/update-6ITLPRDV.js.map +1 -0
- package/dist/cli/version-DAHGZY5N.js +33 -0
- package/dist/cli/{version-XQXYSJ5L.js.map → version-DAHGZY5N.js.map} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +182 -97
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/acp-64VQZLDJ.js.map +0 -1
- package/dist/cli/chat-ZAGX52RV.js +0 -46
- package/dist/cli/chunk-4W2CICFQ.js.map +0 -1
- package/dist/cli/chunk-5X7LZJDE.js +0 -36
- package/dist/cli/chunk-5X7LZJDE.js.map +0 -1
- package/dist/cli/chunk-65Q5HQ26.js +0 -892
- package/dist/cli/chunk-65Q5HQ26.js.map +0 -1
- package/dist/cli/chunk-A3LL4XDV.js.map +0 -1
- package/dist/cli/chunk-A7VHMMDE.js.map +0 -1
- package/dist/cli/chunk-AFFZF3MW.js +0 -36
- package/dist/cli/chunk-AFFZF3MW.js.map +0 -1
- package/dist/cli/chunk-BOFL3T45.js.map +0 -1
- package/dist/cli/chunk-BYZGO3BX.js.map +0 -1
- package/dist/cli/chunk-CD4SCQL4.js.map +0 -1
- package/dist/cli/chunk-CPOV2O73.js +0 -39
- package/dist/cli/chunk-CPOV2O73.js.map +0 -1
- package/dist/cli/chunk-F2AV2QDK.js +0 -16514
- package/dist/cli/chunk-F2AV2QDK.js.map +0 -1
- package/dist/cli/chunk-H4OLWRSX.js.map +0 -1
- package/dist/cli/chunk-IEA6JOIP.js +0 -5430
- package/dist/cli/chunk-IEA6JOIP.js.map +0 -1
- package/dist/cli/chunk-L7W3HJZQ.js +0 -46
- package/dist/cli/chunk-LN27AKV3.js +0 -26
- package/dist/cli/chunk-LN27AKV3.js.map +0 -1
- package/dist/cli/chunk-MHGPBJ2T.js.map +0 -1
- package/dist/cli/chunk-SXLJBFIV.js.map +0 -1
- package/dist/cli/chunk-YJFKFTAL.js.map +0 -1
- package/dist/cli/code-X3M6ENTQ.js.map +0 -1
- package/dist/cli/desktop-ZTMHQR2Y.js.map +0 -1
- package/dist/cli/diff-YASCB7PU.js +0 -153
- package/dist/cli/doctor-XCN5ETVP.js +0 -24
- package/dist/cli/prompt-RSIHN62V.js +0 -14
- package/dist/cli/server-DRFPXXSH.js.map +0 -1
- package/dist/cli/setup-FQL2JJC2.js +0 -516
- package/dist/cli/setup-FQL2JJC2.js.map +0 -1
- package/dist/cli/stats-5RJCATCE.js +0 -12
- package/dist/cli/update-GUCWB4UN.js +0 -13
- package/dist/cli/version-XQXYSJ5L.js +0 -30
- /package/dist/cli/{chat-ZAGX52RV.js.map → chat-7ES4IBNH.js.map} +0 -0
- /package/dist/cli/{doctor-XCN5ETVP.js.map → chunk-TUK7OWJA.js.map} +0 -0
- /package/dist/cli/{prompt-RSIHN62V.js.map → doctor-OT7KH75K.js.map} +0 -0
- /package/dist/cli/{stats-5RJCATCE.js.map → prompt-UW6EFLVR.js.map} +0 -0
- /package/dist/cli/{update-GUCWB4UN.js.map → stats-MQVI2XQH.js.map} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
t
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-RE4RAVFF.js";
|
|
5
6
|
|
|
6
7
|
// src/hooks.ts
|
|
7
8
|
import { spawn } from "child_process";
|
|
@@ -205,4 +206,4 @@ export {
|
|
|
205
206
|
formatHookOutcomeMessage,
|
|
206
207
|
runHooks
|
|
207
208
|
};
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
209
|
+
//# sourceMappingURL=chunk-7O5ALB4C.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks.ts"],"sourcesContent":["/** Shell-command hooks; project scope first, then global. Exit 0=pass, 2=block on Pre*, other=warn. */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { t } from \"./i18n/index.js\";\n\nexport type HookEvent = \"PreToolUse\" | \"PostToolUse\" | \"UserPromptSubmit\" | \"Stop\";\n\n/** All four events as a const array — drives slash listing + validation. */\nexport const HOOK_EVENTS: readonly HookEvent[] = [\n \"PreToolUse\",\n \"PostToolUse\",\n \"UserPromptSubmit\",\n \"Stop\",\n] as const;\n\n/** Only the gating events can block the loop. */\nconst BLOCKING_EVENTS: ReadonlySet<HookEvent> = new Set([\"PreToolUse\", \"UserPromptSubmit\"]);\n\n/** Per-event default timeout. Tool/prompt hooks gate progress, so they're tight. */\nconst DEFAULT_TIMEOUTS_MS: Record<HookEvent, number> = {\n PreToolUse: 5_000,\n UserPromptSubmit: 5_000,\n PostToolUse: 30_000,\n Stop: 30_000,\n};\n\nexport type HookScope = \"project\" | \"global\";\n\nexport interface HookConfig {\n /** Anchored regex; `\"*\"` / omitted = every tool. Pre/PostToolUse only. */\n match?: string;\n /** Shell command to run. Spawned through the platform shell. */\n command: string;\n /** Optional human description — surfaced in `/hooks`. */\n description?: string;\n /** Per-hook timeout override in ms. */\n timeout?: number;\n /** Defaults: project scope → project root; global scope → process.cwd(). */\n cwd?: string;\n}\n\n/** Shape of `<scope>/.reasonix/settings.json` — only `hooks` for now. */\nexport interface HookSettings {\n hooks?: Partial<Record<HookEvent, HookConfig[]>>;\n}\n\n/** A loaded hook with its origin scope baked in (used for ordering and `/hooks`). */\nexport interface ResolvedHook extends HookConfig {\n event: HookEvent;\n scope: HookScope;\n /** Absolute path to the settings.json the hook came from. */\n source: string;\n}\n\n/** Outcome of a single hook invocation. */\nexport interface HookOutcome {\n /** Which hook fired. */\n hook: ResolvedHook;\n /** pass=exit 0; block=exit 2 on blocking event; warn=other non-zero; timeout=killed; error=spawn failed. */\n decision: \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\";\n exitCode: number | null;\n /** Captured stdout (trimmed). May be empty. */\n stdout: string;\n /** Captured stderr (trimmed). The block / warn message comes from here. */\n stderr: string;\n durationMs: number;\n /** Output crossed the per-stream byte cap; surfaced so user knows we kept less than the script wrote. */\n truncated?: boolean;\n}\n\n/** Aggregate report for `runHooks`. */\nexport interface HookReport {\n event: HookEvent;\n outcomes: HookOutcome[];\n /** True iff at least one outcome was a `block` — only meaningful for blocking events. */\n blocked: boolean;\n}\n\nexport const HOOK_SETTINGS_FILENAME = \"settings.json\";\nexport const HOOK_SETTINGS_DIRNAME = \".reasonix\";\n\n/** Where the global settings.json lives. Equivalent to `~/.reasonix/settings.json`. */\nexport function globalSettingsPath(homeDirOverride?: string): string {\n return join(homeDirOverride ?? homedir(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\n/** Where the project settings.json lives for a given root. */\nexport function projectSettingsPath(projectRoot: string): string {\n return join(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\nfunction readSettingsFile(path: string): HookSettings | null {\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") return parsed as HookSettings;\n } catch {\n /* malformed JSON → treat as no hooks; do NOT throw, the user\n * shouldn't lose the whole CLI to a typo in their settings */\n }\n return null;\n}\n\n/** Project hooks fire before global; within a scope, array order. */\nexport interface LoadHookSettingsOptions {\n /** Absolute project root, if any. Without it, only global hooks load. */\n projectRoot?: string;\n /** Override `~` for tests. */\n homeDir?: string;\n}\n\nexport function loadHooks(opts: LoadHookSettingsOptions = {}): ResolvedHook[] {\n const out: ResolvedHook[] = [];\n if (opts.projectRoot) {\n const projPath = projectSettingsPath(opts.projectRoot);\n const settings = readSettingsFile(projPath);\n if (settings) appendResolved(out, settings, \"project\", projPath);\n }\n const globalPath = globalSettingsPath(opts.homeDir);\n const settings = readSettingsFile(globalPath);\n if (settings) appendResolved(out, settings, \"global\", globalPath);\n return out;\n}\n\nfunction appendResolved(\n out: ResolvedHook[],\n settings: HookSettings,\n scope: HookScope,\n source: string,\n): void {\n if (!settings.hooks) return;\n for (const event of HOOK_EVENTS) {\n const list = settings.hooks[event];\n if (!Array.isArray(list)) continue;\n for (const cfg of list) {\n if (!cfg || typeof cfg.command !== \"string\" || cfg.command.trim() === \"\") continue;\n out.push({ ...cfg, event, scope, source });\n }\n }\n}\n\n/** Match field is an ANCHORED regex — `\"file\"` won't trigger on `read_file`; use `\".*file\"`. */\nexport function matchesTool(hook: ResolvedHook, toolName: string): boolean {\n if (hook.event !== \"PreToolUse\" && hook.event !== \"PostToolUse\") return true;\n const m = hook.match;\n if (!m || m === \"*\") return true;\n try {\n const re = new RegExp(`^(?:${m})$`);\n return re.test(toolName);\n } catch {\n /* malformed regex → don't fire (safer than firing on every tool) */\n return false;\n }\n}\n\n/** Payload envelope passed to hook stdin. */\nexport interface HookPayload {\n event: HookEvent;\n cwd: string;\n toolName?: string;\n toolArgs?: unknown;\n toolResult?: string;\n prompt?: string;\n lastAssistantText?: string;\n turn?: number;\n}\n\n/** Test seam — same shape as Node's spawn but returns a Promise of the raw outcome bits. */\nexport interface HookSpawnInput {\n command: string;\n cwd: string;\n stdin: string;\n timeoutMs: number;\n}\n\nexport interface HookSpawnResult {\n exitCode: number | null;\n stdout: string;\n stderr: string;\n timedOut: boolean;\n /** True iff spawn() itself failed (ENOENT, EACCES, …). */\n spawnError?: Error;\n /** Output capped at byte limit — hook ran to completion but consumers see clipped view. */\n truncated?: boolean;\n}\n\n/** Per-stream cap — bounds heap exposure to a runaway child between spawn and timeout. */\nconst HOOK_OUTPUT_CAP_BYTES = 256 * 1024;\n\nexport type HookSpawner = (input: HookSpawnInput) => Promise<HookSpawnResult>;\n\n/** `shell: true` — hook is a shell command by contract; pipes / `&&` / env expansion must work. */\nfunction defaultSpawner(input: HookSpawnInput): Promise<HookSpawnResult> {\n return new Promise<HookSpawnResult>((resolve) => {\n const child = spawn(input.command, {\n cwd: input.cwd,\n shell: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n // Collect raw bytes per stream and decode once at close so a\n // multi-byte UTF-8 sequence split across data chunks doesn't\n // corrupt — same approach shell.ts uses for run_command output.\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let truncated = false;\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n // SIGTERM may not land on Windows for shell children — followed\n // by a hard kill a moment later if the process is still around.\n setTimeout(() => {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* already gone */\n }\n }, 500);\n }, input.timeoutMs);\n\n const onChunk = (kind: \"stdout\" | \"stderr\", chunk: Buffer) => {\n const target = kind === \"stdout\" ? stdoutChunks : stderrChunks;\n const seen = kind === \"stdout\" ? stdoutBytes : stderrBytes;\n if (seen >= HOOK_OUTPUT_CAP_BYTES) {\n truncated = true;\n return;\n }\n const remaining = HOOK_OUTPUT_CAP_BYTES - seen;\n if (chunk.length > remaining) {\n target.push(chunk.subarray(0, remaining));\n if (kind === \"stdout\") stdoutBytes = HOOK_OUTPUT_CAP_BYTES;\n else stderrBytes = HOOK_OUTPUT_CAP_BYTES;\n truncated = true;\n } else {\n target.push(chunk);\n if (kind === \"stdout\") stdoutBytes += chunk.length;\n else stderrBytes += chunk.length;\n }\n };\n child.stdout.on(\"data\", (chunk: Buffer) => onChunk(\"stdout\", chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => onChunk(\"stderr\", chunk));\n child.once(\"error\", (err) => {\n clearTimeout(timer);\n resolve({\n exitCode: null,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut: false,\n spawnError: err,\n truncated: truncated || undefined,\n });\n });\n child.once(\"close\", (code) => {\n clearTimeout(timer);\n resolve({\n exitCode: code,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\").trim(),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\").trim(),\n timedOut,\n truncated: truncated || undefined,\n });\n });\n\n try {\n child.stdin.write(input.stdin);\n child.stdin.end();\n } catch {\n /* stdin write can race with spawn errors; the close handler\n * still fires with exit 0/null */\n }\n });\n}\n\nexport function formatHookOutcomeMessage(outcome: HookOutcome): string {\n if (outcome.decision === \"pass\") return \"\";\n const detail = (outcome.stderr || outcome.stdout || \"\").trim();\n const tag = `${outcome.hook.scope}/${outcome.hook.event}`;\n const cmd =\n outcome.hook.command.length > 60\n ? `${outcome.hook.command.slice(0, 60)}…`\n : outcome.hook.command;\n const truncTag = outcome.truncated ? t(\"hooks.truncated\") : \"\";\n const decision = t(`hooks.decision${capitalize(outcome.decision)}`);\n return detail\n ? t(\"hooks.headWithDetail\", { tag, cmd, decision, truncTag, detail })\n : t(\"hooks.head\", { tag, cmd, decision, truncTag });\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function decideOutcome(\n event: HookEvent,\n raw: HookSpawnResult,\n): \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\" {\n if (raw.spawnError) return \"error\";\n if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? \"block\" : \"warn\";\n if (raw.exitCode === 0) return \"pass\";\n if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return \"block\";\n return \"warn\";\n}\n\nexport interface RunHooksOptions {\n payload: HookPayload;\n hooks: ResolvedHook[];\n /** Test seam — defaults to a real `spawn`. */\n spawner?: HookSpawner;\n}\n\n/** Stops at first `block` so a gating hook can prevent later hooks running against a phantom success. */\nexport async function runHooks(opts: RunHooksOptions): Promise<HookReport> {\n const spawner = opts.spawner ?? defaultSpawner;\n const event = opts.payload.event;\n const toolName = opts.payload.toolName ?? \"\";\n const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));\n\n const outcomes: HookOutcome[] = [];\n let blocked = false;\n const stdin = `${JSON.stringify(opts.payload)}\\n`;\n\n for (const hook of matching) {\n const start = Date.now();\n const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];\n const cwd = hook.cwd ?? opts.payload.cwd;\n const raw = await spawner({ command: hook.command, cwd, stdin, timeoutMs });\n const decision = decideOutcome(event, raw);\n outcomes.push({\n hook,\n decision,\n exitCode: raw.exitCode,\n stdout: raw.stdout,\n stderr:\n raw.stderr ||\n (raw.spawnError ? raw.spawnError.message : \"\") ||\n (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : \"\"),\n durationMs: Date.now() - start,\n truncated: raw.truncated,\n });\n if (decision === \"block\") {\n blocked = true;\n break;\n }\n }\n\n return { event, outcomes, blocked };\n}\n"],"mappings":"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","names":["settings"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks.ts"],"sourcesContent":["/** Shell-command hooks; project scope first, then global. Exit 0=pass, 2=block on Pre*, other=warn. */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { t } from \"./i18n/index.js\";\n\nexport type HookEvent = \"PreToolUse\" | \"PostToolUse\" | \"UserPromptSubmit\" | \"Stop\";\n\n/** All four events as a const array — drives slash listing + validation. */\nexport const HOOK_EVENTS: readonly HookEvent[] = [\n \"PreToolUse\",\n \"PostToolUse\",\n \"UserPromptSubmit\",\n \"Stop\",\n] as const;\n\n/** Only the gating events can block the loop. */\nconst BLOCKING_EVENTS: ReadonlySet<HookEvent> = new Set([\"PreToolUse\", \"UserPromptSubmit\"]);\n\n/** Per-event default timeout. Tool/prompt hooks gate progress, so they're tight. */\nconst DEFAULT_TIMEOUTS_MS: Record<HookEvent, number> = {\n PreToolUse: 5_000,\n UserPromptSubmit: 5_000,\n PostToolUse: 30_000,\n Stop: 30_000,\n};\n\nexport type HookScope = \"project\" | \"global\";\n\nexport interface HookConfig {\n /** Anchored regex; `\"*\"` / omitted = every tool. Pre/PostToolUse only. */\n match?: string;\n /** Shell command to run. Spawned through the platform shell. */\n command: string;\n /** Optional human description — surfaced in `/hooks`. */\n description?: string;\n /** Per-hook timeout override in ms. */\n timeout?: number;\n /** Defaults: project scope → project root; global scope → process.cwd(). */\n cwd?: string;\n}\n\n/** Shape of `<scope>/.reasonix/settings.json` — only `hooks` for now. */\nexport interface HookSettings {\n hooks?: Partial<Record<HookEvent, HookConfig[]>>;\n}\n\n/** A loaded hook with its origin scope baked in (used for ordering and `/hooks`). */\nexport interface ResolvedHook extends HookConfig {\n event: HookEvent;\n scope: HookScope;\n /** Absolute path to the settings.json the hook came from. */\n source: string;\n}\n\n/** Outcome of a single hook invocation. */\nexport interface HookOutcome {\n /** Which hook fired. */\n hook: ResolvedHook;\n /** pass=exit 0; block=exit 2 on blocking event; warn=other non-zero; timeout=killed; error=spawn failed. */\n decision: \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\";\n exitCode: number | null;\n /** Captured stdout (trimmed). May be empty. */\n stdout: string;\n /** Captured stderr (trimmed). The block / warn message comes from here. */\n stderr: string;\n durationMs: number;\n /** Output crossed the per-stream byte cap; surfaced so user knows we kept less than the script wrote. */\n truncated?: boolean;\n}\n\n/** Aggregate report for `runHooks`. */\nexport interface HookReport {\n event: HookEvent;\n outcomes: HookOutcome[];\n /** True iff at least one outcome was a `block` — only meaningful for blocking events. */\n blocked: boolean;\n}\n\nexport const HOOK_SETTINGS_FILENAME = \"settings.json\";\nexport const HOOK_SETTINGS_DIRNAME = \".reasonix\";\n\n/** Where the global settings.json lives. Equivalent to `~/.reasonix/settings.json`. */\nexport function globalSettingsPath(homeDirOverride?: string): string {\n return join(homeDirOverride ?? homedir(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\n/** Where the project settings.json lives for a given root. */\nexport function projectSettingsPath(projectRoot: string): string {\n return join(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\nfunction readSettingsFile(path: string): HookSettings | null {\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") return parsed as HookSettings;\n } catch {\n /* malformed JSON → treat as no hooks; do NOT throw, the user\n * shouldn't lose the whole CLI to a typo in their settings */\n }\n return null;\n}\n\n/** Project hooks fire before global; within a scope, array order. */\nexport interface LoadHookSettingsOptions {\n /** Absolute project root, if any. Without it, only global hooks load. */\n projectRoot?: string;\n /** Override `~` for tests. */\n homeDir?: string;\n}\n\nexport function loadHooks(opts: LoadHookSettingsOptions = {}): ResolvedHook[] {\n const out: ResolvedHook[] = [];\n if (opts.projectRoot) {\n const projPath = projectSettingsPath(opts.projectRoot);\n const settings = readSettingsFile(projPath);\n if (settings) appendResolved(out, settings, \"project\", projPath);\n }\n const globalPath = globalSettingsPath(opts.homeDir);\n const settings = readSettingsFile(globalPath);\n if (settings) appendResolved(out, settings, \"global\", globalPath);\n return out;\n}\n\nfunction appendResolved(\n out: ResolvedHook[],\n settings: HookSettings,\n scope: HookScope,\n source: string,\n): void {\n if (!settings.hooks) return;\n for (const event of HOOK_EVENTS) {\n const list = settings.hooks[event];\n if (!Array.isArray(list)) continue;\n for (const cfg of list) {\n if (!cfg || typeof cfg.command !== \"string\" || cfg.command.trim() === \"\") continue;\n out.push({ ...cfg, event, scope, source });\n }\n }\n}\n\n/** Match field is an ANCHORED regex — `\"file\"` won't trigger on `read_file`; use `\".*file\"`. */\nexport function matchesTool(hook: ResolvedHook, toolName: string): boolean {\n if (hook.event !== \"PreToolUse\" && hook.event !== \"PostToolUse\") return true;\n const m = hook.match;\n if (!m || m === \"*\") return true;\n try {\n const re = new RegExp(`^(?:${m})$`);\n return re.test(toolName);\n } catch {\n /* malformed regex → don't fire (safer than firing on every tool) */\n return false;\n }\n}\n\n/** Payload envelope passed to hook stdin. */\nexport interface HookPayload {\n event: HookEvent;\n cwd: string;\n toolName?: string;\n toolArgs?: unknown;\n toolResult?: string;\n prompt?: string;\n lastAssistantText?: string;\n turn?: number;\n}\n\n/** Test seam — same shape as Node's spawn but returns a Promise of the raw outcome bits. */\nexport interface HookSpawnInput {\n command: string;\n cwd: string;\n stdin: string;\n timeoutMs: number;\n}\n\nexport interface HookSpawnResult {\n exitCode: number | null;\n stdout: string;\n stderr: string;\n timedOut: boolean;\n /** True iff spawn() itself failed (ENOENT, EACCES, …). */\n spawnError?: Error;\n /** Output capped at byte limit — hook ran to completion but consumers see clipped view. */\n truncated?: boolean;\n}\n\n/** Per-stream cap — bounds heap exposure to a runaway child between spawn and timeout. */\nconst HOOK_OUTPUT_CAP_BYTES = 256 * 1024;\n\nexport type HookSpawner = (input: HookSpawnInput) => Promise<HookSpawnResult>;\n\n/** `shell: true` — hook is a shell command by contract; pipes / `&&` / env expansion must work. */\nfunction defaultSpawner(input: HookSpawnInput): Promise<HookSpawnResult> {\n return new Promise<HookSpawnResult>((resolve) => {\n const child = spawn(input.command, {\n cwd: input.cwd,\n shell: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n // Collect raw bytes per stream and decode once at close so a\n // multi-byte UTF-8 sequence split across data chunks doesn't\n // corrupt — same approach shell.ts uses for run_command output.\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let truncated = false;\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n // SIGTERM may not land on Windows for shell children — followed\n // by a hard kill a moment later if the process is still around.\n setTimeout(() => {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* already gone */\n }\n }, 500);\n }, input.timeoutMs);\n\n const onChunk = (kind: \"stdout\" | \"stderr\", chunk: Buffer) => {\n const target = kind === \"stdout\" ? stdoutChunks : stderrChunks;\n const seen = kind === \"stdout\" ? stdoutBytes : stderrBytes;\n if (seen >= HOOK_OUTPUT_CAP_BYTES) {\n truncated = true;\n return;\n }\n const remaining = HOOK_OUTPUT_CAP_BYTES - seen;\n if (chunk.length > remaining) {\n target.push(chunk.subarray(0, remaining));\n if (kind === \"stdout\") stdoutBytes = HOOK_OUTPUT_CAP_BYTES;\n else stderrBytes = HOOK_OUTPUT_CAP_BYTES;\n truncated = true;\n } else {\n target.push(chunk);\n if (kind === \"stdout\") stdoutBytes += chunk.length;\n else stderrBytes += chunk.length;\n }\n };\n child.stdout.on(\"data\", (chunk: Buffer) => onChunk(\"stdout\", chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => onChunk(\"stderr\", chunk));\n child.once(\"error\", (err) => {\n clearTimeout(timer);\n resolve({\n exitCode: null,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut: false,\n spawnError: err,\n truncated: truncated || undefined,\n });\n });\n child.once(\"close\", (code) => {\n clearTimeout(timer);\n resolve({\n exitCode: code,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\").trim(),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\").trim(),\n timedOut,\n truncated: truncated || undefined,\n });\n });\n\n try {\n child.stdin.write(input.stdin);\n child.stdin.end();\n } catch {\n /* stdin write can race with spawn errors; the close handler\n * still fires with exit 0/null */\n }\n });\n}\n\nexport function formatHookOutcomeMessage(outcome: HookOutcome): string {\n if (outcome.decision === \"pass\") return \"\";\n const detail = (outcome.stderr || outcome.stdout || \"\").trim();\n const tag = `${outcome.hook.scope}/${outcome.hook.event}`;\n const cmd =\n outcome.hook.command.length > 60\n ? `${outcome.hook.command.slice(0, 60)}…`\n : outcome.hook.command;\n const truncTag = outcome.truncated ? t(\"hooks.truncated\") : \"\";\n const decision = t(`hooks.decision${capitalize(outcome.decision)}`);\n return detail\n ? t(\"hooks.headWithDetail\", { tag, cmd, decision, truncTag, detail })\n : t(\"hooks.head\", { tag, cmd, decision, truncTag });\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function decideOutcome(\n event: HookEvent,\n raw: HookSpawnResult,\n): \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\" {\n if (raw.spawnError) return \"error\";\n if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? \"block\" : \"warn\";\n if (raw.exitCode === 0) return \"pass\";\n if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return \"block\";\n return \"warn\";\n}\n\nexport interface RunHooksOptions {\n payload: HookPayload;\n hooks: ResolvedHook[];\n /** Test seam — defaults to a real `spawn`. */\n spawner?: HookSpawner;\n}\n\n/** Stops at first `block` so a gating hook can prevent later hooks running against a phantom success. */\nexport async function runHooks(opts: RunHooksOptions): Promise<HookReport> {\n const spawner = opts.spawner ?? defaultSpawner;\n const event = opts.payload.event;\n const toolName = opts.payload.toolName ?? \"\";\n const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));\n\n const outcomes: HookOutcome[] = [];\n let blocked = false;\n const stdin = `${JSON.stringify(opts.payload)}\\n`;\n\n for (const hook of matching) {\n const start = Date.now();\n const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];\n const cwd = hook.cwd ?? opts.payload.cwd;\n const raw = await spawner({ command: hook.command, cwd, stdin, timeoutMs });\n const decision = decideOutcome(event, raw);\n outcomes.push({\n hook,\n decision,\n exitCode: raw.exitCode,\n stdout: raw.stdout,\n stderr:\n raw.stderr ||\n (raw.spawnError ? raw.spawnError.message : \"\") ||\n (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : \"\"),\n durationMs: Date.now() - start,\n truncated: raw.truncated,\n });\n if (decision === \"block\") {\n blocked = true;\n break;\n }\n }\n\n return { event, outcomes, blocked };\n}\n"],"mappings":"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","names":["settings"]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
probeOllama
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XCGGEJTI.js";
|
|
5
6
|
|
|
6
7
|
// src/index/semantic/ollama-launcher.ts
|
|
7
8
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -97,4 +98,4 @@ export {
|
|
|
97
98
|
startOllamaDaemon,
|
|
98
99
|
pullOllamaModel
|
|
99
100
|
};
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
101
|
+
//# sourceMappingURL=chunk-DOYHN4KB.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/index/semantic/ollama-launcher.ts"],"sourcesContent":["/** Daemon spawn is detached + unref'd so it outlives the CLI; non-TTY shells error instead of prompting. */\n\nimport { spawn, spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { probeOllama } from \"./embedding.js\";\n\nexport interface OllamaStatus {\n /** `ollama` binary resolvable on PATH or at the Windows installer path. */\n binaryFound: boolean;\n /** HTTP daemon reachable at the configured base URL. */\n daemonRunning: boolean;\n /** True if `<model>` (or `<model>:latest`) appears in `ollama list`. */\n modelPulled: boolean;\n /** Model the caller asked about — echoed for log clarity. */\n modelName: string;\n /** Models the daemon reported, for diagnostics. Empty when daemon down. */\n installedModels: string[];\n}\n\n/** Falls back to the Windows installer path because PATH refresh is per-shell — daemon may be up while the dashboard process inherited a stale PATH. */\nexport function findOllamaBinary(): string | null {\n const cmd = process.platform === \"win32\" ? \"where\" : \"which\";\n const out = spawnSync(cmd, [\"ollama\"], { encoding: \"utf8\" });\n if (out.status === 0) {\n const first = out.stdout.split(/\\r?\\n/).find((l) => l.trim().length > 0);\n if (first) return first.trim();\n }\n if (process.platform === \"win32\") {\n const local = process.env.LOCALAPPDATA;\n if (local) {\n const candidate = join(local, \"Programs\", \"Ollama\", \"ollama.exe\");\n if (existsSync(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/** Treats `<model>` and `<model>:latest` as the same — Ollama appends `:latest` to plain pulls. */\nexport async function checkOllamaStatus(\n modelName: string,\n baseUrl?: string,\n): Promise<OllamaStatus> {\n const binary = findOllamaBinary();\n const probe = await probeOllama({ baseUrl });\n const installedModels = probe.ok ? probe.models : [];\n const wanted = modelName.includes(\":\") ? modelName : `${modelName}:latest`;\n const modelPulled = installedModels.some((m) => m === modelName || m === wanted);\n return {\n binaryFound: binary !== null,\n daemonRunning: probe.ok,\n modelPulled,\n modelName,\n installedModels,\n };\n}\n\n/** Detached + unref'd so daemon survives the CLI; output discarded so no ghost cmd window on Windows. */\nexport async function startOllamaDaemon(\n opts: { baseUrl?: string; timeoutMs?: number; signal?: AbortSignal } = {},\n): Promise<{ ready: boolean; pid: number | null }> {\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const child = spawn(findOllamaBinary() ?? \"ollama\", [\"serve\"], {\n detached: true,\n stdio: \"ignore\",\n windowsHide: true,\n });\n child.unref();\n\n const pid = child.pid ?? null;\n const start = Date.now();\n while (Date.now() - start < timeoutMs) {\n if (opts.signal?.aborted) return { ready: false, pid };\n const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });\n if (probe.ok) return { ready: true, pid };\n await sleep(500);\n }\n return { ready: false, pid };\n}\n\n/** `onLine` called per line so the CLI can render its own bar instead of ollama's TTY output. */\nexport async function pullOllamaModel(\n modelName: string,\n opts: { onLine?: (line: string, stream: \"stdout\" | \"stderr\") => void; signal?: AbortSignal } = {},\n): Promise<number> {\n return new Promise<number>((resolve) => {\n const child = spawn(findOllamaBinary() ?? \"ollama\", [\"pull\", modelName], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n if (opts.signal) {\n const onAbort = () => child.kill();\n opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n child.once(\"exit\", () => opts.signal?.removeEventListener(\"abort\", onAbort));\n }\n streamLines(child.stdout, (l) => opts.onLine?.(l, \"stdout\"));\n streamLines(child.stderr, (l) => opts.onLine?.(l, \"stderr\"));\n child.once(\"exit\", (code) => resolve(code ?? -1));\n child.once(\"error\", () => resolve(-1));\n });\n}\n\nfunction streamLines(stream: NodeJS.ReadableStream | null, cb: (line: string) => void): void {\n if (!stream) return;\n let buf = \"\";\n stream.setEncoding(\"utf8\");\n stream.on(\"data\", (chunk: string) => {\n buf += chunk;\n let nl = buf.indexOf(\"\\n\");\n while (nl !== -1) {\n const line = buf.slice(0, nl).replace(/\\r$/, \"\");\n buf = buf.slice(nl + 1);\n if (line.length > 0) cb(line);\n nl = buf.indexOf(\"\\n\");\n }\n });\n stream.on(\"end\", () => {\n if (buf.length > 0) cb(buf.replace(/\\r$/, \"\"));\n });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/index/semantic/ollama-launcher.ts"],"sourcesContent":["/** Daemon spawn is detached + unref'd so it outlives the CLI; non-TTY shells error instead of prompting. */\n\nimport { spawn, spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { probeOllama } from \"./embedding.js\";\n\nexport interface OllamaStatus {\n /** `ollama` binary resolvable on PATH or at the Windows installer path. */\n binaryFound: boolean;\n /** HTTP daemon reachable at the configured base URL. */\n daemonRunning: boolean;\n /** True if `<model>` (or `<model>:latest`) appears in `ollama list`. */\n modelPulled: boolean;\n /** Model the caller asked about — echoed for log clarity. */\n modelName: string;\n /** Models the daemon reported, for diagnostics. Empty when daemon down. */\n installedModels: string[];\n}\n\n/** Falls back to the Windows installer path because PATH refresh is per-shell — daemon may be up while the dashboard process inherited a stale PATH. */\nexport function findOllamaBinary(): string | null {\n const cmd = process.platform === \"win32\" ? \"where\" : \"which\";\n const out = spawnSync(cmd, [\"ollama\"], { encoding: \"utf8\" });\n if (out.status === 0) {\n const first = out.stdout.split(/\\r?\\n/).find((l) => l.trim().length > 0);\n if (first) return first.trim();\n }\n if (process.platform === \"win32\") {\n const local = process.env.LOCALAPPDATA;\n if (local) {\n const candidate = join(local, \"Programs\", \"Ollama\", \"ollama.exe\");\n if (existsSync(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/** Treats `<model>` and `<model>:latest` as the same — Ollama appends `:latest` to plain pulls. */\nexport async function checkOllamaStatus(\n modelName: string,\n baseUrl?: string,\n): Promise<OllamaStatus> {\n const binary = findOllamaBinary();\n const probe = await probeOllama({ baseUrl });\n const installedModels = probe.ok ? probe.models : [];\n const wanted = modelName.includes(\":\") ? modelName : `${modelName}:latest`;\n const modelPulled = installedModels.some((m) => m === modelName || m === wanted);\n return {\n binaryFound: binary !== null,\n daemonRunning: probe.ok,\n modelPulled,\n modelName,\n installedModels,\n };\n}\n\n/** Detached + unref'd so daemon survives the CLI; output discarded so no ghost cmd window on Windows. */\nexport async function startOllamaDaemon(\n opts: { baseUrl?: string; timeoutMs?: number; signal?: AbortSignal } = {},\n): Promise<{ ready: boolean; pid: number | null }> {\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const child = spawn(findOllamaBinary() ?? \"ollama\", [\"serve\"], {\n detached: true,\n stdio: \"ignore\",\n windowsHide: true,\n });\n child.unref();\n\n const pid = child.pid ?? null;\n const start = Date.now();\n while (Date.now() - start < timeoutMs) {\n if (opts.signal?.aborted) return { ready: false, pid };\n const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });\n if (probe.ok) return { ready: true, pid };\n await sleep(500);\n }\n return { ready: false, pid };\n}\n\n/** `onLine` called per line so the CLI can render its own bar instead of ollama's TTY output. */\nexport async function pullOllamaModel(\n modelName: string,\n opts: { onLine?: (line: string, stream: \"stdout\" | \"stderr\") => void; signal?: AbortSignal } = {},\n): Promise<number> {\n return new Promise<number>((resolve) => {\n const child = spawn(findOllamaBinary() ?? \"ollama\", [\"pull\", modelName], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n if (opts.signal) {\n const onAbort = () => child.kill();\n opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n child.once(\"exit\", () => opts.signal?.removeEventListener(\"abort\", onAbort));\n }\n streamLines(child.stdout, (l) => opts.onLine?.(l, \"stdout\"));\n streamLines(child.stderr, (l) => opts.onLine?.(l, \"stderr\"));\n child.once(\"exit\", (code) => resolve(code ?? -1));\n child.once(\"error\", () => resolve(-1));\n });\n}\n\nfunction streamLines(stream: NodeJS.ReadableStream | null, cb: (line: string) => void): void {\n if (!stream) return;\n let buf = \"\";\n stream.setEncoding(\"utf8\");\n stream.on(\"data\", (chunk: string) => {\n buf += chunk;\n let nl = buf.indexOf(\"\\n\");\n while (nl !== -1) {\n const line = buf.slice(0, nl).replace(/\\r$/, \"\");\n buf = buf.slice(nl + 1);\n if (line.length > 0) cb(line);\n nl = buf.indexOf(\"\\n\");\n }\n });\n stream.on(\"end\", () => {\n if (buf.length > 0) cb(buf.replace(/\\r$/, \"\"));\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
computeReplayStats
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-FHOGSSCH.js";
|
|
5
6
|
|
|
6
7
|
// src/transcript/diff.ts
|
|
7
8
|
function findNextDivergence(pairs, fromIdx) {
|
|
@@ -304,4 +305,4 @@ export {
|
|
|
304
305
|
renderSummaryTable,
|
|
305
306
|
renderMarkdown
|
|
306
307
|
};
|
|
307
|
-
//# sourceMappingURL=chunk-
|
|
308
|
+
//# sourceMappingURL=chunk-F3PXYSNN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/transcript/diff.ts"],"sourcesContent":["/** Transcript diff — pairs assistant_final by turn number; unmatched extras become only_in_a / only_in_b. */\n\nimport type { ReadTranscriptResult, TranscriptRecord } from \"./log.js\";\nimport { type ReplayStats, computeReplayStats } from \"./replay.js\";\n\nexport interface DiffSide {\n label: string;\n meta: ReadTranscriptResult[\"meta\"];\n records: TranscriptRecord[];\n stats: ReplayStats;\n}\n\nexport interface TurnPair {\n turn: number;\n aAssistant?: TranscriptRecord;\n bAssistant?: TranscriptRecord;\n aTools: TranscriptRecord[];\n bTools: TranscriptRecord[];\n kind: \"match\" | \"diverge\" | \"only_in_a\" | \"only_in_b\";\n /** When kind === \"diverge\", a short one-liner pointing at what differs. */\n divergenceNote?: string;\n}\n\nexport interface DiffReport {\n a: DiffSide;\n b: DiffSide;\n pairs: TurnPair[];\n firstDivergenceTurn: number | null;\n}\n\nexport function findNextDivergence(pairs: TurnPair[], fromIdx: number): number {\n for (let i = fromIdx + 1; i < pairs.length; i++) {\n if (pairs[i]!.kind !== \"match\") return i;\n }\n return -1;\n}\n\nexport function findPrevDivergence(pairs: TurnPair[], fromIdx: number): number {\n const start = Math.min(fromIdx - 1, pairs.length - 1);\n for (let i = start; i >= 0; i--) {\n if (pairs[i]!.kind !== \"match\") return i;\n }\n return -1;\n}\n\nexport function diffTranscripts(\n a: { label: string; parsed: ReadTranscriptResult },\n b: { label: string; parsed: ReadTranscriptResult },\n): DiffReport {\n const aSide: DiffSide = {\n label: a.label,\n meta: a.parsed.meta,\n records: a.parsed.records,\n stats: computeReplayStats(a.parsed.records),\n };\n const bSide: DiffSide = {\n label: b.label,\n meta: b.parsed.meta,\n records: b.parsed.records,\n stats: computeReplayStats(b.parsed.records),\n };\n\n const aByTurn = groupByTurn(a.parsed.records);\n const bByTurn = groupByTurn(b.parsed.records);\n const turns = [...new Set([...aByTurn.keys(), ...bByTurn.keys()])].sort((x, y) => x - y);\n\n const pairs: TurnPair[] = [];\n let firstDivergenceTurn: number | null = null;\n for (const turn of turns) {\n const aGroup = aByTurn.get(turn) ?? { assistant: undefined, tools: [] };\n const bGroup = bByTurn.get(turn) ?? { assistant: undefined, tools: [] };\n const aAssistant = aGroup.assistant;\n const bAssistant = bGroup.assistant;\n const aTools = aGroup.tools;\n const bTools = bGroup.tools;\n\n let kind: TurnPair[\"kind\"];\n let divergenceNote: string | undefined;\n if (!aAssistant && bAssistant) kind = \"only_in_b\";\n else if (aAssistant && !bAssistant) kind = \"only_in_a\";\n else if (!aAssistant && !bAssistant)\n kind = \"diverge\"; // tool-only turn (rare)\n else {\n divergenceNote = classifyDivergence(aAssistant!, bAssistant!, aTools, bTools);\n kind = divergenceNote ? \"diverge\" : \"match\";\n }\n\n if (kind !== \"match\" && firstDivergenceTurn === null) firstDivergenceTurn = turn;\n pairs.push({ turn, aAssistant, bAssistant, aTools, bTools, kind, divergenceNote });\n }\n\n return { a: aSide, b: bSide, pairs, firstDivergenceTurn };\n}\n\nfunction classifyDivergence(\n a: TranscriptRecord,\n b: TranscriptRecord,\n aTools: TranscriptRecord[],\n bTools: TranscriptRecord[],\n): string | undefined {\n const aNames = aTools.map((t) => t.tool ?? \"\").sort();\n const bNames = bTools.map((t) => t.tool ?? \"\").sort();\n if (aNames.join(\",\") !== bNames.join(\",\")) {\n return `tool calls differ: A=[${aNames.join(\",\") || \"—\"}] B=[${bNames.join(\",\") || \"—\"}]`;\n }\n // Same tool names — did they pass different args?\n for (let i = 0; i < aTools.length; i++) {\n const at = aTools[i]!;\n const bt = bTools[i]!;\n if (at.tool !== bt.tool) continue;\n if ((at.args ?? \"\") !== (bt.args ?? \"\")) {\n return `\"${at.tool}\" args differ`;\n }\n }\n const simRatio = similarity(a.content, b.content);\n if (simRatio < 0.75) return `text similarity ${(simRatio * 100).toFixed(0)}%`;\n return undefined;\n}\n\n/** Falls back to token-overlap above 2000 chars to keep diff fast on chatty transcripts. */\nexport function similarity(a: string, b: string): number {\n if (a === b) return 1;\n if (!a && !b) return 1;\n if (!a || !b) return 0;\n const maxLen = Math.max(a.length, b.length);\n if (maxLen > 2000) return tokenOverlap(a, b);\n const dist = levenshtein(a, b);\n return 1 - dist / maxLen;\n}\n\nfunction tokenOverlap(a: string, b: string): number {\n const ta = new Set(a.toLowerCase().split(/\\s+/).filter(Boolean));\n const tb = new Set(b.toLowerCase().split(/\\s+/).filter(Boolean));\n if (ta.size === 0 && tb.size === 0) return 1;\n let shared = 0;\n for (const t of ta) if (tb.has(t)) shared++;\n return (2 * shared) / (ta.size + tb.size);\n}\n\nfunction levenshtein(a: string, b: string): number {\n const m = a.length;\n const n = b.length;\n if (m === 0) return n;\n if (n === 0) return m;\n let prev = new Array(n + 1);\n let curr = new Array(n + 1);\n for (let j = 0; j <= n; j++) prev[j] = j;\n for (let i = 1; i <= m; i++) {\n curr[0] = i;\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);\n }\n [prev, curr] = [curr, prev];\n }\n return prev[n];\n}\n\ninterface TurnGroup {\n assistant?: TranscriptRecord;\n tools: TranscriptRecord[];\n}\n\nfunction groupByTurn(records: TranscriptRecord[]): Map<number, TurnGroup> {\n const out = new Map<number, TurnGroup>();\n for (const rec of records) {\n if (rec.role === \"user\") continue; // user msg is input to the turn, not its output\n const g = out.get(rec.turn) ?? { tools: [] };\n if (rec.role === \"assistant_final\") g.assistant = rec;\n else if (rec.role === \"tool\") g.tools.push(rec);\n out.set(rec.turn, g);\n }\n return out;\n}\n\nexport interface RenderOptions {\n /** Monochrome output (for file redirection or piping). Defaults to true. */\n monochrome?: boolean;\n}\n\nexport function renderSummaryTable(report: DiffReport, _opts: RenderOptions = {}): string {\n const a = report.a;\n const b = report.b;\n const lines: string[] = [];\n lines.push(\"Comparing:\");\n lines.push(` A ${a.label}`);\n lines.push(` B ${b.label}`);\n lines.push(\"\");\n lines.push(row([\"\", \"A\", \"B\", \"Δ\"], [20, 14, 14, 14]));\n lines.push(\n row([\"─\".repeat(20), \"─\".repeat(14), \"─\".repeat(14), \"─\".repeat(14)], [20, 14, 14, 14]),\n );\n lines.push(statRow(\"model calls\", a.stats.turns, b.stats.turns));\n lines.push(statRow(\"user turns\", a.stats.userTurns, b.stats.userTurns));\n lines.push(statRow(\"tool calls\", a.stats.toolCalls, b.stats.toolCalls));\n lines.push(\n row(\n [\n \"cache hit\",\n `${pct(a.stats.cacheHitRatio)}`,\n `${pct(b.stats.cacheHitRatio)}`,\n signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio),\n ],\n [20, 14, 14, 14],\n ),\n );\n lines.push(\n row(\n [\n \"cost (USD)\",\n `$${a.stats.totalCostUsd.toFixed(6)}`,\n `$${b.stats.totalCostUsd.toFixed(6)}`,\n costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd),\n ],\n [20, 14, 14, 14],\n ),\n );\n lines.push(statRow(\"prefix hashes\", a.stats.prefixHashes.length, b.stats.prefixHashes.length));\n lines.push(\"\");\n\n // Prefix stability story — the headline finding when comparing bench modes.\n const aPrefixStable = a.stats.prefixHashes.length <= 1;\n const bPrefixStable = b.stats.prefixHashes.length <= 1;\n if (aPrefixStable !== bPrefixStable) {\n const stable = aPrefixStable ? \"A\" : \"B\";\n const churn = aPrefixStable ? \"B\" : \"A\";\n const churnCount = aPrefixStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;\n lines.push(\n `prefix stability: ${stable} stayed byte-stable across ${Math.max(\n a.stats.turns,\n b.stats.turns,\n )} turns; ${churn} churned ${churnCount} distinct prefixes.`,\n );\n lines.push(\"\");\n } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {\n lines.push(\n `prefix: A and B share the same prefix hash (${a.stats.prefixHashes[0].slice(0, 12)}…) — cache delta is attributable to log stability, not prompt change.`,\n );\n lines.push(\"\");\n }\n\n if (report.firstDivergenceTurn !== null) {\n const p = report.pairs.find((p) => p.turn === report.firstDivergenceTurn);\n lines.push(\n `first divergence: turn ${report.firstDivergenceTurn} — ${p?.divergenceNote ?? \"?\"}`,\n );\n if (p?.aAssistant) lines.push(` A → ${truncate(p.aAssistant.content, 100)}`);\n if (p?.bAssistant) lines.push(` B → ${truncate(p.bAssistant.content, 100)}`);\n } else {\n lines.push(\"no material divergence detected (texts within similarity threshold).\");\n }\n\n return lines.join(\"\\n\");\n}\n\nexport function renderMarkdown(report: DiffReport): string {\n const a = report.a;\n const b = report.b;\n const out: string[] = [];\n out.push(`# Transcript diff: ${a.label} vs ${b.label}`);\n out.push(\"\");\n if (a.meta || b.meta) {\n out.push(\"## Meta\");\n out.push(\"\");\n out.push(`| | ${a.label} | ${b.label} |`);\n out.push(\"|---|---|---|\");\n out.push(`| source | ${a.meta?.source ?? \"—\"} | ${b.meta?.source ?? \"—\"} |`);\n out.push(`| model | ${a.meta?.model ?? \"—\"} | ${b.meta?.model ?? \"—\"} |`);\n out.push(`| task | ${a.meta?.task ?? \"—\"} | ${b.meta?.task ?? \"—\"} |`);\n out.push(`| startedAt | ${a.meta?.startedAt ?? \"—\"} | ${b.meta?.startedAt ?? \"—\"} |`);\n out.push(\"\");\n }\n\n out.push(\"## Summary\");\n out.push(\"\");\n out.push(`| metric | ${a.label} | ${b.label} | delta |`);\n out.push(\"|---|---:|---:|---:|\");\n out.push(\n `| model calls | ${a.stats.turns} | ${b.stats.turns} | ${signed(b.stats.turns - a.stats.turns)} |`,\n );\n out.push(\n `| user turns | ${a.stats.userTurns} | ${b.stats.userTurns} | ${signed(b.stats.userTurns - a.stats.userTurns)} |`,\n );\n out.push(\n `| tool calls | ${a.stats.toolCalls} | ${b.stats.toolCalls} | ${signed(b.stats.toolCalls - a.stats.toolCalls)} |`,\n );\n out.push(\n `| cache hit | ${pct(a.stats.cacheHitRatio)} | ${pct(b.stats.cacheHitRatio)} | **${signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)}** |`,\n );\n out.push(\n `| cost (USD) | $${a.stats.totalCostUsd.toFixed(6)} | $${b.stats.totalCostUsd.toFixed(6)} | ${costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)} |`,\n );\n out.push(\n `| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | — |`,\n );\n out.push(\"\");\n\n out.push(\"## Turn-by-turn\");\n out.push(\"\");\n out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);\n out.push(\"|---:|:---:|---|---|---|\");\n for (const p of report.pairs) {\n const aTools =\n p.aTools\n .map((t) => t.tool)\n .filter(Boolean)\n .join(\", \") || \"—\";\n const bTools =\n p.bTools\n .map((t) => t.tool)\n .filter(Boolean)\n .join(\", \") || \"—\";\n out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? \"\"} |`);\n }\n out.push(\"\");\n\n if (report.firstDivergenceTurn !== null) {\n const p = report.pairs.find((x) => x.turn === report.firstDivergenceTurn);\n out.push(`## First divergence (turn ${report.firstDivergenceTurn})`);\n out.push(\"\");\n out.push(p?.divergenceNote ?? \"\");\n out.push(\"\");\n if (p?.aAssistant) {\n out.push(`**${a.label}:**`);\n out.push(\"\");\n out.push(\"```\");\n out.push(p.aAssistant.content);\n out.push(\"```\");\n out.push(\"\");\n }\n if (p?.bAssistant) {\n out.push(`**${b.label}:**`);\n out.push(\"\");\n out.push(\"```\");\n out.push(p.bAssistant.content);\n out.push(\"```\");\n out.push(\"\");\n }\n }\n return out.join(\"\\n\");\n}\n\nfunction row(cols: string[], widths: number[]): string {\n return cols.map((c, i) => padRight(c, widths[i] ?? c.length)).join(\" \");\n}\n\nfunction statRow(label: string, av: number, bv: number): string {\n return row([label, `${av}`, `${bv}`, signed(bv - av)], [20, 14, 14, 14]);\n}\n\nfunction padRight(s: string, w: number): string {\n return s.length >= w ? s : s + \" \".repeat(w - s.length);\n}\n\nfunction signed(n: number): string {\n if (n === 0) return \"0\";\n return `${n > 0 ? \"+\" : \"\"}${n}`;\n}\n\nfunction signPct(diff: number): string {\n if (diff === 0) return \"0pp\";\n const s = (diff * 100).toFixed(1);\n return `${diff > 0 ? \"+\" : \"\"}${s}pp`;\n}\n\nfunction pct(x: number): string {\n return `${(x * 100).toFixed(1)}%`;\n}\n\nfunction costDelta(a: number, b: number): string {\n if (a === 0 && b === 0) return \"—\";\n if (a === 0) return \"new\";\n const pctChange = ((b - a) / a) * 100;\n return `${pctChange > 0 ? \"+\" : \"\"}${pctChange.toFixed(1)}%`;\n}\n\nfunction truncate(s: string, n: number): string {\n return s.length > n ? `${s.slice(0, n)}…` : s;\n}\n"],"mappings":"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","names":["p"]}
|
|
1
|
+
{"version":3,"sources":["../../src/transcript/diff.ts"],"sourcesContent":["/** Transcript diff — pairs assistant_final by turn number; unmatched extras become only_in_a / only_in_b. */\n\nimport type { ReadTranscriptResult, TranscriptRecord } from \"./log.js\";\nimport { type ReplayStats, computeReplayStats } from \"./replay.js\";\n\nexport interface DiffSide {\n label: string;\n meta: ReadTranscriptResult[\"meta\"];\n records: TranscriptRecord[];\n stats: ReplayStats;\n}\n\nexport interface TurnPair {\n turn: number;\n aAssistant?: TranscriptRecord;\n bAssistant?: TranscriptRecord;\n aTools: TranscriptRecord[];\n bTools: TranscriptRecord[];\n kind: \"match\" | \"diverge\" | \"only_in_a\" | \"only_in_b\";\n /** When kind === \"diverge\", a short one-liner pointing at what differs. */\n divergenceNote?: string;\n}\n\nexport interface DiffReport {\n a: DiffSide;\n b: DiffSide;\n pairs: TurnPair[];\n firstDivergenceTurn: number | null;\n}\n\nexport function findNextDivergence(pairs: TurnPair[], fromIdx: number): number {\n for (let i = fromIdx + 1; i < pairs.length; i++) {\n if (pairs[i]!.kind !== \"match\") return i;\n }\n return -1;\n}\n\nexport function findPrevDivergence(pairs: TurnPair[], fromIdx: number): number {\n const start = Math.min(fromIdx - 1, pairs.length - 1);\n for (let i = start; i >= 0; i--) {\n if (pairs[i]!.kind !== \"match\") return i;\n }\n return -1;\n}\n\nexport function diffTranscripts(\n a: { label: string; parsed: ReadTranscriptResult },\n b: { label: string; parsed: ReadTranscriptResult },\n): DiffReport {\n const aSide: DiffSide = {\n label: a.label,\n meta: a.parsed.meta,\n records: a.parsed.records,\n stats: computeReplayStats(a.parsed.records),\n };\n const bSide: DiffSide = {\n label: b.label,\n meta: b.parsed.meta,\n records: b.parsed.records,\n stats: computeReplayStats(b.parsed.records),\n };\n\n const aByTurn = groupByTurn(a.parsed.records);\n const bByTurn = groupByTurn(b.parsed.records);\n const turns = [...new Set([...aByTurn.keys(), ...bByTurn.keys()])].sort((x, y) => x - y);\n\n const pairs: TurnPair[] = [];\n let firstDivergenceTurn: number | null = null;\n for (const turn of turns) {\n const aGroup = aByTurn.get(turn) ?? { assistant: undefined, tools: [] };\n const bGroup = bByTurn.get(turn) ?? { assistant: undefined, tools: [] };\n const aAssistant = aGroup.assistant;\n const bAssistant = bGroup.assistant;\n const aTools = aGroup.tools;\n const bTools = bGroup.tools;\n\n let kind: TurnPair[\"kind\"];\n let divergenceNote: string | undefined;\n if (!aAssistant && bAssistant) kind = \"only_in_b\";\n else if (aAssistant && !bAssistant) kind = \"only_in_a\";\n else if (!aAssistant && !bAssistant)\n kind = \"diverge\"; // tool-only turn (rare)\n else {\n divergenceNote = classifyDivergence(aAssistant!, bAssistant!, aTools, bTools);\n kind = divergenceNote ? \"diverge\" : \"match\";\n }\n\n if (kind !== \"match\" && firstDivergenceTurn === null) firstDivergenceTurn = turn;\n pairs.push({ turn, aAssistant, bAssistant, aTools, bTools, kind, divergenceNote });\n }\n\n return { a: aSide, b: bSide, pairs, firstDivergenceTurn };\n}\n\nfunction classifyDivergence(\n a: TranscriptRecord,\n b: TranscriptRecord,\n aTools: TranscriptRecord[],\n bTools: TranscriptRecord[],\n): string | undefined {\n const aNames = aTools.map((t) => t.tool ?? \"\").sort();\n const bNames = bTools.map((t) => t.tool ?? \"\").sort();\n if (aNames.join(\",\") !== bNames.join(\",\")) {\n return `tool calls differ: A=[${aNames.join(\",\") || \"—\"}] B=[${bNames.join(\",\") || \"—\"}]`;\n }\n // Same tool names — did they pass different args?\n for (let i = 0; i < aTools.length; i++) {\n const at = aTools[i]!;\n const bt = bTools[i]!;\n if (at.tool !== bt.tool) continue;\n if ((at.args ?? \"\") !== (bt.args ?? \"\")) {\n return `\"${at.tool}\" args differ`;\n }\n }\n const simRatio = similarity(a.content, b.content);\n if (simRatio < 0.75) return `text similarity ${(simRatio * 100).toFixed(0)}%`;\n return undefined;\n}\n\n/** Falls back to token-overlap above 2000 chars to keep diff fast on chatty transcripts. */\nexport function similarity(a: string, b: string): number {\n if (a === b) return 1;\n if (!a && !b) return 1;\n if (!a || !b) return 0;\n const maxLen = Math.max(a.length, b.length);\n if (maxLen > 2000) return tokenOverlap(a, b);\n const dist = levenshtein(a, b);\n return 1 - dist / maxLen;\n}\n\nfunction tokenOverlap(a: string, b: string): number {\n const ta = new Set(a.toLowerCase().split(/\\s+/).filter(Boolean));\n const tb = new Set(b.toLowerCase().split(/\\s+/).filter(Boolean));\n if (ta.size === 0 && tb.size === 0) return 1;\n let shared = 0;\n for (const t of ta) if (tb.has(t)) shared++;\n return (2 * shared) / (ta.size + tb.size);\n}\n\nfunction levenshtein(a: string, b: string): number {\n const m = a.length;\n const n = b.length;\n if (m === 0) return n;\n if (n === 0) return m;\n let prev = new Array(n + 1);\n let curr = new Array(n + 1);\n for (let j = 0; j <= n; j++) prev[j] = j;\n for (let i = 1; i <= m; i++) {\n curr[0] = i;\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);\n }\n [prev, curr] = [curr, prev];\n }\n return prev[n];\n}\n\ninterface TurnGroup {\n assistant?: TranscriptRecord;\n tools: TranscriptRecord[];\n}\n\nfunction groupByTurn(records: TranscriptRecord[]): Map<number, TurnGroup> {\n const out = new Map<number, TurnGroup>();\n for (const rec of records) {\n if (rec.role === \"user\") continue; // user msg is input to the turn, not its output\n const g = out.get(rec.turn) ?? { tools: [] };\n if (rec.role === \"assistant_final\") g.assistant = rec;\n else if (rec.role === \"tool\") g.tools.push(rec);\n out.set(rec.turn, g);\n }\n return out;\n}\n\nexport interface RenderOptions {\n /** Monochrome output (for file redirection or piping). Defaults to true. */\n monochrome?: boolean;\n}\n\nexport function renderSummaryTable(report: DiffReport, _opts: RenderOptions = {}): string {\n const a = report.a;\n const b = report.b;\n const lines: string[] = [];\n lines.push(\"Comparing:\");\n lines.push(` A ${a.label}`);\n lines.push(` B ${b.label}`);\n lines.push(\"\");\n lines.push(row([\"\", \"A\", \"B\", \"Δ\"], [20, 14, 14, 14]));\n lines.push(\n row([\"─\".repeat(20), \"─\".repeat(14), \"─\".repeat(14), \"─\".repeat(14)], [20, 14, 14, 14]),\n );\n lines.push(statRow(\"model calls\", a.stats.turns, b.stats.turns));\n lines.push(statRow(\"user turns\", a.stats.userTurns, b.stats.userTurns));\n lines.push(statRow(\"tool calls\", a.stats.toolCalls, b.stats.toolCalls));\n lines.push(\n row(\n [\n \"cache hit\",\n `${pct(a.stats.cacheHitRatio)}`,\n `${pct(b.stats.cacheHitRatio)}`,\n signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio),\n ],\n [20, 14, 14, 14],\n ),\n );\n lines.push(\n row(\n [\n \"cost (USD)\",\n `$${a.stats.totalCostUsd.toFixed(6)}`,\n `$${b.stats.totalCostUsd.toFixed(6)}`,\n costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd),\n ],\n [20, 14, 14, 14],\n ),\n );\n lines.push(statRow(\"prefix hashes\", a.stats.prefixHashes.length, b.stats.prefixHashes.length));\n lines.push(\"\");\n\n // Prefix stability story — the headline finding when comparing bench modes.\n const aPrefixStable = a.stats.prefixHashes.length <= 1;\n const bPrefixStable = b.stats.prefixHashes.length <= 1;\n if (aPrefixStable !== bPrefixStable) {\n const stable = aPrefixStable ? \"A\" : \"B\";\n const churn = aPrefixStable ? \"B\" : \"A\";\n const churnCount = aPrefixStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;\n lines.push(\n `prefix stability: ${stable} stayed byte-stable across ${Math.max(\n a.stats.turns,\n b.stats.turns,\n )} turns; ${churn} churned ${churnCount} distinct prefixes.`,\n );\n lines.push(\"\");\n } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {\n lines.push(\n `prefix: A and B share the same prefix hash (${a.stats.prefixHashes[0].slice(0, 12)}…) — cache delta is attributable to log stability, not prompt change.`,\n );\n lines.push(\"\");\n }\n\n if (report.firstDivergenceTurn !== null) {\n const p = report.pairs.find((p) => p.turn === report.firstDivergenceTurn);\n lines.push(\n `first divergence: turn ${report.firstDivergenceTurn} — ${p?.divergenceNote ?? \"?\"}`,\n );\n if (p?.aAssistant) lines.push(` A → ${truncate(p.aAssistant.content, 100)}`);\n if (p?.bAssistant) lines.push(` B → ${truncate(p.bAssistant.content, 100)}`);\n } else {\n lines.push(\"no material divergence detected (texts within similarity threshold).\");\n }\n\n return lines.join(\"\\n\");\n}\n\nexport function renderMarkdown(report: DiffReport): string {\n const a = report.a;\n const b = report.b;\n const out: string[] = [];\n out.push(`# Transcript diff: ${a.label} vs ${b.label}`);\n out.push(\"\");\n if (a.meta || b.meta) {\n out.push(\"## Meta\");\n out.push(\"\");\n out.push(`| | ${a.label} | ${b.label} |`);\n out.push(\"|---|---|---|\");\n out.push(`| source | ${a.meta?.source ?? \"—\"} | ${b.meta?.source ?? \"—\"} |`);\n out.push(`| model | ${a.meta?.model ?? \"—\"} | ${b.meta?.model ?? \"—\"} |`);\n out.push(`| task | ${a.meta?.task ?? \"—\"} | ${b.meta?.task ?? \"—\"} |`);\n out.push(`| startedAt | ${a.meta?.startedAt ?? \"—\"} | ${b.meta?.startedAt ?? \"—\"} |`);\n out.push(\"\");\n }\n\n out.push(\"## Summary\");\n out.push(\"\");\n out.push(`| metric | ${a.label} | ${b.label} | delta |`);\n out.push(\"|---|---:|---:|---:|\");\n out.push(\n `| model calls | ${a.stats.turns} | ${b.stats.turns} | ${signed(b.stats.turns - a.stats.turns)} |`,\n );\n out.push(\n `| user turns | ${a.stats.userTurns} | ${b.stats.userTurns} | ${signed(b.stats.userTurns - a.stats.userTurns)} |`,\n );\n out.push(\n `| tool calls | ${a.stats.toolCalls} | ${b.stats.toolCalls} | ${signed(b.stats.toolCalls - a.stats.toolCalls)} |`,\n );\n out.push(\n `| cache hit | ${pct(a.stats.cacheHitRatio)} | ${pct(b.stats.cacheHitRatio)} | **${signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)}** |`,\n );\n out.push(\n `| cost (USD) | $${a.stats.totalCostUsd.toFixed(6)} | $${b.stats.totalCostUsd.toFixed(6)} | ${costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)} |`,\n );\n out.push(\n `| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | — |`,\n );\n out.push(\"\");\n\n out.push(\"## Turn-by-turn\");\n out.push(\"\");\n out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);\n out.push(\"|---:|:---:|---|---|---|\");\n for (const p of report.pairs) {\n const aTools =\n p.aTools\n .map((t) => t.tool)\n .filter(Boolean)\n .join(\", \") || \"—\";\n const bTools =\n p.bTools\n .map((t) => t.tool)\n .filter(Boolean)\n .join(\", \") || \"—\";\n out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? \"\"} |`);\n }\n out.push(\"\");\n\n if (report.firstDivergenceTurn !== null) {\n const p = report.pairs.find((x) => x.turn === report.firstDivergenceTurn);\n out.push(`## First divergence (turn ${report.firstDivergenceTurn})`);\n out.push(\"\");\n out.push(p?.divergenceNote ?? \"\");\n out.push(\"\");\n if (p?.aAssistant) {\n out.push(`**${a.label}:**`);\n out.push(\"\");\n out.push(\"```\");\n out.push(p.aAssistant.content);\n out.push(\"```\");\n out.push(\"\");\n }\n if (p?.bAssistant) {\n out.push(`**${b.label}:**`);\n out.push(\"\");\n out.push(\"```\");\n out.push(p.bAssistant.content);\n out.push(\"```\");\n out.push(\"\");\n }\n }\n return out.join(\"\\n\");\n}\n\nfunction row(cols: string[], widths: number[]): string {\n return cols.map((c, i) => padRight(c, widths[i] ?? c.length)).join(\" \");\n}\n\nfunction statRow(label: string, av: number, bv: number): string {\n return row([label, `${av}`, `${bv}`, signed(bv - av)], [20, 14, 14, 14]);\n}\n\nfunction padRight(s: string, w: number): string {\n return s.length >= w ? s : s + \" \".repeat(w - s.length);\n}\n\nfunction signed(n: number): string {\n if (n === 0) return \"0\";\n return `${n > 0 ? \"+\" : \"\"}${n}`;\n}\n\nfunction signPct(diff: number): string {\n if (diff === 0) return \"0pp\";\n const s = (diff * 100).toFixed(1);\n return `${diff > 0 ? \"+\" : \"\"}${s}pp`;\n}\n\nfunction pct(x: number): string {\n return `${(x * 100).toFixed(1)}%`;\n}\n\nfunction costDelta(a: number, b: number): string {\n if (a === 0 && b === 0) return \"—\";\n if (a === 0) return \"new\";\n const pctChange = ((b - a) / a) * 100;\n return `${pctChange > 0 ? \"+\" : \"\"}${pctChange.toFixed(1)}%`;\n}\n\nfunction truncate(s: string, n: number): string {\n return s.length > n ? `${s.slice(0, n)}…` : s;\n}\n"],"mappings":"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","names":["p"]}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
Usage
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2KDUS647.js";
|
|
5
6
|
import {
|
|
6
7
|
claudeEquivalentCost,
|
|
7
8
|
costUsd,
|
|
8
9
|
inputCostUsd,
|
|
9
10
|
outputCostUsd
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YQ6NTIIE.js";
|
|
11
12
|
|
|
12
13
|
// src/transcript/log.ts
|
|
13
14
|
import { createWriteStream, readFileSync } from "fs";
|
|
@@ -186,4 +187,4 @@ export {
|
|
|
186
187
|
replayFromFile,
|
|
187
188
|
computeReplayStats
|
|
188
189
|
};
|
|
189
|
-
//# sourceMappingURL=chunk-
|
|
190
|
+
//# sourceMappingURL=chunk-FHOGSSCH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/transcript/log.ts","../../src/transcript/replay.ts"],"sourcesContent":["/** Transcripts are receipts (cost/usage/prefix); sessions are memory (ChatMessages). Don't conflate. */\n\nimport { type WriteStream, createWriteStream, readFileSync } from \"node:fs\";\nimport type { LoopEvent } from \"../loop.js\";\nimport type { RawUsage } from \"../types.js\";\n\nexport interface TranscriptRecord {\n /** ISO-8601 timestamp at emit time. */\n ts: string;\n /** 1-based turn number within the session. */\n turn: number;\n /** LoopEvent role — \"assistant_delta\" | \"assistant_final\" | \"tool\" | \"done\" | ... */\n role: string;\n /** For assistant events, the final (or delta) text; for tool events, the tool result. */\n content: string;\n /** Tool name (role === \"tool\"). */\n tool?: string;\n /** JSON-string args the model sent for a tool call (role === \"tool\"). Persisted so diff can explain *why* two runs made different calls. */\n args?: string;\n /** DeepSeek token-usage snapshot (role === \"assistant_final\"). */\n usage?: RawUsage;\n /** USD cost of this turn (role === \"assistant_final\"). */\n cost?: number;\n /** Model id that produced this turn. */\n model?: string;\n /** Lets diff attribute cache-hit delta to log stability vs prompt change. */\n prefixHash?: string;\n /** Optional error message (role === \"error\"). */\n error?: string;\n}\n\nexport interface TranscriptMeta {\n version: 1;\n source: string; // e.g. \"reasonix chat\", \"bench/baseline\", \"bench/reasonix\"\n model?: string;\n task?: string;\n mode?: string;\n repeat?: number;\n startedAt: string;\n}\n\ninterface MetaLine {\n role: \"_meta\";\n meta: TranscriptMeta;\n}\n\nexport interface ReadTranscriptResult {\n meta: TranscriptMeta | null;\n records: TranscriptRecord[];\n}\n\nexport function recordFromLoopEvent(\n ev: LoopEvent,\n extra: { model: string; prefixHash: string },\n): TranscriptRecord {\n const rec: TranscriptRecord = {\n ts: new Date().toISOString(),\n turn: ev.turn,\n role: ev.role,\n content: ev.content,\n };\n if (ev.toolName !== undefined) rec.tool = ev.toolName;\n if (ev.toolArgs !== undefined) rec.args = ev.toolArgs;\n if (ev.error !== undefined) rec.error = ev.error;\n if (ev.stats) {\n rec.usage = {\n prompt_tokens: ev.stats.usage.promptTokens,\n completion_tokens: ev.stats.usage.completionTokens,\n total_tokens: ev.stats.usage.totalTokens,\n prompt_cache_hit_tokens: ev.stats.usage.promptCacheHitTokens,\n prompt_cache_miss_tokens: ev.stats.usage.promptCacheMissTokens,\n };\n rec.cost = ev.stats.cost;\n rec.model = ev.stats.model;\n rec.prefixHash = extra.prefixHash;\n } else if (ev.role === \"assistant_final\") {\n // assistant_final without stats (shouldn't happen in the live loop but\n // might in test fixtures) — still persist model + prefix for continuity.\n rec.model = extra.model;\n rec.prefixHash = extra.prefixHash;\n }\n return rec;\n}\n\n/**\n * Append a record to an open write stream. Caller owns the stream lifecycle.\n */\nexport function writeRecord(stream: WriteStream, record: TranscriptRecord): void {\n stream.write(`${JSON.stringify(record)}\\n`);\n}\n\n/**\n * Write a _meta line to an open write stream. Call exactly once, at the top.\n */\nexport function writeMeta(stream: WriteStream, meta: TranscriptMeta): void {\n const line: MetaLine = { role: \"_meta\", meta };\n stream.write(`${JSON.stringify(line)}\\n`);\n}\n\n/**\n * Convenience: open a stream, write meta, return stream.\n */\nexport function openTranscriptFile(path: string, meta: TranscriptMeta): WriteStream {\n const stream = createWriteStream(path, { flags: \"a\" });\n writeMeta(stream, meta);\n return stream;\n}\n\n/** Tolerant: empty / malformed lines skipped, missing optionals OK — live chats may be mid-write. */\nexport function readTranscript(path: string): ReadTranscriptResult {\n const raw = readFileSync(path, \"utf8\");\n return parseTranscript(raw);\n}\n\nexport function parseTranscript(raw: string): ReadTranscriptResult {\n const out: ReadTranscriptResult = { meta: null, records: [] };\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n let obj: unknown;\n try {\n obj = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!obj || typeof obj !== \"object\") continue;\n const rec = obj as Record<string, unknown>;\n if (rec.role === \"_meta\" && rec.meta && typeof rec.meta === \"object\") {\n out.meta = rec.meta as TranscriptMeta;\n continue;\n }\n if (\n typeof rec.ts === \"string\" &&\n typeof rec.turn === \"number\" &&\n typeof rec.role === \"string\" &&\n typeof rec.content === \"string\"\n ) {\n out.records.push(rec as unknown as TranscriptRecord);\n }\n }\n return out;\n}\n","/** Reconstruct session economics from a transcript alone — offline audit, no API key. */\n\nimport { Usage } from \"../client.js\";\nimport {\n type SessionSummary,\n type TurnStats,\n claudeEquivalentCost,\n costUsd,\n inputCostUsd,\n outputCostUsd,\n} from \"../telemetry/stats.js\";\nimport { type ReadTranscriptResult, type TranscriptRecord, readTranscript } from \"./log.js\";\n\nexport interface TurnPage {\n turn: number;\n records: TranscriptRecord[];\n}\n\nexport function groupRecordsByTurn(records: TranscriptRecord[]): TurnPage[] {\n const byTurn = new Map<number, TranscriptRecord[]>();\n for (const rec of records) {\n const list = byTurn.get(rec.turn);\n if (list) list.push(rec);\n else byTurn.set(rec.turn, [rec]);\n }\n return [...byTurn.entries()]\n .sort(([a], [b]) => a - b)\n .map(([turn, records]) => ({ turn, records }));\n}\n\nexport function computeCumulativeStats(pages: TurnPage[], upToIdx: number): ReplayStats {\n if (upToIdx < 0) return computeReplayStats([]);\n const flat: TranscriptRecord[] = [];\n for (let i = 0; i <= upToIdx && i < pages.length; i++) {\n const records = pages[i]?.records;\n if (records) flat.push(...records);\n }\n return computeReplayStats(flat);\n}\n\nexport interface ReplayStats extends SessionSummary {\n /** Per-turn stats, in turn order. Only assistant_final records contribute. */\n perTurn: TurnStats[];\n /** Unique models that appeared in the transcript's assistant_final records. */\n models: string[];\n /** Unique prefix hashes that appeared. Length > 1 means the prefix churned (cache-hostile). */\n prefixHashes: string[];\n /** Count of user-role records (user turns issued). */\n userTurns: number;\n /** Count of tool-role records (tool calls executed). */\n toolCalls: number;\n}\n\nexport function replayFromFile(path: string): { parsed: ReadTranscriptResult; stats: ReplayStats } {\n const parsed = readTranscript(path);\n return { parsed, stats: computeReplayStats(parsed.records) };\n}\n\nexport function computeReplayStats(records: TranscriptRecord[]): ReplayStats {\n const turns: TurnStats[] = [];\n const models = new Set<string>();\n const prefixHashes = new Set<string>();\n let userTurns = 0;\n let toolCalls = 0;\n\n for (const rec of records) {\n if (rec.role === \"user\") userTurns++;\n else if (rec.role === \"tool\") toolCalls++;\n else if (rec.role === \"assistant_final\") {\n if (rec.model) models.add(rec.model);\n if (rec.prefixHash) prefixHashes.add(rec.prefixHash);\n if (rec.usage && rec.model) {\n const u = new Usage(\n rec.usage.prompt_tokens ?? 0,\n rec.usage.completion_tokens ?? 0,\n rec.usage.total_tokens ?? 0,\n rec.usage.prompt_cache_hit_tokens ?? 0,\n rec.usage.prompt_cache_miss_tokens ?? 0,\n );\n turns.push({\n turn: rec.turn,\n model: rec.model,\n usage: u,\n // `rec.cost` wins when present — honors whatever the writer computed\n // even if pricing tables have since changed. Only recompute when\n // the transcript didn't record it (old format).\n cost: rec.cost ?? costUsd(rec.model, u),\n cacheHitRatio: u.cacheHitRatio,\n });\n }\n }\n }\n\n return {\n perTurn: turns,\n models: [...models],\n prefixHashes: [...prefixHashes],\n userTurns,\n toolCalls,\n ...summarizeTurns(turns),\n };\n}\n\nfunction summarizeTurns(turns: TurnStats[]): SessionSummary {\n const totalCost = turns.reduce((s, t) => s + t.cost, 0);\n const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);\n const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);\n const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);\n let hit = 0;\n let miss = 0;\n for (const t of turns) {\n hit += t.usage.promptCacheHitTokens;\n miss += t.usage.promptCacheMissTokens;\n }\n const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;\n const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;\n const lastTurn = turns[turns.length - 1];\n return {\n turns: turns.length,\n totalCostUsd: round(totalCost, 6),\n totalInputCostUsd: round(totalInput, 6),\n totalOutputCostUsd: round(totalOutput, 6),\n claudeEquivalentUsd: round(totalClaude, 6),\n savingsVsClaudePct: round(savingsVsClaude * 100, 2),\n cacheHitRatio: round(cacheHitRatio, 4),\n lastPromptTokens: lastTurn?.usage.promptTokens ?? 0,\n lastTurnCostUsd: round(lastTurn?.cost ?? 0, 6),\n };\n}\n\nfunction round(n: number, digits: number): number {\n const f = 10 ** digits;\n return Math.round(n * f) / f;\n}\n"],"mappings":"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","names":["records"]}
|
|
1
|
+
{"version":3,"sources":["../../src/transcript/log.ts","../../src/transcript/replay.ts"],"sourcesContent":["/** Transcripts are receipts (cost/usage/prefix); sessions are memory (ChatMessages). Don't conflate. */\n\nimport { type WriteStream, createWriteStream, readFileSync } from \"node:fs\";\nimport type { LoopEvent } from \"../loop.js\";\nimport type { RawUsage } from \"../types.js\";\n\nexport interface TranscriptRecord {\n /** ISO-8601 timestamp at emit time. */\n ts: string;\n /** 1-based turn number within the session. */\n turn: number;\n /** LoopEvent role — \"assistant_delta\" | \"assistant_final\" | \"tool\" | \"done\" | ... */\n role: string;\n /** For assistant events, the final (or delta) text; for tool events, the tool result. */\n content: string;\n /** Tool name (role === \"tool\"). */\n tool?: string;\n /** JSON-string args the model sent for a tool call (role === \"tool\"). Persisted so diff can explain *why* two runs made different calls. */\n args?: string;\n /** DeepSeek token-usage snapshot (role === \"assistant_final\"). */\n usage?: RawUsage;\n /** USD cost of this turn (role === \"assistant_final\"). */\n cost?: number;\n /** Model id that produced this turn. */\n model?: string;\n /** Lets diff attribute cache-hit delta to log stability vs prompt change. */\n prefixHash?: string;\n /** Optional error message (role === \"error\"). */\n error?: string;\n}\n\nexport interface TranscriptMeta {\n version: 1;\n source: string; // e.g. \"reasonix chat\", \"bench/baseline\", \"bench/reasonix\"\n model?: string;\n task?: string;\n mode?: string;\n repeat?: number;\n startedAt: string;\n}\n\ninterface MetaLine {\n role: \"_meta\";\n meta: TranscriptMeta;\n}\n\nexport interface ReadTranscriptResult {\n meta: TranscriptMeta | null;\n records: TranscriptRecord[];\n}\n\nexport function recordFromLoopEvent(\n ev: LoopEvent,\n extra: { model: string; prefixHash: string },\n): TranscriptRecord {\n const rec: TranscriptRecord = {\n ts: new Date().toISOString(),\n turn: ev.turn,\n role: ev.role,\n content: ev.content,\n };\n if (ev.toolName !== undefined) rec.tool = ev.toolName;\n if (ev.toolArgs !== undefined) rec.args = ev.toolArgs;\n if (ev.error !== undefined) rec.error = ev.error;\n if (ev.stats) {\n rec.usage = {\n prompt_tokens: ev.stats.usage.promptTokens,\n completion_tokens: ev.stats.usage.completionTokens,\n total_tokens: ev.stats.usage.totalTokens,\n prompt_cache_hit_tokens: ev.stats.usage.promptCacheHitTokens,\n prompt_cache_miss_tokens: ev.stats.usage.promptCacheMissTokens,\n };\n rec.cost = ev.stats.cost;\n rec.model = ev.stats.model;\n rec.prefixHash = extra.prefixHash;\n } else if (ev.role === \"assistant_final\") {\n // assistant_final without stats (shouldn't happen in the live loop but\n // might in test fixtures) — still persist model + prefix for continuity.\n rec.model = extra.model;\n rec.prefixHash = extra.prefixHash;\n }\n return rec;\n}\n\n/**\n * Append a record to an open write stream. Caller owns the stream lifecycle.\n */\nexport function writeRecord(stream: WriteStream, record: TranscriptRecord): void {\n stream.write(`${JSON.stringify(record)}\\n`);\n}\n\n/**\n * Write a _meta line to an open write stream. Call exactly once, at the top.\n */\nexport function writeMeta(stream: WriteStream, meta: TranscriptMeta): void {\n const line: MetaLine = { role: \"_meta\", meta };\n stream.write(`${JSON.stringify(line)}\\n`);\n}\n\n/**\n * Convenience: open a stream, write meta, return stream.\n */\nexport function openTranscriptFile(path: string, meta: TranscriptMeta): WriteStream {\n const stream = createWriteStream(path, { flags: \"a\" });\n writeMeta(stream, meta);\n return stream;\n}\n\n/** Tolerant: empty / malformed lines skipped, missing optionals OK — live chats may be mid-write. */\nexport function readTranscript(path: string): ReadTranscriptResult {\n const raw = readFileSync(path, \"utf8\");\n return parseTranscript(raw);\n}\n\nexport function parseTranscript(raw: string): ReadTranscriptResult {\n const out: ReadTranscriptResult = { meta: null, records: [] };\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n let obj: unknown;\n try {\n obj = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!obj || typeof obj !== \"object\") continue;\n const rec = obj as Record<string, unknown>;\n if (rec.role === \"_meta\" && rec.meta && typeof rec.meta === \"object\") {\n out.meta = rec.meta as TranscriptMeta;\n continue;\n }\n if (\n typeof rec.ts === \"string\" &&\n typeof rec.turn === \"number\" &&\n typeof rec.role === \"string\" &&\n typeof rec.content === \"string\"\n ) {\n out.records.push(rec as unknown as TranscriptRecord);\n }\n }\n return out;\n}\n","/** Reconstruct session economics from a transcript alone — offline audit, no API key. */\n\nimport { Usage } from \"../client.js\";\nimport {\n type SessionSummary,\n type TurnStats,\n claudeEquivalentCost,\n costUsd,\n inputCostUsd,\n outputCostUsd,\n} from \"../telemetry/stats.js\";\nimport { type ReadTranscriptResult, type TranscriptRecord, readTranscript } from \"./log.js\";\n\nexport interface TurnPage {\n turn: number;\n records: TranscriptRecord[];\n}\n\nexport function groupRecordsByTurn(records: TranscriptRecord[]): TurnPage[] {\n const byTurn = new Map<number, TranscriptRecord[]>();\n for (const rec of records) {\n const list = byTurn.get(rec.turn);\n if (list) list.push(rec);\n else byTurn.set(rec.turn, [rec]);\n }\n return [...byTurn.entries()]\n .sort(([a], [b]) => a - b)\n .map(([turn, records]) => ({ turn, records }));\n}\n\nexport function computeCumulativeStats(pages: TurnPage[], upToIdx: number): ReplayStats {\n if (upToIdx < 0) return computeReplayStats([]);\n const flat: TranscriptRecord[] = [];\n for (let i = 0; i <= upToIdx && i < pages.length; i++) {\n const records = pages[i]?.records;\n if (records) flat.push(...records);\n }\n return computeReplayStats(flat);\n}\n\nexport interface ReplayStats extends SessionSummary {\n /** Per-turn stats, in turn order. Only assistant_final records contribute. */\n perTurn: TurnStats[];\n /** Unique models that appeared in the transcript's assistant_final records. */\n models: string[];\n /** Unique prefix hashes that appeared. Length > 1 means the prefix churned (cache-hostile). */\n prefixHashes: string[];\n /** Count of user-role records (user turns issued). */\n userTurns: number;\n /** Count of tool-role records (tool calls executed). */\n toolCalls: number;\n}\n\nexport function replayFromFile(path: string): { parsed: ReadTranscriptResult; stats: ReplayStats } {\n const parsed = readTranscript(path);\n return { parsed, stats: computeReplayStats(parsed.records) };\n}\n\nexport function computeReplayStats(records: TranscriptRecord[]): ReplayStats {\n const turns: TurnStats[] = [];\n const models = new Set<string>();\n const prefixHashes = new Set<string>();\n let userTurns = 0;\n let toolCalls = 0;\n\n for (const rec of records) {\n if (rec.role === \"user\") userTurns++;\n else if (rec.role === \"tool\") toolCalls++;\n else if (rec.role === \"assistant_final\") {\n if (rec.model) models.add(rec.model);\n if (rec.prefixHash) prefixHashes.add(rec.prefixHash);\n if (rec.usage && rec.model) {\n const u = new Usage(\n rec.usage.prompt_tokens ?? 0,\n rec.usage.completion_tokens ?? 0,\n rec.usage.total_tokens ?? 0,\n rec.usage.prompt_cache_hit_tokens ?? 0,\n rec.usage.prompt_cache_miss_tokens ?? 0,\n );\n turns.push({\n turn: rec.turn,\n model: rec.model,\n usage: u,\n // `rec.cost` wins when present — honors whatever the writer computed\n // even if pricing tables have since changed. Only recompute when\n // the transcript didn't record it (old format).\n cost: rec.cost ?? costUsd(rec.model, u),\n cacheHitRatio: u.cacheHitRatio,\n });\n }\n }\n }\n\n return {\n perTurn: turns,\n models: [...models],\n prefixHashes: [...prefixHashes],\n userTurns,\n toolCalls,\n ...summarizeTurns(turns),\n };\n}\n\nfunction summarizeTurns(turns: TurnStats[]): SessionSummary {\n const totalCost = turns.reduce((s, t) => s + t.cost, 0);\n const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);\n const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);\n const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);\n let hit = 0;\n let miss = 0;\n for (const t of turns) {\n hit += t.usage.promptCacheHitTokens;\n miss += t.usage.promptCacheMissTokens;\n }\n const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;\n const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;\n const lastTurn = turns[turns.length - 1];\n return {\n turns: turns.length,\n totalCostUsd: round(totalCost, 6),\n totalInputCostUsd: round(totalInput, 6),\n totalOutputCostUsd: round(totalOutput, 6),\n claudeEquivalentUsd: round(totalClaude, 6),\n savingsVsClaudePct: round(savingsVsClaude * 100, 2),\n cacheHitRatio: round(cacheHitRatio, 4),\n lastPromptTokens: lastTurn?.usage.promptTokens ?? 0,\n lastTurnCostUsd: round(lastTurn?.cost ?? 0, 6),\n };\n}\n\nfunction round(n: number, digits: number): number {\n const f = 10 ** digits;\n return Math.round(n * f) / f;\n}\n"],"mappings":"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","names":["records"]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
t
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-RE4RAVFF.js";
|
|
5
6
|
|
|
6
7
|
// src/cli/ui/mcp-lifecycle.ts
|
|
7
8
|
var STATE = {
|
|
@@ -42,4 +43,4 @@ export {
|
|
|
42
43
|
formatMcpLifecycleEvent,
|
|
43
44
|
formatMcpSlowToast
|
|
44
45
|
};
|
|
45
|
-
//# sourceMappingURL=chunk-
|
|
46
|
+
//# sourceMappingURL=chunk-H6PS7IUE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/ui/mcp-lifecycle.ts","../../src/cli/ui/mcp-toast.ts"],"sourcesContent":["/** Formats one-liner MCP lifecycle events per `docs/design/agent-tui-terminal.html` §37. */\n\nimport { t } from \"../../i18n/index.js\";\n\nexport type McpLifecycleEvent =\n | { state: \"handshake\"; name: string }\n | {\n state: \"connected\";\n name: string;\n tools: number;\n resources?: number;\n prompts?: number;\n ms: number;\n }\n | { state: \"failed\"; name: string; reason: string }\n | { state: \"disabled\"; name: string }\n | { state: \"reconnect\"; name: string };\n\nconst STATE: Record<McpLifecycleEvent[\"state\"], { glyph: string; label: () => string }> = {\n handshake: { glyph: \"↻\", label: () => t(\"mcpLifecycle.handshake\") },\n connected: { glyph: \"✓\", label: () => t(\"mcpLifecycle.connected\") },\n failed: { glyph: \"✖\", label: () => t(\"mcpLifecycle.failed\") },\n disabled: { glyph: \"○\", label: () => t(\"mcpLifecycle.disabled\") },\n reconnect: { glyph: \"↻\", label: () => t(\"mcpLifecycle.reconnect\") },\n};\n\nconst NAME_COL = 22;\nconst STATE_COL = 15;\n\nexport function formatMcpLifecycleEvent(ev: McpLifecycleEvent): string {\n const { glyph, label } = STATE[ev.state];\n const namePart = `MCP · ${ev.name}`;\n const namePad = \" \".repeat(Math.max(1, NAME_COL - namePart.length));\n const stateField = `${glyph} ${label()}`.padEnd(STATE_COL);\n return `⌘ ${namePart}${namePad}${stateField}${describeDetail(ev)}`;\n}\n\nfunction describeDetail(ev: McpLifecycleEvent): string {\n if (ev.state === \"handshake\") return t(\"mcpLifecycle.initDetail\");\n if (ev.state === \"failed\") return ev.reason;\n if (ev.state === \"disabled\") return t(\"mcpLifecycle.disabledDetail\", { name: ev.name });\n if (ev.state === \"reconnect\") return t(\"mcpLifecycle.reconnectDetail\");\n const parts: string[] = [`${ev.tools} tools`];\n if (ev.resources && ev.resources > 0) parts.push(`${ev.resources} resources`);\n if (ev.prompts && ev.prompts > 0) parts.push(`${ev.prompts} prompts`);\n parts.push(`${ev.ms}ms`);\n return parts.join(\" · \");\n}\n","/** One-line warn toast emitted when an MCP server's p95 crosses the slow threshold (design §32). */\n\nimport { t } from \"../../i18n/index.js\";\n\nexport interface McpSlowToast {\n name: string;\n p95Ms: number;\n sampleSize: number;\n}\n\nexport function formatMcpSlowToast(tst: McpSlowToast): string {\n const seconds = (tst.p95Ms / 1000).toFixed(1);\n return t(\"mcpHealth.slowToast\", { name: tst.name, seconds, sampleSize: tst.sampleSize });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/cli/ui/mcp-lifecycle.ts","../../src/cli/ui/mcp-toast.ts"],"sourcesContent":["/** Formats one-liner MCP lifecycle events per `docs/design/agent-tui-terminal.html` §37. */\n\nimport { t } from \"../../i18n/index.js\";\n\nexport type McpLifecycleEvent =\n | { state: \"handshake\"; name: string }\n | {\n state: \"connected\";\n name: string;\n tools: number;\n resources?: number;\n prompts?: number;\n ms: number;\n }\n | { state: \"failed\"; name: string; reason: string }\n | { state: \"disabled\"; name: string }\n | { state: \"reconnect\"; name: string };\n\nconst STATE: Record<McpLifecycleEvent[\"state\"], { glyph: string; label: () => string }> = {\n handshake: { glyph: \"↻\", label: () => t(\"mcpLifecycle.handshake\") },\n connected: { glyph: \"✓\", label: () => t(\"mcpLifecycle.connected\") },\n failed: { glyph: \"✖\", label: () => t(\"mcpLifecycle.failed\") },\n disabled: { glyph: \"○\", label: () => t(\"mcpLifecycle.disabled\") },\n reconnect: { glyph: \"↻\", label: () => t(\"mcpLifecycle.reconnect\") },\n};\n\nconst NAME_COL = 22;\nconst STATE_COL = 15;\n\nexport function formatMcpLifecycleEvent(ev: McpLifecycleEvent): string {\n const { glyph, label } = STATE[ev.state];\n const namePart = `MCP · ${ev.name}`;\n const namePad = \" \".repeat(Math.max(1, NAME_COL - namePart.length));\n const stateField = `${glyph} ${label()}`.padEnd(STATE_COL);\n return `⌘ ${namePart}${namePad}${stateField}${describeDetail(ev)}`;\n}\n\nfunction describeDetail(ev: McpLifecycleEvent): string {\n if (ev.state === \"handshake\") return t(\"mcpLifecycle.initDetail\");\n if (ev.state === \"failed\") return ev.reason;\n if (ev.state === \"disabled\") return t(\"mcpLifecycle.disabledDetail\", { name: ev.name });\n if (ev.state === \"reconnect\") return t(\"mcpLifecycle.reconnectDetail\");\n const parts: string[] = [`${ev.tools} tools`];\n if (ev.resources && ev.resources > 0) parts.push(`${ev.resources} resources`);\n if (ev.prompts && ev.prompts > 0) parts.push(`${ev.prompts} prompts`);\n parts.push(`${ev.ms}ms`);\n return parts.join(\" · \");\n}\n","/** One-line warn toast emitted when an MCP server's p95 crosses the slow threshold (design §32). */\n\nimport { t } from \"../../i18n/index.js\";\n\nexport interface McpSlowToast {\n name: string;\n p95Ms: number;\n sampleSize: number;\n}\n\nexport function formatMcpSlowToast(tst: McpSlowToast): string {\n const seconds = (tst.p95Ms / 1000).toFixed(1);\n return t(\"mcpHealth.slowToast\", { name: tst.name, seconds, sampleSize: tst.sampleSize });\n}\n"],"mappings":";;;;;;;AAkBA,IAAM,QAAoF;AAAA,EACxF,WAAW,EAAE,OAAO,UAAK,OAAO,MAAM,EAAE,wBAAwB,EAAE;AAAA,EAClE,WAAW,EAAE,OAAO,UAAK,OAAO,MAAM,EAAE,wBAAwB,EAAE;AAAA,EAClE,QAAQ,EAAE,OAAO,UAAK,OAAO,MAAM,EAAE,qBAAqB,EAAE;AAAA,EAC5D,UAAU,EAAE,OAAO,UAAK,OAAO,MAAM,EAAE,uBAAuB,EAAE;AAAA,EAChE,WAAW,EAAE,OAAO,UAAK,OAAO,MAAM,EAAE,wBAAwB,EAAE;AACpE;AAEA,IAAM,WAAW;AACjB,IAAM,YAAY;AAEX,SAAS,wBAAwB,IAA+B;AACrE,QAAM,EAAE,OAAO,MAAM,IAAI,MAAM,GAAG,KAAK;AACvC,QAAM,WAAW,YAAS,GAAG,IAAI;AACjC,QAAM,UAAU,IAAI,OAAO,KAAK,IAAI,GAAG,WAAW,SAAS,MAAM,CAAC;AAClE,QAAM,aAAa,GAAG,KAAK,IAAI,MAAM,CAAC,GAAG,OAAO,SAAS;AACzD,SAAO,UAAK,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,eAAe,EAAE,CAAC;AAClE;AAEA,SAAS,eAAe,IAA+B;AACrD,MAAI,GAAG,UAAU,YAAa,QAAO,EAAE,yBAAyB;AAChE,MAAI,GAAG,UAAU,SAAU,QAAO,GAAG;AACrC,MAAI,GAAG,UAAU,WAAY,QAAO,EAAE,+BAA+B,EAAE,MAAM,GAAG,KAAK,CAAC;AACtF,MAAI,GAAG,UAAU,YAAa,QAAO,EAAE,8BAA8B;AACrE,QAAM,QAAkB,CAAC,GAAG,GAAG,KAAK,QAAQ;AAC5C,MAAI,GAAG,aAAa,GAAG,YAAY,EAAG,OAAM,KAAK,GAAG,GAAG,SAAS,YAAY;AAC5E,MAAI,GAAG,WAAW,GAAG,UAAU,EAAG,OAAM,KAAK,GAAG,GAAG,OAAO,UAAU;AACpE,QAAM,KAAK,GAAG,GAAG,EAAE,IAAI;AACvB,SAAO,MAAM,KAAK,QAAK;AACzB;;;ACrCO,SAAS,mBAAmB,KAA2B;AAC5D,QAAM,WAAW,IAAI,QAAQ,KAAM,QAAQ,CAAC;AAC5C,SAAO,EAAE,uBAAuB,EAAE,MAAM,IAAI,MAAM,SAAS,YAAY,IAAI,WAAW,CAAC;AACzF;","names":[]}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
cacheSavingsUsd,
|
|
4
5
|
claudeEquivalentCost,
|
|
5
6
|
costUsd
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-YQ6NTIIE.js";
|
|
7
8
|
|
|
8
9
|
// src/telemetry/usage.ts
|
|
9
10
|
import {
|
|
@@ -228,4 +229,4 @@ export {
|
|
|
228
229
|
aggregateUsage,
|
|
229
230
|
formatLogSize
|
|
230
231
|
};
|
|
231
|
-
//# sourceMappingURL=chunk-
|
|
232
|
+
//# sourceMappingURL=chunk-HFEAY5DT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/telemetry/usage.ts"],"sourcesContent":["/** Append-only JSONL of per-turn tokens + cost; best-effort writes, never blocks the turn. No prompts/completions logged. */\n\nimport {\n appendFileSync,\n closeSync,\n existsSync,\n fstatSync,\n mkdirSync,\n openSync,\n readFileSync,\n readSync,\n renameSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { Usage } from \"../client.js\";\nimport {\n CLAUDE_SONNET_PRICING,\n DEEPSEEK_PRICING,\n cacheSavingsUsd,\n claudeEquivalentCost,\n costUsd,\n} from \"./stats.js\";\n\n/** One turn's snapshot — serialized verbatim as a JSONL line. */\nexport interface UsageRecord {\n /** Epoch millis when the record was written. */\n ts: number;\n /** Session name if the turn ran inside a persisted session, `null` for ephemeral. */\n session: string | null;\n /** Model id the turn ran against (drives the pricing lookup). */\n model: string;\n promptTokens: number;\n completionTokens: number;\n cacheHitTokens: number;\n cacheMissTokens: number;\n /** Total cost of the turn in USD. */\n costUsd: number;\n /** What the same turn would have cost at Claude Sonnet 4.6 rates. */\n claudeEquivUsd: number;\n /** Absent on legacy records — treat as \"turn\" when missing. */\n kind?: \"turn\" | \"subagent\";\n /** Present when `kind === \"subagent\"`. Attribution metadata for the /stats roll-up. */\n subagent?: {\n /** Skill that spawned it, when the spawn came from a `runAs: subagent` skill. */\n skillName?: string;\n /** First ~60 chars of the task prompt — enough context to recognize a run, never the full text. */\n taskPreview: string;\n /** Tool calls the child loop dispatched before returning. */\n toolIters: number;\n /** Wall-clock ms. */\n durationMs: number;\n };\n}\n\n/** Where the log lives. Tests override via `opts.path`. */\nexport function defaultUsageLogPath(homeDirOverride?: string): string {\n return join(homeDirOverride ?? homedir(), \".reasonix\", \"usage.jsonl\");\n}\n\nexport interface AppendUsageInput {\n session: string | null;\n model: string;\n usage: Usage;\n /** Override the timestamp (tests). */\n now?: number;\n /** Override the log path (tests). */\n path?: string;\n /** When appending a subagent summary row, set `kind: \"subagent\"` and populate `subagent`. */\n kind?: \"turn\" | \"subagent\";\n subagent?: UsageRecord[\"subagent\"];\n}\n\nconst USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;\nconst USAGE_RETENTION_DAYS = 365;\n\nfunction compactUsageLogIfLarge(path: string, now: number): void {\n // Open once for the size check + read so they bind to the same fd\n // (CodeQL js/file-system-race). Concurrent appenders that grow the\n // log between check and read can no longer cause us to act on a\n // stale size and rewrite based on partial content.\n let raw: string;\n try {\n const fd = openSync(path, \"r\");\n try {\n const stat = fstatSync(fd);\n if (stat.size < USAGE_COMPACTION_THRESHOLD_BYTES) return;\n const buf = Buffer.alloc(stat.size);\n let read = 0;\n while (read < stat.size) {\n const n = readSync(fd, buf, read, stat.size - read, read);\n if (n <= 0) break;\n read += n;\n }\n raw = buf.toString(\"utf8\", 0, read);\n } finally {\n closeSync(fd);\n }\n } catch {\n return;\n }\n const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n const lines = raw.split(/\\r?\\n/);\n const kept: string[] = [];\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const rec = JSON.parse(line);\n if (isValidRecord(rec) && rec.ts >= cutoff) kept.push(line);\n } catch {\n /* skip malformed */\n }\n }\n // No-op when nothing aged out — avoids rewrite storms on fresh logs.\n if (kept.length === lines.filter((l) => l.trim()).length) return;\n // Write to a sibling tmp path then rename — atomic from a reader's\n // POV and severs CodeQL's stat→write taint chain. Concurrent\n // appenders during the compaction window lose their entries; we\n // accept that for a best-effort usage log.\n const tmp = `${path}.compacting`;\n try {\n writeFileSync(tmp, kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\", \"utf8\");\n renameSync(tmp, path);\n } catch {\n try {\n unlinkSync(tmp);\n } catch {\n /* tmp may not exist — ignore */\n }\n }\n}\n\n/** Returns the record so tests can assert cost fields without re-reading the log. */\nexport function appendUsage(input: AppendUsageInput): UsageRecord {\n const record: UsageRecord = {\n ts: input.now ?? Date.now(),\n session: input.session,\n model: input.model,\n promptTokens: input.usage.promptTokens,\n completionTokens: input.usage.completionTokens,\n cacheHitTokens: input.usage.promptCacheHitTokens,\n cacheMissTokens: input.usage.promptCacheMissTokens,\n costUsd: costUsd(input.model, input.usage),\n claudeEquivUsd: claudeEquivalentCost(input.usage),\n };\n if (input.kind === \"subagent\") record.kind = \"subagent\";\n if (input.subagent) record.subagent = input.subagent;\n\n const path = input.path ?? defaultUsageLogPath();\n try {\n mkdirSync(dirname(path), { recursive: true });\n appendFileSync(path, `${JSON.stringify(record)}\\n`, \"utf8\");\n compactUsageLogIfLarge(path, record.ts);\n } catch {\n /* best-effort — disk failure shouldn't break the chat */\n }\n return record;\n}\n\nexport function readUsageLog(path: string = defaultUsageLogPath()): UsageRecord[] {\n if (!existsSync(path)) return [];\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {\n return [];\n }\n const out: UsageRecord[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n if (!line.trim()) continue;\n try {\n const rec = JSON.parse(line);\n if (isValidRecord(rec)) out.push(rec);\n } catch {\n /* skip malformed */\n }\n }\n return out;\n}\n\nfunction isValidRecord(rec: unknown): rec is UsageRecord {\n if (!rec || typeof rec !== \"object\") return false;\n const r = rec as Partial<UsageRecord>;\n return (\n typeof r.ts === \"number\" &&\n typeof r.model === \"string\" &&\n typeof r.promptTokens === \"number\" &&\n typeof r.completionTokens === \"number\" &&\n typeof r.cacheHitTokens === \"number\" &&\n typeof r.cacheMissTokens === \"number\" &&\n typeof r.costUsd === \"number\" &&\n typeof r.claudeEquivUsd === \"number\"\n );\n}\n\n/** One row of the `reasonix stats` dashboard — a rolled-up window. */\nexport interface UsageBucket {\n label: string;\n /** Start of the window as epoch millis. `0` = unbounded (all-time). */\n since: number;\n turns: number;\n promptTokens: number;\n completionTokens: number;\n cacheHitTokens: number;\n cacheMissTokens: number;\n costUsd: number;\n claudeEquivUsd: number;\n /** Recomputed from current pricing each aggregate — intentionally NOT frozen with `costUsd`. */\n cacheSavingsUsd: number;\n}\n\n/** Cache hit ratio for a bucket — zero denominator returns 0. */\nexport function bucketCacheHitRatio(b: UsageBucket): number {\n const denom = b.cacheHitTokens + b.cacheMissTokens;\n return denom > 0 ? b.cacheHitTokens / denom : 0;\n}\n\n/** Savings vs Claude as a fraction (0.94 = 94% savings). 0 if Claude cost is 0. */\nexport function bucketSavingsFraction(b: UsageBucket): number {\n return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;\n}\n\nfunction emptyBucket(label: string, since: number): UsageBucket {\n return {\n label,\n since,\n turns: 0,\n promptTokens: 0,\n completionTokens: 0,\n cacheHitTokens: 0,\n cacheMissTokens: 0,\n costUsd: 0,\n claudeEquivUsd: 0,\n cacheSavingsUsd: 0,\n };\n}\n\nfunction addToBucket(b: UsageBucket, r: UsageRecord): void {\n b.turns += 1;\n b.promptTokens += r.promptTokens;\n b.completionTokens += r.completionTokens;\n b.cacheHitTokens += r.cacheHitTokens;\n b.cacheMissTokens += r.cacheMissTokens;\n b.costUsd += r.costUsd;\n b.claudeEquivUsd += r.claudeEquivUsd;\n b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);\n}\n\nexport interface AggregateOptions {\n /** Override `Date.now()` for deterministic tests. */\n now?: number;\n}\n\nexport interface UsageAggregate {\n /** Fixed-order rolling windows: today, week, month, all-time. */\n buckets: UsageBucket[];\n /** Model id → turn count. Sorted descending; top entry is the \"most used.\" */\n byModel: Array<{ model: string; turns: number }>;\n /** Session name → turn count. Sorted descending. Null sessions are grouped under `\"(ephemeral)\"`. */\n bySession: Array<{ session: string; turns: number }>;\n /** Earliest record's ts, or `null` when the log is empty. Drives \"saved $X since <date>\". */\n firstSeen: number | null;\n /** Latest record's ts, or `null` when the log is empty. */\n lastSeen: number | null;\n /** Undefined when no subagent records exist; counts spawns, not internal child-loop turns. */\n subagents?: SubagentAggregate;\n}\n\n/** Rolled-up view of all `kind: \"subagent\"` records. */\nexport interface SubagentAggregate {\n total: number;\n costUsd: number;\n totalDurationMs: number;\n /** Per-skill breakdown. Records without `skillName` (raw spawn_subagent calls) group under `\"(adhoc)\"`. */\n bySkill: Array<{ skillName: string; count: number; costUsd: number; durationMs: number }>;\n}\n\n/** Rolling 24h/7d/30d windows — avoids \"it's 00:03, 'today' is empty\" surprises. */\nexport function aggregateUsage(\n records: UsageRecord[],\n opts: AggregateOptions = {},\n): UsageAggregate {\n const now = opts.now ?? Date.now();\n const day = 24 * 60 * 60 * 1000;\n const today = emptyBucket(\"today\", now - day);\n const week = emptyBucket(\"week\", now - 7 * day);\n const month = emptyBucket(\"month\", now - 30 * day);\n const all = emptyBucket(\"all-time\", 0);\n\n const modelCounts = new Map<string, number>();\n const sessionCounts = new Map<string, number>();\n let firstSeen: number | null = null;\n let lastSeen: number | null = null;\n const skillCounts = new Map<string, { count: number; costUsd: number; durationMs: number }>();\n let subagentTotal = 0;\n let subagentCost = 0;\n let subagentDuration = 0;\n\n for (const r of records) {\n addToBucket(all, r);\n if (r.ts >= today.since) addToBucket(today, r);\n if (r.ts >= week.since) addToBucket(week, r);\n if (r.ts >= month.since) addToBucket(month, r);\n\n modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);\n const sessKey = r.session ?? \"(ephemeral)\";\n sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);\n\n if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;\n if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;\n\n if (r.kind === \"subagent\") {\n subagentTotal += 1;\n subagentCost += r.costUsd;\n const dur = r.subagent?.durationMs ?? 0;\n subagentDuration += dur;\n const key = r.subagent?.skillName?.trim() || \"(adhoc)\";\n const prev = skillCounts.get(key) ?? { count: 0, costUsd: 0, durationMs: 0 };\n prev.count += 1;\n prev.costUsd += r.costUsd;\n prev.durationMs += dur;\n skillCounts.set(key, prev);\n }\n }\n\n const byModel = Array.from(modelCounts.entries())\n .map(([model, turns]) => ({ model, turns }))\n .sort((a, b) => b.turns - a.turns);\n const bySession = Array.from(sessionCounts.entries())\n .map(([session, turns]) => ({ session, turns }))\n .sort((a, b) => b.turns - a.turns);\n\n const subagents: SubagentAggregate | undefined =\n subagentTotal > 0\n ? {\n total: subagentTotal,\n costUsd: subagentCost,\n totalDurationMs: subagentDuration,\n bySkill: Array.from(skillCounts.entries())\n .map(([skillName, v]) => ({ skillName, ...v }))\n .sort((a, b) => b.count - a.count),\n }\n : undefined;\n\n return {\n buckets: [today, week, month, all],\n byModel,\n bySession,\n firstSeen,\n lastSeen,\n subagents,\n };\n}\n\n/** File-size helper for the stats header — \"1.2 MB\" etc. Returns \"\" if missing. */\nexport function formatLogSize(path: string = defaultUsageLogPath()): string {\n if (!existsSync(path)) return \"\";\n try {\n const s = statSync(path);\n const bytes = s.size;\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n } catch {\n return \"\";\n }\n}\n\n/** Re-exports for downstream consumers that also want the pricing constants. */\nexport { CLAUDE_SONNET_PRICING, DEEPSEEK_PRICING };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/telemetry/usage.ts"],"sourcesContent":["/** Append-only JSONL of per-turn tokens + cost; best-effort writes, never blocks the turn. No prompts/completions logged. */\n\nimport {\n appendFileSync,\n closeSync,\n existsSync,\n fstatSync,\n mkdirSync,\n openSync,\n readFileSync,\n readSync,\n renameSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { Usage } from \"../client.js\";\nimport {\n CLAUDE_SONNET_PRICING,\n DEEPSEEK_PRICING,\n cacheSavingsUsd,\n claudeEquivalentCost,\n costUsd,\n} from \"./stats.js\";\n\n/** One turn's snapshot — serialized verbatim as a JSONL line. */\nexport interface UsageRecord {\n /** Epoch millis when the record was written. */\n ts: number;\n /** Session name if the turn ran inside a persisted session, `null` for ephemeral. */\n session: string | null;\n /** Model id the turn ran against (drives the pricing lookup). */\n model: string;\n promptTokens: number;\n completionTokens: number;\n cacheHitTokens: number;\n cacheMissTokens: number;\n /** Total cost of the turn in USD. */\n costUsd: number;\n /** What the same turn would have cost at Claude Sonnet 4.6 rates. */\n claudeEquivUsd: number;\n /** Absent on legacy records — treat as \"turn\" when missing. */\n kind?: \"turn\" | \"subagent\";\n /** Present when `kind === \"subagent\"`. Attribution metadata for the /stats roll-up. */\n subagent?: {\n /** Skill that spawned it, when the spawn came from a `runAs: subagent` skill. */\n skillName?: string;\n /** First ~60 chars of the task prompt — enough context to recognize a run, never the full text. */\n taskPreview: string;\n /** Tool calls the child loop dispatched before returning. */\n toolIters: number;\n /** Wall-clock ms. */\n durationMs: number;\n };\n}\n\n/** Where the log lives. Tests override via `opts.path`. */\nexport function defaultUsageLogPath(homeDirOverride?: string): string {\n return join(homeDirOverride ?? homedir(), \".reasonix\", \"usage.jsonl\");\n}\n\nexport interface AppendUsageInput {\n session: string | null;\n model: string;\n usage: Usage;\n /** Override the timestamp (tests). */\n now?: number;\n /** Override the log path (tests). */\n path?: string;\n /** When appending a subagent summary row, set `kind: \"subagent\"` and populate `subagent`. */\n kind?: \"turn\" | \"subagent\";\n subagent?: UsageRecord[\"subagent\"];\n}\n\nconst USAGE_COMPACTION_THRESHOLD_BYTES = 5 * 1024 * 1024;\nconst USAGE_RETENTION_DAYS = 365;\n\nfunction compactUsageLogIfLarge(path: string, now: number): void {\n // Open once for the size check + read so they bind to the same fd\n // (CodeQL js/file-system-race). Concurrent appenders that grow the\n // log between check and read can no longer cause us to act on a\n // stale size and rewrite based on partial content.\n let raw: string;\n try {\n const fd = openSync(path, \"r\");\n try {\n const stat = fstatSync(fd);\n if (stat.size < USAGE_COMPACTION_THRESHOLD_BYTES) return;\n const buf = Buffer.alloc(stat.size);\n let read = 0;\n while (read < stat.size) {\n const n = readSync(fd, buf, read, stat.size - read, read);\n if (n <= 0) break;\n read += n;\n }\n raw = buf.toString(\"utf8\", 0, read);\n } finally {\n closeSync(fd);\n }\n } catch {\n return;\n }\n const cutoff = now - USAGE_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n const lines = raw.split(/\\r?\\n/);\n const kept: string[] = [];\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const rec = JSON.parse(line);\n if (isValidRecord(rec) && rec.ts >= cutoff) kept.push(line);\n } catch {\n /* skip malformed */\n }\n }\n // No-op when nothing aged out — avoids rewrite storms on fresh logs.\n if (kept.length === lines.filter((l) => l.trim()).length) return;\n // Write to a sibling tmp path then rename — atomic from a reader's\n // POV and severs CodeQL's stat→write taint chain. Concurrent\n // appenders during the compaction window lose their entries; we\n // accept that for a best-effort usage log.\n const tmp = `${path}.compacting`;\n try {\n writeFileSync(tmp, kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\", \"utf8\");\n renameSync(tmp, path);\n } catch {\n try {\n unlinkSync(tmp);\n } catch {\n /* tmp may not exist — ignore */\n }\n }\n}\n\n/** Returns the record so tests can assert cost fields without re-reading the log. */\nexport function appendUsage(input: AppendUsageInput): UsageRecord {\n const record: UsageRecord = {\n ts: input.now ?? Date.now(),\n session: input.session,\n model: input.model,\n promptTokens: input.usage.promptTokens,\n completionTokens: input.usage.completionTokens,\n cacheHitTokens: input.usage.promptCacheHitTokens,\n cacheMissTokens: input.usage.promptCacheMissTokens,\n costUsd: costUsd(input.model, input.usage),\n claudeEquivUsd: claudeEquivalentCost(input.usage),\n };\n if (input.kind === \"subagent\") record.kind = \"subagent\";\n if (input.subagent) record.subagent = input.subagent;\n\n const path = input.path ?? defaultUsageLogPath();\n try {\n mkdirSync(dirname(path), { recursive: true });\n appendFileSync(path, `${JSON.stringify(record)}\\n`, \"utf8\");\n compactUsageLogIfLarge(path, record.ts);\n } catch {\n /* best-effort — disk failure shouldn't break the chat */\n }\n return record;\n}\n\nexport function readUsageLog(path: string = defaultUsageLogPath()): UsageRecord[] {\n if (!existsSync(path)) return [];\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {\n return [];\n }\n const out: UsageRecord[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n if (!line.trim()) continue;\n try {\n const rec = JSON.parse(line);\n if (isValidRecord(rec)) out.push(rec);\n } catch {\n /* skip malformed */\n }\n }\n return out;\n}\n\nfunction isValidRecord(rec: unknown): rec is UsageRecord {\n if (!rec || typeof rec !== \"object\") return false;\n const r = rec as Partial<UsageRecord>;\n return (\n typeof r.ts === \"number\" &&\n typeof r.model === \"string\" &&\n typeof r.promptTokens === \"number\" &&\n typeof r.completionTokens === \"number\" &&\n typeof r.cacheHitTokens === \"number\" &&\n typeof r.cacheMissTokens === \"number\" &&\n typeof r.costUsd === \"number\" &&\n typeof r.claudeEquivUsd === \"number\"\n );\n}\n\n/** One row of the `reasonix stats` dashboard — a rolled-up window. */\nexport interface UsageBucket {\n label: string;\n /** Start of the window as epoch millis. `0` = unbounded (all-time). */\n since: number;\n turns: number;\n promptTokens: number;\n completionTokens: number;\n cacheHitTokens: number;\n cacheMissTokens: number;\n costUsd: number;\n claudeEquivUsd: number;\n /** Recomputed from current pricing each aggregate — intentionally NOT frozen with `costUsd`. */\n cacheSavingsUsd: number;\n}\n\n/** Cache hit ratio for a bucket — zero denominator returns 0. */\nexport function bucketCacheHitRatio(b: UsageBucket): number {\n const denom = b.cacheHitTokens + b.cacheMissTokens;\n return denom > 0 ? b.cacheHitTokens / denom : 0;\n}\n\n/** Savings vs Claude as a fraction (0.94 = 94% savings). 0 if Claude cost is 0. */\nexport function bucketSavingsFraction(b: UsageBucket): number {\n return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;\n}\n\nfunction emptyBucket(label: string, since: number): UsageBucket {\n return {\n label,\n since,\n turns: 0,\n promptTokens: 0,\n completionTokens: 0,\n cacheHitTokens: 0,\n cacheMissTokens: 0,\n costUsd: 0,\n claudeEquivUsd: 0,\n cacheSavingsUsd: 0,\n };\n}\n\nfunction addToBucket(b: UsageBucket, r: UsageRecord): void {\n b.turns += 1;\n b.promptTokens += r.promptTokens;\n b.completionTokens += r.completionTokens;\n b.cacheHitTokens += r.cacheHitTokens;\n b.cacheMissTokens += r.cacheMissTokens;\n b.costUsd += r.costUsd;\n b.claudeEquivUsd += r.claudeEquivUsd;\n b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);\n}\n\nexport interface AggregateOptions {\n /** Override `Date.now()` for deterministic tests. */\n now?: number;\n}\n\nexport interface UsageAggregate {\n /** Fixed-order rolling windows: today, week, month, all-time. */\n buckets: UsageBucket[];\n /** Model id → turn count. Sorted descending; top entry is the \"most used.\" */\n byModel: Array<{ model: string; turns: number }>;\n /** Session name → turn count. Sorted descending. Null sessions are grouped under `\"(ephemeral)\"`. */\n bySession: Array<{ session: string; turns: number }>;\n /** Earliest record's ts, or `null` when the log is empty. Drives \"saved $X since <date>\". */\n firstSeen: number | null;\n /** Latest record's ts, or `null` when the log is empty. */\n lastSeen: number | null;\n /** Undefined when no subagent records exist; counts spawns, not internal child-loop turns. */\n subagents?: SubagentAggregate;\n}\n\n/** Rolled-up view of all `kind: \"subagent\"` records. */\nexport interface SubagentAggregate {\n total: number;\n costUsd: number;\n totalDurationMs: number;\n /** Per-skill breakdown. Records without `skillName` (raw spawn_subagent calls) group under `\"(adhoc)\"`. */\n bySkill: Array<{ skillName: string; count: number; costUsd: number; durationMs: number }>;\n}\n\n/** Rolling 24h/7d/30d windows — avoids \"it's 00:03, 'today' is empty\" surprises. */\nexport function aggregateUsage(\n records: UsageRecord[],\n opts: AggregateOptions = {},\n): UsageAggregate {\n const now = opts.now ?? Date.now();\n const day = 24 * 60 * 60 * 1000;\n const today = emptyBucket(\"today\", now - day);\n const week = emptyBucket(\"week\", now - 7 * day);\n const month = emptyBucket(\"month\", now - 30 * day);\n const all = emptyBucket(\"all-time\", 0);\n\n const modelCounts = new Map<string, number>();\n const sessionCounts = new Map<string, number>();\n let firstSeen: number | null = null;\n let lastSeen: number | null = null;\n const skillCounts = new Map<string, { count: number; costUsd: number; durationMs: number }>();\n let subagentTotal = 0;\n let subagentCost = 0;\n let subagentDuration = 0;\n\n for (const r of records) {\n addToBucket(all, r);\n if (r.ts >= today.since) addToBucket(today, r);\n if (r.ts >= week.since) addToBucket(week, r);\n if (r.ts >= month.since) addToBucket(month, r);\n\n modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);\n const sessKey = r.session ?? \"(ephemeral)\";\n sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);\n\n if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;\n if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;\n\n if (r.kind === \"subagent\") {\n subagentTotal += 1;\n subagentCost += r.costUsd;\n const dur = r.subagent?.durationMs ?? 0;\n subagentDuration += dur;\n const key = r.subagent?.skillName?.trim() || \"(adhoc)\";\n const prev = skillCounts.get(key) ?? { count: 0, costUsd: 0, durationMs: 0 };\n prev.count += 1;\n prev.costUsd += r.costUsd;\n prev.durationMs += dur;\n skillCounts.set(key, prev);\n }\n }\n\n const byModel = Array.from(modelCounts.entries())\n .map(([model, turns]) => ({ model, turns }))\n .sort((a, b) => b.turns - a.turns);\n const bySession = Array.from(sessionCounts.entries())\n .map(([session, turns]) => ({ session, turns }))\n .sort((a, b) => b.turns - a.turns);\n\n const subagents: SubagentAggregate | undefined =\n subagentTotal > 0\n ? {\n total: subagentTotal,\n costUsd: subagentCost,\n totalDurationMs: subagentDuration,\n bySkill: Array.from(skillCounts.entries())\n .map(([skillName, v]) => ({ skillName, ...v }))\n .sort((a, b) => b.count - a.count),\n }\n : undefined;\n\n return {\n buckets: [today, week, month, all],\n byModel,\n bySession,\n firstSeen,\n lastSeen,\n subagents,\n };\n}\n\n/** File-size helper for the stats header — \"1.2 MB\" etc. Returns \"\" if missing. */\nexport function formatLogSize(path: string = defaultUsageLogPath()): string {\n if (!existsSync(path)) return \"\";\n try {\n const s = statSync(path);\n const bytes = s.size;\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n } catch {\n return \"\";\n }\n}\n\n/** Re-exports for downstream consumers that also want the pricing constants. */\nexport { CLAUDE_SONNET_PRICING, DEEPSEEK_PRICING };\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
|
|
3
4
|
// src/adapters/event-source-jsonl.ts
|
|
4
5
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
@@ -51,4 +52,4 @@ export {
|
|
|
51
52
|
recentEventFiles,
|
|
52
53
|
readEventLogFile
|
|
53
54
|
};
|
|
54
|
-
//# sourceMappingURL=chunk-
|
|
55
|
+
//# sourceMappingURL=chunk-J5XJHLWM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/event-source-jsonl.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Event } from \"../core/events.js\";\nimport type { EventSource } from \"../ports/event-sink.js\";\nimport { eventLogPath } from \"./event-sink-jsonl.js\";\n\nconst DAY_MS = 86_400_000;\n\n/** Most-recently-modified `*.events.jsonl` files, capped + filtered by stale-mtime cutoff. */\nexport function recentEventFiles(dir: string, now: number, cap = 8, staleDays = 30): string[] {\n if (!existsSync(dir)) return [];\n let names: string[];\n try {\n names = readdirSync(dir);\n } catch {\n return [];\n }\n const cutoff = now - staleDays * DAY_MS;\n const candidates: Array<{ path: string; mtime: number }> = [];\n for (const name of names) {\n if (!name.endsWith(\".events.jsonl\")) continue;\n const path = join(dir, name);\n let mtime: number;\n try {\n mtime = statSync(path).mtimeMs;\n } catch {\n continue;\n }\n if (mtime < cutoff) continue;\n candidates.push({ path, mtime });\n }\n candidates.sort((a, b) => b.mtime - a.mtime);\n return candidates.slice(0, cap).map((c) => c.path);\n}\n\nexport function readEventLogFile(path: string): Event[] {\n if (!existsSync(path)) return [];\n const raw = readFileSync(path, \"utf8\");\n const out: Event[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const ev = JSON.parse(trimmed) as Event;\n if (ev && typeof ev === \"object\" && typeof (ev as { type?: unknown }).type === \"string\") {\n out.push(ev);\n }\n } catch {\n /* malformed mid-line write — best-effort skip */\n }\n }\n return out;\n}\n\nexport class JsonlEventSource implements EventSource {\n async *read(sessionName: string): AsyncIterable<Event> {\n const events = readEventLogFile(eventLogPath(sessionName));\n for (const ev of events) yield ev;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/event-source-jsonl.ts"],"sourcesContent":["import { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Event } from \"../core/events.js\";\nimport type { EventSource } from \"../ports/event-sink.js\";\nimport { eventLogPath } from \"./event-sink-jsonl.js\";\n\nconst DAY_MS = 86_400_000;\n\n/** Most-recently-modified `*.events.jsonl` files, capped + filtered by stale-mtime cutoff. */\nexport function recentEventFiles(dir: string, now: number, cap = 8, staleDays = 30): string[] {\n if (!existsSync(dir)) return [];\n let names: string[];\n try {\n names = readdirSync(dir);\n } catch {\n return [];\n }\n const cutoff = now - staleDays * DAY_MS;\n const candidates: Array<{ path: string; mtime: number }> = [];\n for (const name of names) {\n if (!name.endsWith(\".events.jsonl\")) continue;\n const path = join(dir, name);\n let mtime: number;\n try {\n mtime = statSync(path).mtimeMs;\n } catch {\n continue;\n }\n if (mtime < cutoff) continue;\n candidates.push({ path, mtime });\n }\n candidates.sort((a, b) => b.mtime - a.mtime);\n return candidates.slice(0, cap).map((c) => c.path);\n}\n\nexport function readEventLogFile(path: string): Event[] {\n if (!existsSync(path)) return [];\n const raw = readFileSync(path, \"utf8\");\n const out: Event[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const ev = JSON.parse(trimmed) as Event;\n if (ev && typeof ev === \"object\" && typeof (ev as { type?: unknown }).type === \"string\") {\n out.push(ev);\n }\n } catch {\n /* malformed mid-line write — best-effort skip */\n }\n }\n return out;\n}\n\nexport class JsonlEventSource implements EventSource {\n async *read(sessionName: string): AsyncIterable<Event> {\n const events = readEventLogFile(eventLogPath(sessionName));\n for (const ev of events) yield ev;\n }\n}\n"],"mappings":";;;;AAAA,SAAS,YAAY,cAAc,aAAa,gBAAgB;AAChE,SAAS,YAAY;AAKrB,IAAM,SAAS;AAGR,SAAS,iBAAiB,KAAa,KAAa,MAAM,GAAG,YAAY,IAAc;AAC5F,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO,CAAC;AAC9B,MAAI;AACJ,MAAI;AACF,YAAQ,YAAY,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,MAAM,YAAY;AACjC,QAAM,aAAqD,CAAC;AAC5D,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,SAAS,eAAe,EAAG;AACrC,UAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,QAAI;AACJ,QAAI;AACF,cAAQ,SAAS,IAAI,EAAE;AAAA,IACzB,QAAQ;AACN;AAAA,IACF;AACA,QAAI,QAAQ,OAAQ;AACpB,eAAW,KAAK,EAAE,MAAM,MAAM,CAAC;AAAA,EACjC;AACA,aAAW,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC3C,SAAO,WAAW,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI;AACnD;AAEO,SAAS,iBAAiB,MAAuB;AACtD,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,QAAM,MAAM,aAAa,MAAM,MAAM;AACrC,QAAM,MAAe,CAAC;AACtB,aAAW,QAAQ,IAAI,MAAM,OAAO,GAAG;AACrC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI;AACF,YAAM,KAAK,KAAK,MAAM,OAAO;AAC7B,UAAI,MAAM,OAAO,OAAO,YAAY,OAAQ,GAA0B,SAAS,UAAU;AACvF,YAAI,KAAK,EAAE;AAAA,MACb;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
|
|
3
4
|
// src/mcp/marketplace-overlay/loader.ts
|
|
4
5
|
import { readFileSync } from "fs";
|
|
@@ -22,4 +23,4 @@ function loadOverlay(lang) {
|
|
|
22
23
|
export {
|
|
23
24
|
loadOverlay
|
|
24
25
|
};
|
|
25
|
-
//# sourceMappingURL=chunk-
|
|
26
|
+
//# sourceMappingURL=chunk-JMBMLOBP.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mcp/marketplace-overlay/loader.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface OverlayEntry {\n title: string;\n description: string;\n}\n\nlet cache: Record<string, OverlayEntry> | null = null;\nlet cachedLang: string | null = null;\n\nexport function loadOverlay(lang: string): Record<string, OverlayEntry> | null {\n if (cachedLang === lang && cache) return cache;\n try {\n const dir = dirname(fileURLToPath(import.meta.url));\n const raw = readFileSync(join(dir, `${lang}.json`), \"utf8\");\n cache = JSON.parse(raw) as Record<string, OverlayEntry>;\n cachedLang = lang;\n return cache;\n } catch {\n return null;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/mcp/marketplace-overlay/loader.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface OverlayEntry {\n title: string;\n description: string;\n}\n\nlet cache: Record<string, OverlayEntry> | null = null;\nlet cachedLang: string | null = null;\n\nexport function loadOverlay(lang: string): Record<string, OverlayEntry> | null {\n if (cachedLang === lang && cache) return cache;\n try {\n const dir = dirname(fileURLToPath(import.meta.url));\n const raw = readFileSync(join(dir, `${lang}.json`), \"utf8\");\n cache = JSON.parse(raw) as Record<string, OverlayEntry>;\n cachedLang = lang;\n return cache;\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAO9B,IAAI,QAA6C;AACjD,IAAI,aAA4B;AAEzB,SAAS,YAAY,MAAmD;AAC7E,MAAI,eAAe,QAAQ,MAAO,QAAO;AACzC,MAAI;AACF,UAAM,MAAM,QAAQ,cAAc,YAAY,GAAG,CAAC;AAClD,UAAM,MAAM,aAAa,KAAK,KAAK,GAAG,IAAI,OAAO,GAAG,MAAM;AAC1D,YAAQ,KAAK,MAAM,GAAG;AACtB,iBAAa;AACb,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|