opencodekit 0.18.18 → 0.18.19

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
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "0.18.18";
23
+ var version = "0.18.19";
24
24
 
25
25
  //#endregion
26
26
  //#region src/utils/license.ts
Binary file
@@ -67,6 +67,15 @@ const RESPONSES_API_ALTERNATE_INPUT_TYPES = [
67
67
  "reasoning",
68
68
  ];
69
69
 
70
+ // Expected ID prefixes per Responses API item type.
71
+ // The OpenAI Responses API validates that item IDs start with specific prefixes.
72
+ // GitHub Copilot's backend (especially GPT models) returns non-standard prefixes
73
+ // (e.g., "h_" instead of "fc_") that the API then rejects on replay.
74
+ const RESPONSES_API_EXPECTED_PREFIXES: Record<string, string> = {
75
+ function_call: "fc_",
76
+ // function_call_output.call_id prefix is validated directly in sanitizeResponseInputIds
77
+ };
78
+
70
79
  function normalizeDomain(url: string): string {
71
80
  return url.replace(/^https?:\/\//, "").replace(/\/$/, "");
72
81
  }
@@ -357,48 +366,133 @@ const MAX_RESPONSE_API_ID_LENGTH = 64;
357
366
  * the OpenAI spec. We hash them to a deterministic 64-char string.
358
367
  * Preserves the original prefix (e.g., "fc_", "msg_", "call_") so that
359
368
  * OpenAI's prefix validation passes.
369
+ *
370
+ * @param id - The original ID to sanitize
371
+ * @param forcedPrefix - If provided, use this prefix instead of the detected one.
372
+ * Used when the original prefix is wrong (e.g., Copilot returns "h_" instead of "fc_").
360
373
  * See: https://github.com/vercel/ai/issues/5171
361
374
  */
362
- function sanitizeResponseId(id: string): string {
363
- if (!id || id.length <= MAX_RESPONSE_API_ID_LENGTH) return id;
364
- // Detect and preserve the original prefix (e.g., "fc_", "msg_", "call_", "resp_")
365
- // The OpenAI Responses API validates that IDs start with specific prefixes
375
+ function sanitizeResponseId(id: string, forcedPrefix?: string): string {
376
+ // Detect the original prefix (e.g., "fc_", "msg_", "call_", "resp_", "h_")
366
377
  const prefixMatch = id.match(/^([a-z]+_)/);
367
- const prefix = prefixMatch ? prefixMatch[1] : "";
368
- // Hash the full ID for deterministic uniqueness
378
+ const detectedPrefix = prefixMatch ? prefixMatch[1] : "";
379
+ const prefix = forcedPrefix ?? detectedPrefix;
380
+
381
+ // If no forced prefix and within length, return as-is
382
+ if (!forcedPrefix && (!id || id.length <= MAX_RESPONSE_API_ID_LENGTH)) {
383
+ return id;
384
+ }
385
+
386
+ // Strip the original prefix to get the core ID
387
+ const coreId = id.slice(detectedPrefix.length);
388
+
389
+ // If just a prefix swap and within length, do a simple replacement
390
+ if (forcedPrefix && (prefix.length + coreId.length) <= MAX_RESPONSE_API_ID_LENGTH) {
391
+ return `${prefix}${coreId}`;
392
+ }
393
+
394
+ // Hash the full original ID for deterministic uniqueness
369
395
  let hash = 0;
370
396
  for (let i = 0; i < id.length; i++) {
371
397
  hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
372
398
  }
373
399
  const hashStr = Math.abs(hash).toString(36);
374
- // Take some chars from after the prefix for additional uniqueness
375
- const afterPrefix = id.slice(prefix.length);
400
+ // Take some chars from the core for additional uniqueness
376
401
  const maxMiddleLen =
377
402
  MAX_RESPONSE_API_ID_LENGTH - prefix.length - hashStr.length - 1;
378
- const middle = afterPrefix.slice(0, Math.max(0, maxMiddleLen));
403
+ const middle = coreId.slice(0, Math.max(0, maxMiddleLen));
379
404
  // Format: prefix + middle + "_" + hash (ensure total <= 64)
380
405
  return `${prefix}${middle}_${hashStr}`.slice(0, MAX_RESPONSE_API_ID_LENGTH);
381
406
  }
382
407
 
408
+ /**
409
+ * Check if an ID has the expected prefix for its item type.
410
+ * Returns the expected prefix if the ID is wrong, or null if it's fine.
411
+ */
412
+ function getExpectedPrefix(item: any): string | null {
413
+ if (!item || typeof item !== "object" || !item.type) return null;
414
+ const expected = RESPONSES_API_EXPECTED_PREFIXES[item.type];
415
+ if (!expected) return null;
416
+ if (typeof item.id === "string" && !item.id.startsWith(expected)) {
417
+ return expected;
418
+ }
419
+ return null;
420
+ }
421
+
383
422
  /**
384
423
  * Sanitize all IDs in a Responses API input array.
385
- * Recursively checks `id` and `call_id` fields on each input item.
424
+ *
425
+ * Handles TWO classes of invalid IDs:
426
+ * 1. Wrong prefix — Copilot GPT models return IDs like "h_xxx" instead of "fc_xxx"
427
+ * for function_call items. These are short but have the wrong prefix.
428
+ * 2. Excessive length — Copilot returns 400+ char IDs that exceed the 64-char limit.
429
+ *
430
+ * Uses a two-pass approach:
431
+ * - Pass 1: Build an ID remap for all invalid IDs (both prefix and length issues)
432
+ * - Pass 2: Apply the remap to both `id` and `call_id` fields consistently,
433
+ * so function_call_output.call_id stays in sync with function_call.id
386
434
  */
387
435
  function sanitizeResponseInputIds(input: any[]): any[] {
388
- return input.map((item: any) => {
389
- if (!item || typeof item !== "object") return item;
390
- const sanitized = { ...item };
436
+ // Pass 1: Build ID remapping
437
+ const idRemap = new Map<string, string>();
438
+
439
+ for (const item of input) {
440
+ if (!item || typeof item !== "object") continue;
441
+
442
+ // Check for wrong prefix (e.g., function_call with "h_" instead of "fc_")
443
+ const expectedPrefix = getExpectedPrefix(item);
444
+ if (expectedPrefix && typeof item.id === "string" && !idRemap.has(item.id)) {
445
+ const newId = sanitizeResponseId(item.id, expectedPrefix);
446
+ if (newId !== item.id) {
447
+ idRemap.set(item.id, newId);
448
+ }
449
+ }
450
+
451
+ // Check for excessive length on id
391
452
  if (
392
- typeof sanitized.id === "string" &&
393
- sanitized.id.length > MAX_RESPONSE_API_ID_LENGTH
453
+ typeof item.id === "string" &&
454
+ item.id.length > MAX_RESPONSE_API_ID_LENGTH &&
455
+ !idRemap.has(item.id)
394
456
  ) {
395
- sanitized.id = sanitizeResponseId(sanitized.id);
457
+ idRemap.set(item.id, sanitizeResponseId(item.id));
458
+ }
459
+
460
+ // Check for wrong prefix on call_id (defensive: handles truncated conversations
461
+ // where function_call_output appears without its corresponding function_call)
462
+ if (
463
+ item.type === "function_call_output" &&
464
+ typeof item.call_id === "string" &&
465
+ !item.call_id.startsWith("fc_") &&
466
+ !idRemap.has(item.call_id)
467
+ ) {
468
+ const newCallId = sanitizeResponseId(item.call_id, "fc_");
469
+ if (newCallId !== item.call_id) {
470
+ idRemap.set(item.call_id, newCallId);
471
+ }
396
472
  }
473
+
474
+ // Check for excessive length on call_id
397
475
  if (
398
- typeof sanitized.call_id === "string" &&
399
- sanitized.call_id.length > MAX_RESPONSE_API_ID_LENGTH
476
+ typeof item.call_id === "string" &&
477
+ item.call_id.length > MAX_RESPONSE_API_ID_LENGTH &&
478
+ !idRemap.has(item.call_id)
400
479
  ) {
401
- sanitized.call_id = sanitizeResponseId(sanitized.call_id);
480
+ idRemap.set(item.call_id, sanitizeResponseId(item.call_id));
481
+ }
482
+ }
483
+
484
+ // No changes needed
485
+ if (idRemap.size === 0) return input;
486
+
487
+ // Pass 2: Apply remapping to both id and call_id fields
488
+ return input.map((item: any) => {
489
+ if (!item || typeof item !== "object") return item;
490
+ const sanitized = { ...item };
491
+ if (typeof sanitized.id === "string" && idRemap.has(sanitized.id)) {
492
+ sanitized.id = idRemap.get(sanitized.id);
493
+ }
494
+ if (typeof sanitized.call_id === "string" && idRemap.has(sanitized.call_id)) {
495
+ sanitized.call_id = idRemap.get(sanitized.call_id);
402
496
  }
403
497
  return sanitized;
404
498
  });
@@ -600,21 +694,22 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
600
694
 
601
695
  // Responses API
602
696
  if (body?.input) {
603
- // Sanitize long IDs from Copilot backend (can be 400+ chars)
604
- // OpenAI Responses API enforces a 64-char max on item IDs
697
+ // Sanitize IDs from Copilot backend:
698
+ // 1. Wrong prefix GPT models return "h_xxx" instead of "fc_xxx"
699
+ // 2. Excessive length — Copilot returns 400+ char IDs (max is 64)
605
700
  const sanitizedInput = sanitizeResponseInputIds(body.input);
606
701
  const inputWasSanitized =
607
702
  sanitizedInput !== body.input &&
608
703
  JSON.stringify(sanitizedInput) !== JSON.stringify(body.input);
609
704
 
610
705
  if (inputWasSanitized) {
611
- log("info", "Sanitized long IDs in Responses API input", {
612
- original_count: body.input.filter(
613
- (item: any) =>
614
- (typeof item?.id === "string" &&
615
- item.id.length > MAX_RESPONSE_API_ID_LENGTH) ||
616
- (typeof item?.call_id === "string" &&
617
- item.call_id.length > MAX_RESPONSE_API_ID_LENGTH),
706
+ log("info", "Sanitized IDs in Responses API input (prefix or length)", {
707
+ items_fixed: body.input.filter(
708
+ (item: any, i: number) =>
709
+ item && sanitizedInput[i] && (
710
+ item.id !== sanitizedInput[i].id ||
711
+ item.call_id !== sanitizedInput[i].call_id
712
+ ),
618
713
  ).length,
619
714
  });
620
715
  modifiedBody = {
@@ -29,13 +29,17 @@ export function isWSL(): boolean {
29
29
  */
30
30
  export async function notify($: any, title: string, message: string): Promise<void> {
31
31
  const platform = process.platform;
32
- const safeTitle = title || "OpenCode";
33
- const safeMessage = message || "";
34
32
 
35
33
  try {
36
34
  if (platform === "darwin") {
35
+ // Escape backslashes and double quotes for AppleScript string literals
36
+ const escapeAS = (s: string) => (s || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
37
+ const safeTitle = escapeAS(title || "OpenCode");
38
+ const safeMessage = escapeAS(message || "");
37
39
  await $`osascript -e ${`display notification "${safeMessage}" with title "${safeTitle}"`}`;
38
40
  } else if (platform === "linux") {
41
+ const safeTitle = title || "OpenCode";
42
+ const safeMessage = message || "";
39
43
  if (isWSL()) {
40
44
  // WSL: try notify-send, fail silently
41
45
  await $`notify-send ${safeTitle} ${safeMessage}`.catch(() => {});
@@ -43,6 +47,10 @@ export async function notify($: any, title: string, message: string): Promise<vo
43
47
  await $`notify-send ${safeTitle} ${safeMessage}`;
44
48
  }
45
49
  } else if (platform === "win32") {
50
+ // Escape single quotes for PowerShell string literals
51
+ const escapePS = (s: string) => (s || "").replace(/'/g, "''");
52
+ const safeTitle = escapePS(title || "OpenCode");
53
+ const safeMessage = escapePS(message || "");
46
54
  // Windows: PowerShell toast (fire and forget)
47
55
  await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${safeMessage}', '${safeTitle}')"`.catch(
48
56
  () => {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.18.18",
3
+ "version": "0.18.19",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": [
6
6
  "agents",