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 +1 -1
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/plugin/copilot-auth.ts +151 -34
- package/dist/template/.opencode/plugin/lib/notify.ts +10 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
Binary file
|
|
Binary file
|
|
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
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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
|
|
364
|
-
|
|
365
|
-
//
|
|
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
|
|
368
|
-
|
|
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 =
|
|
418
|
+
const middle = cleanCore.slice(0, Math.max(0, maxMiddleLen));
|
|
379
419
|
// Format: prefix + middle + "_" + hash (ensure total <= 64)
|
|
380
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
|
399
|
-
|
|
498
|
+
typeof item.call_id === "string" &&
|
|
499
|
+
idNeedsSanitization(item.call_id) &&
|
|
500
|
+
!idRemap.has(item.call_id)
|
|
400
501
|
) {
|
|
401
|
-
|
|
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
|
|
604
|
-
//
|
|
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
|
|
612
|
-
|
|
613
|
-
(item: any) =>
|
|
614
|
-
|
|
615
|
-
item.id
|
|
616
|
-
|
|
617
|
-
|
|
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
|
() => {},
|