@openhoo/hoopilot 0.7.1 → 0.7.3
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 -2
- package/dist/chunk-7GSQVYYT.js +221 -0
- package/dist/chunk-7GSQVYYT.js.map +1 -0
- package/dist/cli.js +473 -83
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +5 -161
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +533 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +532 -155
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,38 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
asRecord,
|
|
4
|
+
envValue,
|
|
5
|
+
isTrustedTokenBaseUrl,
|
|
6
|
+
main,
|
|
7
|
+
trimTrailingSlash,
|
|
8
|
+
truncatedResponseText
|
|
9
|
+
} from "./chunk-7GSQVYYT.js";
|
|
2
10
|
|
|
3
11
|
// src/cli.ts
|
|
4
12
|
import { spawn } from "child_process";
|
|
13
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
14
|
|
|
6
15
|
// src/auth-store.ts
|
|
7
16
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
8
17
|
import { dirname, join } from "path";
|
|
18
|
+
var StoredCopilotAuthError = class extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "StoredCopilotAuthError";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
9
24
|
function authStorePath(env = process.env) {
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
const explicit = envValue(env.HOOPILOT_AUTH_FILE);
|
|
26
|
+
if (explicit) {
|
|
27
|
+
return explicit;
|
|
28
|
+
}
|
|
29
|
+
const xdg = envValue(env.XDG_CONFIG_HOME);
|
|
30
|
+
if (xdg) {
|
|
31
|
+
return join(xdg, "hoopilot", "auth.json");
|
|
32
|
+
}
|
|
33
|
+
const appdata = envValue(env.APPDATA);
|
|
34
|
+
if (appdata) {
|
|
35
|
+
return join(appdata, "hoopilot", "auth.json");
|
|
36
|
+
}
|
|
37
|
+
const home = envValue(env.HOME);
|
|
38
|
+
if (!home) {
|
|
39
|
+
throw new StoredCopilotAuthError(
|
|
40
|
+
"Cannot resolve Hoopilot auth file path without HOOPILOT_AUTH_FILE, XDG_CONFIG_HOME, APPDATA, or HOME."
|
|
41
|
+
);
|
|
12
42
|
}
|
|
13
|
-
const base =
|
|
43
|
+
const base = join(home, ".config");
|
|
14
44
|
return join(base, "hoopilot", "auth.json");
|
|
15
45
|
}
|
|
16
46
|
function readStoredCopilotAuth(path = authStorePath()) {
|
|
47
|
+
let text;
|
|
17
48
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
const token = typeof parsed.token === "string" ? parsed.token.trim() : "";
|
|
23
|
-
if (!token) {
|
|
49
|
+
text = readFileSync(path, "utf8");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === "ENOENT") {
|
|
24
52
|
return void 0;
|
|
25
53
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
token
|
|
32
|
-
};
|
|
54
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
55
|
+
}
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(text);
|
|
33
59
|
} catch {
|
|
34
|
-
|
|
60
|
+
throw new StoredCopilotAuthError(
|
|
61
|
+
`Hoopilot auth file at ${path} is not valid JSON. Run \`hoopilot login\` to replace it.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
65
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
66
|
+
}
|
|
67
|
+
const record = parsed;
|
|
68
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
69
|
+
if (!token) {
|
|
70
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
35
71
|
}
|
|
72
|
+
return {
|
|
73
|
+
apiBaseUrl: typeof record.apiBaseUrl === "string" ? record.apiBaseUrl : void 0,
|
|
74
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : void 0,
|
|
75
|
+
githubDomain: typeof record.githubDomain === "string" ? record.githubDomain : void 0,
|
|
76
|
+
source: typeof record.source === "string" ? record.source : void 0,
|
|
77
|
+
token
|
|
78
|
+
};
|
|
36
79
|
}
|
|
37
80
|
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
38
81
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -54,18 +97,6 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
54
97
|
}
|
|
55
98
|
}
|
|
56
99
|
|
|
57
|
-
// src/util.ts
|
|
58
|
-
function trimTrailingSlash(value) {
|
|
59
|
-
return value.replace(/\/+$/, "");
|
|
60
|
-
}
|
|
61
|
-
async function truncatedResponseText(response, max = 500) {
|
|
62
|
-
const text = await response.text();
|
|
63
|
-
return text.slice(0, max);
|
|
64
|
-
}
|
|
65
|
-
function asRecord(value) {
|
|
66
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
100
|
// src/auth.ts
|
|
70
101
|
var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
|
|
71
102
|
var REFRESH_SKEW_MS = 6e4;
|
|
@@ -79,21 +110,35 @@ var CopilotAuthError = class extends Error {
|
|
|
79
110
|
var CopilotAuth = class {
|
|
80
111
|
#authStorePath;
|
|
81
112
|
#copilotApiBaseUrl;
|
|
113
|
+
#hasCopilotApiBaseUrlOverride;
|
|
82
114
|
#cachedAccess;
|
|
83
115
|
constructor(options = {}) {
|
|
84
|
-
|
|
116
|
+
const envAuthStorePath = envValue(options.env?.HOOPILOT_AUTH_FILE);
|
|
117
|
+
const envCopilotApiBaseUrl = envValue(options.env?.COPILOT_API_BASE_URL);
|
|
118
|
+
this.#authStorePath = options.authStorePath ?? envAuthStorePath;
|
|
119
|
+
this.#hasCopilotApiBaseUrlOverride = Boolean(options.copilotApiBaseUrl ?? envCopilotApiBaseUrl);
|
|
85
120
|
this.#copilotApiBaseUrl = trimTrailingSlash(
|
|
86
|
-
options.copilotApiBaseUrl ??
|
|
121
|
+
options.copilotApiBaseUrl ?? envCopilotApiBaseUrl ?? DEFAULT_COPILOT_API_BASE_URL
|
|
87
122
|
);
|
|
88
123
|
}
|
|
89
124
|
async getAccess() {
|
|
90
125
|
if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
|
|
91
126
|
return this.#cachedAccess;
|
|
92
127
|
}
|
|
93
|
-
|
|
128
|
+
let stored;
|
|
129
|
+
try {
|
|
130
|
+
stored = readStoredCopilotAuth(this.#authStorePath);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof StoredCopilotAuthError) {
|
|
133
|
+
throw new CopilotAuthError(error.message);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
94
137
|
if (stored) {
|
|
95
138
|
return this.#cacheAccess({
|
|
96
|
-
apiBaseUrl: trimTrailingSlash(
|
|
139
|
+
apiBaseUrl: trimTrailingSlash(
|
|
140
|
+
this.#hasCopilotApiBaseUrlOverride ? this.#copilotApiBaseUrl : stored.apiBaseUrl ?? this.#copilotApiBaseUrl
|
|
141
|
+
),
|
|
97
142
|
expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
|
|
98
143
|
source: "github-copilot-oauth",
|
|
99
144
|
token: stored.token
|
|
@@ -111,6 +156,8 @@ var CopilotAuth = class {
|
|
|
111
156
|
|
|
112
157
|
// src/copilot.ts
|
|
113
158
|
var DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com";
|
|
159
|
+
var ALLOWED_COPILOT_API_HOSTS = ["api.githubcopilot.com"];
|
|
160
|
+
var ALLOWED_GITHUB_API_HOSTS = ["api.github.com"];
|
|
114
161
|
var COPILOT_USAGE_API_VERSION = "2025-04-01";
|
|
115
162
|
function applyCopilotHeaders(headers, token) {
|
|
116
163
|
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
@@ -134,13 +181,15 @@ function applyGithubApiHeaders(headers, token) {
|
|
|
134
181
|
}
|
|
135
182
|
var CopilotClient = class {
|
|
136
183
|
#auth;
|
|
184
|
+
#allowUnsafeUpstream;
|
|
137
185
|
#fetch;
|
|
138
186
|
#githubApiBaseUrl;
|
|
139
187
|
constructor(options = {}) {
|
|
140
188
|
this.#auth = new CopilotAuth(options);
|
|
189
|
+
this.#allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
141
190
|
this.#fetch = options.fetch ?? fetch;
|
|
142
191
|
this.#githubApiBaseUrl = trimTrailingSlash(
|
|
143
|
-
options.githubApiBaseUrl ?? options.env?.HOOPILOT_GITHUB_API_BASE_URL ?? DEFAULT_GITHUB_API_BASE_URL
|
|
192
|
+
options.githubApiBaseUrl ?? envValue(options.env?.HOOPILOT_GITHUB_API_BASE_URL) ?? DEFAULT_GITHUB_API_BASE_URL
|
|
144
193
|
);
|
|
145
194
|
}
|
|
146
195
|
/**
|
|
@@ -149,9 +198,13 @@ var CopilotClient = class {
|
|
|
149
198
|
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
150
199
|
*/
|
|
151
200
|
async usage(signal) {
|
|
152
|
-
if (!
|
|
201
|
+
if (!isTrustedTokenBaseUrl(
|
|
202
|
+
this.#githubApiBaseUrl,
|
|
203
|
+
ALLOWED_GITHUB_API_HOSTS,
|
|
204
|
+
this.#allowUnsafeUpstream
|
|
205
|
+
)) {
|
|
153
206
|
throw new Error(
|
|
154
|
-
`Refusing to send the GitHub OAuth token to
|
|
207
|
+
`Refusing to send the GitHub OAuth token to an untrusted GitHub API host: ${this.#githubApiBaseUrl}`
|
|
155
208
|
);
|
|
156
209
|
}
|
|
157
210
|
const access = await this.#auth.getAccess();
|
|
@@ -193,6 +246,15 @@ var CopilotClient = class {
|
|
|
193
246
|
}
|
|
194
247
|
async fetchCopilot(path, init) {
|
|
195
248
|
const access = await this.#auth.getAccess();
|
|
249
|
+
if (!isTrustedTokenBaseUrl(
|
|
250
|
+
access.apiBaseUrl,
|
|
251
|
+
ALLOWED_COPILOT_API_HOSTS,
|
|
252
|
+
this.#allowUnsafeUpstream
|
|
253
|
+
)) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${access.apiBaseUrl}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
196
258
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
197
259
|
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
198
260
|
...init,
|
|
@@ -248,18 +310,6 @@ function usedFrom(entitlement, remaining) {
|
|
|
248
310
|
}
|
|
249
311
|
return Math.max(0, entitlement - remaining);
|
|
250
312
|
}
|
|
251
|
-
function isHttpsOrLoopback(rawUrl) {
|
|
252
|
-
let url;
|
|
253
|
-
try {
|
|
254
|
-
url = new URL(rawUrl);
|
|
255
|
-
} catch {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
if (url.protocol === "https:") {
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1");
|
|
262
|
-
}
|
|
263
313
|
function numberOrUndefined(value) {
|
|
264
314
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
265
315
|
}
|
|
@@ -288,9 +338,9 @@ async function githubCopilotDeviceLogin(options = {}) {
|
|
|
288
338
|
const fetcher = options.fetch ?? fetch;
|
|
289
339
|
const sleeper = options.sleep ?? sleep;
|
|
290
340
|
const domain = normalizeDomain(
|
|
291
|
-
options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
|
|
341
|
+
options.domain ?? envValue(env.HOOPILOT_GITHUB_DOMAIN) ?? DEFAULT_GITHUB_DOMAIN
|
|
292
342
|
);
|
|
293
|
-
const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
343
|
+
const clientId = options.clientId ?? envValue(env.HOOPILOT_GITHUB_CLIENT_ID) ?? envValue(env.COPILOT_GITHUB_CLIENT_ID) ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
|
|
294
344
|
const device = await requestDeviceCode(fetcher, domain, clientId);
|
|
295
345
|
const verificationUrl = device.verification_uri;
|
|
296
346
|
const userCode = device.user_code;
|
|
@@ -448,8 +498,8 @@ var noopLogger = {
|
|
|
448
498
|
};
|
|
449
499
|
function createHoopilotLogger(options = {}) {
|
|
450
500
|
const env = options.env ?? process.env;
|
|
451
|
-
const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
|
|
452
|
-
const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
|
|
501
|
+
const level = parseLogLevel(options.level ?? envValue(env.HOOPILOT_LOG_LEVEL));
|
|
502
|
+
const format = parseLogFormat(options.format ?? envValue(env.HOOPILOT_LOG_FORMAT));
|
|
453
503
|
const pinoOptions = {
|
|
454
504
|
base: {
|
|
455
505
|
service: "hoopilot",
|
|
@@ -499,7 +549,7 @@ function parseLogLevel(value) {
|
|
|
499
549
|
}
|
|
500
550
|
function shouldCreateLogger(options) {
|
|
501
551
|
return Boolean(
|
|
502
|
-
options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
|
|
552
|
+
options.logger || options.logFormat || options.logLevel || envValue(options.env?.HOOPILOT_LOG_FORMAT) || envValue(options.env?.HOOPILOT_LOG_LEVEL)
|
|
503
553
|
);
|
|
504
554
|
}
|
|
505
555
|
function errorDetails(error) {
|
|
@@ -560,6 +610,51 @@ function chatCompletionToCompletion(completion) {
|
|
|
560
610
|
usage: completion.usage
|
|
561
611
|
});
|
|
562
612
|
}
|
|
613
|
+
function completionStreamFromChatStream(chatStream) {
|
|
614
|
+
const encoder = new TextEncoder();
|
|
615
|
+
const decoder = new TextDecoder();
|
|
616
|
+
let buffer = "";
|
|
617
|
+
let sawTerminalEvent = false;
|
|
618
|
+
return new ReadableStream({
|
|
619
|
+
async start(controller) {
|
|
620
|
+
const enqueue = (data) => {
|
|
621
|
+
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
622
|
+
};
|
|
623
|
+
const markTerminal = () => {
|
|
624
|
+
sawTerminalEvent = true;
|
|
625
|
+
};
|
|
626
|
+
const reader = chatStream.getReader();
|
|
627
|
+
try {
|
|
628
|
+
while (true) {
|
|
629
|
+
const result = await reader.read();
|
|
630
|
+
if (result.done) {
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
634
|
+
const blocks = buffer.split(/\r?\n\r?\n/);
|
|
635
|
+
buffer = blocks.pop() ?? "";
|
|
636
|
+
for (const block of blocks) {
|
|
637
|
+
processCompletionSseBlock(block, enqueue, markTerminal);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const tail = `${buffer}${decoder.decode()}`;
|
|
641
|
+
if (tail.trim()) {
|
|
642
|
+
processCompletionSseBlock(tail, enqueue, markTerminal);
|
|
643
|
+
}
|
|
644
|
+
if (!sawTerminalEvent) {
|
|
645
|
+
enqueue("[DONE]");
|
|
646
|
+
}
|
|
647
|
+
controller.close();
|
|
648
|
+
} catch (error) {
|
|
649
|
+
await reader.cancel(error).catch(() => {
|
|
650
|
+
});
|
|
651
|
+
controller.error(error);
|
|
652
|
+
} finally {
|
|
653
|
+
reader.releaseLock();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
563
658
|
function normalizeModelsResponse(upstream) {
|
|
564
659
|
const record = asRecord(upstream);
|
|
565
660
|
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
@@ -650,6 +745,94 @@ function firstChoice(completion) {
|
|
|
650
745
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
651
746
|
return asRecord(choices[0]);
|
|
652
747
|
}
|
|
748
|
+
function processCompletionSseBlock(block, enqueue, markTerminal) {
|
|
749
|
+
let event = "message";
|
|
750
|
+
const dataLines = [];
|
|
751
|
+
for (const line of block.split(/\r?\n/)) {
|
|
752
|
+
const trimmed = line.trim();
|
|
753
|
+
if (trimmed.startsWith("event:")) {
|
|
754
|
+
event = trimmed.slice("event:".length).trim() || event;
|
|
755
|
+
} else if (trimmed.startsWith("data:")) {
|
|
756
|
+
dataLines.push(trimmed.slice("data:".length).trim());
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const data = dataLines.join("\n");
|
|
760
|
+
if (!data) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (data === "[DONE]") {
|
|
764
|
+
markTerminal();
|
|
765
|
+
enqueue("[DONE]");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const parsed = parseJson(data);
|
|
769
|
+
if (!parsed) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const error = completionStreamError(event, parsed);
|
|
773
|
+
if (error) {
|
|
774
|
+
markTerminal();
|
|
775
|
+
enqueue({ error });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const choice = firstChoice(parsed);
|
|
779
|
+
const delta = asRecord(choice.delta);
|
|
780
|
+
const text = contentToText(delta.content);
|
|
781
|
+
const finishReason = choice.finish_reason ?? null;
|
|
782
|
+
const usage = asRecord(parsed.usage);
|
|
783
|
+
const hasUsage = Object.keys(usage).length > 0;
|
|
784
|
+
if (!text && finishReason === null && !hasUsage) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
enqueue(
|
|
788
|
+
removeUndefined({
|
|
789
|
+
choices: text || finishReason !== null ? [
|
|
790
|
+
{
|
|
791
|
+
finish_reason: finishReason,
|
|
792
|
+
index: typeof choice.index === "number" ? choice.index : 0,
|
|
793
|
+
logprobs: null,
|
|
794
|
+
text
|
|
795
|
+
}
|
|
796
|
+
] : [],
|
|
797
|
+
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
798
|
+
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
799
|
+
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
800
|
+
object: "text_completion",
|
|
801
|
+
usage: hasUsage ? usage : void 0
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
function completionStreamError(event, parsed) {
|
|
806
|
+
const responseError = asRecord(asRecord(parsed.response).error);
|
|
807
|
+
const directError = asRecord(parsed.error);
|
|
808
|
+
const error = Object.keys(directError).length > 0 ? directError : Object.keys(responseError).length > 0 ? responseError : void 0;
|
|
809
|
+
if (error) {
|
|
810
|
+
return error;
|
|
811
|
+
}
|
|
812
|
+
if (event === "error" || parsed.type === "response.failed") {
|
|
813
|
+
return removeUndefined({
|
|
814
|
+
code: contentToText(parsed.code) || void 0,
|
|
815
|
+
message: contentToText(parsed.message) || "Upstream streaming request failed.",
|
|
816
|
+
type: contentToText(parsed.type) || "upstream_stream_error"
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
function encodeDataSse(data) {
|
|
822
|
+
if (data === "[DONE]") {
|
|
823
|
+
return "data: [DONE]\n\n";
|
|
824
|
+
}
|
|
825
|
+
return `data: ${JSON.stringify(data)}
|
|
826
|
+
|
|
827
|
+
`;
|
|
828
|
+
}
|
|
829
|
+
function parseJson(data) {
|
|
830
|
+
try {
|
|
831
|
+
return asRecord(JSON.parse(data));
|
|
832
|
+
} catch {
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
653
836
|
function removeUndefined(record) {
|
|
654
837
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
655
838
|
}
|
|
@@ -1029,11 +1212,21 @@ function formatNumber(value) {
|
|
|
1029
1212
|
// src/server.ts
|
|
1030
1213
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1031
1214
|
var DEFAULT_PORT = 4141;
|
|
1215
|
+
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1032
1216
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
1217
|
+
var MAX_REQUEST_BODY_BYTES = 16 * 1024 * 1024;
|
|
1218
|
+
var REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
1219
|
+
var REQUEST_TOO_LARGE_MESSAGE = `Request body must be ${MAX_REQUEST_BODY_BYTES} bytes or smaller.`;
|
|
1033
1220
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
1221
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
1222
|
+
constructor() {
|
|
1223
|
+
super(REQUEST_TOO_LARGE_MESSAGE);
|
|
1224
|
+
this.name = "RequestBodyTooLargeError";
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1034
1227
|
function createHoopilotHandler(options = {}) {
|
|
1035
1228
|
const client = new CopilotClient(options);
|
|
1036
|
-
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1229
|
+
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
1037
1230
|
const logger = serverLogger(options);
|
|
1038
1231
|
const metrics = options.metrics ?? new MetricsRegistry();
|
|
1039
1232
|
const readUsage = createUsageReader(client, metrics);
|
|
@@ -1059,6 +1252,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
1059
1252
|
route,
|
|
1060
1253
|
startedAt
|
|
1061
1254
|
});
|
|
1255
|
+
const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
|
|
1256
|
+
if (browserOrigin) {
|
|
1257
|
+
requestLogger.warn(
|
|
1258
|
+
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
1259
|
+
"blocked unauthenticated browser-origin request"
|
|
1260
|
+
);
|
|
1261
|
+
return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
|
|
1262
|
+
}
|
|
1062
1263
|
if (request.method === "OPTIONS") {
|
|
1063
1264
|
return finish(new Response(null, { headers: corsHeaders() }));
|
|
1064
1265
|
}
|
|
@@ -1111,6 +1312,12 @@ function createHoopilotHandler(options = {}) {
|
|
|
1111
1312
|
"request body was invalid json"
|
|
1112
1313
|
);
|
|
1113
1314
|
return finish(jsonError(400, "invalid_request_error", message));
|
|
1315
|
+
} else if (error instanceof RequestBodyTooLargeError) {
|
|
1316
|
+
requestLogger.warn(
|
|
1317
|
+
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1318
|
+
"request body exceeded size limit"
|
|
1319
|
+
);
|
|
1320
|
+
return finish(jsonError(413, "request_too_large", message));
|
|
1114
1321
|
} else {
|
|
1115
1322
|
requestLogger.error(
|
|
1116
1323
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
@@ -1122,10 +1329,10 @@ function createHoopilotHandler(options = {}) {
|
|
|
1122
1329
|
};
|
|
1123
1330
|
}
|
|
1124
1331
|
function startHoopilotServer(options = {}) {
|
|
1125
|
-
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1126
|
-
const port =
|
|
1127
|
-
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1128
|
-
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1332
|
+
const host = options.host ?? envValue(options.env?.HOST) ?? DEFAULT_HOST;
|
|
1333
|
+
const port = normalizeServerPort(options.port ?? envValue(options.env?.PORT) ?? DEFAULT_PORT);
|
|
1334
|
+
const apiKey = options.apiKey ?? envValue(options.env?.HOOPILOT_API_KEY);
|
|
1335
|
+
const allowUnauthenticated = options.allowUnauthenticated ?? envValue(options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED) === "1";
|
|
1129
1336
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
1130
1337
|
throw new Error(
|
|
1131
1338
|
"Refusing to listen on a non-loopback host without HOOPILOT_API_KEY. Set an API key or pass --allow-unauthenticated."
|
|
@@ -1143,7 +1350,7 @@ function startHoopilotServer(options = {}) {
|
|
|
1143
1350
|
});
|
|
1144
1351
|
return {
|
|
1145
1352
|
server,
|
|
1146
|
-
url: `http://${host}:${server.port}`
|
|
1353
|
+
url: `http://${urlHost(host)}:${server.port}`
|
|
1147
1354
|
};
|
|
1148
1355
|
}
|
|
1149
1356
|
async function handleModels(client, metrics, signal, logger) {
|
|
@@ -1189,8 +1396,19 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
|
|
|
1189
1396
|
}
|
|
1190
1397
|
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
1191
1398
|
const model = normalizeRequestedModel(body.model);
|
|
1192
|
-
if (isStreamingResponse(upstream)) {
|
|
1193
|
-
return proxyResponse(
|
|
1399
|
+
if (isStreamingResponse(upstream) && upstream.body) {
|
|
1400
|
+
return proxyResponse(
|
|
1401
|
+
observeResponseUsage(
|
|
1402
|
+
new Response(completionStreamFromChatStream(upstream.body), {
|
|
1403
|
+
headers: upstream.headers,
|
|
1404
|
+
status: upstream.status,
|
|
1405
|
+
statusText: upstream.statusText
|
|
1406
|
+
}),
|
|
1407
|
+
model,
|
|
1408
|
+
recordTokens,
|
|
1409
|
+
request.signal
|
|
1410
|
+
)
|
|
1411
|
+
);
|
|
1194
1412
|
}
|
|
1195
1413
|
const completion = asRecord(await upstream.json());
|
|
1196
1414
|
const usage = extractTokenUsage(completion.usage);
|
|
@@ -1224,7 +1442,7 @@ async function proxyError(upstream, logger) {
|
|
|
1224
1442
|
{ event: "copilot.request.failed", upstreamStatus: upstream.status },
|
|
1225
1443
|
"copilot upstream request failed"
|
|
1226
1444
|
);
|
|
1227
|
-
return
|
|
1445
|
+
return upstreamErrorResponse(upstream.status, text || upstream.statusText);
|
|
1228
1446
|
}
|
|
1229
1447
|
function proxyResponse(upstream) {
|
|
1230
1448
|
const headers = new Headers(upstream.headers);
|
|
@@ -1241,14 +1459,15 @@ function proxyResponse(upstream) {
|
|
|
1241
1459
|
});
|
|
1242
1460
|
}
|
|
1243
1461
|
async function readJson(request) {
|
|
1462
|
+
const text = await readRequestText(request);
|
|
1244
1463
|
try {
|
|
1245
|
-
return asRecord(
|
|
1464
|
+
return asRecord(JSON.parse(text));
|
|
1246
1465
|
} catch {
|
|
1247
1466
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1248
1467
|
}
|
|
1249
1468
|
}
|
|
1250
1469
|
async function readJsonText(request) {
|
|
1251
|
-
const text = await request
|
|
1470
|
+
const text = await readRequestText(request);
|
|
1252
1471
|
try {
|
|
1253
1472
|
JSON.parse(text);
|
|
1254
1473
|
return text;
|
|
@@ -1256,6 +1475,40 @@ async function readJsonText(request) {
|
|
|
1256
1475
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
1257
1476
|
}
|
|
1258
1477
|
}
|
|
1478
|
+
async function readRequestText(request) {
|
|
1479
|
+
const contentLength = request.headers.get("content-length");
|
|
1480
|
+
if (contentLength) {
|
|
1481
|
+
const declaredBytes = Number(contentLength);
|
|
1482
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_REQUEST_BODY_BYTES) {
|
|
1483
|
+
throw new RequestBodyTooLargeError();
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const body = request.body;
|
|
1487
|
+
if (!body) {
|
|
1488
|
+
return "";
|
|
1489
|
+
}
|
|
1490
|
+
const reader = body.getReader();
|
|
1491
|
+
const decoder = new TextDecoder();
|
|
1492
|
+
let bytes = 0;
|
|
1493
|
+
let text = "";
|
|
1494
|
+
try {
|
|
1495
|
+
while (true) {
|
|
1496
|
+
const { done, value } = await reader.read();
|
|
1497
|
+
if (done) {
|
|
1498
|
+
return `${text}${decoder.decode()}`;
|
|
1499
|
+
}
|
|
1500
|
+
bytes += value.byteLength;
|
|
1501
|
+
if (bytes > MAX_REQUEST_BODY_BYTES) {
|
|
1502
|
+
await reader.cancel().catch(() => {
|
|
1503
|
+
});
|
|
1504
|
+
throw new RequestBodyTooLargeError();
|
|
1505
|
+
}
|
|
1506
|
+
text += decoder.decode(value, { stream: true });
|
|
1507
|
+
}
|
|
1508
|
+
} finally {
|
|
1509
|
+
reader.releaseLock();
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1259
1512
|
function jsonResponse(body, status = 200) {
|
|
1260
1513
|
return new Response(JSON.stringify(body), {
|
|
1261
1514
|
headers: {
|
|
@@ -1277,6 +1530,13 @@ function jsonError(status, code, message) {
|
|
|
1277
1530
|
status
|
|
1278
1531
|
);
|
|
1279
1532
|
}
|
|
1533
|
+
function upstreamErrorResponse(status, text) {
|
|
1534
|
+
const parsedError = asRecord(asRecord(safeParseJson(text)).error);
|
|
1535
|
+
if (Object.keys(parsedError).length > 0) {
|
|
1536
|
+
return jsonResponse({ error: parsedError }, status);
|
|
1537
|
+
}
|
|
1538
|
+
return jsonError(status, "copilot_error", text);
|
|
1539
|
+
}
|
|
1280
1540
|
function websocketUnsupportedResponse() {
|
|
1281
1541
|
const response = jsonError(
|
|
1282
1542
|
426,
|
|
@@ -1301,6 +1561,17 @@ function isAuthorized(request, apiKey) {
|
|
|
1301
1561
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1302
1562
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1303
1563
|
}
|
|
1564
|
+
function forbiddenBrowserOrigin(request, apiKey) {
|
|
1565
|
+
if (apiKey) {
|
|
1566
|
+
return void 0;
|
|
1567
|
+
}
|
|
1568
|
+
const origin = request.headers.get("origin")?.trim();
|
|
1569
|
+
if (origin) {
|
|
1570
|
+
return isLoopbackOrigin(origin) ? void 0 : origin;
|
|
1571
|
+
}
|
|
1572
|
+
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
1573
|
+
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
1574
|
+
}
|
|
1304
1575
|
function isUpstreamAuthStatus(status) {
|
|
1305
1576
|
return status === 401 || status === 403;
|
|
1306
1577
|
}
|
|
@@ -1308,7 +1579,24 @@ function upstreamAuthMessage(message) {
|
|
|
1308
1579
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
1309
1580
|
}
|
|
1310
1581
|
function isLoopbackHost(host) {
|
|
1311
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1582
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1583
|
+
}
|
|
1584
|
+
function urlHost(host) {
|
|
1585
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1586
|
+
}
|
|
1587
|
+
function isLoopbackOrigin(origin) {
|
|
1588
|
+
try {
|
|
1589
|
+
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
1590
|
+
} catch {
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
function normalizeServerPort(value) {
|
|
1595
|
+
const port = Number(value);
|
|
1596
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1597
|
+
throw new Error(`Invalid port: ${value}.`);
|
|
1598
|
+
}
|
|
1599
|
+
return port;
|
|
1312
1600
|
}
|
|
1313
1601
|
function errorMessage(error) {
|
|
1314
1602
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1403,7 +1691,7 @@ function logRequestCompleted(logger, status, stream, durationMs) {
|
|
|
1403
1691
|
}
|
|
1404
1692
|
function requestIdFor(request) {
|
|
1405
1693
|
const existing = request.headers.get("x-request-id")?.trim();
|
|
1406
|
-
return existing
|
|
1694
|
+
return existing && REQUEST_ID_PATTERN.test(existing) ? existing : crypto.randomUUID();
|
|
1407
1695
|
}
|
|
1408
1696
|
function canonicalApiPath(path) {
|
|
1409
1697
|
const withoutTrailingSlash = path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
@@ -1527,7 +1815,8 @@ import {
|
|
|
1527
1815
|
mkdirSync as mkdirSync2,
|
|
1528
1816
|
realpathSync,
|
|
1529
1817
|
renameSync as renameSync2,
|
|
1530
|
-
rmSync
|
|
1818
|
+
rmSync,
|
|
1819
|
+
writeFileSync as writeFileSync2
|
|
1531
1820
|
} from "fs";
|
|
1532
1821
|
import { readFile, writeFile } from "fs/promises";
|
|
1533
1822
|
import { homedir } from "os";
|
|
@@ -1650,6 +1939,46 @@ function upgradeCommandFor(kind) {
|
|
|
1650
1939
|
function shouldCleanupOldBinary(platform, isStandaloneBinary) {
|
|
1651
1940
|
return platform === "win32" && isStandaloneBinary;
|
|
1652
1941
|
}
|
|
1942
|
+
function codexxShimFiles(platform) {
|
|
1943
|
+
if (platform === "win32") {
|
|
1944
|
+
return [
|
|
1945
|
+
{
|
|
1946
|
+
content: `$ErrorActionPreference = 'Stop'
|
|
1947
|
+
$hoopilot = Join-Path $PSScriptRoot 'hoopilot.exe'
|
|
1948
|
+
& $hoopilot codexx @args
|
|
1949
|
+
exit $LASTEXITCODE
|
|
1950
|
+
`,
|
|
1951
|
+
executable: false,
|
|
1952
|
+
name: "codexx.ps1"
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
content: `@echo off
|
|
1956
|
+
setlocal
|
|
1957
|
+
where pwsh >nul 2>nul
|
|
1958
|
+
if %ERRORLEVEL% EQU 0 (
|
|
1959
|
+
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0codexx.ps1" %*
|
|
1960
|
+
) else (
|
|
1961
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codexx.ps1" %*
|
|
1962
|
+
)
|
|
1963
|
+
exit /b %ERRORLEVEL%
|
|
1964
|
+
`,
|
|
1965
|
+
executable: false,
|
|
1966
|
+
name: "codexx.cmd"
|
|
1967
|
+
}
|
|
1968
|
+
];
|
|
1969
|
+
}
|
|
1970
|
+
return [
|
|
1971
|
+
{
|
|
1972
|
+
content: `#!/bin/sh
|
|
1973
|
+
set -eu
|
|
1974
|
+
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
|
1975
|
+
exec "$script_dir/hoopilot" codexx "$@"
|
|
1976
|
+
`,
|
|
1977
|
+
executable: true,
|
|
1978
|
+
name: "codexx"
|
|
1979
|
+
}
|
|
1980
|
+
];
|
|
1981
|
+
}
|
|
1653
1982
|
function formatUpdateNotice(current, latest, kind) {
|
|
1654
1983
|
return `
|
|
1655
1984
|
Update available for hoopilot: ${current} \u2192 ${latest}
|
|
@@ -1959,6 +2288,23 @@ function swapBinary(tmpFile, exePath) {
|
|
|
1959
2288
|
}
|
|
1960
2289
|
}
|
|
1961
2290
|
}
|
|
2291
|
+
function refreshCodexxShim(dir, logger) {
|
|
2292
|
+
try {
|
|
2293
|
+
for (const file of codexxShimFiles(process.platform)) {
|
|
2294
|
+
const path = join2(dir, file.name);
|
|
2295
|
+
writeFileSync2(path, file.content, "utf8");
|
|
2296
|
+
if (file.executable) {
|
|
2297
|
+
chmodSync2(path, 493);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
logger?.warn(
|
|
2302
|
+
{ err: errorDetails(error), event: "update.codexx_shim_failed" },
|
|
2303
|
+
"could not refresh codexx shim"
|
|
2304
|
+
);
|
|
2305
|
+
console.warn(`Updated hoopilot, but could not refresh the codexx shim: ${errorMessage2(error)}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
1962
2308
|
function cleanupOldBinary() {
|
|
1963
2309
|
if (!shouldCleanupOldBinary(process.platform, IS_STANDALONE_BINARY)) {
|
|
1964
2310
|
return;
|
|
@@ -2017,6 +2363,7 @@ async function runUpdate(currentVersion, logger) {
|
|
|
2017
2363
|
chmodSync2(tmpFile, 493);
|
|
2018
2364
|
}
|
|
2019
2365
|
swapBinary(tmpFile, exePath);
|
|
2366
|
+
refreshCodexxShim(dirname2(exePath), logger);
|
|
2020
2367
|
} catch (error) {
|
|
2021
2368
|
const code = error.code;
|
|
2022
2369
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -2040,9 +2387,13 @@ async function runUpdate(currentVersion, logger) {
|
|
|
2040
2387
|
console.log("Restart hoopilot to run the new version.");
|
|
2041
2388
|
}
|
|
2042
2389
|
}
|
|
2390
|
+
function errorMessage2(error) {
|
|
2391
|
+
return error instanceof Error ? error.message : String(error);
|
|
2392
|
+
}
|
|
2043
2393
|
|
|
2044
2394
|
// src/cli.ts
|
|
2045
|
-
|
|
2395
|
+
var ALLOWED_COPILOT_API_HOSTS2 = ["api.githubcopilot.com"];
|
|
2396
|
+
async function main2(argv = Bun.argv.slice(2)) {
|
|
2046
2397
|
cleanupOldBinary();
|
|
2047
2398
|
const command = argv[0];
|
|
2048
2399
|
if (command === "update" || command === "upgrade") {
|
|
@@ -2055,6 +2406,10 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
2055
2406
|
await runUpdate(await getVersion(), logger2);
|
|
2056
2407
|
return;
|
|
2057
2408
|
}
|
|
2409
|
+
if (command === "codexx") {
|
|
2410
|
+
await main(argv.slice(1), process.env);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2058
2413
|
if (command === "login") {
|
|
2059
2414
|
const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
|
|
2060
2415
|
if (args2.help) {
|
|
@@ -2140,43 +2495,68 @@ function parseArgs(argv) {
|
|
|
2140
2495
|
args.noUpdateCheck = true;
|
|
2141
2496
|
continue;
|
|
2142
2497
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
if (!value) {
|
|
2146
|
-
throw new Error(`Missing value for ${arg}.`);
|
|
2498
|
+
if (!arg.startsWith("-")) {
|
|
2499
|
+
throw new Error(`Unknown argument: ${arg}.`);
|
|
2147
2500
|
}
|
|
2501
|
+
const [name, inlineValue] = splitOption(arg);
|
|
2148
2502
|
switch (name) {
|
|
2149
2503
|
case "--api-key":
|
|
2150
|
-
args.apiKey =
|
|
2504
|
+
args.apiKey = optionValue(name, inlineValue, rest);
|
|
2505
|
+
break;
|
|
2506
|
+
case "--api-key-file":
|
|
2507
|
+
args.apiKey = readApiKeyFile(optionValue(name, inlineValue, rest));
|
|
2151
2508
|
break;
|
|
2152
2509
|
case "--auth-file":
|
|
2153
|
-
args.authStorePath =
|
|
2510
|
+
args.authStorePath = optionValue(name, inlineValue, rest);
|
|
2154
2511
|
break;
|
|
2155
2512
|
case "--copilot-api-base-url":
|
|
2156
|
-
args.copilotApiBaseUrl =
|
|
2513
|
+
args.copilotApiBaseUrl = optionValue(name, inlineValue, rest);
|
|
2157
2514
|
break;
|
|
2158
2515
|
case "--log-format":
|
|
2159
|
-
args.logFormat = parseLogFormat(
|
|
2516
|
+
args.logFormat = parseLogFormat(optionValue(name, inlineValue, rest));
|
|
2160
2517
|
break;
|
|
2161
2518
|
case "--log-level":
|
|
2162
|
-
args.logLevel = parseLogLevel(
|
|
2519
|
+
args.logLevel = parseLogLevel(optionValue(name, inlineValue, rest));
|
|
2163
2520
|
break;
|
|
2164
2521
|
case "--host":
|
|
2165
|
-
args.host =
|
|
2522
|
+
args.host = optionValue(name, inlineValue, rest);
|
|
2166
2523
|
break;
|
|
2167
2524
|
case "--port":
|
|
2168
|
-
case "-p":
|
|
2525
|
+
case "-p": {
|
|
2526
|
+
const value = optionValue(name, inlineValue, rest);
|
|
2169
2527
|
args.port = Number(value);
|
|
2170
|
-
if (!Number.isInteger(args.port) || args.port <= 0) {
|
|
2528
|
+
if (!Number.isInteger(args.port) || args.port <= 0 || args.port > 65535) {
|
|
2171
2529
|
throw new Error(`Invalid port: ${value}.`);
|
|
2172
2530
|
}
|
|
2173
2531
|
break;
|
|
2532
|
+
}
|
|
2174
2533
|
default:
|
|
2175
2534
|
throw new Error(`Unknown option: ${name}.`);
|
|
2176
2535
|
}
|
|
2177
2536
|
}
|
|
2178
2537
|
return args;
|
|
2179
2538
|
}
|
|
2539
|
+
function optionValue(name, inlineValue, rest) {
|
|
2540
|
+
const value = inlineValue ?? rest.shift();
|
|
2541
|
+
if (!value) {
|
|
2542
|
+
throw new Error(`Missing value for ${name}.`);
|
|
2543
|
+
}
|
|
2544
|
+
return value;
|
|
2545
|
+
}
|
|
2546
|
+
function splitOption(arg) {
|
|
2547
|
+
const separator = arg.indexOf("=");
|
|
2548
|
+
if (separator === -1) {
|
|
2549
|
+
return [arg, void 0];
|
|
2550
|
+
}
|
|
2551
|
+
return [arg.slice(0, separator), arg.slice(separator + 1)];
|
|
2552
|
+
}
|
|
2553
|
+
function readApiKeyFile(path) {
|
|
2554
|
+
const value = readFileSync2(path, "utf8").trim();
|
|
2555
|
+
if (!value) {
|
|
2556
|
+
throw new Error(`API key file is empty: ${path}.`);
|
|
2557
|
+
}
|
|
2558
|
+
return value;
|
|
2559
|
+
}
|
|
2180
2560
|
async function runLogin(options = {}) {
|
|
2181
2561
|
const logger = options.logger?.child({ component: "auth" }) ?? noopLogger;
|
|
2182
2562
|
logger.debug({ event: "auth.login.started" }, "starting github copilot browser login");
|
|
@@ -2313,8 +2693,14 @@ function roundQuota(value) {
|
|
|
2313
2693
|
}
|
|
2314
2694
|
async function verifyCopilotOAuthToken(token, options = {}) {
|
|
2315
2695
|
const apiBaseUrl = trimTrailingSlash(
|
|
2316
|
-
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2696
|
+
options.copilotApiBaseUrl ?? envValue(options.env?.COPILOT_API_BASE_URL) ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2317
2697
|
);
|
|
2698
|
+
const allowUnsafeUpstream = envValue(options.env?.HOOPILOT_ALLOW_UNSAFE_UPSTREAM) === "1";
|
|
2699
|
+
if (!isTrustedTokenBaseUrl(apiBaseUrl, ALLOWED_COPILOT_API_HOSTS2, allowUnsafeUpstream)) {
|
|
2700
|
+
throw new Error(
|
|
2701
|
+
`Refusing to send the GitHub OAuth token to an untrusted Copilot API host: ${apiBaseUrl}`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2318
2704
|
const fetcher = options.fetch ?? fetch;
|
|
2319
2705
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
2320
2706
|
headers: applyCopilotHeaders(new Headers(), token),
|
|
@@ -2379,6 +2765,7 @@ OpenAI-compatible proxy for GitHub Copilot.
|
|
|
2379
2765
|
|
|
2380
2766
|
Usage:
|
|
2381
2767
|
hoopilot [serve] [options]
|
|
2768
|
+
hoopilot codexx [codex options] [prompt]
|
|
2382
2769
|
hoopilot login [options]
|
|
2383
2770
|
hoopilot models [options]
|
|
2384
2771
|
hoopilot usage [options]
|
|
@@ -2387,6 +2774,7 @@ Usage:
|
|
|
2387
2774
|
|
|
2388
2775
|
Commands:
|
|
2389
2776
|
serve Start the proxy server (default)
|
|
2777
|
+
codexx Run Codex through the local Hoopilot server
|
|
2390
2778
|
login Sign in through GitHub OAuth in a browser and verify Copilot access
|
|
2391
2779
|
models List available GitHub Copilot model IDs
|
|
2392
2780
|
usage Show GitHub Copilot quota and premium-request usage
|
|
@@ -2400,6 +2788,7 @@ Options:
|
|
|
2400
2788
|
-p, --port <port> Port to listen on. Default: 4141
|
|
2401
2789
|
--host <host> Host to listen on. Default: 127.0.0.1
|
|
2402
2790
|
--api-key <key> Require clients to send Authorization: Bearer <key> or x-api-key: <key>
|
|
2791
|
+
--api-key-file <path> Read the local API key from a file instead of argv
|
|
2403
2792
|
--auth-file <path> OAuth credential store path
|
|
2404
2793
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
2405
2794
|
--log-level <level> trace, debug, info, warn, error, fatal, or silent
|
|
@@ -2418,17 +2807,18 @@ Environment:
|
|
|
2418
2807
|
HOOPILOT_LOG_LEVEL trace, debug, info, warn, error, fatal, or silent
|
|
2419
2808
|
COPILOT_API_BASE_URL
|
|
2420
2809
|
HOOPILOT_GITHUB_API_BASE_URL GitHub REST base for the usage/quota lookup. Default: https://api.github.com
|
|
2810
|
+
HOOPILOT_ALLOW_UNSAFE_UPSTREAM Set to 1 to allow nonstandard HTTPS token hosts
|
|
2421
2811
|
HOOPILOT_NO_UPDATE_CHECK Set to disable update checks (also NO_UPDATE_NOTIFIER)
|
|
2422
2812
|
`;
|
|
2423
2813
|
}
|
|
2424
2814
|
if (import.meta.main) {
|
|
2425
|
-
|
|
2815
|
+
main2().catch((error) => {
|
|
2426
2816
|
console.error(error instanceof Error ? error.message : String(error));
|
|
2427
2817
|
process.exit(1);
|
|
2428
2818
|
});
|
|
2429
2819
|
}
|
|
2430
2820
|
export {
|
|
2431
|
-
main,
|
|
2821
|
+
main2 as main,
|
|
2432
2822
|
parseArgs,
|
|
2433
2823
|
runModels,
|
|
2434
2824
|
runUsage,
|