@openhoo/hoopilot 0.3.1 → 0.4.0

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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -23,270 +33,126 @@ __export(index_exports, {
23
33
  CopilotAuth: () => CopilotAuth,
24
34
  CopilotAuthError: () => CopilotAuthError,
25
35
  CopilotClient: () => CopilotClient,
36
+ DEFAULT_LOG_FORMAT: () => DEFAULT_LOG_FORMAT,
37
+ DEFAULT_LOG_LEVEL: () => DEFAULT_LOG_LEVEL,
26
38
  DEFAULT_MODEL: () => DEFAULT_MODEL,
39
+ authStorePath: () => authStorePath,
27
40
  chatCompletionToCompletion: () => chatCompletionToCompletion,
28
41
  chatCompletionToResponse: () => chatCompletionToResponse,
29
42
  completionsRequestToChatCompletion: () => completionsRequestToChatCompletion,
30
43
  createHoopilotHandler: () => createHoopilotHandler,
44
+ createHoopilotLogger: () => createHoopilotLogger,
31
45
  fallbackModels: () => fallbackModels,
46
+ githubCopilotDeviceLogin: () => githubCopilotDeviceLogin,
47
+ noopLogger: () => noopLogger,
32
48
  normalizeModelsResponse: () => normalizeModelsResponse,
49
+ parseLogFormat: () => parseLogFormat,
50
+ parseLogLevel: () => parseLogLevel,
51
+ readStoredCopilotAuth: () => readStoredCopilotAuth,
33
52
  responsesRequestToChatCompletion: () => responsesRequestToChatCompletion,
34
53
  responsesStreamFromChatStream: () => responsesStreamFromChatStream,
35
- splitCommand: () => splitCommand,
36
- startHoopilotServer: () => startHoopilotServer
54
+ startHoopilotServer: () => startHoopilotServer,
55
+ writeStoredCopilotAuth: () => writeStoredCopilotAuth
37
56
  });
38
57
  module.exports = __toCommonJS(index_exports);
39
58
 
59
+ // src/auth-store.ts
60
+ var import_node_fs = require("fs");
61
+ var import_node_path = require("path");
62
+ function authStorePath(env = process.env) {
63
+ if (env.HOOPILOT_AUTH_FILE) {
64
+ return env.HOOPILOT_AUTH_FILE;
65
+ }
66
+ const base = env.XDG_CONFIG_HOME ?? env.APPDATA ?? (env.HOME ? (0, import_node_path.join)(env.HOME, ".config") : (0, import_node_path.join)(process.cwd(), ".config"));
67
+ return (0, import_node_path.join)(base, "hoopilot", "auth.json");
68
+ }
69
+ function readStoredCopilotAuth(path = authStorePath()) {
70
+ try {
71
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
72
+ if (!parsed || typeof parsed !== "object") {
73
+ return void 0;
74
+ }
75
+ const token = typeof parsed.token === "string" ? parsed.token.trim() : "";
76
+ if (!token) {
77
+ return void 0;
78
+ }
79
+ return {
80
+ apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : void 0,
81
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0,
82
+ githubDomain: typeof parsed.githubDomain === "string" ? parsed.githubDomain : void 0,
83
+ source: typeof parsed.source === "string" ? parsed.source : void 0,
84
+ token
85
+ };
86
+ } catch {
87
+ return void 0;
88
+ }
89
+ }
90
+ function writeStoredCopilotAuth(auth, path = authStorePath()) {
91
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
92
+ (0, import_node_fs.writeFileSync)(
93
+ path,
94
+ `${JSON.stringify(
95
+ {
96
+ ...auth,
97
+ createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
98
+ },
99
+ null,
100
+ 2
101
+ )}
102
+ `,
103
+ { mode: 384 }
104
+ );
105
+ try {
106
+ (0, import_node_fs.chmodSync)(path, 384);
107
+ } catch {
108
+ }
109
+ }
110
+
40
111
  // src/auth.ts
41
- var import_node_child_process = require("child_process");
42
- var DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
43
- var DEFAULT_TOKEN_EXCHANGE_URL = "https://api.github.com/copilot_internal/v2/token";
112
+ var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
44
113
  var REFRESH_SKEW_MS = 6e4;
114
+ var STORED_TOKEN_TTL_MS = 10 * 6e4;
45
115
  var CopilotAuthError = class extends Error {
46
116
  constructor(message) {
47
117
  super(message);
48
118
  this.name = "CopilotAuthError";
49
119
  }
50
120
  };
51
- var CopilotTokenExchangeHttpError = class extends CopilotAuthError {
52
- };
53
121
  var CopilotAuth = class {
54
- #authMode;
122
+ #authStorePath;
55
123
  #copilotApiBaseUrl;
56
- #copilotToken;
57
- #env;
58
- #fetch;
59
- #githubToken;
60
- #githubTokenCommand;
61
- #logger;
62
- #tokenExchangeUrl;
63
124
  #cachedAccess;
64
125
  constructor(options = {}) {
65
- this.#authMode = options.authMode ?? "auto";
126
+ this.#authStorePath = options.authStorePath ?? options.env?.HOOPILOT_AUTH_FILE;
66
127
  this.#copilotApiBaseUrl = trimTrailingSlash(
67
128
  options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL
68
129
  );
69
- this.#copilotToken = options.copilotToken;
70
- this.#env = options.env ?? process.env;
71
- this.#fetch = options.fetch ?? fetch;
72
- this.#githubToken = options.githubToken;
73
- this.#githubTokenCommand = options.githubTokenCommand ?? "gh auth token";
74
- this.#logger = options.logger;
75
- this.#tokenExchangeUrl = options.tokenExchangeUrl ?? options.env?.COPILOT_TOKEN_EXCHANGE_URL ?? DEFAULT_TOKEN_EXCHANGE_URL;
76
130
  }
77
131
  async getAccess() {
78
132
  if (this.#cachedAccess && this.#cachedAccess.expiresAtMs - REFRESH_SKEW_MS > Date.now()) {
79
133
  return this.#cachedAccess;
80
134
  }
81
- const directCopilotToken = this.#resolveDirectCopilotToken();
82
- if (directCopilotToken) {
83
- return this.#cacheAccess({
84
- apiBaseUrl: this.#copilotApiBaseUrl,
85
- expiresAtMs: Date.now() + 10 * 6e4,
86
- source: "copilot-token",
87
- token: directCopilotToken
88
- });
89
- }
90
- if (this.#authMode === "copilot-token") {
91
- throw new CopilotAuthError("COPILOT_API_TOKEN or GITHUB_COPILOT_API_TOKEN is required.");
92
- }
93
- const githubToken = this.#resolveGithubToken();
94
- if (!githubToken) {
95
- throw new CopilotAuthError(
96
- "No Copilot credential found. Set COPILOT_API_TOKEN, set COPILOT_GITHUB_TOKEN from gh auth token, or sign in with gh auth login."
97
- );
98
- }
99
- if (isPersonalAccessToken(githubToken)) {
100
- throw new CopilotAuthError(
101
- "GitHub personal access tokens are not supported for Copilot authentication. Use gh auth login or COPILOT_API_TOKEN."
102
- );
103
- }
104
- try {
105
- return this.#cacheAccess(await this.#exchangeGithubToken(githubToken));
106
- } catch (error) {
107
- if (!(error instanceof CopilotTokenExchangeHttpError)) {
108
- throw error;
109
- }
110
- this.#logger?.warn(
111
- `Copilot token exchange failed; falling back to GitHub CLI token mode: ${errorMessage(
112
- error
113
- )}`
114
- );
135
+ const stored = readStoredCopilotAuth(this.#authStorePath);
136
+ if (stored) {
115
137
  return this.#cacheAccess({
116
- apiBaseUrl: this.#copilotApiBaseUrl,
117
- expiresAtMs: Date.now() + 10 * 6e4,
118
- source: "direct-github-token",
119
- token: githubToken
138
+ apiBaseUrl: trimTrailingSlash(stored.apiBaseUrl ?? this.#copilotApiBaseUrl),
139
+ expiresAtMs: Date.now() + STORED_TOKEN_TTL_MS,
140
+ source: "github-copilot-oauth",
141
+ token: stored.token
120
142
  });
121
143
  }
144
+ throw new CopilotAuthError(
145
+ "No GitHub Copilot OAuth credential found. Run `hoopilot login` to sign in through your browser."
146
+ );
122
147
  }
123
148
  #cacheAccess(access) {
124
149
  this.#cachedAccess = access;
125
150
  return access;
126
151
  }
127
- async #exchangeGithubToken(githubToken) {
128
- const response = await this.#fetch(this.#tokenExchangeUrl, {
129
- headers: {
130
- accept: "application/vnd.github+json",
131
- authorization: `token ${githubToken}`,
132
- "editor-plugin-version": "hoopilot/0.1.0",
133
- "editor-version": "Hoopilot/0.1.0",
134
- "user-agent": "hoopilot/0.1.0"
135
- },
136
- method: "GET"
137
- });
138
- if (!response.ok) {
139
- throw new CopilotTokenExchangeHttpError(
140
- `GitHub Copilot token exchange failed with ${response.status}: ${await safeResponseText(
141
- response
142
- )}`
143
- );
144
- }
145
- const body = asRecord(await response.json());
146
- const token = getString(body, "token");
147
- if (!token) {
148
- throw new CopilotAuthError("GitHub Copilot token exchange response did not include a token.");
149
- }
150
- return {
151
- apiBaseUrl: endpointFromResponse(body) ?? this.#copilotApiBaseUrl,
152
- expiresAtMs: expiresAtFromResponse(body),
153
- source: "github-token",
154
- token
155
- };
156
- }
157
- #resolveDirectCopilotToken() {
158
- return firstNonEmpty(
159
- this.#copilotToken,
160
- this.#env.COPILOT_API_TOKEN,
161
- this.#env.GITHUB_COPILOT_API_TOKEN,
162
- this.#env.GITHUB_COPILOT_TOKEN
163
- );
164
- }
165
- #resolveGithubToken() {
166
- return firstNonEmpty(
167
- this.#githubToken,
168
- this.#env.COPILOT_GITHUB_TOKEN,
169
- this.#env.GITHUB_COPILOT_GITHUB_TOKEN,
170
- this.#readGithubTokenCommand()
171
- );
172
- }
173
- #readGithubTokenCommand() {
174
- if (this.#githubTokenCommand === false) {
175
- return void 0;
176
- }
177
- const parts = splitCommand(this.#githubTokenCommand);
178
- const [command, ...args] = parts;
179
- if (!command) {
180
- return void 0;
181
- }
182
- try {
183
- const output = (0, import_node_child_process.execFileSync)(command, args, {
184
- encoding: "utf8",
185
- stdio: ["ignore", "pipe", "ignore"],
186
- timeout: 5e3
187
- });
188
- return output.trim() || void 0;
189
- } catch {
190
- return void 0;
191
- }
192
- }
193
152
  };
194
- function splitCommand(command) {
195
- const parts = [];
196
- let current = "";
197
- let quote;
198
- let escaping = false;
199
- for (const character of command.trim()) {
200
- if (escaping) {
201
- current += character;
202
- escaping = false;
203
- continue;
204
- }
205
- if (character === "\\") {
206
- escaping = true;
207
- continue;
208
- }
209
- if (quote) {
210
- if (character === quote) {
211
- quote = void 0;
212
- } else {
213
- current += character;
214
- }
215
- continue;
216
- }
217
- if (character === "'" || character === '"') {
218
- quote = character;
219
- continue;
220
- }
221
- if (/\s/.test(character)) {
222
- if (current) {
223
- parts.push(current);
224
- current = "";
225
- }
226
- continue;
227
- }
228
- current += character;
229
- }
230
- if (current) {
231
- parts.push(current);
232
- }
233
- return parts;
234
- }
235
- function endpointFromResponse(body) {
236
- const endpoints = asRecord(body.endpoints);
237
- const apiUrl = getString(endpoints, "api") ?? getString(endpoints, "proxy");
238
- return apiUrl ? trimTrailingSlash(apiUrl) : void 0;
239
- }
240
- function expiresAtFromResponse(body) {
241
- const expiresAt = body.expires_at;
242
- if (typeof expiresAt === "number") {
243
- return expiresAt < 1e10 ? expiresAt * 1e3 : expiresAt;
244
- }
245
- if (typeof expiresAt === "string") {
246
- const asNumber = Number(expiresAt);
247
- if (Number.isFinite(asNumber)) {
248
- return asNumber < 1e10 ? asNumber * 1e3 : asNumber;
249
- }
250
- const parsed = Date.parse(expiresAt);
251
- if (Number.isFinite(parsed)) {
252
- return parsed;
253
- }
254
- }
255
- const refreshIn = body.refresh_in;
256
- if (typeof refreshIn === "number" && Number.isFinite(refreshIn)) {
257
- return Date.now() + refreshIn * 1e3;
258
- }
259
- return Date.now() + 10 * 6e4;
260
- }
261
- function firstNonEmpty(...values) {
262
- for (const value of values) {
263
- const trimmed = value?.trim();
264
- if (trimmed) {
265
- return trimmed;
266
- }
267
- }
268
- return void 0;
269
- }
270
- function asRecord(value) {
271
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
272
- }
273
- function getString(record, key) {
274
- const value = record[key];
275
- return typeof value === "string" && value ? value : void 0;
276
- }
277
153
  function trimTrailingSlash(value) {
278
154
  return value.replace(/\/+$/, "");
279
155
  }
280
- async function safeResponseText(response) {
281
- const text = await response.text();
282
- return text.slice(0, 500);
283
- }
284
- function isPersonalAccessToken(token) {
285
- return token.startsWith("github_pat_") || token.startsWith("ghp_");
286
- }
287
- function errorMessage(error) {
288
- return error instanceof Error ? error.message : String(error);
289
- }
290
156
 
291
157
  // src/copilot.ts
292
158
  var CopilotClient = class {
@@ -335,6 +201,7 @@ var CopilotClient = class {
335
201
  headers.set("editor-version", "Hoopilot/0.1.0");
336
202
  headers.set("openai-intent", "conversation-panel");
337
203
  headers.set("user-agent", "hoopilot/0.1.0");
204
+ headers.set("x-github-api-version", "2026-06-01");
338
205
  return this.#fetch(`${access.apiBaseUrl}${path}`, {
339
206
  ...init,
340
207
  headers
@@ -342,6 +209,226 @@ var CopilotClient = class {
342
209
  }
343
210
  };
344
211
 
212
+ // src/github-device.ts
213
+ var import_promises = require("timers/promises");
214
+ var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Iv23lijnNxm2e9UX3CF8";
215
+ var DEFAULT_GITHUB_DOMAIN = "github.com";
216
+ var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
217
+ var POLLING_SAFETY_MARGIN_MS = 3e3;
218
+ async function githubCopilotDeviceLogin(options = {}) {
219
+ const env = options.env ?? process.env;
220
+ const fetcher = options.fetch ?? fetch;
221
+ const sleeper = options.sleep ?? import_promises.setTimeout;
222
+ const domain = normalizeDomain(
223
+ options.domain ?? env.HOOPILOT_GITHUB_DOMAIN ?? DEFAULT_GITHUB_DOMAIN
224
+ );
225
+ const clientId = options.clientId ?? env.HOOPILOT_GITHUB_CLIENT_ID ?? env.COPILOT_GITHUB_CLIENT_ID ?? DEFAULT_GITHUB_COPILOT_CLIENT_ID;
226
+ const device = await requestDeviceCode(fetcher, domain, clientId);
227
+ const verificationUrl = device.verification_uri;
228
+ const userCode = device.user_code;
229
+ const deviceCode = device.device_code;
230
+ if (!verificationUrl || !userCode || !deviceCode) {
231
+ throw new Error("GitHub device authorization response is missing required fields.");
232
+ }
233
+ options.logger?.info(`First copy your one-time code: ${userCode}`);
234
+ options.logger?.info(`Open ${verificationUrl} in your browser to authorize Hoopilot.`);
235
+ await options.openBrowser?.(verificationUrl);
236
+ return {
237
+ domain,
238
+ token: await pollForAccessToken(fetcher, sleeper, domain, clientId, {
239
+ deviceCode,
240
+ expiresIn: positiveSeconds(device.expires_in, 900),
241
+ interval: positiveSeconds(device.interval, 5)
242
+ })
243
+ };
244
+ }
245
+ async function requestDeviceCode(fetcher, domain, clientId) {
246
+ const response = await fetcher(`https://${domain}/login/device/code`, {
247
+ body: JSON.stringify({
248
+ client_id: clientId,
249
+ scope: "read:user"
250
+ }),
251
+ headers: oauthHeaders(),
252
+ method: "POST"
253
+ });
254
+ if (!response.ok) {
255
+ throw new Error(
256
+ `GitHub device authorization failed with ${response.status}: ${await safeResponseText(
257
+ response
258
+ )}`
259
+ );
260
+ }
261
+ return await response.json();
262
+ }
263
+ async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
264
+ let intervalMs = device.interval * 1e3 + POLLING_SAFETY_MARGIN_MS;
265
+ const deadline = Date.now() + device.expiresIn * 1e3;
266
+ while (Date.now() < deadline) {
267
+ await sleeper(intervalMs);
268
+ const response = await fetcher(`https://${domain}/login/oauth/access_token`, {
269
+ body: JSON.stringify({
270
+ client_id: clientId,
271
+ device_code: device.deviceCode,
272
+ grant_type: DEVICE_GRANT_TYPE
273
+ }),
274
+ headers: oauthHeaders(),
275
+ method: "POST"
276
+ });
277
+ if (!response.ok) {
278
+ throw new Error(
279
+ `GitHub device token exchange failed with ${response.status}: ${await safeResponseText(
280
+ response
281
+ )}`
282
+ );
283
+ }
284
+ const data = await response.json();
285
+ if (data.access_token) {
286
+ return data.access_token;
287
+ }
288
+ if (data.error === "authorization_pending") {
289
+ continue;
290
+ }
291
+ if (data.error === "slow_down") {
292
+ intervalMs = positiveSeconds(data.interval, device.interval + 5) * 1e3 + POLLING_SAFETY_MARGIN_MS;
293
+ continue;
294
+ }
295
+ if (data.error === "expired_token") {
296
+ throw new Error("GitHub device login expired. Run `hoopilot login` again.");
297
+ }
298
+ if (data.error === "access_denied") {
299
+ throw new Error("GitHub device login was cancelled.");
300
+ }
301
+ if (data.error) {
302
+ throw new Error(data.error_description || `GitHub device login failed: ${data.error}`);
303
+ }
304
+ }
305
+ throw new Error("GitHub device login timed out. Run `hoopilot login` again.");
306
+ }
307
+ function oauthHeaders() {
308
+ const headers = new Headers();
309
+ headers.set("accept", "application/json");
310
+ headers.set("content-type", "application/json");
311
+ headers.set("user-agent", "hoopilot");
312
+ return headers;
313
+ }
314
+ function normalizeDomain(value) {
315
+ return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
316
+ }
317
+ function positiveSeconds(value, fallback) {
318
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
319
+ }
320
+ async function safeResponseText(response) {
321
+ const text = await response.text();
322
+ return text.slice(0, 500);
323
+ }
324
+
325
+ // src/logger.ts
326
+ var import_pino = __toESM(require("pino"), 1);
327
+ var import_pino_pretty = __toESM(require("pino-pretty"), 1);
328
+ var DEFAULT_LOG_FORMAT = "pretty";
329
+ var DEFAULT_LOG_LEVEL = "info";
330
+ var LOG_FORMATS = ["json", "pretty"];
331
+ var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
332
+ var REDACT_PATHS = [
333
+ "apiKey",
334
+ "authorization",
335
+ "cookie",
336
+ "headers.authorization",
337
+ "headers.Authorization",
338
+ "headers.cookie",
339
+ "headers.Cookie",
340
+ "headers.x-api-key",
341
+ "headers.X-Api-Key",
342
+ "token",
343
+ "*.apiKey",
344
+ "*.authorization",
345
+ "*.cookie",
346
+ "*.token",
347
+ "*.headers.authorization",
348
+ "*.headers.Authorization",
349
+ "*.headers.cookie",
350
+ "*.headers.Cookie",
351
+ "*.headers.x-api-key",
352
+ "*.headers.X-Api-Key"
353
+ ];
354
+ var noopLogger = {
355
+ child: () => noopLogger,
356
+ debug: () => {
357
+ },
358
+ error: () => {
359
+ },
360
+ fatal: () => {
361
+ },
362
+ info: () => {
363
+ },
364
+ trace: () => {
365
+ },
366
+ warn: () => {
367
+ }
368
+ };
369
+ function createHoopilotLogger(options = {}) {
370
+ const env = options.env ?? process.env;
371
+ const level = parseLogLevel(options.level ?? env.HOOPILOT_LOG_LEVEL);
372
+ const format = parseLogFormat(options.format ?? env.HOOPILOT_LOG_FORMAT);
373
+ const pinoOptions = {
374
+ base: {
375
+ service: "hoopilot",
376
+ ...options.base
377
+ },
378
+ level,
379
+ redact: {
380
+ censor: "[Redacted]",
381
+ paths: REDACT_PATHS
382
+ },
383
+ timestamp: import_pino.default.stdTimeFunctions.isoTime
384
+ };
385
+ if (format === "pretty") {
386
+ return (0, import_pino.default)(
387
+ pinoOptions,
388
+ (0, import_pino_pretty.default)({
389
+ colorize: options.colorize ?? process.stderr.isTTY,
390
+ destination: options.stream ?? 1,
391
+ ignore: "pid,hostname",
392
+ singleLine: true,
393
+ translateTime: "SYS:standard"
394
+ })
395
+ );
396
+ }
397
+ if (options.stream) {
398
+ return (0, import_pino.default)(pinoOptions, options.stream);
399
+ }
400
+ return (0, import_pino.default)(pinoOptions);
401
+ }
402
+ function parseLogFormat(value) {
403
+ if (!value) {
404
+ return DEFAULT_LOG_FORMAT;
405
+ }
406
+ if (isLogFormat(value)) {
407
+ return value;
408
+ }
409
+ throw new Error(`Invalid log format: ${value}. Expected one of: ${LOG_FORMATS.join(", ")}.`);
410
+ }
411
+ function parseLogLevel(value) {
412
+ if (!value) {
413
+ return DEFAULT_LOG_LEVEL;
414
+ }
415
+ if (isLogLevel(value)) {
416
+ return value;
417
+ }
418
+ throw new Error(`Invalid log level: ${value}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
419
+ }
420
+ function shouldCreateLogger(options) {
421
+ return Boolean(
422
+ options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
423
+ );
424
+ }
425
+ function isLogFormat(value) {
426
+ return LOG_FORMATS.includes(value);
427
+ }
428
+ function isLogLevel(value) {
429
+ return LOG_LEVELS.includes(value);
430
+ }
431
+
345
432
  // src/openai.ts
346
433
  var DEFAULT_MODEL = "gpt-4.1";
347
434
  function responsesRequestToChatCompletion(request) {
@@ -360,8 +447,8 @@ function responsesRequestToChatCompletion(request) {
360
447
  metadata: request.metadata,
361
448
  model: contentToText(request.model) || DEFAULT_MODEL,
362
449
  presence_penalty: request.presence_penalty,
363
- reasoning_effort: asRecord2(request.reasoning).effort,
364
- response_format: asRecord2(request.text).format,
450
+ reasoning_effort: asRecord(request.reasoning).effort,
451
+ response_format: asRecord(request.text).format,
365
452
  seed: request.seed,
366
453
  stream: request.stream === true,
367
454
  temperature: request.temperature,
@@ -383,7 +470,7 @@ function completionsRequestToChatCompletion(request) {
383
470
  function chatCompletionToResponse(completion, responseId) {
384
471
  const id = responseId ?? `resp_${randomId()}`;
385
472
  const choice = firstChoice(completion);
386
- const message = asRecord2(choice.message);
473
+ const message = asRecord(choice.message);
387
474
  const model = contentToText(completion.model) || DEFAULT_MODEL;
388
475
  const output = outputItemsFromMessage(message);
389
476
  const usage = responseUsage(completion.usage);
@@ -410,7 +497,7 @@ function chatCompletionToResponse(completion, responseId) {
410
497
  }
411
498
  function chatCompletionToCompletion(completion) {
412
499
  const choice = firstChoice(completion);
413
- const message = asRecord2(choice.message);
500
+ const message = asRecord(choice.message);
414
501
  return removeUndefined({
415
502
  choices: [
416
503
  {
@@ -428,9 +515,9 @@ function chatCompletionToCompletion(completion) {
428
515
  });
429
516
  }
430
517
  function normalizeModelsResponse(upstream) {
431
- const record = asRecord2(upstream);
518
+ const record = asRecord(upstream);
432
519
  const data = Array.isArray(record.data) ? record.data : Array.isArray(upstream) ? upstream : [];
433
- const models = data.map((model) => asRecord2(model)).filter((model) => typeof model.id === "string").map((model) => ({
520
+ const models = data.map((model) => asRecord(model)).filter((model) => typeof model.id === "string").map((model) => ({
434
521
  created: model.created ?? 0,
435
522
  id: model.id,
436
523
  object: "model",
@@ -579,7 +666,7 @@ function inputToMessages(input) {
579
666
  }
580
667
  const messages = [];
581
668
  for (const item of input) {
582
- const record = asRecord2(item);
669
+ const record = asRecord(item);
583
670
  if (record.type === "function_call_output") {
584
671
  messages.push({
585
672
  content: contentToText(record.output),
@@ -621,7 +708,7 @@ function chatMessageContent(content) {
621
708
  }
622
709
  const parts = [];
623
710
  for (const part of content) {
624
- const record = asRecord2(part);
711
+ const record = asRecord(part);
625
712
  const type = contentToText(record.type);
626
713
  if (type === "input_text" || type === "output_text" || type === "text") {
627
714
  parts.push({ text: contentToText(record.text), type: "text" });
@@ -679,7 +766,7 @@ function chatTools(tools) {
679
766
  if (!Array.isArray(tools)) {
680
767
  return void 0;
681
768
  }
682
- const converted = tools.map((tool) => asRecord2(tool)).filter((tool) => tool.type === "function").map((tool) => ({
769
+ const converted = tools.map((tool) => asRecord(tool)).filter((tool) => tool.type === "function").map((tool) => ({
683
770
  function: removeUndefined({
684
771
  description: tool.description,
685
772
  name: tool.name,
@@ -694,7 +781,7 @@ function chatToolChoice(toolChoice) {
694
781
  if (typeof toolChoice === "string" || toolChoice === void 0) {
695
782
  return toolChoice;
696
783
  }
697
- const record = asRecord2(toolChoice);
784
+ const record = asRecord(toolChoice);
698
785
  if (record.type === "function" && typeof record.name === "string") {
699
786
  return { function: { name: record.name }, type: "function" };
700
787
  }
@@ -708,8 +795,8 @@ function outputItemsFromMessage(message) {
708
795
  }
709
796
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
710
797
  for (const toolCall of toolCalls) {
711
- const record = asRecord2(toolCall);
712
- const fn = asRecord2(record.function);
798
+ const record = asRecord(toolCall);
799
+ const fn = asRecord(record.function);
713
800
  output.push(
714
801
  functionCallItem({
715
802
  arguments: contentToText(fn.arguments),
@@ -750,10 +837,10 @@ function outputText(output) {
750
837
  return output.flatMap((item) => {
751
838
  const content = item.content;
752
839
  return Array.isArray(content) ? content : [];
753
- }).map((part) => contentToText(asRecord2(part).text)).filter(Boolean).join("");
840
+ }).map((part) => contentToText(asRecord(part).text)).filter(Boolean).join("");
754
841
  }
755
842
  function responseUsage(usage) {
756
- const record = asRecord2(usage);
843
+ const record = asRecord(usage);
757
844
  if (Object.keys(record).length === 0) {
758
845
  return null;
759
846
  }
@@ -767,7 +854,7 @@ function responseUsage(usage) {
767
854
  }
768
855
  function firstChoice(completion) {
769
856
  const choices = Array.isArray(completion.choices) ? completion.choices : [];
770
- return asRecord2(choices[0]);
857
+ return asRecord(choices[0]);
771
858
  }
772
859
  function processChatSseLine(line, enqueue, tools, appendText) {
773
860
  const trimmed = line.trim();
@@ -783,7 +870,7 @@ function processChatSseLine(line, enqueue, tools, appendText) {
783
870
  return;
784
871
  }
785
872
  const choice = firstChoice(parsed);
786
- const delta = asRecord2(choice.delta);
873
+ const delta = asRecord(choice.delta);
787
874
  const content = contentToText(delta.content);
788
875
  if (content) {
789
876
  appendText(content);
@@ -797,8 +884,8 @@ function processChatSseLine(line, enqueue, tools, appendText) {
797
884
  }
798
885
  const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
799
886
  for (const toolCall of toolCalls) {
800
- const record = asRecord2(toolCall);
801
- const fn = asRecord2(record.function);
887
+ const record = asRecord(toolCall);
888
+ const fn = asRecord(record.function);
802
889
  const index = typeof record.index === "number" ? record.index : tools.size;
803
890
  const existing = tools.get(index) ?? {
804
891
  arguments: "",
@@ -846,7 +933,7 @@ data: ${JSON.stringify(data)}
846
933
  }
847
934
  function parseJson(data) {
848
935
  try {
849
- return asRecord2(JSON.parse(data));
936
+ return asRecord(JSON.parse(data));
850
937
  } catch {
851
938
  return void 0;
852
939
  }
@@ -854,7 +941,7 @@ function parseJson(data) {
854
941
  function removeUndefined(record) {
855
942
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
856
943
  }
857
- function asRecord2(value) {
944
+ function asRecord(value) {
858
945
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
859
946
  }
860
947
  function randomId() {
@@ -867,43 +954,111 @@ function epochSeconds() {
867
954
  // src/server.ts
868
955
  var DEFAULT_HOST = "127.0.0.1";
869
956
  var DEFAULT_PORT = 4141;
957
+ var INVALID_JSON_MESSAGE = "Request body must be valid JSON.";
870
958
  function createHoopilotHandler(options = {}) {
871
959
  const client = new CopilotClient(options);
872
960
  const apiKey = options.apiKey ?? options.env?.HOOPILOT_API_KEY;
961
+ const logger = serverLogger(options);
873
962
  return async (request) => {
963
+ const startedAt = performance.now();
874
964
  const url = new URL(request.url);
965
+ const requestId = requestIdFor(request);
966
+ const requestLogger = logger.child({
967
+ method: request.method,
968
+ path: url.pathname,
969
+ requestId,
970
+ route: routeFor(request.method, url.pathname)
971
+ });
875
972
  if (request.method === "OPTIONS") {
876
- return new Response(null, { headers: corsHeaders() });
973
+ return finishResponse(new Response(null, { headers: corsHeaders() }), {
974
+ logger: requestLogger,
975
+ requestId,
976
+ startedAt
977
+ });
877
978
  }
878
979
  if (!isAuthorized(request, apiKey)) {
879
- return jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key.");
980
+ requestLogger.warn({ event: "http.request.unauthorized" }, "invalid hoopilot api key");
981
+ return finishResponse(
982
+ jsonError(401, "invalid_api_key", "Invalid or missing Hoopilot API key."),
983
+ {
984
+ logger: requestLogger,
985
+ requestId,
986
+ startedAt
987
+ }
988
+ );
880
989
  }
881
990
  try {
882
991
  if (request.method === "GET" && (url.pathname === "/" || url.pathname === "/healthz")) {
883
- return jsonResponse({
884
- name: "hoopilot",
885
- object: "health",
886
- status: "ok"
887
- });
992
+ return finishResponse(
993
+ jsonResponse({
994
+ name: "hoopilot",
995
+ object: "health",
996
+ status: "ok"
997
+ }),
998
+ { logger: requestLogger, requestId, startedAt }
999
+ );
888
1000
  }
889
1001
  if (request.method === "GET" && url.pathname === "/v1/models") {
890
- return await handleModels(client, request.signal);
1002
+ return finishResponse(await handleModels(client, request.signal, requestLogger), {
1003
+ logger: requestLogger,
1004
+ requestId,
1005
+ startedAt
1006
+ });
891
1007
  }
892
1008
  if (request.method === "POST" && url.pathname === "/v1/chat/completions") {
893
- return await handleChatCompletions(client, request);
1009
+ return finishResponse(await handleChatCompletions(client, request, requestLogger), {
1010
+ logger: requestLogger,
1011
+ requestId,
1012
+ startedAt
1013
+ });
894
1014
  }
895
1015
  if (request.method === "POST" && url.pathname === "/v1/completions") {
896
- return await handleCompletions(client, request);
1016
+ return finishResponse(await handleCompletions(client, request, requestLogger), {
1017
+ logger: requestLogger,
1018
+ requestId,
1019
+ startedAt
1020
+ });
897
1021
  }
898
1022
  if (request.method === "POST" && url.pathname === "/v1/responses") {
899
- return await handleResponses(client, request);
1023
+ return finishResponse(await handleResponses(client, request, requestLogger), {
1024
+ logger: requestLogger,
1025
+ requestId,
1026
+ startedAt
1027
+ });
900
1028
  }
901
- return jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`);
1029
+ return finishResponse(
1030
+ jsonError(404, "not_found", `No route for ${request.method} ${url.pathname}.`),
1031
+ { logger: requestLogger, requestId, startedAt }
1032
+ );
902
1033
  } catch (error) {
903
1034
  if (error instanceof CopilotAuthError) {
904
- return jsonError(401, "copilot_auth_error", error.message);
1035
+ requestLogger.warn(
1036
+ { err: errorDetails(error), event: "copilot.auth.missing" },
1037
+ "copilot auth failed"
1038
+ );
1039
+ return finishResponse(jsonError(401, "copilot_auth_error", error.message), {
1040
+ logger: requestLogger,
1041
+ requestId,
1042
+ startedAt
1043
+ });
905
1044
  }
906
- return jsonError(500, "internal_error", errorMessage2(error));
1045
+ const message = errorMessage(error);
1046
+ if (message === INVALID_JSON_MESSAGE) {
1047
+ requestLogger.warn(
1048
+ { err: errorDetails(error), event: "http.request.failed" },
1049
+ "request body was invalid json"
1050
+ );
1051
+ } else {
1052
+ requestLogger.error(
1053
+ { err: errorDetails(error), event: "http.request.failed" },
1054
+ "request failed"
1055
+ );
1056
+ }
1057
+ return finishResponse(jsonError(500, "internal_error", message), {
1058
+ logger: requestLogger,
1059
+ requestId,
1060
+ startedAt
1061
+ });
907
1062
  }
908
1063
  };
909
1064
  }
@@ -932,35 +1087,53 @@ function startHoopilotServer(options = {}) {
932
1087
  url: `http://${host}:${server.port}`
933
1088
  };
934
1089
  }
935
- async function handleModels(client, signal) {
1090
+ async function handleModels(client, signal, logger) {
936
1091
  const upstream = await client.models(signal);
937
1092
  if (!upstream.ok) {
1093
+ if (isUpstreamAuthStatus(upstream.status)) {
1094
+ return proxyError(upstream, logger);
1095
+ }
1096
+ logger.warn(
1097
+ {
1098
+ event: "copilot.models.fallback",
1099
+ upstreamPath: "/models",
1100
+ upstreamStatus: upstream.status
1101
+ },
1102
+ "falling back to built-in model list"
1103
+ );
938
1104
  return jsonResponse({ data: fallbackModels(), object: "list" });
939
1105
  }
1106
+ logUpstreamSuccess(logger, "/models", upstream.status);
940
1107
  return jsonResponse(normalizeModelsResponse(await upstream.json()));
941
1108
  }
942
- async function handleChatCompletions(client, request) {
1109
+ async function handleChatCompletions(client, request, logger) {
943
1110
  const upstream = await client.forwardChatCompletions(await request.text(), request.signal);
1111
+ if (!upstream.ok) {
1112
+ return proxyError(upstream, logger);
1113
+ }
1114
+ logUpstreamSuccess(logger, "/chat/completions", upstream.status);
944
1115
  return proxyResponse(upstream);
945
1116
  }
946
- async function handleCompletions(client, request) {
1117
+ async function handleCompletions(client, request, logger) {
947
1118
  const body = await readJson(request);
948
1119
  const upstream = await client.chatCompletions(
949
1120
  completionsRequestToChatCompletion(body),
950
1121
  request.signal
951
1122
  );
952
1123
  if (!upstream.ok) {
953
- return proxyError(upstream);
1124
+ return proxyError(upstream, logger);
954
1125
  }
1126
+ logUpstreamSuccess(logger, "/chat/completions", upstream.status);
955
1127
  return jsonResponse(chatCompletionToCompletion(await upstream.json()));
956
1128
  }
957
- async function handleResponses(client, request) {
1129
+ async function handleResponses(client, request, logger) {
958
1130
  const body = await readJson(request);
959
1131
  const chatRequest = responsesRequestToChatCompletion(body);
960
1132
  const upstream = await client.chatCompletions(chatRequest, request.signal);
961
1133
  if (!upstream.ok) {
962
- return proxyError(upstream);
1134
+ return proxyError(upstream, logger);
963
1135
  }
1136
+ logUpstreamSuccess(logger, "/chat/completions", upstream.status);
964
1137
  if (body.stream === true && upstream.body) {
965
1138
  return new Response(
966
1139
  responsesStreamFromChatStream(upstream.body, {
@@ -978,8 +1151,19 @@ async function handleResponses(client, request) {
978
1151
  }
979
1152
  return jsonResponse(chatCompletionToResponse(await upstream.json()));
980
1153
  }
981
- async function proxyError(upstream) {
1154
+ async function proxyError(upstream, logger) {
982
1155
  const text = await upstream.text();
1156
+ if (isUpstreamAuthStatus(upstream.status)) {
1157
+ logger.warn(
1158
+ { event: "copilot.auth.rejected", upstreamStatus: upstream.status },
1159
+ "copilot rejected credential or account access"
1160
+ );
1161
+ return jsonError(401, "copilot_auth_error", upstreamAuthMessage(text || upstream.statusText));
1162
+ }
1163
+ logger.warn(
1164
+ { event: "copilot.request.failed", upstreamStatus: upstream.status },
1165
+ "copilot upstream request failed"
1166
+ );
983
1167
  return jsonError(upstream.status, "copilot_error", text || upstream.statusText);
984
1168
  }
985
1169
  function proxyResponse(upstream) {
@@ -1001,7 +1185,7 @@ async function readJson(request) {
1001
1185
  const value = await request.json();
1002
1186
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1003
1187
  } catch {
1004
- throw new Error("Request body must be valid JSON.");
1188
+ throw new Error(INVALID_JSON_MESSAGE);
1005
1189
  }
1006
1190
  }
1007
1191
  function jsonResponse(body, status = 200) {
@@ -1040,27 +1224,134 @@ function isAuthorized(request, apiKey) {
1040
1224
  const bearer = authorization.match(/^Bearer\s+(.+)$/i)?.[1];
1041
1225
  return bearer === apiKey || request.headers.get("x-api-key") === apiKey;
1042
1226
  }
1227
+ function isUpstreamAuthStatus(status) {
1228
+ return status === 401 || status === 403;
1229
+ }
1230
+ function upstreamAuthMessage(message) {
1231
+ return `GitHub Copilot rejected the credential or account access: ${message}`;
1232
+ }
1043
1233
  function isLoopbackHost(host) {
1044
1234
  return host === "localhost" || host === "127.0.0.1" || host === "::1";
1045
1235
  }
1046
- function errorMessage2(error) {
1236
+ function errorMessage(error) {
1047
1237
  return error instanceof Error ? error.message : String(error);
1048
1238
  }
1239
+ function serverLogger(options) {
1240
+ if (options.logger) {
1241
+ return options.logger.child({ component: "server" });
1242
+ }
1243
+ if (shouldCreateLogger(options)) {
1244
+ return createHoopilotLogger({
1245
+ env: options.env,
1246
+ format: options.logFormat,
1247
+ level: options.logLevel
1248
+ }).child({ component: "server" });
1249
+ }
1250
+ return noopLogger;
1251
+ }
1252
+ function finishResponse(response, options) {
1253
+ const withRequestId = responseWithRequestId(response, options.requestId);
1254
+ logRequestCompleted(options.logger, withRequestId, options.startedAt);
1255
+ return withRequestId;
1256
+ }
1257
+ function responseWithRequestId(response, requestId) {
1258
+ const headers = new Headers(response.headers);
1259
+ headers.set("x-request-id", requestId);
1260
+ return new Response(response.body, {
1261
+ headers,
1262
+ status: response.status,
1263
+ statusText: response.statusText
1264
+ });
1265
+ }
1266
+ function logRequestCompleted(logger, response, startedAt) {
1267
+ const fields = {
1268
+ durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
1269
+ event: "http.request.completed",
1270
+ status: response.status,
1271
+ stream: isStreamingResponse(response)
1272
+ };
1273
+ if (response.status >= 500) {
1274
+ logger.error(fields, "request completed with server error");
1275
+ return;
1276
+ }
1277
+ if (response.status >= 400) {
1278
+ logger.warn(fields, "request completed with client error");
1279
+ return;
1280
+ }
1281
+ logger.info(fields, "request completed");
1282
+ }
1283
+ function requestIdFor(request) {
1284
+ const existing = request.headers.get("x-request-id")?.trim();
1285
+ return existing || crypto.randomUUID();
1286
+ }
1287
+ function routeFor(method, path) {
1288
+ if (method === "OPTIONS") {
1289
+ return "cors.preflight";
1290
+ }
1291
+ if (method === "GET" && (path === "/" || path === "/healthz")) {
1292
+ return "health";
1293
+ }
1294
+ if (method === "GET" && path === "/v1/models") {
1295
+ return "models";
1296
+ }
1297
+ if (method === "POST" && path === "/v1/chat/completions") {
1298
+ return "chat_completions";
1299
+ }
1300
+ if (method === "POST" && path === "/v1/completions") {
1301
+ return "completions";
1302
+ }
1303
+ if (method === "POST" && path === "/v1/responses") {
1304
+ return "responses";
1305
+ }
1306
+ return "not_found";
1307
+ }
1308
+ function isStreamingResponse(response) {
1309
+ return response.headers.get("content-type")?.includes("text/event-stream") ?? false;
1310
+ }
1311
+ function logUpstreamSuccess(logger, upstreamPath, status) {
1312
+ logger.debug(
1313
+ {
1314
+ event: "copilot.request.completed",
1315
+ upstreamPath,
1316
+ upstreamStatus: status
1317
+ },
1318
+ "copilot upstream request completed"
1319
+ );
1320
+ }
1321
+ function errorDetails(error) {
1322
+ if (error instanceof Error) {
1323
+ return {
1324
+ message: error.message,
1325
+ name: error.name,
1326
+ stack: error.stack
1327
+ };
1328
+ }
1329
+ return { message: String(error) };
1330
+ }
1049
1331
  // Annotate the CommonJS export names for ESM import in node:
1050
1332
  0 && (module.exports = {
1051
1333
  CopilotAuth,
1052
1334
  CopilotAuthError,
1053
1335
  CopilotClient,
1336
+ DEFAULT_LOG_FORMAT,
1337
+ DEFAULT_LOG_LEVEL,
1054
1338
  DEFAULT_MODEL,
1339
+ authStorePath,
1055
1340
  chatCompletionToCompletion,
1056
1341
  chatCompletionToResponse,
1057
1342
  completionsRequestToChatCompletion,
1058
1343
  createHoopilotHandler,
1344
+ createHoopilotLogger,
1059
1345
  fallbackModels,
1346
+ githubCopilotDeviceLogin,
1347
+ noopLogger,
1060
1348
  normalizeModelsResponse,
1349
+ parseLogFormat,
1350
+ parseLogLevel,
1351
+ readStoredCopilotAuth,
1061
1352
  responsesRequestToChatCompletion,
1062
1353
  responsesStreamFromChatStream,
1063
- splitCommand,
1064
- startHoopilotServer
1354
+ startHoopilotServer,
1355
+ writeStoredCopilotAuth
1065
1356
  });
1066
1357
  //# sourceMappingURL=index.cjs.map