github-router 0.3.117 → 0.3.118
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-BHG64Dhh.js → lifecycle-9Cjezlrh.js} +2 -2
- package/dist/{lifecycle-DzJicg68.js → lifecycle-CELOx6yB.js} +64 -4
- package/dist/lifecycle-CELOx6yB.js.map +1 -0
- package/dist/{lifecycle-D6zt0iH_.js → lifecycle-CfYzpXK-.js} +2 -2
- package/dist/{lifecycle-D6zt0iH_.js.map → lifecycle-CfYzpXK-.js.map} +1 -1
- package/dist/{lifecycle-YCqABCX4.js → lifecycle-DIlhE377.js} +2 -2
- package/dist/main.js +307 -19
- package/dist/main.js.map +1 -1
- package/dist/{paths-CDWhYOdp.js → paths-BJvMAFht.js} +3 -3
- package/dist/{paths-CDWhYOdp.js.map → paths-BJvMAFht.js.map} +1 -1
- package/dist/{paths-DNVIKCZP.js → paths-BdQSPUOg.js} +1 -1
- package/package.json +1 -1
- package/dist/lifecycle-DzJicg68.js.map +0 -1
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { c as
|
|
4
|
-
import { a as sweepRegistry, i as registerExitHandlers$1, 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-BJvMAFht.js";
|
|
3
|
+
import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, n as isPidAlive, o as trackChild, p as runManagedExeCapture, r as registerColbertExitHandlers, s as killChildProcessTree, t as getColbertInstanceUuid, u as resolveExecutable } from "./lifecycle-CELOx6yB.js";
|
|
4
|
+
import { a as sweepRegistry, i as registerExitHandlers$1, n as getInstanceUuid, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-CfYzpXK-.js";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { defineCommand, runMain } from "citty";
|
|
7
7
|
import consola from "consola";
|
|
@@ -1415,6 +1415,120 @@ function envInt$1(key, fallback) {
|
|
|
1415
1415
|
const UPSTREAM_FETCH_TIMEOUT_MS = envInt$1("UPSTREAM_FETCH_TIMEOUT_MS", 0);
|
|
1416
1416
|
const UPSTREAM_INACTIVITY_TIMEOUT_MS = envInt$1("UPSTREAM_INACTIVITY_TIMEOUT_MS", 3e5);
|
|
1417
1417
|
|
|
1418
|
+
//#endregion
|
|
1419
|
+
//#region src/lib/process-guard/index.ts
|
|
1420
|
+
/**
|
|
1421
|
+
* Live reaper children, held so the GC can't collect the `ChildProcess`
|
|
1422
|
+
* (and with it the stdin write-end that is the proxy-death signal) while
|
|
1423
|
+
* the proxy runs. Entries self-remove on the reaper's exit. We do NOT
|
|
1424
|
+
* proactively kill these on graceful shutdown: when the proxy exits the OS
|
|
1425
|
+
* closes the stdin pipe, the reaper hits EOF, re-verifies the child's
|
|
1426
|
+
* identity, and reaps any survivor that ignored the graceful SIGTERM —
|
|
1427
|
+
* exactly the backstop we want.
|
|
1428
|
+
*/
|
|
1429
|
+
const _activeReapers = /* @__PURE__ */ new Set();
|
|
1430
|
+
/** Guard is on by default; `GH_ROUTER_DISABLE_PROCESS_GUARD=1` opts out. */
|
|
1431
|
+
function processGuardEnabled() {
|
|
1432
|
+
return parseBoolEnv(process$1.env.GH_ROUTER_DISABLE_PROCESS_GUARD) !== true;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Build the start-time-verified node reaper script (POSIX). PURE — `pid`
|
|
1436
|
+
* is our own integer. `detachedGroup` selects `kill(-pid)` (process group,
|
|
1437
|
+
* the detached CLI) vs `kill(pid)` on POSIX.
|
|
1438
|
+
*
|
|
1439
|
+
* Kept dependency-free (require'd builtins only) so it runs under `node -e`
|
|
1440
|
+
* with no module resolution against the dist bundle.
|
|
1441
|
+
*/
|
|
1442
|
+
function buildNodeReaperScript(pid, detachedGroup) {
|
|
1443
|
+
const target = detachedGroup ? "-PID" : "PID";
|
|
1444
|
+
return `
|
|
1445
|
+
const cp = require("node:child_process");
|
|
1446
|
+
const fs = require("node:fs");
|
|
1447
|
+
const PID = ${pid >>> 0};
|
|
1448
|
+
function startTime() {
|
|
1449
|
+
try {
|
|
1450
|
+
if (process.platform === "linux") {
|
|
1451
|
+
const stat = fs.readFileSync("/proc/" + PID + "/stat", "utf8");
|
|
1452
|
+
const post = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\\s+/);
|
|
1453
|
+
return post[19] || null; // field 22 (starttime) = index 19 after comm
|
|
1454
|
+
}
|
|
1455
|
+
const out = cp.execFileSync("ps", ["-o","lstart=","-p",String(PID)],
|
|
1456
|
+
{ stdio: ["ignore","pipe","ignore"], timeout: 2000 }).toString().trim();
|
|
1457
|
+
return out || null;
|
|
1458
|
+
} catch { return null; }
|
|
1459
|
+
}
|
|
1460
|
+
function alive() { try { process.kill(PID, 0); return true; } catch { return false; } }
|
|
1461
|
+
function treeKill() {
|
|
1462
|
+
try { process.kill(${target}, "SIGTERM"); } catch {}
|
|
1463
|
+
// NOT unref'd: this must keep the loop alive to deliver the escalated
|
|
1464
|
+
// SIGKILL (the onDeath exit timer below stays alive past 500ms too).
|
|
1465
|
+
setTimeout(() => { try { process.kill(${target}, "SIGKILL"); } catch {} }, 500);
|
|
1466
|
+
}
|
|
1467
|
+
const snap = (() => {
|
|
1468
|
+
// The child is definitely ours and alive at startup, so a null here is a
|
|
1469
|
+
// transient probe failure (e.g. a momentary 'ps' hiccup), NOT a real
|
|
1470
|
+
// identity loss. Retry a few times so a one-off failure can't silently
|
|
1471
|
+
// disable the guard for the rest of the run.
|
|
1472
|
+
for (let i = 0; i < 3; i++) { const s = startTime(); if (s !== null) return s; }
|
|
1473
|
+
return null;
|
|
1474
|
+
})();
|
|
1475
|
+
let done = false;
|
|
1476
|
+
function onDeath() {
|
|
1477
|
+
if (done) return; done = true;
|
|
1478
|
+
// Re-verify identity: kill ONLY if the live PID is still our child.
|
|
1479
|
+
if (snap !== null && alive() && startTime() === snap) treeKill();
|
|
1480
|
+
// REF'd (NOT unref'd): stdin has closed, so an unref'd timer would let
|
|
1481
|
+
// the loop empty and the process exit immediately — dropping the 500ms
|
|
1482
|
+
// SIGKILL escalation. Keep the loop alive to deliver it, then exit.
|
|
1483
|
+
setTimeout(() => process.exit(0), 1500);
|
|
1484
|
+
}
|
|
1485
|
+
process.stdin.resume();
|
|
1486
|
+
process.stdin.on("end", onDeath);
|
|
1487
|
+
process.stdin.on("close", onDeath);
|
|
1488
|
+
process.stdin.on("error", onDeath);
|
|
1489
|
+
const cap = setTimeout(() => process.exit(0), 24*3600*1000); if (cap.unref) cap.unref();
|
|
1490
|
+
`.trim();
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Spawn the detached node reaper holding the stdin death-pipe. Always
|
|
1494
|
+
* `detached` so it outlives a force-kill of the proxy, and `unref`'d so it
|
|
1495
|
+
* never holds the proxy's event loop open. Returns null on spawn failure.
|
|
1496
|
+
*/
|
|
1497
|
+
function spawnNodeReaper(pid, detachedGroup) {
|
|
1498
|
+
try {
|
|
1499
|
+
const child = spawn(process$1.execPath, ["-e", buildNodeReaperScript(pid, detachedGroup)], {
|
|
1500
|
+
stdio: [
|
|
1501
|
+
"pipe",
|
|
1502
|
+
"ignore",
|
|
1503
|
+
"ignore"
|
|
1504
|
+
],
|
|
1505
|
+
detached: true,
|
|
1506
|
+
windowsHide: true,
|
|
1507
|
+
shell: false
|
|
1508
|
+
});
|
|
1509
|
+
child.on("error", () => {});
|
|
1510
|
+
child.stdin?.on("error", () => {});
|
|
1511
|
+
_activeReapers.add(child);
|
|
1512
|
+
child.once("exit", () => _activeReapers.delete(child));
|
|
1513
|
+
child.unref();
|
|
1514
|
+
return child;
|
|
1515
|
+
} catch {
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Start the crash-safe guard for a launched CLI child. No-op on Windows
|
|
1521
|
+
* (Node's Job Object already reaps the tree on proxy death) and when
|
|
1522
|
+
* disabled / unspawnable. Fire-and-forget; never throws.
|
|
1523
|
+
*/
|
|
1524
|
+
function startProcessGuard(child) {
|
|
1525
|
+
if (!processGuardEnabled()) return;
|
|
1526
|
+
const pid = child.pid;
|
|
1527
|
+
if (!pid) return;
|
|
1528
|
+
if (process$1.platform === "win32") return;
|
|
1529
|
+
spawnNodeReaper(pid, true);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1418
1532
|
//#endregion
|
|
1419
1533
|
//#region src/lib/toolbelt/path-inject.ts
|
|
1420
1534
|
/**
|
|
@@ -1498,7 +1612,8 @@ const STRIPPED_PARENT_ENV_KEYS = [
|
|
|
1498
1612
|
"CLAUDE_CODE_ADDITIONAL_PROTECTION",
|
|
1499
1613
|
"OPENAI_API_KEY",
|
|
1500
1614
|
"OPENAI_BASE_URL",
|
|
1501
|
-
"CODEX_HOME"
|
|
1615
|
+
"CODEX_HOME",
|
|
1616
|
+
"AIORDIE_CLAUDE_BIND"
|
|
1502
1617
|
];
|
|
1503
1618
|
/**
|
|
1504
1619
|
* Strip auth-related keys from a parent-process env object. The result
|
|
@@ -1618,6 +1733,18 @@ function buildLaunchCommand(target) {
|
|
|
1618
1733
|
})
|
|
1619
1734
|
};
|
|
1620
1735
|
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Whether a resolved Windows executable must be launched through cmd.exe
|
|
1738
|
+
* (`shell:true`). Only batch shims (`.cmd`/`.bat`) need it — and even then
|
|
1739
|
+
* cmd.exe stays alive as the CLI's parent, so `taskkill /T` reaps the
|
|
1740
|
+
* whole tree. A real `.exe` (e.g. the native-installer `claude.exe`) is
|
|
1741
|
+
* spawned DIRECTLY so the CLI is the direct child, with no cmd.exe
|
|
1742
|
+
* intermediary to orphan its node/MCP grandchildren on a kill.
|
|
1743
|
+
*/
|
|
1744
|
+
function windowsLaunchNeedsShell(executable) {
|
|
1745
|
+
const ext = nodePath.extname(executable).toLowerCase();
|
|
1746
|
+
return ext === ".cmd" || ext === ".bat";
|
|
1747
|
+
}
|
|
1621
1748
|
function launchChild(target, server$1, options = {}) {
|
|
1622
1749
|
const { cmd, env } = buildLaunchCommand(target);
|
|
1623
1750
|
const executable = cmd[0];
|
|
@@ -1629,7 +1756,7 @@ function launchChild(target, server$1, options = {}) {
|
|
|
1629
1756
|
}
|
|
1630
1757
|
let child;
|
|
1631
1758
|
try {
|
|
1632
|
-
if (process$1.platform === "win32") child = spawn(cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" "), [], {
|
|
1759
|
+
if (process$1.platform === "win32") if (windowsLaunchNeedsShell(cmd[0])) child = spawn(cmd.map((a) => a.includes(" ") ? `"${a}"` : a).join(" "), [], {
|
|
1633
1760
|
env,
|
|
1634
1761
|
stdio: "inherit",
|
|
1635
1762
|
shell: true
|
|
@@ -1638,6 +1765,11 @@ function launchChild(target, server$1, options = {}) {
|
|
|
1638
1765
|
env,
|
|
1639
1766
|
stdio: "inherit"
|
|
1640
1767
|
});
|
|
1768
|
+
else child = spawn(cmd[0], cmd.slice(1), {
|
|
1769
|
+
env,
|
|
1770
|
+
stdio: "inherit",
|
|
1771
|
+
detached: true
|
|
1772
|
+
});
|
|
1641
1773
|
} catch (error) {
|
|
1642
1774
|
const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1643
1775
|
consola.error(msg);
|
|
@@ -1646,15 +1778,16 @@ function launchChild(target, server$1, options = {}) {
|
|
|
1646
1778
|
if (options.onShutdown) Promise.resolve(options.onShutdown()).catch(() => {});
|
|
1647
1779
|
process$1.exit(1);
|
|
1648
1780
|
}
|
|
1781
|
+
startProcessGuard(child);
|
|
1649
1782
|
let cleaned = false;
|
|
1650
1783
|
let exiting = false;
|
|
1651
1784
|
async function cleanup() {
|
|
1652
1785
|
if (cleaned) return;
|
|
1653
1786
|
cleaned = true;
|
|
1787
|
+
const timeout = setTimeout(() => process$1.exit(1), 5e3);
|
|
1654
1788
|
try {
|
|
1655
|
-
child.
|
|
1789
|
+
killChildProcessTree(child, { detachedGroup: process$1.platform !== "win32" });
|
|
1656
1790
|
} catch {}
|
|
1657
|
-
const timeout = setTimeout(() => process$1.exit(1), 5e3);
|
|
1658
1791
|
try {
|
|
1659
1792
|
await server$1.close(true);
|
|
1660
1793
|
} catch {}
|
|
@@ -1668,11 +1801,33 @@ function launchChild(target, server$1, options = {}) {
|
|
|
1668
1801
|
exiting = true;
|
|
1669
1802
|
process$1.exit(code);
|
|
1670
1803
|
}
|
|
1671
|
-
|
|
1804
|
+
let forwardGrace = null;
|
|
1805
|
+
const lastForwardAt = {
|
|
1806
|
+
SIGINT: 0,
|
|
1807
|
+
SIGTERM: 0
|
|
1808
|
+
};
|
|
1809
|
+
const onSignal = (sig) => {
|
|
1810
|
+
if (process$1.platform !== "win32" && child.pid && !cleaned) {
|
|
1811
|
+
const now = Date.now();
|
|
1812
|
+
if (now - lastForwardAt[sig] > 250) {
|
|
1813
|
+
lastForwardAt[sig] = now;
|
|
1814
|
+
try {
|
|
1815
|
+
process$1.kill(-child.pid, sig);
|
|
1816
|
+
} catch {}
|
|
1817
|
+
}
|
|
1818
|
+
const graceMs = sig === "SIGINT" ? 1e4 : 3e3;
|
|
1819
|
+
if (!forwardGrace) {
|
|
1820
|
+
forwardGrace = setTimeout(() => {
|
|
1821
|
+
cleanup().then(() => exit(130)).catch(() => exit(1));
|
|
1822
|
+
}, graceMs);
|
|
1823
|
+
forwardGrace.unref?.();
|
|
1824
|
+
}
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1672
1827
|
cleanup().then(() => exit(130)).catch(() => exit(1));
|
|
1673
1828
|
};
|
|
1674
|
-
process$1.on("SIGINT", onSignal);
|
|
1675
|
-
process$1.on("SIGTERM", onSignal);
|
|
1829
|
+
process$1.on("SIGINT", () => onSignal("SIGINT"));
|
|
1830
|
+
process$1.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
1676
1831
|
child.on("exit", (exitCode, signal) => {
|
|
1677
1832
|
try {
|
|
1678
1833
|
sweepRegistry();
|
|
@@ -7103,7 +7258,7 @@ function logAudit$1(record) {
|
|
|
7103
7258
|
try {
|
|
7104
7259
|
const fs$2 = await import("node:fs/promises");
|
|
7105
7260
|
const path$1 = await import("node:path");
|
|
7106
|
-
const { PATHS: PATHS$1 } = await import("./paths-
|
|
7261
|
+
const { PATHS: PATHS$1 } = await import("./paths-BdQSPUOg.js");
|
|
7107
7262
|
const dir = path$1.join(PATHS$1.APP_DIR, "browser-mcp");
|
|
7108
7263
|
await fs$2.mkdir(dir, { recursive: true });
|
|
7109
7264
|
const line = JSON.stringify({
|
|
@@ -9900,9 +10055,15 @@ const exitHandler = () => {
|
|
|
9900
10055
|
inFlight$1.clear();
|
|
9901
10056
|
lastUsedSeq.clear();
|
|
9902
10057
|
};
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
10058
|
+
let _exitHandlersRegistered = false;
|
|
10059
|
+
function registerExitHandlers$2() {
|
|
10060
|
+
if (_exitHandlersRegistered) return;
|
|
10061
|
+
_exitHandlersRegistered = true;
|
|
10062
|
+
process$1.on("SIGINT", sigintHandler);
|
|
10063
|
+
process$1.on("SIGTERM", sigtermHandler);
|
|
10064
|
+
process$1.on("exit", exitHandler);
|
|
10065
|
+
}
|
|
10066
|
+
registerExitHandlers$2();
|
|
9906
10067
|
|
|
9907
10068
|
//#endregion
|
|
9908
10069
|
//#region src/vendor/pi/ai/api-registry.ts
|
|
@@ -19437,6 +19598,18 @@ function buildStopHookCommand(execPath, scriptPath) {
|
|
|
19437
19598
|
return `${q(execPath)} internal-stop-hook`;
|
|
19438
19599
|
}
|
|
19439
19600
|
/**
|
|
19601
|
+
* Build the shell command Claude Code runs for the SessionStart/SessionEnd hooks
|
|
19602
|
+
* (registered only when github-router runs inside an ai-or-die Terminal tab). The
|
|
19603
|
+
* sidecar path is baked in as a literal `--out` arg — NOT passed via env — so it
|
|
19604
|
+
* survives `AIORDIE_CLAUDE_BIND` being stripped from the child's environment and a
|
|
19605
|
+
* nested `github-router claude` can't inherit it. Pure (binary + script + out
|
|
19606
|
+
* paths) for unit-testable quoting; the live firing is verified end-to-end.
|
|
19607
|
+
*/
|
|
19608
|
+
function buildSessionBindHookCommand(execPath, scriptPath, outPath) {
|
|
19609
|
+
const q = (s) => `"${s}"`;
|
|
19610
|
+
return `${scriptPath && scriptPath !== execPath ? `${q(execPath)} ${q(scriptPath)}` : q(execPath)} internal-session-bind --out ${q(outPath)}`;
|
|
19611
|
+
}
|
|
19612
|
+
/**
|
|
19440
19613
|
* Read-merge-atomic-write the Stop hook into a Claude Code `settings.json` file
|
|
19441
19614
|
* (the mirrored one). A MISSING file (ENOENT) starts from `{}`; any OTHER read or
|
|
19442
19615
|
* parse error THROWS (the caller's try/catch warns and continues) rather than
|
|
@@ -23346,7 +23519,7 @@ function initProxyFromEnv() {
|
|
|
23346
23519
|
//#endregion
|
|
23347
23520
|
//#region package.json
|
|
23348
23521
|
var name = "github-router";
|
|
23349
|
-
var version$1 = "0.3.
|
|
23522
|
+
var version$1 = "0.3.118";
|
|
23350
23523
|
|
|
23351
23524
|
//#endregion
|
|
23352
23525
|
//#region src/lib/approval.ts
|
|
@@ -25532,6 +25705,15 @@ const claude = defineCommand({
|
|
|
25532
25705
|
if (skillsWritten > 0) process$1.stderr.write(`Floor-raising skills injected (${skillsWritten}/${INJECTED_SKILLS.length}): /gh-research, /gh-orchestrate, /gh-floor-keeper.
|
|
25533
25706
|
`);
|
|
25534
25707
|
}
|
|
25708
|
+
const aiordieSidecar = (process$1.env.AIORDIE_CLAUDE_BIND ?? "").trim();
|
|
25709
|
+
if (aiordieSidecar.length > 0 && !aiordieSidecar.includes("\"")) try {
|
|
25710
|
+
const settingsPath = nodePath.join(PATHS.CLAUDE_CONFIG_DIR, "settings.json");
|
|
25711
|
+
const command = buildSessionBindHookCommand(process$1.execPath, process$1.argv[1], aiordieSidecar);
|
|
25712
|
+
await injectStopHookIntoSettingsFile(settingsPath, command, "SessionStart");
|
|
25713
|
+
await injectStopHookIntoSettingsFile(settingsPath, command, "SessionEnd");
|
|
25714
|
+
} catch (err) {
|
|
25715
|
+
consola.warn(`Could not register the ai-or-die session-bind hook: ${String(err)}`);
|
|
25716
|
+
}
|
|
25535
25717
|
if (args["trust-gate"] === true) try {
|
|
25536
25718
|
const root = await trustRepo(sessionCwd);
|
|
25537
25719
|
process$1.stderr.write(`Structural gate trusted for this repo (${root}); it will run on launch here from now on.\n`);
|
|
@@ -25855,7 +26037,7 @@ async function postJson(url, payload, opts) {
|
|
|
25855
26037
|
* no such handle. Hooks always receive piped/redirected stdin, so this never
|
|
25856
26038
|
* blocks (guarded against an interactive TTY, and any error -> "").
|
|
25857
26039
|
*/
|
|
25858
|
-
function readStdin$
|
|
26040
|
+
function readStdin$2() {
|
|
25859
26041
|
try {
|
|
25860
26042
|
if (process.stdin.isTTY) return "";
|
|
25861
26043
|
return readFileSync(0, "utf8");
|
|
@@ -25886,7 +26068,7 @@ const internalPromptSubmit = defineCommand({
|
|
|
25886
26068
|
},
|
|
25887
26069
|
async run() {
|
|
25888
26070
|
try {
|
|
25889
|
-
const stdin = readStdin$
|
|
26071
|
+
const stdin = readStdin$2();
|
|
25890
26072
|
const steerEnabled = parseBoolEnv(process.env.GH_ROUTER_DISABLE_PROMPT_STEER) !== true;
|
|
25891
26073
|
const runtime = hookMcpRuntimeFromEnv();
|
|
25892
26074
|
let decision;
|
|
@@ -25938,6 +26120,111 @@ const internalPromptSubmit = defineCommand({
|
|
|
25938
26120
|
}
|
|
25939
26121
|
});
|
|
25940
26122
|
|
|
26123
|
+
//#endregion
|
|
26124
|
+
//#region src/internal-session-bind.ts
|
|
26125
|
+
/**
|
|
26126
|
+
* Read the hook payload from stdin SYNCHRONOUSLY (`readFileSync(0)`). An async
|
|
26127
|
+
* stdin read leaves an in-flight libuv FS request that, on Windows, races process
|
|
26128
|
+
* teardown and trips a `uv_async_send` assertion; a synchronous read has no such
|
|
26129
|
+
* handle. Hooks always receive piped stdin (guarded against a TTY; any error -> "").
|
|
26130
|
+
*/
|
|
26131
|
+
function readStdin$1() {
|
|
26132
|
+
try {
|
|
26133
|
+
if (process.stdin.isTTY) return "";
|
|
26134
|
+
return readFileSync(0, "utf8");
|
|
26135
|
+
} catch {
|
|
26136
|
+
return "";
|
|
26137
|
+
}
|
|
26138
|
+
}
|
|
26139
|
+
/**
|
|
26140
|
+
* Resolve a transcript path to its real location. The path claude reports sits
|
|
26141
|
+
* under the per-launch CLAUDE_CONFIG_DIR mirror, whose `projects` entry is a
|
|
26142
|
+
* junction/symlink back to the real `~/.claude/projects`. ai-or-die reads the
|
|
26143
|
+
* real dir, and the per-launch mirror is swept on github-router shutdown, so we
|
|
26144
|
+
* persist the REAL path. The file (and even intermediate dirs) may not exist yet
|
|
26145
|
+
* at SessionStart, so we realpath the DEEPEST EXISTING ancestor and rejoin the
|
|
26146
|
+
* not-yet-created trailing segments. Best-effort: if nothing resolves, keep raw.
|
|
26147
|
+
*/
|
|
26148
|
+
function realTranscriptPath(tp) {
|
|
26149
|
+
if (!tp) return "";
|
|
26150
|
+
try {
|
|
26151
|
+
return realpathSync.native(tp);
|
|
26152
|
+
} catch {}
|
|
26153
|
+
const missing = [];
|
|
26154
|
+
let cur = tp;
|
|
26155
|
+
for (let i = 0; i < 64; i++) {
|
|
26156
|
+
missing.unshift(nodePath.basename(cur));
|
|
26157
|
+
const parent = nodePath.dirname(cur);
|
|
26158
|
+
if (parent === cur) break;
|
|
26159
|
+
try {
|
|
26160
|
+
return nodePath.join(realpathSync.native(parent), ...missing);
|
|
26161
|
+
} catch {
|
|
26162
|
+
cur = parent;
|
|
26163
|
+
}
|
|
26164
|
+
}
|
|
26165
|
+
return tp;
|
|
26166
|
+
}
|
|
26167
|
+
/**
|
|
26168
|
+
* Pure core: turn a raw Claude Code hook payload (stdin string) into the sidecar
|
|
26169
|
+
* record, or `null` when there's nothing to write. Returns null for: non-JSON
|
|
26170
|
+
* input, a subagent/teammate payload (agent_id/agent_type present — top-level
|
|
26171
|
+
* filter), or a missing session_id. Exported for unit tests.
|
|
26172
|
+
*/
|
|
26173
|
+
function decodeSessionBind(stdin) {
|
|
26174
|
+
let payload = {};
|
|
26175
|
+
try {
|
|
26176
|
+
const p = JSON.parse(stdin);
|
|
26177
|
+
if (p && typeof p === "object") payload = p;
|
|
26178
|
+
else return null;
|
|
26179
|
+
} catch {
|
|
26180
|
+
return null;
|
|
26181
|
+
}
|
|
26182
|
+
if (isSubagentContext(payload)) return null;
|
|
26183
|
+
const claudeSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
26184
|
+
if (!claudeSessionId) return null;
|
|
26185
|
+
const event = (typeof payload.hook_event_name === "string" ? payload.hook_event_name : "") === "SessionEnd" ? "end" : "start";
|
|
26186
|
+
const record = {
|
|
26187
|
+
schema: 1,
|
|
26188
|
+
claudeSessionId,
|
|
26189
|
+
transcriptPath: realTranscriptPath(typeof payload.transcript_path === "string" ? payload.transcript_path : ""),
|
|
26190
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : "",
|
|
26191
|
+
event,
|
|
26192
|
+
at: Date.now()
|
|
26193
|
+
};
|
|
26194
|
+
if (event === "start" && typeof payload.source === "string") record.source = payload.source;
|
|
26195
|
+
if (event === "end" && typeof payload.reason === "string") record.reason = payload.reason;
|
|
26196
|
+
return record;
|
|
26197
|
+
}
|
|
26198
|
+
/** Atomically write the sidecar (temp + rename) so the reader never sees a partial file. */
|
|
26199
|
+
function writeSidecar(out, record) {
|
|
26200
|
+
try {
|
|
26201
|
+
mkdirSync(nodePath.dirname(out), { recursive: true });
|
|
26202
|
+
} catch {}
|
|
26203
|
+
const tmp = `${out}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
26204
|
+
writeFileSync(tmp, JSON.stringify(record), { mode: 384 });
|
|
26205
|
+
renameSync(tmp, out);
|
|
26206
|
+
}
|
|
26207
|
+
const internalSessionBind = defineCommand({
|
|
26208
|
+
meta: {
|
|
26209
|
+
name: "internal-session-bind",
|
|
26210
|
+
description: "Internal: the SessionStart/SessionEnd hook. Reads the Claude Code hook payload on stdin and atomically writes the active session id + transcript path to the --out sidecar file (consumed by ai-or-die to bind a tab's sticky-note summariser). Side-effect only."
|
|
26211
|
+
},
|
|
26212
|
+
args: { out: {
|
|
26213
|
+
type: "string",
|
|
26214
|
+
description: "Absolute path to the sidecar file to (atomically) write.",
|
|
26215
|
+
required: false
|
|
26216
|
+
} },
|
|
26217
|
+
run({ args }) {
|
|
26218
|
+
try {
|
|
26219
|
+
const out = typeof args.out === "string" ? args.out.trim() : "";
|
|
26220
|
+
if (!out) return;
|
|
26221
|
+
const record = decodeSessionBind(readStdin$1());
|
|
26222
|
+
if (record) writeSidecar(out, record);
|
|
26223
|
+
} catch {}
|
|
26224
|
+
process.exitCode = 0;
|
|
26225
|
+
}
|
|
26226
|
+
});
|
|
26227
|
+
|
|
25941
26228
|
//#endregion
|
|
25942
26229
|
//#region src/internal-stop-hook.ts
|
|
25943
26230
|
/**
|
|
@@ -26450,7 +26737,7 @@ process.on("uncaughtException", (error) => {
|
|
|
26450
26737
|
const version = getPackageVersion();
|
|
26451
26738
|
const argv = process.argv.slice(2);
|
|
26452
26739
|
const isVersionFlag = argv.includes("--version");
|
|
26453
|
-
const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review";
|
|
26740
|
+
const isInternalHook = argv[0] === "internal-stop-hook" || argv[0] === "internal-prompt-submit" || argv[0] === "internal-stop-review" || argv[0] === "internal-session-bind";
|
|
26454
26741
|
if (!isVersionFlag && !isInternalHook) consola.info(`github-router v${version}`);
|
|
26455
26742
|
await runMain(defineCommand({
|
|
26456
26743
|
meta: {
|
|
@@ -26468,7 +26755,8 @@ await runMain(defineCommand({
|
|
|
26468
26755
|
debug,
|
|
26469
26756
|
"internal-stop-hook": internalStopHook,
|
|
26470
26757
|
"internal-prompt-submit": internalPromptSubmit,
|
|
26471
|
-
"internal-stop-review": internalStopReview
|
|
26758
|
+
"internal-stop-review": internalStopReview,
|
|
26759
|
+
"internal-session-bind": internalSessionBind
|
|
26472
26760
|
}
|
|
26473
26761
|
}));
|
|
26474
26762
|
|