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/browser-ext/manifest.json +1 -1
- package/dist/{lifecycle-pWZ9tKxf.js → lifecycle-C2kZwv-z.js} +2 -2
- package/dist/{lifecycle-hkBEjHb2.js → lifecycle-NQRdfY1u.js} +2 -2
- package/dist/{lifecycle-hkBEjHb2.js.map → lifecycle-NQRdfY1u.js.map} +1 -1
- package/dist/main.js +1092 -92
- package/dist/main.js.map +1 -1
- package/dist/{paths-CZvFif-e.js → paths-CoFnpNZl.js} +5 -2
- package/dist/paths-CoFnpNZl.js.map +1 -0
- package/dist/{paths-CW16Dz9_.js → paths-DhM3Yi80.js} +1 -1
- package/package.json +1 -1
- package/dist/paths-CZvFif-e.js.map +0 -1
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-
|
|
3
|
-
import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-
|
|
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
|
|
763
|
-
const
|
|
764
|
-
const
|
|
765
|
-
const
|
|
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
|
-
|
|
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
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
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
|
-
], {
|
|
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
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
], {
|
|
924
|
-
|
|
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
|
-
|
|
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
|
|
1175
|
-
|
|
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 (!
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 \`
|
|
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`);
|