@openhoo/hoopilot 0.6.0 → 0.6.1
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 -16
- package/dist/cli.js +102 -121
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/codexx.js.map +1 -1
- package/dist/index.cjs +81 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +82 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,20 +97,7 @@ $env:OPENAI_BASE_URL = "http://127.0.0.1:4141/v1"
|
|
|
97
97
|
$env:OPENAI_API_KEY = "local-key"
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Use with Codex CLI after Hoopilot is running:
|
|
101
|
-
|
|
102
|
-
```powershell
|
|
103
|
-
$env:OPENAI_API_KEY = "local-key"
|
|
104
|
-
codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
One-line PowerShell form:
|
|
108
|
-
|
|
109
|
-
```powershell
|
|
110
|
-
$env:OPENAI_API_KEY = "local-key"; codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Or use the bundled `codexx` convenience command after Hoopilot is already running:
|
|
100
|
+
Use with Codex CLI after Hoopilot is running, via the bundled `codexx` command. It runs Codex against the local server with the right model provider — selecting `gpt-5.5` over Copilot's Responses API, which a plain `openai_base_url` override does not configure (see the note below):
|
|
114
101
|
|
|
115
102
|
```powershell
|
|
116
103
|
$env:HOOPILOT_API_KEY = "local-key"
|
|
@@ -203,7 +190,7 @@ Then, in another PowerShell session:
|
|
|
203
190
|
$env:OPENAI_API_KEY = "local-key"
|
|
204
191
|
Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
|
|
205
192
|
http://127.0.0.1:4141/v1/models
|
|
206
|
-
|
|
193
|
+
codexx
|
|
207
194
|
```
|
|
208
195
|
|
|
209
196
|
If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` and confirm the GitHub account has active Copilot access.
|
|
@@ -230,7 +217,7 @@ Options:
|
|
|
230
217
|
```txt
|
|
231
218
|
-p, --port <port> Port to listen on. Default: 4141
|
|
232
219
|
--host <host> Host to listen on. Default: 127.0.0.1
|
|
233
|
-
--api-key <key> Require clients to send Authorization: Bearer <key>
|
|
220
|
+
--api-key <key> Require clients to send Authorization: Bearer <key> or x-api-key: <key>
|
|
234
221
|
--auth-file <path> OAuth credential store path
|
|
235
222
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
236
223
|
--log-level <level> trace, debug, info, warn, error, fatal, or silent
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
|
|
6
6
|
// src/auth-store.ts
|
|
7
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
8
8
|
import { dirname, join } from "path";
|
|
9
9
|
function authStorePath(env = process.env) {
|
|
10
10
|
if (env.HOOPILOT_AUTH_FILE) {
|
|
@@ -36,25 +36,36 @@ function readStoredCopilotAuth(path = authStorePath()) {
|
|
|
36
36
|
}
|
|
37
37
|
function writeStoredCopilotAuth(auth, path = authStorePath()) {
|
|
38
38
|
mkdirSync(dirname(path), { recursive: true });
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
39
|
+
const data = `${JSON.stringify(
|
|
40
|
+
{
|
|
41
|
+
...auth,
|
|
42
|
+
createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
)}
|
|
47
|
+
`;
|
|
48
|
+
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
49
|
+
writeFileSync(tmpPath, data, { mode: 384 });
|
|
50
|
+
renameSync(tmpPath, path);
|
|
52
51
|
try {
|
|
53
52
|
chmodSync(path, 384);
|
|
54
53
|
} catch {
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|
|
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
|
+
|
|
58
69
|
// src/auth.ts
|
|
59
70
|
var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
|
|
60
71
|
var REFRESH_SKEW_MS = 6e4;
|
|
@@ -97,11 +108,19 @@ var CopilotAuth = class {
|
|
|
97
108
|
return access;
|
|
98
109
|
}
|
|
99
110
|
};
|
|
100
|
-
function trimTrailingSlash(value) {
|
|
101
|
-
return value.replace(/\/+$/, "");
|
|
102
|
-
}
|
|
103
111
|
|
|
104
112
|
// src/copilot.ts
|
|
113
|
+
function applyCopilotHeaders(headers, token) {
|
|
114
|
+
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
115
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
116
|
+
headers.set("copilot-integration-id", "vscode-chat");
|
|
117
|
+
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
118
|
+
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
119
|
+
headers.set("openai-intent", "conversation-panel");
|
|
120
|
+
headers.set("user-agent", "hoopilot/0.1.0");
|
|
121
|
+
headers.set("x-github-api-version", "2026-06-01");
|
|
122
|
+
return headers;
|
|
123
|
+
}
|
|
105
124
|
var CopilotClient = class {
|
|
106
125
|
#auth;
|
|
107
126
|
#fetch;
|
|
@@ -140,15 +159,7 @@ var CopilotClient = class {
|
|
|
140
159
|
}
|
|
141
160
|
async fetchCopilot(path, init) {
|
|
142
161
|
const access = await this.#auth.getAccess();
|
|
143
|
-
const headers = new Headers(init.headers);
|
|
144
|
-
headers.set("accept", headers.get("accept") ?? "application/json");
|
|
145
|
-
headers.set("authorization", `Bearer ${access.token}`);
|
|
146
|
-
headers.set("copilot-integration-id", "vscode-chat");
|
|
147
|
-
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
148
|
-
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
149
|
-
headers.set("openai-intent", "conversation-panel");
|
|
150
|
-
headers.set("user-agent", "hoopilot/0.1.0");
|
|
151
|
-
headers.set("x-github-api-version", "2026-06-01");
|
|
162
|
+
const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
|
|
152
163
|
return this.#fetch(`${access.apiBaseUrl}${path}`, {
|
|
153
164
|
...init,
|
|
154
165
|
headers
|
|
@@ -162,6 +173,7 @@ var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
|
162
173
|
var DEFAULT_GITHUB_DOMAIN = "github.com";
|
|
163
174
|
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
164
175
|
var POLLING_SAFETY_MARGIN_MS = 3e3;
|
|
176
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
165
177
|
async function githubCopilotDeviceLogin(options = {}) {
|
|
166
178
|
const env = options.env ?? process.env;
|
|
167
179
|
const fetcher = options.fetch ?? fetch;
|
|
@@ -196,16 +208,20 @@ async function requestDeviceCode(fetcher, domain, clientId) {
|
|
|
196
208
|
scope: "read:user"
|
|
197
209
|
}),
|
|
198
210
|
headers: oauthHeaders(),
|
|
199
|
-
method: "POST"
|
|
211
|
+
method: "POST",
|
|
212
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
200
213
|
});
|
|
201
214
|
if (!response.ok) {
|
|
202
215
|
throw new Error(
|
|
203
|
-
`GitHub device authorization failed with ${response.status}: ${await
|
|
216
|
+
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
204
217
|
response
|
|
205
218
|
)}`
|
|
206
219
|
);
|
|
207
220
|
}
|
|
208
|
-
return
|
|
221
|
+
return parseJsonResponse(
|
|
222
|
+
response,
|
|
223
|
+
"GitHub device authorization response was not valid JSON"
|
|
224
|
+
);
|
|
209
225
|
}
|
|
210
226
|
async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
|
|
211
227
|
let intervalMs = device.interval * 1e3 + POLLING_SAFETY_MARGIN_MS;
|
|
@@ -219,16 +235,20 @@ async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
|
|
|
219
235
|
grant_type: DEVICE_GRANT_TYPE
|
|
220
236
|
}),
|
|
221
237
|
headers: oauthHeaders(),
|
|
222
|
-
method: "POST"
|
|
238
|
+
method: "POST",
|
|
239
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
223
240
|
});
|
|
224
241
|
if (!response.ok) {
|
|
225
242
|
throw new Error(
|
|
226
|
-
`GitHub device token exchange failed with ${response.status}: ${await
|
|
243
|
+
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
227
244
|
response
|
|
228
245
|
)}`
|
|
229
246
|
);
|
|
230
247
|
}
|
|
231
|
-
const data = await
|
|
248
|
+
const data = await parseJsonResponse(
|
|
249
|
+
response,
|
|
250
|
+
"GitHub device token response was not valid JSON"
|
|
251
|
+
);
|
|
232
252
|
if (data.access_token) {
|
|
233
253
|
return data.access_token;
|
|
234
254
|
}
|
|
@@ -264,9 +284,13 @@ function normalizeDomain(value) {
|
|
|
264
284
|
function positiveSeconds(value, fallback) {
|
|
265
285
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
266
286
|
}
|
|
267
|
-
async function
|
|
287
|
+
async function parseJsonResponse(response, context) {
|
|
268
288
|
const text = await response.text();
|
|
269
|
-
|
|
289
|
+
try {
|
|
290
|
+
return JSON.parse(text);
|
|
291
|
+
} catch {
|
|
292
|
+
throw new Error(`${context}: ${text.slice(0, 500)}`);
|
|
293
|
+
}
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
// src/logger.ts
|
|
@@ -369,6 +393,16 @@ function shouldCreateLogger(options) {
|
|
|
369
393
|
options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
|
|
370
394
|
);
|
|
371
395
|
}
|
|
396
|
+
function errorDetails(error) {
|
|
397
|
+
if (error instanceof Error) {
|
|
398
|
+
return {
|
|
399
|
+
message: error.message,
|
|
400
|
+
name: error.name,
|
|
401
|
+
stack: error.stack
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return { message: String(error) };
|
|
405
|
+
}
|
|
372
406
|
function isLogFormat(value) {
|
|
373
407
|
return LOG_FORMATS.includes(value);
|
|
374
408
|
}
|
|
@@ -476,9 +510,6 @@ function firstChoice(completion) {
|
|
|
476
510
|
function removeUndefined(record) {
|
|
477
511
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
|
|
478
512
|
}
|
|
479
|
-
function asRecord(value) {
|
|
480
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
481
|
-
}
|
|
482
513
|
function randomId() {
|
|
483
514
|
return crypto.randomUUID().replaceAll("-", "");
|
|
484
515
|
}
|
|
@@ -668,6 +699,9 @@ async function handleCompletions(client, request, logger) {
|
|
|
668
699
|
return proxyError(upstream, logger);
|
|
669
700
|
}
|
|
670
701
|
logUpstreamSuccess(logger, "/chat/completions", upstream.status);
|
|
702
|
+
if (isStreamingResponse(upstream)) {
|
|
703
|
+
return proxyResponse(upstream);
|
|
704
|
+
}
|
|
671
705
|
return jsonResponse(chatCompletionToCompletion(await upstream.json()));
|
|
672
706
|
}
|
|
673
707
|
async function handleResponses(client, request, logger) {
|
|
@@ -710,8 +744,7 @@ function proxyResponse(upstream) {
|
|
|
710
744
|
}
|
|
711
745
|
async function readJson(request) {
|
|
712
746
|
try {
|
|
713
|
-
|
|
714
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
747
|
+
return asRecord(await request.json());
|
|
715
748
|
} catch {
|
|
716
749
|
throw new Error(INVALID_JSON_MESSAGE);
|
|
717
750
|
}
|
|
@@ -882,16 +915,6 @@ function logUpstreamSuccess(logger, upstreamPath, status) {
|
|
|
882
915
|
"copilot upstream request completed"
|
|
883
916
|
);
|
|
884
917
|
}
|
|
885
|
-
function errorDetails(error) {
|
|
886
|
-
if (error instanceof Error) {
|
|
887
|
-
return {
|
|
888
|
-
message: error.message,
|
|
889
|
-
name: error.name,
|
|
890
|
-
stack: error.stack
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
return { message: String(error) };
|
|
894
|
-
}
|
|
895
918
|
|
|
896
919
|
// src/update.ts
|
|
897
920
|
import { execFileSync } from "child_process";
|
|
@@ -902,7 +925,7 @@ import {
|
|
|
902
925
|
existsSync,
|
|
903
926
|
mkdirSync as mkdirSync2,
|
|
904
927
|
realpathSync,
|
|
905
|
-
renameSync,
|
|
928
|
+
renameSync as renameSync2,
|
|
906
929
|
rmSync
|
|
907
930
|
} from "fs";
|
|
908
931
|
import { readFile, writeFile } from "fs/promises";
|
|
@@ -1116,7 +1139,7 @@ async function getVersion() {
|
|
|
1116
1139
|
}
|
|
1117
1140
|
|
|
1118
1141
|
// src/update.ts
|
|
1119
|
-
var
|
|
1142
|
+
var REQUEST_TIMEOUT_MS2 = 8e3;
|
|
1120
1143
|
var SHA256SUMS = "SHA256SUMS";
|
|
1121
1144
|
function userAgent(version) {
|
|
1122
1145
|
return `hoopilot/${version}`;
|
|
@@ -1153,7 +1176,7 @@ async function fetchLatest(version, etag) {
|
|
|
1153
1176
|
}
|
|
1154
1177
|
const response = await fetch(latestReleaseApiUrl(), {
|
|
1155
1178
|
headers,
|
|
1156
|
-
signal: AbortSignal.timeout(
|
|
1179
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
1157
1180
|
});
|
|
1158
1181
|
if (response.status === 304) {
|
|
1159
1182
|
return { status: 304, etag: etag ?? null, release: null };
|
|
@@ -1192,7 +1215,7 @@ async function maybeNotifyUpdate(currentVersion, kind, logger) {
|
|
|
1192
1215
|
logger?.debug({ event: "update.check.refresh_queued" }, "queued update check refresh");
|
|
1193
1216
|
void refreshState(currentVersion, state.etag ?? null, logger).catch((error) => {
|
|
1194
1217
|
logger?.debug(
|
|
1195
|
-
{ err:
|
|
1218
|
+
{ err: errorDetails(error), event: "update.check.refresh_failed" },
|
|
1196
1219
|
"update check refresh failed"
|
|
1197
1220
|
);
|
|
1198
1221
|
});
|
|
@@ -1254,7 +1277,7 @@ async function downloadToFile(url, dest, version) {
|
|
|
1254
1277
|
const response = await fetch(url, {
|
|
1255
1278
|
headers: { "User-Agent": userAgent(version) },
|
|
1256
1279
|
redirect: "follow",
|
|
1257
|
-
signal: AbortSignal.timeout(
|
|
1280
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2 * 10)
|
|
1258
1281
|
});
|
|
1259
1282
|
if (!response.ok || !response.body) {
|
|
1260
1283
|
throw new Error(`Download failed (${response.status}) for ${url}`);
|
|
@@ -1274,7 +1297,7 @@ async function verifyChecksum(release, assetName, file, version) {
|
|
|
1274
1297
|
const response = await fetch(sums.url, {
|
|
1275
1298
|
headers: { "User-Agent": userAgent(version) },
|
|
1276
1299
|
redirect: "follow",
|
|
1277
|
-
signal: AbortSignal.timeout(
|
|
1300
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
1278
1301
|
});
|
|
1279
1302
|
if (!response.ok) {
|
|
1280
1303
|
throw new Error(`Could not download ${SHA256SUMS} (${response.status}).`);
|
|
@@ -1295,15 +1318,15 @@ function swapBinary(tmpFile, exePath) {
|
|
|
1295
1318
|
rmSync(oldExe, { force: true });
|
|
1296
1319
|
} catch {
|
|
1297
1320
|
}
|
|
1298
|
-
|
|
1321
|
+
renameSync2(exePath, oldExe);
|
|
1299
1322
|
const restore = () => {
|
|
1300
1323
|
try {
|
|
1301
|
-
|
|
1324
|
+
renameSync2(oldExe, exePath);
|
|
1302
1325
|
} catch {
|
|
1303
1326
|
}
|
|
1304
1327
|
};
|
|
1305
1328
|
try {
|
|
1306
|
-
|
|
1329
|
+
renameSync2(tmpFile, exePath);
|
|
1307
1330
|
} catch (error) {
|
|
1308
1331
|
if (error.code === "EXDEV") {
|
|
1309
1332
|
try {
|
|
@@ -1320,7 +1343,7 @@ function swapBinary(tmpFile, exePath) {
|
|
|
1320
1343
|
return;
|
|
1321
1344
|
}
|
|
1322
1345
|
try {
|
|
1323
|
-
|
|
1346
|
+
renameSync2(tmpFile, exePath);
|
|
1324
1347
|
} catch (error) {
|
|
1325
1348
|
const code = error.code;
|
|
1326
1349
|
if (code === "EXDEV") {
|
|
@@ -1416,19 +1439,8 @@ async function runUpdate(currentVersion, logger) {
|
|
|
1416
1439
|
console.log("Restart hoopilot to run the new version.");
|
|
1417
1440
|
}
|
|
1418
1441
|
}
|
|
1419
|
-
function errorDetails2(error) {
|
|
1420
|
-
if (error instanceof Error) {
|
|
1421
|
-
return {
|
|
1422
|
-
message: error.message,
|
|
1423
|
-
name: error.name,
|
|
1424
|
-
stack: error.stack
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
return { message: String(error) };
|
|
1428
|
-
}
|
|
1429
1442
|
|
|
1430
1443
|
// src/cli.ts
|
|
1431
|
-
var DEFAULT_COPILOT_API_BASE_URL2 = "https://api.githubcopilot.com";
|
|
1432
1444
|
async function main(argv = Bun.argv.slice(2)) {
|
|
1433
1445
|
cleanupOldBinary();
|
|
1434
1446
|
const command = argv[0];
|
|
@@ -1438,11 +1450,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
1438
1450
|
console.log(helpText(await getVersion()));
|
|
1439
1451
|
return;
|
|
1440
1452
|
}
|
|
1441
|
-
const logger2 =
|
|
1442
|
-
env: args2.env,
|
|
1443
|
-
format: args2.logFormat,
|
|
1444
|
-
level: args2.logLevel
|
|
1445
|
-
}).child({ component: "cli", command });
|
|
1453
|
+
const logger2 = commandLogger(args2, command);
|
|
1446
1454
|
await runUpdate(await getVersion(), logger2);
|
|
1447
1455
|
return;
|
|
1448
1456
|
}
|
|
@@ -1452,11 +1460,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
1452
1460
|
console.log(helpText(await getVersion()));
|
|
1453
1461
|
return;
|
|
1454
1462
|
}
|
|
1455
|
-
args2.logger =
|
|
1456
|
-
env: args2.env,
|
|
1457
|
-
format: args2.logFormat,
|
|
1458
|
-
level: args2.logLevel
|
|
1459
|
-
}).child({ component: "cli", command: "login" });
|
|
1463
|
+
args2.logger = commandLogger(args2, "login");
|
|
1460
1464
|
await runLogin(args2);
|
|
1461
1465
|
return;
|
|
1462
1466
|
}
|
|
@@ -1466,11 +1470,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
1466
1470
|
console.log(helpText(await getVersion()));
|
|
1467
1471
|
return;
|
|
1468
1472
|
}
|
|
1469
|
-
args2.logger =
|
|
1470
|
-
env: args2.env,
|
|
1471
|
-
format: args2.logFormat,
|
|
1472
|
-
level: args2.logLevel
|
|
1473
|
-
}).child({ component: "cli", command: "models" });
|
|
1473
|
+
args2.logger = commandLogger(args2, "models");
|
|
1474
1474
|
await runModels(args2);
|
|
1475
1475
|
return;
|
|
1476
1476
|
}
|
|
@@ -1483,11 +1483,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
1483
1483
|
console.log(await getVersion());
|
|
1484
1484
|
return;
|
|
1485
1485
|
}
|
|
1486
|
-
const logger =
|
|
1487
|
-
env: args.env,
|
|
1488
|
-
format: args.logFormat,
|
|
1489
|
-
level: args.logLevel
|
|
1490
|
-
}).child({ component: "cli", command: "serve" });
|
|
1486
|
+
const logger = commandLogger(args, "serve");
|
|
1491
1487
|
args.logger = logger;
|
|
1492
1488
|
const started = startHoopilotServer(args);
|
|
1493
1489
|
logger.info(
|
|
@@ -1498,7 +1494,7 @@ async function main(argv = Bun.argv.slice(2)) {
|
|
|
1498
1494
|
},
|
|
1499
1495
|
"hoopilot server started"
|
|
1500
1496
|
);
|
|
1501
|
-
if (!args.noUpdateCheck
|
|
1497
|
+
if (!args.noUpdateCheck) {
|
|
1502
1498
|
void maybeNotifyUpdate(
|
|
1503
1499
|
await getVersion(),
|
|
1504
1500
|
IS_STANDALONE_BINARY ? "binary" : "npm",
|
|
@@ -1604,7 +1600,7 @@ async function runModels(options = {}) {
|
|
|
1604
1600
|
logger.debug({ event: "models.list.started" }, "fetching github copilot models");
|
|
1605
1601
|
const response = await new CopilotClient(options).models();
|
|
1606
1602
|
if (!response.ok) {
|
|
1607
|
-
const message = `GitHub Copilot API model list failed with ${response.status}: ${await
|
|
1603
|
+
const message = `GitHub Copilot API model list failed with ${response.status}: ${await truncatedResponseText(response)}`;
|
|
1608
1604
|
if (response.status === 401 || response.status === 403) {
|
|
1609
1605
|
throw new CopilotAuthError(message);
|
|
1610
1606
|
}
|
|
@@ -1624,16 +1620,16 @@ async function runModels(options = {}) {
|
|
|
1624
1620
|
return ids;
|
|
1625
1621
|
}
|
|
1626
1622
|
async function verifyCopilotOAuthToken(token, options = {}) {
|
|
1627
|
-
const apiBaseUrl =
|
|
1628
|
-
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ??
|
|
1623
|
+
const apiBaseUrl = trimTrailingSlash(
|
|
1624
|
+
options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
|
|
1629
1625
|
);
|
|
1630
1626
|
const fetcher = options.fetch ?? fetch;
|
|
1631
1627
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
1632
|
-
headers:
|
|
1628
|
+
headers: applyCopilotHeaders(new Headers(), token),
|
|
1633
1629
|
method: "GET"
|
|
1634
1630
|
});
|
|
1635
1631
|
if (!response.ok) {
|
|
1636
|
-
const message = `GitHub Copilot API verification failed with ${response.status}: ${await
|
|
1632
|
+
const message = `GitHub Copilot API verification failed with ${response.status}: ${await truncatedResponseText(response)}`;
|
|
1637
1633
|
if (response.status === 401 || response.status === 403) {
|
|
1638
1634
|
throw new CopilotAuthError(message);
|
|
1639
1635
|
}
|
|
@@ -1659,32 +1655,13 @@ function openBrowserBestEffort(url) {
|
|
|
1659
1655
|
} catch {
|
|
1660
1656
|
}
|
|
1661
1657
|
}
|
|
1662
|
-
function copilotHeaders(token) {
|
|
1663
|
-
const headers = new Headers();
|
|
1664
|
-
headers.set("accept", "application/json");
|
|
1665
|
-
headers.set("authorization", `Bearer ${token}`);
|
|
1666
|
-
headers.set("copilot-integration-id", "vscode-chat");
|
|
1667
|
-
headers.set("editor-plugin-version", "hoopilot/0.1.0");
|
|
1668
|
-
headers.set("editor-version", "Hoopilot/0.1.0");
|
|
1669
|
-
headers.set("openai-intent", "conversation-panel");
|
|
1670
|
-
headers.set("user-agent", "hoopilot/0.1.0");
|
|
1671
|
-
headers.set("x-github-api-version", "2026-06-01");
|
|
1672
|
-
return headers;
|
|
1673
|
-
}
|
|
1674
|
-
async function safeResponseText2(response) {
|
|
1675
|
-
const text = await response.text();
|
|
1676
|
-
return text.slice(0, 500);
|
|
1677
|
-
}
|
|
1678
|
-
function trimTrailingSlash2(value) {
|
|
1679
|
-
return value.replace(/\/+$/, "");
|
|
1680
|
-
}
|
|
1681
1658
|
function modelIdsFromResponse(body) {
|
|
1682
|
-
const record =
|
|
1659
|
+
const record = asRecord(body);
|
|
1683
1660
|
const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];
|
|
1684
1661
|
const seen = /* @__PURE__ */ new Set();
|
|
1685
1662
|
const ids = [];
|
|
1686
1663
|
for (const model of data) {
|
|
1687
|
-
const id =
|
|
1664
|
+
const id = asRecord(model).id;
|
|
1688
1665
|
if (typeof id !== "string" || id.length === 0 || seen.has(id)) {
|
|
1689
1666
|
continue;
|
|
1690
1667
|
}
|
|
@@ -1693,12 +1670,16 @@ function modelIdsFromResponse(body) {
|
|
|
1693
1670
|
}
|
|
1694
1671
|
return ids;
|
|
1695
1672
|
}
|
|
1696
|
-
function asRecord2(value) {
|
|
1697
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1698
|
-
}
|
|
1699
1673
|
function withRuntimeEnv(args) {
|
|
1700
1674
|
return { ...args, env: process.env };
|
|
1701
1675
|
}
|
|
1676
|
+
function commandLogger(args, command) {
|
|
1677
|
+
return createHoopilotLogger({
|
|
1678
|
+
env: args.env,
|
|
1679
|
+
format: args.logFormat,
|
|
1680
|
+
level: args.logLevel
|
|
1681
|
+
}).child({ command, component: "cli" });
|
|
1682
|
+
}
|
|
1702
1683
|
function helpText(version) {
|
|
1703
1684
|
return `hoopilot ${version}
|
|
1704
1685
|
|
|
@@ -1720,7 +1701,7 @@ Commands:
|
|
|
1720
1701
|
Options:
|
|
1721
1702
|
-p, --port <port> Port to listen on. Default: 4141
|
|
1722
1703
|
--host <host> Host to listen on. Default: 127.0.0.1
|
|
1723
|
-
--api-key <key> Require clients to send Authorization: Bearer <key>
|
|
1704
|
+
--api-key <key> Require clients to send Authorization: Bearer <key> or x-api-key: <key>
|
|
1724
1705
|
--auth-file <path> OAuth credential store path
|
|
1725
1706
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
1726
1707
|
--log-level <level> trace, debug, info, warn, error, fatal, or silent
|