@oh-my-pi-zen/pi-utils 16.3.6-zen.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.
Files changed (164) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +36 -0
  3. package/dist/types/abortable.d.ts +32 -0
  4. package/dist/types/async.d.ts +6 -0
  5. package/dist/types/binary.d.ts +17 -0
  6. package/dist/types/cli.d.ts +117 -0
  7. package/dist/types/color.d.ts +102 -0
  8. package/dist/types/dirs.d.ts +260 -0
  9. package/dist/types/env.d.ts +74 -0
  10. package/dist/types/fetch-retry.d.ts +94 -0
  11. package/dist/types/format.d.ts +37 -0
  12. package/dist/types/frontmatter.d.ts +25 -0
  13. package/dist/types/fs-error.d.ts +31 -0
  14. package/dist/types/glob.d.ts +28 -0
  15. package/dist/types/index.d.ts +35 -0
  16. package/dist/types/json-parse.d.ts +48 -0
  17. package/dist/types/json.d.ts +4 -0
  18. package/dist/types/logger.d.ts +83 -0
  19. package/dist/types/loop-phase.d.ts +10 -0
  20. package/dist/types/mermaid-ascii.d.ts +11 -0
  21. package/dist/types/mime.d.ts +29 -0
  22. package/dist/types/module-timer.d.ts +1 -0
  23. package/dist/types/path-tree.d.ts +76 -0
  24. package/dist/types/path.d.ts +2 -0
  25. package/dist/types/peek-file.d.ts +29 -0
  26. package/dist/types/postmortem.d.ts +37 -0
  27. package/dist/types/procmgr.d.ts +29 -0
  28. package/dist/types/prompt.d.ts +18 -0
  29. package/dist/types/ptree.d.ts +108 -0
  30. package/dist/types/ring.d.ts +93 -0
  31. package/dist/types/runtime-install.d.ts +68 -0
  32. package/dist/types/sanitize-text.d.ts +29 -0
  33. package/dist/types/snowflake.d.ts +25 -0
  34. package/dist/types/stream.d.ts +68 -0
  35. package/dist/types/tab-spacing.d.ts +24 -0
  36. package/dist/types/temp.d.ts +17 -0
  37. package/dist/types/timing-buffer.d.ts +22 -0
  38. package/dist/types/tls-fetch.d.ts +37 -0
  39. package/dist/types/type-guards.d.ts +3 -0
  40. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  41. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  42. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  43. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  44. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  45. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  46. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  47. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  48. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  49. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  50. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  51. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  52. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  53. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  54. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  55. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  56. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  57. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  58. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  59. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  60. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  61. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  62. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  63. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  64. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  65. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  66. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  67. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  68. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  69. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  70. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  71. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  72. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  73. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  74. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  75. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  76. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  77. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  78. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  79. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  80. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  81. package/dist/types/which.d.ts +37 -0
  82. package/dist/types/worker-host.d.ts +47 -0
  83. package/package.json +62 -0
  84. package/src/abortable.ts +109 -0
  85. package/src/async.ts +50 -0
  86. package/src/binary.ts +50 -0
  87. package/src/cli.ts +457 -0
  88. package/src/color.ts +302 -0
  89. package/src/dirs.ts +925 -0
  90. package/src/env.ts +226 -0
  91. package/src/fetch-retry.ts +355 -0
  92. package/src/format.ts +113 -0
  93. package/src/frontmatter.ts +162 -0
  94. package/src/fs-error.ts +56 -0
  95. package/src/glob.ts +189 -0
  96. package/src/index.ts +55 -0
  97. package/src/json-parse.ts +612 -0
  98. package/src/json.ts +10 -0
  99. package/src/logger.ts +672 -0
  100. package/src/loop-phase.ts +49 -0
  101. package/src/mermaid-ascii.ts +31 -0
  102. package/src/mime.ts +159 -0
  103. package/src/module-timer.ts +148 -0
  104. package/src/path-tree.ts +147 -0
  105. package/src/path.ts +28 -0
  106. package/src/peek-file.ts +188 -0
  107. package/src/postmortem.ts +222 -0
  108. package/src/procmgr.ts +195 -0
  109. package/src/prompt.ts +562 -0
  110. package/src/ptree.ts +395 -0
  111. package/src/ring.ts +169 -0
  112. package/src/runtime-install.ts +372 -0
  113. package/src/sanitize-text.ts +96 -0
  114. package/src/snowflake.ts +121 -0
  115. package/src/stream.ts +432 -0
  116. package/src/tab-spacing.ts +336 -0
  117. package/src/temp.ts +134 -0
  118. package/src/timing-buffer.ts +47 -0
  119. package/src/tls-fetch.ts +178 -0
  120. package/src/type-guards.ts +11 -0
  121. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  122. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  123. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  124. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  125. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  126. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  127. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  128. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  129. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  130. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  131. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  132. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  133. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +215 -0
  134. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  135. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  136. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  137. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  138. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  139. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  140. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  141. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  142. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  143. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  144. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  145. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  146. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  147. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  148. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  149. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  150. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  151. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  152. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  153. package/src/vendor/mermaid-ascii/index.ts +14 -0
  154. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  155. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  156. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  157. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  158. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  159. package/src/vendor/mermaid-ascii/types.ts +164 -0
  160. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  161. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  162. package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
  163. package/src/which.ts +232 -0
  164. package/src/worker-host.ts +92 -0
@@ -0,0 +1,612 @@
1
+ const QUOTE = 0x22;
2
+ const BACKSLASH = 0x5c;
3
+ const U = 0x75;
4
+ const SQUOTE = 0x27;
5
+
6
+ // Valid chars after `\`: " \ / b f n r t u
7
+ const VALID_ESCAPE_CHAR = new Uint8Array(128);
8
+ for (const ch of '"\\/bfnrtu') VALID_ESCAPE_CHAR[ch.charCodeAt(0)] = 1;
9
+
10
+ const CONTROL_ESCAPES: readonly string[] = (() => {
11
+ const e: string[] = [];
12
+ e[0x08] = "\\b";
13
+ e[0x09] = "\\t";
14
+ e[0x0a] = "\\n";
15
+ e[0x0c] = "\\f";
16
+ e[0x0d] = "\\r";
17
+ for (let cp = 0; cp <= 0x1f; cp++) {
18
+ e[cp] ??= `\\u${cp.toString(16).padStart(4, "0")}`;
19
+ }
20
+ return e;
21
+ })();
22
+
23
+ const HEX4_RE = /^[0-9a-fA-F]{4}$/;
24
+
25
+ function isHexDigit(cp: number): boolean {
26
+ return (cp >= 0x30 && cp <= 0x39) || ((cp | 0x20) >= 0x61 && (cp | 0x20) <= 0x66);
27
+ }
28
+
29
+ function isWhitespace(cp: number): boolean {
30
+ return cp === 0x20 || cp === 0x09 || cp === 0x0a || cp === 0x0d;
31
+ }
32
+
33
+ function isIdentChar(cp: number): boolean {
34
+ return (
35
+ (cp >= 0x30 && cp <= 0x39) ||
36
+ ((cp | 0x20) >= 0x61 && (cp | 0x20) <= 0x7a) ||
37
+ cp === 0x5f /* _ */ ||
38
+ cp === 0x24 /* $ */
39
+ );
40
+ }
41
+
42
+ /** Bareword literals: standard JSON plus Python `True`/`False`/`None`. */
43
+ const KEYWORDS: readonly (readonly [string, unknown])[] = [
44
+ ["true", true],
45
+ ["false", false],
46
+ ["null", null],
47
+ ["True", true],
48
+ ["False", false],
49
+ ["None", null],
50
+ ];
51
+
52
+ /**
53
+ * JS-only atoms never recovered as bareword strings — a tool must not execute
54
+ * with a non-finite or undefined argument masquerading as a string.
55
+ */
56
+ const NON_RECOVERABLE_BAREWORDS: Record<string, true> = {
57
+ NaN: true,
58
+ Infinity: true,
59
+ "-Infinity": true,
60
+ "+Infinity": true,
61
+ undefined: true,
62
+ };
63
+
64
+ /**
65
+ * Sentinel returned by partial-mode value parsing when an atomic value
66
+ * (number / keyword) is incomplete at the streaming edge, so the enclosing
67
+ * object/array rolls back to the last valid prefix instead of committing junk.
68
+ */
69
+ const INCOMPLETE = Symbol("incomplete");
70
+
71
+ /**
72
+ * Lightweight string-level repair of the escape/control-char hazards that make
73
+ * otherwise-valid JSON fail `JSON.parse`: raw control characters inside strings
74
+ * are escaped, and invalid `\x` escapes have their backslash escaped. Returns the
75
+ * input unchanged when no repair is needed. Pure string→string; does not parse.
76
+ */
77
+ export function repairJson(json: string): string {
78
+ const len = json.length;
79
+ const parts: string[] = [];
80
+ let lastEmit = 0;
81
+ let inString = false;
82
+ let i = 0;
83
+
84
+ while (i < len) {
85
+ if (!inString) {
86
+ // Fast scan: skip to next quote.
87
+ while (i < len && json.charCodeAt(i) !== QUOTE) i++;
88
+ if (i >= len) break;
89
+ inString = true;
90
+ i++;
91
+ continue;
92
+ }
93
+
94
+ // Fast scan inside string: advance past chars that need no handling.
95
+ while (i < len) {
96
+ const cp = json.charCodeAt(i);
97
+ if (cp < 0x20 || cp === QUOTE || cp === BACKSLASH) break;
98
+ i++;
99
+ }
100
+ if (i >= len) break;
101
+
102
+ const cp = json.charCodeAt(i);
103
+
104
+ if (cp === QUOTE) {
105
+ inString = false;
106
+ i++;
107
+ continue;
108
+ }
109
+
110
+ if (cp === BACKSLASH) {
111
+ // Need at least one char after the backslash; treat EOI as invalid escape.
112
+ if (i + 1 >= len) {
113
+ parts.push(json.slice(lastEmit, i), "\\\\");
114
+ lastEmit = i + 1;
115
+ i++;
116
+ continue;
117
+ }
118
+
119
+ const nextCp = json.charCodeAt(i + 1);
120
+
121
+ if (nextCp === U) {
122
+ // Need full \uXXXX, all four digits, all hex.
123
+ if (
124
+ i + 5 < len &&
125
+ isHexDigit(json.charCodeAt(i + 2)) &&
126
+ isHexDigit(json.charCodeAt(i + 3)) &&
127
+ isHexDigit(json.charCodeAt(i + 4)) &&
128
+ isHexDigit(json.charCodeAt(i + 5))
129
+ ) {
130
+ i += 6;
131
+ continue;
132
+ }
133
+ // Truncated or non-hex \u — escape the backslash, re-process the rest.
134
+ parts.push(json.slice(lastEmit, i), "\\\\");
135
+ lastEmit = i + 1;
136
+ i++;
137
+ continue;
138
+ }
139
+
140
+ if (nextCp < 128 && VALID_ESCAPE_CHAR[nextCp] === 1) {
141
+ i += 2;
142
+ continue;
143
+ }
144
+
145
+ parts.push(json.slice(lastEmit, i), "\\\\");
146
+ lastEmit = i + 1;
147
+ i++;
148
+ continue;
149
+ }
150
+
151
+ // Control character (cp < 0x20).
152
+ parts.push(json.slice(lastEmit, i), CONTROL_ESCAPES[cp]);
153
+ lastEmit = i + 1;
154
+ i++;
155
+ }
156
+
157
+ if (!parts.length) return json;
158
+ if (lastEmit < len) parts.push(json.slice(lastEmit));
159
+ return parts.join("");
160
+ }
161
+
162
+ /**
163
+ * Recursive-descent parser for a forgiving superset of JSON. Beyond strict JSON
164
+ * it accepts, and normalizes, the malformations LLM tool-call bodies leak in
165
+ * practice:
166
+ *
167
+ * - single-quoted strings and unquoted object keys (JSON5);
168
+ * - trailing / stray commas, and `//` + block comments;
169
+ * - Python literals `True` / `False` / `None` and JS `NaN` / `Infinity`;
170
+ * - raw control characters and invalid `\x` escapes inside strings (kept literally);
171
+ * - unescaped quotes inside strings — a quote only closes a string when followed
172
+ * by a value terminator, recovering apostrophes such as `'it's'`;
173
+ * - unquoted string values in object/array value position (strict mode only) —
174
+ * an unrecognized bareword such as `{"paths": packages/foo/*}` is recovered as
175
+ * a string up to the next `,` / `}` / `]` / newline.
176
+ *
177
+ * In `partial` mode an unterminated string/object/array (or a value cut off at
178
+ * end-of-input) is auto-closed with whatever was parsed so far — for streaming.
179
+ * In strict mode, end-of-input mid-value and trailing garbage both throw, so a
180
+ * final parse never silently accepts a half-formed tool call.
181
+ */
182
+ class RelaxedJson {
183
+ readonly #s: string;
184
+ readonly #n: number;
185
+ readonly #partial: boolean;
186
+ #i = 0;
187
+
188
+ constructor(source: string, partial: boolean) {
189
+ this.#s = source;
190
+ this.#n = source.length;
191
+ this.#partial = partial;
192
+ }
193
+
194
+ parse(): unknown {
195
+ this.#ws();
196
+ if (this.#i >= this.#n) {
197
+ if (this.#partial) return undefined;
198
+ throw new SyntaxError("Unexpected end of JSON input");
199
+ }
200
+ const value = this.#value(false);
201
+ if (value === INCOMPLETE) return undefined;
202
+ this.#ws();
203
+ if (!this.#partial && this.#i < this.#n) {
204
+ throw new SyntaxError(`Unexpected trailing characters at position ${this.#i}`);
205
+ }
206
+ return value;
207
+ }
208
+
209
+ #ws(): void {
210
+ const s = this.#s;
211
+ for (;;) {
212
+ while (this.#i < this.#n && isWhitespace(s.charCodeAt(this.#i))) this.#i++;
213
+ if (this.#i + 1 < this.#n && s.charCodeAt(this.#i) === 0x2f /* / */) {
214
+ const next = s.charCodeAt(this.#i + 1);
215
+ if (next === 0x2f /* / line comment */) {
216
+ this.#i += 2;
217
+ while (this.#i < this.#n && s.charCodeAt(this.#i) !== 0x0a) this.#i++;
218
+ continue;
219
+ }
220
+ if (next === 0x2a /* * block comment */) {
221
+ this.#i += 2;
222
+ while (
223
+ this.#i + 1 < this.#n &&
224
+ !(s.charCodeAt(this.#i) === 0x2a && s.charCodeAt(this.#i + 1) === 0x2f)
225
+ ) {
226
+ this.#i++;
227
+ }
228
+ this.#i = Math.min(this.#i + 2, this.#n);
229
+ continue;
230
+ }
231
+ }
232
+ break;
233
+ }
234
+ }
235
+
236
+ #value(allowBareword: boolean): unknown {
237
+ const s = this.#s;
238
+ const c = s[this.#i];
239
+ if (c === "{") return this.#object();
240
+ if (c === "[") return this.#array();
241
+ if (c === '"' || c === "'") return this.#string(s.charCodeAt(this.#i));
242
+ const cc = s.charCodeAt(this.#i);
243
+ if (cc === 0x2d /* - */ || cc === 0x2b /* + */ || cc === 0x2e /* . */ || (cc >= 0x30 && cc <= 0x39)) {
244
+ // JS-only NaN / Infinity are deliberately not accepted: a tool must not
245
+ // execute with a non-finite numeric arg; they fall through #number's
246
+ // NaN guard (strict throw / partial rollback) like other bad tokens.
247
+ return this.#number();
248
+ }
249
+ return this.#keyword(allowBareword);
250
+ }
251
+
252
+ #object(): Record<string, unknown> {
253
+ this.#i++; // consume {
254
+ const out: Record<string, unknown> = {};
255
+ for (;;) {
256
+ this.#ws();
257
+ if (this.#i >= this.#n) {
258
+ if (this.#partial) return out;
259
+ throw new SyntaxError("Unterminated object");
260
+ }
261
+ const c = this.#s[this.#i];
262
+ if (c === "}") {
263
+ this.#i++;
264
+ return out;
265
+ }
266
+ if (c === ",") {
267
+ // Tolerate leading / doubled / trailing commas.
268
+ this.#i++;
269
+ continue;
270
+ }
271
+ const key = this.#key();
272
+ this.#ws();
273
+ if (this.#i < this.#n && this.#s[this.#i] === ":") {
274
+ this.#i++;
275
+ } else if (this.#partial) {
276
+ return out;
277
+ } else {
278
+ throw new SyntaxError("Expected ':' in object");
279
+ }
280
+ this.#ws();
281
+ if (this.#i >= this.#n) {
282
+ if (this.#partial) return out;
283
+ throw new SyntaxError("Expected value after ':'");
284
+ }
285
+ const value = this.#value(true);
286
+ if (value === INCOMPLETE) return out;
287
+ out[key] = value;
288
+ this.#ws();
289
+ const d = this.#i < this.#n ? this.#s[this.#i] : "";
290
+ if (d === ",") {
291
+ this.#i++;
292
+ continue;
293
+ }
294
+ if (d === "}") {
295
+ this.#i++;
296
+ return out;
297
+ }
298
+ if (this.#partial) return out;
299
+ throw new SyntaxError("Expected ',' or '}' in object");
300
+ }
301
+ }
302
+
303
+ #array(): unknown[] {
304
+ this.#i++; // consume [
305
+ const out: unknown[] = [];
306
+ for (;;) {
307
+ this.#ws();
308
+ if (this.#i >= this.#n) {
309
+ if (this.#partial) return out;
310
+ throw new SyntaxError("Unterminated array");
311
+ }
312
+ const c = this.#s[this.#i];
313
+ if (c === "]") {
314
+ this.#i++;
315
+ return out;
316
+ }
317
+ if (c === ",") {
318
+ this.#i++;
319
+ continue;
320
+ }
321
+ const value = this.#value(true);
322
+ if (value === INCOMPLETE) return out;
323
+ out.push(value);
324
+ this.#ws();
325
+ const d = this.#i < this.#n ? this.#s[this.#i] : "";
326
+ if (d === ",") {
327
+ this.#i++;
328
+ continue;
329
+ }
330
+ if (d === "]") {
331
+ this.#i++;
332
+ return out;
333
+ }
334
+ if (this.#partial) return out;
335
+ throw new SyntaxError("Expected ',' or ']' in array");
336
+ }
337
+ }
338
+
339
+ #key(): string {
340
+ const c = this.#s[this.#i];
341
+ if (c === '"' || c === "'") return this.#string(this.#s.charCodeAt(this.#i));
342
+ // Unquoted identifier key: read until a structural delimiter / whitespace.
343
+ const start = this.#i;
344
+ while (this.#i < this.#n) {
345
+ const ch = this.#s[this.#i];
346
+ if (ch === ":" || ch === "," || ch === "}" || isWhitespace(this.#s.charCodeAt(this.#i))) break;
347
+ this.#i++;
348
+ }
349
+ if (this.#i === start) {
350
+ if (this.#partial) return "";
351
+ throw new SyntaxError("Expected object key");
352
+ }
353
+ return this.#s.slice(start, this.#i);
354
+ }
355
+
356
+ #string(quote: number): string {
357
+ const s = this.#s;
358
+ const n = this.#n;
359
+ let i = this.#i + 1; // skip opening quote
360
+ let out = "";
361
+ let runStart = i;
362
+ while (i < n) {
363
+ const cc = s.charCodeAt(i);
364
+ if (cc !== BACKSLASH && cc !== quote) {
365
+ i++;
366
+ continue;
367
+ }
368
+ if (cc === quote) {
369
+ // Apostrophe / inner-quote recovery (a quote that isn't followed by a
370
+ // value terminator is literal) is safe for single quotes and in partial
371
+ // mode. For double quotes in strict mode, close on the first unescaped
372
+ // quote like standard JSON so malformed structure fails loudly instead
373
+ // of silently swallowing commas/colons into one string.
374
+ const lenient = quote === SQUOTE || this.#partial;
375
+ if (!lenient || this.#closesString(i + 1)) {
376
+ out += s.slice(runStart, i);
377
+ this.#i = i + 1;
378
+ return out;
379
+ }
380
+ // Unescaped inner quote (e.g. apostrophe in `'it's'`) — keep it literal.
381
+ i++;
382
+ continue;
383
+ }
384
+ // Backslash escape.
385
+ out += s.slice(runStart, i);
386
+ i++;
387
+ if (i >= n) {
388
+ out += "\\";
389
+ runStart = i;
390
+ break;
391
+ }
392
+ const esc = s.charCodeAt(i);
393
+ switch (esc) {
394
+ case QUOTE:
395
+ out += '"';
396
+ break;
397
+ case SQUOTE:
398
+ out += "'";
399
+ break;
400
+ case BACKSLASH:
401
+ out += "\\";
402
+ break;
403
+ case 0x2f:
404
+ out += "/";
405
+ break;
406
+ case 0x62:
407
+ out += "\b";
408
+ break;
409
+ case 0x66:
410
+ out += "\f";
411
+ break;
412
+ case 0x6e:
413
+ out += "\n";
414
+ break;
415
+ case 0x72:
416
+ out += "\r";
417
+ break;
418
+ case 0x74:
419
+ out += "\t";
420
+ break;
421
+ case U: {
422
+ const hex = s.slice(i + 1, i + 5);
423
+ if (HEX4_RE.test(hex)) {
424
+ out += String.fromCharCode(parseInt(hex, 16));
425
+ i += 4;
426
+ } else {
427
+ out += "\\u"; // invalid \u — keep literal
428
+ }
429
+ break;
430
+ }
431
+ default:
432
+ out += `\\${s[i]}`; // invalid escape — keep backslash literal
433
+ }
434
+ i++;
435
+ runStart = i;
436
+ }
437
+ out += s.slice(runStart, i);
438
+ if (this.#partial) {
439
+ this.#i = i;
440
+ return out;
441
+ }
442
+ throw new SyntaxError("Unterminated string");
443
+ }
444
+
445
+ /** A quote closes a string only when the next non-space char ends a value. */
446
+ #closesString(from: number): boolean {
447
+ const s = this.#s;
448
+ let k = from;
449
+ while (k < this.#n && isWhitespace(s.charCodeAt(k))) k++;
450
+ if (k >= this.#n) return true;
451
+ const c = s[k];
452
+ return c === "," || c === "}" || c === "]" || c === ":";
453
+ }
454
+
455
+ #number(): unknown {
456
+ const s = this.#s;
457
+ const start = this.#i;
458
+ while (this.#i < this.#n) {
459
+ const ch = s[this.#i];
460
+ if (
461
+ (ch >= "0" && ch <= "9") ||
462
+ ch === "-" ||
463
+ ch === "+" ||
464
+ ch === "." ||
465
+ ch === "e" ||
466
+ ch === "E" ||
467
+ ch === "x" ||
468
+ ch === "X" ||
469
+ (ch >= "a" && ch <= "f") ||
470
+ (ch >= "A" && ch <= "F")
471
+ ) {
472
+ this.#i++;
473
+ } else {
474
+ break;
475
+ }
476
+ }
477
+ const token = s.slice(start, this.#i);
478
+ const num = Number(token);
479
+ if (Number.isNaN(num)) {
480
+ if (this.#partial) return INCOMPLETE;
481
+ throw new SyntaxError(`Invalid number: ${token}`);
482
+ }
483
+ return num;
484
+ }
485
+
486
+ #keyword(allowBareword: boolean): unknown {
487
+ const s = this.#s;
488
+ const i = this.#i;
489
+ for (const [word, value] of KEYWORDS) {
490
+ // Require a non-identifier boundary so `Truex` / `nullish` are not misread
491
+ // as the keyword followed by junk.
492
+ if (s.startsWith(word, i) && !isIdentChar(s.charCodeAt(i + word.length))) {
493
+ this.#i += word.length;
494
+ return value;
495
+ }
496
+ }
497
+ if (this.#partial) {
498
+ // Incomplete / unrecognized atomic token at the streaming edge — signal the
499
+ // caller to roll back to the last valid prefix instead of committing junk.
500
+ this.#i = this.#n;
501
+ return INCOMPLETE;
502
+ }
503
+ if (allowBareword) return this.#bareword();
504
+ throw new SyntaxError(`Unexpected token at position ${this.#i}`);
505
+ }
506
+
507
+ /**
508
+ * Strict-mode recovery of an unquoted string value, e.g.
509
+ * `{"paths": packages/foo/*}`: consume until `,` / `}` / `]` / newline and
510
+ * trim trailing whitespace. Recovery still throws — so a final parse never
511
+ * accepts a half-formed or non-finite argument — when the token:
512
+ * - hits end-of-input before a delimiter (truncated value);
513
+ * - contains a `"`, `{`, `[`, or a key-like `:` — this parser accepts
514
+ * unquoted keys, so a missed comma (`{"a": foo "b": 1}`, `{a: foo b: 1}`)
515
+ * would otherwise silently swallow the following field. A colon followed
516
+ * by `/` or `\` stays literal so URL and Windows-path values recover;
517
+ * - is a non-finite atom ({@link NON_RECOVERABLE_BAREWORDS}).
518
+ */
519
+ #bareword(): string {
520
+ const s = this.#s;
521
+ const start = this.#i;
522
+ let i = start;
523
+ while (i < this.#n) {
524
+ const cc = s.charCodeAt(i);
525
+ if (cc === 0x2c /* , */ || cc === 0x7d /* } */ || cc === 0x5d /* ] */ || cc === 0x0a || cc === 0x0d) break;
526
+ if (
527
+ cc === QUOTE ||
528
+ cc === 0x7b /* { */ ||
529
+ cc === 0x5b /* [ */ ||
530
+ (cc === 0x3a /* : */ && s.charCodeAt(i + 1) !== 0x2f /* / */ && s.charCodeAt(i + 1) !== 0x5c) /* \ */
531
+ ) {
532
+ throw new SyntaxError(`Unexpected token at position ${start}`);
533
+ }
534
+ i++;
535
+ }
536
+ if (i >= this.#n) throw new SyntaxError(`Unexpected token at position ${start}`);
537
+ let end = i;
538
+ while (end > start && isWhitespace(s.charCodeAt(end - 1))) end--;
539
+ const word = s.slice(start, end);
540
+ if (NON_RECOVERABLE_BAREWORDS[word]) throw new SyntaxError(`Unexpected token at position ${start}`);
541
+ this.#i = i;
542
+ return word;
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Final-parse a JSON value, repairing the common LLM malformations
548
+ * ({@link RelaxedJson}). Tries strict `JSON.parse` first (fast path, exact JSON
549
+ * semantics), then the relaxed parser. Throws when the input is unrepairable,
550
+ * truncated, or carries trailing garbage — so callers can skip a bad tool call
551
+ * rather than execute a half-formed one.
552
+ */
553
+ export function parseJsonWithRepair<T>(json: string): T {
554
+ try {
555
+ return JSON.parse(json) as T;
556
+ } catch {
557
+ return new RelaxedJson(json, false).parse() as T;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Parse possibly-incomplete JSON during streaming. Always returns a value, never
563
+ * throws: `{}` for empty/whitespace/unrecoverable buffers, and an auto-closed
564
+ * best-effort object for truncated ones.
565
+ */
566
+ export function parseStreamingJson<T = Record<string, unknown>>(partialJson: string | undefined): T {
567
+ const trimmed = partialJson?.trimStart();
568
+ if (!trimmed) return {} as T;
569
+ try {
570
+ return JSON.parse(trimmed) as T;
571
+ } catch {
572
+ try {
573
+ return (new RelaxedJson(trimmed, true).parse() ?? {}) as T;
574
+ } catch {
575
+ return {} as T;
576
+ }
577
+ }
578
+ }
579
+
580
+ /**
581
+ * Default minimum byte growth before `parseStreamingJsonThrottled` will
582
+ * re-parse a streaming tool-call argument buffer. Bounds the mid-stream
583
+ * partial-parse cost from quadratic to linear in N.
584
+ */
585
+ export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
586
+
587
+ /**
588
+ * Throttled variant of {@link parseStreamingJson} for the per-delta hot path.
589
+ *
590
+ * Tool calls arrive as a long sequence of small deltas — calling
591
+ * `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
592
+ * each time, giving O(N²) work in the total buffer length. Throttling skips
593
+ * the re-parse until at least `minGrowthBytes` of new content has arrived
594
+ * since the last successful parse, bounding mid-stream cost to O(N).
595
+ *
596
+ * Each provider tracks the last parsed length on its tool-call block, so the
597
+ * final `toolcall_end` parse (which providers already perform unconditionally)
598
+ * is the authoritative full parse — the throttle only delays mid-stream UI
599
+ * updates by at most `minGrowthBytes` of accumulated partial content.
600
+ *
601
+ * @returns the parsed object plus the new `parsedLen` to persist; or `null`
602
+ * when the buffer has not grown enough to warrant a re-parse.
603
+ */
604
+ export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
605
+ partialJson: string | undefined,
606
+ lastParsedLen: number,
607
+ minGrowthBytes: number = STREAMING_JSON_PARSE_MIN_GROWTH,
608
+ ): { value: T; parsedLen: number } | null {
609
+ const len = partialJson?.length ?? 0;
610
+ if (len === 0 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
611
+ return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
612
+ }
package/src/json.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Try to parse JSON, returning null on failure.
3
+ */
4
+ export function tryParseJson<T = unknown>(content: string): T | null {
5
+ try {
6
+ return JSON.parse(content) as T;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }