opencode-anthropic-fix 2.0.0 → 2.1.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.
@@ -1,5 +1,6 @@
1
1
  import { isTruthyEnv } from "../env.mjs";
2
2
  import { isOpus46Model, isOpus47Model, isOpus48Model } from "./wire-compat.mjs";
3
+ import { truncateGraphemes } from "../unicode-text.mjs";
3
4
 
4
5
  /**
5
6
  * The canonical Claude Code identity text.
@@ -146,16 +147,25 @@ export function tailSystemBlock(text, maxChars, turnThreshold) {
146
147
  }
147
148
 
148
149
  export function compactToolDescription(text) {
149
- return text
150
- .replace(/<example[\s\S]*?<\/example>/gi, "")
151
- .replace(/\|[\s|:-]+\|/g, "")
152
- .replace(/^\|.*\|$/gm, "")
153
- .replace(/^(?:\s*[-*]\s+.{200,})$/gm, (m) => m.slice(0, 200) + "...")
154
- .replace(/^(#{1,3}\s+)/gm, "")
155
- .replace(/\*\*([^*]+)\*\*/g, "$1")
156
- .replace(/`([^`]+)`/g, "$1")
157
- .replace(/\n{3,}/g, "\n\n")
158
- .trim();
150
+ return (
151
+ text
152
+ .replace(/<example[\s\S]*?<\/example>/gi, "")
153
+ .replace(/\|[\s|:-]+\|/g, "")
154
+ .replace(/^\|.*\|$/gm, "")
155
+ // Budget is 200 UTF-16 units on the matched line itself; the "..." suffix
156
+ // is NOT counted against that budget (legacy contract: it is appended
157
+ // unconditionally after the cut). truncateGraphemes only ever cuts on a
158
+ // grapheme boundary, so a surrogate pair or combining sequence sitting
159
+ // across unit 200 is excluded whole rather than split — for ASCII text
160
+ // (single-unit graphemes) this is byte-for-byte identical to the old
161
+ // `m.slice(0, 200)`.
162
+ .replace(/^(?:\s*[-*]\s+.{200,})$/gm, (m) => truncateGraphemes(m, 200) + "...")
163
+ .replace(/^(#{1,3}\s+)/gm, "")
164
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
165
+ .replace(/`([^`]+)`/g, "$1")
166
+ .replace(/\n{3,}/g, "\n\n")
167
+ .trim()
168
+ );
159
169
  }
160
170
 
161
171
  export function compactSystemText(text, mode) {
@@ -5,10 +5,10 @@
5
5
  */
6
6
 
7
7
  import {
8
- BETA_REGISTRY_2_1_233,
8
+ BETA_REGISTRY_2_1_280,
9
9
  buildClaudeCodeRequest,
10
10
  buildClaudeCodeCountTokensRequest,
11
- CLAUDE_CODE_2_1_233_PROFILE,
11
+ CLAUDE_CODE_2_1_280_PROFILE,
12
12
  hasOneMillionContext,
13
13
  isAdaptiveThinkingModel,
14
14
  isClaude3Model,
@@ -22,8 +22,13 @@ import {
22
22
  supportsStructuredOutputs,
23
23
  supportsWebSearch,
24
24
  } from "@tormentalabs/claude-code-wire-compat";
25
+ import { existsSync, readFileSync } from "node:fs";
26
+ import path from "node:path";
27
+ import { fileURLToPath } from "node:url";
25
28
  import { stripStainlessHelperMarkers } from "./headers.mjs";
26
29
 
30
+ const WIRE_COMPAT_PACKAGE_NAME = "@tormentalabs/claude-code-wire-compat";
31
+
27
32
  /**
28
33
  * Model-family predicates, re-exported so the rest of the tree classifies models
29
34
  * through the SAME catalogue the request builder classifies them with.
@@ -76,11 +81,11 @@ export {
76
81
  * than on the wire.
77
82
  *
78
83
  * Shape: `{ KEY: { featureKey, header } }`, deep-frozen by the package. It is
79
- * pinned to 2.1.233 rather than derived from `WIRE_PROFILE` because the package
84
+ * pinned to 2.1.280 rather than derived from `WIRE_PROFILE` because the package
80
85
  * exports no profile->registry accessor; if the emulated baseline moves, this
81
86
  * binding and `WIRE_PROFILE` move together.
82
87
  */
83
- export { BETA_REGISTRY_2_1_233 };
88
+ export { BETA_REGISTRY_2_1_280 };
84
89
 
85
90
  /**
86
91
  * 1M-context eligibility under the profile the plugin actually emulates.
@@ -89,7 +94,7 @@ export { BETA_REGISTRY_2_1_233 };
89
94
  * PROFILE-DEPENDENT: its second parameter selects the model catalogue, and it
90
95
  * defaults to the package's own baseline (2.1.195), not to ours. The two
91
96
  * disagree in practice — `claude-mythos-5` is ineligible under 2.1.195 and
92
- * eligible under 2.1.233 — so calling it bare would gate the plugin's context
97
+ * eligible under 2.1.280 — so calling it bare would gate the plugin's context
93
98
  * decisions on a profile the plugin does not emulate anywhere else.
94
99
  *
95
100
  * This wrapper binds `WIRE_PROFILE`, so eligibility follows the emulated
@@ -113,14 +118,118 @@ export function isEligibleFor1MContextWire(model) {
113
118
  * `mimicryBaseline` reported by `lib/diagnose.mjs` — reads it from here instead
114
119
  * of re-declaring a hand-maintained copy.
115
120
  *
116
- * NOTE: this is a BASELINE, not the profile the request path uses. The
117
- * composition path deliberately passes no explicit `profile` so it inherits the
118
- * package's `DEFAULT_PROFILE` (see `lib/mimicry/adapter-input.mjs`); naming a
119
- * profile here only pins what the plugin reports about itself.
121
+ * THIS IS ALSO THE PROFILE THE REQUEST PATH ACTUALLY USES, not merely a
122
+ * reporting baseline. `buildWireCompatibleRequest` and
123
+ * `buildWireCompatibleCountTokensRequest` both pass this constant as the
124
+ * explicit `profile` argument to `buildClaudeCodeRequest` /
125
+ * `buildClaudeCodeCountTokensRequest` (see the package's own 0.6.0 CHANGELOG:
126
+ * "the pin has to be passed on every call to each of the three entry points
127
+ * that read the default seam -- pinning the builders alone is not enough").
128
+ *
129
+ * WHY AN EXPLICIT PASS, RATHER THAN LETTING THE CALL INHERIT THE PACKAGE'S
130
+ * `DEFAULT_PROFILE`. The dependency is pinned to the `latest` dist-tag
131
+ * (`docs/shared-package-provenance.md`) precisely so package releases --
132
+ * fixes, new opt-in fields -- reach the plugin without a code change. Before
133
+ * this constant was threaded through, an unnamed `profile` argument meant the
134
+ * EMULATED CLIENT VERSION floated on that same tag: 0.6.0 moved
135
+ * `DEFAULT_PROFILE` to 2.1.280 in a minor release and broke 48 plugin tests
136
+ * that were still asserting 2.1.233 bytes. Passing `WIRE_PROFILE` explicitly on
137
+ * every entry point decouples the two axes: `latest` keeps bringing library
138
+ * fixes, and the emulated profile now moves only when this binding is
139
+ * deliberately repointed at a different exported profile singleton -- a
140
+ * one-line change reviewed like any other wire change, not a side effect of
141
+ * `npm update`.
120
142
  *
121
143
  * @type {import('@tormentalabs/claude-code-wire-compat').ClaudeCodeProtocolProfile}
122
144
  */
123
- export const WIRE_PROFILE = CLAUDE_CODE_2_1_233_PROFILE;
145
+ export const WIRE_PROFILE = CLAUDE_CODE_2_1_280_PROFILE;
146
+
147
+ /**
148
+ * Version of `@tormentalabs/claude-code-wire-compat` baked in at BUILD time by
149
+ * `scripts/build.mjs`'s esbuild `define`, read from the package that was
150
+ * actually installed in `node_modules` when the bundle was produced.
151
+ *
152
+ * WHY THIS EXISTS. `getWireCompatPackageVersion` below resolves the version at
153
+ * RUNTIME through `import.meta.resolve`, which requires the package to still
154
+ * be a separate, independently resolvable module -- true for an unbundled
155
+ * import (tests, `node cli.mjs`, a host importing `index.mjs` directly) but
156
+ * false for the bundles `scripts/build.mjs` produces: esbuild INLINES the
157
+ * package's source into the single output file, so there is no
158
+ * `node_modules/@tormentalabs/claude-code-wire-compat` for the bundle to
159
+ * resolve at its own runtime, and the resolution silently degrades to
160
+ * "unknown" -- defeating the diagnostic bundle's whole point of reporting what
161
+ * the running process actually built against.
162
+ *
163
+ * `__WIRE_COMPAT_PACKAGE_VERSION__` is a bare, undeclared global identifier,
164
+ * not an import and not `process.env`, because esbuild's `define` performs a
165
+ * textual substitution of exactly that shape at bundle time. `typeof` never
166
+ * throws on an undeclared identifier, so referencing it this way is exactly as
167
+ * safe unbundled as it is bundled: unbundled, esbuild never touched this file,
168
+ * the identifier stays undeclared, `typeof` reads `"undefined"`, and this
169
+ * constant is `undefined` -- `getWireCompatPackageVersion` then falls through
170
+ * to the runtime resolution exactly as it did before this constant existed.
171
+ *
172
+ * @type {string | undefined}
173
+ */
174
+ /* global __WIRE_COMPAT_PACKAGE_VERSION__:readonly */
175
+ const BUNDLED_WIRE_COMPAT_PACKAGE_VERSION =
176
+ typeof __WIRE_COMPAT_PACKAGE_VERSION__ === "string" ? __WIRE_COMPAT_PACKAGE_VERSION__ : undefined;
177
+
178
+ /**
179
+ * The version of @tormentalabs/claude-code-wire-compat this process actually
180
+ * runs against: the build-time injected version when bundled (see
181
+ * `BUNDLED_WIRE_COMPAT_PACKAGE_VERSION` above), otherwise the version
182
+ * resolved fresh from `node_modules` on every call rather than cached at
183
+ * module load.
184
+ *
185
+ * WHY THE RUNTIME FALLBACK EXISTS. `package.json` pins the dependency to the
186
+ * `latest` dist-tag (see docs/shared-package-provenance.md), so the version an
187
+ * UNBUNDLED run actually runs against is whatever `package-lock.json` resolved
188
+ * at install time -- it is not a literal anywhere in this source tree. A
189
+ * diagnostic bundle that wants to say what wire the running process is
190
+ * actually built against (as opposed to `WIRE_PROFILE.cliVersion`, the
191
+ * BASELINE this file pins) has to read it off disk.
192
+ *
193
+ * Deliberately resolves through `node_modules`, not `package-lock.json`: the
194
+ * lock says what a future `npm ci` WOULD install, `node_modules` says what
195
+ * THIS process actually loaded, and the two can disagree after an `npm
196
+ * install` that has not been re-run (see `scripts/check-wire-compat-drift.mjs`,
197
+ * which checks exactly that disagreement).
198
+ *
199
+ * Resolution failure degrades to "unknown" rather than throwing -- a
200
+ * diagnostic bundle must never fail to build over a missing package.json.
201
+ *
202
+ * Resolves via `import.meta.resolve`, not `createRequire(...).resolve`: the
203
+ * package is ESM-only (its `exports` map declares an `import` condition and
204
+ * no `require` one), so the CJS resolver fails it with "No 'exports' main
205
+ * defined" even though the package is genuinely installed.
206
+ *
207
+ * @returns {string}
208
+ */
209
+ export function getWireCompatPackageVersion() {
210
+ if (BUNDLED_WIRE_COMPAT_PACKAGE_VERSION) return BUNDLED_WIRE_COMPAT_PACKAGE_VERSION;
211
+ try {
212
+ let directory = path.dirname(fileURLToPath(import.meta.resolve(WIRE_COMPAT_PACKAGE_NAME)));
213
+ // Walk up from the package's resolved entry file (e.g. .../dist/index.js)
214
+ // to the package.json alongside its root -- the entry file is not
215
+ // guaranteed to be a direct sibling of it.
216
+ for (let depth = 0; depth < 6; depth += 1) {
217
+ const candidate = path.join(directory, "package.json");
218
+ if (existsSync(candidate)) {
219
+ const manifest = JSON.parse(readFileSync(candidate, "utf8"));
220
+ if (manifest.name === WIRE_COMPAT_PACKAGE_NAME && typeof manifest.version === "string") {
221
+ return manifest.version;
222
+ }
223
+ }
224
+ const parent = path.dirname(directory);
225
+ if (parent === directory) break;
226
+ directory = parent;
227
+ }
228
+ } catch {
229
+ // Fall through to "unknown".
230
+ }
231
+ return "unknown";
232
+ }
124
233
 
125
234
  const PROFILE_OVERRIDE_ENV = "OPENCODE_ANTHROPIC_PROFILE_OVERRIDE";
126
235
 
@@ -408,7 +517,11 @@ export async function buildWireCompatibleRequest(body, transport) {
408
517
  stripStainlessHelperMarkers(parsed?.tools, parsed?.messages);
409
518
  let built;
410
519
  try {
411
- built = await buildClaudeCodeRequest(toClaudeCodeRequestInput(parsed, transport));
520
+ // `WIRE_PROFILE` is passed EXPLICITLY rather than omitted -- see its own
521
+ // doc comment above for why an omitted argument here would let a future
522
+ // package release move the emulated client profile out from under the
523
+ // `latest` dist-tag.
524
+ built = await buildClaudeCodeRequest(toClaudeCodeRequestInput(parsed, transport), WIRE_PROFILE);
412
525
  } catch (error) {
413
526
  throw foldSafeDetailsIntoMessage(error);
414
527
  }
@@ -501,9 +614,10 @@ export function toClaudeCodeCountTokensInput(body, transport) {
501
614
 
502
615
  /**
503
616
  * Build a Claude Code `/v1/messages/count_tokens` request from the JSON body
504
- * received from the host. Mirrors `buildWireCompatibleRequest`, including the
505
- * deliberate absence of an explicit `profile` argument so the count turn
506
- * inherits the package's `DEFAULT_PROFILE` exactly like the main turn does.
617
+ * received from the host. Mirrors `buildWireCompatibleRequest`, including
618
+ * passing `WIRE_PROFILE` as the explicit `profile` argument so the count turn
619
+ * is pinned to the same emulated client profile as the main turn, rather than
620
+ * inheriting whatever the installed package declares as its `DEFAULT_PROFILE`.
507
621
  *
508
622
  * @param {string | undefined} body
509
623
  * @param {Parameters<typeof toClaudeCodeCountTokensInput>[1]} transport
@@ -519,7 +633,10 @@ export async function buildWireCompatibleCountTokensRequest(body, transport) {
519
633
  stripStainlessHelperMarkers(parsed?.tools, parsed?.messages);
520
634
  let built;
521
635
  try {
522
- built = await buildClaudeCodeCountTokensRequest(toClaudeCodeCountTokensInput(parsed, transport));
636
+ // See `buildWireCompatibleRequest`'s equivalent comment: explicit on
637
+ // purpose, so the count-tokens surface cannot silently move to a different
638
+ // profile than the main turn on a package bump.
639
+ built = await buildClaudeCodeCountTokensRequest(toClaudeCodeCountTokensInput(parsed, transport), WIRE_PROFILE);
523
640
  } catch (error) {
524
641
  throw foldSafeDetailsIntoMessage(error);
525
642
  }
@@ -1,3 +1,5 @@
1
+ import { truncateGraphemes } from "./unicode-text.mjs";
2
+
1
3
  /**
2
4
  * Deterministic rolling summarizer — Phase B Task B3.
3
5
  *
@@ -176,18 +178,31 @@ export function formatTemplate(parsed, maxChars) {
176
178
  }
177
179
  const longest = current[longestName];
178
180
  if (longest.length <= EMPTY_SECTION.length) break;
179
- // Chop 10% (at least 8 chars) off the end of the longest section.
181
+ // Chop 10% (at least 8 chars) off the end of the longest section. Cut on
182
+ // a grapheme boundary — never split a surrogate pair or combining
183
+ // sequence — while still guaranteeing progress: the target is always
184
+ // strictly below `longest.length` here, so truncateGraphemes always
185
+ // shortens it (it only ever returns the input unchanged when the input
186
+ // already fits the target).
180
187
  const chop = Math.max(8, Math.floor(longest.length * 0.1));
181
- current = { ...current, [longestName]: longest.slice(0, Math.max(EMPTY_SECTION.length, longest.length - chop)) };
188
+ const target = Math.max(EMPTY_SECTION.length, longest.length - chop);
189
+ current = { ...current, [longestName]: truncateGraphemes(longest, target) };
182
190
  out = render(current);
183
191
  }
184
192
 
185
- // Final hard cap: if we somehow still exceed, slice the envelope. Still
193
+ // Final hard cap: if we somehow still exceed, cut the envelope on a
194
+ // grapheme boundary instead of slicing UTF-16 units blindly. Still
186
195
  // deterministic (input-driven only).
187
196
  if (out.length > maxChars) {
188
197
  const closing = "\n</session-summary>";
189
- const head = out.slice(0, Math.max(0, maxChars - closing.length));
190
- out = head + closing;
198
+ if (maxChars <= closing.length) {
199
+ // The budget doesn't even cover the closing tag by itself. Match the
200
+ // pre-existing fallback shape — a bare, well-formed closing tag —
201
+ // rather than emitting an envelope with no closing tag at all.
202
+ out = closing;
203
+ } else {
204
+ out = truncateGraphemes(out, maxChars, { suffix: closing });
205
+ }
191
206
  }
192
207
  return out;
193
208
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Grapheme-safe text truncation.
3
+ *
4
+ * Every plugin-side text budget that ends up on the wire (system prompt
5
+ * compaction, rolling-summary sections/envelope, and any future caller) has
6
+ * historically been enforced with `String.prototype.slice(0, N)`. `slice`
7
+ * counts UTF-16 code units and has no notion of "user-perceived character":
8
+ * it can cut a surrogate pair in half (producing a lone surrogate, which
9
+ * `@tormentalabs/claude-code-wire-compat` rejects with `INVALID_UNICODE`) or
10
+ * cut a multi-codepoint grapheme cluster in half (producing well-formed
11
+ * UTF-16 that nonetheless renders as a broken glyph — a dangling combining
12
+ * mark, half a ZWJ sequence, half a flag, a skin-tone modifier with no base
13
+ * emoji, and so on).
14
+ *
15
+ * `truncateGraphemes` is the single, pure helper that replaces those
16
+ * `slice(0, N)` call sites. It only ever cuts on a grapheme-cluster boundary
17
+ * (per `Intl.Segmenter` with `granularity: "grapheme"`) and never emits a
18
+ * lone surrogate or a split cluster.
19
+ *
20
+ * Budget unit: UTF-16 code units — i.e. `String.prototype.length`. This is
21
+ * the same unit the `slice(0, N)` call sites it replaces already used, so
22
+ * swapping in this helper with the same `N` preserves the legacy budget
23
+ * semantics (it does NOT switch callers to a UTF-8 byte budget or a visual
24
+ * column budget; a caller that needs one of those must convert before and
25
+ * after calling in).
26
+ *
27
+ * @module lib/unicode-text
28
+ */
29
+
30
+ /**
31
+ * Return the longest prefix of `text` that:
32
+ * - ends exactly on a grapheme-cluster boundary (never splits a surrogate
33
+ * pair and never splits a base character from a combining mark, ZWJ
34
+ * sequence, flag, or emoji modifier);
35
+ * - together with `options.suffix` appended, has a UTF-16 length
36
+ * (`.length`) less than or equal to `maxUnits`.
37
+ *
38
+ * Edge-case contract (all explicit; the function never throws):
39
+ * - `text === ""` -> `""`.
40
+ * - `maxUnits` is `NaN`, or not a number at all -> treated as `0` (the
41
+ * most conservative reading: if the budget itself is meaningless,
42
+ * nothing is guaranteed to fit).
43
+ * - `maxUnits < 0` -> treated as `0`.
44
+ * - `maxUnits === Infinity` -> `text` is returned unchanged; there is no
45
+ * ceiling to cut against.
46
+ * - `text.length <= maxUnits` -> `text` is returned unchanged, WITHOUT the
47
+ * suffix appended. Nothing was truncated, so nothing marks that it was —
48
+ * this matches the legacy call sites, where the suffix only ever showed
49
+ * up when a cut actually happened.
50
+ * - The very first grapheme cluster of `text` is already wider than the
51
+ * room left for the prefix (`maxUnits - suffix.length`) -> the prefix is
52
+ * `""`. The cluster is NEVER split to make it fit; if `options.suffix`
53
+ * itself fits within `maxUnits` on its own it is still returned alone,
54
+ * otherwise the overall result is `""`.
55
+ * - Long combining-mark sequences, ZWJ emoji, flags, and skin-tone
56
+ * modifiers are each a single grapheme cluster to `Intl.Segmenter` and
57
+ * are therefore included or excluded as one indivisible unit, however
58
+ * many UTF-16 units or code points they span.
59
+ *
60
+ * Performance: `text` is segmented at most once. If `text.length <=
61
+ * maxUnits`, segmentation is skipped entirely (fast path) — no reason to pay
62
+ * for it when no truncation can possibly be needed. When truncation IS
63
+ * needed, the segments are walked forward exactly once, accumulating length
64
+ * until the next segment would exceed the budget; this is O(n) in the length
65
+ * of `text`, never quadratic, and always terminates because the walk is a
66
+ * single forward pass over a finite iterator.
67
+ *
68
+ * @param {string} text
69
+ * @param {number} maxUnits - budget in UTF-16 code units (`.length`).
70
+ * @param {{suffix?: string}} [options]
71
+ * @returns {string}
72
+ */
73
+ export function truncateGraphemes(text, maxUnits, options) {
74
+ const suffix = options && typeof options.suffix === "string" ? options.suffix : "";
75
+ const input = typeof text === "string" ? text : String(text ?? "");
76
+ if (input.length === 0) return "";
77
+
78
+ // Normalize the budget: non-numbers and NaN are the most conservative
79
+ // reading (0); negative budgets clamp to 0; Infinity is left as-is and
80
+ // falls through the normal comparisons below (any finite length is <=
81
+ // Infinity, so it hits the "already fits" fast path further down).
82
+ let limit = maxUnits;
83
+ if (typeof limit !== "number" || Number.isNaN(limit)) {
84
+ limit = 0;
85
+ } else if (limit < 0) {
86
+ limit = 0;
87
+ }
88
+
89
+ // Fast path: already within budget. Skip segmenting entirely, and do NOT
90
+ // append the suffix — nothing was truncated.
91
+ if (input.length <= limit) return input;
92
+
93
+ const budgetForPrefix = limit - suffix.length;
94
+ if (budgetForPrefix <= 0) {
95
+ // No room for even one grapheme once the suffix is reserved. Return the
96
+ // suffix alone if it fits the whole budget by itself; otherwise "".
97
+ // Either way, no partial cluster is ever produced.
98
+ return suffix.length <= limit ? suffix : "";
99
+ }
100
+
101
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
102
+ let prefix = "";
103
+ let used = 0;
104
+ for (const { segment } of segmenter.segment(input)) {
105
+ const segmentLength = segment.length;
106
+ if (used + segmentLength > budgetForPrefix) break;
107
+ prefix += segment;
108
+ used += segmentLength;
109
+ }
110
+ return prefix + suffix;
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,6 +32,8 @@
32
32
  "install:link": "node scripts/install.mjs link",
33
33
  "install:copy": "npm run build && node scripts/install.mjs copy",
34
34
  "uninstall": "node scripts/install.mjs uninstall",
35
+ "sync:wire-compat": "node scripts/sync-wire-compat.mjs",
36
+ "check:wire-compat-drift": "node scripts/check-wire-compat-drift.mjs",
35
37
  "prepare": "husky"
36
38
  },
37
39
  "lint-staged": {
@@ -55,6 +57,6 @@
55
57
  "vitest": "^4.0.18"
56
58
  },
57
59
  "dependencies": {
58
- "@tormentalabs/claude-code-wire-compat": "0.5.0"
60
+ "@tormentalabs/claude-code-wire-compat": "latest"
59
61
  }
60
62
  }