opencode-anthropic-fix 1.0.0 → 2.1.0

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.
@@ -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": "1.0.0",
3
+ "version": "2.1.0",
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": {