github-router 0.3.68 → 0.3.71

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.
package/dist/main.js CHANGED
@@ -1,22 +1,21 @@
1
1
  #!/usr/bin/env node
2
- import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeRuntimeFileSecure, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS } from "./paths-CZvFif-e.js";
3
- import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-hkBEjHb2.js";
2
+ import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeRuntimeFileSecure, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS } from "./paths-CoFnpNZl.js";
3
+ import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-NQRdfY1u.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { defineCommand, runMain } from "citty";
6
6
  import consola from "consola";
7
7
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
8
- import fs, { readFile, stat } from "node:fs/promises";
8
+ import fs, { chmod, copyFile, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
9
9
  import os, { homedir, platform } from "node:os";
10
10
  import * as path$1 from "node:path";
11
11
  import path, { dirname, join } from "node:path";
12
12
  import process$1 from "node:process";
13
13
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
14
- import { promisify } from "node:util";
15
14
  import fs$1, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
16
  import { createInterface } from "node:readline";
17
17
  import Parser from "web-tree-sitter";
18
18
  import WebSocket from "ws";
19
- import { fileURLToPath } from "node:url";
20
19
  import { events } from "fetch-event-stream";
21
20
  import { Type } from "typebox";
22
21
  import "partial-json";
@@ -26,6 +25,7 @@ import "yaml";
26
25
  import "ignore";
27
26
  import { z } from "zod";
28
27
  import { Writable } from "node:stream";
28
+ import { gunzipSync, inflateRawSync } from "node:zlib";
29
29
  import { serve } from "srvx";
30
30
  import { getProxyForUrl } from "proxy-from-env";
31
31
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
@@ -757,24 +757,262 @@ const checkUsage = defineCommand({
757
757
  }
758
758
  });
759
759
 
760
+ //#endregion
761
+ //#region src/lib/exec.ts
762
+ /**
763
+ * Parse a boolean-ish env value. Returns `undefined` when unset or
764
+ * unrecognized so callers can apply their own default. Accepts
765
+ * `1|true|yes|on` (true) and `0|false|no|off|<empty>` (false),
766
+ * case-insensitive. The single shared parser for all new `GH_ROUTER_*`
767
+ * flags so on/off semantics don't drift per call site.
768
+ */
769
+ function parseBoolEnv(value) {
770
+ if (value === void 0) return void 0;
771
+ const v = value.trim().toLowerCase();
772
+ if (v === "1" || v === "true" || v === "yes" || v === "on") return true;
773
+ if (v === "0" || v === "false" || v === "no" || v === "off" || v === "") return false;
774
+ }
775
+ /** Read the PATH value from an env object, case-insensitively. */
776
+ function pathValueOf(env) {
777
+ for (const key of Object.keys(env)) if (key.toLowerCase() === "path") return env[key] ?? "";
778
+ return "";
779
+ }
780
+ /**
781
+ * Resolve an executable name to an absolute path against PATH, honoring
782
+ * `PATHEXT` on Windows and **excluding the current working directory**.
783
+ *
784
+ * Returns `null` when unresolved — callers treat that as "tool absent"
785
+ * and skip (best-effort). Spawning the returned absolute path means
786
+ * `cmd.exe`'s implicit cwd-first lookup never applies, closing the
787
+ * planted-`npm.cmd` vector.
788
+ */
789
+ function resolveExecutable(name$1, opts = {}) {
790
+ const platform$1 = opts.platform ?? process$1.platform;
791
+ const env = opts.env ?? process$1.env;
792
+ const cwdRaw = opts.cwd ?? (typeof process$1.cwd === "function" ? process$1.cwd() : void 0);
793
+ const resolvedCwd = cwdRaw ? path.resolve(cwdRaw) : null;
794
+ const dirs = pathValueOf(env).split(path.delimiter).filter((d) => d.length > 0 && d !== ".");
795
+ const isWin = platform$1 === "win32";
796
+ if (!isWin && name$1.includes("/")) return existsSync(name$1) ? path.resolve(name$1) : null;
797
+ if (isWin && (name$1.includes("\\") || name$1.includes("/"))) return existsSync(name$1) ? path.resolve(name$1) : null;
798
+ const exts = isWin && path.extname(name$1) === "" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim()).filter(Boolean) : [""];
799
+ for (const dir of dirs) {
800
+ if (resolvedCwd && path.resolve(dir) === resolvedCwd) continue;
801
+ for (const ext of exts) {
802
+ const candidate = path.join(dir, name$1 + ext);
803
+ if (existsSync(candidate)) return candidate;
804
+ }
805
+ }
806
+ return null;
807
+ }
808
+ /**
809
+ * Quote one argument for a `cmd.exe /c "<line>"` command line so the
810
+ * target program receives it verbatim and no `cmd.exe` metacharacter
811
+ * retains shell meaning.
812
+ *
813
+ * Two phases (the canonical Windows approach — Colascione / Rust std):
814
+ * 1. **argv quoting** so `CommandLineToArgvW` in the target parses
815
+ * the token as one argument (double-quote, backslash-escape).
816
+ * 2. **caret-escaping** every `cmd.exe` metacharacter — including the
817
+ * quotes from phase 1 — so `cmd.exe` is never in quote-mode, strips
818
+ * the carets, and hands the argv-quoted string to the program.
819
+ *
820
+ * `%` is special: it cannot be reliably escaped on the `cmd.exe`
821
+ * *command line* (caret does not stop `%VAR%` expansion there). Rather
822
+ * than mis-escape, we **throw** — our callers never pass `%`, so this
823
+ * fails closed on the one unescapable injection vector.
824
+ */
825
+ function quoteWinArg(arg) {
826
+ if (arg.includes("%")) throw new Error("buildExecInvocation: argument contains '%', which cannot be safely escaped on the Windows command line; refusing to build the command.");
827
+ let quoted;
828
+ if (arg.length > 0 && !/[ \t\n\v"&|<>()^!]/.test(arg)) quoted = arg;
829
+ else {
830
+ let s = "\"";
831
+ let backslashes = 0;
832
+ for (const ch of arg) if (ch === "\\") backslashes++;
833
+ else if (ch === "\"") {
834
+ s += "\\".repeat(backslashes * 2 + 1) + "\"";
835
+ backslashes = 0;
836
+ } else {
837
+ s += "\\".repeat(backslashes) + ch;
838
+ backslashes = 0;
839
+ }
840
+ s += "\\".repeat(backslashes * 2) + "\"";
841
+ quoted = s;
842
+ }
843
+ return quoted.replace(/[()!^"<>&|]/g, "^$&");
844
+ }
845
+ /**
846
+ * Build the platform-correct `spawn` invocation for a command given as
847
+ * an argv array. Pure / unit-testable (no spawn).
848
+ *
849
+ * - win32 → a single caret/argv-quoted command string + `shell:true`
850
+ * + empty args array (the empty array avoids the DEP0190 warning
851
+ * that fires when args and `shell:true` are combined). `cmd[0]`
852
+ * should already be an absolute path from `resolveExecutable`.
853
+ * - posix → `(cmd[0], cmd.slice(1))` with `shell:false` — no shell,
854
+ * no injection surface.
855
+ */
856
+ function buildExecInvocation(cmd, platform$1 = process$1.platform) {
857
+ if (cmd.length === 0) throw new Error("buildExecInvocation: empty command");
858
+ if (platform$1 === "win32") return {
859
+ command: cmd.map(quoteWinArg).join(" "),
860
+ args: [],
861
+ shell: true
862
+ };
863
+ return {
864
+ command: cmd[0],
865
+ args: cmd.slice(1),
866
+ shell: false
867
+ };
868
+ }
869
+ function runInternal(cmd, stdoutMode, opts) {
870
+ const { command, args, shell } = buildExecInvocation(cmd);
871
+ return new Promise((resolve, reject) => {
872
+ let child;
873
+ try {
874
+ child = spawn(command, args, {
875
+ cwd: opts.cwd,
876
+ env: opts.env ?? process$1.env,
877
+ shell,
878
+ windowsHide: true,
879
+ stdio: [
880
+ "ignore",
881
+ stdoutMode,
882
+ stdoutMode === "inherit" ? "inherit" : "pipe"
883
+ ]
884
+ });
885
+ } catch (err) {
886
+ reject(err instanceof Error ? err : new Error(String(err)));
887
+ return;
888
+ }
889
+ let stdout = "";
890
+ let stderr = "";
891
+ let timedOut = false;
892
+ let settled = false;
893
+ const timer = opts.timeoutMs ? setTimeout(() => {
894
+ timedOut = true;
895
+ killTree(child.pid);
896
+ }, opts.timeoutMs) : void 0;
897
+ timer?.unref?.();
898
+ child.stdout?.on("data", (c) => {
899
+ stdout += c.toString("utf8");
900
+ });
901
+ child.stderr?.on("data", (c) => {
902
+ stderr += c.toString("utf8");
903
+ });
904
+ child.stdout?.on("error", () => {});
905
+ child.stderr?.on("error", () => {});
906
+ const finish = (code) => {
907
+ if (settled) return;
908
+ settled = true;
909
+ if (timer) clearTimeout(timer);
910
+ resolve({
911
+ stdout,
912
+ stderr,
913
+ code,
914
+ timedOut
915
+ });
916
+ };
917
+ child.on("error", (err) => {
918
+ if (settled) return;
919
+ settled = true;
920
+ if (timer) clearTimeout(timer);
921
+ reject(err);
922
+ });
923
+ child.on("close", (code) => finish(code));
924
+ });
925
+ }
926
+ /** Kill a process tree best-effort (taskkill /T on Windows). */
927
+ function killTree(pid) {
928
+ if (!pid) return;
929
+ try {
930
+ if (process$1.platform === "win32") spawn("taskkill", [
931
+ "/T",
932
+ "/F",
933
+ "/PID",
934
+ String(pid)
935
+ ], {
936
+ stdio: "ignore",
937
+ windowsHide: true
938
+ });
939
+ else process$1.kill(pid, "SIGTERM");
940
+ } catch {}
941
+ }
942
+ /** Run a command and capture stdout/stderr. Rejects on spawn error. */
943
+ function runCommandCapture(cmd, opts = {}) {
944
+ return runInternal(cmd, "pipe", opts);
945
+ }
946
+ /** Run a command discarding output (still captures stderr for errors). */
947
+ function runCommandVoid(cmd, opts = {}) {
948
+ return runInternal(cmd, "pipe", opts);
949
+ }
950
+
951
+ //#endregion
952
+ //#region src/lib/update-lock.ts
953
+ /** A lock older than this is treated as stale (crashed holder) and stolen. */
954
+ const STALE_LOCK_MS = 600 * 1e3;
955
+ function lockPath(name$1) {
956
+ return path.join(os.homedir(), ".local", "share", "github-router", name$1);
957
+ }
958
+ /**
959
+ * Run `fn` while holding an exclusive lockfile named `name` under the
960
+ * app dir. Returns `true` if the lock was acquired and `fn` ran,
961
+ * `false` if another process already holds it (caller skips silently).
962
+ *
963
+ * A lock left by a crashed process older than `STALE_LOCK_MS` is
964
+ * stolen so the updater can never be wedged permanently.
965
+ */
966
+ async function withInstallLock(name$1, fn) {
967
+ const p = lockPath(name$1);
968
+ let handle = await tryCreateLock(p);
969
+ if (!handle) {
970
+ let stale = false;
971
+ try {
972
+ const s = await stat(p);
973
+ stale = Date.now() - s.mtimeMs > STALE_LOCK_MS;
974
+ } catch {
975
+ stale = true;
976
+ }
977
+ if (!stale) return false;
978
+ await rm(p, { force: true }).catch(() => {});
979
+ handle = await tryCreateLock(p);
980
+ if (!handle) return false;
981
+ }
982
+ try {
983
+ await handle.close();
984
+ await fn();
985
+ return true;
986
+ } finally {
987
+ await rm(p, { force: true }).catch(() => {});
988
+ }
989
+ }
990
+ async function tryCreateLock(p) {
991
+ try {
992
+ return await open(p, "wx");
993
+ } catch {
994
+ return null;
995
+ }
996
+ }
997
+
760
998
  //#endregion
761
999
  //#region src/lib/claude-version-check.ts
762
- const execFileAsync = promisify(execFile);
763
- const NPM_PACKAGE = "@anthropic-ai/claude-code";
764
- const THROTTLE_HOURS = 1;
765
- const NPM_VIEW_TIMEOUT_MS = 5e3;
1000
+ const NPM_PACKAGE$1 = "@anthropic-ai/claude-code";
1001
+ const THROTTLE_HOURS$1 = 1;
1002
+ const NPM_VIEW_TIMEOUT_MS$1 = 5e3;
1003
+ const CLAUDE_VERSION_TIMEOUT_MS = 3e3;
766
1004
  const NPM_INSTALL_TIMEOUT_MS = 12e4;
767
1005
  /** Path to the throttle cache. Created on demand. */
768
- function cacheFilePath() {
1006
+ function cacheFilePath$1() {
769
1007
  return path.join(os.homedir(), ".local", "share", "github-router", "last-update-check");
770
1008
  }
771
1009
  /**
772
1010
  * Read the throttle cache. Returns null on missing/corrupt file —
773
1011
  * triggers a fresh check.
774
1012
  */
775
- async function readCache() {
1013
+ async function readCache$1() {
776
1014
  try {
777
- const raw = await fs.readFile(cacheFilePath(), "utf8");
1015
+ const raw = await fs.readFile(cacheFilePath$1(), "utf8");
778
1016
  const parsed = JSON.parse(raw);
779
1017
  if (typeof parsed.checkedAt !== "string" || parsed.installedVersion !== null && typeof parsed.installedVersion !== "string" || parsed.latestVersion !== null && typeof parsed.latestVersion !== "string") return null;
780
1018
  return parsed;
@@ -782,37 +1020,38 @@ async function readCache() {
782
1020
  return null;
783
1021
  }
784
1022
  }
785
- async function writeCache(cache) {
1023
+ async function writeCache$1(cache) {
786
1024
  try {
787
- await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true });
788
- await fs.writeFile(cacheFilePath(), JSON.stringify(cache), { mode: 384 });
1025
+ await fs.mkdir(path.dirname(cacheFilePath$1()), { recursive: true });
1026
+ await fs.writeFile(cacheFilePath$1(), JSON.stringify(cache), { mode: 384 });
789
1027
  } catch (err) {
790
1028
  consola.debug("Failed to write claude version-check cache:", err);
791
1029
  }
792
1030
  }
793
1031
  /** Check if it's been more than THROTTLE_HOURS since the last check. */
794
- function shouldCheckNow(cache) {
1032
+ function shouldCheckNow$1(cache) {
795
1033
  if (!cache) return true;
796
1034
  const lastCheck = new Date(cache.checkedAt).getTime();
797
1035
  if (Number.isNaN(lastCheck)) return true;
798
- return (Date.now() - lastCheck) / 1e3 / 3600 >= THROTTLE_HOURS;
1036
+ return (Date.now() - lastCheck) / 1e3 / 3600 >= THROTTLE_HOURS$1;
799
1037
  }
800
1038
  /**
801
1039
  * Read the installed `claude` version. Returns null if claude is not
802
1040
  * on PATH or the version probe fails (e.g. older versions that don't
803
1041
  * support `--version` cleanly).
804
- */
805
- function getInstalledVersion() {
1042
+ *
1043
+ * Windows-safe: `claude` is a `.cmd` shim that `execFile` cannot launch
1044
+ * directly. We resolve it to an absolute path (excluding the cwd, so a
1045
+ * planted `claude.cmd` in an untrusted repo can't run) and invoke it
1046
+ * through the shared exec helper.
1047
+ */
1048
+ async function getInstalledVersion() {
1049
+ const claudePath = resolveExecutable("claude");
1050
+ if (!claudePath) return null;
806
1051
  try {
807
- const match = execFileSync("claude", ["--version"], {
808
- stdio: [
809
- "ignore",
810
- "pipe",
811
- "ignore"
812
- ],
813
- timeout: 3e3,
814
- encoding: "utf8"
815
- }).match(/^(\d+\.\d+\.\d+)/);
1052
+ const { stdout, code } = await runCommandCapture([claudePath, "--version"], { timeoutMs: CLAUDE_VERSION_TIMEOUT_MS });
1053
+ if (code !== 0) return null;
1054
+ const match = stdout.match(/(\d+\.\d+\.\d+)/);
816
1055
  return match ? match[1] : null;
817
1056
  } catch {
818
1057
  return null;
@@ -821,15 +1060,22 @@ function getInstalledVersion() {
821
1060
  /**
822
1061
  * Fetch the latest version of @anthropic-ai/claude-code from the npm
823
1062
  * registry. Returns null on network failure / npm unavailable.
1063
+ *
1064
+ * Windows-safe: `npm` is `npm.cmd`; resolved to an absolute path
1065
+ * (excluding cwd) before invocation.
824
1066
  */
825
- async function getLatestVersion() {
1067
+ async function getLatestVersion$1() {
1068
+ const npmPath = resolveExecutable("npm");
1069
+ if (!npmPath) return null;
826
1070
  try {
827
- const { stdout } = await execFileAsync("npm", [
1071
+ const { stdout, code } = await runCommandCapture([
1072
+ npmPath,
828
1073
  "view",
829
- NPM_PACKAGE,
1074
+ NPM_PACKAGE$1,
830
1075
  "version",
831
1076
  "--silent"
832
- ], { timeout: NPM_VIEW_TIMEOUT_MS });
1077
+ ], { timeoutMs: NPM_VIEW_TIMEOUT_MS$1 });
1078
+ if (code !== 0) return null;
833
1079
  const v = stdout.trim();
834
1080
  return /^\d+\.\d+\.\d+/.test(v) ? v : null;
835
1081
  } catch {
@@ -867,8 +1113,8 @@ async function checkClaudeVersion(opts = {}) {
867
1113
  skipped: true,
868
1114
  skipReason: "disabled"
869
1115
  };
870
- const cache = await readCache();
871
- if (!opts.force && !shouldCheckNow(cache)) return {
1116
+ const cache = await readCache$1();
1117
+ if (!opts.force && !shouldCheckNow$1(cache)) return {
872
1118
  installed: cache?.installedVersion !== null,
873
1119
  installedVersion: cache?.installedVersion ?? null,
874
1120
  latestVersion: cache?.latestVersion ?? null,
@@ -876,7 +1122,7 @@ async function checkClaudeVersion(opts = {}) {
876
1122
  skipped: true,
877
1123
  skipReason: "throttled"
878
1124
  };
879
- const installedVersion = getInstalledVersion();
1125
+ const installedVersion = await getInstalledVersion();
880
1126
  if (installedVersion === null) return {
881
1127
  installed: false,
882
1128
  installedVersion: null,
@@ -885,8 +1131,8 @@ async function checkClaudeVersion(opts = {}) {
885
1131
  skipped: true,
886
1132
  skipReason: "no-claude"
887
1133
  };
888
- const latestVersion = await getLatestVersion();
889
- await writeCache({
1134
+ const latestVersion = await getLatestVersion$1();
1135
+ await writeCache$1({
890
1136
  checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
891
1137
  installedVersion,
892
1138
  latestVersion
@@ -908,23 +1154,190 @@ async function checkClaudeVersion(opts = {}) {
908
1154
  };
909
1155
  }
910
1156
  /**
911
- * Run `npm install -g @anthropic-ai/claude-code@latest` synchronously.
912
- * Throws on failure the caller decides whether to abort the launch
913
- * or continue with the older version.
914
- */
915
- async function autoUpdateClaude(latestVersion) {
916
- consola.info(`Updating ${NPM_PACKAGE} to ${latestVersion} (this may take ~30s)...`);
917
- try {
918
- await execFileAsync("npm", [
1157
+ * Heuristic: did `claude update` fail because the subcommand does not
1158
+ * exist (a build predating `claude update`), as opposed to a transient
1159
+ * update error? We only npm-fall-back on this specific signal — falling
1160
+ * back on any failure would install a conflicting npm copy on
1161
+ * native-installer machines.
1162
+ *
1163
+ * Matches the usage/Commander-style "unknown command" messages CLIs emit
1164
+ * for an unrecognized subcommand. Deliberately narrow.
1165
+ */
1166
+ function looksLikeUnknownUpdateCommand(output) {
1167
+ return /unknown command|unknown subcommand|unrecognized (sub)?command|invalid command|command not found|is not a (known|valid) command/i.test(output);
1168
+ }
1169
+ async function updateClaude(latestVersion, deps = {}) {
1170
+ const _resolve = deps.resolveExecutable ?? resolveExecutable;
1171
+ const _capture = deps.runCommandCapture ?? runCommandCapture;
1172
+ const _void = deps.runCommandVoid ?? runCommandVoid;
1173
+ const _lock = deps.withInstallLock ?? withInstallLock;
1174
+ const claudePath = _resolve("claude");
1175
+ if (!claudePath) throw new Error("claude not found on PATH");
1176
+ if (!await _lock("claude-update.lock", async () => {
1177
+ consola.info(`Updating Claude Code to ${latestVersion} via \`claude update\`...`);
1178
+ const { code, stdout, stderr } = await _capture([claudePath, "update"], { timeoutMs: NPM_INSTALL_TIMEOUT_MS });
1179
+ const combined = `${stdout}${stderr}`;
1180
+ const trimmed = combined.trim();
1181
+ if (trimmed) process.stdout.write(trimmed.endsWith("\n") ? trimmed : `${trimmed}\n`);
1182
+ if (code === 0) {
1183
+ consola.success(`Claude Code updated to ${latestVersion}`);
1184
+ return;
1185
+ }
1186
+ if (!looksLikeUnknownUpdateCommand(combined)) throw new Error(`\`claude update\` exited with code ${code}`);
1187
+ const npmPath = _resolve("npm");
1188
+ if (!npmPath) throw new Error(`this Claude Code build predates \`claude update\` and npm is not on PATH; update manually: npm install -g ${NPM_PACKAGE$1}@latest`);
1189
+ consola.warn(`This Claude Code build predates \`claude update\`; falling back to \`npm install -g ${NPM_PACKAGE$1}@latest\`.`);
1190
+ const { code: npmCode, stderr: npmStderr } = await _void([
1191
+ npmPath,
919
1192
  "install",
920
1193
  "-g",
921
- `${NPM_PACKAGE}@latest`,
1194
+ `${NPM_PACKAGE$1}@latest`,
1195
+ "--silent"
1196
+ ], { timeoutMs: NPM_INSTALL_TIMEOUT_MS });
1197
+ if (npmCode !== 0) throw new Error(`npm install failed: ${npmStderr.trim() || `exit ${npmCode}`}`);
1198
+ consola.success(`${NPM_PACKAGE$1} updated to ${latestVersion}`);
1199
+ })) consola.debug("Claude Code update already in progress in another process; skipping.");
1200
+ }
1201
+
1202
+ //#endregion
1203
+ //#region src/lib/version.ts
1204
+ /**
1205
+ * Read this binary's published version from package.json at runtime.
1206
+ *
1207
+ * Done at runtime (not baked at build time) because release.yml builds
1208
+ * BEFORE `npm version patch` bumps the version — a build-time inline
1209
+ * would always ship the pre-bump value. The npm tarball ships package.json
1210
+ * alongside `dist/`, so a sibling-up lookup from import.meta.url resolves
1211
+ * cleanly in both dev (`src/lib/`) and bundled (`dist/`) layouts.
1212
+ *
1213
+ * Returns `"unknown"` if package.json can't be located or parsed —
1214
+ * never throws, so the CLI never fails to start over version reporting.
1215
+ */
1216
+ function getPackageVersion() {
1217
+ try {
1218
+ const here = dirname(fileURLToPath(import.meta.url));
1219
+ const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
1220
+ for (const path$2 of candidates) try {
1221
+ const raw = readFileSync(path$2, "utf8");
1222
+ const parsed = JSON.parse(raw);
1223
+ if (typeof parsed.version === "string" && (parsed.name === "github-router" || parsed.name === "@animeshkundu/github-router")) return parsed.version;
1224
+ } catch {}
1225
+ } catch {}
1226
+ return "unknown";
1227
+ }
1228
+
1229
+ //#endregion
1230
+ //#region src/lib/self-update.ts
1231
+ const NPM_PACKAGE = "github-router";
1232
+ const THROTTLE_HOURS = 1;
1233
+ const NPM_VIEW_TIMEOUT_MS = 5e3;
1234
+ function cacheFilePath() {
1235
+ return path.join(os.homedir(), ".local", "share", "github-router", "last-self-update-check");
1236
+ }
1237
+ async function readCache() {
1238
+ try {
1239
+ const parsed = JSON.parse(await fs.readFile(cacheFilePath(), "utf8"));
1240
+ if (typeof parsed.checkedAt !== "string") return null;
1241
+ return parsed;
1242
+ } catch {
1243
+ return null;
1244
+ }
1245
+ }
1246
+ async function writeCache(cache) {
1247
+ try {
1248
+ await fs.mkdir(path.dirname(cacheFilePath()), { recursive: true });
1249
+ await fs.writeFile(cacheFilePath(), JSON.stringify(cache), { mode: 384 });
1250
+ } catch (err) {
1251
+ consola.debug("Failed to write self-update cache:", err);
1252
+ }
1253
+ }
1254
+ function shouldCheckNow(cache) {
1255
+ if (!cache) return true;
1256
+ const last = new Date(cache.checkedAt).getTime();
1257
+ if (Number.isNaN(last)) return true;
1258
+ return (Date.now() - last) / 1e3 / 3600 >= THROTTLE_HOURS;
1259
+ }
1260
+ async function getLatestVersion(npmPath) {
1261
+ try {
1262
+ const { stdout, code } = await runCommandCapture([
1263
+ npmPath,
1264
+ "view",
1265
+ NPM_PACKAGE,
1266
+ "version",
922
1267
  "--silent"
923
- ], { timeout: NPM_INSTALL_TIMEOUT_MS });
924
- consola.success(`${NPM_PACKAGE} updated to ${latestVersion}`);
1268
+ ], { timeoutMs: NPM_VIEW_TIMEOUT_MS });
1269
+ if (code !== 0) return null;
1270
+ const v = stdout.trim();
1271
+ return /^\d+\.\d+\.\d+/.test(v) ? v : null;
1272
+ } catch {
1273
+ return null;
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Spawn a detached process that waits for THIS proxy (pid) to exit,
1278
+ * then runs `npm install -g github-router@latest`. Fully detached and
1279
+ * unref'd so it outlives the proxy; output discarded.
1280
+ *
1281
+ * The waiter is a tiny inline Node script (Node is guaranteed present —
1282
+ * the proxy runs on it) that polls `process.kill(pid, 0)` until the
1283
+ * parent is gone, then execs npm. This avoids the Windows file-lock by
1284
+ * never touching the global install while the proxy holds it open.
1285
+ */
1286
+ function spawnDetachedUpdater(npmPath) {
1287
+ const waiter = `
1288
+ const pid = ${process.pid};
1289
+ const { spawn } = require("node:child_process");
1290
+ function alive() { try { process.kill(pid, 0); return true } catch { return false } }
1291
+ const timer = setInterval(() => {
1292
+ if (alive()) return;
1293
+ clearInterval(timer);
1294
+ const args = ["install", "-g", ${JSON.stringify(`${NPM_PACKAGE}@latest`)}, "--silent"];
1295
+ const isWin = process.platform === "win32";
1296
+ const child = spawn(${JSON.stringify(npmPath)}, args, {
1297
+ stdio: "ignore", windowsHide: true, shell: isWin, detached: !isWin,
1298
+ });
1299
+ child.on("error", () => process.exit(0));
1300
+ child.on("exit", () => process.exit(0));
1301
+ // Safety: never hang forever.
1302
+ setTimeout(() => process.exit(0), 180000).unref();
1303
+ }, 500);
1304
+ // Safety cap on the wait itself (e.g. extremely long sessions still
1305
+ // eventually give up rather than leak the waiter).
1306
+ setTimeout(() => { clearInterval(timer); process.exit(0); }, 24 * 3600 * 1000).unref();
1307
+ `.trim();
1308
+ spawn(process.execPath, ["-e", waiter], {
1309
+ detached: true,
1310
+ stdio: "ignore",
1311
+ windowsHide: true
1312
+ }).unref();
1313
+ }
1314
+ /**
1315
+ * Probe npm for a newer `github-router` and, if found, queue a detached
1316
+ * post-exit update. Returns quickly; never throws. Call AFTER the
1317
+ * server is listening so the bounded probe can't delay binding.
1318
+ */
1319
+ async function runSelfUpdate(opts) {
1320
+ if (!opts.selfUpdate) return;
1321
+ if (parseBoolEnv(process.env.GH_ROUTER_NO_SELF_UPDATE) === true) return;
1322
+ try {
1323
+ const cache = await readCache();
1324
+ if (!opts.force && !shouldCheckNow(cache)) return;
1325
+ const installed = getPackageVersion();
1326
+ if (installed === "unknown") return;
1327
+ const npmPath = resolveExecutable("npm");
1328
+ if (!npmPath) return;
1329
+ const latest = await getLatestVersion(npmPath);
1330
+ await writeCache({
1331
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
1332
+ installedVersion: installed,
1333
+ latestVersion: latest
1334
+ });
1335
+ if (!latest || !isNewer(installed, latest)) return;
1336
+ if (await withInstallLock("self-update.lock", async () => {
1337
+ spawnDetachedUpdater(npmPath);
1338
+ })) consola.info(`github-router ${installed} → ${latest} update queued; it takes effect on the next launch.`);
925
1339
  } catch (err) {
926
- const msg = err instanceof Error ? err.message : String(err);
927
- throw new Error(`npm install failed: ${msg}`);
1340
+ consola.debug("Self-update check failed:", err);
928
1341
  }
929
1342
  }
930
1343
 
@@ -1039,6 +1452,47 @@ function envInt$1(key, fallback) {
1039
1452
  const UPSTREAM_FETCH_TIMEOUT_MS = envInt$1("UPSTREAM_FETCH_TIMEOUT_MS", 0);
1040
1453
  const UPSTREAM_INACTIVITY_TIMEOUT_MS = envInt$1("UPSTREAM_INACTIVITY_TIMEOUT_MS", 3e5);
1041
1454
 
1455
+ //#endregion
1456
+ //#region src/lib/toolbelt/path-inject.ts
1457
+ /**
1458
+ * The key under which `env` stores PATH, matched case-insensitively.
1459
+ * Falls back to the platform-conventional spelling when absent.
1460
+ */
1461
+ function pathEnvKey(env) {
1462
+ for (const key of Object.keys(env)) if (key.toLowerCase() === "path") return key;
1463
+ return process.platform === "win32" ? "Path" : "PATH";
1464
+ }
1465
+ /**
1466
+ * Compute the PATH override that prepends `binDir`, reusing the parent's
1467
+ * existing key casing so a subsequent merge can't introduce a duplicate
1468
+ * case-variant key. Returns a single-entry patch suitable for
1469
+ * `Object.assign(vars, ...)`.
1470
+ */
1471
+ function toolbeltPathOverride(parentEnv, binDir) {
1472
+ const key = pathEnvKey(parentEnv);
1473
+ const current = parentEnv[key] ?? "";
1474
+ return { [key]: current ? `${binDir}${path.delimiter}${current}` : binDir };
1475
+ }
1476
+ /**
1477
+ * Defense-in-depth: collapse all case-variant PATH keys in `env` into a
1478
+ * single canonical key. Mutates and returns `env`. If duplicates with
1479
+ * differing values exist (only possible via a mismatched-casing merge),
1480
+ * the longest value wins — the toolbelt-prepended PATH is strictly
1481
+ * longer than the original, so this preserves the injection.
1482
+ */
1483
+ function collapsePathKeys(env) {
1484
+ const keys = Object.keys(env).filter((k) => k.toLowerCase() === "path");
1485
+ if (keys.length <= 1) return env;
1486
+ let bestValue = "";
1487
+ for (const k of keys) {
1488
+ const v = env[k] ?? "";
1489
+ if (v.length >= bestValue.length) bestValue = v;
1490
+ delete env[k];
1491
+ }
1492
+ env[process.platform === "win32" ? "Path" : "PATH"] = bestValue;
1493
+ return env;
1494
+ }
1495
+
1042
1496
  //#endregion
1043
1497
  //#region src/lib/launch.ts
1044
1498
  /**
@@ -1103,6 +1557,22 @@ function commandExists(name$1) {
1103
1557
  }
1104
1558
  }
1105
1559
  /**
1560
+ * Whether the launcher can execute `executable`.
1561
+ *
1562
+ * `buildLaunchCommand` resolves the CLI to an ABSOLUTE path (anti-shadow).
1563
+ * `where.exe` (and POSIX `which`) reject a full path argument — `where`
1564
+ * returns "Could not find files for the given pattern(s)" for an absolute
1565
+ * path even when the file exists — so the where/which probe is only valid
1566
+ * for bare command names. For an absolute path (already resolved against
1567
+ * PATH and existence-checked by `resolveExecutable`), check the
1568
+ * filesystem directly. Without this split, every launch where the CLI is
1569
+ * installed fails with a spurious "not found on PATH".
1570
+ */
1571
+ function isExecutableAvailable(executable) {
1572
+ if (path.isAbsolute(executable)) return existsSync(executable);
1573
+ return commandExists(executable);
1574
+ }
1575
+ /**
1106
1576
  * Provider-config flags (`-c model_providers.github_router=...`) that
1107
1577
  * point Codex at our proxy. Extracted from `buildCodexCmd` so the new
1108
1578
  * `codex mcp-server` MCP-config builder can reuse the exact same
@@ -1170,22 +1640,25 @@ function buildCodexCmd(target) {
1170
1640
  return cmd;
1171
1641
  }
1172
1642
  function buildLaunchCommand(target) {
1643
+ const cmd = target.kind === "claude-code" ? [
1644
+ "claude",
1645
+ "--dangerously-skip-permissions",
1646
+ ...target.extraArgs
1647
+ ] : buildCodexCmd(target);
1648
+ const resolved = resolveExecutable(cmd[0], { env: process$1.env });
1649
+ if (resolved) cmd[0] = resolved;
1173
1650
  return {
1174
- cmd: target.kind === "claude-code" ? [
1175
- "claude",
1176
- "--dangerously-skip-permissions",
1177
- ...target.extraArgs
1178
- ] : buildCodexCmd(target),
1179
- env: {
1651
+ cmd,
1652
+ env: collapsePathKeys({
1180
1653
  ...sanitizeParentEnv(process$1.env),
1181
1654
  ...target.envVars
1182
- }
1655
+ })
1183
1656
  };
1184
1657
  }
1185
1658
  function launchChild(target, server$1, options = {}) {
1186
1659
  const { cmd, env } = buildLaunchCommand(target);
1187
1660
  const executable = cmd[0];
1188
- if (!commandExists(executable)) {
1661
+ if (!isExecutableAvailable(executable)) {
1189
1662
  const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
1190
1663
  consola.error(msg);
1191
1664
  process$1.stderr.write(msg + "\n");
@@ -2499,33 +2972,6 @@ function round4(x) {
2499
2972
  return Math.round(x * 1e4) / 1e4;
2500
2973
  }
2501
2974
 
2502
- //#endregion
2503
- //#region src/lib/version.ts
2504
- /**
2505
- * Read this binary's published version from package.json at runtime.
2506
- *
2507
- * Done at runtime (not baked at build time) because release.yml builds
2508
- * BEFORE `npm version patch` bumps the version — a build-time inline
2509
- * would always ship the pre-bump value. The npm tarball ships package.json
2510
- * alongside `dist/`, so a sibling-up lookup from import.meta.url resolves
2511
- * cleanly in both dev (`src/lib/`) and bundled (`dist/`) layouts.
2512
- *
2513
- * Returns `"unknown"` if package.json can't be located or parsed —
2514
- * never throws, so the CLI never fails to start over version reporting.
2515
- */
2516
- function getPackageVersion() {
2517
- try {
2518
- const here = dirname(fileURLToPath(import.meta.url));
2519
- const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
2520
- for (const path$2 of candidates) try {
2521
- const raw = readFileSync(path$2, "utf8");
2522
- const parsed = JSON.parse(raw);
2523
- if (typeof parsed.version === "string" && (parsed.name === "github-router" || parsed.name === "@animeshkundu/github-router")) return parsed.version;
2524
- } catch {}
2525
- } catch {}
2526
- return "unknown";
2527
- }
2528
-
2529
2975
  //#endregion
2530
2976
  //#region src/lib/browser-mcp/browser-detect.ts
2531
2977
  let cached;
@@ -3496,7 +3942,7 @@ function logAudit$1(record) {
3496
3942
  try {
3497
3943
  const fs$2 = await import("node:fs/promises");
3498
3944
  const path$2 = await import("node:path");
3499
- const { PATHS: PATHS$1 } = await import("./paths-CW16Dz9_.js");
3945
+ const { PATHS: PATHS$1 } = await import("./paths-DhM3Yi80.js");
3500
3946
  const dir = path$2.join(PATHS$1.APP_DIR, "browser-mcp");
3501
3947
  await fs$2.mkdir(dir, { recursive: true });
3502
3948
  const line = JSON.stringify({
@@ -10226,6 +10672,239 @@ async function searchWeb(query, signal) {
10226
10672
  }
10227
10673
  }
10228
10674
 
10675
+ //#endregion
10676
+ //#region src/lib/toolbelt/manifest.ts
10677
+ function platformArchKey(platform$1 = process.platform, arch = process.arch) {
10678
+ return `${platform$1}-${arch}`;
10679
+ }
10680
+ function assetFor(spec, platform$1 = process.platform, arch = process.arch) {
10681
+ return spec.assets[platformArchKey(platform$1, arch)];
10682
+ }
10683
+ const TOOLBELT_TOOLS = [
10684
+ {
10685
+ command: "fd",
10686
+ binBasename: "fd",
10687
+ assets: {
10688
+ "win32-x64": {
10689
+ url: "https://github.com/sharkdp/fd/releases/download/v10.4.2/fd-v10.4.2-x86_64-pc-windows-msvc.zip",
10690
+ sha256: "b2816e506390a89941c63c9187d58a3cc10e9a55f2ef0685f9ea0eccaf7c98c8",
10691
+ archive: "zip"
10692
+ },
10693
+ "win32-arm64": {
10694
+ url: "https://github.com/sharkdp/fd/releases/download/v10.4.2/fd-v10.4.2-aarch64-pc-windows-msvc.zip",
10695
+ sha256: "4f9110c2d5b33a7f760bfa5510f4c113d828109f7277d421b1053a9943c0fc92",
10696
+ archive: "zip"
10697
+ },
10698
+ "darwin-arm64": {
10699
+ url: "https://github.com/sharkdp/fd/releases/download/v10.4.2/fd-v10.4.2-aarch64-apple-darwin.tar.gz",
10700
+ sha256: "623dc0afc81b92e4d4606b380d7bc91916ba7b97814263e554d50923a39e480a",
10701
+ archive: "tar.gz"
10702
+ },
10703
+ "linux-x64": {
10704
+ url: "https://github.com/sharkdp/fd/releases/download/v10.4.2/fd-v10.4.2-x86_64-unknown-linux-musl.tar.gz",
10705
+ sha256: "e3257d48e29a6be965187dbd24ce9af564e0fe67b3e73c9bdcd180f4ec11bdde",
10706
+ archive: "tar.gz"
10707
+ },
10708
+ "linux-arm64": {
10709
+ url: "https://github.com/sharkdp/fd/releases/download/v10.4.2/fd-v10.4.2-aarch64-unknown-linux-musl.tar.gz",
10710
+ sha256: "f32d3657473fba74e2600babc8db0b93420d51169223b7e8143b2ed55d8fd9e8",
10711
+ archive: "tar.gz"
10712
+ }
10713
+ }
10714
+ },
10715
+ {
10716
+ command: "sd",
10717
+ binBasename: "sd",
10718
+ assets: {
10719
+ "win32-x64": {
10720
+ url: "https://github.com/chmln/sd/releases/download/v1.1.0/sd-v1.1.0-x86_64-pc-windows-msvc.zip",
10721
+ sha256: "59837c2e7c911099aca1cc46b663bcdc5a949fd3e9fbbaf34fc73e5d5d71007c",
10722
+ archive: "zip"
10723
+ },
10724
+ "darwin-x64": {
10725
+ url: "https://github.com/chmln/sd/releases/download/v1.1.0/sd-v1.1.0-x86_64-apple-darwin.tar.gz",
10726
+ sha256: "1fca1e9c91813a8aac6821063c923107ba0f66a83309e095edcd3b202f67f97e",
10727
+ archive: "tar.gz"
10728
+ },
10729
+ "darwin-arm64": {
10730
+ url: "https://github.com/chmln/sd/releases/download/v1.1.0/sd-v1.1.0-aarch64-apple-darwin.tar.gz",
10731
+ sha256: "4bd3c09226376ca0a1d69589c91e86276fae36c5fbaaee669afce583f6682030",
10732
+ archive: "tar.gz"
10733
+ },
10734
+ "linux-x64": {
10735
+ url: "https://github.com/chmln/sd/releases/download/v1.1.0/sd-v1.1.0-x86_64-unknown-linux-musl.tar.gz",
10736
+ sha256: "02f00f4777d43e8e95b7b8d49e1a0d6e502fed4b8e79c1c8b8063857a30caa2e",
10737
+ archive: "tar.gz"
10738
+ },
10739
+ "linux-arm64": {
10740
+ url: "https://github.com/chmln/sd/releases/download/v1.1.0/sd-v1.1.0-aarch64-unknown-linux-musl.tar.gz",
10741
+ sha256: "ec8c93c0533ff21f4851d11566808d4082544baf063d9b96ea77c27e98b7cd99",
10742
+ archive: "tar.gz"
10743
+ }
10744
+ }
10745
+ },
10746
+ {
10747
+ command: "jq",
10748
+ binBasename: "jq",
10749
+ assets: {
10750
+ "win32-x64": {
10751
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe",
10752
+ sha256: "23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334",
10753
+ archive: "raw"
10754
+ },
10755
+ "darwin-x64": {
10756
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-amd64",
10757
+ sha256: "e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f",
10758
+ archive: "raw"
10759
+ },
10760
+ "darwin-arm64": {
10761
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-arm64",
10762
+ sha256: "a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603",
10763
+ archive: "raw"
10764
+ },
10765
+ "linux-x64": {
10766
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64",
10767
+ sha256: "020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d",
10768
+ archive: "raw"
10769
+ },
10770
+ "linux-arm64": {
10771
+ url: "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-arm64",
10772
+ sha256: "6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4",
10773
+ archive: "raw"
10774
+ }
10775
+ }
10776
+ },
10777
+ {
10778
+ command: "yq",
10779
+ binBasename: "yq",
10780
+ assets: {
10781
+ "win32-x64": {
10782
+ url: "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_windows_amd64.exe",
10783
+ sha256: "2aee32f1de46a20672f48c25df3018839798bd509143f2ce05fdab1550ff5592",
10784
+ archive: "raw"
10785
+ },
10786
+ "win32-arm64": {
10787
+ url: "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_windows_arm64.exe",
10788
+ sha256: "448208550332ca33ef816e4cee49fc1e79987b8a08a451c6ae529703c8cfc8a9",
10789
+ archive: "raw"
10790
+ },
10791
+ "darwin-x64": {
10792
+ url: "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_darwin_amd64",
10793
+ sha256: "616b0a0f6a5b79d746f05a169c2b9bb40dee00c605ef165b9a1c1681bba738ac",
10794
+ archive: "raw"
10795
+ },
10796
+ "darwin-arm64": {
10797
+ url: "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_darwin_arm64",
10798
+ sha256: "541ba2287560df70f561955e2d7f7e1cd00cf2a15a884f6b5c87a4bfa887bc07",
10799
+ archive: "raw"
10800
+ },
10801
+ "linux-x64": {
10802
+ url: "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_linux_amd64",
10803
+ sha256: "d56bf5c6819e8e696340c312bd70f849dc1678a7cda9c2ad63eebd906371d56b",
10804
+ archive: "raw"
10805
+ },
10806
+ "linux-arm64": {
10807
+ url: "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_linux_arm64",
10808
+ sha256: "03061b2a50c7a498de2bbb92d7cb078ce433011f085a4994117c2726be4106ea",
10809
+ archive: "raw"
10810
+ }
10811
+ }
10812
+ },
10813
+ {
10814
+ command: "ast-grep",
10815
+ binBasename: "ast-grep",
10816
+ aliases: ["sg"],
10817
+ assets: {
10818
+ "win32-x64": {
10819
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.43.0/app-x86_64-pc-windows-msvc.zip",
10820
+ sha256: "a4febbc8c48671e5729d85e29e4ebe5a051b7250d19545bca18e725ccf40ef61",
10821
+ archive: "zip"
10822
+ },
10823
+ "win32-arm64": {
10824
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.43.0/app-aarch64-pc-windows-msvc.zip",
10825
+ sha256: "a519fdd90324bf6858fde2d3feb2b862d67b834dc11af8f5b6c2c8143ab6a6c5",
10826
+ archive: "zip"
10827
+ },
10828
+ "darwin-x64": {
10829
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.43.0/app-x86_64-apple-darwin.zip",
10830
+ sha256: "6d703090b106747b2f56086b6ccc7e798fe78bcae70257aa20519b220153555b",
10831
+ archive: "zip"
10832
+ },
10833
+ "darwin-arm64": {
10834
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.43.0/app-aarch64-apple-darwin.zip",
10835
+ sha256: "8c847d0a29aa4b3101b3361e0b3ee7fb53c7e497adc9ed1afc9615538cd40782",
10836
+ archive: "zip"
10837
+ },
10838
+ "linux-x64": {
10839
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.43.0/app-x86_64-unknown-linux-gnu.zip",
10840
+ sha256: "a26253a9c821d935f7e383e40f0de7c2ca62a4121de1f73a6d81ec32eae631e0",
10841
+ archive: "zip"
10842
+ },
10843
+ "linux-arm64": {
10844
+ url: "https://github.com/ast-grep/ast-grep/releases/download/0.43.0/app-aarch64-unknown-linux-gnu.zip",
10845
+ sha256: "e706846148493967f3ab8011334817edd86ce5acbec10718b2a7b40799c640ff",
10846
+ archive: "zip"
10847
+ }
10848
+ }
10849
+ }
10850
+ ];
10851
+
10852
+ //#endregion
10853
+ //#region src/lib/toolbelt/index.ts
10854
+ /** Default ON; disable with GH_ROUTER_DISABLE_TOOLBELT (truthy). */
10855
+ function toolbeltEnabled() {
10856
+ return parseBoolEnv(process.env.GH_ROUTER_DISABLE_TOOLBELT) !== true;
10857
+ }
10858
+ /** Per-tool opt-out via GH_ROUTER_TOOLBELT_SKIP="jq,yq". */
10859
+ function toolbeltSkipSet() {
10860
+ const raw = process.env.GH_ROUTER_TOOLBELT_SKIP;
10861
+ if (!raw) return /* @__PURE__ */ new Set();
10862
+ return new Set(raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean));
10863
+ }
10864
+ /** Absolute path to the bundled `@vscode/ripgrep` binary, or null. */
10865
+ function vscodeRipgrepPath() {
10866
+ try {
10867
+ const mod = createRequire(import.meta.url)("@vscode/ripgrep");
10868
+ if (mod.rgPath && existsSync(mod.rgPath)) return mod.rgPath;
10869
+ } catch {}
10870
+ return null;
10871
+ }
10872
+ /**
10873
+ * Every curated tool the spawned agent can actually invoke this launch
10874
+ * — whether it is already on the user's system PATH OR will be
10875
+ * materialized into the toolbelt bin (gap-fill). Used for the awareness
10876
+ * one-liner so the model is told about ALL available fast tools, not
10877
+ * just the ones we had to download. (Provisioning still only downloads
10878
+ * the gap-fill subset; this is purely the advertised set.)
10879
+ */
10880
+ function availableToolCommands() {
10881
+ if (!toolbeltEnabled()) return [];
10882
+ const skip = toolbeltSkipSet();
10883
+ const out = [];
10884
+ if (!skip.has("rg") && (resolveExecutable("rg") || vscodeRipgrepPath())) out.push("rg");
10885
+ for (const spec of TOOLBELT_TOOLS) {
10886
+ if (skip.has(spec.command)) continue;
10887
+ if (resolveExecutable(spec.command) || assetFor(spec)) out.push(spec.command);
10888
+ }
10889
+ return out;
10890
+ }
10891
+ const TOOL_DESC = {
10892
+ rg: "rg (fast regex search)",
10893
+ fd: "fd (fast file finder)",
10894
+ jq: "jq (JSON processor)",
10895
+ sd: "sd (find & replace)",
10896
+ "ast-grep": "ast-grep / sg (structural code search & rewrite)",
10897
+ yq: "yq (YAML / TOML / XML processor)"
10898
+ };
10899
+ /**
10900
+ * The one-line CLAUDE.md / system-prompt note advertising the exposed
10901
+ * tools, or null when none are exposed.
10902
+ */
10903
+ function buildToolbeltAwareness(commands) {
10904
+ if (commands.length === 0) return null;
10905
+ return "Fast CLI tools are available on your PATH; prefer them when applicable: " + commands.map((c) => TOOL_DESC[c] ?? c).join(", ") + ".";
10906
+ }
10907
+
10229
10908
  //#endregion
10230
10909
  //#region src/lib/worker-agent/bash.ts
10231
10910
  /**
@@ -10272,6 +10951,7 @@ function buildEnv() {
10272
10951
  const v = process$1.env[key];
10273
10952
  if (v !== void 0) env[key] = v;
10274
10953
  }
10954
+ if (toolbeltEnabled()) Object.assign(env, toolbeltPathOverride(env, PATHS.TOOLBELT_BIN_DIR));
10275
10955
  return env;
10276
10956
  }
10277
10957
  /**
@@ -13491,6 +14171,8 @@ const PEER_MARKER_OPEN = "<!-- gh-router peer-mcp awareness — auto-injected, r
13491
14171
  const PEER_MARKER_CLOSE = "<!-- /gh-router peer-mcp awareness -->";
13492
14172
  const STYLE_MARKER_OPEN = "<!-- gh-router style directive — auto-injected, regenerated per launch -->";
13493
14173
  const STYLE_MARKER_CLOSE = "<!-- /gh-router style directive -->";
14174
+ const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injected, regenerated per launch -->";
14175
+ const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
13494
14176
  /**
13495
14177
  * Writing / communication style directive injected at the TOP of the
13496
14178
  * mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
@@ -13837,6 +14519,303 @@ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTI
13837
14519
  label: "style-directive"
13838
14520
  });
13839
14521
  }
14522
+ /**
14523
+ * Append the toolbelt awareness one-liner (which CLI tools are on PATH)
14524
+ * to the bottom of the mirrored CLAUDE.md so descendant agents (Agent
14525
+ * subagents, agent-teams teammates) learn about the provisioned tools.
14526
+ * The main agent gets the same line via `--append-system-prompt`.
14527
+ * Separate marker fence from the peer-awareness / style blocks.
14528
+ */
14529
+ async function appendToolbeltAwarenessToMirroredClaudeMd(snippet) {
14530
+ await injectMarkerBlock({
14531
+ snippet,
14532
+ markerOpen: TOOLBELT_MARKER_OPEN,
14533
+ markerClose: TOOLBELT_MARKER_CLOSE,
14534
+ position: "bottom",
14535
+ label: "toolbelt-awareness"
14536
+ });
14537
+ }
14538
+
14539
+ //#endregion
14540
+ //#region src/lib/toolbelt/extract.ts
14541
+ function baseName(p) {
14542
+ const norm = p.replace(/\\/g, "/");
14543
+ const idx = norm.lastIndexOf("/");
14544
+ return idx === -1 ? norm : norm.slice(idx + 1);
14545
+ }
14546
+ /**
14547
+ * Extract the first REGULAR-FILE tar member whose basename equals
14548
+ * `wantBasename` (optionally with a `.exe` suffix). Returns its bytes,
14549
+ * or null if absent. `buf` is the gzip-compressed tarball.
14550
+ */
14551
+ function extractTarGzMember(buf, wantBasename) {
14552
+ let tar;
14553
+ try {
14554
+ tar = gunzipSync(buf);
14555
+ } catch {
14556
+ return null;
14557
+ }
14558
+ const wants = new Set([wantBasename, `${wantBasename}.exe`]);
14559
+ let offset = 0;
14560
+ while (offset + 512 <= tar.length) {
14561
+ const header = tar.subarray(offset, offset + 512);
14562
+ if (header.every((b) => b === 0)) break;
14563
+ const name$1 = readTarString(header, 0, 100);
14564
+ const prefix = readTarString(header, 345, 155);
14565
+ const fullName = prefix ? `${prefix}/${name$1}` : name$1;
14566
+ const sizeOctal = readTarString(header, 124, 12).trim();
14567
+ const size = parseInt(sizeOctal || "0", 8);
14568
+ const typeflag = String.fromCharCode(header[156]);
14569
+ const dataStart = offset + 512;
14570
+ if ((typeflag === "0" || typeflag === "\0") && wants.has(baseName(fullName))) {
14571
+ if (dataStart + size > tar.length) return null;
14572
+ return Buffer.from(tar.subarray(dataStart, dataStart + size));
14573
+ }
14574
+ offset = dataStart + Math.ceil(size / 512) * 512;
14575
+ }
14576
+ return null;
14577
+ }
14578
+ function readTarString(block, start$1, len) {
14579
+ const slice = block.subarray(start$1, start$1 + len);
14580
+ const nul = slice.indexOf(0);
14581
+ return slice.subarray(0, nul === -1 ? len : nul).toString("utf8");
14582
+ }
14583
+ /**
14584
+ * Extract the first REGULAR-FILE zip member whose basename equals
14585
+ * `wantBasename` (optionally `.exe`). Supports stored (0) and deflate
14586
+ * (8) compression. Rejects directories and unix-symlink entries.
14587
+ */
14588
+ function extractZipMember(buf, wantBasename) {
14589
+ const wants = new Set([wantBasename, `${wantBasename}.exe`]);
14590
+ const EOCD_SIG = 101010256;
14591
+ let eocd = -1;
14592
+ const minStart = Math.max(0, buf.length - 65557);
14593
+ for (let i = buf.length - 22; i >= minStart; i--) if (buf.readUInt32LE(i) === EOCD_SIG) {
14594
+ eocd = i;
14595
+ break;
14596
+ }
14597
+ if (eocd === -1) return null;
14598
+ const entryCount = buf.readUInt16LE(eocd + 10);
14599
+ let cd = buf.readUInt32LE(eocd + 16);
14600
+ const CEN_SIG = 33639248;
14601
+ for (let i = 0; i < entryCount; i++) {
14602
+ if (cd + 46 > buf.length || buf.readUInt32LE(cd) !== CEN_SIG) return null;
14603
+ const method = buf.readUInt16LE(cd + 10);
14604
+ const compSize = buf.readUInt32LE(cd + 20);
14605
+ const nameLen = buf.readUInt16LE(cd + 28);
14606
+ const extraLen = buf.readUInt16LE(cd + 30);
14607
+ const commentLen = buf.readUInt16LE(cd + 32);
14608
+ const externalAttrs = buf.readUInt32LE(cd + 38);
14609
+ const localOffset = buf.readUInt32LE(cd + 42);
14610
+ const name$1 = buf.subarray(cd + 46, cd + 46 + nameLen).toString("utf8");
14611
+ const isSymlink = (externalAttrs >>> 16 & 61440) === 40960;
14612
+ const isDir = name$1.endsWith("/");
14613
+ if (!isSymlink && !isDir && wants.has(baseName(name$1))) return readZipLocalEntry(buf, localOffset, method, compSize);
14614
+ cd += 46 + nameLen + extraLen + commentLen;
14615
+ }
14616
+ return null;
14617
+ }
14618
+ function readZipLocalEntry(buf, localOffset, method, compSize) {
14619
+ if (localOffset + 30 > buf.length || buf.readUInt32LE(localOffset) !== 67324752) return null;
14620
+ const nameLen = buf.readUInt16LE(localOffset + 26);
14621
+ const extraLen = buf.readUInt16LE(localOffset + 28);
14622
+ const dataStart = localOffset + 30 + nameLen + extraLen;
14623
+ const comp = buf.subarray(dataStart, dataStart + compSize);
14624
+ try {
14625
+ if (method === 0) return Buffer.from(comp);
14626
+ if (method === 8) return inflateRawSync(comp);
14627
+ } catch {
14628
+ return null;
14629
+ }
14630
+ return null;
14631
+ }
14632
+
14633
+ //#endregion
14634
+ //#region src/lib/toolbelt/provision.ts
14635
+ /** Per-download cap (bytes) — these binaries are a few MB at most. */
14636
+ const MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024;
14637
+ const DOWNLOAD_TIMEOUT_MS = 3e4;
14638
+ const EXE_EXT = process$1.platform === "win32" ? ".exe" : "";
14639
+ /**
14640
+ * Materialize the toolbelt. Returns the list of command names exposed
14641
+ * in `bin/` after provisioning. Best-effort; never throws.
14642
+ */
14643
+ async function provisionToolbelt() {
14644
+ if (!toolbeltEnabled()) return [];
14645
+ const binDir = PATHS.TOOLBELT_BIN_DIR;
14646
+ try {
14647
+ await mkdir(binDir, { recursive: true });
14648
+ } catch (err) {
14649
+ consola.debug("toolbelt: could not create bin dir:", err);
14650
+ return [];
14651
+ }
14652
+ const skip = toolbeltSkipSet();
14653
+ await withInstallLock("toolbelt.lock", async () => {
14654
+ await pruneUnexpected(binDir);
14655
+ await provisionRg(binDir, skip).catch((err) => consola.debug("toolbelt: rg skipped:", err));
14656
+ await Promise.all(TOOLBELT_TOOLS.map((spec) => provisionTool(spec, binDir, skip).catch((err) => consola.debug(`toolbelt: ${spec.command} skipped:`, err))));
14657
+ });
14658
+ return exposedCommands(binDir);
14659
+ }
14660
+ /** Names allowed to live in `bin/` (managed binaries + their sidecars). */
14661
+ function expectedFileNames() {
14662
+ const names = /* @__PURE__ */ new Set();
14663
+ const add = (base) => {
14664
+ names.add(base + EXE_EXT);
14665
+ names.add(`${base}${EXE_EXT}.sha256`);
14666
+ };
14667
+ add("rg");
14668
+ for (const spec of TOOLBELT_TOOLS) {
14669
+ add(spec.binBasename);
14670
+ for (const a of spec.aliases ?? []) add(a);
14671
+ }
14672
+ return names;
14673
+ }
14674
+ /** Remove any file in `bin/` that isn't a managed binary or sidecar. */
14675
+ async function pruneUnexpected(binDir) {
14676
+ const expected = expectedFileNames();
14677
+ let entries;
14678
+ try {
14679
+ entries = await readdir(binDir);
14680
+ } catch {
14681
+ return;
14682
+ }
14683
+ for (const name$1 of entries) {
14684
+ if (name$1.endsWith(".tmp")) continue;
14685
+ if (!expected.has(name$1)) await rm(path.join(binDir, name$1), { force: true }).catch(() => {});
14686
+ }
14687
+ }
14688
+ async function provisionRg(binDir, skip) {
14689
+ const dest = path.join(binDir, "rg" + EXE_EXT);
14690
+ if (skip.has("rg") || resolveExecutable("rg")) {
14691
+ await removeBin(dest);
14692
+ return;
14693
+ }
14694
+ if (existsSync(dest)) return;
14695
+ const src = vscodeRipgrepPath();
14696
+ if (!src) return;
14697
+ const tmp = tempName(dest);
14698
+ try {
14699
+ await link(src, tmp);
14700
+ } catch {
14701
+ await copyFile(src, tmp);
14702
+ }
14703
+ if (process$1.platform !== "win32") await chmod(tmp, 493).catch(() => {});
14704
+ await commit(tmp, dest);
14705
+ }
14706
+ async function provisionTool(spec, binDir, skip) {
14707
+ const dest = path.join(binDir, spec.binBasename + EXE_EXT);
14708
+ const sidecar = `${dest}.sha256`;
14709
+ const asset = assetFor(spec);
14710
+ if (skip.has(spec.command) || !asset) {
14711
+ await removeTool(spec, binDir);
14712
+ return;
14713
+ }
14714
+ if (resolveExecutable(spec.command)) {
14715
+ await removeTool(spec, binDir);
14716
+ return;
14717
+ }
14718
+ if (existsSync(dest) && await sidecarMatches(sidecar, asset.sha256)) {
14719
+ await ensureAliases(spec, binDir, dest);
14720
+ return;
14721
+ }
14722
+ await atomicInstall(dest, await downloadAndExtract(spec, asset));
14723
+ await writeFile(sidecar, asset.sha256).catch(() => {});
14724
+ await ensureAliases(spec, binDir, dest);
14725
+ }
14726
+ async function downloadAndExtract(spec, asset) {
14727
+ const data = await download(asset.url);
14728
+ const digest = createHash("sha256").update(data).digest("hex");
14729
+ if (digest !== asset.sha256) throw new Error(`checksum mismatch for ${spec.command} (${asset.url}): expected ${asset.sha256}, got ${digest}`);
14730
+ if (asset.archive === "raw") return data;
14731
+ const member = asset.archive === "zip" ? extractZipMember(data, spec.binBasename) : extractTarGzMember(data, spec.binBasename);
14732
+ if (!member) throw new Error(`binary "${spec.binBasename}" not found in ${asset.url}`);
14733
+ return member;
14734
+ }
14735
+ async function download(url) {
14736
+ const controller = new AbortController();
14737
+ const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
14738
+ try {
14739
+ const res = await fetch(url, {
14740
+ signal: controller.signal,
14741
+ redirect: "follow",
14742
+ headers: { "User-Agent": "github-router-toolbelt" }
14743
+ });
14744
+ if (!res.ok) throw new Error(`download ${url}: HTTP ${res.status}`);
14745
+ const buf = Buffer.from(await res.arrayBuffer());
14746
+ if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`download ${url}: exceeds ${MAX_DOWNLOAD_BYTES} bytes`);
14747
+ return buf;
14748
+ } finally {
14749
+ clearTimeout(timer);
14750
+ }
14751
+ }
14752
+ /** Write to a unique temp then atomically rename into place. */
14753
+ async function atomicInstall(dest, bytes) {
14754
+ const tmp = tempName(dest);
14755
+ await writeFile(tmp, bytes);
14756
+ if (process$1.platform !== "win32") await chmod(tmp, 493).catch(() => {});
14757
+ await commit(tmp, dest);
14758
+ }
14759
+ /** Rename tmp→dest, handling Windows replace-existing / in-use locks. */
14760
+ async function commit(tmp, dest) {
14761
+ try {
14762
+ await rename(tmp, dest);
14763
+ } catch {
14764
+ try {
14765
+ await rm(dest, { force: true });
14766
+ await rename(tmp, dest);
14767
+ } catch (err) {
14768
+ await rm(tmp, { force: true }).catch(() => {});
14769
+ throw err;
14770
+ }
14771
+ }
14772
+ }
14773
+ async function ensureAliases(spec, binDir, dest) {
14774
+ for (const alias of spec.aliases ?? []) {
14775
+ const ap = path.join(binDir, alias + EXE_EXT);
14776
+ if (existsSync(ap)) continue;
14777
+ const tmp = tempName(ap);
14778
+ try {
14779
+ await copyFile(dest, tmp);
14780
+ if (process$1.platform !== "win32") await chmod(tmp, 493).catch(() => {});
14781
+ await commit(tmp, ap);
14782
+ } catch (err) {
14783
+ consola.debug(`toolbelt: alias ${alias} skipped:`, err);
14784
+ }
14785
+ }
14786
+ }
14787
+ async function removeTool(spec, binDir) {
14788
+ await removeBin(path.join(binDir, spec.binBasename + EXE_EXT));
14789
+ for (const alias of spec.aliases ?? []) await removeBin(path.join(binDir, alias + EXE_EXT));
14790
+ }
14791
+ async function removeBin(dest) {
14792
+ await rm(dest, { force: true }).catch(() => {});
14793
+ await rm(`${dest}.sha256`, { force: true }).catch(() => {});
14794
+ }
14795
+ async function sidecarMatches(sidecar, sha256) {
14796
+ try {
14797
+ return (await readFile(sidecar, "utf8")).trim() === sha256;
14798
+ } catch {
14799
+ return false;
14800
+ }
14801
+ }
14802
+ function tempName(dest) {
14803
+ return `${dest}.${process$1.pid}.${randomBytes(4).toString("hex")}.tmp`;
14804
+ }
14805
+ /** The command names currently exposed in `bin/`. */
14806
+ async function exposedCommands(binDir) {
14807
+ let files;
14808
+ try {
14809
+ files = new Set(await readdir(binDir));
14810
+ } catch {
14811
+ return [];
14812
+ }
14813
+ const present = (base) => files.has(base + EXE_EXT);
14814
+ const out = [];
14815
+ if (present("rg")) out.push("rg");
14816
+ for (const spec of TOOLBELT_TOOLS) if (present(spec.binBasename)) out.push(spec.command);
14817
+ return out;
14818
+ }
13840
14819
 
13841
14820
  //#endregion
13842
14821
  //#region src/lib/proxy.ts
@@ -13887,7 +14866,7 @@ function initProxyFromEnv() {
13887
14866
  //#endregion
13888
14867
  //#region package.json
13889
14868
  var name = "github-router";
13890
- var version$1 = "0.3.68";
14869
+ var version$1 = "0.3.71";
13891
14870
 
13892
14871
  //#endregion
13893
14872
  //#region src/lib/approval.ts
@@ -15731,6 +16710,11 @@ const sharedServerArgs = {
15731
16710
  type: "boolean",
15732
16711
  default: false,
15733
16712
  description: "Force humanlike pacing on ALL browser tool dispatches: Beta-distributed inter-action delays (800-4600 ms), Bezier mouse trajectories with overshoot-and-correct, per-keystroke jitter with word-end pauses, scroll chunking. Use for known anti-bot sites (Cloudflare, Datadome). Off by default (auto mode); GH_ROUTER_HUMANLIKE=1 is the env equivalent. GH_ROUTER_BROWSER_NO_HUMANLIKE=1 hard-disables (wins over --humanlike, for tests)."
16713
+ },
16714
+ "self-update": {
16715
+ type: "boolean",
16716
+ default: true,
16717
+ description: "Update github-router itself to the latest npm version on launch (throttled once/hour). Best-effort and non-blocking: the proxy serves immediately and a detached updater applies the new version after this process exits (it takes effect on the NEXT launch; the running process keeps its current build). Disable with --no-self-update or GH_ROUTER_NO_SELF_UPDATE=1. Skipped silently if npm/network unavailable."
15734
16718
  }
15735
16719
  };
15736
16720
  const allowedAccountTypes = new Set([
@@ -15836,6 +16820,7 @@ function getClaudeCodeEnvVars(serverUrl, model) {
15836
16820
  "CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING",
15837
16821
  "CLAUDE_CODE_ENABLE_TASKS"
15838
16822
  ]) if (process.env[key] === void 0) vars[key] = "1";
16823
+ if (toolbeltEnabled()) Object.assign(vars, toolbeltPathOverride(process.env, PATHS.TOOLBELT_BIN_DIR));
15839
16824
  return vars;
15840
16825
  }
15841
16826
  /**
@@ -15851,11 +16836,13 @@ function getClaudeCodeEnvVars(serverUrl, model) {
15851
16836
  * masks any cached login.
15852
16837
  */
15853
16838
  function getCodexEnvVars(serverUrl) {
15854
- return {
16839
+ const vars = {
15855
16840
  OPENAI_BASE_URL: `${serverUrl}/v1`,
15856
16841
  OPENAI_API_KEY: "dummy",
15857
16842
  CODEX_HOME: PATHS.CODEX_HOME
15858
16843
  };
16844
+ if (toolbeltEnabled()) Object.assign(vars, toolbeltPathOverride(process.env, PATHS.TOOLBELT_BIN_DIR));
16845
+ return vars;
15859
16846
  }
15860
16847
 
15861
16848
  //#endregion
@@ -15895,7 +16882,7 @@ const claude = defineCommand({
15895
16882
  "auto-update": {
15896
16883
  type: "boolean",
15897
16884
  default: true,
15898
- description: "Check for and install latest Claude Code on launch (throttled to once per hour via ~/.local/share/github-router/last-update-check). Set to false (--no-auto-update) to keep the current installed version. Falls back gracefully if npm/network unavailable."
16885
+ description: "Check for and install the latest Claude Code on launch via `claude update` (throttled to once per hour via ~/.local/share/github-router/last-update-check). `claude update` respects the real install method (native installer or npm), so it never creates a conflicting second install; builds too old to support it fall back to `npm install -g @anthropic-ai/claude-code@latest`. Set to false (--no-auto-update) to check and warn only. Falls back gracefully if claude/npm/network unavailable."
15899
16886
  },
15900
16887
  "update-check": {
15901
16888
  type: "boolean",
@@ -15918,12 +16905,12 @@ const claude = defineCommand({
15918
16905
  if (versionCheck.skipped && versionCheck.skipReason === "no-claude") consola.debug("claude --version probe failed; skipping auto-update.");
15919
16906
  else if (versionCheck.skipped && versionCheck.skipReason === "no-npm") consola.debug("npm view @anthropic-ai/claude-code failed; skipping auto-update check (likely offline).");
15920
16907
  else if (versionCheck.needsUpdate && versionCheck.installedVersion && versionCheck.latestVersion) if (args["auto-update"] !== false) try {
15921
- await autoUpdateClaude(versionCheck.latestVersion);
16908
+ await updateClaude(versionCheck.latestVersion);
15922
16909
  } catch (err) {
15923
16910
  const msg = err instanceof Error ? err.message : String(err);
15924
- consola.warn(`Auto-update of Claude Code from ${versionCheck.installedVersion} to ${versionCheck.latestVersion} failed (${msg}); continuing with installed version. Run \`npm install -g @anthropic-ai/claude-code@latest\` manually to retry.`);
16911
+ consola.warn(`Auto-update of Claude Code from ${versionCheck.installedVersion} to ${versionCheck.latestVersion} failed (${msg}); continuing with installed version. Run \`claude update\` (or \`npm install -g @anthropic-ai/claude-code@latest\`) manually to retry.`);
15925
16912
  }
15926
- else consola.warn(`Claude Code v${versionCheck.installedVersion} is installed; v${versionCheck.latestVersion} is available. Run with --auto-update (the default) to install on launch, or \`npm install -g @anthropic-ai/claude-code@latest\` manually.`);
16913
+ else consola.warn(`Claude Code v${versionCheck.installedVersion} is installed; v${versionCheck.latestVersion} is available. Run with --auto-update (the default) to install on launch, or \`claude update\` manually.`);
15927
16914
  } catch (err) {
15928
16915
  consola.debug("Claude version check failed:", err);
15929
16916
  }
@@ -15941,6 +16928,7 @@ const claude = defineCommand({
15941
16928
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
15942
16929
  process$1.exit(1);
15943
16930
  }
16931
+ runSelfUpdate({ selfUpdate: args["self-update"] !== false });
15944
16932
  try {
15945
16933
  await ensureClaudeConfigMirror();
15946
16934
  } catch (err) {
@@ -15972,6 +16960,15 @@ const claude = defineCommand({
15972
16960
  process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code (${banner})...\n`);
15973
16961
  const envVars = getClaudeCodeEnvVars(serverUrl, chosenSlug);
15974
16962
  const extraArgs = args._ ?? [];
16963
+ if (toolbeltEnabled()) {
16964
+ provisionToolbelt().catch((err) => consola.debug("Toolbelt provisioning failed:", err));
16965
+ const toolbeltLine = buildToolbeltAwareness(availableToolCommands());
16966
+ if (toolbeltLine) try {
16967
+ await appendToolbeltAwarenessToMirroredClaudeMd(toolbeltLine);
16968
+ } catch (err) {
16969
+ consola.warn(`Toolbelt CLAUDE.md append failed: ${err instanceof Error ? err.message : String(err)}`);
16970
+ }
16971
+ }
15975
16972
  const baseShutdown = async () => {
15976
16973
  await removeOwnClaudeConfigMirror();
15977
16974
  };
@@ -16071,6 +17068,8 @@ const codex = defineCommand({
16071
17068
  consola.error("Failed to start server:", error instanceof Error ? error.message : error);
16072
17069
  process$1.exit(1);
16073
17070
  }
17071
+ runSelfUpdate({ selfUpdate: args["self-update"] !== false });
17072
+ if (toolbeltEnabled()) provisionToolbelt().catch(() => {});
16074
17073
  const usingDefault = !args.model;
16075
17074
  const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
16076
17075
  enableFileLogging();
@@ -16443,6 +17442,7 @@ const start = defineCommand({
16443
17442
  port: parsed.port ?? DEFAULT_PORT,
16444
17443
  silent: false
16445
17444
  });
17445
+ runSelfUpdate({ selfUpdate: args["self-update"] !== false });
16446
17446
  if (args.cc) generateClaudeCodeCommand(serverUrl, args.model);
16447
17447
  if (args.cx) generateCodexCommand(serverUrl, args.model);
16448
17448
  consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);