@openhoo/hoopilot 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/auth-store.ts
2
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
3
3
  import { dirname, join } from "path";
4
4
  function authStorePath(env = process.env) {
5
5
  if (env.HOOPILOT_AUTH_FILE) {
@@ -31,25 +31,36 @@ function readStoredCopilotAuth(path = authStorePath()) {
31
31
  }
32
32
  function writeStoredCopilotAuth(auth, path = authStorePath()) {
33
33
  mkdirSync(dirname(path), { recursive: true });
34
- writeFileSync(
35
- path,
36
- `${JSON.stringify(
37
- {
38
- ...auth,
39
- createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
40
- },
41
- null,
42
- 2
43
- )}
44
- `,
45
- { mode: 384 }
46
- );
34
+ const data = `${JSON.stringify(
35
+ {
36
+ ...auth,
37
+ createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
38
+ },
39
+ null,
40
+ 2
41
+ )}
42
+ `;
43
+ const tmpPath = `${path}.${process.pid}.tmp`;
44
+ writeFileSync(tmpPath, data, { mode: 384 });
45
+ renameSync(tmpPath, path);
47
46
  try {
48
47
  chmodSync(path, 384);
49
48
  } catch {
50
49
  }
51
50
  }
52
51
 
52
+ // src/util.ts
53
+ function trimTrailingSlash(value) {
54
+ return value.replace(/\/+$/, "");
55
+ }
56
+ async function truncatedResponseText(response, max = 500) {
57
+ const text = await response.text();
58
+ return text.slice(0, max);
59
+ }
60
+ function asRecord(value) {
61
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
62
+ }
63
+
53
64
  // src/auth.ts
54
65
  var DEFAULT_COPILOT_API_BASE_URL = "https://api.githubcopilot.com";
55
66
  var REFRESH_SKEW_MS = 6e4;
@@ -92,11 +103,19 @@ var CopilotAuth = class {
92
103
  return access;
93
104
  }
94
105
  };
95
- function trimTrailingSlash(value) {
96
- return value.replace(/\/+$/, "");
97
- }
98
106
 
99
107
  // src/copilot.ts
108
+ function applyCopilotHeaders(headers, token) {
109
+ headers.set("accept", headers.get("accept") ?? "application/json");
110
+ headers.set("authorization", `Bearer ${token}`);
111
+ headers.set("copilot-integration-id", "vscode-chat");
112
+ headers.set("editor-plugin-version", "hoopilot/0.1.0");
113
+ headers.set("editor-version", "Hoopilot/0.1.0");
114
+ headers.set("openai-intent", "conversation-panel");
115
+ headers.set("user-agent", "hoopilot/0.1.0");
116
+ headers.set("x-github-api-version", "2026-06-01");
117
+ return headers;
118
+ }
100
119
  var CopilotClient = class {
101
120
  #auth;
102
121
  #fetch;
@@ -135,15 +154,7 @@ var CopilotClient = class {
135
154
  }
136
155
  async fetchCopilot(path, init) {
137
156
  const access = await this.#auth.getAccess();
138
- const headers = new Headers(init.headers);
139
- headers.set("accept", headers.get("accept") ?? "application/json");
140
- headers.set("authorization", `Bearer ${access.token}`);
141
- headers.set("copilot-integration-id", "vscode-chat");
142
- headers.set("editor-plugin-version", "hoopilot/0.1.0");
143
- headers.set("editor-version", "Hoopilot/0.1.0");
144
- headers.set("openai-intent", "conversation-panel");
145
- headers.set("user-agent", "hoopilot/0.1.0");
146
- headers.set("x-github-api-version", "2026-06-01");
157
+ const headers = applyCopilotHeaders(new Headers(init.headers), access.token);
147
158
  return this.#fetch(`${access.apiBaseUrl}${path}`, {
148
159
  ...init,
149
160
  headers
@@ -157,6 +168,7 @@ var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
157
168
  var DEFAULT_GITHUB_DOMAIN = "github.com";
158
169
  var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
159
170
  var POLLING_SAFETY_MARGIN_MS = 3e3;
171
+ var REQUEST_TIMEOUT_MS = 15e3;
160
172
  async function githubCopilotDeviceLogin(options = {}) {
161
173
  const env = options.env ?? process.env;
162
174
  const fetcher = options.fetch ?? fetch;
@@ -191,16 +203,20 @@ async function requestDeviceCode(fetcher, domain, clientId) {
191
203
  scope: "read:user"
192
204
  }),
193
205
  headers: oauthHeaders(),
194
- method: "POST"
206
+ method: "POST",
207
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
195
208
  });
196
209
  if (!response.ok) {
197
210
  throw new Error(
198
- `GitHub device authorization failed with ${response.status}: ${await safeResponseText(
211
+ `GitHub device authorization failed with ${response.status}: ${await truncatedResponseText(
199
212
  response
200
213
  )}`
201
214
  );
202
215
  }
203
- return await response.json();
216
+ return parseJsonResponse(
217
+ response,
218
+ "GitHub device authorization response was not valid JSON"
219
+ );
204
220
  }
205
221
  async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
206
222
  let intervalMs = device.interval * 1e3 + POLLING_SAFETY_MARGIN_MS;
@@ -214,16 +230,20 @@ async function pollForAccessToken(fetcher, sleeper, domain, clientId, device) {
214
230
  grant_type: DEVICE_GRANT_TYPE
215
231
  }),
216
232
  headers: oauthHeaders(),
217
- method: "POST"
233
+ method: "POST",
234
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
218
235
  });
219
236
  if (!response.ok) {
220
237
  throw new Error(
221
- `GitHub device token exchange failed with ${response.status}: ${await safeResponseText(
238
+ `GitHub device token exchange failed with ${response.status}: ${await truncatedResponseText(
222
239
  response
223
240
  )}`
224
241
  );
225
242
  }
226
- const data = await response.json();
243
+ const data = await parseJsonResponse(
244
+ response,
245
+ "GitHub device token response was not valid JSON"
246
+ );
227
247
  if (data.access_token) {
228
248
  return data.access_token;
229
249
  }
@@ -259,9 +279,13 @@ function normalizeDomain(value) {
259
279
  function positiveSeconds(value, fallback) {
260
280
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
261
281
  }
262
- async function safeResponseText(response) {
282
+ async function parseJsonResponse(response, context) {
263
283
  const text = await response.text();
264
- return text.slice(0, 500);
284
+ try {
285
+ return JSON.parse(text);
286
+ } catch {
287
+ throw new Error(`${context}: ${text.slice(0, 500)}`);
288
+ }
265
289
  }
266
290
 
267
291
  // src/logger.ts
@@ -364,6 +388,16 @@ function shouldCreateLogger(options) {
364
388
  options.logger || options.logFormat || options.logLevel || options.env?.HOOPILOT_LOG_FORMAT || options.env?.HOOPILOT_LOG_LEVEL
365
389
  );
366
390
  }
391
+ function errorDetails(error) {
392
+ if (error instanceof Error) {
393
+ return {
394
+ message: error.message,
395
+ name: error.name,
396
+ stack: error.stack
397
+ };
398
+ }
399
+ return { message: String(error) };
400
+ }
367
401
  function isLogFormat(value) {
368
402
  return LOG_FORMATS.includes(value);
369
403
  }
@@ -541,17 +575,18 @@ function responsesStreamFromChatStream(chatStream, options) {
541
575
  const lines = buffer.split(/\r?\n/);
542
576
  buffer = lines.pop() ?? "";
543
577
  for (const line of lines) {
544
- processChatSseLine(line, enqueue, tools, (delta) => {
578
+ processChatSseLine(messageId, line, enqueue, tools, (delta) => {
545
579
  text += delta;
546
580
  });
547
581
  }
548
582
  }
549
583
  if (buffer) {
550
- processChatSseLine(buffer, enqueue, tools, (delta) => {
584
+ processChatSseLine(messageId, buffer, enqueue, tools, (delta) => {
551
585
  text += delta;
552
586
  });
553
587
  }
554
- const output = streamOutputItems(messageId, text, [...tools.values()]);
588
+ const toolItems = [...tools.values()].map(functionCallItem);
589
+ const output = [messageOutputItem(text, messageId), ...toolItems];
555
590
  enqueue("response.output_text.done", {
556
591
  content_index: 0,
557
592
  item_id: messageId,
@@ -575,8 +610,7 @@ function responsesStreamFromChatStream(chatStream, options) {
575
610
  output_index: 0,
576
611
  type: "response.output_item.done"
577
612
  });
578
- tools.forEach((tool, index) => {
579
- const item = functionCallItem(tool);
613
+ toolItems.forEach((item, index) => {
580
614
  const outputIndex = index + 1;
581
615
  enqueue("response.output_item.added", {
582
616
  item,
@@ -584,7 +618,7 @@ function responsesStreamFromChatStream(chatStream, options) {
584
618
  type: "response.output_item.added"
585
619
  });
586
620
  enqueue("response.function_call_arguments.done", {
587
- arguments: tool.arguments,
621
+ arguments: item.arguments,
588
622
  item_id: item.id,
589
623
  output_index: outputIndex,
590
624
  type: "response.function_call_arguments.done"
@@ -602,6 +636,8 @@ function responsesStreamFromChatStream(chatStream, options) {
602
636
  enqueue("done", "[DONE]");
603
637
  controller.close();
604
638
  } catch (error) {
639
+ await reader.cancel(error).catch(() => {
640
+ });
605
641
  controller.error(error);
606
642
  } finally {
607
643
  reader.releaseLock();
@@ -808,7 +844,7 @@ function firstChoice(completion) {
808
844
  const choices = Array.isArray(completion.choices) ? completion.choices : [];
809
845
  return asRecord(choices[0]);
810
846
  }
811
- function processChatSseLine(line, enqueue, tools, appendText) {
847
+ function processChatSseLine(messageId, line, enqueue, tools, appendText) {
812
848
  const trimmed = line.trim();
813
849
  if (!trimmed.startsWith("data:")) {
814
850
  return;
@@ -829,7 +865,7 @@ function processChatSseLine(line, enqueue, tools, appendText) {
829
865
  enqueue("response.output_text.delta", {
830
866
  content_index: 0,
831
867
  delta: content,
832
- item_id: "",
868
+ item_id: messageId,
833
869
  output_index: 0,
834
870
  type: "response.output_text.delta"
835
871
  });
@@ -851,9 +887,6 @@ function processChatSseLine(line, enqueue, tools, appendText) {
851
887
  tools.set(index, existing);
852
888
  }
853
889
  }
854
- function streamOutputItems(messageId, text, tools) {
855
- return [messageOutputItem(text, messageId), ...tools.map((tool) => functionCallItem(tool))];
856
- }
857
890
  function baseStreamResponse(id, model, createdAt, status, output) {
858
891
  return {
859
892
  created_at: createdAt,
@@ -893,9 +926,6 @@ function parseJson(data) {
893
926
  function removeUndefined(record) {
894
927
  return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== void 0));
895
928
  }
896
- function asRecord(value) {
897
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
898
- }
899
929
  function randomId() {
900
930
  return crypto.randomUUID().replaceAll("-", "");
901
931
  }
@@ -1085,6 +1115,9 @@ async function handleCompletions(client, request, logger) {
1085
1115
  return proxyError(upstream, logger);
1086
1116
  }
1087
1117
  logUpstreamSuccess(logger, "/chat/completions", upstream.status);
1118
+ if (isStreamingResponse(upstream)) {
1119
+ return proxyResponse(upstream);
1120
+ }
1088
1121
  return jsonResponse(chatCompletionToCompletion(await upstream.json()));
1089
1122
  }
1090
1123
  async function handleResponses(client, request, logger) {
@@ -1127,8 +1160,7 @@ function proxyResponse(upstream) {
1127
1160
  }
1128
1161
  async function readJson(request) {
1129
1162
  try {
1130
- const value = await request.json();
1131
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1163
+ return asRecord(await request.json());
1132
1164
  } catch {
1133
1165
  throw new Error(INVALID_JSON_MESSAGE);
1134
1166
  }
@@ -1299,16 +1331,6 @@ function logUpstreamSuccess(logger, upstreamPath, status) {
1299
1331
  "copilot upstream request completed"
1300
1332
  );
1301
1333
  }
1302
- function errorDetails(error) {
1303
- if (error instanceof Error) {
1304
- return {
1305
- message: error.message,
1306
- name: error.name,
1307
- stack: error.stack
1308
- };
1309
- }
1310
- return { message: String(error) };
1311
- }
1312
1334
  export {
1313
1335
  CopilotAuth,
1314
1336
  CopilotAuthError,