@openhoo/hoopilot 0.7.1 → 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 +321 -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 +360 -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 +359 -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
|
}
|
|
@@ -1123,7 +1273,7 @@ function createHoopilotHandler(options = {}) {
|
|
|
1123
1273
|
}
|
|
1124
1274
|
function startHoopilotServer(options = {}) {
|
|
1125
1275
|
const host = options.host ?? options.env?.HOST ?? DEFAULT_HOST;
|
|
1126
|
-
const port =
|
|
1276
|
+
const port = normalizeServerPort(options.port ?? options.env?.PORT ?? DEFAULT_PORT);
|
|
1127
1277
|
const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
|
|
1128
1278
|
const allowUnauthenticated = options.allowUnauthenticated ?? options.env?.HOOPILOT_ALLOW_UNAUTHENTICATED === "1";
|
|
1129
1279
|
if (!isLoopbackHost(host) && !apiKey && !allowUnauthenticated) {
|
|
@@ -1189,8 +1339,19 @@ async function handleCompletions(client, metrics, recordTokens, request, logger)
|
|
|
1189
1339
|
}
|
|
1190
1340
|
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
1191
1341
|
const model = normalizeRequestedModel(body.model);
|
|
1192
|
-
if (isStreamingResponse(upstream)) {
|
|
1193
|
-
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
|
+
);
|
|
1194
1355
|
}
|
|
1195
1356
|
const completion = asRecord(await upstream.json());
|
|
1196
1357
|
const usage = extractTokenUsage(completion.usage);
|
|
@@ -1224,7 +1385,7 @@ async function proxyError(upstream, logger) {
|
|
|
1224
1385
|
{ event: "copilot.request.failed", upstreamStatus: upstream.status },
|
|
1225
1386
|
"copilot upstream request failed"
|
|
1226
1387
|
);
|
|
1227
|
-
return
|
|
1388
|
+
return upstreamErrorResponse(upstream.status, text || upstream.statusText);
|
|
1228
1389
|
}
|
|
1229
1390
|
function proxyResponse(upstream) {
|
|
1230
1391
|
const headers = new Headers(upstream.headers);
|
|
@@ -1277,6 +1438,13 @@ function jsonError(status, code, message) {
|
|
|
1277
1438
|
status
|
|
1278
1439
|
);
|
|
1279
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
|
+
}
|
|
1280
1448
|
function websocketUnsupportedResponse() {
|
|
1281
1449
|
const response = jsonError(
|
|
1282
1450
|
426,
|
|
@@ -1301,6 +1469,17 @@ function isAuthorized(request, apiKey) {
|
|
|
1301
1469
|
const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
1302
1470
|
return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
|
|
1303
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
|
+
}
|
|
1304
1483
|
function isUpstreamAuthStatus(status) {
|
|
1305
1484
|
return status === 401 || status === 403;
|
|
1306
1485
|
}
|
|
@@ -1308,7 +1487,21 @@ function upstreamAuthMessage(message) {
|
|
|
1308
1487
|
return `GitHub Copilot rejected the credential or account access: ${message}`;
|
|
1309
1488
|
}
|
|
1310
1489
|
function isLoopbackHost(host) {
|
|
1311
|
-
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;
|
|
1312
1505
|
}
|
|
1313
1506
|
function errorMessage(error) {
|
|
1314
1507
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1527,7 +1720,8 @@ import {
|
|
|
1527
1720
|
mkdirSync as mkdirSync2,
|
|
1528
1721
|
realpathSync,
|
|
1529
1722
|
renameSync as renameSync2,
|
|
1530
|
-
rmSync
|
|
1723
|
+
rmSync,
|
|
1724
|
+
writeFileSync as writeFileSync2
|
|
1531
1725
|
} from "fs";
|
|
1532
1726
|
import { readFile, writeFile } from "fs/promises";
|
|
1533
1727
|
import { homedir } from "os";
|
|
@@ -1650,6 +1844,46 @@ function upgradeCommandFor(kind) {
|
|
|
1650
1844
|
function shouldCleanupOldBinary(platform, isStandaloneBinary) {
|
|
1651
1845
|
return platform === "win32" && isStandaloneBinary;
|
|
1652
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
|
+
}
|
|
1653
1887
|
function formatUpdateNotice(current, latest, kind) {
|
|
1654
1888
|
return `
|
|
1655
1889
|
Update available for hoopilot: ${current} \u2192 ${latest}
|
|
@@ -1959,6 +2193,23 @@ function swapBinary(tmpFile, exePath) {
|
|
|
1959
2193
|
}
|
|
1960
2194
|
}
|
|
1961
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
|
+
}
|
|
1962
2213
|
function cleanupOldBinary() {
|
|
1963
2214
|
if (!shouldCleanupOldBinary(process.platform, IS_STANDALONE_BINARY)) {
|
|
1964
2215
|
return;
|
|
@@ -2017,6 +2268,7 @@ async function runUpdate(currentVersion, logger) {
|
|
|
2017
2268
|
chmodSync2(tmpFile, 493);
|
|
2018
2269
|
}
|
|
2019
2270
|
swapBinary(tmpFile, exePath);
|
|
2271
|
+
refreshCodexxShim(dirname2(exePath), logger);
|
|
2020
2272
|
} catch (error) {
|
|
2021
2273
|
const code = error.code;
|
|
2022
2274
|
if (code === "EACCES" || code === "EPERM") {
|
|
@@ -2040,9 +2292,12 @@ async function runUpdate(currentVersion, logger) {
|
|
|
2040
2292
|
console.log("Restart hoopilot to run the new version.");
|
|
2041
2293
|
}
|
|
2042
2294
|
}
|
|
2295
|
+
function errorMessage2(error) {
|
|
2296
|
+
return error instanceof Error ? error.message : String(error);
|
|
2297
|
+
}
|
|
2043
2298
|
|
|
2044
2299
|
// src/cli.ts
|
|
2045
|
-
async function
|
|
2300
|
+
async function main2(argv = Bun.argv.slice(2)) {
|
|
2046
2301
|
cleanupOldBinary();
|
|
2047
2302
|
const command = argv[0];
|
|
2048
2303
|
if (command === "update" || command === "upgrade") {
|
|
@@ -2055,6 +2310,10 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
2055
2310
|
await runUpdate(await getVersion(), logger2);
|
|
2056
2311
|
return;
|
|
2057
2312
|
}
|
|
2313
|
+
if (command === "codexx") {
|
|
2314
|
+
await main(argv.slice(1), process.env);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2058
2317
|
if (command === "login") {
|
|
2059
2318
|
const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
|
|
2060
2319
|
if (args2.help) {
|
|
@@ -2140,7 +2399,7 @@ function parseArgs(argv) {
|
|
|
2140
2399
|
args.noUpdateCheck = true;
|
|
2141
2400
|
continue;
|
|
2142
2401
|
}
|
|
2143
|
-
const [name, inlineValue] = arg
|
|
2402
|
+
const [name, inlineValue] = splitOption(arg);
|
|
2144
2403
|
const value = inlineValue ?? rest.shift();
|
|
2145
2404
|
if (!value) {
|
|
2146
2405
|
throw new Error(`Missing value for ${arg}.`);
|
|
@@ -2149,6 +2408,9 @@ function parseArgs(argv) {
|
|
|
2149
2408
|
case "--api-key":
|
|
2150
2409
|
args.apiKey = value;
|
|
2151
2410
|
break;
|
|
2411
|
+
case "--api-key-file":
|
|
2412
|
+
args.apiKey = readApiKeyFile(value);
|
|
2413
|
+
break;
|
|
2152
2414
|
case "--auth-file":
|
|
2153
2415
|
args.authStorePath = value;
|
|
2154
2416
|
break;
|
|
@@ -2167,7 +2429,7 @@ function parseArgs(argv) {
|
|
|
2167
2429
|
case "--port":
|
|
2168
2430
|
case "-p":
|
|
2169
2431
|
args.port = Number(value);
|
|
2170
|
-
if (!Number.isInteger(args.port) || args.port <= 0) {
|
|
2432
|
+
if (!Number.isInteger(args.port) || args.port <= 0 || args.port > 65535) {
|
|
2171
2433
|
throw new Error(`Invalid port: ${value}.`);
|
|
2172
2434
|
}
|
|
2173
2435
|
break;
|
|
@@ -2177,6 +2439,20 @@ function parseArgs(argv) {
|
|
|
2177
2439
|
}
|
|
2178
2440
|
return args;
|
|
2179
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
|
+
}
|
|
2180
2456
|
async function runLogin(options = {}) {
|
|
2181
2457
|
const logger = options.logger?.child({ component: "auth" }) ?? noopLogger;
|
|
2182
2458
|
logger.debug({ event: "auth.login.started" }, "starting github copilot browser login");
|
|
@@ -2315,6 +2591,9 @@ async function verifyCopilotOAuthToken(token, options = {}) {
|
|
|
2315
2591
|
const apiBaseUrl = trimTrailingSlash(
|
|
2316
2592
|
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
2317
2593
|
);
|
|
2594
|
+
if (!isHttpsOrLoopbackUrl(apiBaseUrl)) {
|
|
2595
|
+
throw new Error(`Refusing to send the GitHub OAuth token to a non-HTTPS host: ${apiBaseUrl}`);
|
|
2596
|
+
}
|
|
2318
2597
|
const fetcher = options.fetch ?? fetch;
|
|
2319
2598
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
2320
2599
|
headers: applyCopilotHeaders(new Headers(), token),
|
|
@@ -2379,6 +2658,7 @@ OpenAI-compatible proxy for GitHub Copilot.
|
|
|
2379
2658
|
|
|
2380
2659
|
Usage:
|
|
2381
2660
|
hoopilot [serve] [options]
|
|
2661
|
+
hoopilot codexx [codex options] [prompt]
|
|
2382
2662
|
hoopilot login [options]
|
|
2383
2663
|
hoopilot models [options]
|
|
2384
2664
|
hoopilot usage [options]
|
|
@@ -2387,6 +2667,7 @@ Usage:
|
|
|
2387
2667
|
|
|
2388
2668
|
Commands:
|
|
2389
2669
|
serve Start the proxy server (default)
|
|
2670
|
+
codexx Run Codex through the local Hoopilot server
|
|
2390
2671
|
login Sign in through GitHub OAuth in a browser and verify Copilot access
|
|
2391
2672
|
models List available GitHub Copilot model IDs
|
|
2392
2673
|
usage Show GitHub Copilot quota and premium-request usage
|
|
@@ -2400,6 +2681,7 @@ Options:
|
|
|
2400
2681
|
-p, --port <port> Port to listen on. Default: 4141
|
|
2401
2682
|
--host <host> Host to listen on. Default: 127.0.0.1
|
|
2402
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
|
|
2403
2685
|
--auth-file <path> OAuth credential store path
|
|
2404
2686
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
2405
2687
|
--log-level <level> trace, debug, info, warn, error, fatal, or silent
|
|
@@ -2422,13 +2704,13 @@ Environment:
|
|
|
2422
2704
|
`;
|
|
2423
2705
|
}
|
|
2424
2706
|
if (import.meta.main) {
|
|
2425
|
-
|
|
2707
|
+
main2().catch((error) => {
|
|
2426
2708
|
console.error(error instanceof Error ? error.message : String(error));
|
|
2427
2709
|
process.exit(1);
|
|
2428
2710
|
});
|
|
2429
2711
|
}
|
|
2430
2712
|
export {
|
|
2431
|
-
main,
|
|
2713
|
+
main2 as main,
|
|
2432
2714
|
parseArgs,
|
|
2433
2715
|
runModels,
|
|
2434
2716
|
runUsage,
|