opencodekit 0.18.18 → 0.18.20

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.20";
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
  }
@@ -351,54 +360,161 @@ function swapModelInBody(
351
360
 
352
361
  // Maximum length for item IDs in the OpenAI Responses API
353
362
  const MAX_RESPONSE_API_ID_LENGTH = 64;
363
+ // OpenAI Responses API only allows: letters, numbers, underscores, dashes
364
+ const INVALID_ID_CHARS = /[^a-zA-Z0-9_-]/g;
365
+
366
+ /** Check if an ID contains characters not allowed by the Responses API */
367
+ function hasInvalidIdChars(id: string): boolean {
368
+ // Use a non-global regex for .test() to avoid lastIndex state bug
369
+ return /[^a-zA-Z0-9_-]/.test(id);
370
+ }
371
+
354
372
  /**
355
- * Sanitize an ID to fit within the Responses API 64-char limit.
356
- * GitHub Copilot returns proprietary long IDs (400+ chars) that violate
357
- * the OpenAI spec. We hash them to a deterministic 64-char string.
358
- * Preserves the original prefix (e.g., "fc_", "msg_", "call_") so that
359
- * OpenAI's prefix validation passes.
373
+ * Sanitize an ID for the Responses API.
374
+ * Handles three issues from GitHub Copilot:
375
+ * 1. Invalid characters Copilot IDs contain +, |, /, = (base64-like encoding)
376
+ * 2. Wrong prefix GPT models return "h_" instead of "fc_" for function_call items
377
+ * 3. Excessive length — Copilot returns 400+ char IDs (max is 64)
378
+ *
379
+ * Approach matches anomalyco/opencode: replace invalid chars with "_", preserve prefix,
380
+ * truncate to 64 chars, strip trailing underscores.
381
+ *
382
+ * @param id - The original ID to sanitize
383
+ * @param forcedPrefix - If provided, use this prefix instead of the detected one.
360
384
  * See: https://github.com/vercel/ai/issues/5171
361
385
  */
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
386
+ function sanitizeResponseId(id: string, forcedPrefix?: string): string {
387
+ if (!id) return id;
388
+
389
+ // Detect the original prefix (e.g., "fc_", "msg_", "call_", "resp_", "h_")
366
390
  const prefixMatch = id.match(/^([a-z]+_)/);
367
- const prefix = prefixMatch ? prefixMatch[1] : "";
368
- // Hash the full ID for deterministic uniqueness
391
+ const detectedPrefix = prefixMatch ? prefixMatch[1] : "";
392
+ const prefix = forcedPrefix ?? detectedPrefix;
393
+
394
+ // Strip the original prefix to get the core ID
395
+ const rawCore = id.slice(detectedPrefix.length);
396
+ // Replace invalid characters with underscores (same as anomalyco/opencode)
397
+ const cleanCore = rawCore.replace(INVALID_ID_CHARS, "_").replace(/_+$/g, "");
398
+
399
+ // Check if any sanitization is actually needed
400
+ const needsSanitization = forcedPrefix || hasInvalidIdChars(rawCore) ||
401
+ id.length > MAX_RESPONSE_API_ID_LENGTH;
402
+
403
+ if (!needsSanitization) return id;
404
+
405
+ // If result fits within length and core is non-empty, use cleaned core directly
406
+ if (cleanCore.length > 0 && (prefix.length + cleanCore.length) <= MAX_RESPONSE_API_ID_LENGTH) {
407
+ return `${prefix}${cleanCore}`;
408
+ }
409
+
410
+ // Hash the full original ID for deterministic uniqueness when truncating
369
411
  let hash = 0;
370
412
  for (let i = 0; i < id.length; i++) {
371
413
  hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
372
414
  }
373
415
  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);
376
416
  const maxMiddleLen =
377
417
  MAX_RESPONSE_API_ID_LENGTH - prefix.length - hashStr.length - 1;
378
- const middle = afterPrefix.slice(0, Math.max(0, maxMiddleLen));
418
+ const middle = cleanCore.slice(0, Math.max(0, maxMiddleLen));
379
419
  // Format: prefix + middle + "_" + hash (ensure total <= 64)
380
- return `${prefix}${middle}_${hashStr}`.slice(0, MAX_RESPONSE_API_ID_LENGTH);
420
+ const result = `${prefix}${middle}_${hashStr}`.slice(0, MAX_RESPONSE_API_ID_LENGTH);
421
+ // Strip trailing underscores from truncation
422
+ return result.replace(/_+$/, "");
423
+ }
424
+
425
+ /**
426
+ * Check if an ID has the expected prefix for its item type.
427
+ * Returns the expected prefix if the ID is wrong, or null if it's fine.
428
+ */
429
+ function getExpectedPrefix(item: any): string | null {
430
+ if (!item || typeof item !== "object" || !item.type) return null;
431
+ const expected = RESPONSES_API_EXPECTED_PREFIXES[item.type];
432
+ if (!expected) return null;
433
+ if (typeof item.id === "string" && !item.id.startsWith(expected)) {
434
+ return expected;
435
+ }
436
+ return null;
437
+ }
438
+
439
+ /** Check if a string ID needs sanitization (invalid chars or too long) */
440
+ function idNeedsSanitization(id: string): boolean {
441
+ return id.length > MAX_RESPONSE_API_ID_LENGTH || hasInvalidIdChars(id);
381
442
  }
382
443
 
383
444
  /**
384
445
  * Sanitize all IDs in a Responses API input array.
385
- * Recursively checks `id` and `call_id` fields on each input item.
446
+ *
447
+ * Handles THREE classes of invalid IDs:
448
+ * 1. Invalid characters — Copilot IDs contain +, |, /, = (only [a-zA-Z0-9_-] allowed)
449
+ * 2. Wrong prefix — Copilot GPT models return IDs like "h_xxx" instead of "fc_xxx"
450
+ * 3. Excessive length — Copilot returns 400+ char IDs that exceed the 64-char limit.
451
+ *
452
+ * Uses a two-pass approach:
453
+ * - Pass 1: Build an ID remap for all invalid IDs
454
+ * - Pass 2: Apply the remap to both `id` and `call_id` fields consistently,
455
+ * so function_call_output.call_id stays in sync with function_call.id
386
456
  */
387
457
  function sanitizeResponseInputIds(input: any[]): any[] {
388
- return input.map((item: any) => {
389
- if (!item || typeof item !== "object") return item;
390
- const sanitized = { ...item };
458
+ // Pass 1: Build ID remapping
459
+ const idRemap = new Map<string, string>();
460
+
461
+ for (const item of input) {
462
+ if (!item || typeof item !== "object") continue;
463
+
464
+ // Check for wrong prefix (e.g., function_call with "h_" instead of "fc_")
465
+ const expectedPrefix = getExpectedPrefix(item);
466
+ if (expectedPrefix && typeof item.id === "string" && !idRemap.has(item.id)) {
467
+ const newId = sanitizeResponseId(item.id, expectedPrefix);
468
+ if (newId !== item.id) {
469
+ idRemap.set(item.id, newId);
470
+ }
471
+ }
472
+
473
+ // Check for invalid chars or excessive length on id
474
+ if (
475
+ typeof item.id === "string" &&
476
+ idNeedsSanitization(item.id) &&
477
+ !idRemap.has(item.id)
478
+ ) {
479
+ idRemap.set(item.id, sanitizeResponseId(item.id));
480
+ }
481
+
482
+ // Check for wrong prefix on call_id (defensive: handles truncated conversations
483
+ // where function_call_output appears without its corresponding function_call)
391
484
  if (
392
- typeof sanitized.id === "string" &&
393
- sanitized.id.length > MAX_RESPONSE_API_ID_LENGTH
485
+ item.type === "function_call_output" &&
486
+ typeof item.call_id === "string" &&
487
+ !item.call_id.startsWith("fc_") &&
488
+ !idRemap.has(item.call_id)
394
489
  ) {
395
- sanitized.id = sanitizeResponseId(sanitized.id);
490
+ const newCallId = sanitizeResponseId(item.call_id, "fc_");
491
+ if (newCallId !== item.call_id) {
492
+ idRemap.set(item.call_id, newCallId);
493
+ }
396
494
  }
495
+
496
+ // Check for invalid chars or excessive length on call_id
397
497
  if (
398
- typeof sanitized.call_id === "string" &&
399
- sanitized.call_id.length > MAX_RESPONSE_API_ID_LENGTH
498
+ typeof item.call_id === "string" &&
499
+ idNeedsSanitization(item.call_id) &&
500
+ !idRemap.has(item.call_id)
400
501
  ) {
401
- sanitized.call_id = sanitizeResponseId(sanitized.call_id);
502
+ idRemap.set(item.call_id, sanitizeResponseId(item.call_id));
503
+ }
504
+ }
505
+
506
+ // No changes needed
507
+ if (idRemap.size === 0) return input;
508
+
509
+ // Pass 2: Apply remapping to both id and call_id fields
510
+ return input.map((item: any) => {
511
+ if (!item || typeof item !== "object") return item;
512
+ const sanitized = { ...item };
513
+ if (typeof sanitized.id === "string" && idRemap.has(sanitized.id)) {
514
+ sanitized.id = idRemap.get(sanitized.id);
515
+ }
516
+ if (typeof sanitized.call_id === "string" && idRemap.has(sanitized.call_id)) {
517
+ sanitized.call_id = idRemap.get(sanitized.call_id);
402
518
  }
403
519
  return sanitized;
404
520
  });
@@ -600,21 +716,22 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
600
716
 
601
717
  // Responses API
602
718
  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
719
+ // Sanitize IDs from Copilot backend:
720
+ // 1. Wrong prefix GPT models return "h_xxx" instead of "fc_xxx"
721
+ // 2. Excessive length — Copilot returns 400+ char IDs (max is 64)
605
722
  const sanitizedInput = sanitizeResponseInputIds(body.input);
606
723
  const inputWasSanitized =
607
724
  sanitizedInput !== body.input &&
608
725
  JSON.stringify(sanitizedInput) !== JSON.stringify(body.input);
609
726
 
610
727
  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),
728
+ log("info", "Sanitized IDs in Responses API input (prefix or length)", {
729
+ items_fixed: body.input.filter(
730
+ (item: any, i: number) =>
731
+ item && sanitizedInput[i] && (
732
+ item.id !== sanitizedInput[i].id ||
733
+ item.call_id !== sanitizedInput[i].call_id
734
+ ),
618
735
  ).length,
619
736
  });
620
737
  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.20",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": [
6
6
  "agents",