opencodekit 0.18.17 → 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.17";
23
+ var version = "0.18.19";
24
24
 
25
25
  //#endregion
26
26
  //#region src/utils/license.ts
Binary file
@@ -161,8 +161,8 @@
161
161
  "claude-haiku-4.5": {
162
162
  "attachment": true,
163
163
  "limit": {
164
- "context": 200000,
165
- "output": 64000
164
+ "context": 144000,
165
+ "output": 32000
166
166
  },
167
167
  "options": {
168
168
  "thinking_budget": 10000,
@@ -174,13 +174,13 @@
174
174
  "variants": {
175
175
  "high": {
176
176
  "options": {
177
- "thinking_budget": 8000,
177
+ "thinking_budget": 16000,
178
178
  "type": "enabled"
179
179
  }
180
180
  },
181
181
  "max": {
182
182
  "options": {
183
- "thinking_budget": 16000,
183
+ "thinking_budget": 32000,
184
184
  "type": "enabled"
185
185
  }
186
186
  }
@@ -189,8 +189,8 @@
189
189
  "claude-opus-4.5": {
190
190
  "attachment": true,
191
191
  "limit": {
192
- "context": 200000,
193
- "output": 64000
192
+ "context": 160000,
193
+ "output": 32000
194
194
  },
195
195
  "options": {
196
196
  "thinking_budget": 10000
@@ -201,12 +201,12 @@
201
201
  "variants": {
202
202
  "high": {
203
203
  "options": {
204
- "thinking_budget": 8000
204
+ "thinking_budget": 16000
205
205
  }
206
206
  },
207
207
  "max": {
208
208
  "options": {
209
- "thinking_budget": 16000
209
+ "thinking_budget": 32000
210
210
  }
211
211
  }
212
212
  }
@@ -214,7 +214,7 @@
214
214
  "claude-opus-4.6": {
215
215
  "attachment": true,
216
216
  "limit": {
217
- "context": 200000,
217
+ "context": 144000,
218
218
  "output": 64000
219
219
  },
220
220
  "options": {
@@ -259,21 +259,33 @@
259
259
  "claude-sonnet-4": {
260
260
  "attachment": true,
261
261
  "limit": {
262
- "context": 200000,
263
- "output": 64000
262
+ "context": 216000,
263
+ "output": 16000
264
264
  },
265
265
  "options": {
266
266
  "thinking_budget": 10000
267
267
  },
268
268
  "reasoning": true,
269
269
  "temperature": true,
270
- "tool_call": true
270
+ "tool_call": true,
271
+ "variants": {
272
+ "high": {
273
+ "options": {
274
+ "thinking_budget": 16000
275
+ }
276
+ },
277
+ "max": {
278
+ "options": {
279
+ "thinking_budget": 32000
280
+ }
281
+ }
282
+ }
271
283
  },
272
284
  "claude-sonnet-4.5": {
273
285
  "attachment": true,
274
286
  "limit": {
275
- "context": 200000,
276
- "output": 64000
287
+ "context": 144000,
288
+ "output": 32000
277
289
  },
278
290
  "options": {
279
291
  "thinking_budget": 10000
@@ -284,12 +296,12 @@
284
296
  "variants": {
285
297
  "high": {
286
298
  "options": {
287
- "thinking_budget": 8000
299
+ "thinking_budget": 16000
288
300
  }
289
301
  },
290
302
  "max": {
291
303
  "options": {
292
- "thinking_budget": 16000
304
+ "thinking_budget": 32000
293
305
  }
294
306
  }
295
307
  }
@@ -298,7 +310,7 @@
298
310
  "attachment": true,
299
311
  "limit": {
300
312
  "context": 200000,
301
- "output": 64000
313
+ "output": 32000
302
314
  },
303
315
  "options": {
304
316
  "thinking": {
@@ -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
  }
@@ -355,41 +364,135 @@ const MAX_RESPONSE_API_ID_LENGTH = 64;
355
364
  * Sanitize an ID to fit within the Responses API 64-char limit.
356
365
  * GitHub Copilot returns proprietary long IDs (400+ chars) that violate
357
366
  * the OpenAI spec. We hash them to a deterministic 64-char string.
367
+ * Preserves the original prefix (e.g., "fc_", "msg_", "call_") so that
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_").
358
373
  * See: https://github.com/vercel/ai/issues/5171
359
374
  */
360
- function sanitizeResponseId(id: string): string {
361
- if (!id || id.length <= MAX_RESPONSE_API_ID_LENGTH) return id;
362
- // Use a simple hash: take first 8 chars + hash of full string for uniqueness
363
- // Format: "h_" + first 8 chars + "_" + base36 hash (up to ~50 chars total)
375
+ function sanitizeResponseId(id: string, forcedPrefix?: string): string {
376
+ // Detect the original prefix (e.g., "fc_", "msg_", "call_", "resp_", "h_")
377
+ const prefixMatch = id.match(/^([a-z]+_)/);
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
364
395
  let hash = 0;
365
396
  for (let i = 0; i < id.length; i++) {
366
397
  hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
367
398
  }
368
399
  const hashStr = Math.abs(hash).toString(36);
369
- const prefix = id.slice(0, 8);
370
- // Ensure total length <= 64: "h_" (2) + prefix (8) + "_" (1) + hash
371
- return `h_${prefix}_${hashStr}`.slice(0, MAX_RESPONSE_API_ID_LENGTH);
400
+ // Take some chars from the core for additional uniqueness
401
+ const maxMiddleLen =
402
+ MAX_RESPONSE_API_ID_LENGTH - prefix.length - hashStr.length - 1;
403
+ const middle = coreId.slice(0, Math.max(0, maxMiddleLen));
404
+ // Format: prefix + middle + "_" + hash (ensure total <= 64)
405
+ return `${prefix}${middle}_${hashStr}`.slice(0, MAX_RESPONSE_API_ID_LENGTH);
406
+ }
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;
372
420
  }
373
421
 
374
422
  /**
375
423
  * Sanitize all IDs in a Responses API input array.
376
- * 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
377
434
  */
378
435
  function sanitizeResponseInputIds(input: any[]): any[] {
379
- return input.map((item: any) => {
380
- if (!item || typeof item !== "object") return item;
381
- 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
382
452
  if (
383
- typeof sanitized.id === "string" &&
384
- 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)
385
456
  ) {
386
- 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
+ }
387
472
  }
473
+
474
+ // Check for excessive length on call_id
388
475
  if (
389
- typeof sanitized.call_id === "string" &&
390
- 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)
391
479
  ) {
392
- 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);
393
496
  }
394
497
  return sanitized;
395
498
  });
@@ -513,7 +616,12 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
513
616
  return cleanedMsg;
514
617
  }
515
618
 
516
- // If content is an array, check for thinking blocks
619
+ // If content is an array, strip ALL thinking blocks.
620
+ // Reasoning is communicated via reasoning_text/reasoning_opaque
621
+ // fields, not via thinking blocks in the content array.
622
+ // Even thinking blocks WITH signatures can cause
623
+ // "Invalid signature in thinking block" errors when
624
+ // signatures are expired or from a different context.
517
625
  if (Array.isArray(msg.content)) {
518
626
  const hasThinkingBlock = msg.content.some(
519
627
  (part: any) => part.type === "thinking",
@@ -521,22 +629,10 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
521
629
  if (hasThinkingBlock) {
522
630
  log(
523
631
  "debug",
524
- `Message ${idx} has thinking blocks in content array`,
632
+ `Stripping all thinking blocks from message ${idx}`,
525
633
  );
526
- // Filter out thinking blocks without signatures
527
634
  const cleanedContent = msg.content.filter(
528
- (part: any) => {
529
- if (part.type === "thinking") {
530
- if (!part.signature) {
531
- log(
532
- "warn",
533
- `Removing thinking block without signature`,
534
- );
535
- return false;
536
- }
537
- }
538
- return true;
539
- },
635
+ (part: any) => part.type !== "thinking",
540
636
  );
541
637
  return {
542
638
  ...msg,
@@ -598,21 +694,22 @@ export const CopilotAuthPlugin: Plugin = async ({ client: sdk }) => {
598
694
 
599
695
  // Responses API
600
696
  if (body?.input) {
601
- // Sanitize long IDs from Copilot backend (can be 400+ chars)
602
- // 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)
603
700
  const sanitizedInput = sanitizeResponseInputIds(body.input);
604
701
  const inputWasSanitized =
605
702
  sanitizedInput !== body.input &&
606
703
  JSON.stringify(sanitizedInput) !== JSON.stringify(body.input);
607
704
 
608
705
  if (inputWasSanitized) {
609
- log("info", "Sanitized long IDs in Responses API input", {
610
- original_count: body.input.filter(
611
- (item: any) =>
612
- (typeof item?.id === "string" &&
613
- item.id.length > MAX_RESPONSE_API_ID_LENGTH) ||
614
- (typeof item?.call_id === "string" &&
615
- 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
+ ),
616
713
  ).length,
617
714
  });
618
715
  modifiedBody = {
@@ -90,24 +90,51 @@ export function createHooks(deps: HookDeps) {
90
90
  }
91
91
  }
92
92
 
93
- // --- Session error: show actual error details ---
93
+ // --- Session error: classify and guide ---
94
94
  if (event.type === "session.error") {
95
95
  const props = event.properties as Record<string, unknown> | undefined;
96
96
  const errorMsg = props?.error
97
- ? String(props.error).slice(0, 120)
97
+ ? String(props.error).slice(0, 200)
98
98
  : props?.message
99
- ? String(props.message).slice(0, 120)
99
+ ? String(props.message).slice(0, 200)
100
100
  : "Unknown error";
101
101
 
102
- // Classify: match the specific AI SDK error pattern
103
- const isTokenOverflow =
102
+ // Log full error for debugging
103
+ await log(`Session error: ${errorMsg}`, "warn");
104
+
105
+ // Classify error and provide specific guidance
106
+ let guidance: string;
107
+ if (
104
108
  /token.{0,20}(exceed|limit)/i.test(errorMsg) ||
105
- errorMsg.includes("context_length_exceeded");
106
- const guidance = isTokenOverflow
107
- ? "Context too large — use /compact or start a new session"
108
- : "Save important learnings with observation tool";
109
+ errorMsg.includes("context_length_exceeded")
110
+ ) {
111
+ guidance = "Context too large — use /compact or start a new session";
112
+ } else if (
113
+ /rate.?limit|429|too many requests/i.test(errorMsg)
114
+ ) {
115
+ guidance = "Rate limited — wait a moment and retry";
116
+ } else if (
117
+ /unauthorized|401|403|auth/i.test(errorMsg)
118
+ ) {
119
+ guidance = "Auth error — check API key or token";
120
+ } else if (
121
+ /timeout|ETIMEDOUT|ECONNRESET|network|fetch failed/i.test(errorMsg)
122
+ ) {
123
+ guidance = "Network error — check connection and retry";
124
+ } else if (
125
+ /invalid.*signature|thinking block/i.test(errorMsg)
126
+ ) {
127
+ guidance = "API format error — try starting a new session";
128
+ } else if (
129
+ /500|502|503|504|internal server|service unavailable/i.test(errorMsg)
130
+ ) {
131
+ guidance = "Server error — retry in a few seconds";
132
+ } else {
133
+ guidance = "Unexpected error — save work with observation tool if needed";
134
+ }
109
135
 
110
- await showToast("Session Error", `${guidance} (${errorMsg})`, "warning");
136
+ const short = errorMsg.length > 80 ? `${errorMsg.slice(0, 80)}…` : errorMsg;
137
+ await showToast("Session Error", `${guidance} (${short})`, "warning");
111
138
  }
112
139
  },
113
140
 
@@ -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.17",
3
+ "version": "0.18.19",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": [
6
6
  "agents",