github-router 0.3.249 → 0.3.255

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 (95) hide show
  1. package/dist/attribution-settings-CegtE0TY.js +2347 -0
  2. package/dist/attribution-settings-CegtE0TY.js.map +1 -0
  3. package/dist/auth-CP4q2CjO.js +44 -0
  4. package/dist/auth-CP4q2CjO.js.map +1 -0
  5. package/dist/browser-ext/background.js +8 -1
  6. package/dist/browser-ext/manifest.json +1 -1
  7. package/dist/check-usage-6PB5IUFb.js +43 -0
  8. package/dist/check-usage-6PB5IUFb.js.map +1 -0
  9. package/dist/claude-BiZxoNWq.js +593 -0
  10. package/dist/claude-BiZxoNWq.js.map +1 -0
  11. package/dist/client-CQMfroGV.js +413 -0
  12. package/dist/client-CQMfroGV.js.map +1 -0
  13. package/dist/codex-ST21X3WF.js +85 -0
  14. package/dist/codex-ST21X3WF.js.map +1 -0
  15. package/dist/debug-CjmE9sgG.js +81 -0
  16. package/dist/debug-CjmE9sgG.js.map +1 -0
  17. package/dist/engine-DNCX0Hfd.js +2 -0
  18. package/dist/{lifecycle-DHZsVN8j.js → exec-DTO2oin0.js} +3 -130
  19. package/dist/exec-DTO2oin0.js.map +1 -0
  20. package/dist/file-log-reporter-IFQcL5IK.js +158 -0
  21. package/dist/file-log-reporter-IFQcL5IK.js.map +1 -0
  22. package/dist/gate-discovery-CepmbZnc.js +635 -0
  23. package/dist/gate-discovery-CepmbZnc.js.map +1 -0
  24. package/dist/get-copilot-usage-DD1N0Xlh.js +11 -0
  25. package/dist/get-copilot-usage-DD1N0Xlh.js.map +1 -0
  26. package/dist/hook-mcp-client-DBvm9608.js +136 -0
  27. package/dist/hook-mcp-client-DBvm9608.js.map +1 -0
  28. package/dist/internal-artifact-open-D2cQ2QCw.js +319 -0
  29. package/dist/internal-artifact-open-D2cQ2QCw.js.map +1 -0
  30. package/dist/internal-first-mate-guard-7v2CtMA6.js +2 -0
  31. package/dist/internal-first-mate-guard-La0tIjTU.js +126 -0
  32. package/dist/internal-first-mate-guard-La0tIjTU.js.map +1 -0
  33. package/dist/internal-plan-review-ipnT3bhj.js +114 -0
  34. package/dist/internal-plan-review-ipnT3bhj.js.map +1 -0
  35. package/dist/internal-prompt-submit-DOj75_hh.js +121 -0
  36. package/dist/internal-prompt-submit-DOj75_hh.js.map +1 -0
  37. package/dist/internal-session-bind-DwK2iGW0.js +133 -0
  38. package/dist/internal-session-bind-DwK2iGW0.js.map +1 -0
  39. package/dist/internal-stop-hook-CNPGJm63.js +212 -0
  40. package/dist/internal-stop-hook-CNPGJm63.js.map +1 -0
  41. package/dist/internal-stop-review-AUcFssrW.js +122 -0
  42. package/dist/internal-stop-review-AUcFssrW.js.map +1 -0
  43. package/dist/internal-worker-guard-Lu8VHj5k.js +75 -0
  44. package/dist/internal-worker-guard-Lu8VHj5k.js.map +1 -0
  45. package/dist/internal-workspace-header-BRMz0Yql.js +24 -0
  46. package/dist/internal-workspace-header-BRMz0Yql.js.map +1 -0
  47. package/dist/lifecycle-Byw5s1G8.js +134 -0
  48. package/dist/lifecycle-Byw5s1G8.js.map +1 -0
  49. package/dist/lifecycle-D80CaWie.js +2 -0
  50. package/dist/{lifecycle-CUf1UDrH.js → lifecycle-DD88ka3f.js} +3 -3
  51. package/dist/{lifecycle-CUf1UDrH.js.map → lifecycle-DD88ka3f.js.map} +1 -1
  52. package/dist/lifecycle-DzqCqDlM.js +2 -0
  53. package/dist/main.js +43 -12884
  54. package/dist/main.js.map +1 -1
  55. package/dist/mcp-workspace-header-CGJbNeHb.js +21 -0
  56. package/dist/mcp-workspace-header-CGJbNeHb.js.map +1 -0
  57. package/dist/models-B6UnGyRo.js +158 -0
  58. package/dist/models-B6UnGyRo.js.map +1 -0
  59. package/dist/orchestration-hRU4TdWr.js +39 -0
  60. package/dist/orchestration-hRU4TdWr.js.map +1 -0
  61. package/dist/{paths-BjTMI_xK.js → paths-C1cLZ7Ga.js} +52 -20
  62. package/dist/paths-C1cLZ7Ga.js.map +1 -0
  63. package/dist/paths-CwKwc2pi.js +2 -0
  64. package/dist/{peer-mcp-personas-DJzLpfDJ.js → peer-mcp-personas-BDet588n.js} +1006 -2488
  65. package/dist/peer-mcp-personas-BDet588n.js.map +1 -0
  66. package/dist/plan-review-hook-C8x0uKPf.js +159 -0
  67. package/dist/plan-review-hook-C8x0uKPf.js.map +1 -0
  68. package/dist/prompt-submit-hook-BrkkLAAB.js +153 -0
  69. package/dist/prompt-submit-hook-BrkkLAAB.js.map +1 -0
  70. package/dist/provision-DSkPa5hY.js +656 -0
  71. package/dist/provision-DSkPa5hY.js.map +1 -0
  72. package/dist/serve-sa_7c-yD.js +1511 -0
  73. package/dist/serve-sa_7c-yD.js.map +1 -0
  74. package/dist/server-setup-DcO9qvN3.js +4776 -0
  75. package/dist/server-setup-DcO9qvN3.js.map +1 -0
  76. package/dist/start-siGxuZhO.js +129 -0
  77. package/dist/start-siGxuZhO.js.map +1 -0
  78. package/dist/stop-gate-hook-DQc2KV7B.js +686 -0
  79. package/dist/stop-gate-hook-DQc2KV7B.js.map +1 -0
  80. package/dist/stop-gate-policy-CHwWbuF_.js +268 -0
  81. package/dist/stop-gate-policy-CHwWbuF_.js.map +1 -0
  82. package/dist/token-R2ocFC3M.js +1054 -0
  83. package/dist/token-R2ocFC3M.js.map +1 -0
  84. package/dist/version-_Q1WpsQp.js +32 -0
  85. package/dist/version-_Q1WpsQp.js.map +1 -0
  86. package/dist/worker-dispatch-Bj1uYyG9.js +222 -0
  87. package/dist/worker-dispatch-Bj1uYyG9.js.map +1 -0
  88. package/package.json +1 -1
  89. package/dist/engine-BuuHUb4t.js +0 -2
  90. package/dist/lifecycle-0rXrwjWk.js +0 -2
  91. package/dist/lifecycle-DHZsVN8j.js.map +0 -1
  92. package/dist/lifecycle-hzPwXc3I.js +0 -2
  93. package/dist/paths-BjTMI_xK.js.map +0 -1
  94. package/dist/paths-D3XgmTcQ.js +0 -2
  95. package/dist/peer-mcp-personas-DJzLpfDJ.js.map +0 -1
@@ -0,0 +1,656 @@
1
+ import { D as TOOLBELT_TOOLS, E as vscodeRipgrepPath, O as assetFor, Pt as withInstallLock, T as toolbeltSkipSet, vt as extractTarGzMember, w as toolbeltEnabled, wt as collapsePathKeys, yt as extractZipMember } from "./peer-mcp-personas-BDet588n.js";
2
+ import { t as PATHS } from "./paths-C1cLZ7Ga.js";
3
+ import { a as resolveExecutable, r as parseBoolEnv, t as killChildProcessTree } from "./exec-DTO2oin0.js";
4
+ import { a as sweepRegistry } from "./lifecycle-DD88ka3f.js";
5
+ import consola from "consola";
6
+ import { existsSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { createHash, randomBytes } from "node:crypto";
9
+ import { chmod, copyFile, link, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
10
+ import process from "node:process";
11
+ import { execFileSync, spawn } from "node:child_process";
12
+ //#region src/lib/process-guard/index.ts
13
+ /**
14
+ * Crash-safe process guard (POSIX only).
15
+ *
16
+ * If the proxy dies WITHOUT running its own shutdown handlers (hard crash,
17
+ * SIGKILL, OOM), nothing in `cleanup()` runs to reap the launched CLI's
18
+ * process tree. This guard is the last-resort net for that case.
19
+ *
20
+ * Platform split:
21
+ * - **Windows: no guard needed.** Node assigns each child to a Job
22
+ * Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, so when the proxy
23
+ * dies (any cause) the OS tears down the whole descendant tree —
24
+ * verified: force-killing a Node parent kills child AND grandchild.
25
+ * `startProcessGuard` is a no-op on win32.
26
+ * - **POSIX: a detached `node -e` reaper.** There is no job-object
27
+ * equivalent (a killed parent's children reparent to init), so we
28
+ * spawn a detached watchdog that learns of proxy death via an inherited
29
+ * stdin PIPE EOF (the proxy holds the write end; the OS closes it on
30
+ * proxy death) and reaps the CLI's process group.
31
+ *
32
+ * Never kill the WRONG process: if the child's PID/PGID was recycled after
33
+ * the child exited, a blind `kill(-pid)` could hit an innocent group. The
34
+ * reaper snapshots the child's process start-time at startup and, on proxy
35
+ * death, kills ONLY if the live PID's start-time still matches. A mismatch
36
+ * / unreadable probe → skip (fail-safe). We never trade an orphan-leak for
37
+ * killing the user's work.
38
+ *
39
+ * The reaper is detached + unref'd so it survives the proxy's SIGKILL, and
40
+ * self-exits once it has acted (or after a 24h cap) so it can never become
41
+ * the orphan it exists to prevent. Opt out with
42
+ * `GH_ROUTER_DISABLE_PROCESS_GUARD=1`.
43
+ */
44
+ /**
45
+ * Live reaper children, held so the GC can't collect the `ChildProcess`
46
+ * (and with it the stdin write-end that is the proxy-death signal) while
47
+ * the proxy runs. Entries self-remove on the reaper's exit. We do NOT
48
+ * proactively kill these on graceful shutdown: when the proxy exits the OS
49
+ * closes the stdin pipe, the reaper hits EOF, re-verifies the child's
50
+ * identity, and reaps any survivor that ignored the graceful SIGTERM —
51
+ * exactly the backstop we want.
52
+ */
53
+ const _activeReapers = /* @__PURE__ */ new Set();
54
+ /** Guard is on by default; `GH_ROUTER_DISABLE_PROCESS_GUARD=1` opts out. */
55
+ function processGuardEnabled() {
56
+ return parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROCESS_GUARD) !== true;
57
+ }
58
+ /**
59
+ * Build the start-time-verified node reaper script (POSIX). PURE — `pid`
60
+ * is our own integer. `detachedGroup` selects `kill(-pid)` (process group,
61
+ * the detached CLI) vs `kill(pid)` on POSIX.
62
+ *
63
+ * Kept dependency-free (require'd builtins only) so it runs under `node -e`
64
+ * with no module resolution against the dist bundle.
65
+ */
66
+ function buildNodeReaperScript(pid, detachedGroup) {
67
+ const target = detachedGroup ? "-PID" : "PID";
68
+ return `
69
+ const cp = require("node:child_process");
70
+ const fs = require("node:fs");
71
+ const PID = ${pid >>> 0};
72
+ function startTime() {
73
+ try {
74
+ if (process.platform === "linux") {
75
+ const stat = fs.readFileSync("/proc/" + PID + "/stat", "utf8");
76
+ const post = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\\s+/);
77
+ return post[19] || null; // field 22 (starttime) = index 19 after comm
78
+ }
79
+ const out = cp.execFileSync("ps", ["-o","lstart=","-p",String(PID)],
80
+ { stdio: ["ignore","pipe","ignore"], timeout: 2000 }).toString().trim();
81
+ return out || null;
82
+ } catch { return null; }
83
+ }
84
+ function alive() { try { process.kill(PID, 0); return true; } catch { return false; } }
85
+ function treeKill() {
86
+ try { process.kill(${target}, "SIGTERM"); } catch {}
87
+ // NOT unref'd: this must keep the loop alive to deliver the escalated
88
+ // SIGKILL (the onDeath exit timer below stays alive past 500ms too).
89
+ setTimeout(() => { try { process.kill(${target}, "SIGKILL"); } catch {} }, 500);
90
+ }
91
+ const snap = (() => {
92
+ // The child is definitely ours and alive at startup, so a null here is a
93
+ // transient probe failure (e.g. a momentary 'ps' hiccup), NOT a real
94
+ // identity loss. Retry a few times so a one-off failure can't silently
95
+ // disable the guard for the rest of the run.
96
+ for (let i = 0; i < 3; i++) { const s = startTime(); if (s !== null) return s; }
97
+ return null;
98
+ })();
99
+ let done = false;
100
+ function onDeath() {
101
+ if (done) return; done = true;
102
+ // Re-verify identity: kill ONLY if the live PID is still our child.
103
+ if (snap !== null && alive() && startTime() === snap) treeKill();
104
+ // REF'd (NOT unref'd): stdin has closed, so an unref'd timer would let
105
+ // the loop empty and the process exit immediately — dropping the 500ms
106
+ // SIGKILL escalation. Keep the loop alive to deliver it, then exit.
107
+ setTimeout(() => process.exit(0), 1500);
108
+ }
109
+ process.stdin.resume();
110
+ process.stdin.on("end", onDeath);
111
+ process.stdin.on("close", onDeath);
112
+ process.stdin.on("error", onDeath);
113
+ const cap = setTimeout(() => process.exit(0), 24*3600*1000); if (cap.unref) cap.unref();
114
+ `.trim();
115
+ }
116
+ /**
117
+ * Spawn the detached node reaper holding the stdin death-pipe. Always
118
+ * `detached` so it outlives a force-kill of the proxy, and `unref`'d so it
119
+ * never holds the proxy's event loop open. Returns null on spawn failure.
120
+ */
121
+ function spawnNodeReaper(pid, detachedGroup) {
122
+ try {
123
+ const child = spawn(process.execPath, ["-e", buildNodeReaperScript(pid, detachedGroup)], {
124
+ stdio: [
125
+ "pipe",
126
+ "ignore",
127
+ "ignore"
128
+ ],
129
+ detached: true,
130
+ windowsHide: true,
131
+ shell: false
132
+ });
133
+ child.on("error", () => {});
134
+ child.stdin?.on("error", () => {});
135
+ _activeReapers.add(child);
136
+ child.once("exit", () => _activeReapers.delete(child));
137
+ child.unref();
138
+ return child;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ /**
144
+ * Start the crash-safe guard for a launched CLI child. No-op on Windows
145
+ * (Node's Job Object already reaps the tree on proxy death) and when
146
+ * disabled / unspawnable. Fire-and-forget; never throws.
147
+ */
148
+ function startProcessGuard(child) {
149
+ if (!processGuardEnabled()) return;
150
+ const pid = child.pid;
151
+ if (!pid) return;
152
+ if (process.platform === "win32") return;
153
+ spawnNodeReaper(pid, true);
154
+ }
155
+ //#endregion
156
+ //#region src/lib/stripped-env-keys.ts
157
+ /**
158
+ * Auth / routing / remote / wire-fingerprint environment keys that must never
159
+ * reach a github-router-spawned Claude or Codex child — through ANY channel.
160
+ *
161
+ * Two channels carry these keys to a spawned agent, and both must be defended:
162
+ * 1. The PARENT PROCESS ENV — a user's shell exports. Stripped by
163
+ * `sanitizeParentEnv` (`src/lib/launch.ts`) before the proxy's overrides.
164
+ * 2. The serve mirror `settings.json` `env` BLOCK — a user's own
165
+ * `~/.claude/settings.json`, snapshot-mirrored and applied to the
166
+ * SDK-spawned claude via `settingSources`. Stripped by
167
+ * `sanitizeServeSettingsEnv` (`src/lib/mcp-permissions-settings.ts`).
168
+ *
169
+ * If any of these survived, a Bedrock/Vertex/gateway user's config would
170
+ * re-route the session OFF the github-router proxy, inject real auth that wins
171
+ * over the synthetic `.credentials.json`, or activate Claude Code's
172
+ * unimplemented Bridge/remote code path. `CLAUDE_CODE_COORDINATOR_MODE` (a
173
+ * toolset-stripper) is NOT here — it's serve-specific, added to the serve list
174
+ * directly. `ANTHROPIC_SMALL_FAST_MODEL` is deliberately NOT stripped (users
175
+ * legitimately rely on it; `resolveModel` translates unrecognized values).
176
+ */
177
+ const STRIPPED_AUTH_ROUTING_ENV_KEYS = [
178
+ "ANTHROPIC_API_KEY",
179
+ "ANTHROPIC_AUTH_TOKEN",
180
+ "ANTHROPIC_BASE_URL",
181
+ "ANTHROPIC_CUSTOM_HEADERS",
182
+ "ANTHROPIC_MODEL",
183
+ "CLAUDE_CODE_OAUTH_TOKEN",
184
+ "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
185
+ "CLAUDE_CODE_USE_BEDROCK",
186
+ "CLAUDE_CODE_USE_VERTEX",
187
+ "CLAUDE_CODE_USE_FOUNDRY",
188
+ "CLAUDE_CONFIG_DIR",
189
+ "CLAUDE_BRIDGE_OAUTH_TOKEN",
190
+ "CLAUDE_BRIDGE_BASE_URL",
191
+ "CLAUDE_BRIDGE_SESSION_INGRESS_URL",
192
+ "SESSION_INGRESS_URL",
193
+ "CLAUDE_CODE_REMOTE",
194
+ "CLAUDE_CODE_CONTAINER_ID",
195
+ "CLAUDE_CODE_REMOTE_SESSION_ID",
196
+ "CLAUDE_CODE_SESSION_ID",
197
+ "CLAUDE_CODE_ADDITIONAL_PROTECTION",
198
+ "OPENAI_API_KEY",
199
+ "OPENAI_BASE_URL",
200
+ "CODEX_HOME",
201
+ "AIORDIE_CLAUDE_BIND",
202
+ "AIORDIE_TOKEN",
203
+ "AIORDIE_INSECURE_TLS"
204
+ ];
205
+ //#endregion
206
+ //#region src/lib/launch.ts
207
+ /**
208
+ * Auth-related env keys we strip from the parent before spawning the child CLI.
209
+ * The proxy provides its own values for everything we care about, so the child
210
+ * behaves as if the user had no parent-env auth at all. The canonical list lives
211
+ * in `./stripped-env-keys` (shared with serve's settings.json `env` strip).
212
+ *
213
+ * Why strip rather than override-with-empty-string: Claude Code emits "Auth
214
+ * conflict" warnings whenever both ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY
215
+ * are present (any value); stripping suppresses it AND prevents an inherited
216
+ * real key from leaking. Cloud-provider toggles + OAuth tokens are simpler
217
+ * dropped than overridden — a missing var is unambiguously absent everywhere.
218
+ */
219
+ const STRIPPED_PARENT_ENV_KEYS = STRIPPED_AUTH_ROUTING_ENV_KEYS;
220
+ /**
221
+ * Strip auth-related keys from a parent-process env object. The result
222
+ * is suitable to spread into a spawned child's env BEFORE the proxy's
223
+ * explicit overrides, so the proxy is the only source of truth for
224
+ * auth — and stale shell exports can't leak through.
225
+ */
226
+ function sanitizeParentEnv(parent) {
227
+ const sanitized = { ...parent };
228
+ for (const key of STRIPPED_PARENT_ENV_KEYS) delete sanitized[key];
229
+ return sanitized;
230
+ }
231
+ function commandExists(name) {
232
+ try {
233
+ execFileSync(process.platform === "win32" ? "where.exe" : "which", [name], { stdio: "ignore" });
234
+ return true;
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+ /**
240
+ * Whether the launcher can execute `executable`.
241
+ *
242
+ * `buildLaunchCommand` resolves the CLI to an ABSOLUTE path (anti-shadow).
243
+ * `where.exe` (and POSIX `which`) reject a full path argument — `where`
244
+ * returns "Could not find files for the given pattern(s)" for an absolute
245
+ * path even when the file exists — so the where/which probe is only valid
246
+ * for bare command names. For an absolute path (already resolved against
247
+ * PATH and existence-checked by `resolveExecutable`), check the
248
+ * filesystem directly. Without this split, every launch where the CLI is
249
+ * installed fails with a spurious "not found on PATH".
250
+ */
251
+ function isExecutableAvailable(executable) {
252
+ if (path.isAbsolute(executable)) return existsSync(executable);
253
+ return commandExists(executable);
254
+ }
255
+ /**
256
+ * Provider-config flags (`-c model_providers.github_router=...`) that
257
+ * point Codex at our proxy. Extracted from `buildCodexCmd` so the new
258
+ * `codex mcp-server` MCP-config builder can reuse the exact same
259
+ * provider definition — drift between the two paths would silently
260
+ * break the MCP wiring.
261
+ */
262
+ function buildCodexProviderConfigFlags(serverUrl) {
263
+ return [
264
+ "-c",
265
+ `model_providers.github_router={name="github-router",base_url="${serverUrl}/v1",wire_api="responses",env_key="OPENAI_API_KEY"}`,
266
+ "-c",
267
+ "model_provider=github_router"
268
+ ];
269
+ }
270
+ /**
271
+ * Inspect the installed `codex` binary. Used by the codex-MCP wiring
272
+ * in `claude.ts` to gate `--codex-cli`. Codex 0.129.0 introduced the
273
+ * `mcp-server` subcommand; older versions don't expose it, so we
274
+ * downgrade to the HTTP backend with a warning.
275
+ */
276
+ function getCodexVersion() {
277
+ if (!commandExists("codex")) return { ok: false };
278
+ let raw;
279
+ try {
280
+ raw = execFileSync("codex", ["--version"], {
281
+ encoding: "utf8",
282
+ stdio: [
283
+ "ignore",
284
+ "pipe",
285
+ "ignore"
286
+ ]
287
+ }).trim();
288
+ } catch {
289
+ return { ok: false };
290
+ }
291
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw);
292
+ if (!m) return {
293
+ ok: false,
294
+ version: raw
295
+ };
296
+ const major = Number.parseInt(m[1], 10);
297
+ const minor = Number.parseInt(m[2], 10);
298
+ const version = `${m[1]}.${m[2]}.${m[3]}`;
299
+ return {
300
+ ok: major > 0 || major === 0 && minor >= 129,
301
+ version
302
+ };
303
+ }
304
+ /**
305
+ * Codex 0.129.0 broke two things the launcher had been relying on:
306
+ * (1) `--full-auto` was removed in favor of `--sandbox` + `--ask-for-approval`;
307
+ * passing it now exits the child immediately with
308
+ * `error: unexpected argument '--full-auto' found`.
309
+ * (2) `OPENAI_BASE_URL` is silently ignored — Codex hardcodes
310
+ * `https://api.openai.com/v1/responses` and 401s out without an
311
+ * explicit `-c model_providers.<name>.base_url` override.
312
+ *
313
+ * `buildCodexCmd` builds the launch argv that works on Codex 0.129+ while
314
+ * still being compatible with older versions that accept the same flags.
315
+ */
316
+ function buildCodexCmd(target) {
317
+ const cmd = ["codex"];
318
+ if (target.serverUrl) cmd.push(...buildCodexProviderConfigFlags(target.serverUrl));
319
+ cmd.push("--sandbox", "workspace-write", "--ask-for-approval", "on-request", "-m", target.model ?? "gpt-5.6-sol", ...target.extraArgs);
320
+ return cmd;
321
+ }
322
+ function buildLaunchCommand(target) {
323
+ const wantsPermissionMode = target.kind === "claude-code" && target.extraArgs.some((arg) => arg === "--permission-mode" || arg.startsWith("--permission-mode="));
324
+ const cmd = target.kind === "claude-code" ? wantsPermissionMode ? ["claude", ...target.extraArgs] : [
325
+ "claude",
326
+ "--dangerously-skip-permissions",
327
+ ...target.extraArgs
328
+ ] : buildCodexCmd(target);
329
+ const resolved = resolveExecutable(cmd[0], { env: process.env });
330
+ if (resolved) cmd[0] = resolved;
331
+ return {
332
+ cmd,
333
+ env: collapsePathKeys({
334
+ ...sanitizeParentEnv(process.env),
335
+ ...target.envVars
336
+ })
337
+ };
338
+ }
339
+ /**
340
+ * Whether a resolved Windows executable must be launched through cmd.exe
341
+ * (`shell:true`). Only batch shims (`.cmd`/`.bat`) need it — and even then
342
+ * cmd.exe stays alive as the CLI's parent, so `taskkill /T` reaps the
343
+ * whole tree. A real `.exe` (e.g. the native-installer `claude.exe`) is
344
+ * spawned DIRECTLY so the CLI is the direct child, with no cmd.exe
345
+ * intermediary to orphan its node/MCP grandchildren on a kill.
346
+ */
347
+ function windowsLaunchNeedsShell(executable) {
348
+ const ext = path.extname(executable).toLowerCase();
349
+ return ext === ".cmd" || ext === ".bat";
350
+ }
351
+ function launchChild(target, server, options = {}) {
352
+ const { cmd, env } = buildLaunchCommand(target);
353
+ const executable = cmd[0];
354
+ if (!isExecutableAvailable(executable)) {
355
+ const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
356
+ consola.error(msg);
357
+ process.stderr.write(msg + "\n");
358
+ process.exit(1);
359
+ }
360
+ let child;
361
+ try {
362
+ if (process.platform === "win32") if (windowsLaunchNeedsShell(cmd[0])) {
363
+ const quoted = cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ");
364
+ child = spawn(quoted, [], {
365
+ env,
366
+ stdio: "inherit",
367
+ shell: true
368
+ });
369
+ } else child = spawn(cmd[0], cmd.slice(1), {
370
+ env,
371
+ stdio: "inherit"
372
+ });
373
+ else child = spawn(cmd[0], cmd.slice(1), {
374
+ env,
375
+ stdio: "inherit",
376
+ detached: true
377
+ });
378
+ } catch (error) {
379
+ const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
380
+ consola.error(msg);
381
+ process.stderr.write(msg + "\n");
382
+ server.close(true).catch(() => {});
383
+ if (options.onShutdown) Promise.resolve(options.onShutdown()).catch(() => {});
384
+ process.exit(1);
385
+ }
386
+ startProcessGuard(child);
387
+ let cleaned = false;
388
+ let exiting = false;
389
+ async function cleanup() {
390
+ if (cleaned) return;
391
+ cleaned = true;
392
+ const timeout = setTimeout(() => process.exit(1), 5e3);
393
+ try {
394
+ killChildProcessTree(child, { detachedGroup: process.platform !== "win32" });
395
+ } catch {}
396
+ try {
397
+ await server.close(true);
398
+ } catch {}
399
+ if (options.onShutdown) try {
400
+ await options.onShutdown();
401
+ } catch {}
402
+ clearTimeout(timeout);
403
+ }
404
+ function exit(code) {
405
+ if (exiting) return;
406
+ exiting = true;
407
+ process.exit(code);
408
+ }
409
+ let forwardGrace = null;
410
+ const lastForwardAt = {
411
+ SIGINT: 0,
412
+ SIGTERM: 0
413
+ };
414
+ const onSignal = (sig) => {
415
+ if (process.platform !== "win32" && child.pid && !cleaned) {
416
+ const now = Date.now();
417
+ if (now - lastForwardAt[sig] > 250) {
418
+ lastForwardAt[sig] = now;
419
+ try {
420
+ process.kill(-child.pid, sig);
421
+ } catch {}
422
+ }
423
+ const graceMs = sig === "SIGINT" ? 1e4 : 3e3;
424
+ if (!forwardGrace) {
425
+ forwardGrace = setTimeout(() => {
426
+ cleanup().then(() => exit(130)).catch(() => exit(1));
427
+ }, graceMs);
428
+ forwardGrace.unref?.();
429
+ }
430
+ return;
431
+ }
432
+ cleanup().then(() => exit(130)).catch(() => exit(1));
433
+ };
434
+ process.on("SIGINT", () => onSignal("SIGINT"));
435
+ process.on("SIGTERM", () => onSignal("SIGTERM"));
436
+ child.on("exit", (exitCode, signal) => {
437
+ try {
438
+ sweepRegistry();
439
+ } catch {}
440
+ const code = exitCode ?? (signal ? 128 : 1);
441
+ cleanup().then(() => exit(code)).catch(() => exit(1));
442
+ });
443
+ child.on("error", () => {
444
+ cleanup().then(() => exit(1)).catch(() => exit(1));
445
+ });
446
+ }
447
+ //#endregion
448
+ //#region src/lib/toolbelt/provision.ts
449
+ /**
450
+ * Toolbelt provisioner: materialize curated CLI tools into the durable
451
+ * router `bin/` dir so the spawned agent can call them on PATH.
452
+ *
453
+ * Properties (all load-bearing):
454
+ * - **Best-effort.** Every tool is isolated in try/catch; a failure
455
+ * skips that one tool and never throws to the launcher.
456
+ * - **Gap-fill.** A tool already on the user's PATH is NOT
457
+ * materialized (and a stale copy is removed) so we never shadow the
458
+ * user's own `jq`/`yq`/etc.
459
+ * - **Verified.** The downloaded archive's SHA256 must match the
460
+ * hardcoded manifest digest BEFORE extraction.
461
+ * - **Safe extraction.** Only the expected regular-file binary is
462
+ * pulled out; symlink/hardlink/device entries are rejected.
463
+ * - **Concurrency-safe.** A cross-process lock serializes writers;
464
+ * unique temp names + atomic rename avoid half-written binaries.
465
+ * - **Integrity-pruned.** Unexpected files in `bin/` are removed each
466
+ * launch so a prompt-injected agent can't plant a `git.cmd` that
467
+ * shadows real tools in future sessions.
468
+ */
469
+ /** Per-download cap (bytes) — these binaries are a few MB at most. */
470
+ const MAX_DOWNLOAD_BYTES = 67108864;
471
+ const DOWNLOAD_TIMEOUT_MS = 3e4;
472
+ const EXE_EXT = process.platform === "win32" ? ".exe" : "";
473
+ /**
474
+ * Materialize the toolbelt. Returns the list of command names exposed
475
+ * in `bin/` after provisioning. Best-effort; never throws.
476
+ */
477
+ async function provisionToolbelt() {
478
+ if (!toolbeltEnabled()) return [];
479
+ const binDir = PATHS.TOOLBELT_BIN_DIR;
480
+ try {
481
+ await mkdir(binDir, { recursive: true });
482
+ } catch (err) {
483
+ consola.debug("toolbelt: could not create bin dir:", err);
484
+ return [];
485
+ }
486
+ const skip = toolbeltSkipSet();
487
+ await withInstallLock("toolbelt.lock", async () => {
488
+ await pruneUnexpected(binDir);
489
+ await provisionRg(binDir, skip).catch((err) => consola.debug("toolbelt: rg skipped:", err));
490
+ await Promise.all(TOOLBELT_TOOLS.map((spec) => provisionTool(spec, binDir, skip).catch((err) => consola.debug(`toolbelt: ${spec.command} skipped:`, err))));
491
+ });
492
+ return exposedCommands(binDir);
493
+ }
494
+ /** Names allowed to live in `bin/` (managed binaries + their sidecars). */
495
+ function expectedFileNames() {
496
+ const names = /* @__PURE__ */ new Set();
497
+ const add = (base) => {
498
+ names.add(base + EXE_EXT);
499
+ names.add(`${base}${EXE_EXT}.sha256`);
500
+ };
501
+ add("rg");
502
+ for (const spec of TOOLBELT_TOOLS) {
503
+ add(spec.binBasename);
504
+ for (const a of spec.aliases ?? []) add(a);
505
+ }
506
+ return names;
507
+ }
508
+ /** Remove any file in `bin/` that isn't a managed binary or sidecar. */
509
+ async function pruneUnexpected(binDir) {
510
+ const expected = expectedFileNames();
511
+ let entries;
512
+ try {
513
+ entries = await readdir(binDir);
514
+ } catch {
515
+ return;
516
+ }
517
+ for (const name of entries) {
518
+ if (name.endsWith(".tmp")) continue;
519
+ if (!expected.has(name)) await rm(path.join(binDir, name), { force: true }).catch(() => {});
520
+ }
521
+ }
522
+ async function provisionRg(binDir, skip) {
523
+ const dest = path.join(binDir, "rg" + EXE_EXT);
524
+ if (skip.has("rg") || resolveExecutable("rg")) {
525
+ await removeBin(dest);
526
+ return;
527
+ }
528
+ if (existsSync(dest)) return;
529
+ const src = vscodeRipgrepPath();
530
+ if (!src) return;
531
+ const tmp = tempName(dest);
532
+ try {
533
+ await link(src, tmp);
534
+ } catch {
535
+ await copyFile(src, tmp);
536
+ }
537
+ if (process.platform !== "win32") await chmod(tmp, 493).catch(() => {});
538
+ await commit(tmp, dest);
539
+ }
540
+ async function provisionTool(spec, binDir, skip) {
541
+ const dest = path.join(binDir, spec.binBasename + EXE_EXT);
542
+ const sidecar = `${dest}.sha256`;
543
+ const asset = assetFor(spec);
544
+ if (skip.has(spec.command) || !asset) {
545
+ await removeTool(spec, binDir);
546
+ return;
547
+ }
548
+ if (resolveExecutable(spec.command)) {
549
+ await removeTool(spec, binDir);
550
+ return;
551
+ }
552
+ if (existsSync(dest) && await sidecarMatches(sidecar, asset.sha256)) {
553
+ await ensureAliases(spec, binDir, dest);
554
+ return;
555
+ }
556
+ await atomicInstall(dest, await downloadAndExtract(spec, asset));
557
+ await writeFile(sidecar, asset.sha256).catch(() => {});
558
+ await ensureAliases(spec, binDir, dest);
559
+ }
560
+ async function downloadAndExtract(spec, asset) {
561
+ const data = await download(asset.url);
562
+ const digest = createHash("sha256").update(data).digest("hex");
563
+ if (digest !== asset.sha256) throw new Error(`checksum mismatch for ${spec.command} (${asset.url}): expected ${asset.sha256}, got ${digest}`);
564
+ if (asset.archive === "raw") return data;
565
+ const member = asset.archive === "zip" ? extractZipMember(data, spec.binBasename) : extractTarGzMember(data, spec.binBasename);
566
+ if (!member) throw new Error(`binary "${spec.binBasename}" not found in ${asset.url}`);
567
+ return member;
568
+ }
569
+ async function download(url) {
570
+ const controller = new AbortController();
571
+ const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
572
+ try {
573
+ const res = await fetch(url, {
574
+ signal: controller.signal,
575
+ redirect: "follow",
576
+ headers: { "User-Agent": "github-router-toolbelt" }
577
+ });
578
+ if (!res.ok) throw new Error(`download ${url}: HTTP ${res.status}`);
579
+ const buf = Buffer.from(await res.arrayBuffer());
580
+ if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`download ${url}: exceeds ${MAX_DOWNLOAD_BYTES} bytes`);
581
+ return buf;
582
+ } finally {
583
+ clearTimeout(timer);
584
+ }
585
+ }
586
+ /** Write to a unique temp then atomically rename into place. */
587
+ async function atomicInstall(dest, bytes) {
588
+ const tmp = tempName(dest);
589
+ await writeFile(tmp, bytes);
590
+ if (process.platform !== "win32") await chmod(tmp, 493).catch(() => {});
591
+ await commit(tmp, dest);
592
+ }
593
+ /** Rename tmp→dest, handling Windows replace-existing / in-use locks. */
594
+ async function commit(tmp, dest) {
595
+ try {
596
+ await rename(tmp, dest);
597
+ } catch {
598
+ try {
599
+ await rm(dest, { force: true });
600
+ await rename(tmp, dest);
601
+ } catch (err) {
602
+ await rm(tmp, { force: true }).catch(() => {});
603
+ throw err;
604
+ }
605
+ }
606
+ }
607
+ async function ensureAliases(spec, binDir, dest) {
608
+ for (const alias of spec.aliases ?? []) {
609
+ const ap = path.join(binDir, alias + EXE_EXT);
610
+ if (existsSync(ap)) continue;
611
+ const tmp = tempName(ap);
612
+ try {
613
+ await copyFile(dest, tmp);
614
+ if (process.platform !== "win32") await chmod(tmp, 493).catch(() => {});
615
+ await commit(tmp, ap);
616
+ } catch (err) {
617
+ consola.debug(`toolbelt: alias ${alias} skipped:`, err);
618
+ }
619
+ }
620
+ }
621
+ async function removeTool(spec, binDir) {
622
+ await removeBin(path.join(binDir, spec.binBasename + EXE_EXT));
623
+ for (const alias of spec.aliases ?? []) await removeBin(path.join(binDir, alias + EXE_EXT));
624
+ }
625
+ async function removeBin(dest) {
626
+ await rm(dest, { force: true }).catch(() => {});
627
+ await rm(`${dest}.sha256`, { force: true }).catch(() => {});
628
+ }
629
+ async function sidecarMatches(sidecar, sha256) {
630
+ try {
631
+ return (await readFile(sidecar, "utf8")).trim() === sha256;
632
+ } catch {
633
+ return false;
634
+ }
635
+ }
636
+ function tempName(dest) {
637
+ return `${dest}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
638
+ }
639
+ /** The command names currently exposed in `bin/`. */
640
+ async function exposedCommands(binDir) {
641
+ let files;
642
+ try {
643
+ files = new Set(await readdir(binDir));
644
+ } catch {
645
+ return [];
646
+ }
647
+ const present = (base) => files.has(base + EXE_EXT);
648
+ const out = [];
649
+ if (present("rg")) out.push("rg");
650
+ for (const spec of TOOLBELT_TOOLS) if (present(spec.binBasename)) out.push(spec.command);
651
+ return out;
652
+ }
653
+ //#endregion
654
+ export { STRIPPED_AUTH_ROUTING_ENV_KEYS as a, launchChild as i, buildCodexProviderConfigFlags as n, getCodexVersion as r, provisionToolbelt as t };
655
+
656
+ //# sourceMappingURL=provision-DSkPa5hY.js.map