github-router 0.3.313 → 0.3.315

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 (102) hide show
  1. package/dist/aic-ledger-BS47yb_C.js +298 -0
  2. package/dist/aic-ledger-BS47yb_C.js.map +1 -0
  3. package/dist/{attribution-settings-Dnt04Iyn.js → attribution-settings-DXmmyuEE.js} +8 -8
  4. package/dist/{attribution-settings-Dnt04Iyn.js.map → attribution-settings-DXmmyuEE.js.map} +1 -1
  5. package/dist/{auth-O_UG1ABO.js → auth-D8t41c5D.js} +3 -3
  6. package/dist/{auth-O_UG1ABO.js.map → auth-D8t41c5D.js.map} +1 -1
  7. package/dist/browser-ext/manifest.json +1 -1
  8. package/dist/{check-usage-dicJOWML.js → check-usage-BpBKOjGZ.js} +4 -4
  9. package/dist/{check-usage-dicJOWML.js.map → check-usage-BpBKOjGZ.js.map} +1 -1
  10. package/dist/{claude-D_Me80Yi.js → claude-Cmmu3-cL.js} +140 -20
  11. package/dist/claude-Cmmu3-cL.js.map +1 -0
  12. package/dist/{codex-CE7zZm-M.js → codex-DqTKKoEx.js} +5 -5
  13. package/dist/{codex-CE7zZm-M.js.map → codex-DqTKKoEx.js.map} +1 -1
  14. package/dist/copilot-discount-IbXR5A28.js +220 -0
  15. package/dist/copilot-discount-IbXR5A28.js.map +1 -0
  16. package/dist/{debug-2PgImRnr.js → debug-CA7pg8V9.js} +2 -2
  17. package/dist/{debug-2PgImRnr.js.map → debug-CA7pg8V9.js.map} +1 -1
  18. package/dist/engine-b9XaKLp0.js +2 -0
  19. package/dist/{exec-y8C_MU8A.js → exec-COUT4QsS.js} +2 -2
  20. package/dist/{exec-y8C_MU8A.js.map → exec-COUT4QsS.js.map} +1 -1
  21. package/dist/{gate-discovery-_flEdhYo.js → gate-discovery-CPk6Z8yc.js} +6 -6
  22. package/dist/{gate-discovery-_flEdhYo.js.map → gate-discovery-CPk6Z8yc.js.map} +1 -1
  23. package/dist/{get-copilot-usage-Cjy8TiFf.js → get-copilot-usage-FSQE8szQ.js} +2 -2
  24. package/dist/{get-copilot-usage-Cjy8TiFf.js.map → get-copilot-usage-FSQE8szQ.js.map} +1 -1
  25. package/dist/hooks.mjs +615 -15
  26. package/dist/hooks.sha256 +1 -1
  27. package/dist/internal-aic-status-BN0xRLSF.js +432 -0
  28. package/dist/internal-aic-status-BN0xRLSF.js.map +1 -0
  29. package/dist/{internal-artifact-open-CxeIfsW_.js → internal-artifact-open-B3stUu74.js} +2 -2
  30. package/dist/{internal-artifact-open-CxeIfsW_.js.map → internal-artifact-open-B3stUu74.js.map} +1 -1
  31. package/dist/{internal-fast-dispatch-guard-D8YNlnuQ.js → internal-fast-dispatch-guard-2QjUlhqO.js} +1 -1
  32. package/dist/{internal-fast-dispatch-guard-19xQEouu.js → internal-fast-dispatch-guard-CAwYj_Yz.js} +2 -2
  33. package/dist/{internal-fast-dispatch-guard-19xQEouu.js.map → internal-fast-dispatch-guard-CAwYj_Yz.js.map} +1 -1
  34. package/dist/{internal-first-mate-guard-BuweUgjb.js → internal-first-mate-guard-B0rbLb0L.js} +3 -3
  35. package/dist/{internal-first-mate-guard-BuweUgjb.js.map → internal-first-mate-guard-B0rbLb0L.js.map} +1 -1
  36. package/dist/{internal-first-mate-guard-Nf1H0mCK.js → internal-first-mate-guard-VgnHBp0s.js} +1 -1
  37. package/dist/{internal-max-dispatch-guard-_Pp4LltM.js → internal-max-dispatch-guard-BzKsY9ze.js} +1 -1
  38. package/dist/{internal-max-dispatch-guard-DZ7KLWxP.js → internal-max-dispatch-guard-l1NGYHtF.js} +2 -2
  39. package/dist/{internal-max-dispatch-guard-DZ7KLWxP.js.map → internal-max-dispatch-guard-l1NGYHtF.js.map} +1 -1
  40. package/dist/{internal-plan-review-DLduDz41.js → internal-plan-review-QUvNZmMR.js} +3 -3
  41. package/dist/{internal-plan-review-DLduDz41.js.map → internal-plan-review-QUvNZmMR.js.map} +1 -1
  42. package/dist/{internal-prompt-submit-DG94f8E-.js → internal-prompt-submit-fj5ccft_.js} +5 -5
  43. package/dist/{internal-prompt-submit-DG94f8E-.js.map → internal-prompt-submit-fj5ccft_.js.map} +1 -1
  44. package/dist/{internal-session-bind-D6Hpl-84.js → internal-session-bind-0Hmak8PR.js} +2 -2
  45. package/dist/{internal-session-bind-D6Hpl-84.js.map → internal-session-bind-0Hmak8PR.js.map} +1 -1
  46. package/dist/{internal-stop-hook-BJ3o61-L.js → internal-stop-hook-BKiKYeda.js} +6 -6
  47. package/dist/{internal-stop-hook-BJ3o61-L.js.map → internal-stop-hook-BKiKYeda.js.map} +1 -1
  48. package/dist/{internal-stop-review-DlKzZgFm.js → internal-stop-review-8v2llYDB.js} +2 -2
  49. package/dist/{internal-stop-review-DlKzZgFm.js.map → internal-stop-review-8v2llYDB.js.map} +1 -1
  50. package/dist/{internal-worker-guard-D7sfByyo.js → internal-worker-guard-BI4Pl0Ew.js} +2 -2
  51. package/dist/{internal-worker-guard-D7sfByyo.js.map → internal-worker-guard-BI4Pl0Ew.js.map} +1 -1
  52. package/dist/{internal-workspace-header-CU5OjLCB.js → internal-workspace-header-kg9FoNXK.js} +2 -2
  53. package/dist/{internal-workspace-header-CU5OjLCB.js.map → internal-workspace-header-kg9FoNXK.js.map} +1 -1
  54. package/dist/lifecycle-BpqAyaAj.js +2 -0
  55. package/dist/lifecycle-CL2WiyeQ.js +2 -0
  56. package/dist/{lifecycle-CtRA3Y7p.js → lifecycle-Cx1wVrLb.js} +3 -3
  57. package/dist/{lifecycle-CtRA3Y7p.js.map → lifecycle-Cx1wVrLb.js.map} +1 -1
  58. package/dist/{lifecycle-BCnarXJV.js → lifecycle-IwotwedG.js} +2 -2
  59. package/dist/{lifecycle-BCnarXJV.js.map → lifecycle-IwotwedG.js.map} +1 -1
  60. package/dist/main.js +21 -20
  61. package/dist/main.js.map +1 -1
  62. package/dist/{mcp-workspace-header-B60WSR1Y.js → mcp-workspace-header-BYgWioLH.js} +2 -2
  63. package/dist/{mcp-workspace-header-B60WSR1Y.js.map → mcp-workspace-header-BYgWioLH.js.map} +1 -1
  64. package/dist/{models-COuMOfM6.js → models-DgLeb-Dv.js} +3 -3
  65. package/dist/{models-COuMOfM6.js.map → models-DgLeb-Dv.js.map} +1 -1
  66. package/dist/{orchestration-DjLA-lxB.js → orchestration-Dc3mO0G3.js} +3 -3
  67. package/dist/{orchestration-DjLA-lxB.js.map → orchestration-Dc3mO0G3.js.map} +1 -1
  68. package/dist/paths-BXXCVFvA.js +2 -0
  69. package/dist/{paths-De1Qh9Q9.js → paths-CMxlQEfQ.js} +3 -3
  70. package/dist/{paths-De1Qh9Q9.js.map → paths-CMxlQEfQ.js.map} +1 -1
  71. package/dist/{peer-mcp-personas-DklYru_1.js → peer-mcp-personas-CYWJSxPK.js} +40 -13
  72. package/dist/{peer-mcp-personas-DklYru_1.js.map → peer-mcp-personas-CYWJSxPK.js.map} +1 -1
  73. package/dist/{plan-review-hook-vL6BYmA8.js → plan-review-hook-BIMwWkh6.js} +4 -4
  74. package/dist/{plan-review-hook-vL6BYmA8.js.map → plan-review-hook-BIMwWkh6.js.map} +1 -1
  75. package/dist/{prompt-submit-hook-DfqufWC1.js → prompt-submit-hook-BcFpjHXg.js} +3 -3
  76. package/dist/{prompt-submit-hook-DfqufWC1.js.map → prompt-submit-hook-BcFpjHXg.js.map} +1 -1
  77. package/dist/{provision-BYdIsKcp.js → provision-AwSjt-vR.js} +9 -5
  78. package/dist/provision-AwSjt-vR.js.map +1 -0
  79. package/dist/{self-invocation-B84JCHps.js → self-invocation-cRl4RqfW.js} +2 -2
  80. package/dist/{self-invocation-B84JCHps.js.map → self-invocation-cRl4RqfW.js.map} +1 -1
  81. package/dist/{serve-DI4QgSSB.js → serve-BG2O6Slz.js} +13 -13
  82. package/dist/{serve-DI4QgSSB.js.map → serve-BG2O6Slz.js.map} +1 -1
  83. package/dist/{server-setup-s2Os8RfG.js → server-setup-D7ABbEs1.js} +109 -10
  84. package/dist/server-setup-D7ABbEs1.js.map +1 -0
  85. package/dist/{start-DV5iDhbP.js → start-Dpwpnq99.js} +3 -3
  86. package/dist/{start-DV5iDhbP.js.map → start-Dpwpnq99.js.map} +1 -1
  87. package/dist/{stop-gate-hook-BEU8qQrC.js → stop-gate-hook-BUCxa5Kc.js} +4 -4
  88. package/dist/{stop-gate-hook-BEU8qQrC.js.map → stop-gate-hook-BUCxa5Kc.js.map} +1 -1
  89. package/dist/{stop-gate-policy-qzqXfRxE.js → stop-gate-policy--bpyxxXU.js} +3 -3
  90. package/dist/{stop-gate-policy-qzqXfRxE.js.map → stop-gate-policy--bpyxxXU.js.map} +1 -1
  91. package/dist/{token-CCg-Ov5q.js → token-ClzS23Y2.js} +2 -2
  92. package/dist/{token-CCg-Ov5q.js.map → token-ClzS23Y2.js.map} +1 -1
  93. package/dist/{worker-dispatch-CH-NN5FW.js → worker-dispatch-DMIdrlsF.js} +2 -2
  94. package/dist/{worker-dispatch-CH-NN5FW.js.map → worker-dispatch-DMIdrlsF.js.map} +1 -1
  95. package/package.json +1 -1
  96. package/dist/claude-D_Me80Yi.js.map +0 -1
  97. package/dist/engine-DyzGsCUb.js +0 -2
  98. package/dist/lifecycle-B0USSJvd.js +0 -2
  99. package/dist/lifecycle-CQFjaQc0.js +0 -2
  100. package/dist/paths-DyX6SEnd.js +0 -2
  101. package/dist/provision-BYdIsKcp.js.map +0 -1
  102. package/dist/server-setup-s2Os8RfG.js.map +0 -1
package/dist/hooks.sha256 CHANGED
@@ -1 +1 @@
1
- 4fcac8bff9996b835fd54e27d98f5262a4579be8f8b55a1bae1bdd7aaaa0431e
1
+ a3747ee44a5aac64ec5a952b9efb60112356d31d45f8d04ae939401d0d96a7e5
@@ -0,0 +1,432 @@
1
+ import { o as resolveExecutable, t as buildExecInvocation } from "./exec-COUT4QsS.js";
2
+ import { o as formatAicStatus, s as readAicSnapshotFile } from "./aic-ledger-BS47yb_C.js";
3
+ import { t as discountedUsdForSnapshot } from "./copilot-discount-IbXR5A28.js";
4
+ import { defineCommand } from "citty";
5
+ import { readFileSync } from "node:fs";
6
+ import { spawnSync } from "node:child_process";
7
+ //#region src/lib/default-statusline.ts
8
+ /**
9
+ * Rich default status line for `github-router claude` sessions.
10
+ *
11
+ * Replaces the need for users to maintain their own PowerShell/bash status
12
+ * script: `internal-aic-status` renders these segments natively from the
13
+ * Claude Code stdin JSON, then pins the `[AIC x.xx]` ledger fragment ahead
14
+ * of them. Zero dependencies, cross-platform (Windows 11 is the primary
15
+ * target — no `powershell`/`bash`/`jq` required), and fast (no shell
16
+ * startup; one bounded `git rev-parse` at most).
17
+ *
18
+ * Segment order (left = highest priority; narrow terminals drop from the
19
+ * right, `AIC` is never dropped):
20
+ *
21
+ * `[AIC] | [ctx bar] % | model | dir (branch) | ~$actual | in/out | dur | +a -r`
22
+ *
23
+ * The `$` segment is the DISCOUNTED actual (`~$` = factor-discounted
24
+ * session total from `copilot-discount.ts`, capped at AIC × $0.01) — NOT
25
+ * Claude's `cost.total_cost_usd`, which prices at Anthropic list rates and
26
+ * reads ~10-100x high against Copilot billing (a 28-day sample: 41.1B tokens
27
+ * for $5,142 ≈ $0.13/1M blended vs $15-75/1M list). Rendering list price
28
+ * next to the real `[AIC]` billing unit would show two contradictory
29
+ * numbers, so list price is deliberately not rendered at all.
30
+ *
31
+ * Width comes from `COLUMNS` (set by Claude Code for status scripts since
32
+ * v2.1.153); fallback is 120 when unset/unparseable so a missing `COLUMNS`
33
+ * never needlessly drops segments.
34
+ */
35
+ const ESC = "\x1B";
36
+ const RESET = `${ESC}[0m`;
37
+ const DIM = `${ESC}[90m`;
38
+ const GREEN = `${ESC}[32m`;
39
+ const YELLOW = `${ESC}[33m`;
40
+ const RED = `${ESC}[31m`;
41
+ const ORANGE = `${ESC}[38;5;208m`;
42
+ const CYAN = `${ESC}[36m`;
43
+ const MAGENTA = `${ESC}[35m`;
44
+ const BLUE = `${ESC}[34m`;
45
+ const AIC_COLOR = `${ESC}[1;33m`;
46
+ const CTX_BAR_LEN = 10;
47
+ const GIT_TIMEOUT_MS = 300;
48
+ const WIDTH_FALLBACK = 120;
49
+ const SEP_TEXT = `${DIM} | ${RESET}`;
50
+ function asRecord(value) {
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
52
+ return value;
53
+ }
54
+ function finiteNumber(value) {
55
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
56
+ }
57
+ function nonEmptyString(value) {
58
+ return typeof value === "string" && value.length > 0 ? value : void 0;
59
+ }
60
+ /**
61
+ * Parse Claude Code's status-line stdin JSON into the fields we render.
62
+ * Total function: malformed JSON or unexpected shapes yield `{}` (every
63
+ * segment then renders its `--` placeholder or is omitted) — never throws.
64
+ */
65
+ function parseStatusInput(raw) {
66
+ if (!raw.trim()) return {};
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(raw);
70
+ } catch {
71
+ return {};
72
+ }
73
+ const root = asRecord(parsed);
74
+ if (!root) return {};
75
+ const out = {};
76
+ const ctx = asRecord(root.context_window);
77
+ if (ctx) {
78
+ const pct = finiteNumber(ctx.used_percentage);
79
+ if (pct !== void 0) out.usedPct = pct;
80
+ const tIn = finiteNumber(ctx.total_input_tokens);
81
+ if (tIn !== void 0 && tIn >= 0) out.totalInputTokens = Math.floor(tIn);
82
+ const tOut = finiteNumber(ctx.total_output_tokens);
83
+ if (tOut !== void 0 && tOut >= 0) out.totalOutputTokens = Math.floor(tOut);
84
+ }
85
+ const cost = asRecord(root.cost);
86
+ if (cost) {
87
+ const dur = finiteNumber(cost.total_duration_ms);
88
+ if (dur !== void 0 && dur >= 0) out.totalDurationMs = dur;
89
+ const la = finiteNumber(cost.total_lines_added);
90
+ if (la !== void 0 && la >= 0) out.linesAdded = Math.floor(la);
91
+ const lr = finiteNumber(cost.total_lines_removed);
92
+ if (lr !== void 0 && lr >= 0) out.linesRemoved = Math.floor(lr);
93
+ }
94
+ const workspace = asRecord(root.workspace);
95
+ const cwd = (workspace ? nonEmptyString(workspace.current_dir) : void 0) ?? nonEmptyString(root.cwd);
96
+ if (cwd) out.cwd = cwd;
97
+ const model = asRecord(root.model);
98
+ if (model) {
99
+ const name = nonEmptyString(model.display_name) ?? nonEmptyString(model.id);
100
+ if (name) out.modelName = name;
101
+ }
102
+ return out;
103
+ }
104
+ /** Compact token count: `999` → `999`, `15234` → `15.2k`, `2.1M`. */
105
+ function compactTokens(n) {
106
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
107
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
108
+ return `${n}`;
109
+ }
110
+ /** `45000` → `0m45s`, `198000` → `3m18s`, `3720000` → `1h02m`. */
111
+ function formatDurationMs(ms) {
112
+ const totalSeconds = Math.floor(ms / 1e3);
113
+ const hours = Math.floor(totalSeconds / 3600);
114
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
115
+ const seconds = totalSeconds % 60;
116
+ if (hours >= 1) return `${hours}h${String(minutes).padStart(2, "0")}m`;
117
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
118
+ }
119
+ /** Basename of a cwd, tolerant of `/` and `\` separators + trailing slash. */
120
+ function dirLeaf(cwd) {
121
+ const trimmed = cwd.replace(/[/\\]+$/, "");
122
+ const parts = trimmed.split(/[/\\]/);
123
+ return parts[parts.length - 1] ?? trimmed;
124
+ }
125
+ function buildCtxSegment(usedPct) {
126
+ if (usedPct === void 0 || !Number.isFinite(usedPct)) return {
127
+ text: `${DIM}[----------] --%${RESET}`,
128
+ plain: "[----------] --%"
129
+ };
130
+ const u = Math.max(0, Math.min(100, Math.round(usedPct)));
131
+ let filled = Math.round(u / 100 * CTX_BAR_LEN);
132
+ if (filled > CTX_BAR_LEN) filled = CTX_BAR_LEN;
133
+ if (filled < 0) filled = 0;
134
+ const bar = `#`.repeat(filled) + `-`.repeat(CTX_BAR_LEN - filled);
135
+ return {
136
+ text: `${u < 50 ? GREEN : u < 75 ? YELLOW : RED}[${bar}] ${u}%${RESET}`,
137
+ plain: `[${bar}] ${u}%`
138
+ };
139
+ }
140
+ /**
141
+ * Discounted-actual `$` segment. Callers pass the capped total from
142
+ * `discountedUsdForSnapshot` — this function only formats. Placeholder
143
+ * `~$--` until the first priced upstream response lands in the ledger.
144
+ * Thresholds are tuned for ACTUALS scale (a session runs ~$0.10-2.00) —
145
+ * not the old list-price scale.
146
+ */
147
+ function buildUsdSegment(actualUsd) {
148
+ if (actualUsd === void 0 || !Number.isFinite(actualUsd) || actualUsd < 0) return {
149
+ text: `${DIM}~$--${RESET}`,
150
+ plain: "~$--"
151
+ };
152
+ const c = actualUsd;
153
+ const color = c < 1 ? GREEN : c < 5 ? YELLOW : c < 20 ? ORANGE : RED;
154
+ const formatted = c < 10 ? c.toFixed(2) : c.toFixed(1);
155
+ return {
156
+ text: `${color}~$${formatted}${RESET}`,
157
+ plain: `~$${formatted}`
158
+ };
159
+ }
160
+ function buildToksSegment(tIn, tOut) {
161
+ if (tIn === void 0 || tOut === void 0) return {
162
+ text: `${DIM}--/--${RESET}`,
163
+ plain: "--/--"
164
+ };
165
+ const cin = compactTokens(tIn);
166
+ const cout = compactTokens(tOut);
167
+ return {
168
+ text: `${CYAN}${cin}${RESET}${DIM}/${RESET}${MAGENTA}${cout}${RESET}`,
169
+ plain: `${cin}/${cout}`
170
+ };
171
+ }
172
+ function buildDurSegment(durMs) {
173
+ if (durMs === void 0 || !Number.isFinite(durMs) || durMs < 0) return {
174
+ text: `${DIM}--${RESET}`,
175
+ plain: "--"
176
+ };
177
+ const formatted = formatDurationMs(durMs);
178
+ return {
179
+ text: `${DIM}${formatted}${RESET}`,
180
+ plain: formatted
181
+ };
182
+ }
183
+ function buildLinesSegment(added, removed) {
184
+ const a = added ?? 0;
185
+ const r = removed ?? 0;
186
+ return {
187
+ text: `${GREEN}+${a}${RESET} ${RED}-${r}${RESET}`,
188
+ plain: `+${a} -${r}`
189
+ };
190
+ }
191
+ function buildModelSegment(modelName) {
192
+ if (!modelName || modelName === "--") return void 0;
193
+ return {
194
+ text: `${MAGENTA}${modelName}${RESET}`,
195
+ plain: modelName
196
+ };
197
+ }
198
+ function buildDirGitSegment(cwd, branch) {
199
+ const leaf = cwd ? dirLeaf(cwd) : "";
200
+ const br = branch && branch !== "HEAD" ? branch : "";
201
+ if (!leaf && !br) return void 0;
202
+ if (leaf && br) return {
203
+ text: `${BLUE}${leaf}${RESET} ${CYAN}(${br})${RESET}`,
204
+ plain: `${leaf} (${br})`
205
+ };
206
+ if (leaf) return {
207
+ text: `${BLUE}${leaf}${RESET}`,
208
+ plain: leaf
209
+ };
210
+ return {
211
+ text: `${CYAN}${br}${RESET}`,
212
+ plain: br
213
+ };
214
+ }
215
+ /**
216
+ * Resolve the current git branch for `cwd`. Bounded (`GIT_TIMEOUT_MS`) and
217
+ * total: any failure (no git binary, not a repo, detached `HEAD`, timeout,
218
+ * `%` in a Windows path that cmd.exe cannot safely quote) yields `""`
219
+ * (caller omits the branch) — never throws.
220
+ */
221
+ function resolveGitBranch(cwd) {
222
+ try {
223
+ if (!cwd) return "";
224
+ const gitPath = resolveExecutable("git");
225
+ if (!gitPath) return "";
226
+ const { command, args, shell } = buildExecInvocation([
227
+ gitPath,
228
+ "-C",
229
+ cwd,
230
+ "rev-parse",
231
+ "--abbrev-ref",
232
+ "HEAD"
233
+ ]);
234
+ const result = spawnSync(command, args, {
235
+ shell,
236
+ timeout: GIT_TIMEOUT_MS,
237
+ encoding: "utf8",
238
+ stdio: [
239
+ "ignore",
240
+ "pipe",
241
+ "ignore"
242
+ ]
243
+ });
244
+ if (result.error) return "";
245
+ const out = typeof result.stdout === "string" ? result.stdout.trim() : "";
246
+ if (!out || out === "HEAD") return "";
247
+ return out.split("\n")[0]?.trim() ?? "";
248
+ } catch {
249
+ return "";
250
+ }
251
+ }
252
+ /** Terminal width for the drop-right-to-left assembly. */
253
+ function terminalWidth(env = process.env) {
254
+ const raw = env.COLUMNS;
255
+ if (raw !== void 0) {
256
+ const n = Number(raw);
257
+ if (Number.isInteger(n) && n > 0) return n;
258
+ }
259
+ return WIDTH_FALLBACK;
260
+ }
261
+ /**
262
+ * Assemble the full status line. `aicFragment` (e.g. `[AIC 12.42]`) is
263
+ * PINNED — it renders even when nothing else fits. `actualUsd` feeds the
264
+ * `~$` segment (the capped factor-discounted total from
265
+ * `discountedUsdForSnapshot`, same snapshot the fragment came from).
266
+ * Droppable segments in priority order: ctx, model, dir/git, ~$, toks,
267
+ * dur, lines.
268
+ */
269
+ function assembleStatusLine(aicFragment, input, opts = {}) {
270
+ const aic = aicFragment.trim();
271
+ const aicText = aic ? `${AIC_COLOR}${aic}${RESET}` : "";
272
+ const droppable = [buildCtxSegment(input.usedPct)];
273
+ const model = buildModelSegment(input.modelName);
274
+ if (model) droppable.push(model);
275
+ const dirGit = buildDirGitSegment(input.cwd, opts.branch);
276
+ if (dirGit) droppable.push(dirGit);
277
+ droppable.push(buildUsdSegment(opts.actualUsd), buildToksSegment(input.totalInputTokens, input.totalOutputTokens), buildDurSegment(input.totalDurationMs), buildLinesSegment(input.linesAdded, input.linesRemoved));
278
+ const width = opts.width ?? terminalWidth();
279
+ const sepLen = 3;
280
+ let count = droppable.length;
281
+ while (count > 0) {
282
+ let total = aic ? aic.length : 0;
283
+ for (let i = 0; i < count; i++) {
284
+ if (aic || i > 0) total += sepLen;
285
+ total += droppable[i].plain.length;
286
+ }
287
+ if (total <= width) break;
288
+ if (count === 1 && !aic) break;
289
+ count--;
290
+ }
291
+ const parts = [];
292
+ if (aic) parts.push(aicText);
293
+ for (let i = 0; i < count; i++) parts.push(droppable[i].text);
294
+ if (parts.length === 0) return "";
295
+ return parts.join(SEP_TEXT);
296
+ }
297
+ /**
298
+ * Convenience: parse stdin JSON + assemble with branch resolution.
299
+ * `branchOverride` skips the `git` spawn (used by tests). `actualUsd`
300
+ * comes from `discountedUsdForSnapshot` on the same ledger snapshot as
301
+ * `aicFragment` (read by the caller — this module never touches the
302
+ * ledger file).
303
+ */
304
+ function buildRichStatusLine(stdinRaw, aicFragment, opts = {}) {
305
+ const input = parseStatusInput(stdinRaw);
306
+ const branch = opts.branchOverride !== void 0 ? opts.branchOverride : resolveGitBranch(input.cwd);
307
+ return assembleStatusLine(aicFragment, input, {
308
+ ...opts.width !== void 0 ? { width: opts.width } : {},
309
+ ...branch ? { branch } : {},
310
+ ...opts.actualUsd !== void 0 ? { actualUsd: opts.actualUsd } : {}
311
+ });
312
+ }
313
+ //#endregion
314
+ //#region src/internal-aic-status.ts
315
+ /**
316
+ * The internal `internal-aic-status` subcommand: the executable a spawned
317
+ * Claude Code session's `statusLine` invokes (registered into the mirrored
318
+ * settings.json by the launcher — see `src/lib/aic-statusline-settings.ts`).
319
+ *
320
+ * Default-on rich status line: Claude Code's stdin JSON is rendered natively
321
+ * (see `src/lib/default-statusline.ts`) as
322
+ * `[ctx bar] % | model | dir (branch) | ~$actual | in/out | dur | +a -r`,
323
+ * with this launch's AIC ledger total PINNED ahead of it: `[AIC 12.42] ...`.
324
+ * The `~$` segment is the factor-discounted actual (per-model static
325
+ * factors over ledger nano, capped at AIC × $0.01 — see
326
+ * `src/lib/copilot-discount.ts`), not Claude's list-price `total_cost_usd`.
327
+ * Narrow terminals drop segments right-to-left; AIC is never dropped.
328
+ *
329
+ * When the user already had a `statusLine` command, its text arrives via
330
+ * `GH_ROUTER_AIC_USER_STATUSLINE` and is run instead of the rich default
331
+ * (their script already renders ctx/cost/model itself — rendering both
332
+ * would duplicate every segment), so the output stays one composed line:
333
+ * `[AIC 12.42] <user line>`.
334
+ *
335
+ * Status-line contract: stdout is the rendered line (first line wins), fast
336
+ * (<1s budget — ledger read is local, git branch has a 300ms cap, the user
337
+ * command gets a 5s cap), and NEVER failing (any error → print whatever is
338
+ * available → exit 0).
339
+ */
340
+ /**
341
+ * Read stdin synchronously. Claude Code pipes session JSON to the
342
+ * statusLine command; an unread pipe can SIGPIPE the writer on some
343
+ * platforms. Returns "" when stdin is a TTY or unreadable — callers render
344
+ * placeholders/omit segments rather than failing.
345
+ */
346
+ function readStdin() {
347
+ try {
348
+ if (process.stdin.isTTY) return "";
349
+ return readFileSync(0, "utf8");
350
+ } catch {
351
+ return "";
352
+ }
353
+ }
354
+ /**
355
+ * Pure core: compose the status line from an AIC fragment and a user line.
356
+ * Exported for unit tests.
357
+ */
358
+ function composeAicStatusLine(aicFragment, userLine) {
359
+ const aic = aicFragment.trim();
360
+ const user = userLine.split("\n")[0]?.trim() ?? "";
361
+ if (aic && user) return `${aic} ${user}`;
362
+ return aic || user;
363
+ }
364
+ /**
365
+ * Run the user's own statusLine command (from `GH_ROUTER_AIC_USER_STATUSLINE`),
366
+ * returning its first stdout line or "" on any failure/timeout. Exported for
367
+ * unit tests (command + timeout injectable).
368
+ */
369
+ function runUserStatusLine(command, timeoutMs = 5e3) {
370
+ if (!command.trim()) return "";
371
+ try {
372
+ const result = spawnSync(command, {
373
+ shell: true,
374
+ timeout: timeoutMs,
375
+ encoding: "utf8",
376
+ stdio: [
377
+ "ignore",
378
+ "pipe",
379
+ "ignore"
380
+ ],
381
+ env: process.env
382
+ });
383
+ if (result.error) return "";
384
+ return (typeof result.stdout === "string" ? result.stdout : "").split("\n")[0]?.trim() ?? "";
385
+ } catch {
386
+ return "";
387
+ }
388
+ }
389
+ function readAicSnapshot() {
390
+ try {
391
+ const ledgerPath = process.env.GH_ROUTER_AIC_LEDGER;
392
+ if (!ledgerPath) return { fragment: "" };
393
+ const snapshot = readAicSnapshotFile(ledgerPath);
394
+ if (!snapshot) return { fragment: "" };
395
+ if (snapshot.requests === 0 || snapshot.totalNanoAiu <= 0) return { fragment: "" };
396
+ return {
397
+ fragment: formatAicStatus(snapshot),
398
+ actualUsd: discountedUsdForSnapshot(snapshot).total
399
+ };
400
+ } catch {
401
+ return { fragment: "" };
402
+ }
403
+ }
404
+ const internalAicStatus = defineCommand({
405
+ meta: {
406
+ name: "internal-aic-status",
407
+ description: "Internal: Claude Code statusLine handler printing AIC total + rich session line."
408
+ },
409
+ async run() {
410
+ const stdinRaw = readStdin();
411
+ const { fragment: aic, actualUsd } = readAicSnapshot();
412
+ const userCommand = process.env.GH_ROUTER_AIC_USER_STATUSLINE ?? "";
413
+ if (userCommand.trim()) {
414
+ const line = composeAicStatusLine(aic, runUserStatusLine(userCommand));
415
+ if (line) process.stdout.write(`${line}\n`);
416
+ process.exitCode = 0;
417
+ return;
418
+ }
419
+ let line;
420
+ try {
421
+ line = buildRichStatusLine(stdinRaw, aic, { ...actualUsd !== void 0 ? { actualUsd } : {} });
422
+ } catch {
423
+ line = aic.trim();
424
+ }
425
+ if (line) process.stdout.write(`${line}\n`);
426
+ process.exitCode = 0;
427
+ }
428
+ });
429
+ //#endregion
430
+ export { internalAicStatus };
431
+
432
+ //# sourceMappingURL=internal-aic-status-BN0xRLSF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-aic-status-BN0xRLSF.js","names":[],"sources":["../src/lib/default-statusline.ts","../src/internal-aic-status.ts"],"sourcesContent":["/**\n * Rich default status line for `github-router claude` sessions.\n *\n * Replaces the need for users to maintain their own PowerShell/bash status\n * script: `internal-aic-status` renders these segments natively from the\n * Claude Code stdin JSON, then pins the `[AIC x.xx]` ledger fragment ahead\n * of them. Zero dependencies, cross-platform (Windows 11 is the primary\n * target — no `powershell`/`bash`/`jq` required), and fast (no shell\n * startup; one bounded `git rev-parse` at most).\n *\n * Segment order (left = highest priority; narrow terminals drop from the\n * right, `AIC` is never dropped):\n *\n * `[AIC] | [ctx bar] % | model | dir (branch) | ~$actual | in/out | dur | +a -r`\n *\n * The `$` segment is the DISCOUNTED actual (`~$` = factor-discounted\n * session total from `copilot-discount.ts`, capped at AIC × $0.01) — NOT\n * Claude's `cost.total_cost_usd`, which prices at Anthropic list rates and\n * reads ~10-100x high against Copilot billing (a 28-day sample: 41.1B tokens\n * for $5,142 ≈ $0.13/1M blended vs $15-75/1M list). Rendering list price\n * next to the real `[AIC]` billing unit would show two contradictory\n * numbers, so list price is deliberately not rendered at all.\n *\n * Width comes from `COLUMNS` (set by Claude Code for status scripts since\n * v2.1.153); fallback is 120 when unset/unparseable so a missing `COLUMNS`\n * never needlessly drops segments.\n */\n\nimport { spawnSync } from \"node:child_process\"\n\nimport { USD_PER_AIC_CREDIT } from \"./copilot-discount\"\nimport {\n buildExecInvocation,\n resolveExecutable,\n} from \"./exec\"\n\nconst ESC = \"\\x1b\"\nconst RESET = `${ESC}[0m`\nconst DIM = `${ESC}[90m`\nconst GREEN = `${ESC}[32m`\nconst YELLOW = `${ESC}[33m`\nconst RED = `${ESC}[31m`\nconst ORANGE = `${ESC}[38;5;208m`\nconst CYAN = `${ESC}[36m`\nconst MAGENTA = `${ESC}[35m`\nconst BLUE = `${ESC}[34m`\nconst AIC_COLOR = `${ESC}[1;33m`\n\nconst CTX_BAR_LEN = 10\nconst GIT_TIMEOUT_MS = 300\nconst WIDTH_FALLBACK = 120\nconst SEP_PLAIN = \" | \"\nconst SEP_TEXT = `${DIM} | ${RESET}`\n\nexport interface StatusInput {\n usedPct?: number\n totalInputTokens?: number\n totalOutputTokens?: number\n totalDurationMs?: number\n linesAdded?: number\n linesRemoved?: number\n cwd?: string\n modelName?: string\n}\n\nexport interface StatusSegment {\n text: string\n plain: string\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return undefined\n }\n return value as Record<string, unknown>\n}\n\nfunction finiteNumber(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined\n}\n\n/**\n * Parse Claude Code's status-line stdin JSON into the fields we render.\n * Total function: malformed JSON or unexpected shapes yield `{}` (every\n * segment then renders its `--` placeholder or is omitted) — never throws.\n */\nexport function parseStatusInput(raw: string): StatusInput {\n if (!raw.trim()) return {}\n let parsed: unknown\n try {\n parsed = JSON.parse(raw) as unknown\n } catch {\n return {}\n }\n const root = asRecord(parsed)\n if (!root) return {}\n const out: StatusInput = {}\n\n const ctx = asRecord(root.context_window)\n if (ctx) {\n const pct = finiteNumber(ctx.used_percentage)\n if (pct !== undefined) out.usedPct = pct\n const tIn = finiteNumber(ctx.total_input_tokens)\n if (tIn !== undefined && tIn >= 0) out.totalInputTokens = Math.floor(tIn)\n const tOut = finiteNumber(ctx.total_output_tokens)\n if (tOut !== undefined && tOut >= 0) out.totalOutputTokens = Math.floor(tOut)\n }\n\n const cost = asRecord(root.cost)\n if (cost) {\n const dur = finiteNumber(cost.total_duration_ms)\n if (dur !== undefined && dur >= 0) out.totalDurationMs = dur\n const la = finiteNumber(cost.total_lines_added)\n if (la !== undefined && la >= 0) out.linesAdded = Math.floor(la)\n const lr = finiteNumber(cost.total_lines_removed)\n if (lr !== undefined && lr >= 0) out.linesRemoved = Math.floor(lr)\n }\n\n const workspace = asRecord(root.workspace)\n const wsDir = workspace ? nonEmptyString(workspace.current_dir) : undefined\n const cwd = wsDir ?? nonEmptyString(root.cwd)\n if (cwd) out.cwd = cwd\n\n const model = asRecord(root.model)\n if (model) {\n const name =\n nonEmptyString(model.display_name) ?? nonEmptyString(model.id)\n if (name) out.modelName = name\n }\n\n return out\n}\n\n/** Compact token count: `999` → `999`, `15234` → `15.2k`, `2.1M`. */\nexport function compactTokens(n: number): string {\n if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`\n if (n >= 1000) return `${(n / 1000).toFixed(1)}k`\n return `${n}`\n}\n\n/** `45000` → `0m45s`, `198000` → `3m18s`, `3720000` → `1h02m`. */\nexport function formatDurationMs(ms: number): string {\n const totalSeconds = Math.floor(ms / 1000)\n const hours = Math.floor(totalSeconds / 3600)\n const minutes = Math.floor((totalSeconds % 3600) / 60)\n const seconds = totalSeconds % 60\n if (hours >= 1) {\n return `${hours}h${String(minutes).padStart(2, \"0\")}m`\n }\n return `${minutes}m${String(seconds).padStart(2, \"0\")}s`\n}\n\n/** Basename of a cwd, tolerant of `/` and `\\` separators + trailing slash. */\nexport function dirLeaf(cwd: string): string {\n const trimmed = cwd.replace(/[/\\\\]+$/, \"\")\n const parts = trimmed.split(/[/\\\\]/)\n return parts[parts.length - 1] ?? trimmed\n}\n\nexport function buildCtxSegment(usedPct: number | undefined): StatusSegment {\n if (usedPct === undefined || !Number.isFinite(usedPct)) {\n return {\n text: `${DIM}[----------] --%${RESET}`,\n plain: \"[----------] --%\",\n }\n }\n const u = Math.max(0, Math.min(100, Math.round(usedPct)))\n let filled = Math.round((u / 100) * CTX_BAR_LEN)\n if (filled > CTX_BAR_LEN) filled = CTX_BAR_LEN\n if (filled < 0) filled = 0\n const bar = `#`.repeat(filled) + `-`.repeat(CTX_BAR_LEN - filled)\n const color = u < 50 ? GREEN : u < 75 ? YELLOW : RED\n return {\n text: `${color}[${bar}] ${u}%${RESET}`,\n plain: `[${bar}] ${u}%`,\n }\n}\n\n/**\n * AIC credits → undiscounted USD. 1 credit ≈ $0.01 — the same convention as\n * the session exit summary (`aic-ledger.ts`). Kept for tests/callers that\n * need the raw conversion; the status line itself shows the\n * factor-discounted total from `copilot-discount.ts`.\n */\nexport function usdFromAicCredits(credits: number): number {\n return credits * USD_PER_AIC_CREDIT\n}\n\n/**\n * Discounted-actual `$` segment. Callers pass the capped total from\n * `discountedUsdForSnapshot` — this function only formats. Placeholder\n * `~$--` until the first priced upstream response lands in the ledger.\n * Thresholds are tuned for ACTUALS scale (a session runs ~$0.10-2.00) —\n * not the old list-price scale.\n */\nexport function buildUsdSegment(\n actualUsd: number | undefined,\n): StatusSegment {\n if (actualUsd === undefined || !Number.isFinite(actualUsd) || actualUsd < 0) {\n return { text: `${DIM}~$--${RESET}`, plain: \"~$--\" }\n }\n const c = actualUsd\n const color = c < 1 ? GREEN : c < 5 ? YELLOW : c < 20 ? ORANGE : RED\n const formatted = c < 10 ? c.toFixed(2) : c.toFixed(1)\n return {\n text: `${color}~$${formatted}${RESET}`,\n plain: `~$${formatted}`,\n }\n}\n\nexport function buildToksSegment(\n tIn: number | undefined,\n tOut: number | undefined,\n): StatusSegment {\n if (tIn === undefined || tOut === undefined) {\n return { text: `${DIM}--/--${RESET}`, plain: \"--/--\" }\n }\n const cin = compactTokens(tIn)\n const cout = compactTokens(tOut)\n return {\n text: `${CYAN}${cin}${RESET}${DIM}/${RESET}${MAGENTA}${cout}${RESET}`,\n plain: `${cin}/${cout}`,\n }\n}\n\nexport function buildDurSegment(durMs: number | undefined): StatusSegment {\n if (durMs === undefined || !Number.isFinite(durMs) || durMs < 0) {\n return { text: `${DIM}--${RESET}`, plain: \"--\" }\n }\n const formatted = formatDurationMs(durMs)\n return { text: `${DIM}${formatted}${RESET}`, plain: formatted }\n}\n\nexport function buildLinesSegment(\n added: number | undefined,\n removed: number | undefined,\n): StatusSegment {\n const a = added ?? 0\n const r = removed ?? 0\n return {\n text: `${GREEN}+${a}${RESET} ${RED}-${r}${RESET}`,\n plain: `+${a} -${r}`,\n }\n}\n\nexport function buildModelSegment(\n modelName: string | undefined,\n): StatusSegment | undefined {\n if (!modelName || modelName === \"--\") return undefined\n return {\n text: `${MAGENTA}${modelName}${RESET}`,\n plain: modelName,\n }\n}\n\nexport function buildDirGitSegment(\n cwd: string | undefined,\n branch: string | undefined,\n): StatusSegment | undefined {\n const leaf = cwd ? dirLeaf(cwd) : \"\"\n const br = branch && branch !== \"HEAD\" ? branch : \"\"\n if (!leaf && !br) return undefined\n if (leaf && br) {\n return {\n text: `${BLUE}${leaf}${RESET} ${CYAN}(${br})${RESET}`,\n plain: `${leaf} (${br})`,\n }\n }\n if (leaf) {\n return { text: `${BLUE}${leaf}${RESET}`, plain: leaf }\n }\n return { text: `${CYAN}${br}${RESET}`, plain: br }\n}\n\n/**\n * Resolve the current git branch for `cwd`. Bounded (`GIT_TIMEOUT_MS`) and\n * total: any failure (no git binary, not a repo, detached `HEAD`, timeout,\n * `%` in a Windows path that cmd.exe cannot safely quote) yields `\"\"`\n * (caller omits the branch) — never throws.\n */\nexport function resolveGitBranch(cwd: string | undefined): string {\n try {\n if (!cwd) return \"\"\n const gitPath = resolveExecutable(\"git\")\n if (!gitPath) return \"\"\n const { command, args, shell } = buildExecInvocation([\n gitPath,\n \"-C\",\n cwd,\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\",\n ])\n const result = spawnSync(command, args, {\n shell,\n timeout: GIT_TIMEOUT_MS,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n })\n if (result.error) return \"\"\n const out =\n typeof result.stdout === \"string\" ? result.stdout.trim() : \"\"\n if (!out || out === \"HEAD\") return \"\"\n return out.split(\"\\n\")[0]?.trim() ?? \"\"\n } catch {\n return \"\"\n }\n}\n\n/** Terminal width for the drop-right-to-left assembly. */\nexport function terminalWidth(env: NodeJS.ProcessEnv = process.env): number {\n const raw = env.COLUMNS\n if (raw !== undefined) {\n const n = Number(raw)\n if (Number.isInteger(n) && n > 0) return n\n }\n return WIDTH_FALLBACK\n}\n\n/**\n * Assemble the full status line. `aicFragment` (e.g. `[AIC 12.42]`) is\n * PINNED — it renders even when nothing else fits. `actualUsd` feeds the\n * `~$` segment (the capped factor-discounted total from\n * `discountedUsdForSnapshot`, same snapshot the fragment came from).\n * Droppable segments in priority order: ctx, model, dir/git, ~$, toks,\n * dur, lines.\n */\nexport function assembleStatusLine(\n aicFragment: string,\n input: StatusInput,\n opts: { width?: number; branch?: string; actualUsd?: number } = {},\n): string {\n const aic = aicFragment.trim()\n const aicText = aic ? `${AIC_COLOR}${aic}${RESET}` : \"\"\n\n const droppable: Array<StatusSegment> = [buildCtxSegment(input.usedPct)]\n const model = buildModelSegment(input.modelName)\n if (model) droppable.push(model)\n // Location context sits right after ctx/model: more useful than the\n // numbers when the bar is wide, still dropped before ctx/model.\n const dirGit = buildDirGitSegment(input.cwd, opts.branch)\n if (dirGit) droppable.push(dirGit)\n droppable.push(\n buildUsdSegment(opts.actualUsd),\n buildToksSegment(input.totalInputTokens, input.totalOutputTokens),\n buildDurSegment(input.totalDurationMs),\n buildLinesSegment(input.linesAdded, input.linesRemoved),\n )\n\n const width = opts.width ?? terminalWidth()\n const sepLen = SEP_PLAIN.length\n\n // Drop from the end (lowest priority) until the line fits. AIC is pinned:\n // it is excluded from the dropping loop entirely.\n let count = droppable.length\n while (count > 0) {\n let total = aic ? aic.length : 0\n for (let i = 0; i < count; i++) {\n if (aic || i > 0) total += sepLen\n total += droppable[i].plain.length\n }\n if (total <= width) break\n // Never drop the last segment when there is no AIC: an over-wide line\n // with real data beats an empty status line.\n if (count === 1 && !aic) break\n count--\n }\n\n const parts: Array<string> = []\n if (aic) parts.push(aicText)\n for (let i = 0; i < count; i++) parts.push(droppable[i].text)\n if (parts.length === 0) return \"\"\n return parts.join(SEP_TEXT)\n}\n\n/**\n * Convenience: parse stdin JSON + assemble with branch resolution.\n * `branchOverride` skips the `git` spawn (used by tests). `actualUsd`\n * comes from `discountedUsdForSnapshot` on the same ledger snapshot as\n * `aicFragment` (read by the caller — this module never touches the\n * ledger file).\n */\nexport function buildRichStatusLine(\n stdinRaw: string,\n aicFragment: string,\n opts: { width?: number; branchOverride?: string; actualUsd?: number } = {},\n): string {\n const input = parseStatusInput(stdinRaw)\n const branch =\n opts.branchOverride !== undefined\n ? opts.branchOverride\n : resolveGitBranch(input.cwd)\n return assembleStatusLine(aicFragment, input, {\n ...(opts.width !== undefined ? { width: opts.width } : {}),\n ...(branch ? { branch } : {}),\n ...(opts.actualUsd !== undefined ? { actualUsd: opts.actualUsd } : {}),\n })\n}\n","/**\n * The internal `internal-aic-status` subcommand: the executable a spawned\n * Claude Code session's `statusLine` invokes (registered into the mirrored\n * settings.json by the launcher — see `src/lib/aic-statusline-settings.ts`).\n *\n * Default-on rich status line: Claude Code's stdin JSON is rendered natively\n * (see `src/lib/default-statusline.ts`) as\n * `[ctx bar] % | model | dir (branch) | ~$actual | in/out | dur | +a -r`,\n * with this launch's AIC ledger total PINNED ahead of it: `[AIC 12.42] ...`.\n * The `~$` segment is the factor-discounted actual (per-model static\n * factors over ledger nano, capped at AIC × $0.01 — see\n * `src/lib/copilot-discount.ts`), not Claude's list-price `total_cost_usd`.\n * Narrow terminals drop segments right-to-left; AIC is never dropped.\n *\n * When the user already had a `statusLine` command, its text arrives via\n * `GH_ROUTER_AIC_USER_STATUSLINE` and is run instead of the rich default\n * (their script already renders ctx/cost/model itself — rendering both\n * would duplicate every segment), so the output stays one composed line:\n * `[AIC 12.42] <user line>`.\n *\n * Status-line contract: stdout is the rendered line (first line wins), fast\n * (<1s budget — ledger read is local, git branch has a 300ms cap, the user\n * command gets a 5s cap), and NEVER failing (any error → print whatever is\n * available → exit 0).\n */\n\nimport { defineCommand } from \"citty\"\n\nimport { spawnSync } from \"node:child_process\"\nimport { readFileSync } from \"node:fs\"\n\nimport {\n formatAicStatus,\n readAicSnapshotFile,\n} from \"./lib/aic-ledger\"\nimport { discountedUsdForSnapshot } from \"./lib/copilot-discount\"\nimport { buildRichStatusLine } from \"./lib/default-statusline\"\n\n/**\n * Read stdin synchronously. Claude Code pipes session JSON to the\n * statusLine command; an unread pipe can SIGPIPE the writer on some\n * platforms. Returns \"\" when stdin is a TTY or unreadable — callers render\n * placeholders/omit segments rather than failing.\n */\nfunction readStdin(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n // Best-effort; the ledger read below does not depend on stdin.\n return \"\"\n }\n}\n\n/**\n * Pure core: compose the status line from an AIC fragment and a user line.\n * Exported for unit tests.\n */\nexport function composeAicStatusLine(\n aicFragment: string,\n userLine: string,\n): string {\n const aic = aicFragment.trim()\n const user = userLine.split(\"\\n\")[0]?.trim() ?? \"\"\n if (aic && user) return `${aic} ${user}`\n return aic || user\n}\n\n/**\n * Run the user's own statusLine command (from `GH_ROUTER_AIC_USER_STATUSLINE`),\n * returning its first stdout line or \"\" on any failure/timeout. Exported for\n * unit tests (command + timeout injectable).\n */\nexport function runUserStatusLine(\n command: string,\n timeoutMs = 5000,\n): string {\n if (!command.trim()) return \"\"\n try {\n const result = spawnSync(command, {\n shell: true,\n timeout: timeoutMs,\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n // Inherit env so user scripts see the same session context they would\n // under their own statusLine (cwd included — spawnSync inherits it).\n env: process.env,\n })\n if (result.error) return \"\"\n const out = typeof result.stdout === \"string\" ? result.stdout : \"\"\n return out.split(\"\\n\")[0]?.trim() ?? \"\"\n } catch {\n return \"\"\n }\n}\n\nfunction readAicSnapshot(): { fragment: string; actualUsd?: number } {\n try {\n const ledgerPath = process.env.GH_ROUTER_AIC_LEDGER\n if (!ledgerPath) return { fragment: \"\" }\n const snapshot = readAicSnapshotFile(ledgerPath)\n if (!snapshot) return { fragment: \"\" }\n // No priced responses yet → no fragment AND no USD: the `~$`\n // segment renders its `~$--` placeholder instead of `~$0.00`.\n if (snapshot.requests === 0 || snapshot.totalNanoAiu <= 0) {\n return { fragment: \"\" }\n }\n return {\n fragment: formatAicStatus(snapshot),\n actualUsd: discountedUsdForSnapshot(snapshot).total,\n }\n } catch {\n return { fragment: \"\" }\n }\n}\n\nexport const internalAicStatus = defineCommand({\n meta: {\n name: \"internal-aic-status\",\n description:\n \"Internal: Claude Code statusLine handler printing AIC total + rich session line.\",\n },\n async run() {\n const stdinRaw = readStdin()\n const { fragment: aic, actualUsd } = readAicSnapshot()\n const userCommand = process.env.GH_ROUTER_AIC_USER_STATUSLINE ?? \"\"\n // Wrap mode: the user's own script already renders ctx/cost/model from\n // the same stdin JSON — run it and prepend AIC only.\n if (userCommand.trim()) {\n const userLine = runUserStatusLine(userCommand)\n const line = composeAicStatusLine(aic, userLine)\n if (line) process.stdout.write(`${line}\\n`)\n process.exitCode = 0\n return\n }\n // Default mode: native rich line with AIC pinned ahead of it. The\n // builder never throws; an empty result (no ledger yet AND no stdin)\n // prints nothing rather than a placeholder line.\n let line: string\n try {\n line = buildRichStatusLine(stdinRaw, aic, {\n ...(actualUsd !== undefined ? { actualUsd } : {}),\n })\n } catch {\n line = aic.trim()\n }\n if (line) process.stdout.write(`${line}\\n`)\n // Signal success WITHOUT tearing the loop down — same resolution as the\n // other internal hooks (see main.ts help-path comment): a hard\n // process.exit() can abort through a pipe on Windows.\n process.exitCode = 0\n },\n})\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as ArtifactClient } from "./client-CQMfroGV.js";
2
- import { a as isSubagentContext } from "./stop-gate-policy-qzqXfRxE.js";
2
+ import { a as isSubagentContext } from "./stop-gate-policy--bpyxxXU.js";
3
3
  import { defineCommand } from "citty";
4
4
  import { readFileSync, realpathSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
@@ -316,4 +316,4 @@ const internalArtifactOpen = defineCommand({
316
316
  //#endregion
317
317
  export { internalArtifactOpen };
318
318
 
319
- //# sourceMappingURL=internal-artifact-open-CxeIfsW_.js.map
319
+ //# sourceMappingURL=internal-artifact-open-B3stUu74.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal-artifact-open-CxeIfsW_.js","names":[],"sources":["../src/lib/artifact/plan-html.ts","../src/internal-artifact-open.ts"],"sourcesContent":["/**\n * Render a plan's markdown into a self-contained, styled HTML document for the\n * ai-or-die Artifact panel. HTML is the canonical review artifact (lavish's\n * model); converting here means the hands-off auto-open shows a formatted,\n * annotatable page instead of raw markdown.\n *\n * Security: the iframe ai-or-die serves this into runs with\n * `allow-scripts allow-same-origin`, so a `<script>` smuggled into the plan\n * markdown could read the per-session asset token. Two defenses:\n * 1. marked is configured to ESCAPE raw HTML tokens (render as visible text).\n * 2. `link`/`image` hrefs with script-bearing schemes (javascript:, vbscript:,\n * non-image data:) are neutralised before rendering.\n * No separate sanitizer/dep needed.\n */\nimport { Marked } from \"marked\"\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\")\n}\n\n/** Decode the HTML character references a browser would resolve in an attribute\n * value, so an entity-obfuscated scheme (`javascript&colon;`, `j&#97;vascript:`)\n * cannot slip past the scheme check below. Numeric refs cover any letter; the\n * small named map covers the scheme-relevant punctuation. */\nconst NAMED_REFS: Record<string, string> = {\n colon: \":\", tab: \"\\t\", newline: \"\\n\", sol: \"/\", semi: \";\", amp: \"&\", lpar: \"(\", rpar: \")\",\n}\nfunction decodeEntities(s: string): string {\n return s.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);?/g, (m, body: string) => {\n if (body[0] === \"#\") {\n const code = body[1] === \"x\" || body[1] === \"X\"\n ? Number.parseInt(body.slice(2), 16)\n : Number.parseInt(body.slice(1), 10)\n return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : m\n }\n return NAMED_REFS[body.toLowerCase()] ?? m\n })\n}\n\n/** Block script-bearing URL schemes (defense-in-depth on top of HTML escaping). */\nfunction isDangerousUrl(url: string, isImage: boolean): boolean {\n // Decode entities first (a browser resolves them in href), then drop ASCII\n // control chars + spaces (code <= 0x20) a browser ignores inside a scheme.\n const v = Array.from(decodeEntities(url)).filter((c) => c.charCodeAt(0) > 0x20).join(\"\").toLowerCase()\n if (v.startsWith(\"javascript:\") || v.startsWith(\"vbscript:\")) return true\n if (v.startsWith(\"data:\")) {\n // Allow only raster image data URIs (and only for <img>); SVG can script.\n return !(isImage && v.startsWith(\"data:image/\") && !v.startsWith(\"data:image/svg\"))\n }\n return false\n}\n\n/** A marked instance that escapes raw HTML (raw markup never becomes live). */\nfunction makeMarked(): Marked {\n const m = new Marked({ gfm: true, breaks: false })\n m.use({\n renderer: {\n // Raw HTML in the source is rendered as escaped text, never live markup.\n html(token: unknown): string {\n const text = typeof token === \"string\" ? token : ((token as { text?: string }).text ?? \"\")\n return escapeHtml(text)\n },\n },\n })\n return m\n}\n\ninterface MdToken {\n type?: string\n href?: string\n tokens?: MdToken[]\n items?: MdToken[]\n header?: Array<{ tokens?: MdToken[] }>\n rows?: Array<Array<{ tokens?: MdToken[] }>>\n}\n\n/**\n * Recursively neutralise script-bearing link/image hrefs in the lexed token tree.\n * Done as an explicit walk (not marked's `walkTokens` hook) because we render via\n * the low-level `lexer`+`parser` split, which does not run registered walkTokens.\n */\nfunction neutralizeUrls(tokens: MdToken[]): void {\n for (const t of tokens) {\n if ((t.type === \"link\" || t.type === \"image\") && typeof t.href === \"string\") {\n if (isDangerousUrl(t.href, t.type === \"image\")) t.href = \"#\"\n }\n if (t.tokens) neutralizeUrls(t.tokens)\n if (t.items) neutralizeUrls(t.items)\n if (t.header) for (const cell of t.header) if (cell.tokens) neutralizeUrls(cell.tokens)\n if (t.rows) for (const row of t.rows) for (const cell of row) if (cell.tokens) neutralizeUrls(cell.tokens)\n }\n}\n\n/** Add `data-source-line=\"N\"` to the first opening tag of a rendered block. */\nfunction tagSourceLine(html: string, line: number): string {\n return html.replace(/^(\\s*)<([a-zA-Z][\\w-]*)/, `$1<$2 data-source-line=\"${line}\"`)\n}\n\n/**\n * Render markdown to inner HTML, tagging each top-level block with its 1-based\n * source line so an annotation in the panel can map back to a plan line.\n */\nexport function renderMarkdownBody(source: string): string {\n const m = makeMarked()\n let tokens: Array<{ raw?: string }>\n try {\n tokens = m.lexer(source) as Array<{ raw?: string }>\n } catch {\n // Lexing should not fail, but never throw out of the hook path.\n return `<pre>${escapeHtml(source)}</pre>`\n }\n neutralizeUrls(tokens as MdToken[])\n let line = 1\n let out = \"\"\n for (const tok of tokens) {\n let html: string\n try {\n html = m.parser([tok] as Parameters<typeof m.parser>[0])\n } catch {\n html = \"\"\n }\n if (html.trim()) out += tagSourceLine(html, line)\n line += (tok.raw?.match(/\\n/g)?.length ?? 0)\n }\n return out || `<pre>${escapeHtml(source)}</pre>`\n}\n\n/**\n * Wrap rendered markdown in a complete, self-contained HTML document with\n * readable typography (works even before ai-or-die injects its annotation SDK).\n */\nexport function renderPlanHtml(source: string, title = \"Plan\"): string {\n return wrapArtifactDocument(renderMarkdownBody(source), title)\n}\n\nconst BASE_STYLE = ` :root {\n --bg: #0f1115; --fg: #f7f3ea; --muted: #aeb6c6; --border: #2a2f3a;\n --accent: #f4c95d; --code-bg: #171a21;\n --serif: 'Iowan Old Style', Georgia, 'Times New Roman', serif;\n --sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n }\n * { box-sizing: border-box; }\n html, body { margin: 0; background: var(--bg); color: var(--fg); }\n body { font-family: var(--serif); font-size: 16px; line-height: 1.6;\n padding: 32px clamp(16px, 5vw, 56px); }\n main { max-width: 78ch; margin: 0 auto; }\n h1, h2, h3, h4 { font-family: var(--sans); line-height: 1.25; margin: 1.6em 0 0.5em; }\n h1 { font-size: 1.9em; } h2 { font-size: 1.45em; padding-bottom: 0.2em;\n border-bottom: 1px solid var(--border); } h3 { font-size: 1.2em; }\n p, li { color: var(--fg); }\n a { color: var(--accent); }\n code { font-family: var(--mono); font-size: 0.9em; background: var(--code-bg);\n padding: 0.12em 0.35em; border-radius: 5px; }\n pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px;\n padding: 12px 14px; overflow: auto; }\n pre code { background: none; padding: 0; }\n blockquote { margin: 1em 0; padding: 0.2em 1em; border-left: 3px solid var(--accent);\n color: var(--muted); }\n table { border-collapse: collapse; width: 100%; margin: 1em 0; font-family: var(--sans);\n font-size: 0.95em; }\n th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }\n th { background: var(--code-bg); }\n hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }\n ul, ol { padding-left: 1.4em; }`\n\nconst INTERACTIVE_STYLE = `\n .aod-group { margin: 1.4em 0; padding: 14px 16px; border: 1px solid var(--border);\n border-radius: 10px; background: var(--code-bg); }\n .aod-prompt { font-family: var(--sans); font-weight: 600; margin: 0 0 0.6em; }\n .aod-options { display: flex; flex-wrap: wrap; gap: 8px; }\n .aod-multi-select .aod-options { flex-direction: column; gap: 6px; }\n .aod-action, .aod-submit { font-family: var(--sans); font-size: 0.95em; font-weight: 600;\n cursor: pointer; border: 1px solid var(--border); border-radius: 8px;\n padding: 8px 12px; background: var(--bg); color: var(--fg); }\n .aod-action:hover, .aod-submit:hover { border-color: var(--accent); }\n .aod-submit { margin-top: 10px; background: var(--accent); color: #17130a; border-color: var(--accent); }\n .aod-check { display: flex; align-items: center; gap: 8px; font-family: var(--sans); cursor: pointer; }\n .aod-steps { list-style: decimal; }\n .aod-step { margin: 0.5em 0; }\n .aod-step-actions { display: inline-flex; gap: 6px; margin-left: 8px; }`\n\n/**\n * Wrap rendered inner HTML in the self-contained document shell. `extraStyle` is\n * appended to the inline stylesheet so the interactive variant can add its own\n * rules without a second `<style>` block. Static callers pass \"\" (default), so\n * the static document is byte-identical to the pre-refactor output.\n */\nfunction wrapArtifactDocument(body: string, title: string, extraStyle = \"\"): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHtml(title)}</title>\n<style>\n${BASE_STYLE}${extraStyle}\n</style>\n</head>\n<body>\n<main>\n${body}\n</main>\n</body>\n</html>\n`\n}\n\n/* -------------------------------------------------------------------------- */\n/* Interactive artifacts (contract v2.2 §4: declarative data-aod-* vocab) */\n/* -------------------------------------------------------------------------- */\n\nexport interface InteractiveOption {\n /** Stable id echoed back as `elementId` in the action event. Required. */\n elementId: string\n /** Human-visible label. */\n label: string\n /** Optional value echoed as `data-aod-value` (checkbox submits use \"true\"/\"false\"). */\n value?: string\n}\n\n/** One immediate-fire choice group: each option posts an action the instant it is clicked. */\nexport interface ChooseOneBlock {\n kind: \"choose-one\"\n /** `data-aod-group` shared by the options (marks them as one decision). */\n group: string\n prompt?: string\n /** `data-aod-action` verb for every option (default \"choose\"). */\n action?: string\n options: InteractiveOption[]\n}\n\n/** A multi-select group: checkbox toggles plus one submit that carries {group, selected}. */\nexport interface MultiSelectBlock {\n kind: \"multi-select\"\n group: string\n prompt?: string\n submitLabel?: string\n submitElementId?: string\n options: InteractiveOption[]\n}\n\nexport interface StepAction {\n /** `data-aod-action` verb (e.g. \"approve\", \"skip\"). */\n action: string\n label: string\n value?: string\n}\n\nexport interface StepItem {\n /** Stable `data-aod-id` echoed as `elementId`. */\n elementId: string\n label: string\n sourceLine?: number\n actions: StepAction[]\n}\n\n/** An ordered list of plan steps, each with its own inline action buttons. */\nexport interface StepsBlock {\n kind: \"steps\"\n steps: StepItem[]\n}\n\nexport type InteractiveBlock = ChooseOneBlock | MultiSelectBlock | StepsBlock\n\nexport interface InteractivePlanModel {\n /** Optional prose rendered above the interactive blocks (same escaping as static). */\n markdown?: string\n blocks: InteractiveBlock[]\n}\n\n/** Build a ` data-x=\"y\"` attribute string, dropping undefined values, all escaped. */\nfunction dataAttrs(pairs: Array<[string, string | undefined]>): string {\n let out = \"\"\n for (const [name, value] of pairs) {\n if (value === undefined) continue\n out += ` ${name}=\"${escapeHtml(value)}\"`\n }\n return out\n}\n\nfunction renderChooseOne(block: ChooseOneBlock): string {\n const action = block.action && block.action.trim() ? block.action : \"choose\"\n const prompt = block.prompt ? `<p class=\"aod-prompt\">${escapeHtml(block.prompt)}</p>` : \"\"\n const options = block.options\n .filter((o) => o.elementId.trim() !== \"\")\n .map((o) => {\n const attrs = dataAttrs([\n [\"data-aod-action\", action],\n [\"data-aod-group\", block.group],\n [\"data-aod-id\", o.elementId],\n [\"data-aod-value\", o.value],\n ])\n return `<button type=\"button\" class=\"aod-action\"${attrs}>${escapeHtml(o.label)}</button>`\n })\n .join(\"\\n\")\n return `<div class=\"aod-group aod-choose-one\"${dataAttrs([[\"data-aod-group\", block.group]])}>\n${prompt}<div class=\"aod-options\">\n${options}\n</div>\n</div>`\n}\n\nfunction renderMultiSelect(block: MultiSelectBlock): string {\n const prompt = block.prompt ? `<p class=\"aod-prompt\">${escapeHtml(block.prompt)}</p>` : \"\"\n const options = block.options\n .filter((o) => o.elementId.trim() !== \"\")\n .map((o) => {\n const attrs = dataAttrs([\n [\"data-aod-action\", \"check\"],\n [\"data-aod-group\", block.group],\n [\"data-aod-id\", o.elementId],\n [\"data-aod-value\", o.value],\n ])\n return `<label class=\"aod-check\"><input type=\"checkbox\"${attrs}> <span>${escapeHtml(o.label)}</span></label>`\n })\n .join(\"\\n\")\n const submitId = block.submitElementId && block.submitElementId.trim()\n ? block.submitElementId\n : `${block.group}-submit`\n const submitAttrs = dataAttrs([\n [\"data-aod-action\", \"submit\"],\n [\"data-aod-group\", block.group],\n [\"data-aod-id\", submitId],\n ])\n const submitLabel = block.submitLabel && block.submitLabel.trim() ? block.submitLabel : \"Submit\"\n return `<div class=\"aod-group aod-multi-select\"${dataAttrs([[\"data-aod-group\", block.group]])}>\n${prompt}<div class=\"aod-options\">\n${options}\n</div>\n<button type=\"button\" class=\"aod-submit\"${submitAttrs}>${escapeHtml(submitLabel)}</button>\n</div>`\n}\n\nfunction renderSteps(block: StepsBlock): string {\n const items = block.steps\n .filter((s) => s.elementId.trim() !== \"\")\n .map((s) => {\n const liAttrs = dataAttrs([\n [\"data-aod-id\", s.elementId],\n [\"data-source-line\", s.sourceLine === undefined ? undefined : String(s.sourceLine)],\n ])\n const actions = s.actions\n .map((a) => {\n const attrs = dataAttrs([\n [\"data-aod-action\", a.action],\n [\"data-aod-id\", s.elementId],\n [\"data-aod-value\", a.value],\n ])\n return `<button type=\"button\" class=\"aod-action\"${attrs}>${escapeHtml(a.label)}</button>`\n })\n .join(\"\")\n return `<li class=\"aod-step\"${liAttrs}><span class=\"aod-step-label\">${escapeHtml(s.label)}</span><span class=\"aod-step-actions\">${actions}</span></li>`\n })\n .join(\"\\n\")\n return `<ol class=\"aod-steps\">\n${items}\n</ol>`\n}\n\nfunction renderInteractiveBlock(block: InteractiveBlock): string {\n switch (block.kind) {\n case \"choose-one\":\n return renderChooseOne(block)\n case \"multi-select\":\n return renderMultiSelect(block)\n case \"steps\":\n return renderSteps(block)\n default:\n return \"\"\n }\n}\n\n/**\n * Render a typed interactive plan model into a self-contained HTML document that\n * carries the declarative `data-aod-*` action vocabulary the ai-or-die panel SDK\n * wires (contract v2.2 §4). Optional prose (`markdown`) is rendered with the SAME\n * escaping + dangerous-URL neutralization as the static path; all control\n * attributes and labels are HTML-escaped, so no author string can break out of an\n * attribute or inject live markup.\n */\nexport function renderInteractivePlanHtml(model: InteractivePlanModel, title = \"Plan\"): string {\n const prose = model.markdown && model.markdown.trim() ? renderMarkdownBody(model.markdown) : \"\"\n const blocks = model.blocks.map(renderInteractiveBlock).filter((h) => h.trim() !== \"\").join(\"\\n\")\n const body = [prose, blocks].filter((s) => s.trim() !== \"\").join(\"\\n\")\n return wrapArtifactDocument(body || `<pre>${escapeHtml(JSON.stringify(model))}</pre>`, title, INTERACTIVE_STYLE)\n}\n","/**\n * The internal `internal-artifact-open` subcommand: invoked by a spawned Claude\n * Code session's `PostToolUse` hook (matcher `ExitPlanMode`), registered into the\n * mirrored settings.json by the launcher when running inside an ai-or-die tab\n * (`AIORDIE_SESSION_ID` set). It auto-opens the just-finalized plan in the\n * ai-or-die Artifact review panel so the human reviews it WITHOUT the model\n * having to call artifact_open itself.\n *\n * Auth: AIORDIE_TOKEN is stripped from the child env, so the token can't come via\n * env or argv (argv leaks to `ps`). The launcher writes a mode-600\n * `.aiordie-artifact.json` into CLAUDE_CONFIG_DIR; this hook reads it.\n *\n * Plan: the EXACT plan for THIS session comes from the ExitPlanMode payload —\n * `tool_input.planFilePath` (per-session; never the globally-newest file in the\n * shared plans dir) with `tool_input.plan` as the markdown fallback. The plan\n * markdown is rendered to a self-contained styled HTML file (HTML is the\n * canonical artifact, lavish's model — formatted + annotatable, not raw md), and\n * THAT `.html` is opened.\n *\n * Side-effect only: never writes stdout, never throws, always exits 0. Skips\n * subagent payloads (agent_id/agent_type) like the other hooks.\n */\n\nimport { defineCommand } from \"citty\"\n\nimport { readFileSync, realpathSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport path from \"node:path\"\n\nimport { ArtifactClient } from \"./lib/artifact/client\"\nimport { renderPlanHtml } from \"./lib/artifact/plan-html\"\nimport { isSubagentContext } from \"./lib/orchestration/stop-gate-policy\"\n\ninterface ArtifactCreds {\n baseUrl: string\n token: string\n sessionId: string\n insecureTLS: boolean\n}\n\ninterface ExitPlanPayload {\n planFilePath?: string\n planMarkdown?: string\n}\n\nfunction readStdin(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n return \"\"\n }\n}\n\nfunction credsPath(): string {\n const dir = process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), \".claude\")\n return path.join(dir, \".aiordie-artifact.json\")\n}\n\nfunction isSubagent(payload: unknown): boolean {\n try {\n return isSubagentContext(payload as Parameters<typeof isSubagentContext>[0])\n } catch {\n return false\n }\n}\n\n/** Pull the per-session plan path + markdown content out of the hook payload. */\nexport function parseExitPlanPayload(raw: string): ExitPlanPayload {\n try {\n const obj = JSON.parse(raw) as { tool_input?: { planFilePath?: unknown; plan?: unknown } }\n const ti = obj.tool_input ?? {}\n const planFilePath = typeof ti.planFilePath === \"string\" && ti.planFilePath.trim() ? ti.planFilePath : undefined\n const planMarkdown = typeof ti.plan === \"string\" && ti.plan.trim() ? ti.plan : undefined\n return { planFilePath, planMarkdown }\n } catch {\n return {}\n }\n}\n\nfunction readCreds(): ArtifactCreds | undefined {\n try {\n const parsed = JSON.parse(readFileSync(credsPath(), \"utf8\")) as Partial<ArtifactCreds>\n if (!parsed.baseUrl || !parsed.token || !parsed.sessionId) return undefined\n return {\n baseUrl: parsed.baseUrl,\n token: parsed.token,\n sessionId: parsed.sessionId,\n insecureTLS: parsed.insecureTLS === true,\n }\n } catch {\n return undefined\n }\n}\n\n/**\n * Resolve the plan markdown + a title, preferring the file at `planFilePath`\n * (the source of truth, may be newer than the payload's snapshot), falling back\n * to the inline `tool_input.plan` content.\n */\nfunction resolvePlanMarkdown(p: ExitPlanPayload): { markdown: string; title: string; nearPath?: string } | undefined {\n if (p.planFilePath) {\n try {\n const md = readFileSync(p.planFilePath, \"utf8\")\n if (md.trim()) return { markdown: md, title: path.basename(p.planFilePath, path.extname(p.planFilePath)), nearPath: p.planFilePath }\n } catch {\n /* fall through to inline content */\n }\n }\n if (p.planMarkdown) return { markdown: p.planMarkdown, title: \"Plan\" }\n return undefined\n}\n\n/**\n * Write the rendered HTML next to the plan (an allowed root) when we have a real\n * path, else into the plans dir under CLAUDE_CONFIG_DIR. Returns the realpath of\n * the written `.html`, or undefined on failure.\n */\nfunction writePlanHtml(html: string, nearPath: string | undefined): string | undefined {\n let target: string\n if (nearPath) {\n // `<plan>.md` -> `<plan>.aiordie.html` (or append when there is no .md ext).\n const base = nearPath.replace(/\\.(md|markdown)$/i, \"\")\n target = `${base}.aiordie.html`\n } else {\n const dir = path.join(process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), \".claude\"), \"plans\")\n target = path.join(dir, `aiordie-plan-${process.pid}.html`)\n }\n try {\n writeFileSync(target, html, \"utf8\")\n try {\n return realpathSync.native(target)\n } catch {\n return target\n }\n } catch {\n return undefined\n }\n}\n\nexport const internalArtifactOpen = defineCommand({\n meta: { name: \"internal-artifact-open\", description: \"Open the finalized plan in the ai-or-die panel\" },\n async run() {\n try {\n const raw = readStdin()\n let payload: unknown\n try {\n payload = JSON.parse(raw)\n } catch {\n payload = undefined\n }\n if (isSubagent(payload)) return\n const creds = readCreds()\n if (!creds) return\n const plan = resolvePlanMarkdown(parseExitPlanPayload(raw))\n if (!plan) return\n const htmlPath = writePlanHtml(renderPlanHtml(plan.markdown, plan.title), plan.nearPath)\n if (!htmlPath) return\n const client = new ArtifactClient(creds)\n await client.open(htmlPath)\n } catch {\n /* side-effect only: never block, never throw */\n }\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"internal-artifact-open-B3stUu74.js","names":[],"sources":["../src/lib/artifact/plan-html.ts","../src/internal-artifact-open.ts"],"sourcesContent":["/**\n * Render a plan's markdown into a self-contained, styled HTML document for the\n * ai-or-die Artifact panel. HTML is the canonical review artifact (lavish's\n * model); converting here means the hands-off auto-open shows a formatted,\n * annotatable page instead of raw markdown.\n *\n * Security: the iframe ai-or-die serves this into runs with\n * `allow-scripts allow-same-origin`, so a `<script>` smuggled into the plan\n * markdown could read the per-session asset token. Two defenses:\n * 1. marked is configured to ESCAPE raw HTML tokens (render as visible text).\n * 2. `link`/`image` hrefs with script-bearing schemes (javascript:, vbscript:,\n * non-image data:) are neutralised before rendering.\n * No separate sanitizer/dep needed.\n */\nimport { Marked } from \"marked\"\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\")\n}\n\n/** Decode the HTML character references a browser would resolve in an attribute\n * value, so an entity-obfuscated scheme (`javascript&colon;`, `j&#97;vascript:`)\n * cannot slip past the scheme check below. Numeric refs cover any letter; the\n * small named map covers the scheme-relevant punctuation. */\nconst NAMED_REFS: Record<string, string> = {\n colon: \":\", tab: \"\\t\", newline: \"\\n\", sol: \"/\", semi: \";\", amp: \"&\", lpar: \"(\", rpar: \")\",\n}\nfunction decodeEntities(s: string): string {\n return s.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);?/g, (m, body: string) => {\n if (body[0] === \"#\") {\n const code = body[1] === \"x\" || body[1] === \"X\"\n ? Number.parseInt(body.slice(2), 16)\n : Number.parseInt(body.slice(1), 10)\n return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : m\n }\n return NAMED_REFS[body.toLowerCase()] ?? m\n })\n}\n\n/** Block script-bearing URL schemes (defense-in-depth on top of HTML escaping). */\nfunction isDangerousUrl(url: string, isImage: boolean): boolean {\n // Decode entities first (a browser resolves them in href), then drop ASCII\n // control chars + spaces (code <= 0x20) a browser ignores inside a scheme.\n const v = Array.from(decodeEntities(url)).filter((c) => c.charCodeAt(0) > 0x20).join(\"\").toLowerCase()\n if (v.startsWith(\"javascript:\") || v.startsWith(\"vbscript:\")) return true\n if (v.startsWith(\"data:\")) {\n // Allow only raster image data URIs (and only for <img>); SVG can script.\n return !(isImage && v.startsWith(\"data:image/\") && !v.startsWith(\"data:image/svg\"))\n }\n return false\n}\n\n/** A marked instance that escapes raw HTML (raw markup never becomes live). */\nfunction makeMarked(): Marked {\n const m = new Marked({ gfm: true, breaks: false })\n m.use({\n renderer: {\n // Raw HTML in the source is rendered as escaped text, never live markup.\n html(token: unknown): string {\n const text = typeof token === \"string\" ? token : ((token as { text?: string }).text ?? \"\")\n return escapeHtml(text)\n },\n },\n })\n return m\n}\n\ninterface MdToken {\n type?: string\n href?: string\n tokens?: MdToken[]\n items?: MdToken[]\n header?: Array<{ tokens?: MdToken[] }>\n rows?: Array<Array<{ tokens?: MdToken[] }>>\n}\n\n/**\n * Recursively neutralise script-bearing link/image hrefs in the lexed token tree.\n * Done as an explicit walk (not marked's `walkTokens` hook) because we render via\n * the low-level `lexer`+`parser` split, which does not run registered walkTokens.\n */\nfunction neutralizeUrls(tokens: MdToken[]): void {\n for (const t of tokens) {\n if ((t.type === \"link\" || t.type === \"image\") && typeof t.href === \"string\") {\n if (isDangerousUrl(t.href, t.type === \"image\")) t.href = \"#\"\n }\n if (t.tokens) neutralizeUrls(t.tokens)\n if (t.items) neutralizeUrls(t.items)\n if (t.header) for (const cell of t.header) if (cell.tokens) neutralizeUrls(cell.tokens)\n if (t.rows) for (const row of t.rows) for (const cell of row) if (cell.tokens) neutralizeUrls(cell.tokens)\n }\n}\n\n/** Add `data-source-line=\"N\"` to the first opening tag of a rendered block. */\nfunction tagSourceLine(html: string, line: number): string {\n return html.replace(/^(\\s*)<([a-zA-Z][\\w-]*)/, `$1<$2 data-source-line=\"${line}\"`)\n}\n\n/**\n * Render markdown to inner HTML, tagging each top-level block with its 1-based\n * source line so an annotation in the panel can map back to a plan line.\n */\nexport function renderMarkdownBody(source: string): string {\n const m = makeMarked()\n let tokens: Array<{ raw?: string }>\n try {\n tokens = m.lexer(source) as Array<{ raw?: string }>\n } catch {\n // Lexing should not fail, but never throw out of the hook path.\n return `<pre>${escapeHtml(source)}</pre>`\n }\n neutralizeUrls(tokens as MdToken[])\n let line = 1\n let out = \"\"\n for (const tok of tokens) {\n let html: string\n try {\n html = m.parser([tok] as Parameters<typeof m.parser>[0])\n } catch {\n html = \"\"\n }\n if (html.trim()) out += tagSourceLine(html, line)\n line += (tok.raw?.match(/\\n/g)?.length ?? 0)\n }\n return out || `<pre>${escapeHtml(source)}</pre>`\n}\n\n/**\n * Wrap rendered markdown in a complete, self-contained HTML document with\n * readable typography (works even before ai-or-die injects its annotation SDK).\n */\nexport function renderPlanHtml(source: string, title = \"Plan\"): string {\n return wrapArtifactDocument(renderMarkdownBody(source), title)\n}\n\nconst BASE_STYLE = ` :root {\n --bg: #0f1115; --fg: #f7f3ea; --muted: #aeb6c6; --border: #2a2f3a;\n --accent: #f4c95d; --code-bg: #171a21;\n --serif: 'Iowan Old Style', Georgia, 'Times New Roman', serif;\n --sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;\n --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n }\n * { box-sizing: border-box; }\n html, body { margin: 0; background: var(--bg); color: var(--fg); }\n body { font-family: var(--serif); font-size: 16px; line-height: 1.6;\n padding: 32px clamp(16px, 5vw, 56px); }\n main { max-width: 78ch; margin: 0 auto; }\n h1, h2, h3, h4 { font-family: var(--sans); line-height: 1.25; margin: 1.6em 0 0.5em; }\n h1 { font-size: 1.9em; } h2 { font-size: 1.45em; padding-bottom: 0.2em;\n border-bottom: 1px solid var(--border); } h3 { font-size: 1.2em; }\n p, li { color: var(--fg); }\n a { color: var(--accent); }\n code { font-family: var(--mono); font-size: 0.9em; background: var(--code-bg);\n padding: 0.12em 0.35em; border-radius: 5px; }\n pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px;\n padding: 12px 14px; overflow: auto; }\n pre code { background: none; padding: 0; }\n blockquote { margin: 1em 0; padding: 0.2em 1em; border-left: 3px solid var(--accent);\n color: var(--muted); }\n table { border-collapse: collapse; width: 100%; margin: 1em 0; font-family: var(--sans);\n font-size: 0.95em; }\n th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }\n th { background: var(--code-bg); }\n hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }\n ul, ol { padding-left: 1.4em; }`\n\nconst INTERACTIVE_STYLE = `\n .aod-group { margin: 1.4em 0; padding: 14px 16px; border: 1px solid var(--border);\n border-radius: 10px; background: var(--code-bg); }\n .aod-prompt { font-family: var(--sans); font-weight: 600; margin: 0 0 0.6em; }\n .aod-options { display: flex; flex-wrap: wrap; gap: 8px; }\n .aod-multi-select .aod-options { flex-direction: column; gap: 6px; }\n .aod-action, .aod-submit { font-family: var(--sans); font-size: 0.95em; font-weight: 600;\n cursor: pointer; border: 1px solid var(--border); border-radius: 8px;\n padding: 8px 12px; background: var(--bg); color: var(--fg); }\n .aod-action:hover, .aod-submit:hover { border-color: var(--accent); }\n .aod-submit { margin-top: 10px; background: var(--accent); color: #17130a; border-color: var(--accent); }\n .aod-check { display: flex; align-items: center; gap: 8px; font-family: var(--sans); cursor: pointer; }\n .aod-steps { list-style: decimal; }\n .aod-step { margin: 0.5em 0; }\n .aod-step-actions { display: inline-flex; gap: 6px; margin-left: 8px; }`\n\n/**\n * Wrap rendered inner HTML in the self-contained document shell. `extraStyle` is\n * appended to the inline stylesheet so the interactive variant can add its own\n * rules without a second `<style>` block. Static callers pass \"\" (default), so\n * the static document is byte-identical to the pre-refactor output.\n */\nfunction wrapArtifactDocument(body: string, title: string, extraStyle = \"\"): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHtml(title)}</title>\n<style>\n${BASE_STYLE}${extraStyle}\n</style>\n</head>\n<body>\n<main>\n${body}\n</main>\n</body>\n</html>\n`\n}\n\n/* -------------------------------------------------------------------------- */\n/* Interactive artifacts (contract v2.2 §4: declarative data-aod-* vocab) */\n/* -------------------------------------------------------------------------- */\n\nexport interface InteractiveOption {\n /** Stable id echoed back as `elementId` in the action event. Required. */\n elementId: string\n /** Human-visible label. */\n label: string\n /** Optional value echoed as `data-aod-value` (checkbox submits use \"true\"/\"false\"). */\n value?: string\n}\n\n/** One immediate-fire choice group: each option posts an action the instant it is clicked. */\nexport interface ChooseOneBlock {\n kind: \"choose-one\"\n /** `data-aod-group` shared by the options (marks them as one decision). */\n group: string\n prompt?: string\n /** `data-aod-action` verb for every option (default \"choose\"). */\n action?: string\n options: InteractiveOption[]\n}\n\n/** A multi-select group: checkbox toggles plus one submit that carries {group, selected}. */\nexport interface MultiSelectBlock {\n kind: \"multi-select\"\n group: string\n prompt?: string\n submitLabel?: string\n submitElementId?: string\n options: InteractiveOption[]\n}\n\nexport interface StepAction {\n /** `data-aod-action` verb (e.g. \"approve\", \"skip\"). */\n action: string\n label: string\n value?: string\n}\n\nexport interface StepItem {\n /** Stable `data-aod-id` echoed as `elementId`. */\n elementId: string\n label: string\n sourceLine?: number\n actions: StepAction[]\n}\n\n/** An ordered list of plan steps, each with its own inline action buttons. */\nexport interface StepsBlock {\n kind: \"steps\"\n steps: StepItem[]\n}\n\nexport type InteractiveBlock = ChooseOneBlock | MultiSelectBlock | StepsBlock\n\nexport interface InteractivePlanModel {\n /** Optional prose rendered above the interactive blocks (same escaping as static). */\n markdown?: string\n blocks: InteractiveBlock[]\n}\n\n/** Build a ` data-x=\"y\"` attribute string, dropping undefined values, all escaped. */\nfunction dataAttrs(pairs: Array<[string, string | undefined]>): string {\n let out = \"\"\n for (const [name, value] of pairs) {\n if (value === undefined) continue\n out += ` ${name}=\"${escapeHtml(value)}\"`\n }\n return out\n}\n\nfunction renderChooseOne(block: ChooseOneBlock): string {\n const action = block.action && block.action.trim() ? block.action : \"choose\"\n const prompt = block.prompt ? `<p class=\"aod-prompt\">${escapeHtml(block.prompt)}</p>` : \"\"\n const options = block.options\n .filter((o) => o.elementId.trim() !== \"\")\n .map((o) => {\n const attrs = dataAttrs([\n [\"data-aod-action\", action],\n [\"data-aod-group\", block.group],\n [\"data-aod-id\", o.elementId],\n [\"data-aod-value\", o.value],\n ])\n return `<button type=\"button\" class=\"aod-action\"${attrs}>${escapeHtml(o.label)}</button>`\n })\n .join(\"\\n\")\n return `<div class=\"aod-group aod-choose-one\"${dataAttrs([[\"data-aod-group\", block.group]])}>\n${prompt}<div class=\"aod-options\">\n${options}\n</div>\n</div>`\n}\n\nfunction renderMultiSelect(block: MultiSelectBlock): string {\n const prompt = block.prompt ? `<p class=\"aod-prompt\">${escapeHtml(block.prompt)}</p>` : \"\"\n const options = block.options\n .filter((o) => o.elementId.trim() !== \"\")\n .map((o) => {\n const attrs = dataAttrs([\n [\"data-aod-action\", \"check\"],\n [\"data-aod-group\", block.group],\n [\"data-aod-id\", o.elementId],\n [\"data-aod-value\", o.value],\n ])\n return `<label class=\"aod-check\"><input type=\"checkbox\"${attrs}> <span>${escapeHtml(o.label)}</span></label>`\n })\n .join(\"\\n\")\n const submitId = block.submitElementId && block.submitElementId.trim()\n ? block.submitElementId\n : `${block.group}-submit`\n const submitAttrs = dataAttrs([\n [\"data-aod-action\", \"submit\"],\n [\"data-aod-group\", block.group],\n [\"data-aod-id\", submitId],\n ])\n const submitLabel = block.submitLabel && block.submitLabel.trim() ? block.submitLabel : \"Submit\"\n return `<div class=\"aod-group aod-multi-select\"${dataAttrs([[\"data-aod-group\", block.group]])}>\n${prompt}<div class=\"aod-options\">\n${options}\n</div>\n<button type=\"button\" class=\"aod-submit\"${submitAttrs}>${escapeHtml(submitLabel)}</button>\n</div>`\n}\n\nfunction renderSteps(block: StepsBlock): string {\n const items = block.steps\n .filter((s) => s.elementId.trim() !== \"\")\n .map((s) => {\n const liAttrs = dataAttrs([\n [\"data-aod-id\", s.elementId],\n [\"data-source-line\", s.sourceLine === undefined ? undefined : String(s.sourceLine)],\n ])\n const actions = s.actions\n .map((a) => {\n const attrs = dataAttrs([\n [\"data-aod-action\", a.action],\n [\"data-aod-id\", s.elementId],\n [\"data-aod-value\", a.value],\n ])\n return `<button type=\"button\" class=\"aod-action\"${attrs}>${escapeHtml(a.label)}</button>`\n })\n .join(\"\")\n return `<li class=\"aod-step\"${liAttrs}><span class=\"aod-step-label\">${escapeHtml(s.label)}</span><span class=\"aod-step-actions\">${actions}</span></li>`\n })\n .join(\"\\n\")\n return `<ol class=\"aod-steps\">\n${items}\n</ol>`\n}\n\nfunction renderInteractiveBlock(block: InteractiveBlock): string {\n switch (block.kind) {\n case \"choose-one\":\n return renderChooseOne(block)\n case \"multi-select\":\n return renderMultiSelect(block)\n case \"steps\":\n return renderSteps(block)\n default:\n return \"\"\n }\n}\n\n/**\n * Render a typed interactive plan model into a self-contained HTML document that\n * carries the declarative `data-aod-*` action vocabulary the ai-or-die panel SDK\n * wires (contract v2.2 §4). Optional prose (`markdown`) is rendered with the SAME\n * escaping + dangerous-URL neutralization as the static path; all control\n * attributes and labels are HTML-escaped, so no author string can break out of an\n * attribute or inject live markup.\n */\nexport function renderInteractivePlanHtml(model: InteractivePlanModel, title = \"Plan\"): string {\n const prose = model.markdown && model.markdown.trim() ? renderMarkdownBody(model.markdown) : \"\"\n const blocks = model.blocks.map(renderInteractiveBlock).filter((h) => h.trim() !== \"\").join(\"\\n\")\n const body = [prose, blocks].filter((s) => s.trim() !== \"\").join(\"\\n\")\n return wrapArtifactDocument(body || `<pre>${escapeHtml(JSON.stringify(model))}</pre>`, title, INTERACTIVE_STYLE)\n}\n","/**\n * The internal `internal-artifact-open` subcommand: invoked by a spawned Claude\n * Code session's `PostToolUse` hook (matcher `ExitPlanMode`), registered into the\n * mirrored settings.json by the launcher when running inside an ai-or-die tab\n * (`AIORDIE_SESSION_ID` set). It auto-opens the just-finalized plan in the\n * ai-or-die Artifact review panel so the human reviews it WITHOUT the model\n * having to call artifact_open itself.\n *\n * Auth: AIORDIE_TOKEN is stripped from the child env, so the token can't come via\n * env or argv (argv leaks to `ps`). The launcher writes a mode-600\n * `.aiordie-artifact.json` into CLAUDE_CONFIG_DIR; this hook reads it.\n *\n * Plan: the EXACT plan for THIS session comes from the ExitPlanMode payload —\n * `tool_input.planFilePath` (per-session; never the globally-newest file in the\n * shared plans dir) with `tool_input.plan` as the markdown fallback. The plan\n * markdown is rendered to a self-contained styled HTML file (HTML is the\n * canonical artifact, lavish's model — formatted + annotatable, not raw md), and\n * THAT `.html` is opened.\n *\n * Side-effect only: never writes stdout, never throws, always exits 0. Skips\n * subagent payloads (agent_id/agent_type) like the other hooks.\n */\n\nimport { defineCommand } from \"citty\"\n\nimport { readFileSync, realpathSync, writeFileSync } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport path from \"node:path\"\n\nimport { ArtifactClient } from \"./lib/artifact/client\"\nimport { renderPlanHtml } from \"./lib/artifact/plan-html\"\nimport { isSubagentContext } from \"./lib/orchestration/stop-gate-policy\"\n\ninterface ArtifactCreds {\n baseUrl: string\n token: string\n sessionId: string\n insecureTLS: boolean\n}\n\ninterface ExitPlanPayload {\n planFilePath?: string\n planMarkdown?: string\n}\n\nfunction readStdin(): string {\n try {\n if (process.stdin.isTTY) return \"\"\n return readFileSync(0, \"utf8\")\n } catch {\n return \"\"\n }\n}\n\nfunction credsPath(): string {\n const dir = process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), \".claude\")\n return path.join(dir, \".aiordie-artifact.json\")\n}\n\nfunction isSubagent(payload: unknown): boolean {\n try {\n return isSubagentContext(payload as Parameters<typeof isSubagentContext>[0])\n } catch {\n return false\n }\n}\n\n/** Pull the per-session plan path + markdown content out of the hook payload. */\nexport function parseExitPlanPayload(raw: string): ExitPlanPayload {\n try {\n const obj = JSON.parse(raw) as { tool_input?: { planFilePath?: unknown; plan?: unknown } }\n const ti = obj.tool_input ?? {}\n const planFilePath = typeof ti.planFilePath === \"string\" && ti.planFilePath.trim() ? ti.planFilePath : undefined\n const planMarkdown = typeof ti.plan === \"string\" && ti.plan.trim() ? ti.plan : undefined\n return { planFilePath, planMarkdown }\n } catch {\n return {}\n }\n}\n\nfunction readCreds(): ArtifactCreds | undefined {\n try {\n const parsed = JSON.parse(readFileSync(credsPath(), \"utf8\")) as Partial<ArtifactCreds>\n if (!parsed.baseUrl || !parsed.token || !parsed.sessionId) return undefined\n return {\n baseUrl: parsed.baseUrl,\n token: parsed.token,\n sessionId: parsed.sessionId,\n insecureTLS: parsed.insecureTLS === true,\n }\n } catch {\n return undefined\n }\n}\n\n/**\n * Resolve the plan markdown + a title, preferring the file at `planFilePath`\n * (the source of truth, may be newer than the payload's snapshot), falling back\n * to the inline `tool_input.plan` content.\n */\nfunction resolvePlanMarkdown(p: ExitPlanPayload): { markdown: string; title: string; nearPath?: string } | undefined {\n if (p.planFilePath) {\n try {\n const md = readFileSync(p.planFilePath, \"utf8\")\n if (md.trim()) return { markdown: md, title: path.basename(p.planFilePath, path.extname(p.planFilePath)), nearPath: p.planFilePath }\n } catch {\n /* fall through to inline content */\n }\n }\n if (p.planMarkdown) return { markdown: p.planMarkdown, title: \"Plan\" }\n return undefined\n}\n\n/**\n * Write the rendered HTML next to the plan (an allowed root) when we have a real\n * path, else into the plans dir under CLAUDE_CONFIG_DIR. Returns the realpath of\n * the written `.html`, or undefined on failure.\n */\nfunction writePlanHtml(html: string, nearPath: string | undefined): string | undefined {\n let target: string\n if (nearPath) {\n // `<plan>.md` -> `<plan>.aiordie.html` (or append when there is no .md ext).\n const base = nearPath.replace(/\\.(md|markdown)$/i, \"\")\n target = `${base}.aiordie.html`\n } else {\n const dir = path.join(process.env.CLAUDE_CONFIG_DIR || path.join(homedir(), \".claude\"), \"plans\")\n target = path.join(dir, `aiordie-plan-${process.pid}.html`)\n }\n try {\n writeFileSync(target, html, \"utf8\")\n try {\n return realpathSync.native(target)\n } catch {\n return target\n }\n } catch {\n return undefined\n }\n}\n\nexport const internalArtifactOpen = defineCommand({\n meta: { name: \"internal-artifact-open\", description: \"Open the finalized plan in the ai-or-die panel\" },\n async run() {\n try {\n const raw = readStdin()\n let payload: unknown\n try {\n payload = JSON.parse(raw)\n } catch {\n payload = undefined\n }\n if (isSubagent(payload)) return\n const creds = readCreds()\n if (!creds) return\n const plan = resolvePlanMarkdown(parseExitPlanPayload(raw))\n if (!plan) return\n const htmlPath = writePlanHtml(renderPlanHtml(plan.markdown, plan.title), plan.nearPath)\n if (!htmlPath) return\n const client = new ArtifactClient(creds)\n await client.open(htmlPath)\n } catch {\n /* side-effect only: never block, never throw */\n }\n },\n})\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { n as internalFastDispatchGuard } from "./internal-fast-dispatch-guard-19xQEouu.js";
1
+ import { n as internalFastDispatchGuard } from "./internal-fast-dispatch-guard-CAwYj_Yz.js";
2
2
  export { internalFastDispatchGuard };