github-router 0.3.66 → 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/README.md +6 -5
- 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 +1154 -122
- 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,38 +1154,224 @@ 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`,
|
|
922
1195
|
"--silent"
|
|
923
|
-
], {
|
|
924
|
-
|
|
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",
|
|
1267
|
+
"--silent"
|
|
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
|
|
|
931
1344
|
//#endregion
|
|
932
1345
|
//#region src/lib/port.ts
|
|
933
1346
|
const DEFAULT_PORT = 8787;
|
|
934
|
-
const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
|
|
1347
|
+
const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
|
|
1348
|
+
"claude-opus-4-7",
|
|
1349
|
+
"claude-opus-4-6",
|
|
1350
|
+
"claude-opus-4-5"
|
|
1351
|
+
];
|
|
935
1352
|
/**
|
|
936
1353
|
* Cap-aware default picker for `ANTHROPIC_MODEL` on the implicit-default
|
|
937
1354
|
* path. Returns `claude-opus-${family}[1m]` when the live Copilot catalog
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
1355
|
+
* shows the family is 1M-capable, else the bare `claude-opus-${family}`
|
|
1356
|
+
* slug. `family` defaults to `"4.8"` so the no-arg call selects the
|
|
1357
|
+
* current default; explicit values like `"4.7"` or `"4.6"` are used to
|
|
1358
|
+
* honor the `github-router claude -m <version>` family shorthand.
|
|
1359
|
+
*
|
|
1360
|
+
* **Dual-signal 1M detection**. The Opus families have evolved different
|
|
1361
|
+
* shapes in Copilot's catalog over time:
|
|
1362
|
+
* 1. **Sibling-slug signal** — `opus-${family}-1m` (or `opus-${family}-1m-internal`)
|
|
1363
|
+
* exists as a separate catalog entry distinct from the base slug.
|
|
1364
|
+
* This is how 4.6 and 4.7 ship (`claude-opus-4.6-1m`,
|
|
1365
|
+
* `claude-opus-4.7-1m-internal`). Matched by the version-anchored
|
|
1366
|
+
* regex below.
|
|
1367
|
+
* 2. **Base-slug capability signal** — the catalog entry whose id IS
|
|
1368
|
+
* the base `opus-${family}` slug advertises
|
|
1369
|
+
* `capabilities.limits.max_context_window_tokens >= 1_000_000`. This
|
|
1370
|
+
* is how 4.8 ships — there is no `-1m` sibling; the single
|
|
1371
|
+
* `claude-opus-4.8` id is the 1M variant.
|
|
1372
|
+
* Either signal flips on the `[1m]` decoration. Both signals together
|
|
1373
|
+
* also flip it on (no double-counting). The breadcrumb log names which
|
|
1374
|
+
* signal fired so users can spot catalog shape changes.
|
|
943
1375
|
*
|
|
944
1376
|
* The `[1m]` literal-bracket suffix is Claude Code's local 1M-context
|
|
945
1377
|
* unlock — cc-backup `src/utils/context.ts:35-40` matches `/\[1m\]/i`
|
|
@@ -949,14 +1381,14 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = ["claude-opus-4-6", "claude-opus-4-5"];
|
|
|
949
1381
|
* proxy routes the underlying request.
|
|
950
1382
|
*
|
|
951
1383
|
* Cap-awareness matters because on non-enterprise Copilot tiers there
|
|
952
|
-
* is no
|
|
1384
|
+
* is no 1M opus backend; sending `[1m]` there would either 400 at
|
|
953
1385
|
* Copilot or (with `resolveModel`'s graceful-degrade) silently
|
|
954
1386
|
* downgrade upstream while Claude Code still over-accounts context.
|
|
955
1387
|
* This helper detects the catalog state at launch and only opts in
|
|
956
1388
|
* when the backend can actually serve 1M.
|
|
957
1389
|
*
|
|
958
1390
|
* Sonnet/Haiku families are intentionally NOT given `[1m]` defaults
|
|
959
|
-
* because Copilot has no
|
|
1391
|
+
* because Copilot has no 1M backend for them (and Anthropic-side
|
|
960
1392
|
* `modelSupports1M` doesn't list haiku at all). See
|
|
961
1393
|
* `src/lib/server-setup.ts:getClaudeCodeEnvVars` for the
|
|
962
1394
|
* `ANTHROPIC_DEFAULT_{SONNET,HAIKU,OPUS}_MODEL` tier defaults.
|
|
@@ -966,18 +1398,25 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = ["claude-opus-4-6", "claude-opus-4-5"];
|
|
|
966
1398
|
* can't tell the difference between "no catalog yet" and "no 1M
|
|
967
1399
|
* variant" — defaulting safe-side preserves the pre-change behavior).
|
|
968
1400
|
*/
|
|
969
|
-
const DEFAULT_OPUS_FAMILY = "4.
|
|
1401
|
+
const DEFAULT_OPUS_FAMILY = "4.8";
|
|
1402
|
+
const ONE_M_TOKENS = 1e6;
|
|
970
1403
|
function pickClaudeDefault(opusFamily = DEFAULT_OPUS_FAMILY) {
|
|
971
1404
|
const dotted = opusFamily.replace(/-/g, ".");
|
|
972
1405
|
const bareSlug = `claude-opus-${dotted.replace(/\./g, "-")}`;
|
|
973
1406
|
const versionPattern = dotted.replace(/\./g, "[.-]");
|
|
974
1407
|
const oneMRegex = new RegExp(`opus-${versionPattern}-1m(?:$|-)`, "i");
|
|
1408
|
+
const baseSlugRegex = new RegExp(`^claude-opus-${versionPattern}$`, "i");
|
|
975
1409
|
const familyRegex = new RegExp(`opus-${versionPattern}(?:$|[-.])`, "i");
|
|
976
1410
|
const models$1 = state.models?.data ?? [];
|
|
977
|
-
const
|
|
1411
|
+
const siblingOneM = models$1.some((m) => oneMRegex.test(m.id));
|
|
1412
|
+
const baseSlugMaxContext = models$1.reduce((max, m) => baseSlugRegex.test(m.id) ? Math.max(max, m.capabilities?.limits?.max_context_window_tokens ?? 0) : max, 0);
|
|
1413
|
+
const baseSlugOneM = baseSlugMaxContext >= ONE_M_TOKENS;
|
|
1414
|
+
const has1m = siblingOneM || baseSlugOneM;
|
|
978
1415
|
if (opusFamily !== DEFAULT_OPUS_FAMILY && state.models && models$1.length > 0 && !models$1.some((m) => familyRegex.test(m.id))) consola.warn(`Requested Opus family "${dotted}" not found in Copilot catalog; using "${bareSlug}" anyway (resolveModel may not find a backend for it).`);
|
|
979
1416
|
if (has1m) {
|
|
980
|
-
|
|
1417
|
+
const signal = siblingOneM ? baseSlugOneM ? "sibling-slug + base-slug 1M capability" : `sibling slug opus-${dotted}-1m` : `base slug ${bareSlug} (max_context_window_tokens=${baseSlugMaxContext})`;
|
|
1418
|
+
const pinHint = siblingOneM ? ` Pass --model ${bareSlug} to pin 200K.` : ` (No separate 200K variant of ${dotted} exists in the catalog — the bare slug IS the 1M backend.)`;
|
|
1419
|
+
consola.info(`Catalog signals opus-${dotted} is 1M-capable (${signal}); defaulting ANTHROPIC_MODEL to "${bareSlug}[1m]" so Claude Code accounts for 1M context locally. Set CLAUDE_CODE_DISABLE_1M_CONTEXT=1 to opt out (HIPAA).${pinHint}`);
|
|
981
1420
|
return `${bareSlug}[1m]`;
|
|
982
1421
|
}
|
|
983
1422
|
return bareSlug;
|
|
@@ -1013,6 +1452,47 @@ function envInt$1(key, fallback) {
|
|
|
1013
1452
|
const UPSTREAM_FETCH_TIMEOUT_MS = envInt$1("UPSTREAM_FETCH_TIMEOUT_MS", 0);
|
|
1014
1453
|
const UPSTREAM_INACTIVITY_TIMEOUT_MS = envInt$1("UPSTREAM_INACTIVITY_TIMEOUT_MS", 3e5);
|
|
1015
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
|
+
|
|
1016
1496
|
//#endregion
|
|
1017
1497
|
//#region src/lib/launch.ts
|
|
1018
1498
|
/**
|
|
@@ -1077,6 +1557,22 @@ function commandExists(name$1) {
|
|
|
1077
1557
|
}
|
|
1078
1558
|
}
|
|
1079
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
|
+
/**
|
|
1080
1576
|
* Provider-config flags (`-c model_providers.github_router=...`) that
|
|
1081
1577
|
* point Codex at our proxy. Extracted from `buildCodexCmd` so the new
|
|
1082
1578
|
* `codex mcp-server` MCP-config builder can reuse the exact same
|
|
@@ -1144,22 +1640,25 @@ function buildCodexCmd(target) {
|
|
|
1144
1640
|
return cmd;
|
|
1145
1641
|
}
|
|
1146
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;
|
|
1147
1650
|
return {
|
|
1148
|
-
cmd
|
|
1149
|
-
|
|
1150
|
-
"--dangerously-skip-permissions",
|
|
1151
|
-
...target.extraArgs
|
|
1152
|
-
] : buildCodexCmd(target),
|
|
1153
|
-
env: {
|
|
1651
|
+
cmd,
|
|
1652
|
+
env: collapsePathKeys({
|
|
1154
1653
|
...sanitizeParentEnv(process$1.env),
|
|
1155
1654
|
...target.envVars
|
|
1156
|
-
}
|
|
1655
|
+
})
|
|
1157
1656
|
};
|
|
1158
1657
|
}
|
|
1159
1658
|
function launchChild(target, server$1, options = {}) {
|
|
1160
1659
|
const { cmd, env } = buildLaunchCommand(target);
|
|
1161
1660
|
const executable = cmd[0];
|
|
1162
|
-
if (!
|
|
1661
|
+
if (!isExecutableAvailable(executable)) {
|
|
1163
1662
|
const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
|
|
1164
1663
|
consola.error(msg);
|
|
1165
1664
|
process$1.stderr.write(msg + "\n");
|
|
@@ -2473,33 +2972,6 @@ function round4(x) {
|
|
|
2473
2972
|
return Math.round(x * 1e4) / 1e4;
|
|
2474
2973
|
}
|
|
2475
2974
|
|
|
2476
|
-
//#endregion
|
|
2477
|
-
//#region src/lib/version.ts
|
|
2478
|
-
/**
|
|
2479
|
-
* Read this binary's published version from package.json at runtime.
|
|
2480
|
-
*
|
|
2481
|
-
* Done at runtime (not baked at build time) because release.yml builds
|
|
2482
|
-
* BEFORE `npm version patch` bumps the version — a build-time inline
|
|
2483
|
-
* would always ship the pre-bump value. The npm tarball ships package.json
|
|
2484
|
-
* alongside `dist/`, so a sibling-up lookup from import.meta.url resolves
|
|
2485
|
-
* cleanly in both dev (`src/lib/`) and bundled (`dist/`) layouts.
|
|
2486
|
-
*
|
|
2487
|
-
* Returns `"unknown"` if package.json can't be located or parsed —
|
|
2488
|
-
* never throws, so the CLI never fails to start over version reporting.
|
|
2489
|
-
*/
|
|
2490
|
-
function getPackageVersion() {
|
|
2491
|
-
try {
|
|
2492
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
2493
|
-
const candidates = [join(here, "..", "..", "package.json"), join(here, "..", "package.json")];
|
|
2494
|
-
for (const path$2 of candidates) try {
|
|
2495
|
-
const raw = readFileSync(path$2, "utf8");
|
|
2496
|
-
const parsed = JSON.parse(raw);
|
|
2497
|
-
if (typeof parsed.version === "string" && (parsed.name === "github-router" || parsed.name === "@animeshkundu/github-router")) return parsed.version;
|
|
2498
|
-
} catch {}
|
|
2499
|
-
} catch {}
|
|
2500
|
-
return "unknown";
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
2975
|
//#endregion
|
|
2504
2976
|
//#region src/lib/browser-mcp/browser-detect.ts
|
|
2505
2977
|
let cached;
|
|
@@ -3470,7 +3942,7 @@ function logAudit$1(record) {
|
|
|
3470
3942
|
try {
|
|
3471
3943
|
const fs$2 = await import("node:fs/promises");
|
|
3472
3944
|
const path$2 = await import("node:path");
|
|
3473
|
-
const { PATHS: PATHS$1 } = await import("./paths-
|
|
3945
|
+
const { PATHS: PATHS$1 } = await import("./paths-DhM3Yi80.js");
|
|
3474
3946
|
const dir = path$2.join(PATHS$1.APP_DIR, "browser-mcp");
|
|
3475
3947
|
await fs$2.mkdir(dir, { recursive: true });
|
|
3476
3948
|
const line = JSON.stringify({
|
|
@@ -8494,17 +8966,21 @@ function browserToolsEnabled() {
|
|
|
8494
8966
|
return hasSupportedBrowserInstalled();
|
|
8495
8967
|
}
|
|
8496
8968
|
/**
|
|
8497
|
-
* The 1M-context Opus variant (`claude-opus-4.
|
|
8498
|
-
*
|
|
8499
|
-
* opus_critic prefers it so it can take large artifacts in one shot
|
|
8969
|
+
* The 1M-context Opus 4.6 variant (`claude-opus-4.6-1m`, `max_prompt_tokens`
|
|
8970
|
+
* 936K). opus_critic prefers it so it can take large artifacts in one shot
|
|
8500
8971
|
* (the whole point of pairing it with gpt-5.5 as the big-window peers);
|
|
8501
|
-
* falls back to the 200K `claude-opus-4-
|
|
8502
|
-
*
|
|
8503
|
-
|
|
8504
|
-
|
|
8972
|
+
* falls back to the 200K `claude-opus-4-6` when the catalog doesn't carry
|
|
8973
|
+
* a 1M 4.6 slug. The regex is version-anchored to 4.6 AND requires a
|
|
8974
|
+
* `-1m` suffix boundary (not a permissive `.*1m`), so it does NOT
|
|
8975
|
+
* false-positive on `claude-opus-4.7-1m-internal` (stand_in's pinned
|
|
8976
|
+
* 4.7 row), `claude-opus-4.6-1max` (hypothetical), or `claude-opus-4.8`
|
|
8977
|
+
* (1M-without-sibling). Tolerates dotted (`opus-4.6-1m`) and dashed
|
|
8978
|
+
* (`opus-4-6-1m`) catalog separators.
|
|
8979
|
+
*/
|
|
8980
|
+
const OPUS_1M_RE = /opus-4[.-]6-1m(?:$|-)/i;
|
|
8505
8981
|
function resolveOpusCriticModel() {
|
|
8506
8982
|
const oneM = state.models?.data?.find((m) => OPUS_1M_RE.test(m.id));
|
|
8507
|
-
return oneM ? oneM.id : "claude-opus-4-
|
|
8983
|
+
return oneM ? oneM.id : "claude-opus-4-6";
|
|
8508
8984
|
}
|
|
8509
8985
|
function activePersonas() {
|
|
8510
8986
|
return PERSONAS_READ.filter((p) => !p.requiresGeminiCatalog || geminiAvailable()).map((p) => p.toolNameHttp === "opus_critic" ? {
|
|
@@ -8600,6 +9076,8 @@ function toolError(message) {
|
|
|
8600
9076
|
* gpt-5.3-codex high on ~600B = 16.0s → ~64s on 12KB
|
|
8601
9077
|
* claude-opus-4-7 medium (thinking=3000) on a trivial prompt = 22.5s
|
|
8602
9078
|
* but model self-paces budget → ~50s+ on a real ~6KB review
|
|
9079
|
+
* (still applicable to stand_in's 4.7 row; opus_critic now runs on
|
|
9080
|
+
* 4.6 with similar empirical shape)
|
|
8603
9081
|
*
|
|
8604
9082
|
* Returns `{tooLong: true, capBytes}` when the (persona, effort, briefBytes)
|
|
8605
9083
|
* tuple is empirically predicted to bust the 60s ceiling.
|
|
@@ -10194,6 +10672,239 @@ async function searchWeb(query, signal) {
|
|
|
10194
10672
|
}
|
|
10195
10673
|
}
|
|
10196
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
|
+
|
|
10197
10908
|
//#endregion
|
|
10198
10909
|
//#region src/lib/worker-agent/bash.ts
|
|
10199
10910
|
/**
|
|
@@ -10240,6 +10951,7 @@ function buildEnv() {
|
|
|
10240
10951
|
const v = process$1.env[key];
|
|
10241
10952
|
if (v !== void 0) env[key] = v;
|
|
10242
10953
|
}
|
|
10954
|
+
if (toolbeltEnabled()) Object.assign(env, toolbeltPathOverride(env, PATHS.TOOLBELT_BIN_DIR));
|
|
10243
10955
|
return env;
|
|
10244
10956
|
}
|
|
10245
10957
|
/**
|
|
@@ -12082,10 +12794,11 @@ function round2(n) {
|
|
|
12082
12794
|
* **xhigh on long-running personas works via SSE-streamed /mcp responses**
|
|
12083
12795
|
* (handler.ts:handleToolsCallSSE). Claude Code's MCP HTTP client honors
|
|
12084
12796
|
* `text/event-stream` responses without applying the ~60s per-tool-call
|
|
12085
|
-
* timer that previously broke xhigh on gpt-5.5 (~56s wall) and
|
|
12086
|
-
*
|
|
12087
|
-
*
|
|
12088
|
-
*
|
|
12797
|
+
* timer that previously broke xhigh on gpt-5.5 (~56s wall) and on
|
|
12798
|
+
* Anthropic Opus families (high+ thinking budgets). opus-critic itself
|
|
12799
|
+
* now runs on claude-opus-4-6 which doesn't advertise xhigh, so the
|
|
12800
|
+
* SSE long-tail concern there is moot; the SSE machinery still applies
|
|
12801
|
+
* to the other personas that do expose xhigh.
|
|
12089
12802
|
*/
|
|
12090
12803
|
const EFFORT_LEVELS = [
|
|
12091
12804
|
"low",
|
|
@@ -12263,9 +12976,9 @@ const PERSONAS_READ = Object.freeze([
|
|
|
12263
12976
|
{
|
|
12264
12977
|
agentName: "opus-critic",
|
|
12265
12978
|
toolNameHttp: "opus_critic",
|
|
12266
|
-
model: "claude-opus-4-
|
|
12979
|
+
model: "claude-opus-4-6",
|
|
12267
12980
|
endpoint: "/v1/messages",
|
|
12268
|
-
description: "Adversarial second opinion from a fresh-context Opus 4.
|
|
12981
|
+
description: "Adversarial second opinion from a fresh-context Opus 4.6 — same lab as the lead, limited blind-spot diversity vs cross-lab critics. On enterprise catalogs that carry Opus-4.6-1M it runs with a ≈936K-token input window; otherwise ≈168K. Pinned one minor behind the default Opus so the panel spans more of the version curve. Catches confabulation. Pass artifact verbatim.",
|
|
12269
12982
|
baseInstructions: OPUS_CRITIC_BASE,
|
|
12270
12983
|
agentPrompt: "",
|
|
12271
12984
|
writeCapable: false,
|
|
@@ -12273,10 +12986,9 @@ const PERSONAS_READ = Object.freeze([
|
|
|
12273
12986
|
allowedEfforts: [
|
|
12274
12987
|
"low",
|
|
12275
12988
|
"medium",
|
|
12276
|
-
"high"
|
|
12277
|
-
"xhigh"
|
|
12989
|
+
"high"
|
|
12278
12990
|
],
|
|
12279
|
-
defaultEffort: "
|
|
12991
|
+
defaultEffort: "high"
|
|
12280
12992
|
}
|
|
12281
12993
|
]);
|
|
12282
12994
|
const PERSONAS_WRITE = Object.freeze([{
|
|
@@ -13459,6 +14171,8 @@ const PEER_MARKER_OPEN = "<!-- gh-router peer-mcp awareness — auto-injected, r
|
|
|
13459
14171
|
const PEER_MARKER_CLOSE = "<!-- /gh-router peer-mcp awareness -->";
|
|
13460
14172
|
const STYLE_MARKER_OPEN = "<!-- gh-router style directive — auto-injected, regenerated per launch -->";
|
|
13461
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 -->";
|
|
13462
14176
|
/**
|
|
13463
14177
|
* Writing / communication style directive injected at the TOP of the
|
|
13464
14178
|
* mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
|
|
@@ -13805,6 +14519,303 @@ async function prependStyleDirectiveToMirroredClaudeMd(directive = STYLE_DIRECTI
|
|
|
13805
14519
|
label: "style-directive"
|
|
13806
14520
|
});
|
|
13807
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
|
+
}
|
|
13808
14819
|
|
|
13809
14820
|
//#endregion
|
|
13810
14821
|
//#region src/lib/proxy.ts
|
|
@@ -13855,7 +14866,7 @@ function initProxyFromEnv() {
|
|
|
13855
14866
|
//#endregion
|
|
13856
14867
|
//#region package.json
|
|
13857
14868
|
var name = "github-router";
|
|
13858
|
-
var version$1 = "0.3.
|
|
14869
|
+
var version$1 = "0.3.71";
|
|
13859
14870
|
|
|
13860
14871
|
//#endregion
|
|
13861
14872
|
//#region src/lib/approval.ts
|
|
@@ -15699,6 +16710,11 @@ const sharedServerArgs = {
|
|
|
15699
16710
|
type: "boolean",
|
|
15700
16711
|
default: false,
|
|
15701
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."
|
|
15702
16718
|
}
|
|
15703
16719
|
};
|
|
15704
16720
|
const allowedAccountTypes = new Set([
|
|
@@ -15792,10 +16808,10 @@ function getClaudeCodeEnvVars(serverUrl, model) {
|
|
|
15792
16808
|
DISABLE_TELEMETRY: "1"
|
|
15793
16809
|
};
|
|
15794
16810
|
if (model) vars.ANTHROPIC_MODEL = model;
|
|
15795
|
-
if (process.env.ANTHROPIC_SMALL_FAST_MODEL === void 0) vars.ANTHROPIC_SMALL_FAST_MODEL = "claude-
|
|
16811
|
+
if (process.env.ANTHROPIC_SMALL_FAST_MODEL === void 0) vars.ANTHROPIC_SMALL_FAST_MODEL = "claude-sonnet-4-6";
|
|
15796
16812
|
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL === void 0) vars.ANTHROPIC_DEFAULT_SONNET_MODEL = "claude-sonnet-4-6";
|
|
15797
16813
|
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL === void 0) vars.ANTHROPIC_DEFAULT_HAIKU_MODEL = "claude-haiku-4-5";
|
|
15798
|
-
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL === void 0) vars.ANTHROPIC_DEFAULT_OPUS_MODEL = "claude-opus-4-
|
|
16814
|
+
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL === void 0) vars.ANTHROPIC_DEFAULT_OPUS_MODEL = "claude-opus-4-8";
|
|
15799
16815
|
if (process.env.CLAUDE_CODE_PLAN_V2_AGENT_COUNT === void 0) vars.CLAUDE_CODE_PLAN_V2_AGENT_COUNT = "7";
|
|
15800
16816
|
for (const key of [
|
|
15801
16817
|
"CLAUDE_CODE_ENABLE_EXPERIMENTAL_ADVISOR_TOOL",
|
|
@@ -15804,6 +16820,7 @@ function getClaudeCodeEnvVars(serverUrl, model) {
|
|
|
15804
16820
|
"CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING",
|
|
15805
16821
|
"CLAUDE_CODE_ENABLE_TASKS"
|
|
15806
16822
|
]) if (process.env[key] === void 0) vars[key] = "1";
|
|
16823
|
+
if (toolbeltEnabled()) Object.assign(vars, toolbeltPathOverride(process.env, PATHS.TOOLBELT_BIN_DIR));
|
|
15807
16824
|
return vars;
|
|
15808
16825
|
}
|
|
15809
16826
|
/**
|
|
@@ -15819,11 +16836,13 @@ function getClaudeCodeEnvVars(serverUrl, model) {
|
|
|
15819
16836
|
* masks any cached login.
|
|
15820
16837
|
*/
|
|
15821
16838
|
function getCodexEnvVars(serverUrl) {
|
|
15822
|
-
|
|
16839
|
+
const vars = {
|
|
15823
16840
|
OPENAI_BASE_URL: `${serverUrl}/v1`,
|
|
15824
16841
|
OPENAI_API_KEY: "dummy",
|
|
15825
16842
|
CODEX_HOME: PATHS.CODEX_HOME
|
|
15826
16843
|
};
|
|
16844
|
+
if (toolbeltEnabled()) Object.assign(vars, toolbeltPathOverride(process.env, PATHS.TOOLBELT_BIN_DIR));
|
|
16845
|
+
return vars;
|
|
15827
16846
|
}
|
|
15828
16847
|
|
|
15829
16848
|
//#endregion
|
|
@@ -15863,7 +16882,7 @@ const claude = defineCommand({
|
|
|
15863
16882
|
"auto-update": {
|
|
15864
16883
|
type: "boolean",
|
|
15865
16884
|
default: true,
|
|
15866
|
-
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."
|
|
15867
16886
|
},
|
|
15868
16887
|
"update-check": {
|
|
15869
16888
|
type: "boolean",
|
|
@@ -15886,12 +16905,12 @@ const claude = defineCommand({
|
|
|
15886
16905
|
if (versionCheck.skipped && versionCheck.skipReason === "no-claude") consola.debug("claude --version probe failed; skipping auto-update.");
|
|
15887
16906
|
else if (versionCheck.skipped && versionCheck.skipReason === "no-npm") consola.debug("npm view @anthropic-ai/claude-code failed; skipping auto-update check (likely offline).");
|
|
15888
16907
|
else if (versionCheck.needsUpdate && versionCheck.installedVersion && versionCheck.latestVersion) if (args["auto-update"] !== false) try {
|
|
15889
|
-
await
|
|
16908
|
+
await updateClaude(versionCheck.latestVersion);
|
|
15890
16909
|
} catch (err) {
|
|
15891
16910
|
const msg = err instanceof Error ? err.message : String(err);
|
|
15892
|
-
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.`);
|
|
15893
16912
|
}
|
|
15894
|
-
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.`);
|
|
15895
16914
|
} catch (err) {
|
|
15896
16915
|
consola.debug("Claude version check failed:", err);
|
|
15897
16916
|
}
|
|
@@ -15909,6 +16928,7 @@ const claude = defineCommand({
|
|
|
15909
16928
|
consola.error("Failed to start server:", error instanceof Error ? error.message : error);
|
|
15910
16929
|
process$1.exit(1);
|
|
15911
16930
|
}
|
|
16931
|
+
runSelfUpdate({ selfUpdate: args["self-update"] !== false });
|
|
15912
16932
|
try {
|
|
15913
16933
|
await ensureClaudeConfigMirror();
|
|
15914
16934
|
} catch (err) {
|
|
@@ -15940,6 +16960,15 @@ const claude = defineCommand({
|
|
|
15940
16960
|
process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code (${banner})...\n`);
|
|
15941
16961
|
const envVars = getClaudeCodeEnvVars(serverUrl, chosenSlug);
|
|
15942
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
|
+
}
|
|
15943
16972
|
const baseShutdown = async () => {
|
|
15944
16973
|
await removeOwnClaudeConfigMirror();
|
|
15945
16974
|
};
|
|
@@ -16039,6 +17068,8 @@ const codex = defineCommand({
|
|
|
16039
17068
|
consola.error("Failed to start server:", error instanceof Error ? error.message : error);
|
|
16040
17069
|
process$1.exit(1);
|
|
16041
17070
|
}
|
|
17071
|
+
runSelfUpdate({ selfUpdate: args["self-update"] !== false });
|
|
17072
|
+
if (toolbeltEnabled()) provisionToolbelt().catch(() => {});
|
|
16042
17073
|
const usingDefault = !args.model;
|
|
16043
17074
|
const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
|
|
16044
17075
|
enableFileLogging();
|
|
@@ -16411,6 +17442,7 @@ const start = defineCommand({
|
|
|
16411
17442
|
port: parsed.port ?? DEFAULT_PORT,
|
|
16412
17443
|
silent: false
|
|
16413
17444
|
});
|
|
17445
|
+
runSelfUpdate({ selfUpdate: args["self-update"] !== false });
|
|
16414
17446
|
if (args.cc) generateClaudeCodeCommand(serverUrl, args.model);
|
|
16415
17447
|
if (args.cx) generateCodexCommand(serverUrl, args.model);
|
|
16416
17448
|
consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
|