github-router 0.3.276 → 0.3.285

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 (107) hide show
  1. package/dist/{attribution-settings-VOPvSn6v.js → attribution-settings-B8M2fvhz.js} +44 -45
  2. package/dist/attribution-settings-B8M2fvhz.js.map +1 -0
  3. package/dist/{auth-CYoRwhC9.js → auth-VUL2Zxvw.js} +3 -3
  4. package/dist/{auth-CYoRwhC9.js.map → auth-VUL2Zxvw.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-DDUyg8ZB.js → check-usage-CcLdFPGr.js} +4 -4
  7. package/dist/{check-usage-DDUyg8ZB.js.map → check-usage-CcLdFPGr.js.map} +1 -1
  8. package/dist/{claude-OTHh7iKz.js → claude-e-JIQGhR.js} +55 -38
  9. package/dist/claude-e-JIQGhR.js.map +1 -0
  10. package/dist/{codex-tCxK30Su.js → codex-DSnVZ9oK.js} +5 -5
  11. package/dist/{codex-tCxK30Su.js.map → codex-DSnVZ9oK.js.map} +1 -1
  12. package/dist/{debug-CJgsxoVw.js → debug-CtzYWxpJ.js} +2 -2
  13. package/dist/{debug-CJgsxoVw.js.map → debug-CtzYWxpJ.js.map} +1 -1
  14. package/dist/engine-DMveEa9x.js +2 -0
  15. package/dist/{gate-discovery-nf7PGgnS.js → gate-discovery-BCFwLm0q.js} +5 -5
  16. package/dist/{gate-discovery-nf7PGgnS.js.map → gate-discovery-BCFwLm0q.js.map} +1 -1
  17. package/dist/{get-copilot-usage-D1OAH0EU.js → get-copilot-usage-B-EDAqQb.js} +2 -2
  18. package/dist/{get-copilot-usage-D1OAH0EU.js.map → get-copilot-usage-B-EDAqQb.js.map} +1 -1
  19. package/dist/hooks.mjs +37870 -0
  20. package/dist/hooks.sha256 +1 -0
  21. package/dist/{internal-artifact-open-BCz9lnq0.js → internal-artifact-open-Dj5Nlq0L.js} +2 -2
  22. package/dist/{internal-artifact-open-BCz9lnq0.js.map → internal-artifact-open-Dj5Nlq0L.js.map} +1 -1
  23. package/dist/{internal-first-mate-guard-7v2CtMA6.js → internal-first-mate-guard-B7p4NttK.js} +1 -1
  24. package/dist/{internal-first-mate-guard-La0tIjTU.js → internal-first-mate-guard-DOgFVki5.js} +5 -4
  25. package/dist/{internal-first-mate-guard-La0tIjTU.js.map → internal-first-mate-guard-DOgFVki5.js.map} +1 -1
  26. package/dist/{internal-plan-review-jRMpHu68.js → internal-plan-review-DRFHET_B.js} +11 -4
  27. package/dist/internal-plan-review-DRFHET_B.js.map +1 -0
  28. package/dist/{internal-prompt-submit-CqWRFCuv.js → internal-prompt-submit-f1LR2P2G.js} +4 -4
  29. package/dist/{internal-prompt-submit-CqWRFCuv.js.map → internal-prompt-submit-f1LR2P2G.js.map} +1 -1
  30. package/dist/{internal-session-bind-BiHWDxuC.js → internal-session-bind-DhZhxJ3T.js} +2 -2
  31. package/dist/{internal-session-bind-BiHWDxuC.js.map → internal-session-bind-DhZhxJ3T.js.map} +1 -1
  32. package/dist/{internal-stop-hook-Ds9LwmZA.js → internal-stop-hook-Cf-7w3RH.js} +68 -20
  33. package/dist/internal-stop-hook-Cf-7w3RH.js.map +1 -0
  34. package/dist/{internal-stop-review-BOFj7R3q.js → internal-stop-review-BBsLcbPG.js} +2 -2
  35. package/dist/{internal-stop-review-BOFj7R3q.js.map → internal-stop-review-BBsLcbPG.js.map} +1 -1
  36. package/dist/{internal-worker-guard-Lu8VHj5k.js → internal-worker-guard-CKgYFiFO.js} +2 -2
  37. package/dist/{internal-worker-guard-Lu8VHj5k.js.map → internal-worker-guard-CKgYFiFO.js.map} +1 -1
  38. package/dist/{internal-workspace-header-BRMz0Yql.js → internal-workspace-header-OYgHEnFt.js} +2 -2
  39. package/dist/{internal-workspace-header-BRMz0Yql.js.map → internal-workspace-header-OYgHEnFt.js.map} +1 -1
  40. package/dist/lib/tree-sitter-pool/lifecycle-C7W8D2lx.js +165 -0
  41. package/dist/lib/tree-sitter-pool/lifecycle-Dx0SnP7Z.js +157 -0
  42. package/dist/lib/tree-sitter-pool/worker.js +287 -14
  43. package/dist/{lifecycle-LA4rAuAL.js → lifecycle-B7CHqKlF.js} +2 -2
  44. package/dist/{lifecycle-LA4rAuAL.js.map → lifecycle-B7CHqKlF.js.map} +1 -1
  45. package/dist/lifecycle-CbmHMMSD.js +2 -0
  46. package/dist/{lifecycle-AGXnd-ZY.js → lifecycle-D-rL81tT.js} +2 -2
  47. package/dist/{lifecycle-AGXnd-ZY.js.map → lifecycle-D-rL81tT.js.map} +1 -1
  48. package/dist/lifecycle-DTcZwa7U.js +2 -0
  49. package/dist/lifecycle-K9oVtGde.mjs +16 -0
  50. package/dist/main.js +18 -18
  51. package/dist/{mcp-workspace-header-CGJbNeHb.js → mcp-workspace-header-ucs2SDST.js} +5 -6
  52. package/dist/mcp-workspace-header-ucs2SDST.js.map +1 -0
  53. package/dist/{models-4Q45Q2Dw.js → models-C16mBK2M.js} +3 -3
  54. package/dist/{models-4Q45Q2Dw.js.map → models-C16mBK2M.js.map} +1 -1
  55. package/dist/{orchestration-CpkG8ScN.js → orchestration-CtM6FYNx.js} +2 -2
  56. package/dist/{orchestration-CpkG8ScN.js.map → orchestration-CtM6FYNx.js.map} +1 -1
  57. package/dist/package-root-B-osctCk.js +29 -0
  58. package/dist/package-root-B-osctCk.js.map +1 -0
  59. package/dist/paths-CV9K7Xqm.js +2 -0
  60. package/dist/{paths-j2B7b0DZ.js → paths-wLC0InjX.js} +28 -4
  61. package/dist/paths-wLC0InjX.js.map +1 -0
  62. package/dist/{peer-mcp-personas-DRL_xT4h.js → peer-mcp-personas-V6stFvpq.js} +1158 -297
  63. package/dist/peer-mcp-personas-V6stFvpq.js.map +1 -0
  64. package/dist/{plan-review-hook-Dk5zTNke.js → plan-review-hook-Lf9ISdF6.js} +5 -6
  65. package/dist/plan-review-hook-Lf9ISdF6.js.map +1 -0
  66. package/dist/{prompt-submit-hook-lvTWwaTV.js → prompt-submit-hook-BlijaOn7.js} +6 -11
  67. package/dist/prompt-submit-hook-BlijaOn7.js.map +1 -0
  68. package/dist/{provision-HzZ547dW.js → provision-BpL6gZIt.js} +4 -4
  69. package/dist/{provision-HzZ547dW.js.map → provision-BpL6gZIt.js.map} +1 -1
  70. package/dist/self-invocation-CKMjcA5F.js +380 -0
  71. package/dist/self-invocation-CKMjcA5F.js.map +1 -0
  72. package/dist/{serve-CM3OmF4I.js → serve-CXUf7RtJ.js} +34 -27
  73. package/dist/serve-CXUf7RtJ.js.map +1 -0
  74. package/dist/{server-setup-C9r7jYnz.js → server-setup-Bppjt9xO.js} +81 -207
  75. package/dist/server-setup-Bppjt9xO.js.map +1 -0
  76. package/dist/{start-KAUCsDao.js → start-C1-jrHfU.js} +3 -3
  77. package/dist/{start-KAUCsDao.js.map → start-C1-jrHfU.js.map} +1 -1
  78. package/dist/{stop-gate-hook-CaUrldLh.js → stop-gate-hook-DgJ6sW8N.js} +100 -52
  79. package/dist/stop-gate-hook-DgJ6sW8N.js.map +1 -0
  80. package/dist/{stop-gate-policy-lAabj_pP.js → stop-gate-policy-DG5yYWGn.js} +2 -2
  81. package/dist/{stop-gate-policy-lAabj_pP.js.map → stop-gate-policy-DG5yYWGn.js.map} +1 -1
  82. package/dist/{token-CxPaBEUS.js → token-CnlB0884.js} +2 -2
  83. package/dist/token-CnlB0884.js.map +1 -0
  84. package/dist/{version-_Q1WpsQp.js → version-C8x2hQrZ.js} +14 -2
  85. package/dist/version-C8x2hQrZ.js.map +1 -0
  86. package/dist/{worker-dispatch-Bj1uYyG9.js → worker-dispatch-zW8Zi69V.js} +11 -7
  87. package/dist/worker-dispatch-zW8Zi69V.js.map +1 -0
  88. package/package.json +2 -2
  89. package/dist/attribution-settings-VOPvSn6v.js.map +0 -1
  90. package/dist/claude-OTHh7iKz.js.map +0 -1
  91. package/dist/engine-BKJKMAzh.js +0 -2
  92. package/dist/internal-plan-review-jRMpHu68.js.map +0 -1
  93. package/dist/internal-stop-hook-Ds9LwmZA.js.map +0 -1
  94. package/dist/lifecycle-DCrKbaIU.js +0 -2
  95. package/dist/lifecycle-KKSTKwd6.js +0 -2
  96. package/dist/mcp-workspace-header-CGJbNeHb.js.map +0 -1
  97. package/dist/paths-DN3Nio42.js +0 -2
  98. package/dist/paths-j2B7b0DZ.js.map +0 -1
  99. package/dist/peer-mcp-personas-DRL_xT4h.js.map +0 -1
  100. package/dist/plan-review-hook-Dk5zTNke.js.map +0 -1
  101. package/dist/prompt-submit-hook-lvTWwaTV.js.map +0 -1
  102. package/dist/serve-CM3OmF4I.js.map +0 -1
  103. package/dist/server-setup-C9r7jYnz.js.map +0 -1
  104. package/dist/stop-gate-hook-CaUrldLh.js.map +0 -1
  105. package/dist/token-CxPaBEUS.js.map +0 -1
  106. package/dist/version-_Q1WpsQp.js.map +0 -1
  107. package/dist/worker-dispatch-Bj1uYyG9.js.map +0 -1
@@ -0,0 +1,380 @@
1
+ import { t as PACKAGE_ROOT_FLAG } from "./package-root-B-osctCk.js";
2
+ import { t as PATHS } from "./paths-wLC0InjX.js";
3
+ import consola from "consola";
4
+ import { chmodSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { createHash } from "node:crypto";
8
+ import os from "node:os";
9
+ //#region src/lib/hook-launcher/provision.ts
10
+ /**
11
+ * Publish the self-contained hook launcher (`dist/hooks.mjs`) to a stable,
12
+ * content-addressed path under `<APP_DIR>/hooks/`.
13
+ *
14
+ * Why this exists: `github-router claude` persists Claude Code hook commands
15
+ * into a settings.json that outlives the process that wrote it, built from
16
+ * `process.argv[1]`. Under `bunx` that path is inside `$TMPDIR`, and macOS's
17
+ * per-user temp reaper deletes files there (keeping the directory skeleton) —
18
+ * so `node <tmp>/…/dist/main.js` died on its first bare import with
19
+ * `ERR_MODULE_NOT_FOUND` and EVERY hook in a long-running session failed until
20
+ * the tree happened to be re-extracted. `bunx pkg@latest` also re-extracts in
21
+ * place whenever npm's `latest` advances, so a live session's hooks could
22
+ * silently start executing a different version's code.
23
+ *
24
+ * Deliberate departures from `provisionBrowserAssets()`, which this otherwise
25
+ * mirrors:
26
+ *
27
+ * - **Content-addressed, immutable names.** The digest IS the filename, so a
28
+ * new build never overwrites an old launcher and there is nothing to
29
+ * garbage-collect. Version-keyed names would need GC, and GC across
30
+ * concurrent proxies is a race: proxy N deleting N+1's launcher (or the
31
+ * reverse) breaks a live session's hooks. At ~1.5 MB, keeping stale
32
+ * launchers costs less than the race would.
33
+ *
34
+ * - **The source is verified before it is published.** Hashing the bytes
35
+ * read from `dist/hooks.mjs` only identifies those bytes; it does not
36
+ * prove they are a complete artifact. A concurrent in-place `bunx`
37
+ * extraction can hand over a truncated file, which content-addressing
38
+ * would happily preserve forever under its own hash. The build writes a
39
+ * `dist/hooks.sha256` sidecar and we publish only on a match.
40
+ *
41
+ * - **Failures are not latched.** A transient `EBUSY` (Windows antivirus, a
42
+ * concurrent reader) must not disable provisioning for the process
43
+ * lifetime, so only success is cached.
44
+ *
45
+ * Never throws: the caller degrades to `process.argv[1]` with a visible
46
+ * warning rather than failing the launch.
47
+ */
48
+ /** Sidecar written by `scripts/stamp-hooks-digest.ts` next to the bundle. */
49
+ const DIGEST_SIDECAR = "hooks.sha256";
50
+ /** Basename of the built launcher bundle. */
51
+ const BUNDLE_NAME = "hooks.mjs";
52
+ /**
53
+ * Publish retries. A rename can lose a race with another proxy publishing the
54
+ * same digest, or hit a Windows sharing violation while an antivirus scanner
55
+ * holds the temp file. Both clear in milliseconds.
56
+ */
57
+ const PUBLISH_ATTEMPTS = 3;
58
+ let _published;
59
+ let _inFlight;
60
+ /**
61
+ * Resolve the built bundle that sits beside the running code.
62
+ *
63
+ * In a built layout every chunk lives in `dist/`, so a sibling lookup finds
64
+ * `dist/hooks.mjs`. Running from source it resolves to
65
+ * `src/lib/hook-launcher/hooks.mjs`, which does not exist — which is exactly
66
+ * the wanted behaviour: a `bun run dev` session must keep using
67
+ * `process.argv[1]` rather than silently adopting a stale bundle from some
68
+ * earlier build.
69
+ */
70
+ function bundledLauncherPath() {
71
+ try {
72
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), BUNDLE_NAME);
73
+ } catch {
74
+ return;
75
+ }
76
+ }
77
+ function sha256(buf) {
78
+ return createHash("sha256").update(buf).digest("hex");
79
+ }
80
+ /**
81
+ * Read the build's expected digest. Absent or malformed means "cannot verify",
82
+ * which is treated as a refusal to publish rather than as permission to skip
83
+ * the check — an unverified source is the one thing content-addressing cannot
84
+ * recover from.
85
+ */
86
+ function expectedDigest(bundlePath) {
87
+ try {
88
+ const raw = readFileSync(path.join(path.dirname(bundlePath), DIGEST_SIDECAR), "utf8").trim();
89
+ return /^[0-9a-f]{64}$/.test(raw) ? raw : void 0;
90
+ } catch {
91
+ return;
92
+ }
93
+ }
94
+ /** True when `target` already holds exactly the bytes its name claims. */
95
+ function targetIsValid(target, digest) {
96
+ try {
97
+ return sha256(readFileSync(target)) === digest;
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * Write `bytes` to `target` via a unique same-directory temp file plus a
104
+ * rename, so a crash mid-write can never leave a partial file under the name
105
+ * a hook command points at.
106
+ *
107
+ * The target is never deleted first: on Windows that would open a window where
108
+ * a concurrently-launching session finds no launcher at all. A rename onto an
109
+ * existing path can fail there, so a losing racer validates what the winner
110
+ * wrote and accepts it.
111
+ */
112
+ function publish(target, bytes, digest) {
113
+ if (targetIsValid(target, digest)) return true;
114
+ for (let attempt = 0; attempt < PUBLISH_ATTEMPTS; attempt++) {
115
+ const tmp = `${target}.${process.pid}-${attempt}.tmp`;
116
+ try {
117
+ writeFileSync(tmp, bytes, { mode: 384 });
118
+ renameSync(tmp, target);
119
+ return true;
120
+ } catch {
121
+ try {
122
+ rmSync(tmp, { force: true });
123
+ } catch {}
124
+ }
125
+ }
126
+ return targetIsValid(target, digest);
127
+ }
128
+ /** A stranded publish temp is dead well inside this. */
129
+ const TMP_MAX_AGE_MS = 864e5;
130
+ /**
131
+ * Launchers older than this are reaped. Generous on purpose — see the residual
132
+ * risk in `sweepStaleLaunchers`.
133
+ */
134
+ const LAUNCHER_MAX_AGE_MS = 2592e6;
135
+ /**
136
+ * Reap stranded publish temps and long-unused launchers.
137
+ *
138
+ * Without this the directory grows by ~1.5 MB per shipped version forever, and
139
+ * this project ships a patch release per PR with self-update on by default.
140
+ *
141
+ * The immutable content-addressed naming exists to avoid a GC race, so the
142
+ * sweep is built to not reintroduce one: it never touches the digest THIS
143
+ * launch resolved, and every launch refreshes its own launcher's mtime, so a
144
+ * file survives for as long as some proxy keeps launching against it. Residual
145
+ * risk, stated plainly: a single proxy running continuously for more than 30
146
+ * days without relaunching could have its launcher reaped by a newer proxy.
147
+ * That is bounded and far rarer than the unbounded growth it prevents, but it
148
+ * is not zero — a PID-liveness marker would close it if it ever bites.
149
+ */
150
+ function sweepStaleLaunchers(dir, keep) {
151
+ const now = Date.now();
152
+ let entries;
153
+ try {
154
+ entries = readdirSync(dir);
155
+ } catch {
156
+ return;
157
+ }
158
+ for (const name of entries) {
159
+ if (name === path.basename(keep)) continue;
160
+ const isTmp = name.endsWith(".tmp");
161
+ const isLauncher = /^hooks-[0-9a-f]{64}\.mjs$/.test(name);
162
+ if (!isTmp && !isLauncher) continue;
163
+ const target = path.join(dir, name);
164
+ try {
165
+ if (now - statSync(target).mtimeMs > (isTmp ? TMP_MAX_AGE_MS : LAUNCHER_MAX_AGE_MS)) rmSync(target, { force: true });
166
+ } catch {}
167
+ }
168
+ }
169
+ /**
170
+ * Materialize the launcher and return its stable absolute path, or undefined
171
+ * when it could not be published (running from source, bundle missing or
172
+ * unverifiable, filesystem refusing the write).
173
+ *
174
+ * Single-flight so the startup call and any lazy caller collapse to one run.
175
+ */
176
+ function provisionHookLauncher() {
177
+ if (_published) return Promise.resolve(_published);
178
+ if (_inFlight) return _inFlight;
179
+ _inFlight = Promise.resolve().then(() => provisionImpl()).then((result) => {
180
+ if (result) _published = result;
181
+ return result;
182
+ }).finally(() => {
183
+ _inFlight = void 0;
184
+ });
185
+ return _inFlight;
186
+ }
187
+ function provisionImpl() {
188
+ const bundlePath = bundledLauncherPath();
189
+ if (!bundlePath) return void 0;
190
+ return publishLauncherFrom(bundlePath);
191
+ }
192
+ /**
193
+ * Verify and publish one specific bundle. Split out from the source resolution
194
+ * above so the publication semantics are testable: running from source,
195
+ * `bundledLauncherPath()` deliberately points at a file that does not exist,
196
+ * which is the right behaviour but leaves nothing to exercise.
197
+ *
198
+ * @internal — production callers go through `provisionHookLauncher()`.
199
+ */
200
+ function publishLauncherFrom(bundlePath) {
201
+ try {
202
+ let bytes;
203
+ try {
204
+ bytes = readFileSync(bundlePath);
205
+ } catch {
206
+ return;
207
+ }
208
+ const digest = sha256(bytes);
209
+ const expected = expectedDigest(bundlePath);
210
+ if (!expected) {
211
+ consola.debug("[hook-launcher] no digest sidecar beside the bundle; not publishing");
212
+ return;
213
+ }
214
+ if (expected !== digest) {
215
+ consola.debug("[hook-launcher] bundle digest does not match the build; not publishing");
216
+ return;
217
+ }
218
+ const dir = PATHS.HOOK_LAUNCHER_DIR;
219
+ mkdirSync(dir, {
220
+ recursive: true,
221
+ mode: 448
222
+ });
223
+ try {
224
+ chmodSync(dir, 448);
225
+ } catch {}
226
+ const target = path.join(dir, `hooks-${digest}.mjs`);
227
+ if (!publish(target, bytes, digest)) return void 0;
228
+ try {
229
+ const now = /* @__PURE__ */ new Date();
230
+ utimesSync(target, now, now);
231
+ } catch {}
232
+ sweepStaleLaunchers(dir, target);
233
+ return target;
234
+ } catch (err) {
235
+ consola.debug("[hook-launcher] provisioning skipped:", err);
236
+ return;
237
+ }
238
+ }
239
+ //#endregion
240
+ //#region src/lib/hook-launcher/self-invocation.ts
241
+ /**
242
+ * How this process names ITSELF in a command string that gets persisted to
243
+ * disk and executed later by someone else.
244
+ *
245
+ * Every Claude Code hook, the MCP `headersHelper`, and the worker-guard are
246
+ * registered as shell commands inside a settings.json that outlives the
247
+ * process that wrote it. They were built from `(process.execPath,
248
+ * process.argv[1])`, which under `bunx` points into `$TMPDIR` — a directory
249
+ * macOS reaps and `bunx pkg@latest` re-extracts in place. A persisted command
250
+ * pointing there stops working mid-session (see
251
+ * `src/lib/hook-launcher/provision.ts` for the failure in full).
252
+ *
253
+ * So the pair is resolved ONCE per launch, through the stable relocated
254
+ * launcher when one can be published, and every builder composes its command
255
+ * from that single answer.
256
+ */
257
+ function quote(s) {
258
+ return `"${s}"`;
259
+ }
260
+ /**
261
+ * Compose a persisted command string: the binary, the script when it differs
262
+ * from the binary, the baked package root when there is one, then the args.
263
+ *
264
+ * Every builder in the codebase routes through here so the three parts cannot
265
+ * drift apart — and so a future part is added in one place, not eleven.
266
+ */
267
+ function buildSelfCommand(inv, args) {
268
+ const parts = [quote(inv.execPath)];
269
+ if (inv.scriptPath && inv.scriptPath !== inv.execPath) parts.push(quote(inv.scriptPath));
270
+ if (inv.packageRoot) parts.push(PACKAGE_ROOT_FLAG, quote(inv.packageRoot));
271
+ if (args) parts.push(args);
272
+ return parts.join(" ");
273
+ }
274
+ /** The invocation describing the currently running entrypoint, unrelocated. */
275
+ function currentInvocation() {
276
+ return {
277
+ execPath: process.execPath,
278
+ scriptPath: process.argv[1]
279
+ };
280
+ }
281
+ let _resolved;
282
+ /**
283
+ * Resolve the invocation to bake into every persisted command this launch
284
+ * writes. Awaited BEFORE the settings are written, never fire-and-forget: a
285
+ * command persisted while provisioning is still pending would name the very
286
+ * temp path this exists to avoid, and the session would stay broken however
287
+ * well provisioning later went.
288
+ *
289
+ * Falls back to the running entrypoint if the launcher cannot be published, so
290
+ * a provisioning failure degrades rather than breaking a launch. The caller is
291
+ * responsible for surfacing that fallback — see `hookLauncherDegradedWarning`.
292
+ */
293
+ async function resolveSelfInvocation() {
294
+ if (_resolved) return _resolved;
295
+ const launcher = await provisionHookLauncher();
296
+ _resolved = launcher ? {
297
+ execPath: process.execPath,
298
+ scriptPath: launcher,
299
+ packageRoot: currentPackageRoot()
300
+ } : currentInvocation();
301
+ return _resolved;
302
+ }
303
+ /**
304
+ * A human-facing warning when this launch is about to persist hook commands
305
+ * pointing into a directory the OS may reap — the exact configuration that
306
+ * produced the original bug.
307
+ *
308
+ * Returns the message rather than logging it, mirroring
309
+ * `colbertDegradedWarning`: `claude` and `codex` call `enableFileLogging()`,
310
+ * which redirects `consola.warn` to the error log, so a warning logged here
311
+ * would be invisible on the one path where it matters most. The caller writes
312
+ * it to stderr next to the readiness line instead.
313
+ *
314
+ * Silent when running from source, which has no bundle to publish and is not a
315
+ * degraded state.
316
+ */
317
+ function hookLauncherDegradedWarning(inv) {
318
+ if (!inv.scriptPath) return null;
319
+ if (inv.scriptPath.startsWith(PATHS.HOOK_LAUNCHER_DIR)) {
320
+ if (inv.packageRoot) return null;
321
+ return "Hook launcher published without a package root; hook version reporting will be degraded. This usually means the install tree is partially missing — reinstall github-router.";
322
+ }
323
+ if (isUnderVolatileRoot(inv.scriptPath)) return "Hook commands point into a temporary directory and will stop working when the OS reaps it (the stable launcher could not be published). Reinstall with `npm install -g github-router` to avoid this.";
324
+ return null;
325
+ }
326
+ /**
327
+ * The package root of the RUNNING code, derived while we are still inside the
328
+ * package tree — the one moment it is knowable without guessing.
329
+ *
330
+ * The name is checked, not just the presence of a package.json: this value is
331
+ * baked into a persisted command and consumed by `packageRoot()` as the
332
+ * highest-priority source, so handing it the WRONG root would be worse than
333
+ * handing it none. Matches `findPackageRoot()` in
334
+ * `browser-mcp/native-host-installer.ts`, whose fallbacks this supersedes.
335
+ */
336
+ function currentPackageRoot() {
337
+ const entry = typeof process.argv[1] === "string" ? process.argv[1] : void 0;
338
+ if (!entry) return void 0;
339
+ let cur = path.dirname(entry);
340
+ for (let i = 0; i < 10; i++) {
341
+ try {
342
+ const pkg = JSON.parse(readFileSync(path.join(cur, "package.json"), "utf8"));
343
+ if (typeof pkg.name === "string" && pkg.name.includes("github-router")) return cur;
344
+ } catch {}
345
+ const parent = path.dirname(cur);
346
+ if (parent === cur) break;
347
+ cur = parent;
348
+ }
349
+ }
350
+ /**
351
+ * Whether a path sits under a directory the OS is entitled to clean out from
352
+ * under a running session: the per-user temp dir on every platform, which is
353
+ * where `bunx` (unlike `npx`, which uses a cache under $HOME) installs.
354
+ *
355
+ * Both sides are compared in resolved AND unresolved form. macOS reports
356
+ * `$TMPDIR` as `/var/folders/…` while a process sees `/private/var/folders/…`,
357
+ * so a single-form prefix test silently never matches; and `realpathSync`
358
+ * fails on a path that does not exist, which would otherwise make the answer
359
+ * depend on whether the file happens to be there.
360
+ */
361
+ function isUnderVolatileRoot(target) {
362
+ try {
363
+ const prefixes = /* @__PURE__ */ new Set();
364
+ for (const root of [os.tmpdir(), realpathIfPossible(os.tmpdir())]) prefixes.add(root.endsWith(path.sep) ? root : root + path.sep);
365
+ return [target, realpathIfPossible(target)].some((t) => [...prefixes].some((p) => t.startsWith(p)));
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+ function realpathIfPossible(p) {
371
+ try {
372
+ return realpathSync(p);
373
+ } catch {
374
+ return p;
375
+ }
376
+ }
377
+ //#endregion
378
+ export { hookLauncherDegradedWarning as n, resolveSelfInvocation as r, buildSelfCommand as t };
379
+
380
+ //# sourceMappingURL=self-invocation-CKMjcA5F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-invocation-CKMjcA5F.js","names":[],"sources":["../src/lib/hook-launcher/provision.ts","../src/lib/hook-launcher/self-invocation.ts"],"sourcesContent":["/**\n * Publish the self-contained hook launcher (`dist/hooks.mjs`) to a stable,\n * content-addressed path under `<APP_DIR>/hooks/`.\n *\n * Why this exists: `github-router claude` persists Claude Code hook commands\n * into a settings.json that outlives the process that wrote it, built from\n * `process.argv[1]`. Under `bunx` that path is inside `$TMPDIR`, and macOS's\n * per-user temp reaper deletes files there (keeping the directory skeleton) —\n * so `node <tmp>/…/dist/main.js` died on its first bare import with\n * `ERR_MODULE_NOT_FOUND` and EVERY hook in a long-running session failed until\n * the tree happened to be re-extracted. `bunx pkg@latest` also re-extracts in\n * place whenever npm's `latest` advances, so a live session's hooks could\n * silently start executing a different version's code.\n *\n * Deliberate departures from `provisionBrowserAssets()`, which this otherwise\n * mirrors:\n *\n * - **Content-addressed, immutable names.** The digest IS the filename, so a\n * new build never overwrites an old launcher and there is nothing to\n * garbage-collect. Version-keyed names would need GC, and GC across\n * concurrent proxies is a race: proxy N deleting N+1's launcher (or the\n * reverse) breaks a live session's hooks. At ~1.5 MB, keeping stale\n * launchers costs less than the race would.\n *\n * - **The source is verified before it is published.** Hashing the bytes\n * read from `dist/hooks.mjs` only identifies those bytes; it does not\n * prove they are a complete artifact. A concurrent in-place `bunx`\n * extraction can hand over a truncated file, which content-addressing\n * would happily preserve forever under its own hash. The build writes a\n * `dist/hooks.sha256` sidecar and we publish only on a match.\n *\n * - **Failures are not latched.** A transient `EBUSY` (Windows antivirus, a\n * concurrent reader) must not disable provisioning for the process\n * lifetime, so only success is cached.\n *\n * Never throws: the caller degrades to `process.argv[1]` with a visible\n * warning rather than failing the launch.\n */\n\nimport { createHash } from \"node:crypto\"\nimport {\n chmodSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n utimesSync,\n writeFileSync,\n} from \"node:fs\"\nimport path from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nimport consola from \"consola\"\n\nimport { PATHS } from \"../paths\"\n\n/** Sidecar written by `scripts/stamp-hooks-digest.ts` next to the bundle. */\nconst DIGEST_SIDECAR = \"hooks.sha256\"\n\n/** Basename of the built launcher bundle. */\nconst BUNDLE_NAME = \"hooks.mjs\"\n\n/**\n * Publish retries. A rename can lose a race with another proxy publishing the\n * same digest, or hit a Windows sharing violation while an antivirus scanner\n * holds the temp file. Both clear in milliseconds.\n */\nconst PUBLISH_ATTEMPTS = 3\n\nlet _published: string | undefined\nlet _inFlight: Promise<string | undefined> | undefined\n\n/** @internal — reset module state between test cases. */\nexport function __resetHookLauncherForTests(): void {\n _published = undefined\n _inFlight = undefined\n}\n\n/**\n * Resolve the built bundle that sits beside the running code.\n *\n * In a built layout every chunk lives in `dist/`, so a sibling lookup finds\n * `dist/hooks.mjs`. Running from source it resolves to\n * `src/lib/hook-launcher/hooks.mjs`, which does not exist — which is exactly\n * the wanted behaviour: a `bun run dev` session must keep using\n * `process.argv[1]` rather than silently adopting a stale bundle from some\n * earlier build.\n */\nfunction bundledLauncherPath(): string | undefined {\n try {\n return path.join(path.dirname(fileURLToPath(import.meta.url)), BUNDLE_NAME)\n } catch {\n return undefined\n }\n}\n\nfunction sha256(buf: Buffer): string {\n return createHash(\"sha256\").update(buf).digest(\"hex\")\n}\n\n/**\n * Read the build's expected digest. Absent or malformed means \"cannot verify\",\n * which is treated as a refusal to publish rather than as permission to skip\n * the check — an unverified source is the one thing content-addressing cannot\n * recover from.\n */\nfunction expectedDigest(bundlePath: string): string | undefined {\n try {\n const raw = readFileSync(\n path.join(path.dirname(bundlePath), DIGEST_SIDECAR),\n \"utf8\",\n ).trim()\n return /^[0-9a-f]{64}$/.test(raw) ? raw : undefined\n } catch {\n return undefined\n }\n}\n\n/** True when `target` already holds exactly the bytes its name claims. */\nfunction targetIsValid(target: string, digest: string): boolean {\n try {\n return sha256(readFileSync(target)) === digest\n } catch {\n return false\n }\n}\n\n/**\n * Write `bytes` to `target` via a unique same-directory temp file plus a\n * rename, so a crash mid-write can never leave a partial file under the name\n * a hook command points at.\n *\n * The target is never deleted first: on Windows that would open a window where\n * a concurrently-launching session finds no launcher at all. A rename onto an\n * existing path can fail there, so a losing racer validates what the winner\n * wrote and accepts it.\n */\nfunction publish(target: string, bytes: Buffer, digest: string): boolean {\n // Checked once up front rather than per attempt: validation re-hashes ~1.5 MB\n // and the retries exist for rename contention, which revalidating cannot fix.\n if (targetIsValid(target, digest)) return true\n for (let attempt = 0; attempt < PUBLISH_ATTEMPTS; attempt++) {\n const tmp = `${target}.${process.pid}-${attempt}.tmp`\n try {\n writeFileSync(tmp, bytes, { mode: 0o600 })\n renameSync(tmp, target)\n return true\n } catch {\n try {\n rmSync(tmp, { force: true })\n } catch {\n // Best-effort. A Windows sharing violation can strand the temp file;\n // `sweepStaleLaunchers` reaps it later rather than leaking 1.5 MB.\n }\n }\n }\n return targetIsValid(target, digest)\n}\n\n/** A stranded publish temp is dead well inside this. */\nconst TMP_MAX_AGE_MS = 24 * 60 * 60 * 1000\n\n/**\n * Launchers older than this are reaped. Generous on purpose — see the residual\n * risk in `sweepStaleLaunchers`.\n */\nconst LAUNCHER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\n/**\n * Reap stranded publish temps and long-unused launchers.\n *\n * Without this the directory grows by ~1.5 MB per shipped version forever, and\n * this project ships a patch release per PR with self-update on by default.\n *\n * The immutable content-addressed naming exists to avoid a GC race, so the\n * sweep is built to not reintroduce one: it never touches the digest THIS\n * launch resolved, and every launch refreshes its own launcher's mtime, so a\n * file survives for as long as some proxy keeps launching against it. Residual\n * risk, stated plainly: a single proxy running continuously for more than 30\n * days without relaunching could have its launcher reaped by a newer proxy.\n * That is bounded and far rarer than the unbounded growth it prevents, but it\n * is not zero — a PID-liveness marker would close it if it ever bites.\n */\nfunction sweepStaleLaunchers(dir: string, keep: string): void {\n const now = Date.now()\n let entries: Array<string>\n try {\n entries = readdirSync(dir)\n } catch {\n return\n }\n for (const name of entries) {\n if (name === path.basename(keep)) continue\n const isTmp = name.endsWith(\".tmp\")\n // Strict shape match, like sweepAgedWorkerDiffs: this directory is ours,\n // but a name-shape check means a false positive can only ever cost a file\n // the next launch regenerates.\n const isLauncher = /^hooks-[0-9a-f]{64}\\.mjs$/.test(name)\n if (!isTmp && !isLauncher) continue\n const target = path.join(dir, name)\n try {\n const age = now - statSync(target).mtimeMs\n if (age > (isTmp ? TMP_MAX_AGE_MS : LAUNCHER_MAX_AGE_MS)) {\n rmSync(target, { force: true })\n }\n } catch {\n /* best-effort */\n }\n }\n}\n\n/**\n * Materialize the launcher and return its stable absolute path, or undefined\n * when it could not be published (running from source, bundle missing or\n * unverifiable, filesystem refusing the write).\n *\n * Single-flight so the startup call and any lazy caller collapse to one run.\n */\nexport function provisionHookLauncher(): Promise<string | undefined> {\n if (_published) return Promise.resolve(_published)\n if (_inFlight) return _inFlight\n _inFlight = Promise.resolve()\n .then(() => provisionImpl())\n .then((result) => {\n // Cache SUCCESS only. Latching a transient failure would disable the\n // stable launcher for the whole process lifetime.\n if (result) _published = result\n return result\n })\n .finally(() => {\n _inFlight = undefined\n })\n return _inFlight\n}\n\nfunction provisionImpl(): string | undefined {\n const bundlePath = bundledLauncherPath()\n if (!bundlePath) return undefined\n return publishLauncherFrom(bundlePath)\n}\n\n/**\n * Verify and publish one specific bundle. Split out from the source resolution\n * above so the publication semantics are testable: running from source,\n * `bundledLauncherPath()` deliberately points at a file that does not exist,\n * which is the right behaviour but leaves nothing to exercise.\n *\n * @internal — production callers go through `provisionHookLauncher()`.\n */\nexport function publishLauncherFrom(bundlePath: string): string | undefined {\n try {\n let bytes: Buffer\n try {\n bytes = readFileSync(bundlePath)\n } catch {\n // Running from source, or the package tree was reaped out from under us.\n // Either way there is nothing to publish; a launcher published by an\n // earlier launch still stands.\n return undefined\n }\n\n const digest = sha256(bytes)\n const expected = expectedDigest(bundlePath)\n if (!expected) {\n // \"Cannot verify\" and \"verified bad\" both mean do-not-publish. Treating\n // an absent sidecar as permission would delete the integrity check in\n // exactly the degraded conditions it exists for.\n consola.debug(\n \"[hook-launcher] no digest sidecar beside the bundle; not publishing\",\n )\n return undefined\n }\n if (expected !== digest) {\n // The classic cause is reading mid-`bunx`-re-extraction. Publishing here\n // would freeze a torn bundle under a stable name forever, because\n // content-addressing gives a truncated file its own perfectly valid hash.\n consola.debug(\n \"[hook-launcher] bundle digest does not match the build; not publishing\",\n )\n return undefined\n }\n\n const dir = PATHS.HOOK_LAUNCHER_DIR\n // 0o700 / 0o600, matching the convention for the runtime dir in paths.ts.\n // This is not incidental hygiene: the published file is EXECUTED by node on\n // every prompt of every session, so a group- or world-writable path here\n // would let a local attacker swap the launcher and run code inside the\n // user's agent. chmod is best-effort — it is a no-op on Windows, where the\n // ACL inherited from APP_DIR governs.\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n try {\n chmodSync(dir, 0o700)\n } catch {\n /* best-effort: Windows, or a dir we do not own */\n }\n const target = path.join(dir, `hooks-${digest}.mjs`)\n if (!publish(target, bytes, digest)) return undefined\n\n // Mark this launcher as still in use before sweeping, so a long-lived\n // install that keeps relaunching against an old version is never reaped by\n // a newer proxy's sweep.\n try {\n const now = new Date()\n utimesSync(target, now, now)\n } catch {\n /* best-effort */\n }\n sweepStaleLaunchers(dir, target)\n return target\n } catch (err) {\n consola.debug(\"[hook-launcher] provisioning skipped:\", err)\n return undefined\n }\n}\n","/**\n * How this process names ITSELF in a command string that gets persisted to\n * disk and executed later by someone else.\n *\n * Every Claude Code hook, the MCP `headersHelper`, and the worker-guard are\n * registered as shell commands inside a settings.json that outlives the\n * process that wrote it. They were built from `(process.execPath,\n * process.argv[1])`, which under `bunx` points into `$TMPDIR` — a directory\n * macOS reaps and `bunx pkg@latest` re-extracts in place. A persisted command\n * pointing there stops working mid-session (see\n * `src/lib/hook-launcher/provision.ts` for the failure in full).\n *\n * So the pair is resolved ONCE per launch, through the stable relocated\n * launcher when one can be published, and every builder composes its command\n * from that single answer.\n */\n\nimport { readFileSync, realpathSync } from \"node:fs\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport { PACKAGE_ROOT_FLAG } from \"../package-root\"\nimport { PATHS } from \"../paths\"\nimport { provisionHookLauncher } from \"./provision\"\n\n/** The binary + script + package root a persisted command should name. */\nexport interface SelfInvocation {\n /** The node/bun binary to exec. */\n execPath: string\n /** Script to pass to it, omitted for a packaged single-file build. */\n scriptPath?: string\n /**\n * The github-router package root, baked in only when `scriptPath` is the\n * relocated launcher. From `<APP_DIR>/hooks/` nothing can derive it: the\n * pre-existing walk in `browser-mcp/native-host-installer.ts` falls back to\n * `process.cwd()`, which would silently resolve to the USER'S WORKSPACE.\n */\n packageRoot?: string\n}\n\nfunction quote(s: string): string {\n return `\"${s}\"`\n}\n\n/**\n * Compose a persisted command string: the binary, the script when it differs\n * from the binary, the baked package root when there is one, then the args.\n *\n * Every builder in the codebase routes through here so the three parts cannot\n * drift apart — and so a future part is added in one place, not eleven.\n */\nexport function buildSelfCommand(inv: SelfInvocation, args: string): string {\n const parts = [quote(inv.execPath)]\n if (inv.scriptPath && inv.scriptPath !== inv.execPath) {\n parts.push(quote(inv.scriptPath))\n }\n if (inv.packageRoot) {\n parts.push(PACKAGE_ROOT_FLAG, quote(inv.packageRoot))\n }\n if (args) parts.push(args)\n return parts.join(\" \")\n}\n\n/** The invocation describing the currently running entrypoint, unrelocated. */\nexport function currentInvocation(): SelfInvocation {\n return { execPath: process.execPath, scriptPath: process.argv[1] }\n}\n\nlet _resolved: SelfInvocation | undefined\n\n/** @internal — reset module state between test cases. */\nexport function __resetSelfInvocationForTests(): void {\n _resolved = undefined\n}\n\n/**\n * Resolve the invocation to bake into every persisted command this launch\n * writes. Awaited BEFORE the settings are written, never fire-and-forget: a\n * command persisted while provisioning is still pending would name the very\n * temp path this exists to avoid, and the session would stay broken however\n * well provisioning later went.\n *\n * Falls back to the running entrypoint if the launcher cannot be published, so\n * a provisioning failure degrades rather than breaking a launch. The caller is\n * responsible for surfacing that fallback — see `hookLauncherDegradedWarning`.\n */\nexport async function resolveSelfInvocation(): Promise<SelfInvocation> {\n if (_resolved) return _resolved\n const launcher = await provisionHookLauncher()\n _resolved =\n launcher ?\n {\n execPath: process.execPath,\n scriptPath: launcher,\n packageRoot: currentPackageRoot(),\n }\n : currentInvocation()\n return _resolved\n}\n\n/**\n * A human-facing warning when this launch is about to persist hook commands\n * pointing into a directory the OS may reap — the exact configuration that\n * produced the original bug.\n *\n * Returns the message rather than logging it, mirroring\n * `colbertDegradedWarning`: `claude` and `codex` call `enableFileLogging()`,\n * which redirects `consola.warn` to the error log, so a warning logged here\n * would be invisible on the one path where it matters most. The caller writes\n * it to stderr next to the readiness line instead.\n *\n * Silent when running from source, which has no bundle to publish and is not a\n * degraded state.\n */\nexport function hookLauncherDegradedWarning(\n inv: SelfInvocation,\n): string | null {\n if (!inv.scriptPath) return null\n // Our own published launcher is checked FIRST, and is authoritative: we know\n // we published it, so it can never warrant the \"could not publish\" message\n // below even if APP_DIR itself sits somewhere unusual.\n if (inv.scriptPath.startsWith(PATHS.HOOK_LAUNCHER_DIR)) {\n if (inv.packageRoot) return null\n // Relocated but rootless. Today this only degrades `--version` inside the\n // launcher, because nothing in the hook graph consumes the package root for\n // anything else. It is reported anyway because it is a trap rather than a\n // bug: the day a hook subcommand pulls in code_search or the browser\n // installer, `packageRoot()` falls through to `process.cwd()` and starts\n // resolving against the user's REPO with no signal at all.\n return (\n \"Hook launcher published without a package root; hook version reporting \"\n + \"will be degraded. This usually means the install tree is partially \"\n + \"missing — reinstall github-router.\"\n )\n }\n if (isUnderVolatileRoot(inv.scriptPath)) {\n return (\n \"Hook commands point into a temporary directory and will stop working \"\n + \"when the OS reaps it (the stable launcher could not be published). \"\n + \"Reinstall with `npm install -g github-router` to avoid this.\"\n )\n }\n return null\n}\n\n/**\n * The package root of the RUNNING code, derived while we are still inside the\n * package tree — the one moment it is knowable without guessing.\n *\n * The name is checked, not just the presence of a package.json: this value is\n * baked into a persisted command and consumed by `packageRoot()` as the\n * highest-priority source, so handing it the WRONG root would be worse than\n * handing it none. Matches `findPackageRoot()` in\n * `browser-mcp/native-host-installer.ts`, whose fallbacks this supersedes.\n */\nfunction currentPackageRoot(): string | undefined {\n const entry = typeof process.argv[1] === \"string\" ? process.argv[1] : undefined\n if (!entry) return undefined\n let cur = path.dirname(entry)\n for (let i = 0; i < 10; i++) {\n try {\n const pkg = JSON.parse(\n readFileSync(path.join(cur, \"package.json\"), \"utf8\"),\n ) as { name?: unknown }\n if (typeof pkg.name === \"string\" && pkg.name.includes(\"github-router\")) {\n return cur\n }\n } catch {\n // Not here, or unreadable; walk up.\n }\n const parent = path.dirname(cur)\n if (parent === cur) break\n cur = parent\n }\n return undefined\n}\n\n/**\n * Whether a path sits under a directory the OS is entitled to clean out from\n * under a running session: the per-user temp dir on every platform, which is\n * where `bunx` (unlike `npx`, which uses a cache under $HOME) installs.\n *\n * Both sides are compared in resolved AND unresolved form. macOS reports\n * `$TMPDIR` as `/var/folders/…` while a process sees `/private/var/folders/…`,\n * so a single-form prefix test silently never matches; and `realpathSync`\n * fails on a path that does not exist, which would otherwise make the answer\n * depend on whether the file happens to be there.\n */\nexport function isUnderVolatileRoot(target: string): boolean {\n try {\n const prefixes = new Set<string>()\n for (const root of [os.tmpdir(), realpathIfPossible(os.tmpdir())]) {\n prefixes.add(root.endsWith(path.sep) ? root : root + path.sep)\n }\n const targets = [target, realpathIfPossible(target)]\n return targets.some((t) => [...prefixes].some((p) => t.startsWith(p)))\n } catch {\n return false\n }\n}\n\nfunction realpathIfPossible(p: string): string {\n try {\n return realpathSync(p)\n } catch {\n return p\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,MAAM,iBAAiB;;AAGvB,MAAM,cAAc;;;;;;AAOpB,MAAM,mBAAmB;AAEzB,IAAI;AACJ,IAAI;;;;;;;;;;;AAkBJ,SAAS,sBAA0C;CACjD,IAAI;EACF,OAAO,KAAK,KAAK,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,WAAW;CAC5E,QAAQ;EACN;CACF;AACF;AAEA,SAAS,OAAO,KAAqB;CACnC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,KAAK;AACtD;;;;;;;AAQA,SAAS,eAAe,YAAwC;CAC9D,IAAI;EACF,MAAM,MAAM,aACV,KAAK,KAAK,KAAK,QAAQ,UAAU,GAAG,cAAc,GAClD,MACF,CAAC,CAAC,KAAK;EACP,OAAO,iBAAiB,KAAK,GAAG,IAAI,MAAM,KAAA;CAC5C,QAAQ;EACN;CACF;AACF;;AAGA,SAAS,cAAc,QAAgB,QAAyB;CAC9D,IAAI;EACF,OAAO,OAAO,aAAa,MAAM,CAAC,MAAM;CAC1C,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAS,QAAQ,QAAgB,OAAe,QAAyB;CAGvE,IAAI,cAAc,QAAQ,MAAM,GAAG,OAAO;CAC1C,KAAK,IAAI,UAAU,GAAG,UAAU,kBAAkB,WAAW;EAC3D,MAAM,MAAM,GAAG,OAAO,GAAG,QAAQ,IAAI,GAAG,QAAQ;EAChD,IAAI;GACF,cAAc,KAAK,OAAO,EAAE,MAAM,IAAM,CAAC;GACzC,WAAW,KAAK,MAAM;GACtB,OAAO;EACT,QAAQ;GACN,IAAI;IACF,OAAO,KAAK,EAAE,OAAO,KAAK,CAAC;GAC7B,QAAQ,CAGR;EACF;CACF;CACA,OAAO,cAAc,QAAQ,MAAM;AACrC;;AAGA,MAAM,iBAAiB;;;;;AAMvB,MAAM,sBAAsB;;;;;;;;;;;;;;;;AAiB5B,SAAS,oBAAoB,KAAa,MAAoB;CAC5D,MAAM,MAAM,KAAK,IAAI;CACrB,IAAI;CACJ,IAAI;EACF,UAAU,YAAY,GAAG;CAC3B,QAAQ;EACN;CACF;CACA,KAAK,MAAM,QAAQ,SAAS;EAC1B,IAAI,SAAS,KAAK,SAAS,IAAI,GAAG;EAClC,MAAM,QAAQ,KAAK,SAAS,MAAM;EAIlC,MAAM,aAAa,4BAA4B,KAAK,IAAI;EACxD,IAAI,CAAC,SAAS,CAAC,YAAY;EAC3B,MAAM,SAAS,KAAK,KAAK,KAAK,IAAI;EAClC,IAAI;GAEF,IADY,MAAM,SAAS,MAAM,CAAC,CAAC,WACxB,QAAQ,iBAAiB,sBAClC,OAAO,QAAQ,EAAE,OAAO,KAAK,CAAC;EAElC,QAAQ,CAER;CACF;AACF;;;;;;;;AASA,SAAgB,wBAAqD;CACnE,IAAI,YAAY,OAAO,QAAQ,QAAQ,UAAU;CACjD,IAAI,WAAW,OAAO;CACtB,YAAY,QAAQ,QAAQ,CAAC,CAC1B,WAAW,cAAc,CAAC,CAAC,CAC3B,MAAM,WAAW;EAGhB,IAAI,QAAQ,aAAa;EACzB,OAAO;CACT,CAAC,CAAC,CACD,cAAc;EACb,YAAY,KAAA;CACd,CAAC;CACH,OAAO;AACT;AAEA,SAAS,gBAAoC;CAC3C,MAAM,aAAa,oBAAoB;CACvC,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,OAAO,oBAAoB,UAAU;AACvC;;;;;;;;;AAUA,SAAgB,oBAAoB,YAAwC;CAC1E,IAAI;EACF,IAAI;EACJ,IAAI;GACF,QAAQ,aAAa,UAAU;EACjC,QAAQ;GAIN;EACF;EAEA,MAAM,SAAS,OAAO,KAAK;EAC3B,MAAM,WAAW,eAAe,UAAU;EAC1C,IAAI,CAAC,UAAU;GAIb,QAAQ,MACN,qEACF;GACA;EACF;EACA,IAAI,aAAa,QAAQ;GAIvB,QAAQ,MACN,wEACF;GACA;EACF;EAEA,MAAM,MAAM,MAAM;EAOlB,UAAU,KAAK;GAAE,WAAW;GAAM,MAAM;EAAM,CAAC;EAC/C,IAAI;GACF,UAAU,KAAK,GAAK;EACtB,QAAQ,CAER;EACA,MAAM,SAAS,KAAK,KAAK,KAAK,SAAS,OAAO,KAAK;EACnD,IAAI,CAAC,QAAQ,QAAQ,OAAO,MAAM,GAAG,OAAO,KAAA;EAK5C,IAAI;GACF,MAAM,sBAAM,IAAI,KAAK;GACrB,WAAW,QAAQ,KAAK,GAAG;EAC7B,QAAQ,CAER;EACA,oBAAoB,KAAK,MAAM;EAC/B,OAAO;CACT,SAAS,KAAK;EACZ,QAAQ,MAAM,yCAAyC,GAAG;EAC1D;CACF;AACF;;;;;;;;;;;;;;;;;;;ACnRA,SAAS,MAAM,GAAmB;CAChC,OAAO,IAAI,EAAE;AACf;;;;;;;;AASA,SAAgB,iBAAiB,KAAqB,MAAsB;CAC1E,MAAM,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC;CAClC,IAAI,IAAI,cAAc,IAAI,eAAe,IAAI,UAC3C,MAAM,KAAK,MAAM,IAAI,UAAU,CAAC;CAElC,IAAI,IAAI,aACN,MAAM,KAAK,mBAAmB,MAAM,IAAI,WAAW,CAAC;CAEtD,IAAI,MAAM,MAAM,KAAK,IAAI;CACzB,OAAO,MAAM,KAAK,GAAG;AACvB;;AAGA,SAAgB,oBAAoC;CAClD,OAAO;EAAE,UAAU,QAAQ;EAAU,YAAY,QAAQ,KAAK;CAAG;AACnE;AAEA,IAAI;;;;;;;;;;;;AAkBJ,eAAsB,wBAAiD;CACrE,IAAI,WAAW,OAAO;CACtB,MAAM,WAAW,MAAM,sBAAsB;CAC7C,YACE,WACE;EACE,UAAU,QAAQ;EAClB,YAAY;EACZ,aAAa,mBAAmB;CAClC,IACA,kBAAkB;CACtB,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAgB,4BACd,KACe;CACf,IAAI,CAAC,IAAI,YAAY,OAAO;CAI5B,IAAI,IAAI,WAAW,WAAW,MAAM,iBAAiB,GAAG;EACtD,IAAI,IAAI,aAAa,OAAO;EAO5B,OACE;CAIJ;CACA,IAAI,oBAAoB,IAAI,UAAU,GACpC,OACE;CAKJ,OAAO;AACT;;;;;;;;;;;AAYA,SAAS,qBAAyC;CAChD,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,WAAW,QAAQ,KAAK,KAAK,KAAA;CACtE,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,IAAI,MAAM,KAAK,QAAQ,KAAK;CAC5B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK;EAC3B,IAAI;GACF,MAAM,MAAM,KAAK,MACf,aAAa,KAAK,KAAK,KAAK,cAAc,GAAG,MAAM,CACrD;GACA,IAAI,OAAO,IAAI,SAAS,YAAY,IAAI,KAAK,SAAS,eAAe,GACnE,OAAO;EAEX,QAAQ,CAER;EACA,MAAM,SAAS,KAAK,QAAQ,GAAG;EAC/B,IAAI,WAAW,KAAK;EACpB,MAAM;CACR;AAEF;;;;;;;;;;;;AAaA,SAAgB,oBAAoB,QAAyB;CAC3D,IAAI;EACF,MAAM,2BAAW,IAAI,IAAY;EACjC,KAAK,MAAM,QAAQ,CAAC,GAAG,OAAO,GAAG,mBAAmB,GAAG,OAAO,CAAC,CAAC,GAC9D,SAAS,IAAI,KAAK,SAAS,KAAK,GAAG,IAAI,OAAO,OAAO,KAAK,GAAG;EAG/D,OAAO,CADU,QAAQ,mBAAmB,MAAM,CACrC,CAAC,CAAC,MAAM,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,MAAM,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;CACvE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,mBAAmB,GAAmB;CAC7C,IAAI;EACF,OAAO,aAAa,CAAC;CACvB,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -1,15 +1,16 @@
1
- import { $ as browserToolsEnabled, C as buildToolbeltAwareness, Ct as provisionBrowserAssets, Et as provisionAndIndexColbert, Ft as DEFAULT_CLAUDE_MODEL_FALLBACKS, Ht as pickClaudeDefault, J as agentToolsEnabled, Q as browserCompoundToolsEnabled, S as availableToolCommands, X as brainstormModel, Z as browseAgentEnabled, a as buildPeerAwarenessSnippet, at as nativeSubagentModel, ct as scribeModel, et as fleetToolsEnabled, it as genericModel, lt as standInToolEnabled, nt as genericCheapModel, ot as reviewerModel, rt as genericFastModel, s as enumerateInjectedMcpToolNames, st as scoutModel, tt as geminiAvailable, ut as workerToolsEnabled, w as toolbeltEnabled, x as buildEnv } from "./peer-mcp-personas-DRL_xT4h.js";
2
- import { t as getPackageVersion } from "./version-_Q1WpsQp.js";
3
- import { o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, t as PATHS } from "./paths-j2B7b0DZ.js";
4
- import { A as state, d as resolveModel, m as getGitHubUser } from "./token-CxPaBEUS.js";
1
+ import { $t as resolveLeadSlugArg, C as availableToolCommands, Mt as provisionAndIndexColbert, Qt as pickClaudeDefault, S as buildEnv, T as toolbeltEnabled, Wt as DEFAULT_CLAUDE_MODEL_FALLBACKS, a as buildPeerAwarenessSnippet, at as browserCompoundToolsEnabled, ct as geminiAvailable, dt as nativeSubagentModel, ft as reviewerModel, gt as workerToolsEnabled, ht as standInToolEnabled, it as browseAgentEnabled, kt as provisionBrowserAssets, lt as generalPurposeFastModel, mt as scribeModel, ot as browserToolsEnabled, pt as scoutModel, rt as brainstormModel, s as enumerateInjectedMcpToolNames, st as fleetToolsEnabled, tt as agentToolsEnabled, ut as implementerFastModel, w as buildToolbeltAwareness } from "./peer-mcp-personas-V6stFvpq.js";
2
+ import { t as getPackageVersion } from "./version-C8x2hQrZ.js";
3
+ import { o as removeOwnClaudeConfigMirror, r as ensureClaudeConfigMirror, t as PATHS } from "./paths-wLC0InjX.js";
4
+ import { A as state, d as resolveModel, m as getGitHubUser } from "./token-CnlB0884.js";
5
5
  import { a as resolveExecutable, o as runCommandCapture, t as killChildProcessTree } from "./exec-y8C_MU8A.js";
6
- import { a as sharedServerArgs, i as setupAndServe, o as startKeepAwake, r as parseSharedArgs, s as stopKeepAwake, t as getClaudeCodeEnvVars, u as runSelfUpdate } from "./server-setup-C9r7jYnz.js";
7
- import { r as getCodexVersion, t as provisionToolbelt } from "./provision-HzZ547dW.js";
8
- import { s as injectStopHookIntoSettingsFile } from "./stop-gate-hook-CaUrldLh.js";
9
- import { l as guardToolMatcher, n as activeDispatchModes, r as buildWorkerGuardHookCommand } from "./worker-dispatch-Bj1uYyG9.js";
10
- import { _ as injectPeerMcpIntoMirror, a as appendPeerAwarenessToMirroredClaudeMd, b as workersKeyOf, d as SEAMLESS_BUILTIN_TOOLS, f as configureServeDefaultPermissionMode, g as BUILTIN_SUBAGENT_DEFINITIONS, h as sanitizeServeSettingsEnv, l as prependOperatingDefaultsToMirroredClaudeMd, m as planModeAllowRules, n as INJECTED_SKILLS, o as appendToolbeltAwarenessToMirroredClaudeMd, p as injectAllowRules, r as writeInjectedSkill, s as buildOperatingDefaultsDirective, t as injectAttributionSuppressionIntoSettingsFile, u as prependStyleDirectiveToMirroredClaudeMd, v as resolveCodexCliBackend, x as writePeerMcpRuntimeFiles, y as resolveGroupKeysFromMirror } from "./attribution-settings-VOPvSn6v.js";
11
- import { i as planReviewEnabled, t as buildPlanReviewHookCommand } from "./plan-review-hook-Dk5zTNke.js";
12
- import { t as buildPromptSubmitHookCommand } from "./prompt-submit-hook-lvTWwaTV.js";
6
+ import { a as sharedServerArgs, i as setupAndServe, o as startKeepAwake, r as parseSharedArgs, s as stopKeepAwake, t as getClaudeCodeEnvVars, u as runSelfUpdate } from "./server-setup-Bppjt9xO.js";
7
+ import { n as hookLauncherDegradedWarning, r as resolveSelfInvocation } from "./self-invocation-CKMjcA5F.js";
8
+ import { r as getCodexVersion, t as provisionToolbelt } from "./provision-BpL6gZIt.js";
9
+ import { s as injectStopHookIntoSettingsFile } from "./stop-gate-hook-DgJ6sW8N.js";
10
+ import { l as guardToolMatcher, n as activeDispatchModes, r as buildWorkerGuardHookCommand } from "./worker-dispatch-zW8Zi69V.js";
11
+ import { _ as injectPeerMcpIntoMirror, a as appendPeerAwarenessToMirroredClaudeMd, b as workersKeyOf, d as SEAMLESS_BUILTIN_TOOLS, f as configureServeDefaultPermissionMode, g as BUILTIN_SUBAGENT_DEFINITIONS, h as sanitizeServeSettingsEnv, l as prependOperatingDefaultsToMirroredClaudeMd, m as planModeAllowRules, n as INJECTED_SKILLS, o as appendToolbeltAwarenessToMirroredClaudeMd, p as injectAllowRules, r as writeInjectedSkill, s as buildOperatingDefaultsDirective, t as injectAttributionSuppressionIntoSettingsFile, u as prependStyleDirectiveToMirroredClaudeMd, v as resolveCodexCliBackend, x as writePeerMcpRuntimeFiles, y as resolveGroupKeysFromMirror } from "./attribution-settings-B8M2fvhz.js";
12
+ import { i as planReviewEnabled, t as buildPlanReviewHookCommand } from "./plan-review-hook-Lf9ISdF6.js";
13
+ import { t as buildPromptSubmitHookCommand } from "./prompt-submit-hook-BlijaOn7.js";
13
14
  import { defineCommand } from "citty";
14
15
  import consola from "consola";
15
16
  import fs from "node:fs";
@@ -523,7 +524,7 @@ const NOOP = { cleanup: async () => {} };
523
524
  * Tab-specific bits (ai-or-die hooks, artifact review) are intentionally left
524
525
  * out — they don't apply to the serve control plane.
525
526
  */
526
- async function provisionServeEnhancements(serverUrl, opts = {}) {
527
+ async function provisionServeEnhancements(serverUrl, opts) {
527
528
  try {
528
529
  const tunnelExposed = opts.tunnelExposed === true;
529
530
  const browseAllowed = browserToolsEnabled() && (!tunnelExposed || opts.browseOverTunnel === true);
@@ -543,12 +544,12 @@ async function provisionServeEnhancements(serverUrl, opts = {}) {
543
544
  const { keys: groupKeys } = await resolveGroupKeysFromMirror(enabledGroups);
544
545
  const nativeAvailability = {
545
546
  scoutAvailable: scoutModel() != null,
546
- genericAvailable: genericModel() != null,
547
- genericFastAvailable: genericFastModel() != null,
548
- genericCheapAvailable: genericCheapModel() != null
547
+ implementerFastAvailable: implementerFastModel() != null,
548
+ generalPurposeFastAvailable: generalPurposeFastModel() != null
549
549
  };
550
550
  const runtime = await writePeerMcpRuntimeFiles(serverUrl, {
551
551
  codexCli: opts.codexCli === true,
552
+ selfInvocation: opts.selfInvocation,
552
553
  geminiAvailable: gem,
553
554
  groupKeys,
554
555
  workerToolsAvailable: workerToolsEnabled(),
@@ -558,14 +559,14 @@ async function provisionServeEnhancements(serverUrl, opts = {}) {
558
559
  brainstormModel: brainstormModel(),
559
560
  scoutModel: scoutModel(),
560
561
  scribeModel: scribeModel(),
561
- genericModel: genericModel(),
562
- genericFastModel: genericFastModel(),
563
- genericCheapModel: genericCheapModel(),
562
+ implementerFastModel: implementerFastModel(),
563
+ generalPurposeFastModel: generalPurposeFastModel(),
564
564
  builtinSubagents: BUILTIN_SUBAGENT_DEFINITIONS
565
565
  });
566
566
  state.peerMcpNonce = runtime.nonce;
567
567
  const injected = await injectPeerMcpIntoMirror(serverUrl, {
568
568
  codexCli: opts.codexCli === true,
569
+ selfInvocation: opts.selfInvocation,
569
570
  geminiAvailable: gem,
570
571
  groupKeys,
571
572
  nonce: runtime.nonce
@@ -605,19 +606,19 @@ async function provisionServeEnhancements(serverUrl, opts = {}) {
605
606
  const injectedServerKeys = injected.ok ? injected.serversAdded : [...Object.values(groupKeys).filter((k) => Boolean(k)), ...opts.codexCli === true ? ["codex-cli"] : []];
606
607
  await injectAllowRules(settingsPath, planModeAllowRules(injectedServerKeys)).catch((err) => consola.warn(`Could not auto-approve injected tools: ${String(err)}`));
607
608
  if (workerToolsEnabled()) {
608
- const promptCmd = buildPromptSubmitHookCommand(process.execPath, process.argv[1]);
609
+ const promptCmd = buildPromptSubmitHookCommand(opts.selfInvocation);
609
610
  await injectStopHookIntoSettingsFile(settingsPath, promptCmd, "UserPromptSubmit", 45).catch((err) => consola.warn(`Could not register the UserPromptSubmit hook: ${String(err)}`));
610
611
  if (!injected.ok) consola.warn("Workers non-blocking guard NOT registered: subagent MCP injection fell back to parent-only, so worker-* dispatchers cannot reach the workers server. Raw worker tools remain usable on the main thread.");
611
612
  else if (process.env.GH_ROUTER_DISABLE_WORKER_GUARD !== "1") {
612
613
  const workersKey = workersKeyOf(groupKeys);
613
614
  const modes = activeDispatchModes({ browse: browseAllowed && browseAgentEnabled() });
614
- const cmd = buildWorkerGuardHookCommand(process.execPath, process.argv[1], workersKey, modes);
615
+ const cmd = buildWorkerGuardHookCommand(opts.selfInvocation, workersKey, modes);
615
616
  const matcher = guardToolMatcher(workersKey, modes);
616
617
  await injectStopHookIntoSettingsFile(settingsPath, cmd, "PreToolUse", 10, matcher).catch((err) => consola.warn(`Could not register the workers PreToolUse guard hook: ${String(err)}`));
617
618
  } else consola.info("Workers non-blocking guard disabled via GH_ROUTER_DISABLE_WORKER_GUARD=1; raw worker tools remain callable on the main thread.");
618
619
  }
619
620
  if (planReviewEnabled()) try {
620
- const command = buildPlanReviewHookCommand(process.execPath, process.argv[1]);
621
+ const command = buildPlanReviewHookCommand(opts.selfInvocation);
621
622
  await injectStopHookIntoSettingsFile(settingsPath, command, "PostToolUse", void 0, "ExitPlanMode");
622
623
  } catch (err) {
623
624
  consola.warn(`Could not register the advisory plan-review hook: ${String(err)}`);
@@ -1346,14 +1347,14 @@ const serve = defineCommand({
1346
1347
  provisionToolbelt().catch((err) => consola.debug("Toolbelt provisioning failed:", err));
1347
1348
  startKeepAwake();
1348
1349
  if (browserToolsEnabled()) provisionBrowserAssets().catch((err) => consola.debug("Browser extension provisioning failed:", err));
1349
- const opusFamilyShorthand = args.model?.match(/^(\d+\.\d+)$/)?.[1];
1350
- let chosenSlug = opusFamilyShorthand ? pickClaudeDefault(opusFamilyShorthand) : args.model ?? pickClaudeDefault();
1351
- if (!args.model && state.models) {
1350
+ let chosenSlug = resolveLeadSlugArg(args.model);
1351
+ if (!args.model?.trim() && state.models) {
1352
1352
  const inCache = (slug) => state.models?.data.some((m) => m.id === resolveModel(slug)) ?? false;
1353
1353
  if (!inCache(chosenSlug)) {
1354
1354
  for (const fb of DEFAULT_CLAUDE_MODEL_FALLBACKS) if (inCache(fb)) {
1355
- consola.info(`Default model "${chosenSlug}" not in your Copilot model list; falling back to "${fb}".`);
1356
- chosenSlug = fb;
1355
+ const fallbackSlug = pickClaudeDefault(fb.replace(/^claude-opus-/, ""));
1356
+ consola.info(`Default model "${chosenSlug}" not in your Copilot model list; falling back to "${fallbackSlug}".`);
1357
+ chosenSlug = fallbackSlug;
1357
1358
  break;
1358
1359
  }
1359
1360
  }
@@ -1367,7 +1368,13 @@ const serve = defineCommand({
1367
1368
  if (tunnelExposed && browserToolsEnabled() && args["browse-over-tunnel"] !== true) consola.info("Browser MCP is disabled over the tunnel (session-hijack/SSRF risk). Pass --browse-over-tunnel to enable it for fully-trusted access.");
1368
1369
  if (tunnelExposed && state.agentsEnabled && args["agents-over-tunnel"] !== true) consola.info("first-mate is disabled over the tunnel (it mints a GitHub write token). Pass --agents-over-tunnel to enable it for fully-trusted access.");
1369
1370
  if (tunnelExposed && state.fleetEnabled && args["fleet-over-tunnel"] !== true) consola.info("Fleet MCP is disabled over the tunnel (it drives remote coding sessions with your fleet credentials). Pass --fleet-over-tunnel to enable it for fully-trusted access.");
1371
+ const selfInvocation = await resolveSelfInvocation();
1372
+ {
1373
+ const warning = hookLauncherDegradedWarning(selfInvocation);
1374
+ if (warning) process.stderr.write(`${warning}\n`);
1375
+ }
1370
1376
  const enhancements = await provisionServeEnhancements(serverUrl, {
1377
+ selfInvocation,
1371
1378
  codexCli: backend === "cli",
1372
1379
  tunnelExposed,
1373
1380
  browseOverTunnel: args["browse-over-tunnel"] === true,
@@ -1522,4 +1529,4 @@ const serve = defineCommand({
1522
1529
  //#endregion
1523
1530
  export { serve };
1524
1531
 
1525
- //# sourceMappingURL=serve-CM3OmF4I.js.map
1532
+ //# sourceMappingURL=serve-CXUf7RtJ.js.map