@agentguard-run/burn 0.2.6 → 0.3.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +69 -0
  3. package/dist/src/adapters/codex.js +6 -0
  4. package/dist/src/canvas.d.ts +36 -0
  5. package/dist/src/canvas.js +132 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +111 -25
  8. package/dist/src/defaults.d.ts +1 -1
  9. package/dist/src/defaults.js +4 -1
  10. package/dist/src/frames.d.ts +38 -0
  11. package/dist/src/frames.js +98 -0
  12. package/dist/src/gateway.js +3 -0
  13. package/dist/src/hook/pre-tool-use.d.ts +1 -0
  14. package/dist/src/hook/pre-tool-use.js +15 -2
  15. package/dist/src/idle/cache.d.ts +8 -0
  16. package/dist/src/idle/cache.js +70 -0
  17. package/dist/src/idle/classify.d.ts +13 -0
  18. package/dist/src/idle/classify.js +133 -0
  19. package/dist/src/idle/cli.d.ts +2 -0
  20. package/dist/src/idle/cli.js +72 -0
  21. package/dist/src/idle/collect.d.ts +38 -0
  22. package/dist/src/idle/collect.js +543 -0
  23. package/dist/src/idle/hook.d.ts +17 -0
  24. package/dist/src/idle/hook.js +61 -0
  25. package/dist/src/idle/platform.d.ts +25 -0
  26. package/dist/src/idle/platform.js +154 -0
  27. package/dist/src/idle/reap.d.ts +41 -0
  28. package/dist/src/idle/reap.js +262 -0
  29. package/dist/src/idle/render.d.ts +10 -0
  30. package/dist/src/idle/render.js +108 -0
  31. package/dist/src/idle/types.d.ts +56 -0
  32. package/dist/src/idle/types.js +8 -0
  33. package/dist/src/install.js +4 -2
  34. package/dist/src/policy.js +47 -1
  35. package/dist/src/presentation.d.ts +5 -0
  36. package/dist/src/presentation.js +23 -0
  37. package/dist/src/recording.d.ts +22 -0
  38. package/dist/src/recording.js +80 -0
  39. package/dist/src/render-recording.d.ts +13 -0
  40. package/dist/src/render-recording.js +193 -0
  41. package/dist/src/replay/render.js +17 -12
  42. package/dist/src/types.d.ts +6 -0
  43. package/docs/burn-idle-audit.md +71 -0
  44. package/docs/burn-render.md +37 -0
  45. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-09-20)
4
+
5
+ - Add local ps and JSON audits for Claude Code and Codex processes, leftover plugin daemons, automation browsers, workspace disk and current swap. Report held memory and cost exposure with explicit unknown evidence.
6
+ - Add interactive reap with typed row selection, SIGTERM only, a bounded wait and a result for each selection. Never delete workspaces, force-kill processes or close reap itself or its ancestors. Refuse recent terminal activity, dirty or unverified working directories and changed process identities.
7
+ - Add configurable idle-session, browser and orphan-workspace WARN thresholds, including team threshold files. SessionStart uses a five-minute metadata cache and a bounded advisory check that skips on timeout and never blocks.
8
+ - Keep existing plain STOP and WARN hook cards and no-color command bytes unchanged. Add a fixed-grid colored Canvas for terminal screens, with clipped cells, three-row block digits, bars and smaller-terminal layouts.
9
+ - Add opt-in timestamped structured frame recording for commands and offline recorded-run MP4/GIF rendering, local system fonts, numbered PNG fallback and SHA256 provenance. No audit or rendering data leaves the machine.
10
+ - Validation: 248 tests pass with no skips, including the 240-process stress test: 40 admitted, 200 denied, 240 signed receipts and zero lock failures.
11
+
12
+ ## 0.2.7 (2026-09-19)
13
+
14
+ - Fix the STOP badge padding so every card row is 66 visible characters wide. Preserve the card wording.
15
+ - Use light box-drawing glyphs for the STOP frame after checking a real Claude Code hook denial. Start the frame below the host Error prefix so its edges align.
16
+ - Test both spawn and model-call cards with ANSI colour enabled and disabled, including wrapped advice.
17
+
3
18
  ## 0.2.6 (2026-09-19)
4
19
 
5
20
  - Fix inflated enforcement usage: repeated assistant content blocks and copied child histories now share the same provider response identity as `why`. Persist only usage deltas; rebuild old counters once without replacing their receipt chain head.
package/README.md CHANGED
@@ -16,6 +16,56 @@ The two init commands print hook configuration for review. Merge the relevant sn
16
16
 
17
17
  For Claude Code, configure its `statusLine` command as `agentguard-burn statusline`. It receives the host JSON on stdin. Codex 0.154.0 has no external status command slot. Use `agentguard-burn statusline SESSION_ID` in a companion terminal and the native Codex limit indicators. See [host fields and setup](docs/USAGE_AND_PRICING.md#what-the-hosts-expose).
18
18
 
19
+ ## Local idle audit
20
+
21
+ Burn 0.3.0 reports held memory and disk cost exposure on this machine:
22
+
23
+ ```sh
24
+ agentguard-burn ps
25
+ agentguard-burn ps --json
26
+ agentguard-burn reap
27
+ ```
28
+
29
+ `ps` lists Claude Code and Codex sessions with their Node children, leftover plugin daemons, automation-owned Chrome browsers, and candidate workspaces. Process rows are ranked by resident memory, then workspace rows by disk size. Normal Chrome profiles are excluded. Browser rows show their process count, total resident memory and available window count. Unknown values are explicit. RSS can count shared memory more than once; it is not a bill.
30
+
31
+ Session idle time uses the controlling terminal's access time when readable, otherwise transcript file-write metadata. The table labels the method. A transcript fallback can warn but cannot prove that a terminal has been inactive for 24 hours. Workspace rows show size, last modification, dirty status and open-handle evidence. No transcript content or process arguments are printed or uploaded. Swap is reported only: swap is only released by the operating system on reboot. Burn does not touch it.
32
+
33
+ `reap` prints the same numbered audit, then requires an interactive terminal and numbers typed at the prompt. There is no `--yes` flag and piped confirmation is refused. It sends only SIGTERM to the selected eligible processes, waits, and reports whether they exited. It never sends SIGKILL and never deletes files or directories.
34
+
35
+ Reap refuses:
36
+
37
+ - Any session with terminal input in the last 24 hours, or unavailable terminal-idle evidence. Changing a warning threshold does not change this safety limit.
38
+ - Any process whose working directory has modified or untracked files, or whose directory safety cannot be verified.
39
+ - Every workspace, including any workspace with open file handles. Workspace rows are information only.
40
+ - The process running reap and all its ancestors, including a selected group containing one of them.
41
+ - A process whose identity, ownership or safety evidence changed while confirmation was pending. New or replaced PIDs are never added to the confirmed selection.
42
+
43
+ It rechecks the selected processes before signaling. Permission errors and processes that ignore SIGTERM are reported. They are not force-closed.
44
+
45
+ Add these fields under `thresholds` in the existing `~/.agentguard/burn-policy.json` (or `AGENTGUARD_HOME/burn-policy.json`):
46
+
47
+ ```json
48
+ {
49
+ "thresholds": {
50
+ "idle_session_warn_hours": 24,
51
+ "idle_browser_warn_minutes": 60,
52
+ "orphan_workspace_warn_days": 2
53
+ }
54
+ }
55
+ ```
56
+
57
+ Merge the fields into your existing policy. Missing or invalid values use the shipped defaults. A local `teamPolicyFile` can point to a JSON file containing `thresholds`; relative paths resolve from the Burn home. Team threshold values take precedence over local threshold values, including these three audit settings. Neither policy loading nor the audit rewrites your policy.
58
+
59
+ The Claude Code and Codex hook snippets now include SessionStart. Review the new `agentguard-burn init claude` or `agentguard-burn init codex` output to update existing installations. At session start, a fresh five-minute local audit cache is checked. A cache miss gets a bounded scan with a 150 ms budget; timeout or unavailable evidence skips the advisory. A warning is one line, never a table and never a deny:
60
+
61
+ ```text
62
+ N idle agent sessions holding X GB, run agentguard-burn ps
63
+ ```
64
+
65
+ The line is WARN-only in both shadow and enforce modes. If only browser or workspace rows cross their thresholds, the session count can be zero; run `ps` for those details. The full `ps` command refreshes the cache. All collection and caching stays local.
66
+
67
+ macOS uses local process, file-handle and swap metadata. Linux uses `/proc` and available local tools, and says what could not be read. Windows prints `not supported on this platform yet` and exits successfully. See the [docs page draft](docs/burn-idle-audit.md) for the audit boundary and limitations.
68
+
19
69
  ## Why this session
20
70
 
21
71
  ```sh
@@ -311,6 +361,25 @@ Both token limits default to `null`. Closed gateway sessions are excluded;
311
361
  hook and gateway session files are both read. WARN never blocks. STOP records
312
362
  a would-block decision in shadow and blocks new work only in enforce mode.
313
363
 
364
+ ## Terminal canvas and local recordings
365
+
366
+ Interactive screens use a fixed 104 by 35 cell canvas, with mint for good, slate for neutral, amber for WARN and red for STOP. Smaller terminals drop side detail and paginate rows without wrapping. Use --no-color for the existing plain-text commands and hook cards. Hook JSON and the one-line statusline remain host protocols, with no canvas control sequences inserted.
367
+
368
+ Any command accepts an explicit local recording path:
369
+
370
+ ```sh
371
+ agentguard-burn ps --record ./idle-run.jsonl
372
+ agentguard-burn status --no-color --record ./status-run.jsonl
373
+ agentguard-burn replay --record ./history-run.jsonl
374
+ agentguard-burn render ./idle-run.jsonl --mp4 ./idle-run.mp4 --gif ./idle-run.gif
375
+ ```
376
+
377
+ Each JSONL frame contains a wall-clock timestamp and structured observations, never rendered terminal text. Audit recordings omit raw process arguments and the full process table. They can contain local paths, PIDs, identifiers and resource measurements, so review them before sharing. Recording is opt-in and stays on disk. A recording failure prints a notice without changing a hook decision or cleanup result. Appended recordings must have nondecreasing timestamps.
378
+
379
+ The render command draws recorded Canvas cells at 1920 by 1080 using installed Menlo on macOS or DejaVu Sans Mono on Linux. The title says RECORDED RUN and the footer says recorded run. It preserves frame timestamp intervals and holds the final frame for one second. ffmpeg encodes the MP4 and optional GIF; without it, numbered PNG files and a reproducible ffmpeg command remain. No fonts or assets are fetched. The native PNG dependency loads only for render, never during session-start checks.
380
+
381
+ A provenance JSON beside the requested MP4 records the source recording's SHA256, renderer version, render time, font and frame times. Existing outputs are refused. The source recording stays unchanged. See [the render draft](docs/burn-render.md) for the format and limits.
382
+
314
383
  ## What it never does
315
384
 
316
385
  No prompts, responses, file contents, or tool inputs are persisted or rendered.
@@ -34,6 +34,7 @@ const node_fs_1 = require("node:fs");
34
34
  const events_1 = require("../events");
35
35
  const render_1 = require("../replay/render");
36
36
  const live_1 = require("../insights/live");
37
+ const hook_1 = require("../idle/hook");
37
38
  const pre_tool_use_1 = require("../hook/pre-tool-use");
38
39
  /** Fields this renderer must never emit. Codex rejects the whole object if they appear. */
39
40
  exports.CODEX_FORBIDDEN_FIELDS = ['continue', 'stopReason', 'suppressOutput', 'decision', 'reason'];
@@ -43,6 +44,10 @@ function handleCodexHook(raw, gateway, now = Date.now()) {
43
44
  const parsed = parse(raw);
44
45
  if (!parsed)
45
46
  return {};
47
+ if (parsed.event === 'SessionStart') {
48
+ const warning = (0, hook_1.sessionStartWarning)(gateway.dataDirectory, (0, pre_tool_use_1.loadPolicy)(gateway.dataDirectory), now);
49
+ return warning ? { systemMessage: warning } : {};
50
+ }
46
51
  const observation = (0, live_1.observeTool)(gateway.dataDirectory, { session_id: parsed.sessionId, transcript_path: parsed.transcriptPath, tool_name: parsed.toolName }, 'codex', (0, pre_tool_use_1.loadPolicy)(gateway.dataDirectory), now);
47
52
  const output = parsed.event === 'PreToolUse' && SPAWN_TOOLS.has(parsed.toolName) ? handleCodexSpawnHook(raw, gateway, now) : {};
48
53
  return observation.messages.length ? { ...output, systemMessage: [output.systemMessage, ...observation.messages].filter(Boolean).join('\n') } : output;
@@ -200,6 +205,7 @@ function walk(value, path) {
200
205
  function codexHooksSnippet(command) {
201
206
  return {
202
207
  hooks: {
208
+ SessionStart: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 1 }] }],
203
209
  PreToolUse: [{ matcher: '.*', hooks: [{ type: 'command', command, timeout: 15 }] }],
204
210
  },
205
211
  };
@@ -0,0 +1,36 @@
1
+ export type CanvasColor = 'mint' | 'slate' | 'amber' | 'red' | 'white' | 'none';
2
+ export type CanvasColorMode = 'truecolor' | '256' | 'none';
3
+ export interface CanvasCell {
4
+ char: string;
5
+ color: CanvasColor;
6
+ }
7
+ export interface CanvasSnapshot {
8
+ width: number;
9
+ height: number;
10
+ cells: CanvasCell[][];
11
+ }
12
+ export declare const CANVAS_PALETTE: Record<Exclude<CanvasColor, 'none'>, {
13
+ hex: string;
14
+ rgb: [number, number, number];
15
+ ansi256: number;
16
+ }>;
17
+ export declare class Canvas {
18
+ readonly width: number;
19
+ readonly height: number;
20
+ readonly colorMode: CanvasColorMode;
21
+ private readonly cells;
22
+ constructor(options?: {
23
+ width?: number;
24
+ height?: number;
25
+ colour?: boolean;
26
+ colorMode?: CanvasColorMode;
27
+ });
28
+ put(row: number, col: number, text: string, color?: CanvasColor): this;
29
+ digits(row: number, col: number, value: string | number, color?: CanvasColor): this;
30
+ bar(row: number, col: number, width: number, fraction: number, color?: CanvasColor): this;
31
+ rule(row: number, col: number, width?: number): this;
32
+ header(left?: string, right?: string): this;
33
+ footer(hints: string, clock: string): this;
34
+ toJSON(): CanvasSnapshot;
35
+ render(): string;
36
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Canvas = exports.CANVAS_PALETTE = void 0;
4
+ /** A clipped grid of terminal cells. Rendering adds only palette-owned ANSI. */
5
+ const node_util_1 = require("node:util");
6
+ exports.CANVAS_PALETTE = {
7
+ mint: { hex: '#2ABCB4', rgb: [42, 188, 180], ansi256: 37 },
8
+ slate: { hex: '#64748B', rgb: [100, 116, 139], ansi256: 66 },
9
+ amber: { hex: '#F59E0B', rgb: [245, 158, 11], ansi256: 208 },
10
+ red: { hex: '#EF4444', rgb: [239, 68, 68], ansi256: 203 },
11
+ white: { hex: '#E2E8F0', rgb: [226, 232, 240], ansi256: 254 },
12
+ };
13
+ const GLYPHS = {
14
+ '0': ['█▀█', '█ █', '█▄█'], '1': [' ▀█', ' █', ' █'],
15
+ '2': ['▀▀█', '▄▀▀', '█▄▄'], '3': ['▀▀█', ' ▀█', '▄▄█'],
16
+ '4': ['█ █', '▀▀█', ' █'], '5': ['█▀▀', '▀▀█', '▄▄█'],
17
+ '6': ['█▀▀', '█▀█', '█▄█'], '7': ['▀▀█', ' █', ' █'],
18
+ '8': ['█▀█', '█▀█', '█▄█'], '9': ['█▀█', '▀▀█', '▄▄█'],
19
+ '.': [' ', ' ', '▄'], '-': [' ', '▀▀▀', ' '],
20
+ };
21
+ const colors = new Set(['mint', 'slate', 'amber', 'red', 'white', 'none']);
22
+ function dimension(value, fallback, limit) {
23
+ return Math.max(1, Math.min(limit, Math.floor(typeof value === 'number' && Number.isFinite(value) ? value : fallback)));
24
+ }
25
+ function wide(code) {
26
+ return code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a
27
+ || (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || (code >= 0xac00 && code <= 0xd7a3)
28
+ || (code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe10 && code <= 0xfe19)
29
+ || (code >= 0xfe30 && code <= 0xfe6f) || (code >= 0xff00 && code <= 0xff60)
30
+ || (code >= 0xffe0 && code <= 0xffe6) || (code >= 0x1f000 && code <= 0x1faff)
31
+ || (code >= 0x20000 && code <= 0x3fffd));
32
+ }
33
+ /** Keep one printable column per cell; preserve bullets and box-drawing rules. */
34
+ function characters(text) {
35
+ return Array.from((0, node_util_1.stripVTControlCharacters)(String(text)).replace(/[\r\n\t]/g, ' ').replace(/[\p{Cc}\p{Cf}\p{M}]/gu, ''))
36
+ .map(char => wide(char.codePointAt(0)) ? '?' : char);
37
+ }
38
+ class Canvas {
39
+ width;
40
+ height;
41
+ colorMode;
42
+ cells;
43
+ constructor(options = {}) {
44
+ this.width = dimension(options.width, Math.min(104, process.stdout.columns || 104), 500);
45
+ this.height = dimension(options.height, Math.min(35, process.stdout.rows || 35), 200);
46
+ this.colorMode = options.colour === false ? 'none' : options.colorMode
47
+ ?? (options.colour !== true && process.env.NO_COLOR !== undefined ? 'none' : /^(truecolor|24bit)$/i.test(process.env.COLORTERM || '') ? 'truecolor' : '256');
48
+ this.cells = Array.from({ length: this.height }, () => Array.from({ length: this.width }, () => ({ char: ' ', color: 'none' })));
49
+ }
50
+ put(row, col, text, color = 'white') {
51
+ if (!Number.isFinite(row) || !Number.isFinite(col))
52
+ return this;
53
+ row = Math.floor(row);
54
+ col = Math.floor(col);
55
+ if (row < 0 || row >= this.height || col >= this.width)
56
+ return this;
57
+ const selected = colors.has(color) ? color : 'none';
58
+ for (const [index, char] of characters(text).entries()) {
59
+ const x = col + index;
60
+ if (x >= this.width)
61
+ break;
62
+ if (x >= 0)
63
+ this.cells[row][x] = { char, color: selected };
64
+ }
65
+ return this;
66
+ }
67
+ digits(row, col, value, color = 'mint') {
68
+ let x = col;
69
+ for (const char of characters(String(value))) {
70
+ const glyph = GLYPHS[char] ?? [' ', ' ', ' '];
71
+ for (let y = 0; y < 3; y++)
72
+ this.put(row + y, x, glyph[y], color);
73
+ x += glyph[0].length + 1;
74
+ if (x >= this.width)
75
+ break;
76
+ }
77
+ return this;
78
+ }
79
+ bar(row, col, width, fraction, color = 'mint') {
80
+ if (!Number.isFinite(width) || width <= 0)
81
+ return this;
82
+ const length = Math.min(500, Math.floor(width));
83
+ const ratio = Number.isFinite(fraction) ? Math.max(0, Math.min(1, fraction)) : 0;
84
+ const filled = Math.round(length * ratio);
85
+ return this.put(row, col, '█'.repeat(filled) + '░'.repeat(length - filled), color);
86
+ }
87
+ rule(row, col, width = this.width - col) {
88
+ if (!Number.isFinite(width) || width <= 0)
89
+ return this;
90
+ return this.put(row, col, '─'.repeat(Math.min(500, Math.floor(width))), 'slate');
91
+ }
92
+ header(left = 'AGENTGUARD / BURN', right = 'LIVE') {
93
+ const rightChars = characters(right).slice(0, this.width);
94
+ const start = this.width - rightChars.length;
95
+ this.put(0, 0, characters(left).slice(0, Math.max(0, start - (rightChars.length ? 2 : 0))).join(''), 'mint');
96
+ this.put(0, start, rightChars.join(''), 'slate');
97
+ if (this.height > 1)
98
+ this.rule(1, 0);
99
+ return this;
100
+ }
101
+ footer(hints, clock) {
102
+ if (this.height > 1)
103
+ this.rule(this.height - 2, 0);
104
+ const clockChars = characters(clock).slice(0, this.width), start = this.width - clockChars.length;
105
+ this.put(this.height - 1, 0, characters(hints).slice(0, Math.max(0, start - (clockChars.length ? 2 : 0))).join(''), 'slate');
106
+ return this.put(this.height - 1, start, clockChars.join(''), 'white');
107
+ }
108
+ toJSON() {
109
+ return { width: this.width, height: this.height, cells: this.cells.map(row => row.map(cell => ({ ...cell }))) };
110
+ }
111
+ render() {
112
+ return this.cells.map(row => {
113
+ if (this.colorMode === 'none')
114
+ return row.map(cell => cell.char).join('');
115
+ let current = 'none', text = '';
116
+ for (const cell of row) {
117
+ if (cell.color !== current) {
118
+ current = cell.color;
119
+ if (current === 'none')
120
+ text += '\x1b[0m';
121
+ else {
122
+ const color = exports.CANVAS_PALETTE[current];
123
+ text += this.colorMode === 'truecolor' ? `\x1b[38;2;${color.rgb.join(';')}m` : `\x1b[38;5;${color.ansi256}m`;
124
+ }
125
+ }
126
+ text += cell.char;
127
+ }
128
+ return text + (current !== 'none' ? '\x1b[0m' : '');
129
+ }).join('\n');
130
+ }
131
+ }
132
+ exports.Canvas = Canvas;
package/dist/src/cli.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  /**
3
3
  * agentguard-burn
4
4
  *
5
+ * ps local agent processes, held memory and orphan workspace disk
6
+ * reap interactively SIGTERM selected safe processes, never delete
5
7
  * replay what enforcement would have stopped, on your history
6
8
  * calibrate fit thresholds to your own usage, write shadow policy
7
9
  * status mode, shadow decisions, eligibility, every host's sessions
package/dist/src/cli.js CHANGED
@@ -3,6 +3,8 @@
3
3
  /**
4
4
  * agentguard-burn
5
5
  *
6
+ * ps local agent processes, held memory and orphan workspace disk
7
+ * reap interactively SIGTERM selected safe processes, never delete
6
8
  * replay what enforcement would have stopped, on your history
7
9
  * calibrate fit thresholds to your own usage, write shadow policy
8
10
  * status mode, shadow decisions, eligibility, every host's sessions
@@ -16,7 +18,42 @@
16
18
  * cursor-hook (internal) Cursor hook entry point
17
19
  * codex-hook (internal) Codex hook entry point
18
20
  */
21
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ var desc = Object.getOwnPropertyDescriptor(m, k);
24
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
25
+ desc = { enumerable: true, get: function() { return m[k]; } };
26
+ }
27
+ Object.defineProperty(o, k2, desc);
28
+ }) : (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ o[k2] = m[k];
31
+ }));
32
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
33
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34
+ }) : function(o, v) {
35
+ o["default"] = v;
36
+ });
37
+ var __importStar = (this && this.__importStar) || (function () {
38
+ var ownKeys = function(o) {
39
+ ownKeys = Object.getOwnPropertyNames || function (o) {
40
+ var ar = [];
41
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
42
+ return ar;
43
+ };
44
+ return ownKeys(o);
45
+ };
46
+ return function (mod) {
47
+ if (mod && mod.__esModule) return mod;
48
+ var result = {};
49
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
50
+ __setModuleDefault(result, mod);
51
+ return result;
52
+ };
53
+ })();
19
54
  Object.defineProperty(exports, "__esModule", { value: true });
55
+ const presentation_1 = require("./presentation");
56
+ const recording_1 = require("./recording");
20
57
  const node_fs_1 = require("node:fs");
21
58
  const node_os_1 = require("node:os");
22
59
  const node_path_1 = require("node:path");
@@ -37,8 +74,12 @@ const eligibility_1 = require("./eligibility");
37
74
  const sessions_1 = require("./insights/sessions");
38
75
  const transcript_1 = require("./insights/transcript");
39
76
  const live_1 = require("./insights/live");
77
+ const attribution_1 = require("./insights/attribution");
78
+ const pricing_1 = require("./insights/pricing");
79
+ const transcript_2 = require("./insights/transcript");
40
80
  const render_2 = require("./insights/render");
41
81
  const blocks_1 = require("./insights/blocks");
82
+ const cli_1 = require("./idle/cli");
42
83
  const HOME = process.env.AGENTGUARD_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.agentguard');
43
84
  const PROXY_HOSTS = ['ollama', 'vllm', 'lm-studio', 'openai-compatible'];
44
85
  function readStdinJson() {
@@ -75,23 +116,42 @@ function shadowEligibility() {
75
116
  return (0, eligibility_1.computeShadowEligibility)(readDecisions());
76
117
  }
77
118
  async function main(argv) {
78
- const [command, ...rest] = argv;
119
+ const [command, ...rawRest] = argv;
120
+ const noColor = rawRest.includes('--no-color');
121
+ const rest = rawRest.filter(value => value !== '--no-color');
122
+ const show = (data, plain, protocol = false) => (0, presentation_1.present)(data, plain, { colour: Boolean(process.stdout.isTTY) && !noColor && process.env.NO_COLOR === undefined, protocol });
79
123
  const flag = (name) => {
80
124
  const i = rest.indexOf(name);
81
125
  return i >= 0 ? rest[i + 1] : undefined;
82
126
  };
83
- const has = (name) => rest.includes(name);
127
+ const has = (name) => rawRest.includes(name);
84
128
  switch (command) {
129
+ case 'ps':
130
+ case 'reap':
131
+ return (0, cli_1.runIdleCommand)(command, rawRest, HOME);
132
+ case 'render': {
133
+ if (process.platform === 'win32') {
134
+ process.stdout.write('not supported on this platform yet\n');
135
+ return 0;
136
+ }
137
+ const source = rest[0], mp4 = flag('--mp4'), gif = flag('--gif');
138
+ if (!source || source.startsWith('--') || !mp4 || mp4.startsWith('--') || (has('--gif') && (!gif || gif.startsWith('--'))))
139
+ throw new Error('Usage: agentguard-burn render <run.jsonl> --mp4 <out.mp4> [--gif <out.gif>]');
140
+ const { renderRecording } = await Promise.resolve().then(() => __importStar(require('./render-recording')));
141
+ const result = await renderRecording(source, { mp4, gif });
142
+ show({ kind: 'command', command: 'render', values: { ...result } }, `PNG frames: ${result.framesDir}\n${result.mp4 ? `MP4: ${result.mp4}\n` : `ffmpeg unavailable. Run: ${result.ffmpegCommand}\n`}${result.gif ? `GIF: ${result.gif}\n` : ''}Provenance: ${result.provenancePath}\n`);
143
+ return 0;
144
+ }
85
145
  case 'blocks': {
86
146
  const session = rest.find(value => value !== 'json' && value !== '--json');
87
147
  const report = (0, blocks_1.blocksReport)(HOME, session, { rates: (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME)) });
88
- process.stdout.write((rest.includes('json') || has('--json') ? JSON.stringify(report, null, 2) : (0, blocks_1.renderBlocks)(report)) + '\n');
148
+ show({ kind: 'command', command, values: { ...report } }, (rest.includes('json') || has('--json') ? JSON.stringify(report, null, 2) : (0, blocks_1.renderBlocks)(report)) + '\n', rest.includes('json') || has('--json'));
89
149
  return 0;
90
150
  }
91
151
  case 'why': {
92
152
  const location = (0, sessions_1.selectInsightTranscript)(rest.find(value => value !== 'json'));
93
153
  const transcript = (0, transcript_1.readInsightSession)(location.path, { host: location.host, sessionId: location.sessionId });
94
- process.stdout.write(rest.includes('json') ? JSON.stringify(transcript, null, 2) + '\n' : (0, render_2.renderWhy)(transcript, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
154
+ show({ kind: 'command', command, values: { session: transcript.sessionId, observations: transcript.observations, diagnostics: transcript.diagnostics, attribution: (0, attribution_1.attributeTurns)((0, transcript_2.deduplicateTurns)(transcript.turns)).buckets, prices: (0, transcript_2.deduplicateTurns)(transcript.turns).map(turn => (0, pricing_1.priceTurn)(turn, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME)))) } }, rest.includes('json') ? JSON.stringify(transcript, null, 2) + '\n' : (0, render_2.renderWhy)(transcript, (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n', rest.includes('json'));
95
155
  return 0;
96
156
  }
97
157
  case 'rewrites': {
@@ -99,11 +159,12 @@ async function main(argv) {
99
159
  const locations = session === 'all' ? (0, sessions_1.discoverInsightTranscripts)() : [(0, sessions_1.selectInsightTranscript)(session)];
100
160
  const transcripts = locations.map(location => (0, transcript_1.readInsightTranscript)(location.path, { host: location.host, sessionId: location.sessionId }));
101
161
  const rates = (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME));
102
- process.stdout.write(rest.includes('json') ? JSON.stringify((0, render_2.rewriteReport)(transcripts, rates), null, 2) + '\n' : (0, render_2.renderRewrites)(transcripts, rates) + '\n');
162
+ show({ kind: 'command', command, values: (0, render_2.rewriteReport)(transcripts, rates) }, rest.includes('json') ? JSON.stringify((0, render_2.rewriteReport)(transcripts, rates), null, 2) + '\n' : (0, render_2.renderRewrites)(transcripts, rates) + '\n', rest.includes('json'));
103
163
  return 0;
104
164
  }
105
165
  case 'pricing': {
106
- process.stdout.write((0, render_2.renderPricing)((0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME))) + '\n');
166
+ const rates = (0, live_1.readPricing)(HOME, (0, pre_tool_use_1.loadPolicy)(HOME));
167
+ show({ kind: 'command', command, values: { rates: { ...pricing_1.MODEL_PRICING, ...rates } } }, (0, render_2.renderPricing)(rates) + '\n');
107
168
  return 0;
108
169
  }
109
170
  case 'statusline':
@@ -112,7 +173,7 @@ async function main(argv) {
112
173
  const payload = raw && typeof raw === 'object' ? raw : {};
113
174
  const location = (0, sessions_1.selectInsightTranscript)(payload.transcript_path || rest[0] || payload.session_id);
114
175
  const observation = (0, live_1.observeTool)(HOME, { ...payload, session_id: payload.session_id || location.sessionId, transcript_path: location.path }, location.host, (0, pre_tool_use_1.loadPolicy)(HOME));
115
- process.stdout.write((observation.snapshot ? (0, live_1.renderStatusLine)(observation.snapshot) : 'AgentGuard pace unavailable: local usage metadata is not readable.') + '\n');
176
+ show({ kind: 'command', command, values: { snapshot: observation.snapshot ? { host: observation.snapshot.host, session: observation.snapshot.sessionId, pace: observation.snapshot.pace, forecast: observation.snapshot.forecast, lastTurnTokens: observation.snapshot.lastTurnTokens, windows: observation.snapshot.windows } : null } }, (observation.snapshot ? (0, live_1.renderStatusLine)(observation.snapshot) : 'AgentGuard pace unavailable: local usage metadata is not readable.') + '\n', command === 'statusline');
116
177
  return observation.snapshot ? 0 : 1;
117
178
  }
118
179
  case 'hook': {
@@ -124,6 +185,11 @@ async function main(argv) {
124
185
  catch {
125
186
  // Unparseable payload: allow. We never break the host over our own bug.
126
187
  }
188
+ if (input?.hook_event_name === 'SessionStart') {
189
+ const warning = await (0, cli_1.prepareSessionStart)(input, HOME);
190
+ process.stdout.write(JSON.stringify(warning ? { continue: true, suppressOutput: false, systemMessage: warning } : { continue: true, suppressOutput: true }));
191
+ return 0;
192
+ }
127
193
  process.stdout.write(JSON.stringify((0, pre_tool_use_1.handlePreToolUse)(input, HOME)));
128
194
  return 0;
129
195
  }
@@ -132,12 +198,7 @@ async function main(argv) {
132
198
  const paths = rest.filter((a) => a.endsWith('.jsonl'));
133
199
  const targets = paths.length ? paths : (0, simulate_1.discoverTranscripts)();
134
200
  const summary = (0, simulate_1.replayAll)(targets, policy.thresholds, Number(flag('--min-tokens') ?? 0));
135
- if (has('--json')) {
136
- process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
137
- }
138
- else {
139
- process.stdout.write(`${(0, render_1.renderReplay)(summary, { colour: !has('--no-color'), top: Number(flag('--top') ?? 8) })}\n`);
140
- }
201
+ show({ kind: 'replay', summary }, has('--json') ? `${JSON.stringify(summary, null, 2)}\n` : `${(0, render_1.renderReplay)(summary, { colour: false, top: Number(flag('--top') ?? 8) })}\n`, has('--json'));
141
202
  return 0;
142
203
  }
143
204
  case 'calibrate': {
@@ -145,7 +206,7 @@ async function main(argv) {
145
206
  const result = (0, calibrate_1.calibrate)(summary.sessions);
146
207
  savePolicy(result.policy);
147
208
  const t = result.policy.thresholds;
148
- process.stdout.write([
209
+ show({ kind: 'command', command, values: { ...result } }, [
149
210
  `Calibrated from ${result.sessionsUsed} of ${result.sessionsTotal} sessions (${result.sessionsExcluded} excluded as pathological).`,
150
211
  ` raw p90 ${(result.rawWarnP90 / 1e9).toFixed(2)}B raw p99 ${(result.rawStopP99 / 1e9).toFixed(2)}B`,
151
212
  ` sustained WARN ${(t.sustained.warnTokens / 1e9).toFixed(2)}B STOP ${(t.sustained.stopTokens / 1e9).toFixed(2)}B`,
@@ -163,7 +224,7 @@ async function main(argv) {
163
224
  const gateway = new gateway_1.Gateway(HOME, { sign: false });
164
225
  const lc = policy.thresholds.localCompute;
165
226
  const override = (0, override_1.readOverride)(HOME);
166
- process.stdout.write([
227
+ show({ kind: 'command', command, values: { mode: policy.mode, thresholds: policy.thresholds, eligibility: e, override: override ? { active: true, once: override.once, until: override.until } : null, hosts: ['claude', 'cursor', 'codex'].map(h => { const { command: omitted, ...metadata } = (0, install_1.health)(h); return metadata; }), sessions: [...(0, status_1.readHookSessions)(HOME), ...gateway.sessions()].map(row => ({ sessionId: row.sessionId, hosts: row.hosts, tokens: row.state.totalTokens, spawns: row.state.spawnCount, liveSpawns: row.liveSpawns, capabilities: row.capabilities })) } }, [
167
228
  `mode: ${policy.mode}${policy.mode === 'enforce' ? ' (the next STOP blocks)' : ' (recording only; nothing is blocked)'}`,
168
229
  `thresholds: fan-out lifetime WARN ${policy.thresholds.fanout.warn}; STOP above ${policy.thresholds.fanout.stop} spawns in ${policy.thresholds.fanout.windowActiveMinutes ?? 120} active minutes; depth above ${policy.thresholds.fanout.maxDepth}`,
169
230
  `spawn-rate: WARN ${policy.thresholds.spawnRate?.warn ?? 8}; STOP ${policy.thresholds.spawnRate?.stop ?? 16} in ${policy.thresholds.spawnRate?.windowActiveMinutes ?? 15} active minutes; ${policy.thresholds.spawnRate?.enforce === false ? 'shadow only' : 'enforcing in enforce mode'}`,
@@ -206,7 +267,7 @@ async function main(argv) {
206
267
  process.stderr.write(`init: refusing to touch ${(0, install_1.configPath)(target)}: ${error instanceof Error ? error.message : String(error)}\n`);
207
268
  return 1;
208
269
  }
209
- process.stdout.write((result.changed ? `Wrote ${result.file}${result.backup ? ` (backup: ${result.backup})` : ''}\n` : `${result.file} already has the current hook; nothing changed.\n`) +
270
+ show({ kind: 'command', command, values: { host: target, changed: result.changed, file: result.file, mode: policy.mode } }, (result.changed ? `Wrote ${result.file}${result.backup ? ` (backup: ${result.backup})` : ''}\n` : `${result.file} already has the current hook; nothing changed.\n`) +
210
271
  ` ${result.command}\n` +
211
272
  (label ? `${label}\n` : '') +
212
273
  tail +
@@ -214,7 +275,7 @@ async function main(argv) {
214
275
  return 0;
215
276
  }
216
277
  const snippet = target === 'cursor' ? (0, cursor_1.cursorHooksSnippet)(`node ${cli} cursor-hook`) : target === 'codex' ? (0, codex_1.codexHooksSnippet)(`node ${cli} codex-hook`) : (0, pre_tool_use_1.settingsSnippet)(`node ${cli} hook`);
217
- process.stdout.write(`Add this to ${(0, install_1.configPath)(target).replace(process.env.HOME ?? '', '~')} (merge into existing "hooks"), or run: agentguard-burn init ${target} --write\n\n${JSON.stringify(snippet, null, 2)}\n\n` +
278
+ show({ kind: 'command', command, values: { host: target, config: (0, install_1.configPath)(target), snippet, mode: policy.mode } }, `Add this to ${(0, install_1.configPath)(target).replace(process.env.HOME ?? '', '~')} (merge into existing "hooks"), or run: agentguard-burn init ${target} --write\n\n${JSON.stringify(snippet, null, 2)}\n\n` +
218
279
  (label ? `${label}\n` : '') +
219
280
  tail +
220
281
  (target === 'claude' ? `Other hosts: agentguard-burn init cursor | init codex | proxy --upstream http://127.0.0.1:11434 --host ollama\n` : ''));
@@ -226,8 +287,14 @@ async function main(argv) {
226
287
  return 0;
227
288
  }
228
289
  case 'codex-hook': {
290
+ const input = readStdinJson();
291
+ if (input && typeof input === 'object' && input.hook_event_name === 'SessionStart') {
292
+ const warning = await (0, cli_1.prepareSessionStart)(input, HOME);
293
+ process.stdout.write(JSON.stringify(warning ? { systemMessage: warning } : {}));
294
+ return 0;
295
+ }
229
296
  const gateway = new gateway_1.Gateway(HOME);
230
- process.stdout.write(JSON.stringify((0, codex_1.handleCodexHook)(readStdinJson(), gateway)));
297
+ process.stdout.write(JSON.stringify((0, codex_1.handleCodexHook)(input, gateway)));
231
298
  return 0;
232
299
  }
233
300
  case 'proxy': {
@@ -251,6 +318,7 @@ async function main(argv) {
251
318
  log: (l) => process.stderr.write(`${l}\n`),
252
319
  });
253
320
  const policy = (0, pre_tool_use_1.loadPolicy)(HOME);
321
+ (0, recording_1.recordFrame)({ kind: 'command', command, values: { listening: running.address.origin, host, mode: policy.mode } });
254
322
  process.stderr.write(`agentguard-burn proxy ${running.address.origin} -> ${upstreamRaw} (${host}, ${policy.mode} mode)\n` +
255
323
  `Point your agent at ${running.address.origin}. Send x-agentguard-session: <id> so calls join a session; without it each client port is its own low-confidence session.\n` +
256
324
  `Ctrl-C to stop.\n`);
@@ -263,7 +331,7 @@ async function main(argv) {
263
331
  }
264
332
  case 'conformance': {
265
333
  const result = await (0, conformance_1.runConformance)();
266
- process.stdout.write(result.text + '\n');
334
+ show({ kind: 'command', command, values: { ok: result.ok, checks: result.checks } }, result.text + '\n');
267
335
  return result.ok ? 0 : 1;
268
336
  }
269
337
  case 'enforce': {
@@ -274,18 +342,18 @@ async function main(argv) {
274
342
  return 2;
275
343
  }
276
344
  savePolicy({ ...policy, mode: 'enforce' });
277
- process.stdout.write('Enforcement ON. The next STOP will block the spawn.\n');
345
+ show({ kind: 'command', command, values: { mode: 'enforce' } }, 'Enforcement ON. The next STOP will block the spawn.\n');
278
346
  return 0;
279
347
  }
280
348
  case 'shadow': {
281
349
  savePolicy({ ...(0, pre_tool_use_1.loadPolicy)(HOME), mode: 'shadow' });
282
- process.stdout.write('Back to shadow mode. Decisions are recorded, nothing is blocked.\n');
350
+ show({ kind: 'command', command, values: { mode: 'shadow' } }, 'Back to shadow mode. Decisions are recorded, nothing is blocked.\n');
283
351
  return 0;
284
352
  }
285
353
  case 'resume': {
286
354
  if (has('--clear')) {
287
355
  (0, override_1.clearOverride)(HOME);
288
- process.stdout.write('Override cleared. The next STOP blocks.\n');
356
+ show({ kind: 'command', command, values: { override: false } }, 'Override cleared. The next STOP blocks.\n');
289
357
  return 0;
290
358
  }
291
359
  const reason = flag('--reason');
@@ -296,13 +364,17 @@ async function main(argv) {
296
364
  const once = has('--once');
297
365
  const now = Date.now();
298
366
  (0, override_1.writeOverride)(HOME, once ? { at: now, once: true, reason } : { at: now, once: false, reason, until: now + override_1.OVERRIDE_WINDOW_MS });
299
- process.stdout.write(once
367
+ show({ kind: 'command', command, values: { once, at: now, until: once ? null : now + override_1.OVERRIDE_WINDOW_MS } }, once
300
368
  ? `Override recorded for the next STOP only, on any host: "${reason}"\n`
301
369
  : `Override recorded for the next ${override_1.OVERRIDE_WINDOW_MS / 60000} minutes, on any host: "${reason}"\n (agentguard-burn resume --clear to end it early)\n`);
302
370
  return 0;
303
371
  }
304
372
  default:
305
- process.stdout.write('agentguard-burn <blocks|why|rewrites|pace|statusline|pricing|replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
373
+ show({ kind: 'command', command: 'help', values: { commands: ['ps', 'reap', 'render', 'blocks', 'why', 'rewrites', 'pace', 'statusline', 'pricing', 'replay', 'calibrate', 'status', 'init', 'enforce', 'shadow', 'resume', 'proxy', 'conformance'] } }, 'agentguard-burn <ps|reap|render|blocks|why|rewrites|pace|statusline|pricing|replay|calibrate|status|init|enforce|shadow|resume|proxy|conformance>\n' +
374
+ ' ps [--json] local agent processes, held memory and workspace disk\n' +
375
+ ' render <run.jsonl> --mp4 <out.mp4> [--gif <out.gif>] local recorded-run export\n' +
376
+ ' --record <file.jsonl> append structured frames for any command; --no-color keeps plain output\n' +
377
+ ' reap numbered interactive SIGTERM, never delete or force-kill\n' +
306
378
  ' blocks [session|all] stored WARN/STOP receipts and that session\'s child usage\n' +
307
379
  ' why [session] token attribution and API list-price equivalents\n' +
308
380
  ' rewrites [session|all] full-prefix cache writes by cause\n' +
@@ -322,7 +394,21 @@ async function main(argv) {
322
394
  return command ? 64 : 0;
323
395
  }
324
396
  }
325
- main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
397
+ async function run() {
398
+ const parsed = (0, recording_1.recordingArguments)(process.argv.slice(2));
399
+ (0, recording_1.configureRecording)(parsed.file);
400
+ try {
401
+ const code = await main(parsed.args);
402
+ if (!(0, recording_1.recordedFrameCount)())
403
+ (0, recording_1.recordFrame)({ kind: 'command', command: parsed.args[0] ?? 'help', values: { exitCode: code } });
404
+ return code;
405
+ }
406
+ catch (error) {
407
+ (0, recording_1.recordFrame)({ kind: 'command', command: parsed.args[0] ?? 'help', values: { exitCode: 4, failed: true } });
408
+ throw error;
409
+ }
410
+ }
411
+ run().then((code) => process.exit(code), (error) => {
326
412
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
327
413
  process.exit(4);
328
414
  });
@@ -11,7 +11,7 @@ import type { Policy, Thresholds } from './types';
11
11
  * Spawn rate 8/16 over 15 active minutes warns/stops by default. Sustained
12
12
  * thresholds remain 3.5B/5B pending review of deduplicated calibration.
13
13
  * This is a dated replay snapshot and an upper bound, not labelled evidence
14
- * of false-positive rates or guaranteed savings.
14
+ * of false-positive rates or guaranteed reductions in cost exposure.
15
15
  *
16
16
  * A previous draft shipped a 50M session budget. Routine sessions on the same
17
17
  * machine run 1.4B to 3.5B, so that default would have fired within minutes on
@@ -13,7 +13,7 @@ exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_
13
13
  * Spawn rate 8/16 over 15 active minutes warns/stops by default. Sustained
14
14
  * thresholds remain 3.5B/5B pending review of deduplicated calibration.
15
15
  * This is a dated replay snapshot and an upper bound, not labelled evidence
16
- * of false-positive rates or guaranteed savings.
16
+ * of false-positive rates or guaranteed reductions in cost exposure.
17
17
  *
18
18
  * A previous draft shipped a 50M session budget. Routine sessions on the same
19
19
  * machine run 1.4B to 3.5B, so that default would have fired within minutes on
@@ -25,6 +25,9 @@ exports.SHADOW_MIN_DAYS = exports.SHADOW_MIN_DECISIONS = exports.ACTIVE_GAP_CAP_
25
25
  * absent from these thresholds.
26
26
  */
27
27
  exports.DEFAULT_THRESHOLDS = {
28
+ idle_session_warn_hours: 24,
29
+ idle_browser_warn_minutes: 60,
30
+ orphan_workspace_warn_days: 2,
28
31
  fanout: { warn: 24, stop: 40, maxDepth: 2, windowActiveMinutes: 120 },
29
32
  sustained: { warnTokens: 3_500_000_000, stopTokens: 5_000_000_000 },
30
33
  burnDebt: {