@openhoo/hoopilot 0.7.0 → 0.7.2
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 +3 -2
- package/dist/chunk-TEDEVCKM.js +167 -0
- package/dist/chunk-TEDEVCKM.js.map +1 -0
- package/dist/cli.js +322 -39
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +5 -161
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +361 -122
- 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 +360 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
main
|
|
4
|
+
} from "./chunk-TEDEVCKM.js";
|
|
2
5
|
|
|
3
6
|
// src/cli.ts
|
|
4
7
|
import { spawn } from "child_process";
|
|
8
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
9
|
|
|
6
10
|
// src/auth-store.ts
|
|
7
11
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
8
12
|
import { dirname, join } from "path";
|
|
13
|
+
var StoredCopilotAuthError = class extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "StoredCopilotAuthError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
9
19
|
function authStorePath(env = process.env) {
|
|
10
20
|
if (env.HOOPILOT_AUTH_FILE) {
|
|
11
21
|
return env.HOOPILOT_AUTH_FILE;
|
|
@@ -14,25 +24,38 @@ function authStorePath(env = process.env) {
|
|
|
14
24
|
return join(base, "hoopilot", "auth.json");
|
|
15
25
|
}
|
|
16
26
|
function readStoredCopilotAuth(path = authStorePath()) {
|
|
27
|
+
let text;
|
|
17
28
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
const token = typeof parsed.token === "string" ? parsed.token.trim() : "";
|
|
23
|
-
if (!token) {
|
|
29
|
+
text = readFileSync(path, "utf8");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.code === "ENOENT") {
|
|
24
32
|
return void 0;
|
|
25
33
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
token
|
|
32
|
-
};
|
|
34
|
+
throw new StoredCopilotAuthError(`Could not read Hoopilot auth file at ${path}.`);
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(text);
|
|
33
39
|
} catch {
|
|
34
|
-
|
|
40
|
+
throw new StoredCopilotAuthError(
|
|
41
|
+
`Hoopilot auth file at ${path} is not valid JSON. Run \`hoopilot login\` to replace it.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
45
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} must contain a JSON object.`);
|
|
35
46
|
}
|
|
47
|
+
const record = parsed;
|
|
48
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
49
|
+
if (!token) {
|
|
50
|
+
throw new StoredCopilotAuthError(`Hoopilot auth file at ${path} does not contain a token.`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
apiBaseUrl: typeof record.apiBaseUrl === "string" ? record.apiBaseUrl : void 0,
|
|
54
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : void 0,
|
|
55
|
+
githubDomain: typeof record.githubDomain === "string" ? record.githubDomain : void 0,
|
|
56
|
+
source: typeof record.source === "string" ? record.source : void 0,
|
|
57
|
+
token
|
|
58
|
+
};
|
|
36
59
|
}
|
|
37
60
|
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
38
61
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -58,6 +81,18 @@ function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
|
58
81
|
function trimTrailingSlash(value) {
|
|
59
82
|
return value.replace(/\/+$/, "");
|
|
60
83
|
}
|
|
84
|
+
function isHttpsOrLoopbackUrl(rawUrl) {
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(rawUrl);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (url.protocol === "https:") {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]");
|
|
95
|
+
}
|
|
61
96
|
async function truncatedResponseText(response, max = 500) {
|
|
62
97
|
const text = await response.text();
|
|
63
98
|
return text.slice(0, max);
|
|
@@ -90,7 +125,15 @@ var CopilotAuth = class {
|
|
|
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
139
|
apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
|
|
@@ -149,7 +192,7 @@ var CopilotClient = class {
|
|
|
149
192
|
* accepted directly here — no Copilot token exchange is required to read quota.
|
|
150
193
|
*/
|
|
151
194
|
async usage(signal) {
|
|
152
|
-
if (!
|
|
195
|
+
if (!isHttpsOrLoopbackUrl(this.#githubApiBaseUrl)) {
|
|
153
196
|
throw new Error(
|
|
154
197
|
`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${this.#githubApiBaseUrl}`
|
|
155
198
|
);
|
|
@@ -193,6 +236,11 @@ var CopilotClient = class {
|
|
|
193
236
|
}
|
|
194
237
|
async fetchCopilot(path, init) {
|
|
195
238
|
const access = await this.#auth.getAccess();
|
|
239
|
+
if (!isHttpsOrLoopbackUrl(access.apiBaseUrl)) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${access.apiBaseUrl}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
196
244
|
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
197
245
|
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
198
246
|
...init,
|
|
@@ -248,18 +296,6 @@ function usedFrom(entitlement, remaining) {
|
|
|
248
296
|
}
|
|
249
297
|
return Math.max(0, entitlement - remaining);
|
|
250
298
|
}
|
|
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
299
|
function numberOrUndefined(value) {
|
|
264
300
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
265
301
|
}
|
|
@@ -560,6 +596,51 @@ function chatCompletionToCompletion(completion) {
|
|
|
560
596
|
usage: completion.usage
|
|
561
597
|
});
|
|
562
598
|
}
|
|
599
|
+
function completionStreamFromChatStream(chatStream) {
|
|
600
|
+
const encoder = new TextEncoder();
|
|
601
|
+
const decoder = new TextDecoder();
|
|
602
|
+
let buffer = "";
|
|
603
|
+
let sawDone = false;
|
|
604
|
+
return new ReadableStream({
|
|
605
|
+
async start(controller) {
|
|
606
|
+
const enqueue = (data) => {
|
|
607
|
+
controller.enqueue(encoder.encode(encodeDataSse(data)));
|
|
608
|
+
};
|
|
609
|
+
const reader = chatStream.getReader();
|
|
610
|
+
try {
|
|
611
|
+
while (true) {
|
|
612
|
+
const result = await reader.read();
|
|
613
|
+
if (result.done) {
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
617
|
+
const lines = buffer.split(/\r?\n/);
|
|
618
|
+
buffer = lines.pop() ?? "";
|
|
619
|
+
for (const line of lines) {
|
|
620
|
+
processCompletionSseLine(line, enqueue, () => {
|
|
621
|
+
sawDone = true;
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (buffer) {
|
|
626
|
+
processCompletionSseLine(buffer, enqueue, () => {
|
|
627
|
+
sawDone = true;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (!sawDone) {
|
|
631
|
+
enqueue("[DONE]");
|
|
632
|
+
}
|
|
633
|
+
controller.close();
|
|
634
|
+
} catch (error) {
|
|
635
|
+
await reader.cancel(error).catch(() => {
|
|
636
|
+
});
|
|
637
|
+
controller.error(error);
|
|
638
|
+
} finally {
|
|
639
|
+
reader.releaseLock();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
563
644
|
function normalizeModelsResponse(upstream) {
|
|
564
645
|
const record = asRecord(upstream);
|
|
565
646
|
const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
|
|
@@ -650,6 +731,66 @@ function firstChoice(completion) {
|
|
|
650
731
|
const choices = Array.isArray(completion.choices) ? completion.choices : [];
|
|
651
732
|
return asRecord(choices[0]);
|
|
652
733
|
}
|
|
734
|
+
function processCompletionSseLine(line, enqueue, markDone) {
|
|
735
|
+
const trimmed = line.trim();
|
|
736
|
+
if (!trimmed.startsWith("data:")) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const data = trimmed.slice("data:".length).trim();
|
|
740
|
+
if (!data) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (data === "[DONE]") {
|
|
744
|
+
markDone();
|
|
745
|
+
enqueue("[DONE]");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const parsed = parseJson(data);
|
|
749
|
+
if (!parsed) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const choice = firstChoice(parsed);
|
|
753
|
+
const delta = asRecord(choice.delta);
|
|
754
|
+
const text = contentToText(delta.content);
|
|
755
|
+
const finishReason = choice.finish_reason ?? null;
|
|
756
|
+
const usage = asRecord(parsed.usage);
|
|
757
|
+
const hasUsage = Object.keys(usage).length > 0;
|
|
758
|
+
if (!text && finishReason === null && !hasUsage) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
enqueue(
|
|
762
|
+
removeUndefined({
|
|
763
|
+
choices: text || finishReason !== null ? [
|
|
764
|
+
{
|
|
765
|
+
finish_reason: finishReason,
|
|
766
|
+
index: typeof choice.index === "number" ? choice.index : 0,
|
|
767
|
+
logprobs: null,
|
|
768
|
+
text
|
|
769
|
+
}
|
|
770
|
+
] : [],
|
|
771
|
+
created: typeof parsed.created === "number" ? parsed.created : epochSeconds(),
|
|
772
|
+
id: contentToText(parsed.id) || `cmpl_${randomId()}`,
|
|
773
|
+
model: contentToText(parsed.model) || DEFAULT_MODEL,
|
|
774
|
+
object: "text_completion",
|
|
775
|
+
usage: hasUsage ? usage : void 0
|
|
776
|
+
})
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function encodeDataSse(data) {
|
|
780
|
+
if (data === "[DONE]") {
|
|
781
|
+
return "data: [DONE]\n\n";
|
|
782
|
+
}
|
|
783
|
+
return `data: ${JSON.stringify(data)}
|
|
784
|
+
|
|
785
|
+
`;
|
|
786
|
+
}
|
|
787
|
+
function parseJson(data) {
|
|
788
|
+
try {
|
|
789
|
+
return asRecord(JSON.parse(data));
|
|
790
|
+
} catch {
|
|
791
|
+
return void 0;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
653
794
|
function removeUndefined(record) {
|
|
654
795
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
655
796
|
}
|
|
@@ -1029,6 +1170,7 @@ function formatNumber(value) {
|
|
|
1029
1170
|
// src/server.ts
|
|
1030
1171
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1031
1172
|
var DEFAULT_PORT = 4141;
|
|
1173
|
+
var FORBIDDEN_BROWSER_ORIGIN_MESSAGE = "Browser-origin requests require HOOPILOT_API_KEY unless the Origin is loopback.";
|
|
1032
1174
|
var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
|
|
1033
1175
|
var USAGE_CACHE_TTL_MS = 6e4;
|
|
1034
1176
|
function createHoopilotHandler(options = {}) {
|
|
@@ -1059,6 +1201,14 @@ function createHoopilotHandler(options = {}) {
|
|
|
1059
1201
|
route,
|
|
1060
1202
|
startedAt
|
|
1061
1203
|
});
|
|
1204
|
+
const browserOrigin = forbiddenBrowserOrigin(request, apiKey);
|
|
1205
|
+
if (browserOrigin) {
|
|
1206
|
+
requestLogger.warn(
|
|
1207
|
+
{ event: "http.request.forbidden_origin", origin: browserOrigin },
|
|
1208
|
+
"blocked unauthenticated browser-origin request"
|
|
1209
|
+
);
|
|
1210
|
+
return finish(jsonError(403, "forbidden_origin", FORBIDDEN_BROWSER_ORIGIN_MESSAGE));
|
|
1211
|
+
}
|
|
1062
1212
|
if (request.method === "OPTIONS") {
|
|
1063
1213
|
return finish(new Response(null, { headers: corsHeaders() }));
|
|
1064
1214
|
}
|
|
@@ -1110,6 +1260,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
1110
1260
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
1111
1261
|
"request body was invalid json"
|
|
1112
1262
|
);
|
|
1263
|
+
return finish(jsonError(400, "invalid_request_error", message));
|
|
1113
1264
|
} else {
|
|
1114
1265
|
requestLogger.error(
|
|
1115
1266
|
{ err: errorDetails(error), event: "http.request.failed" },
|
|
@@ -1122,7 +1273,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
1122
1273
|
}
|
|
1123
1274
|
function startHoopilotServer(options = {}) {
|
|
1124
1275
|
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1125
|
-
const port =
|
|
1276
|
+
const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
1126
1277
|
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1127
1278
|
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1128
1279
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
@@ -1188,8 +1339,19 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
|
|
|
1188
1339
|
}
|
|
1189
1340
|
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
1190
1341
|
const model = normalizeRequestedModel(body.model);
|
|
1191
|
-
if (isStreamingResponse(upstream)) {
|
|
1192
|
-
return proxyResponse(
|
|
1342
|
+
if (isStreamingResponse(upstream) && upstream.body) {
|
|
1343
|
+
return proxyResponse(
|
|
1344
|
+
observeResponseUsage(
|
|
1345
|
+
new Response(completionStreamFromChatStream(upstream.body), {
|
|
1346
|
+
headers: upstream.headers,
|
|
1347
|
+
status: upstream.status,
|
|
1348
|
+
statusText: upstream.statusText
|
|
1349
|
+
}),
|
|
1350
|
+
model,
|
|
1351
|
+
recordTokens,
|
|
1352
|
+
request.signal
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1193
1355
|
}
|
|
1194
1356
|
const completion = asRecord(await upstream.json());
|
|
1195
1357
|
const usage = extractTokenUsage(completion.usage);
|
|
@@ -1223,7 +1385,7 @@ async function proxyError(upstream, logger) {
|
|
|
1223
1385
|
{ event: "copilot.request.failed", upstreamStatus: upstream.status },
|
|
1224
1386
|
"copilot upstream request failed"
|
|
1225
1387
|
);
|
|
1226
|
-
return
|
|
1388
|
+
return upstreamErrorResponse(upstream.status, text || upstream.statusText);
|
|
1227
1389
|
}
|
|
1228
1390
|
function proxyResponse(upstream) {
|
|
1229
1391
|
const headers = new Headers(upstream.headers);
|
|
@@ -1276,6 +1438,13 @@ function jsonError(status, code, message) {
|
|
|
1276
1438
|
status
|
|
1277
1439
|
);
|
|
1278
1440
|
}
|
|
1441
|
+
function upstreamErrorResponse(status, text) {
|
|
1442
|
+
const parsedError = asRecord(asRecord(safeParseJson(text)).error);
|
|
1443
|
+
if (Object.keys(parsedError).length > 0) {
|
|
1444
|
+
return jsonResponse({ error: parsedError }, status);
|
|
1445
|
+
}
|
|
1446
|
+
return jsonError(status, "copilot_error", text);
|
|
1447
|
+
}
|
|
1279
1448
|
function websocketUnsupportedResponse() {
|
|
1280
1449
|
const response = jsonError(
|
|
1281
1450
|
426,
|
|
@@ -1300,6 +1469,17 @@ function isAuthorized(request, apiKey) {
|
|
|
1300
1469
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1301
1470
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1302
1471
|
}
|
|
1472
|
+
function forbiddenBrowserOrigin(request, apiKey) {
|
|
1473
|
+
if (apiKey) {
|
|
1474
|
+
return void 0;
|
|
1475
|
+
}
|
|
1476
|
+
const origin = request.headers.get("origin")?.trim();
|
|
1477
|
+
if (origin) {
|
|
1478
|
+
return isLoopbackOrigin(origin) ? void 0 : origin;
|
|
1479
|
+
}
|
|
1480
|
+
const fetchSite = request.headers.get("sec-fetch-site")?.toLowerCase();
|
|
1481
|
+
return fetchSite === "cross-site" ? "cross-site" : void 0;
|
|
1482
|
+
}
|
|
1303
1483
|
function isUpstreamAuthStatus(status) {
|
|
1304
1484
|
return status === 401 || status === 403;
|
|
1305
1485
|
}
|
|
@@ -1307,7 +1487,21 @@ function upstreamAuthMessage(message) {
|
|
|
1307
1487
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
1308
1488
|
}
|
|
1309
1489
|
function isLoopbackHost(host) {
|
|
1310
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
1490
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1491
|
+
}
|
|
1492
|
+
function isLoopbackOrigin(origin) {
|
|
1493
|
+
try {
|
|
1494
|
+
return isLoopbackHost(new URL(origin).hostname.toLowerCase());
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function normalizeServerPort(value) {
|
|
1500
|
+
const port = Number(value);
|
|
1501
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1502
|
+
throw new Error(`Invalid port: ${value}.`);
|
|
1503
|
+
}
|
|
1504
|
+
return port;
|
|
1311
1505
|
}
|
|
1312
1506
|
function errorMessage(error) {
|
|
1313
1507
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1526,7 +1720,8 @@ import {
|
|
|
1526
1720
|
mkdirSync as mkdirSync2,
|
|
1527
1721
|
realpathSync,
|
|
1528
1722
|
renameSync as renameSync2,
|
|
1529
|
-
rmSync
|
|
1723
|
+
rmSync,
|
|
1724
|
+
writeFileSync as writeFileSync2
|
|
1530
1725
|
} from "fs";
|
|
1531
1726
|
import { readFile, writeFile } from "fs/promises";
|
|
1532
1727
|
import { homedir } from "os";
|
|
@@ -1649,6 +1844,46 @@ function upgradeCommandFor(kind) {
|
|
|
1649
1844
|
function shouldCleanupOldBinary(platform, isStandaloneBinary) {
|
|
1650
1845
|
return platform === "win32" && isStandaloneBinary;
|
|
1651
1846
|
}
|
|
1847
|
+
function codexxShimFiles(platform) {
|
|
1848
|
+
if (platform === "win32") {
|
|
1849
|
+
return [
|
|
1850
|
+
{
|
|
1851
|
+
content: `$ErrorActionPreference = 'Stop'
|
|
1852
|
+
$hoopilot = Join-Path $PSScriptRoot 'hoopilot.exe'
|
|
1853
|
+
& $hoopilot codexx @args
|
|
1854
|
+
exit $LASTEXITCODE
|
|
1855
|
+
`,
|
|
1856
|
+
executable: false,
|
|
1857
|
+
name: "codexx.ps1"
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
content: `@echo off
|
|
1861
|
+
setlocal
|
|
1862
|
+
where pwsh >nul 2>nul
|
|
1863
|
+
if %ERRORLEVEL% EQU 0 (
|
|
1864
|
+
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0codexx.ps1" %*
|
|
1865
|
+
) else (
|
|
1866
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0codexx.ps1" %*
|
|
1867
|
+
)
|
|
1868
|
+
exit /b %ERRORLEVEL%
|
|
1869
|
+
`,
|
|
1870
|
+
executable: false,
|
|
1871
|
+
name: "codexx.cmd"
|
|
1872
|
+
}
|
|
1873
|
+
];
|
|
1874
|
+
}
|
|
1875
|
+
return [
|
|
1876
|
+
{
|
|
1877
|
+
content: `#!/bin/sh
|
|
1878
|
+
set -eu
|
|
1879
|
+
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
|
1880
|
+
exec "$script_dir/hoopilot" codexx "$@"
|
|
1881
|
+
`,
|
|
1882
|
+
executable: true,
|
|
1883
|
+
name: "codexx"
|
|
1884
|
+
}
|
|
1885
|
+
];
|
|
1886
|
+
}
|
|
1652
1887
|
function formatUpdateNotice(current, latest, kind) {
|
|
1653
1888
|
return `
|
|
1654
1889
|
Update available for hoopilot: ${current} \u2192 ${latest}
|
|
@@ -1958,6 +2193,23 @@ function swapBinary(tmpFile, exePath) {
|
|
|
1958
2193
|
}
|
|
1959
2194
|
}
|
|
1960
2195
|
}
|
|
2196
|
+
function refreshCodexxShim(dir, logger) {
|
|
2197
|
+
try {
|
|
2198
|
+
for (const file of codexxShimFiles(process.platform)) {
|
|
2199
|
+
const path = join2(dir, file.name);
|
|
2200
|
+
writeFileSync2(path, file.content, "utf8");
|
|
2201
|
+
if (file.executable) {
|
|
2202
|
+
chmodSync2(path, 493);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
logger?.warn(
|
|
2207
|
+
{ err: errorDetails(error), event: "update.codexx_shim_failed" },
|
|
2208
|
+
"could not refresh codexx shim"
|
|
2209
|
+
);
|
|
2210
|
+
console.warn(`Updated hoopilot, but could not refresh the codexx shim: ${errorMessage2(error)}`);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
1961
2213
|
function cleanupOldBinary() {
|
|
1962
2214
|
if (!shouldCleanupOldBinary(process.platform, IS_STANDALONE_BINARY)) {
|
|
1963
2215
|
return;
|
|
@@ -2016,6 +2268,7 @@ async function runUpdate(currentVersion, logger) {
|
|
|
2016
2268
|
chmodSync2(tmpFile, 493);
|
|
2017
2269
|
}
|
|
2018
2270
|
swapBinary(tmpFile, exePath);
|
|
2271
|
+
refreshCodexxShim(dirname2(exePath), logger);
|
|
2019
2272
|
} catch (error) {
|
|
2020
2273
|
const code = error.code;
|
|
2021
2274
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -2039,9 +2292,12 @@ async function runUpdate(currentVersion, logger) {
|
|
|
2039
2292
|
console.log("Restart hoopilot to run the new version.");
|
|
2040
2293
|
}
|
|
2041
2294
|
}
|
|
2295
|
+
function errorMessage2(error) {
|
|
2296
|
+
return error instanceof Error ? error.message : String(error);
|
|
2297
|
+
}
|
|
2042
2298
|
|
|
2043
2299
|
// src/cli.ts
|
|
2044
|
-
async function
|
|
2300
|
+
async function main2(argv = Bun.argv.slice(2)) {
|
|
2045
2301
|
cleanupOldBinary();
|
|
2046
2302
|
const command = argv[0];
|
|
2047
2303
|
if (command === "update" || command === "upgrade") {
|
|
@@ -2054,6 +2310,10 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
2054
2310
|
await runUpdate(await getVersion(), logger2);
|
|
2055
2311
|
return;
|
|
2056
2312
|
}
|
|
2313
|
+
if (command === "codexx") {
|
|
2314
|
+
await main(argv.slice(1), process.env);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2057
2317
|
if (command === "login") {
|
|
2058
2318
|
const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
|
|
2059
2319
|
if (args2.help) {
|
|
@@ -2139,7 +2399,7 @@ function parseArgs(argv) {
|
|
|
2139
2399
|
args.noUpdateCheck = true;
|
|
2140
2400
|
continue;
|
|
2141
2401
|
}
|
|
2142
|
-
const [name, inlineValue] = arg
|
|
2402
|
+
const [name, inlineValue] = splitOption(arg);
|
|
2143
2403
|
const value = inlineValue ?? rest.shift();
|
|
2144
2404
|
if (!value) {
|
|
2145
2405
|
throw new Error(`Missing value for ${arg}.`);
|
|
@@ -2148,6 +2408,9 @@ function parseArgs(argv) {
|
|
|
2148
2408
|
case "--api-key":
|
|
2149
2409
|
args.apiKey = value;
|
|
2150
2410
|
break;
|
|
2411
|
+
case "--api-key-file":
|
|
2412
|
+
args.apiKey = readApiKeyFile(value);
|
|
2413
|
+
break;
|
|
2151
2414
|
case "--auth-file":
|
|
2152
2415
|
args.authStorePath = value;
|
|
2153
2416
|
break;
|
|
@@ -2166,7 +2429,7 @@ function parseArgs(argv) {
|
|
|
2166
2429
|
case "--port":
|
|
2167
2430
|
case "-p":
|
|
2168
2431
|
args.port = Number(value);
|
|
2169
|
-
if (!Number.isInteger(args.port) || args.port <= 0) {
|
|
2432
|
+
if (!Number.isInteger(args.port) || args.port <= 0 || args.port > 65535) {
|
|
2170
2433
|
throw new Error(`Invalid port: ${value}.`);
|
|
2171
2434
|
}
|
|
2172
2435
|
break;
|
|
@@ -2176,6 +2439,20 @@ function parseArgs(argv) {
|
|
|
2176
2439
|
}
|
|
2177
2440
|
return args;
|
|
2178
2441
|
}
|
|
2442
|
+
function splitOption(arg) {
|
|
2443
|
+
const separator = arg.indexOf("=");
|
|
2444
|
+
if (separator === -1) {
|
|
2445
|
+
return [arg, void 0];
|
|
2446
|
+
}
|
|
2447
|
+
return [arg.slice(0, separator), arg.slice(separator + 1)];
|
|
2448
|
+
}
|
|
2449
|
+
function readApiKeyFile(path) {
|
|
2450
|
+
const value = readFileSync2(path, "utf8").trim();
|
|
2451
|
+
if (!value) {
|
|
2452
|
+
throw new Error(`API key file is empty: ${path}.`);
|
|
2453
|
+
}
|
|
2454
|
+
return value;
|
|
2455
|
+
}
|
|
2179
2456
|
async function runLogin(options = {}) {
|
|
2180
2457
|
const logger = options.logger?.child({ component: "auth" }) ?? noopLogger;
|
|
2181
2458
|
logger.debug({ event: "auth.login.started" }, "starting github copilot browser login");
|
|
@@ -2314,6 +2591,9 @@ async function verifyCopilotOAuthToken(token, options = {}) {
|
|
|
2314
2591
|
const apiBaseUrl = trimTrailingSlash(
|
|
2315
2592
|
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2316
2593
|
);
|
|
2594
|
+
if (!isHttpsOrLoopbackUrl(apiBaseUrl)) {
|
|
2595
|
+
throw new Error(`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${apiBaseUrl}`);
|
|
2596
|
+
}
|
|
2317
2597
|
const fetcher = options.fetch ?? fetch;
|
|
2318
2598
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
2319
2599
|
headers: applyCopilotHeaders(new Headers(), token),
|
|
@@ -2378,6 +2658,7 @@ OpenAI-compatible proxy for GitHub Copilot.
|
|
|
2378
2658
|
|
|
2379
2659
|
Usage:
|
|
2380
2660
|
hoopilot [serve] [options]
|
|
2661
|
+
hoopilot codexx [codex options] [prompt]
|
|
2381
2662
|
hoopilot login [options]
|
|
2382
2663
|
hoopilot models [options]
|
|
2383
2664
|
hoopilot usage [options]
|
|
@@ -2386,6 +2667,7 @@ Usage:
|
|
|
2386
2667
|
|
|
2387
2668
|
Commands:
|
|
2388
2669
|
serve Start the proxy server (default)
|
|
2670
|
+
codexx Run Codex through the local Hoopilot server
|
|
2389
2671
|
login Sign in through GitHub OAuth in a browser and verify Copilot access
|
|
2390
2672
|
models List available GitHub Copilot model IDs
|
|
2391
2673
|
usage Show GitHub Copilot quota and premium-request usage
|
|
@@ -2399,6 +2681,7 @@ Options:
|
|
|
2399
2681
|
-p, --port <port> Port to listen on. Default: 4141
|
|
2400
2682
|
--host <host> Host to listen on. Default: 127.0.0.1
|
|
2401
2683
|
--api-key <key> Require clients to send Authorization: Bearer <key> or x-api-key: <key>
|
|
2684
|
+
--api-key-file <path> Read the local API key from a file instead of argv
|
|
2402
2685
|
--auth-file <path> OAuth credential store path
|
|
2403
2686
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
2404
2687
|
--log-level <level> trace, debug, info, warn, error, fatal, or silent
|
|
@@ -2421,13 +2704,13 @@ Environment:
|
|
|
2421
2704
|
`;
|
|
2422
2705
|
}
|
|
2423
2706
|
if (import.meta.main) {
|
|
2424
|
-
|
|
2707
|
+
main2().catch((error) => {
|
|
2425
2708
|
console.error(error instanceof Error ? error.message : String(error));
|
|
2426
2709
|
process.exit(1);
|
|
2427
2710
|
});
|
|
2428
2711
|
}
|
|
2429
2712
|
export {
|
|
2430
|
-
main,
|
|
2713
|
+
main2 as main,
|
|
2431
2714
|
parseArgs,
|
|
2432
2715
|
runModels,
|
|
2433
2716
|
runUsage,
|