github-router 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +189 -16
- package/dist/main.js.map +1 -1
- package/package.json +6 -3
package/dist/main.js
CHANGED
|
@@ -6,6 +6,8 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import process$1 from "node:process";
|
|
9
|
+
import fs$1 from "node:fs";
|
|
10
|
+
import { Writable } from "node:stream";
|
|
9
11
|
import { execFileSync, spawn } from "node:child_process";
|
|
10
12
|
import { serve } from "srvx";
|
|
11
13
|
import { getProxyForUrl } from "proxy-from-env";
|
|
@@ -19,9 +21,11 @@ import clipboard from "clipboardy";
|
|
|
19
21
|
//#region src/lib/paths.ts
|
|
20
22
|
const APP_DIR = path.join(os.homedir(), ".local", "share", "github-router");
|
|
21
23
|
const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
|
|
24
|
+
const ERROR_LOG_PATH = path.join(APP_DIR, "error.log");
|
|
22
25
|
const PATHS = {
|
|
23
26
|
APP_DIR,
|
|
24
|
-
GITHUB_TOKEN_PATH
|
|
27
|
+
GITHUB_TOKEN_PATH,
|
|
28
|
+
ERROR_LOG_PATH
|
|
25
29
|
};
|
|
26
30
|
async function ensurePaths() {
|
|
27
31
|
await fs.mkdir(PATHS.APP_DIR, { recursive: true });
|
|
@@ -462,6 +466,100 @@ const checkUsage = defineCommand({
|
|
|
462
466
|
}
|
|
463
467
|
});
|
|
464
468
|
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/lib/file-log-reporter.ts
|
|
471
|
+
const MAX_LOG_BYTES = 1024 * 1024;
|
|
472
|
+
const DEDUP_MAX = 1e3;
|
|
473
|
+
const ARG_MAX_LEN = 2048;
|
|
474
|
+
const DEDUP_KEY_MAX_LEN = 200;
|
|
475
|
+
const CREDENTIAL_RE = /\b(eyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]+){0,2}|gh[opsu]_[A-Za-z0-9_]{20,}|Bearer\s+\S{20,})\b/g;
|
|
476
|
+
const ALLOWED_TYPES = new Set([
|
|
477
|
+
"fatal",
|
|
478
|
+
"error",
|
|
479
|
+
"warn"
|
|
480
|
+
]);
|
|
481
|
+
function sanitize(line) {
|
|
482
|
+
return line.replace(CREDENTIAL_RE, "[REDACTED]");
|
|
483
|
+
}
|
|
484
|
+
function serializeArg(arg) {
|
|
485
|
+
if (typeof arg === "string") return arg;
|
|
486
|
+
if (arg instanceof Error) {
|
|
487
|
+
const parts = [arg.message];
|
|
488
|
+
if (arg.stack) parts.push(arg.stack);
|
|
489
|
+
return parts.join("\n");
|
|
490
|
+
}
|
|
491
|
+
return String(arg);
|
|
492
|
+
}
|
|
493
|
+
function formatLogLine(logObj) {
|
|
494
|
+
return sanitize(`${logObj.date.toISOString()} [${(logObj.type ?? "error").toUpperCase()}] ${logObj.args.map((a) => {
|
|
495
|
+
const s = serializeArg(a);
|
|
496
|
+
return s.length > ARG_MAX_LEN ? s.slice(0, ARG_MAX_LEN) + "…" : s;
|
|
497
|
+
}).join(" ").replace(/\r\n|\r|\n/g, "\\n")}\n`);
|
|
498
|
+
}
|
|
499
|
+
function makeDedupeKey(logObj) {
|
|
500
|
+
const firstArg = logObj.args.length > 0 ? serializeArg(logObj.args[0]) : "";
|
|
501
|
+
const key = `${logObj.type}:${firstArg}`;
|
|
502
|
+
return key.length > DEDUP_KEY_MAX_LEN ? key.slice(0, DEDUP_KEY_MAX_LEN) : key;
|
|
503
|
+
}
|
|
504
|
+
function rotateIfNeeded(filePath) {
|
|
505
|
+
let size;
|
|
506
|
+
try {
|
|
507
|
+
size = fs$1.statSync(filePath).size;
|
|
508
|
+
} catch {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (size <= MAX_LOG_BYTES) return;
|
|
512
|
+
try {
|
|
513
|
+
fs$1.renameSync(filePath, filePath + ".1");
|
|
514
|
+
} catch {}
|
|
515
|
+
}
|
|
516
|
+
var FileLogReporter = class {
|
|
517
|
+
filePath;
|
|
518
|
+
seen = /* @__PURE__ */ new Set();
|
|
519
|
+
writing = false;
|
|
520
|
+
constructor(filePath) {
|
|
521
|
+
this.filePath = filePath;
|
|
522
|
+
rotateIfNeeded(filePath);
|
|
523
|
+
}
|
|
524
|
+
log(logObj, _ctx) {
|
|
525
|
+
if (!ALLOWED_TYPES.has(logObj.type)) return;
|
|
526
|
+
if (this.writing) return;
|
|
527
|
+
const key = makeDedupeKey(logObj);
|
|
528
|
+
if (this.seen.has(key)) return;
|
|
529
|
+
if (this.seen.size >= DEDUP_MAX) this.seen.clear();
|
|
530
|
+
this.seen.add(key);
|
|
531
|
+
const line = formatLogLine(logObj);
|
|
532
|
+
this.writing = true;
|
|
533
|
+
try {
|
|
534
|
+
const fd = fs$1.openSync(this.filePath, "a", 384);
|
|
535
|
+
fs$1.writeSync(fd, line);
|
|
536
|
+
fs$1.closeSync(fd);
|
|
537
|
+
} catch {} finally {
|
|
538
|
+
this.writing = false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
const nullStream = new Writable({ write(_chunk, _encoding, cb) {
|
|
543
|
+
cb();
|
|
544
|
+
} });
|
|
545
|
+
/**
|
|
546
|
+
* Switch consola to file-only mode for TUI sessions.
|
|
547
|
+
* Removes the terminal reporter and installs a file reporter that
|
|
548
|
+
* persists errors and warnings to disk with dedup and credential scrubbing.
|
|
549
|
+
*
|
|
550
|
+
* Also sinks consola's stdout/stderr streams as belt-and-suspenders:
|
|
551
|
+
* even if a terminal reporter is re-added, it cannot write to the terminal.
|
|
552
|
+
* Crash handlers that call process.stderr.write() directly are unaffected.
|
|
553
|
+
* FileLogReporter uses fs.writeSync() directly and is also unaffected.
|
|
554
|
+
*/
|
|
555
|
+
function enableFileLogging() {
|
|
556
|
+
const reporter = new FileLogReporter(PATHS.ERROR_LOG_PATH);
|
|
557
|
+
consola.options.throttle = 0;
|
|
558
|
+
consola.setReporters([reporter]);
|
|
559
|
+
consola.options.stdout = nullStream;
|
|
560
|
+
consola.options.stderr = nullStream;
|
|
561
|
+
}
|
|
562
|
+
|
|
465
563
|
//#endregion
|
|
466
564
|
//#region src/lib/port.ts
|
|
467
565
|
const DEFAULT_PORT = 8787;
|
|
@@ -506,7 +604,9 @@ function launchChild(target, server$1) {
|
|
|
506
604
|
const { cmd, env } = buildLaunchCommand(target);
|
|
507
605
|
const executable = cmd[0];
|
|
508
606
|
if (!commandExists(executable)) {
|
|
509
|
-
|
|
607
|
+
const msg = `"${executable}" not found on PATH. Install it first, then try again.`;
|
|
608
|
+
consola.error(msg);
|
|
609
|
+
process$1.stderr.write(msg + "\n");
|
|
510
610
|
process$1.exit(1);
|
|
511
611
|
}
|
|
512
612
|
let child;
|
|
@@ -521,7 +621,9 @@ function launchChild(target, server$1) {
|
|
|
521
621
|
stdio: "inherit"
|
|
522
622
|
});
|
|
523
623
|
} catch (error) {
|
|
524
|
-
|
|
624
|
+
const msg = `Failed to launch ${executable}: ${error instanceof Error ? error.message : String(error)}`;
|
|
625
|
+
consola.error(msg);
|
|
626
|
+
process$1.stderr.write(msg + "\n");
|
|
525
627
|
server$1.close(true).catch(() => {});
|
|
526
628
|
process$1.exit(1);
|
|
527
629
|
}
|
|
@@ -549,10 +651,13 @@ function launchChild(target, server$1) {
|
|
|
549
651
|
};
|
|
550
652
|
process$1.on("SIGINT", onSignal);
|
|
551
653
|
process$1.on("SIGTERM", onSignal);
|
|
552
|
-
child.on("exit", (exitCode) => {
|
|
553
|
-
|
|
654
|
+
child.on("exit", (exitCode, signal) => {
|
|
655
|
+
const code = exitCode ?? (signal ? 128 : 1);
|
|
656
|
+
cleanup().then(() => exit(code)).catch(() => exit(1));
|
|
657
|
+
});
|
|
658
|
+
child.on("error", () => {
|
|
659
|
+
cleanup().then(() => exit(1)).catch(() => exit(1));
|
|
554
660
|
});
|
|
555
|
-
child.on("error", () => exit(1));
|
|
556
661
|
}
|
|
557
662
|
|
|
558
663
|
//#endregion
|
|
@@ -1776,31 +1881,99 @@ function extractUserQuery(input) {
|
|
|
1776
1881
|
}
|
|
1777
1882
|
}
|
|
1778
1883
|
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Compaction prompt used when GitHub Copilot API does not support
|
|
1886
|
+
* /responses/compact natively. Matches the prompt Codex CLI uses for
|
|
1887
|
+
* local (non-OpenAI) compaction.
|
|
1888
|
+
*/
|
|
1889
|
+
const COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
1890
|
+
|
|
1891
|
+
Include:
|
|
1892
|
+
- Current progress and key decisions made
|
|
1893
|
+
- Important context, constraints, or user preferences
|
|
1894
|
+
- What remains to be done (clear next steps)
|
|
1895
|
+
- Any critical data, examples, or references needed to continue
|
|
1896
|
+
|
|
1897
|
+
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
|
|
1779
1898
|
async function handleResponsesCompact(c) {
|
|
1780
1899
|
const startTime = Date.now();
|
|
1781
1900
|
await checkRateLimit(state);
|
|
1782
1901
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1783
1902
|
if (state.manualApprove) await awaitApproval();
|
|
1784
|
-
const body = await c.req.
|
|
1903
|
+
const body = await c.req.json();
|
|
1785
1904
|
const response = await fetch(`${copilotBaseUrl(state)}/responses/compact`, {
|
|
1786
1905
|
method: "POST",
|
|
1787
1906
|
headers: copilotHeaders(state),
|
|
1788
|
-
body
|
|
1907
|
+
body: JSON.stringify(body)
|
|
1789
1908
|
});
|
|
1790
|
-
if (
|
|
1909
|
+
if (response.ok) {
|
|
1791
1910
|
logRequest({
|
|
1792
1911
|
method: "POST",
|
|
1793
1912
|
path: c.req.path,
|
|
1794
|
-
status:
|
|
1913
|
+
status: 200
|
|
1914
|
+
}, void 0, startTime);
|
|
1915
|
+
return c.json(await response.json());
|
|
1916
|
+
}
|
|
1917
|
+
if (response.status === 404) {
|
|
1918
|
+
consola.debug("Copilot API does not support /responses/compact, using synthetic compaction");
|
|
1919
|
+
return await syntheticCompact(c, body, startTime);
|
|
1920
|
+
}
|
|
1921
|
+
logRequest({
|
|
1922
|
+
method: "POST",
|
|
1923
|
+
path: c.req.path,
|
|
1924
|
+
status: response.status
|
|
1925
|
+
}, void 0, startTime);
|
|
1926
|
+
throw new HTTPError("Copilot responses/compact request failed", response);
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Synthetic compaction: sends the conversation history to Copilot's
|
|
1930
|
+
* regular /responses endpoint with a compaction prompt appended,
|
|
1931
|
+
* then returns the model's summary in the compact response format.
|
|
1932
|
+
*/
|
|
1933
|
+
async function syntheticCompact(c, body, startTime) {
|
|
1934
|
+
const input = Array.isArray(body.input) ? [...body.input] : [];
|
|
1935
|
+
input.push({
|
|
1936
|
+
type: "message",
|
|
1937
|
+
role: "user",
|
|
1938
|
+
content: [{
|
|
1939
|
+
type: "input_text",
|
|
1940
|
+
text: COMPACTION_PROMPT
|
|
1941
|
+
}]
|
|
1942
|
+
});
|
|
1943
|
+
const payload = {
|
|
1944
|
+
model: body.model,
|
|
1945
|
+
input,
|
|
1946
|
+
instructions: body.instructions,
|
|
1947
|
+
stream: false,
|
|
1948
|
+
store: false
|
|
1949
|
+
};
|
|
1950
|
+
let result;
|
|
1951
|
+
try {
|
|
1952
|
+
result = await createResponses(payload);
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
if (error instanceof HTTPError) logRequest({
|
|
1955
|
+
method: "POST",
|
|
1956
|
+
path: c.req.path,
|
|
1957
|
+
status: error.response.status
|
|
1795
1958
|
}, void 0, startTime);
|
|
1796
|
-
throw
|
|
1959
|
+
throw error;
|
|
1797
1960
|
}
|
|
1798
1961
|
logRequest({
|
|
1799
1962
|
method: "POST",
|
|
1800
1963
|
path: c.req.path,
|
|
1801
1964
|
status: 200
|
|
1802
1965
|
}, void 0, startTime);
|
|
1803
|
-
return c.json(
|
|
1966
|
+
return c.json({
|
|
1967
|
+
id: `resp_compact_${randomUUID().replace(/-/g, "").slice(0, 24)}`,
|
|
1968
|
+
object: "response.compaction",
|
|
1969
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1970
|
+
output: result.output,
|
|
1971
|
+
usage: result.usage ?? {
|
|
1972
|
+
input_tokens: 0,
|
|
1973
|
+
output_tokens: 0,
|
|
1974
|
+
total_tokens: 0
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1804
1977
|
}
|
|
1805
1978
|
|
|
1806
1979
|
//#endregion
|
|
@@ -2081,6 +2254,7 @@ const claude = defineCommand({
|
|
|
2081
2254
|
consola.error("Failed to start server:", error instanceof Error ? error.message : error);
|
|
2082
2255
|
process$1.exit(1);
|
|
2083
2256
|
}
|
|
2257
|
+
enableFileLogging();
|
|
2084
2258
|
let resolvedModel;
|
|
2085
2259
|
if (args.model) {
|
|
2086
2260
|
resolvedModel = resolveModel(args.model);
|
|
@@ -2090,8 +2264,7 @@ const claude = defineCommand({
|
|
|
2090
2264
|
consola.warn(`Model "${resolvedModel}" not found. Available claude models: ${available.join(", ")}`);
|
|
2091
2265
|
}
|
|
2092
2266
|
}
|
|
2093
|
-
|
|
2094
|
-
consola.level = 1;
|
|
2267
|
+
process$1.stderr.write(`Server ready on ${serverUrl}, launching Claude Code...\n`);
|
|
2095
2268
|
launchChild({
|
|
2096
2269
|
kind: "claude-code",
|
|
2097
2270
|
envVars: getClaudeCodeEnvVars(serverUrl, resolvedModel ?? args.model),
|
|
@@ -2137,6 +2310,7 @@ const codex = defineCommand({
|
|
|
2137
2310
|
process$1.exit(1);
|
|
2138
2311
|
}
|
|
2139
2312
|
const requestedModel = args.model ?? DEFAULT_CODEX_MODEL;
|
|
2313
|
+
enableFileLogging();
|
|
2140
2314
|
const codexModel = resolveCodexModel(requestedModel);
|
|
2141
2315
|
if (codexModel !== requestedModel) consola.info(`Model "${requestedModel}" resolved to "${codexModel}"`);
|
|
2142
2316
|
const modelEntry = state.models?.data.find((m) => m.id === codexModel);
|
|
@@ -2147,8 +2321,7 @@ const codex = defineCommand({
|
|
|
2147
2321
|
const ctx = modelEntry.capabilities?.limits?.max_context_window_tokens;
|
|
2148
2322
|
if (ctx) consola.info(`Model context window: ${ctx.toLocaleString()} tokens`);
|
|
2149
2323
|
}
|
|
2150
|
-
|
|
2151
|
-
consola.level = 1;
|
|
2324
|
+
process$1.stderr.write(`Server ready on ${serverUrl}, launching Codex CLI (${codexModel})...\n`);
|
|
2152
2325
|
launchChild({
|
|
2153
2326
|
kind: "codex",
|
|
2154
2327
|
envVars: getCodexEnvVars(serverUrl),
|