@openhoo/hoopilot 0.5.8 → 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 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
- codex -m gpt-5.5 -c 'model_reasoning_effort="xhigh"' -c 'openai_base_url="http://127.0.0.1:4141/v1"'
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.
@@ -213,6 +200,7 @@ If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` an
213
200
  ```powershell
214
201
  hoopilot [serve] [options]
215
202
  hoopilot login [options]
203
+ hoopilot models [options]
216
204
  ```
217
205
 
218
206
  Commands:
@@ -220,6 +208,7 @@ Commands:
220
208
  ```txt
221
209
  serve Start the proxy server (default)
222
210
  login Sign in through GitHub OAuth in a browser and verify Copilot access
211
+ models List available GitHub Copilot model IDs
223
212
  update, upgrade Update hoopilot to the latest release
224
213
  ```
225
214
 
@@ -228,7 +217,7 @@ Options:
228
217
  ```txt
229
218
  -p, --port <port> Port to listen on. Default: 4141
230
219
  --host <host> Host to listen on. Default: 127.0.0.1
231
- --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>
232
221
  --auth-file <path> OAuth credential store path
233
222
  --copilot-api-base-url <url> Copilot API base URL override
234
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
- writeFileSync(
40
- path,
41
- `${JSON.stringify(
42
- {
43
- ...auth,
44
- createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
45
- },
46
- null,
47
- 2
48
- )}
49
- `,
50
- { mode: 384 }
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,9 +108,64 @@ var CopilotAuth = class {
97
108
  return access;
98
109
  }
99
110
  };
100
- function trimTrailingSlash(value) {
101
- return value.replace(/\/+$/, "");
111
+
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;
102
123
  }
124
+ var CopilotClient = class {
125
+ #auth;
126
+ #fetch;
127
+ constructor(options = {}) {
128
+ this.#auth = new CopilotAuth(options);
129
+ this.#fetch = options.fetch ?? fetch;
130
+ }
131
+ async chatCompletions(body, signal) {
132
+ return this.fetchCopilot("/chat/completions", {
133
+ body: JSON.stringify(body),
134
+ headers: {
135
+ "content-type": "application/json"
136
+ },
137
+ method: "POST",
138
+ signal
139
+ });
140
+ }
141
+ async responses(body, signal) {
142
+ return this.fetchCopilot("/responses", {
143
+ body,
144
+ headers: {
145
+ "content-type": "application/json"
146
+ },
147
+ method: "POST",
148
+ signal
149
+ });
150
+ }
151
+ async models(signal) {
152
+ return this.fetchCopilot("/models", {
153
+ headers: {
154
+ accept: "application/json"
155
+ },
156
+ method: "GET",
157
+ signal
158
+ });
159
+ }
160
+ async fetchCopilot(path, init) {
161
+ const access = await this.#auth.getAccess();
162
+ const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
163
+ return this.#fetch(`${access.apiBaseUrl}${path}`, {
164
+ ...init,
165
+ headers
166
+ });
167
+ }
168
+ };
103
169
 
104
170
  // src/github-device.ts
105
171
  import { setTimeout as sleep } from "timers/promises";
@@ -107,6 +173,7 @@ var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
107
173
  var DEFAULT_GITHUB_DOMAIN = "github.com";
108
174
  var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
109
175
  var POLLING_SAFETY_MARGIN_MS = 3e3;
176
+ var REQUEST_TIMEOUT_MS = 15e3;
110
177
  async function githubCopilotDeviceLogin(options = {}) {
111
178
  const env = options.env ?? process.env;
112
179
  const fetcher = options.fetch ?? fetch;
@@ -141,16 +208,20 @@ async function requestDeviceCode(fetcher, domain, clientId) {
141
208
  scope: "read:user"
142
209
  }),
143
210
  headers: oauthHeaders(),
144
- method: "POST"
211
+ method: "POST",
212
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
145
213
  });
146
214
  if (!response.ok) {
147
215
  throw new Error(
148
- `GitHub device authorization failed with ${response.status}: ${await safeResponseText(
216
+ `GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
149
217
  response
150
218
  )}`
151
219
  );
152
220
  }
153
- return await response.json();
221
+ return parseJsonResponse(
222
+ response,
223
+ "GitHub device authorization response was not valid JSON"
224
+ );
154
225
  }
155
226
  async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
156
227
  let intervalMs = device.interval * 1e3 + POLLING_SAFETY_MARGIN_MS;
@@ -164,16 +235,20 @@ async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
164
235
  grant_type: DEVICE_GRANT_TYPE
165
236
  }),
166
237
  headers: oauthHeaders(),
167
- method: "POST"
238
+ method: "POST",
239
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
168
240
  });
169
241
  if (!response.ok) {
170
242
  throw new Error(
171
- `GitHub device token exchange failed with ${response.status}: ${await safeResponseText(
243
+ `GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
172
244
  response
173
245
  )}`
174
246
  );
175
247
  }
176
- const data = await response.json();
248
+ const data = await parseJsonResponse(
249
+ response,
250
+ "GitHub device token response was not valid JSON"
251
+ );
177
252
  if (data.access_token) {
178
253
  return data.access_token;
179
254
  }
@@ -209,9 +284,13 @@ function normalizeDomain(value) {
209
284
  function positiveSeconds(value, fallback) {
210
285
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
211
286
  }
212
- async function safeResponseText(response) {
287
+ async function parseJsonResponse(response, context) {
213
288
  const text = await response.text();
214
- return text.slice(0, 500);
289
+ try {
290
+ return JSON.parse(text);
291
+ } catch {
292
+ throw new Error(`${context}: ${text.slice(0, 500)}`);
293
+ }
215
294
  }
216
295
 
217
296
  // src/logger.ts
@@ -314,6 +393,16 @@ function shouldCreateLogger(options) {
314
393
  options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
315
394
  );
316
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
+ }
317
406
  function isLogFormat(value) {
318
407
  return LOG_FORMATS.includes(value);
319
408
  }
@@ -321,61 +410,6 @@ function isLogLevel(value) {
321
410
  return LOG_LEVELS.includes(value);
322
411
  }
323
412
 
324
- // src/copilot.ts
325
- var CopilotClient = class {
326
- #auth;
327
- #fetch;
328
- constructor(options = {}) {
329
- this.#auth = new CopilotAuth(options);
330
- this.#fetch = options.fetch ?? fetch;
331
- }
332
- async chatCompletions(body, signal) {
333
- return this.fetchCopilot("/chat/completions", {
334
- body: JSON.stringify(body),
335
- headers: {
336
- "content-type": "application/json"
337
- },
338
- method: "POST",
339
- signal
340
- });
341
- }
342
- async responses(body, signal) {
343
- return this.fetchCopilot("/responses", {
344
- body,
345
- headers: {
346
- "content-type": "application/json"
347
- },
348
- method: "POST",
349
- signal
350
- });
351
- }
352
- async models(signal) {
353
- return this.fetchCopilot("/models", {
354
- headers: {
355
- accept: "application/json"
356
- },
357
- method: "GET",
358
- signal
359
- });
360
- }
361
- async fetchCopilot(path, init) {
362
- const access = await this.#auth.getAccess();
363
- const headers = new Headers(init.headers);
364
- headers.set("accept", headers.get("accept") ?? "application/json");
365
- headers.set("authorization", `Bearer ${access.token}`);
366
- headers.set("copilot-integration-id", "vscode-chat");
367
- headers.set("editor-plugin-version", "hoopilot/0.1.0");
368
- headers.set("editor-version", "Hoopilot/0.1.0");
369
- headers.set("openai-intent", "conversation-panel");
370
- headers.set("user-agent", "hoopilot/0.1.0");
371
- headers.set("x-github-api-version", "2026-06-01");
372
- return this.#fetch(`${access.apiBaseUrl}${path}`, {
373
- ...init,
374
- headers
375
- });
376
- }
377
- };
378
-
379
413
  // src/openai.ts
380
414
  var DEFAULT_MODEL = "gpt-4.1";
381
415
  function normalizeChatCompletionRequest(request) {
@@ -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
- const value = await request.json();
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 REQUEST_TIMEOUT_MS = 8e3;
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(REQUEST_TIMEOUT_MS)
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: errorDetails2(error), event: "update.check.refresh_failed" },
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(REQUEST_TIMEOUT_MS * 10)
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(REQUEST_TIMEOUT_MS)
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
- renameSync(exePath, oldExe);
1321
+ renameSync2(exePath, oldExe);
1299
1322
  const restore = () => {
1300
1323
  try {
1301
- renameSync(oldExe, exePath);
1324
+ renameSync2(oldExe, exePath);
1302
1325
  } catch {
1303
1326
  }
1304
1327
  };
1305
1328
  try {
1306
- renameSync(tmpFile, exePath);
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
- renameSync(tmpFile, exePath);
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 = createHoopilotLogger({
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,14 +1460,20 @@ async function main(argv = Bun.argv.slice(2)) {
1452
1460
  console.log(helpText(await getVersion()));
1453
1461
  return;
1454
1462
  }
1455
- args2.logger = createHoopilotLogger({
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
  }
1467
+ if (command === "models") {
1468
+ const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
1469
+ if (args2.help) {
1470
+ console.log(helpText(await getVersion()));
1471
+ return;
1472
+ }
1473
+ args2.logger = commandLogger(args2, "models");
1474
+ await runModels(args2);
1475
+ return;
1476
+ }
1463
1477
  const args = withRuntimeEnv(parseArgs(argv));
1464
1478
  if (args.help) {
1465
1479
  console.log(helpText(await getVersion()));
@@ -1469,11 +1483,7 @@ async function main(argv = Bun.argv.slice(2)) {
1469
1483
  console.log(await getVersion());
1470
1484
  return;
1471
1485
  }
1472
- const logger = createHoopilotLogger({
1473
- env: args.env,
1474
- format: args.logFormat,
1475
- level: args.logLevel
1476
- }).child({ component: "cli", command: "serve" });
1486
+ const logger = commandLogger(args, "serve");
1477
1487
  args.logger = logger;
1478
1488
  const started = startHoopilotServer(args);
1479
1489
  logger.info(
@@ -1484,7 +1494,7 @@ async function main(argv = Bun.argv.slice(2)) {
1484
1494
  },
1485
1495
  "hoopilot server started"
1486
1496
  );
1487
- if (!args.noUpdateCheck && process.env.HOOPILOT_NO_UPDATE_CHECK !== "1") {
1497
+ if (!args.noUpdateCheck) {
1488
1498
  void maybeNotifyUpdate(
1489
1499
  await getVersion(),
1490
1500
  IS_STANDALONE_BINARY ? "binary" : "npm",
@@ -1585,17 +1595,41 @@ async function runLogin(options = {}) {
1585
1595
  console.log(`Copilot OAuth credential stored at ${path}`);
1586
1596
  console.log("Copilot authentication ready.");
1587
1597
  }
1598
+ async function runModels(options = {}) {
1599
+ const logger = options.logger?.child({ component: "models" }) ?? noopLogger;
1600
+ logger.debug({ event: "models.list.started" }, "fetching github copilot models");
1601
+ const response = await new CopilotClient(options).models();
1602
+ if (!response.ok) {
1603
+ const message = `GitHub Copilot API model list failed with ${response.status}: ${await truncatedResponseText(response)}`;
1604
+ if (response.status === 401 || response.status === 403) {
1605
+ throw new CopilotAuthError(message);
1606
+ }
1607
+ throw new Error(message);
1608
+ }
1609
+ const ids = modelIdsFromResponse(await response.json().catch(() => void 0));
1610
+ if (ids.length === 0) {
1611
+ throw new Error("GitHub Copilot API returned no model IDs.");
1612
+ }
1613
+ logger.debug(
1614
+ { count: ids.length, event: "models.list.succeeded" },
1615
+ "github copilot models fetched"
1616
+ );
1617
+ for (const id of ids) {
1618
+ console.log(id);
1619
+ }
1620
+ return ids;
1621
+ }
1588
1622
  async function verifyCopilotOAuthToken(token, options = {}) {
1589
- const apiBaseUrl = trimTrailingSlash2(
1590
- options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL2
1623
+ const apiBaseUrl = trimTrailingSlash(
1624
+ options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
1591
1625
  );
1592
1626
  const fetcher = options.fetch ?? fetch;
1593
1627
  const response = await fetcher(`${apiBaseUrl}/models`, {
1594
- headers: copilotHeaders(token),
1628
+ headers: applyCopilotHeaders(new Headers(), token),
1595
1629
  method: "GET"
1596
1630
  });
1597
1631
  if (!response.ok) {
1598
- const message = `GitHub Copilot API verification failed with ${response.status}: ${await safeResponseText2(response)}`;
1632
+ const message = `GitHub Copilot API verification failed with ${response.status}: ${await truncatedResponseText(response)}`;
1599
1633
  if (response.status === 401 || response.status === 403) {
1600
1634
  throw new CopilotAuthError(message);
1601
1635
  }
@@ -1621,28 +1655,31 @@ function openBrowserBestEffort(url) {
1621
1655
  } catch {
1622
1656
  }
1623
1657
  }
1624
- function copilotHeaders(token) {
1625
- const headers = new Headers();
1626
- headers.set("accept", "application/json");
1627
- headers.set("authorization", `Bearer ${token}`);
1628
- headers.set("copilot-integration-id", "vscode-chat");
1629
- headers.set("editor-plugin-version", "hoopilot/0.1.0");
1630
- headers.set("editor-version", "Hoopilot/0.1.0");
1631
- headers.set("openai-intent", "conversation-panel");
1632
- headers.set("user-agent", "hoopilot/0.1.0");
1633
- headers.set("x-github-api-version", "2026-06-01");
1634
- return headers;
1635
- }
1636
- async function safeResponseText2(response) {
1637
- const text = await response.text();
1638
- return text.slice(0, 500);
1639
- }
1640
- function trimTrailingSlash2(value) {
1641
- return value.replace(/\/+$/, "");
1658
+ function modelIdsFromResponse(body) {
1659
+ const record = asRecord(body);
1660
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];
1661
+ const seen = /* @__PURE__ */ new Set();
1662
+ const ids = [];
1663
+ for (const model of data) {
1664
+ const id = asRecord(model).id;
1665
+ if (typeof id !== "string" || id.length === 0 || seen.has(id)) {
1666
+ continue;
1667
+ }
1668
+ seen.add(id);
1669
+ ids.push(id);
1670
+ }
1671
+ return ids;
1642
1672
  }
1643
1673
  function withRuntimeEnv(args) {
1644
1674
  return { ...args, env: process.env };
1645
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
+ }
1646
1683
  function helpText(version) {
1647
1684
  return `hoopilot ${version}
1648
1685
 
@@ -1651,18 +1688,20 @@ OpenAI-compatible proxy for GitHub Copilot.
1651
1688
  Usage:
1652
1689
  hoopilot [serve] [options]
1653
1690
  hoopilot login [options]
1691
+ hoopilot models [options]
1654
1692
  hoopilot update
1655
1693
  npx @openhoo/hoopilot [options]
1656
1694
 
1657
1695
  Commands:
1658
1696
  serve Start the proxy server (default)
1659
1697
  login Sign in through GitHub OAuth in a browser and verify Copilot access
1698
+ models List available GitHub Copilot model IDs
1660
1699
  update, upgrade Update hoopilot to the latest release
1661
1700
 
1662
1701
  Options:
1663
1702
  -p, --port <port> Port to listen on. Default: 4141
1664
1703
  --host <host> Host to listen on. Default: 127.0.0.1
1665
- --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>
1666
1705
  --auth-file <path> OAuth credential store path
1667
1706
  --copilot-api-base-url <url> Copilot API base URL override
1668
1707
  --log-level <level> trace, debug, info, warn, error, fatal, or silent
@@ -1692,6 +1731,7 @@ if (import.meta.main) {
1692
1731
  export {
1693
1732
  main,
1694
1733
  parseArgs,
1734
+ runModels,
1695
1735
  verifyCopilotOAuthToken
1696
1736
  };
1697
1737
  //# sourceMappingURL=cli.js.map