@ironbee-ai/cli 0.11.4 → 0.12.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 (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +14 -1
  3. package/dist/clients/claude/hooks/session-start.d.ts.map +1 -1
  4. package/dist/clients/claude/hooks/session-start.js +7 -0
  5. package/dist/clients/claude/hooks/session-start.js.map +1 -1
  6. package/dist/clients/claude/hooks/session-status.d.ts +129 -0
  7. package/dist/clients/claude/hooks/session-status.d.ts.map +1 -0
  8. package/dist/clients/claude/hooks/session-status.js +444 -0
  9. package/dist/clients/claude/hooks/session-status.js.map +1 -0
  10. package/dist/clients/claude/index.d.ts +26 -0
  11. package/dist/clients/claude/index.d.ts.map +1 -1
  12. package/dist/clients/claude/index.js +146 -0
  13. package/dist/clients/claude/index.js.map +1 -1
  14. package/dist/commands/config.d.ts +5 -4
  15. package/dist/commands/config.d.ts.map +1 -1
  16. package/dist/commands/config.js +10 -4
  17. package/dist/commands/config.js.map +1 -1
  18. package/dist/commands/hook.js +44 -0
  19. package/dist/commands/hook.js.map +1 -1
  20. package/dist/commands/statusline-toggle.d.ts +29 -0
  21. package/dist/commands/statusline-toggle.d.ts.map +1 -0
  22. package/dist/commands/statusline-toggle.js +114 -0
  23. package/dist/commands/statusline-toggle.js.map +1 -0
  24. package/dist/commands/statusline.d.ts +24 -0
  25. package/dist/commands/statusline.d.ts.map +1 -0
  26. package/dist/commands/statusline.js +79 -0
  27. package/dist/commands/statusline.js.map +1 -0
  28. package/dist/hooks/core/session-state.d.ts +25 -0
  29. package/dist/hooks/core/session-state.d.ts.map +1 -1
  30. package/dist/hooks/core/session-state.js +49 -1
  31. package/dist/hooks/core/session-state.js.map +1 -1
  32. package/dist/index.js +2 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/lib/config.d.ts +64 -0
  35. package/dist/lib/config.d.ts.map +1 -1
  36. package/dist/lib/config.js +48 -0
  37. package/dist/lib/config.js.map +1 -1
  38. package/dist/lib/event.d.ts +60 -0
  39. package/dist/lib/event.d.ts.map +1 -1
  40. package/dist/lib/event.js +1 -0
  41. package/dist/lib/event.js.map +1 -1
  42. package/dist/lib/gitignore.d.ts.map +1 -1
  43. package/dist/lib/gitignore.js +11 -0
  44. package/dist/lib/gitignore.js.map +1 -1
  45. package/dist/lib/install-snapshots.d.ts +70 -0
  46. package/dist/lib/install-snapshots.d.ts.map +1 -0
  47. package/dist/lib/install-snapshots.js +168 -0
  48. package/dist/lib/install-snapshots.js.map +1 -0
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 (2026-05-23)
4
+
5
+ ### Features
6
+
7
+ * **statusline:** add session_status events via a chained statusline wrapper ([#16](https://github.com/ironbee-ai/ironbee-cli/issues/16)) ([a0a5a10](https://github.com/ironbee-ai/ironbee-cli/commit/a0a5a109d9691531711ec1d20ecc5d176aafcc15))
8
+
3
9
  ## 0.11.4 (2026-05-21)
4
10
 
5
11
  ### Features
package/README.md CHANGED
@@ -159,6 +159,18 @@ Turns off enforcement but keeps the telemetry path intact. Session lifecycle and
159
159
 
160
160
  The toggle re-renders all client artifacts (hooks, skill, rule, MCP servers, permissions) atomically. The change takes effect on the next agent session — restart your editor / agent after toggling.
161
161
 
162
+ ### Optional: statusline `session_status` events (Claude)
163
+
164
+ ```bash
165
+ ironbee statusline enable
166
+ ```
167
+
168
+ Wraps your Claude Code statusline so that, on every statusline tick, IronBee emits a `session_status` event carrying live **context-window size**, **cost**, and **subscription rate-limit utilization** — signals that are unavailable to hooks or the transcript and can only be read from the statusline. Your existing statusline keeps rendering exactly as before: IronBee installs its wrapper into `.claude/settings.local.json` (the highest-priority, gitignored settings layer) and **chains** to whatever statusline you already had (committed project, global, or local), so the display is unchanged — we just piggyback the telemetry.
169
+
170
+ Auto-enabled when a collector is configured (same as analytics); otherwise opt in with `ironbee statusline enable` or `statusLine.enable: true`. `ironbee statusline disable` restores your original statusline and stops the events. If you change your upstream statusline mid-session, `ironbee statusline sync` re-resolves it for active sessions (otherwise it's picked up on the next session). The change takes effect on the next agent session — restart your editor after toggling.
171
+
172
+ > Claude-only — Cursor has no equivalent statusline mechanism. The events ship through the same queue + collector pipeline as everything else; nothing is written when no collector is configured.
173
+
162
174
  ### Cursor: additional setup
163
175
 
164
176
  Cursor requires manual activation of MCP servers after install:
@@ -187,6 +199,7 @@ ironbee browser <enable|disable> [-g|--local] [--client <name>] Manage the b
187
199
  ironbee backend <enable|disable> [-g|--local] [--client <name>] Manage the runtime-agnostic backend protocol cycle (HTTP/gRPC/GraphQL/WS via backend-devtools)
188
200
  ironbee node <enable|disable> [-g|--local] [--client <name>] Manage the Node.js runtime debug cycle (V8 inspector probes via node-devtools)
189
201
  ironbee verification <enable|disable> [-g|--local] [--client <name>] Master verification toggle (enable = enforce; disable = monitoring-only, no enforcement but sessions/tools still ship to collector)
202
+ ironbee statusline <enable|disable|sync> [-g|--local] [--client <name>] Manage the Claude statusline integration (emits session_status events — context-window size, cost, subscription rate-limits — and chains your existing statusline). sync re-resolves the chained statusline for active sessions
190
203
  ironbee config get <key> [-g|--project|--local] Read a config value (default: merged effective value; flags narrow to one of the three layers)
191
204
  ironbee config set <key> <value> [-g|--local] [--client <name>] [--no-rerender] [--json] [--apply-all|--no-apply-all] Write a config value; auto re-renders client artifacts on artifact-affecting keys; -g writes global, --local writes project-local (gitignored)
192
205
  ironbee config unset <key> [-g|--local] [--client <name>] [--no-rerender] [--apply-all|--no-apply-all] Remove a config value (idempotent); same target / rerender rules as set
@@ -320,7 +333,7 @@ Target flags are mutually exclusive: pass at most one of `-g/--global`, `--proje
320
333
 
321
334
  **Type coercion** — `set` parses the value as JSON when it can (`true`/`42`/`[…]`/`{…}`) and falls back to a raw string when JSON parse fails. URLs and paths pass through unquoted; pass `--json` to force strict JSON parsing (e.g. when you want the literal string `"42"` instead of the number `42`).
322
335
 
323
- **Smart artifact re-render** — when a top-level key affects installed client artifacts (`verification`, `collector`, `browser`, `backend`, `node`, `browserDevTools`, `backendDevTools`, `nodeDevTools`), `set` and `unset` re-render the client files (hooks, MCP entries, skill, rule, permissions) automatically — same code path `verification enable` / `backend enable` / `node enable` use. Other keys (`maxRetries`, `recording`, `jobQueue`, `analytics`, `import`, `ignoredVerifyPatterns`) are pure config flips that the next agent session picks up — no rerender needed.
336
+ **Smart artifact re-render** — when a top-level key affects installed client artifacts (`verification`, `telemetry`, `statusLine`, `collector`, `browser`, `backend`, `node`, `browserDevTools`, `backendDevTools`, `nodeDevTools`), `set` and `unset` re-render the client files (hooks, MCP entries, skill, rule, permissions, statusline) automatically — same code path `verification enable` / `backend enable` / `node enable` use. Other keys (`maxRetries`, `recording`, `jobQueue`, `analytics`, `import`, `ignoredVerifyPatterns`) are pure config flips that the next agent session picks up — no rerender needed.
324
337
 
325
338
  Pass `--no-rerender` to skip the rerender on artifact-affecting keys (handy for scripted bulk edits — follow up with `ironbee install` to resync). If a rerender fails midway, the config file is rolled back to its prior bytes so disk state never diverges from installed artifacts.
326
339
 
@@ -1 +1 @@
1
- {"version":3,"file":"session-start.d.ts","sourceRoot":"","sources":["../../../../src/clients/claude/hooks/session-start.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,wBAAsB,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0F3D"}
1
+ {"version":3,"file":"session-start.d.ts","sourceRoot":"","sources":["../../../../src/clients/claude/hooks/session-start.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiBH,wBAAsB,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG3D"}
@@ -13,6 +13,7 @@ const actions_1 = require("../../../hooks/core/actions");
13
13
  const session_state_1 = require("../../../hooks/core/session-state");
14
14
  const util_1 = require("../util");
15
15
  const config_1 = require("../../../lib/config");
16
+ const session_status_1 = require("./session-status");
16
17
  const logger_1 = require("../../../lib/logger");
17
18
  const output_1 = require("../../../lib/output");
18
19
  const stdin_1 = require("../../../lib/stdin");
@@ -38,6 +39,12 @@ async function run(projectDir) {
38
39
  // them on next start.
39
40
  (0, session_state_1.setUserEmail)(sessionDir, (0, util_1.getClaudeUserEmail)());
40
41
  (0, session_state_1.setUsage)(sessionDir, (0, util_1.resolveClaudeUsage)());
42
+ // Resolve + cache the chained statusline command once per session so the
43
+ // statusline wrapper reads a single state.json field per tick instead of
44
+ // walking the settings cascade. Gated on the feature; harmless when off.
45
+ if ((0, config_1.isSessionStatusEnabled)((0, config_1.loadConfig)(projectDir))) {
46
+ (0, session_state_1.setChainedStatusLine)(sessionDir, (0, session_status_1.resolveChainTarget)(projectDir) ?? null);
47
+ }
41
48
  const entry = {
42
49
  ...(0, actions_1.baseFields)(actionsFile),
43
50
  type: "session_start",
@@ -1 +1 @@
1
- {"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../../../src/clients/claude/hooks/session-start.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-start.js","sourceRoot":"","sources":["../../../../src/clients/claude/hooks/session-start.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Claude Code — statusline wrapper (`ironbee hook session-status`)
3
+ *
4
+ * Installed as the project's `statusLine.command`. On every statusline tick
5
+ * it does two independent things:
6
+ * 1. Submits a `session_status` event (context-window size, cost,
7
+ * subscription rate-limits) to the queue — signals unavailable to hooks
8
+ * or the transcript.
9
+ * 2. Chains to the user's ORIGINAL statusline command so their statusline
10
+ * keeps rendering exactly as before (we wrap, never replace).
11
+ *
12
+ * The event submit is fire-and-forget and must NEVER block or break the
13
+ * statusline render. The chain target is resolved once at SessionStart and
14
+ * cached in state.json; this wrapper falls back to live cascade resolution
15
+ * if the cache is missing/unreadable, so a cache miss can never make the
16
+ * user's statusline vanish.
17
+ */
18
+ import { SessionStatusEvent } from "../../../lib/event";
19
+ /** Subset of the statusline stdin JSON this wrapper consumes. */
20
+ interface StatusLinePayload {
21
+ session_id?: string;
22
+ model?: {
23
+ id?: string;
24
+ };
25
+ workspace?: {
26
+ project_dir?: string;
27
+ };
28
+ cost?: {
29
+ total_cost_usd?: number;
30
+ total_duration_ms?: number;
31
+ total_api_duration_ms?: number;
32
+ total_lines_added?: number;
33
+ total_lines_removed?: number;
34
+ };
35
+ context_window?: {
36
+ total_input_tokens?: number;
37
+ total_output_tokens?: number;
38
+ context_window_size?: number;
39
+ used_percentage?: number;
40
+ remaining_percentage?: number;
41
+ current_usage?: {
42
+ input_tokens?: number;
43
+ output_tokens?: number;
44
+ cache_creation_input_tokens?: number;
45
+ cache_read_input_tokens?: number;
46
+ } | null;
47
+ };
48
+ rate_limits?: {
49
+ five_hour?: {
50
+ used_percentage?: number;
51
+ resets_at?: number;
52
+ };
53
+ seven_day?: {
54
+ used_percentage?: number;
55
+ resets_at?: number;
56
+ };
57
+ };
58
+ }
59
+ /**
60
+ * True only when the command is EXACTLY our bare wrapper invocation. A tight
61
+ * match so a user who wraps us themselves (`tee >(ironbee hook session-status)
62
+ * | real-cmd`) is NOT mis-identified as "ours" — that compound is their own
63
+ * statusline and must be chained, not skipped.
64
+ */
65
+ export declare function isIronbeeStatusLine(command: string | undefined): boolean;
66
+ /**
67
+ * Resolve the project dir anchor. Prefers `CLAUDE_PROJECT_DIR` to stay
68
+ * consistent with every other hook and with SessionStart (session runtime
69
+ * files — state.json, queue — live under exactly this path), falling back to
70
+ * the statusline's `workspace.project_dir` and finally cwd. NOT canonicalized
71
+ * — the snapshot store canonicalizes internally on lookup, and the session
72
+ * dir must match the raw path SessionStart wrote under.
73
+ */
74
+ export declare function resolveProjectDir(input: StatusLinePayload): string;
75
+ /**
76
+ * Resolve the chain target = the statusline command Claude Code WOULD run
77
+ * without us = the highest-priority non-ours `statusLine.command` across the
78
+ * settings layers, walking high → low:
79
+ * 1. localSettings layer — our wrapper overwrote it, so the original lives
80
+ * in the install snapshot.
81
+ * 2. projectSettings (`.claude/settings.json`) — untouched, read live.
82
+ * 3. userSettings (`~/.claude/settings.json`) — untouched, read live.
83
+ * First non-ours wins. `undefined` = nothing to chain.
84
+ */
85
+ export declare function resolveChainTarget(projectDir: string): string | undefined;
86
+ /**
87
+ * Strip the bracketed runtime suffix from the model id
88
+ * (`claude-opus-4-7[1m]` → `claude-opus-4-7`). The `[1m]` extended-context
89
+ * marker is redundant on the event — `context_window.context_window_size`
90
+ * already carries the effective window — so we keep the model field a clean
91
+ * canonical id.
92
+ */
93
+ export declare function normalizeModelId(id: string | undefined): string;
94
+ /**
95
+ * Build the wire event. Maps fields EXPLICITLY from the statusline JSON (R12)
96
+ * so our wire contract is independent of host JSON drift. `model` is the flat
97
+ * id string. `actionsFile` is only the derivation anchor for `baseFields`
98
+ * (id/session_id/project_name + user/usage) — we never write to it.
99
+ */
100
+ export declare function buildSessionStatusEvent(input: StatusLinePayload, projectDir: string, sessionId: string): SessionStatusEvent;
101
+ /**
102
+ * Deterministic per-tick id. `total_duration_ms` is monotonic within a
103
+ * session (`Date.now() - startTime`), so each tick gets a distinct id and the
104
+ * backend keeps every tick; a cancelled-then-refired identical tick collides
105
+ * on the same id (dedup). UUID-shaped via the same hashing convention as the
106
+ * analytics deterministic ids.
107
+ */
108
+ export declare function deriveSessionStatusEventId(sessionId: string, totalDurationMs: number): string;
109
+ /**
110
+ * Signature over the RESOURCE-METRIC fields of the event — the fields that
111
+ * only change on an API response. Deliberately EXCLUDES:
112
+ * - `cost.total_duration` / `total_api_duration` — wall-clock durations that
113
+ * tick up on every statusline fire (including them would make the
114
+ * signature differ every tick, defeating the dedup).
115
+ * - `id` / `timestamp` — per-emit, not metrics.
116
+ * - `activity_id` — correlation metadata, not a resource metric (a new turn
117
+ * with frozen metrics shouldn't re-emit; the next real metric change
118
+ * captures the new activity_id).
119
+ * Used by skip-if-unchanged: two ticks with the same signature carry the same
120
+ * resource snapshot, so the later one is a redundant emit and is skipped.
121
+ */
122
+ export declare function computeSessionStatusSignature(e: SessionStatusEvent): string;
123
+ /**
124
+ * Statusline entry point. Reads stdin, submits the event (gated, fire-and-
125
+ * forget), then chains the user's original statusline.
126
+ */
127
+ export declare function runSessionStatus(): Promise<void>;
128
+ export {};
129
+ //# sourceMappingURL=session-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-status.d.ts","sourceRoot":"","sources":["../../../../src/clients/claude/hooks/session-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAeH,OAAO,EAAuC,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAO7F,iEAAiE;AACjE,UAAU,iBAAiB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxB,SAAS,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,IAAI,CAAC,EAAE;QACH,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;IACF,cAAc,CAAC,EAAE;QACb,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,aAAa,CAAC,EAAE;YACZ,YAAY,CAAC,EAAE,MAAM,CAAC;YACtB,aAAa,CAAC,EAAE,MAAM,CAAC;YACvB,2BAA2B,CAAC,EAAE,MAAM,CAAC;YACrC,uBAAuB,CAAC,EAAE,MAAM,CAAC;SACpC,GAAG,IAAI,CAAC;KACZ,CAAC;IACF,WAAW,CAAC,EAAE;QACV,SAAS,CAAC,EAAE;YAAE,eAAe,CAAC,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7D,SAAS,CAAC,EAAE;YAAE,eAAe,CAAC,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAChE,CAAC;CACL;AAID;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAIlE;AAwBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAczE;AAMD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE/D;AA+BD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,kBAAkB,CA6C3H;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAK7F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAAC,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAY3E;AAgLD;;;GAGG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+CtD"}
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ /**
3
+ * Claude Code — statusline wrapper (`ironbee hook session-status`)
4
+ *
5
+ * Installed as the project's `statusLine.command`. On every statusline tick
6
+ * it does two independent things:
7
+ * 1. Submits a `session_status` event (context-window size, cost,
8
+ * subscription rate-limits) to the queue — signals unavailable to hooks
9
+ * or the transcript.
10
+ * 2. Chains to the user's ORIGINAL statusline command so their statusline
11
+ * keeps rendering exactly as before (we wrap, never replace).
12
+ *
13
+ * The event submit is fire-and-forget and must NEVER block or break the
14
+ * statusline render. The chain target is resolved once at SessionStart and
15
+ * cached in state.json; this wrapper falls back to live cascade resolution
16
+ * if the cache is missing/unreadable, so a cache miss can never make the
17
+ * user's statusline vanish.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.isIronbeeStatusLine = isIronbeeStatusLine;
21
+ exports.resolveProjectDir = resolveProjectDir;
22
+ exports.resolveChainTarget = resolveChainTarget;
23
+ exports.normalizeModelId = normalizeModelId;
24
+ exports.buildSessionStatusEvent = buildSessionStatusEvent;
25
+ exports.deriveSessionStatusEventId = deriveSessionStatusEventId;
26
+ exports.computeSessionStatusSignature = computeSessionStatusSignature;
27
+ exports.runSessionStatus = runSessionStatus;
28
+ const child_process_1 = require("child_process");
29
+ const crypto_1 = require("crypto");
30
+ const fs_1 = require("fs");
31
+ const os_1 = require("os");
32
+ const path_1 = require("path");
33
+ const install_snapshots_1 = require("../../../lib/install-snapshots");
34
+ const config_1 = require("../../../lib/config");
35
+ const logger_1 = require("../../../lib/logger");
36
+ const stdin_1 = require("../../../lib/stdin");
37
+ const queue_1 = require("../../../queue");
38
+ const actions_1 = require("../../../hooks/core/actions");
39
+ const session_state_1 = require("../../../hooks/core/session-state");
40
+ const CLIENT = "claude";
41
+ /**
42
+ * True only when the command is EXACTLY our bare wrapper invocation. A tight
43
+ * match so a user who wraps us themselves (`tee >(ironbee hook session-status)
44
+ * | real-cmd`) is NOT mis-identified as "ours" — that compound is their own
45
+ * statusline and must be chained, not skipped.
46
+ */
47
+ function isIronbeeStatusLine(command) {
48
+ if (typeof command !== "string") {
49
+ return false;
50
+ }
51
+ return /^\s*ironbee\s+hook\s+session-status(\s+--client\s+\S+)?\s*$/.test(command);
52
+ }
53
+ /**
54
+ * Resolve the project dir anchor. Prefers `CLAUDE_PROJECT_DIR` to stay
55
+ * consistent with every other hook and with SessionStart (session runtime
56
+ * files — state.json, queue — live under exactly this path), falling back to
57
+ * the statusline's `workspace.project_dir` and finally cwd. NOT canonicalized
58
+ * — the snapshot store canonicalizes internally on lookup, and the session
59
+ * dir must match the raw path SessionStart wrote under.
60
+ */
61
+ function resolveProjectDir(input) {
62
+ return process.env.CLAUDE_PROJECT_DIR
63
+ ?? input.workspace?.project_dir
64
+ ?? process.cwd();
65
+ }
66
+ /** Read `statusLine.command` from a settings.json file; undefined on any failure. */
67
+ function readStatusLineCommand(settingsPath) {
68
+ if (!(0, fs_1.existsSync)(settingsPath)) {
69
+ return undefined;
70
+ }
71
+ try {
72
+ const parsed = JSON.parse((0, fs_1.readFileSync)(settingsPath, "utf-8"));
73
+ if (parsed === null || typeof parsed !== "object") {
74
+ return undefined;
75
+ }
76
+ const sl = parsed.statusLine;
77
+ if (sl === null || typeof sl !== "object") {
78
+ return undefined;
79
+ }
80
+ const cmd = sl.command;
81
+ return typeof cmd === "string" && cmd.length > 0 ? cmd : undefined;
82
+ }
83
+ catch (e) {
84
+ logger_1.logger.debug(`session-status: failed to read statusLine from ${settingsPath}: ${e instanceof Error ? e.message : e}`);
85
+ return undefined;
86
+ }
87
+ }
88
+ /**
89
+ * Resolve the chain target = the statusline command Claude Code WOULD run
90
+ * without us = the highest-priority non-ours `statusLine.command` across the
91
+ * settings layers, walking high → low:
92
+ * 1. localSettings layer — our wrapper overwrote it, so the original lives
93
+ * in the install snapshot.
94
+ * 2. projectSettings (`.claude/settings.json`) — untouched, read live.
95
+ * 3. userSettings (`~/.claude/settings.json`) — untouched, read live.
96
+ * First non-ours wins. `undefined` = nothing to chain.
97
+ */
98
+ function resolveChainTarget(projectDir) {
99
+ const snapshot = (0, install_snapshots_1.readStatusLineSnapshot)(projectDir, CLIENT)?.command;
100
+ if (snapshot && !isIronbeeStatusLine(snapshot)) {
101
+ return snapshot;
102
+ }
103
+ const projectCmd = readStatusLineCommand((0, path_1.join)(projectDir, ".claude", "settings.json"));
104
+ if (projectCmd && !isIronbeeStatusLine(projectCmd)) {
105
+ return projectCmd;
106
+ }
107
+ const userCmd = readStatusLineCommand((0, path_1.join)((0, os_1.homedir)(), ".claude", "settings.json"));
108
+ if (userCmd && !isIronbeeStatusLine(userCmd)) {
109
+ return userCmd;
110
+ }
111
+ return undefined;
112
+ }
113
+ function num(value) {
114
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
115
+ }
116
+ /**
117
+ * Strip the bracketed runtime suffix from the model id
118
+ * (`claude-opus-4-7[1m]` → `claude-opus-4-7`). The `[1m]` extended-context
119
+ * marker is redundant on the event — `context_window.context_window_size`
120
+ * already carries the effective window — so we keep the model field a clean
121
+ * canonical id.
122
+ */
123
+ function normalizeModelId(id) {
124
+ return (id ?? "").replace(/\[[^\]]*\]/g, "").trim();
125
+ }
126
+ /** Map the statusline's `current_usage` explicitly (R12); null passes through. */
127
+ function mapCurrentUsage(usage) {
128
+ if (usage === null || usage === undefined) {
129
+ return null;
130
+ }
131
+ return {
132
+ input_tokens: num(usage.input_tokens),
133
+ output_tokens: num(usage.output_tokens),
134
+ cache_creation_input_tokens: num(usage.cache_creation_input_tokens),
135
+ cache_read_input_tokens: num(usage.cache_read_input_tokens),
136
+ };
137
+ }
138
+ function mapRateLimitWindow(w) {
139
+ if (!w || typeof w.used_percentage !== "number" || typeof w.resets_at !== "number") {
140
+ return undefined;
141
+ }
142
+ // The statusline reports `resets_at` in epoch seconds; convert to ms to
143
+ // match every other IronBee timestamp.
144
+ return { used_percentage: w.used_percentage, resets_at: w.resets_at * 1000 };
145
+ }
146
+ /**
147
+ * Build the wire event. Maps fields EXPLICITLY from the statusline JSON (R12)
148
+ * so our wire contract is independent of host JSON drift. `model` is the flat
149
+ * id string. `actionsFile` is only the derivation anchor for `baseFields`
150
+ * (id/session_id/project_name + user/usage) — we never write to it.
151
+ */
152
+ function buildSessionStatusEvent(input, projectDir, sessionId) {
153
+ const sessionDir = (0, path_1.join)(projectDir, ".ironbee", "sessions", sessionId);
154
+ const actionsFile = (0, path_1.join)(sessionDir, "actions.jsonl");
155
+ const cw = input.context_window ?? {};
156
+ const cost = input.cost ?? {};
157
+ const rl = mapRateLimitWindow(input.rate_limits?.five_hour);
158
+ const rl7 = mapRateLimitWindow(input.rate_limits?.seven_day);
159
+ // The active activity in flight at this tick (torn-read-safe via readState's
160
+ // try/catch). Empty string when no activity is open — e.g. a tick between
161
+ // turns or before the first user prompt.
162
+ const state = (0, session_state_1.readState)(sessionDir);
163
+ const event = {
164
+ ...(0, actions_1.baseFields)(actionsFile),
165
+ id: deriveSessionStatusEventId(sessionId, num(cost.total_duration_ms)),
166
+ type: "session_status",
167
+ timestamp: Date.now(),
168
+ activity_id: state.activeActivityId ?? "",
169
+ model: normalizeModelId(input.model?.id),
170
+ cost: {
171
+ total_cost_usd: num(cost.total_cost_usd),
172
+ // Host fields carry the `_ms` suffix; our wire shape drops it (ms implied).
173
+ total_duration: num(cost.total_duration_ms),
174
+ total_api_duration: num(cost.total_api_duration_ms),
175
+ total_lines_added: num(cost.total_lines_added),
176
+ total_lines_removed: num(cost.total_lines_removed),
177
+ },
178
+ context_window: {
179
+ total_input_tokens: num(cw.total_input_tokens),
180
+ total_output_tokens: num(cw.total_output_tokens),
181
+ context_window_size: num(cw.context_window_size),
182
+ used_percentage: num(cw.used_percentage),
183
+ remaining_percentage: num(cw.remaining_percentage),
184
+ current_usage: mapCurrentUsage(cw.current_usage),
185
+ },
186
+ };
187
+ if (rl || rl7) {
188
+ event.rate_limits = {};
189
+ if (rl) {
190
+ event.rate_limits.five_hour = rl;
191
+ }
192
+ if (rl7) {
193
+ event.rate_limits.seven_day = rl7;
194
+ }
195
+ }
196
+ return event;
197
+ }
198
+ /**
199
+ * Deterministic per-tick id. `total_duration_ms` is monotonic within a
200
+ * session (`Date.now() - startTime`), so each tick gets a distinct id and the
201
+ * backend keeps every tick; a cancelled-then-refired identical tick collides
202
+ * on the same id (dedup). UUID-shaped via the same hashing convention as the
203
+ * analytics deterministic ids.
204
+ */
205
+ function deriveSessionStatusEventId(sessionId, totalDurationMs) {
206
+ const hex = (0, crypto_1.createHash)("sha256")
207
+ .update(`session_status:${sessionId}:${totalDurationMs}`)
208
+ .digest("hex");
209
+ return formatHexAsUuid(hex);
210
+ }
211
+ /**
212
+ * Signature over the RESOURCE-METRIC fields of the event — the fields that
213
+ * only change on an API response. Deliberately EXCLUDES:
214
+ * - `cost.total_duration` / `total_api_duration` — wall-clock durations that
215
+ * tick up on every statusline fire (including them would make the
216
+ * signature differ every tick, defeating the dedup).
217
+ * - `id` / `timestamp` — per-emit, not metrics.
218
+ * - `activity_id` — correlation metadata, not a resource metric (a new turn
219
+ * with frozen metrics shouldn't re-emit; the next real metric change
220
+ * captures the new activity_id).
221
+ * Used by skip-if-unchanged: two ticks with the same signature carry the same
222
+ * resource snapshot, so the later one is a redundant emit and is skipped.
223
+ */
224
+ function computeSessionStatusSignature(e) {
225
+ const source = {
226
+ model: e.model,
227
+ context_window: e.context_window,
228
+ cost: {
229
+ total_cost_usd: e.cost.total_cost_usd,
230
+ total_lines_added: e.cost.total_lines_added,
231
+ total_lines_removed: e.cost.total_lines_removed,
232
+ },
233
+ rate_limits: e.rate_limits,
234
+ };
235
+ return (0, crypto_1.createHash)("sha256").update(JSON.stringify(source)).digest("hex");
236
+ }
237
+ /** Path of the per-session statusline state file (single writer: this wrapper). */
238
+ function statuslineStatePath(projectDir, sessionId) {
239
+ return (0, path_1.join)(projectDir, ".ironbee", "sessions", sessionId, "statusline-state.json");
240
+ }
241
+ function readStatuslineState(path) {
242
+ if (!(0, fs_1.existsSync)(path)) {
243
+ return {};
244
+ }
245
+ try {
246
+ const parsed = JSON.parse((0, fs_1.readFileSync)(path, "utf-8"));
247
+ if (parsed === null || typeof parsed !== "object") {
248
+ return {};
249
+ }
250
+ const obj = parsed;
251
+ return {
252
+ sig: typeof obj.sig === "string" ? obj.sig : undefined,
253
+ lastEmitMs: typeof obj.lastEmitMs === "number" ? obj.lastEmitMs : undefined,
254
+ };
255
+ }
256
+ catch (e) {
257
+ logger_1.logger.debug(`session-status: failed to read statusline state ${path}: ${e instanceof Error ? e.message : e}`);
258
+ return {};
259
+ }
260
+ }
261
+ /** Atomic write (tmp+rename). Single writer, so no lost-update concern. */
262
+ function writeStatuslineState(path, state) {
263
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
264
+ try {
265
+ (0, fs_1.writeFileSync)(tmp, JSON.stringify(state));
266
+ (0, fs_1.renameSync)(tmp, path);
267
+ }
268
+ catch (e) {
269
+ try {
270
+ if ((0, fs_1.existsSync)(tmp)) {
271
+ (0, fs_1.unlinkSync)(tmp);
272
+ }
273
+ }
274
+ catch {
275
+ // best-effort cleanup
276
+ }
277
+ logger_1.logger.debug(`session-status: failed to write statusline state ${path}: ${e instanceof Error ? e.message : e}`);
278
+ }
279
+ }
280
+ /** Format a hex digest as a UUID-shaped string (8-4-4-4-12). */
281
+ function formatHexAsUuid(hex) {
282
+ const h = hex.padEnd(32, "0").slice(0, 32);
283
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
284
+ }
285
+ /**
286
+ * Submit the session_status event. Gated on `isSessionStatusEnabled` and on
287
+ * the session being IronBee-tracked. Skips empty pre-first-response ticks (no
288
+ * usage yet) AND content-identical ticks (skip-if-unchanged): the statusline
289
+ * fires far more often than API responses (vim toggles, permission-mode
290
+ * changes, re-renders during tool execution) but the metrics only change on an
291
+ * API response, so without this the wire fills with byte-identical events.
292
+ * Fire-and-forget: errors are swallowed so the statusline render is never
293
+ * affected.
294
+ */
295
+ async function submitSessionStatusEvent(input, projectDir) {
296
+ const sessionId = input.session_id;
297
+ if (!sessionId) {
298
+ return;
299
+ }
300
+ const cfg = (0, config_1.loadConfig)(projectDir);
301
+ if (!(0, config_1.isSessionStatusEnabled)(cfg)) {
302
+ return;
303
+ }
304
+ // Session-tracking guard: only emit for IronBee-tracked sessions.
305
+ const sessionDir = (0, path_1.join)(projectDir, ".ironbee", "sessions", sessionId);
306
+ if (!(0, fs_1.existsSync)(sessionDir)) {
307
+ return;
308
+ }
309
+ // Skip empty ticks before the first API response (no usage, zero cost).
310
+ const cw = input.context_window ?? {};
311
+ if ((cw.current_usage === null || cw.current_usage === undefined) && num(cw.total_input_tokens) === 0) {
312
+ return;
313
+ }
314
+ const event = buildSessionStatusEvent(input, projectDir, sessionId);
315
+ const sig = computeSessionStatusSignature(event);
316
+ const statePath = statuslineStatePath(projectDir, sessionId);
317
+ const prev = readStatuslineState(statePath);
318
+ // 1) Skip-if-unchanged: same resource signature → redundant tick, skip
319
+ // without writing (the dominant case — vim/permission/re-render ticks).
320
+ if (prev.sig === sig) {
321
+ return;
322
+ }
323
+ // 2) Min-interval throttle: even on a real metric change, don't emit more
324
+ // often than `emitMinIntervalSeconds` (default 10; 0 disables). Rapid
325
+ // changes coalesce — the next tick after the interval elapses emits the
326
+ // latest state. Skip WITHOUT advancing state so the pending change is
327
+ // still emitted once the window opens.
328
+ const intervalMs = (0, config_1.getStatusLineEmitMinIntervalSeconds)(cfg) * 1000;
329
+ const now = Date.now();
330
+ if (intervalMs > 0 && prev.lastEmitMs !== undefined && now - prev.lastEmitMs < intervalMs) {
331
+ return;
332
+ }
333
+ (0, queue_1.submit)(projectDir, sessionId, "send_event", event);
334
+ writeStatuslineState(statePath, { sig, lastEmitMs: now });
335
+ }
336
+ /**
337
+ * Spawn the chained statusline command, piping our stdin to it and inheriting
338
+ * its stdout (so its output becomes the rendered statusline). On Windows,
339
+ * Claude Code runs statuslines via Git Bash, so a bash command would break
340
+ * under cmd.exe — prefer Git Bash there when available.
341
+ */
342
+ function runChained(userCmd, stdinPayload) {
343
+ return new Promise((resolve) => {
344
+ let child;
345
+ const bash = process.platform === "win32" ? findGitBash() : undefined;
346
+ try {
347
+ child = bash
348
+ ? (0, child_process_1.spawn)(bash, ["-c", userCmd], { stdio: ["pipe", "inherit", "inherit"] })
349
+ : (0, child_process_1.spawn)(userCmd, [], { stdio: ["pipe", "inherit", "inherit"], shell: true });
350
+ }
351
+ catch (e) {
352
+ logger_1.logger.debug(`session-status: failed to spawn chained statusline: ${e instanceof Error ? e.message : e}`);
353
+ resolve();
354
+ return;
355
+ }
356
+ child.on("error", (e) => {
357
+ logger_1.logger.debug(`session-status: chained statusline error: ${e.message}`);
358
+ resolve();
359
+ });
360
+ child.on("exit", (code) => {
361
+ // Preserve the chained command's exit code so its render semantics
362
+ // (CC drops stdout on non-zero) match the unwrapped behavior.
363
+ process.exitCode = code ?? 0;
364
+ resolve();
365
+ });
366
+ if (child.stdin) {
367
+ child.stdin.on("error", () => { });
368
+ child.stdin.write(stdinPayload);
369
+ child.stdin.end();
370
+ }
371
+ });
372
+ }
373
+ /** Common Git Bash install locations on Windows; undefined → fall back to cmd.exe. */
374
+ function findGitBash() {
375
+ const candidates = [
376
+ "C:\\Program Files\\Git\\bin\\bash.exe",
377
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
378
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
379
+ ];
380
+ for (const c of candidates) {
381
+ if ((0, fs_1.existsSync)(c)) {
382
+ return c;
383
+ }
384
+ }
385
+ return undefined;
386
+ }
387
+ /** Minimal default statusline when there is nothing to chain. */
388
+ function buildDefaultLine(input) {
389
+ const model = input.model?.id ?? "?";
390
+ const pct = num(input.context_window?.used_percentage);
391
+ return `[${model}] ${pct}% ctx`;
392
+ }
393
+ /**
394
+ * Statusline entry point. Reads stdin, submits the event (gated, fire-and-
395
+ * forget), then chains the user's original statusline.
396
+ */
397
+ async function runSessionStatus() {
398
+ let raw;
399
+ try {
400
+ raw = (0, stdin_1.readStdin)();
401
+ }
402
+ catch (e) {
403
+ logger_1.logger.debug(`session-status: failed to read stdin: ${e}`);
404
+ return;
405
+ }
406
+ let input;
407
+ try {
408
+ input = JSON.parse(raw);
409
+ }
410
+ catch (e) {
411
+ logger_1.logger.debug(`session-status: failed to parse stdin: ${e}`);
412
+ return;
413
+ }
414
+ const projectDir = resolveProjectDir(input);
415
+ // 1) Submit our metrics — never blocks/breaks the render.
416
+ await submitSessionStatusEvent(input, projectDir).catch((e) => {
417
+ logger_1.logger.debug(`session-status: submit failed: ${e instanceof Error ? e.message : e}`);
418
+ });
419
+ // 2) Chain the user's original statusline. Primary: SessionStart cache.
420
+ // Fallback: live resolution (cache missing/torn/absent) so a cache miss
421
+ // never makes the user's statusline disappear.
422
+ let chained;
423
+ if (input.session_id) {
424
+ const sessionDir = (0, path_1.join)(projectDir, ".ironbee", "sessions", input.session_id);
425
+ try {
426
+ chained = (0, session_state_1.getChainedStatusLine)(sessionDir);
427
+ }
428
+ catch {
429
+ chained = undefined;
430
+ }
431
+ }
432
+ if (chained === undefined) {
433
+ chained = resolveChainTarget(projectDir);
434
+ }
435
+ if (chained && !isIronbeeStatusLine(chained)) {
436
+ await runChained(chained, raw);
437
+ return;
438
+ }
439
+ // 3) Nothing to chain → silent unless renderDefault is on.
440
+ if ((0, config_1.getStatusLineRenderDefault)((0, config_1.loadConfig)(projectDir))) {
441
+ process.stdout.write(buildDefaultLine(input) + "\n");
442
+ }
443
+ }
444
+ //# sourceMappingURL=session-status.js.map