@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 +5 -16
- package/dist/cli.js +194 -154
- 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.
|
|
@@ -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
|
-
|
|
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,9 +108,64 @@ var CopilotAuth = class {
|
|
|
97
108
|
return access;
|
|
98
109
|
}
|
|
99
110
|
};
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
216
|
+
`GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
|
|
149
217
|
response
|
|
150
218
|
)}`
|
|
151
219
|
);
|
|
152
220
|
}
|
|
153
|
-
return
|
|
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
|
|
243
|
+
`GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
|
|
172
244
|
response
|
|
173
245
|
)}`
|
|
174
246
|
);
|
|
175
247
|
}
|
|
176
|
-
const data = await
|
|
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
|
|
287
|
+
async function parseJsonResponse(response, context) {
|
|
213
288
|
const text = await response.text();
|
|
214
|
-
|
|
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
|
-
|
|
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,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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
1590
|
-
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
|
|
1591
1625
|
);
|
|
1592
1626
|
const fetcher = options.fetch ?? fetch;
|
|
1593
1627
|
const response = await fetcher(`${apiBaseUrl}/models`, {
|
|
1594
|
-
headers:
|
|
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
|
|
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
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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
|