@linxiraos/pi-utils 1.1.8 → 1.1.9

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 (170) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/README.md +18 -18
  3. package/package.json +57 -63
  4. package/src/async.ts +16 -0
  5. package/src/dirs.ts +15 -10
  6. package/src/incoming-json.ts +856 -0
  7. package/src/index.ts +1 -0
  8. package/src/json-lexer.ts +413 -0
  9. package/src/json-parse.ts +93 -350
  10. package/src/tls-fetch.ts +2 -16
  11. package/THIRD-PARTY-NOTICES.txt +0 -22909
  12. package/dist/types/abortable.d.ts +0 -32
  13. package/dist/types/acp/connection.d.ts +0 -118
  14. package/dist/types/acp/protocol.d.ts +0 -526
  15. package/dist/types/acp/schema.d.ts +0 -41
  16. package/dist/types/acp/stream.d.ts +0 -8
  17. package/dist/types/acp/transport.d.ts +0 -88
  18. package/dist/types/acp.d.ts +0 -6
  19. package/dist/types/ar/arj.d.ts +0 -5
  20. package/dist/types/ar/asar.d.ts +0 -7
  21. package/dist/types/ar/bytes.d.ts +0 -20
  22. package/dist/types/ar/cab.d.ts +0 -5
  23. package/dist/types/ar/checksums.d.ts +0 -10
  24. package/dist/types/ar/codecs/bzip2.d.ts +0 -4
  25. package/dist/types/ar/codecs/gzip.d.ts +0 -6
  26. package/dist/types/ar/codecs/lzma.d.ts +0 -6
  27. package/dist/types/ar/codecs/lzw.d.ts +0 -4
  28. package/dist/types/ar/codecs/lzx.d.ts +0 -7
  29. package/dist/types/ar/codecs/xz.d.ts +0 -4
  30. package/dist/types/ar/codecs/zstd.d.ts +0 -6
  31. package/dist/types/ar/cpio.d.ts +0 -7
  32. package/dist/types/ar/deb.d.ts +0 -5
  33. package/dist/types/ar/entries.d.ts +0 -22
  34. package/dist/types/ar/error.d.ts +0 -8
  35. package/dist/types/ar/index.d.ts +0 -11
  36. package/dist/types/ar/iso.d.ts +0 -5
  37. package/dist/types/ar/limits.d.ts +0 -33
  38. package/dist/types/ar/lzh.d.ts +0 -7
  39. package/dist/types/ar/open.d.ts +0 -45
  40. package/dist/types/ar/paths.d.ts +0 -18
  41. package/dist/types/ar/rar/rar4-decoder.d.ts +0 -6
  42. package/dist/types/ar/rar/rar5-decoder.d.ts +0 -6
  43. package/dist/types/ar/rar.d.ts +0 -5
  44. package/dist/types/ar/reader.d.ts +0 -28
  45. package/dist/types/ar/registry.d.ts +0 -17
  46. package/dist/types/ar/rpm.d.ts +0 -5
  47. package/dist/types/ar/sevenzip/decode.d.ts +0 -33
  48. package/dist/types/ar/sevenzip.d.ts +0 -5
  49. package/dist/types/ar/source.d.ts +0 -54
  50. package/dist/types/ar/tar.d.ts +0 -9
  51. package/dist/types/ar/types.d.ts +0 -99
  52. package/dist/types/ar/unix-ar.d.ts +0 -7
  53. package/dist/types/ar/write.d.ts +0 -10
  54. package/dist/types/ar/zip.d.ts +0 -10
  55. package/dist/types/async.d.ts +0 -22
  56. package/dist/types/binary.d.ts +0 -23
  57. package/dist/types/browsers.d.ts +0 -68
  58. package/dist/types/chalk.d.ts +0 -125
  59. package/dist/types/cli.d.ts +0 -134
  60. package/dist/types/color.d.ts +0 -136
  61. package/dist/types/dates.d.ts +0 -7
  62. package/dist/types/dirs.d.ts +0 -325
  63. package/dist/types/docx/converter.d.ts +0 -46
  64. package/dist/types/docx/xml.d.ts +0 -26
  65. package/dist/types/docx.d.ts +0 -11
  66. package/dist/types/dom/core.d.ts +0 -431
  67. package/dist/types/dom/parser.d.ts +0 -7
  68. package/dist/types/dom/selector.d.ts +0 -5
  69. package/dist/types/dom.d.ts +0 -5
  70. package/dist/types/env.d.ts +0 -133
  71. package/dist/types/fetch-retry.d.ts +0 -98
  72. package/dist/types/file-lock.d.ts +0 -23
  73. package/dist/types/format.d.ts +0 -37
  74. package/dist/types/frontmatter.d.ts +0 -46
  75. package/dist/types/fs-error.d.ts +0 -31
  76. package/dist/types/glob.d.ts +0 -28
  77. package/dist/types/headers.d.ts +0 -34
  78. package/dist/types/index.d.ts +0 -42
  79. package/dist/types/json-parse.d.ts +0 -83
  80. package/dist/types/json.d.ts +0 -20
  81. package/dist/types/logger/rotating-file.d.ts +0 -18
  82. package/dist/types/logger.d.ts +0 -96
  83. package/dist/types/loop-phase.d.ts +0 -10
  84. package/dist/types/lru.d.ts +0 -46
  85. package/dist/types/marked/core.d.ts +0 -445
  86. package/dist/types/marked.d.ts +0 -2
  87. package/dist/types/materialize-string.d.ts +0 -7
  88. package/dist/types/math-delimiters.d.ts +0 -45
  89. package/dist/types/mermaid-ascii.d.ts +0 -11
  90. package/dist/types/mime.d.ts +0 -29
  91. package/dist/types/module-timer.d.ts +0 -1
  92. package/dist/types/path-tree.d.ts +0 -76
  93. package/dist/types/path.d.ts +0 -4
  94. package/dist/types/peek-file.d.ts +0 -29
  95. package/dist/types/postmortem.d.ts +0 -178
  96. package/dist/types/process-name.d.ts +0 -7
  97. package/dist/types/procmgr.d.ts +0 -74
  98. package/dist/types/prompt.d.ts +0 -18
  99. package/dist/types/ptree.d.ts +0 -124
  100. package/dist/types/readability/readability.d.ts +0 -9
  101. package/dist/types/readability/readerable.d.ts +0 -10
  102. package/dist/types/readability/types.d.ts +0 -70
  103. package/dist/types/readability.d.ts +0 -4
  104. package/dist/types/ring.d.ts +0 -93
  105. package/dist/types/runtime-install.d.ts +0 -85
  106. package/dist/types/sanitize-text.d.ts +0 -29
  107. package/dist/types/snowflake.d.ts +0 -25
  108. package/dist/types/sqlite.d.ts +0 -26
  109. package/dist/types/stderr-guard.d.ts +0 -22
  110. package/dist/types/stream.d.ts +0 -75
  111. package/dist/types/tab-spacing.d.ts +0 -24
  112. package/dist/types/temp.d.ts +0 -24
  113. package/dist/types/template.d.ts +0 -62
  114. package/dist/types/timing-buffer.d.ts +0 -22
  115. package/dist/types/tls-fetch.d.ts +0 -37
  116. package/dist/types/turndown/gfm.d.ts +0 -11
  117. package/dist/types/turndown/html.d.ts +0 -5
  118. package/dist/types/turndown/service.d.ts +0 -21
  119. package/dist/types/turndown/types.d.ts +0 -70
  120. package/dist/types/turndown.d.ts +0 -4
  121. package/dist/types/type-guards.d.ts +0 -5
  122. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +0 -41
  123. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +0 -89
  124. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +0 -7
  125. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +0 -12
  126. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +0 -66
  127. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +0 -48
  128. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +0 -43
  129. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +0 -7
  130. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +0 -56
  131. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +0 -65
  132. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +0 -27
  133. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +0 -17
  134. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +0 -7
  135. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +0 -11
  136. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +0 -34
  137. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +0 -11
  138. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +0 -11
  139. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +0 -26
  140. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +0 -31
  141. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +0 -11
  142. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +0 -59
  143. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +0 -17
  144. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +0 -30
  145. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +0 -55
  146. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +0 -206
  147. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +0 -51
  148. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +0 -2
  149. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +0 -6
  150. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +0 -102
  151. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +0 -6
  152. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +0 -76
  153. package/dist/types/vendor/mermaid-ascii/index.d.ts +0 -1
  154. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +0 -9
  155. package/dist/types/vendor/mermaid-ascii/parser.d.ts +0 -7
  156. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +0 -6
  157. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +0 -130
  158. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +0 -23
  159. package/dist/types/vendor/mermaid-ascii/types.d.ts +0 -114
  160. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +0 -25
  161. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +0 -6
  162. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +0 -145
  163. package/dist/types/version.d.ts +0 -18
  164. package/dist/types/vterm/buffer.d.ts +0 -99
  165. package/dist/types/vterm/query-responder.d.ts +0 -24
  166. package/dist/types/vterm/terminal.d.ts +0 -44
  167. package/dist/types/vterm.d.ts +0 -9
  168. package/dist/types/which.d.ts +0 -37
  169. package/dist/types/worker-host.d.ts +0 -51
  170. package/dist/types/xml.d.ts +0 -31
package/src/json-parse.ts CHANGED
@@ -1,11 +1,21 @@
1
- const QUOTE = 0x22;
2
- const BACKSLASH = 0x5c;
3
- const U = 0x75;
4
- const SQUOTE = 0x27;
1
+ import {
2
+ BACKSLASH,
3
+ COLON,
4
+ COMMA,
5
+ isHexDigit,
6
+ isNumberStart,
7
+ isWhitespace,
8
+ JsonLexer,
9
+ LBRACE,
10
+ LBRACKET,
11
+ QUOTE,
12
+ RBRACE,
13
+ RBRACKET,
14
+ SQUOTE,
15
+ VALID_ESCAPE_CHAR,
16
+ } from "./json-lexer";
5
17
 
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;
18
+ const U = 0x75;
9
19
 
10
20
  const CONTROL_ESCAPES: readonly string[] = (() => {
11
21
  const e: string[] = [];
@@ -20,47 +30,6 @@ const CONTROL_ESCAPES: readonly string[] = (() => {
20
30
  return e;
21
31
  })();
22
32
 
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
33
  /**
65
34
  * Sentinel returned by partial-mode value parsing when an atomic value
66
35
  * (number / keyword) is incomplete at the streaming edge, so the enclosing
@@ -160,139 +129,109 @@ export function repairJson(json: string): string {
160
129
  }
161
130
 
162
131
  /**
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.
132
+ * Recursive-descent tree builder over the tolerant {@link JsonLexer} grammar
133
+ * (single quotes, unquoted keys, comments, Python literals, stray commas,
134
+ * literal invalid escapes, apostrophe recovery, bareword values).
176
135
  *
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.
136
+ * In `partial` mode (lexer mode `streaming`) an unterminated string/object/array
137
+ * (or a value cut off at end-of-input) is auto-closed with whatever was parsed
138
+ * so far, and an incomplete trailing atom rolls the enclosing container back
139
+ * to its last valid prefix. In strict mode, end-of-input mid-value and trailing
140
+ * garbage both throw, so a final parse never silently accepts a half-formed
141
+ * tool call.
181
142
  */
182
143
  class RelaxedJson {
183
- readonly #s: string;
184
- readonly #n: number;
144
+ readonly #lex: JsonLexer;
185
145
  readonly #partial: boolean;
186
- #i = 0;
187
146
 
188
147
  constructor(source: string, partial: boolean) {
189
- this.#s = source;
190
- this.#n = source.length;
148
+ this.#lex = new JsonLexer(source, partial ? "streaming" : "strict");
191
149
  this.#partial = partial;
192
150
  }
193
151
 
194
152
  parse(): unknown {
195
- this.#ws();
196
- if (this.#i >= this.#n) {
153
+ const lex = this.#lex;
154
+ lex.ws();
155
+ if (lex.atEnd) {
197
156
  if (this.#partial) return undefined;
198
157
  throw new SyntaxError("Unexpected end of JSON input");
199
158
  }
200
159
  const value = this.#value(false);
201
160
  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}`);
161
+ lex.ws();
162
+ if (!this.#partial && !lex.atEnd) {
163
+ throw new SyntaxError(`Unexpected trailing characters at position ${lex.pos}`);
205
164
  }
206
165
  return value;
207
166
  }
208
167
 
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
168
  #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();
169
+ const lex = this.#lex;
170
+ const c = lex.peek();
171
+ if (c === LBRACE) return this.#object();
172
+ if (c === LBRACKET) return this.#array();
173
+ if (c === QUOTE || c === SQUOTE) return lex.string(c).value;
174
+ // JS-only NaN / Infinity are deliberately not accepted: a tool must not
175
+ // execute with a non-finite numeric arg; they fail the lexer's finite
176
+ // guard (strict throw / partial rollback) like other bad tokens.
177
+ if (isNumberStart(c)) return lex.number() ?? INCOMPLETE;
178
+ const keyword = lex.keyword();
179
+ if (keyword !== undefined) return keyword;
180
+ if (this.#partial) {
181
+ // Incomplete / unrecognized atomic token at the streaming edge — signal the
182
+ // caller to roll back to the last valid prefix instead of committing junk.
183
+ lex.pos = lex.src.length;
184
+ return INCOMPLETE;
248
185
  }
249
- return this.#keyword(allowBareword);
186
+ if (allowBareword) return lex.bareword();
187
+ throw new SyntaxError(`Unexpected token at position ${lex.pos}`);
250
188
  }
251
189
 
252
190
  #object(): Record<string, unknown> {
253
- this.#i++; // consume {
191
+ const lex = this.#lex;
192
+ lex.pos++; // consume {
254
193
  const out: Record<string, unknown> = {};
255
194
  for (;;) {
256
- this.#ws();
257
- if (this.#i >= this.#n) {
195
+ lex.ws();
196
+ if (lex.atEnd) {
258
197
  if (this.#partial) return out;
259
198
  throw new SyntaxError("Unterminated object");
260
199
  }
261
- const c = this.#s[this.#i];
262
- if (c === "}") {
263
- this.#i++;
200
+ const c = lex.peek();
201
+ if (c === RBRACE) {
202
+ lex.pos++;
264
203
  return out;
265
204
  }
266
- if (c === ",") {
205
+ if (c === COMMA) {
267
206
  // Tolerate leading / doubled / trailing commas.
268
- this.#i++;
207
+ lex.pos++;
269
208
  continue;
270
209
  }
271
210
  const key = this.#key();
272
- this.#ws();
273
- if (this.#i < this.#n && this.#s[this.#i] === ":") {
274
- this.#i++;
211
+ lex.ws();
212
+ if (lex.peek() === COLON) {
213
+ lex.pos++;
275
214
  } else if (this.#partial) {
276
215
  return out;
277
216
  } else {
278
217
  throw new SyntaxError("Expected ':' in object");
279
218
  }
280
- this.#ws();
281
- if (this.#i >= this.#n) {
219
+ lex.ws();
220
+ if (lex.atEnd) {
282
221
  if (this.#partial) return out;
283
222
  throw new SyntaxError("Expected value after ':'");
284
223
  }
285
224
  const value = this.#value(true);
286
225
  if (value === INCOMPLETE) return out;
287
226
  out[key] = value;
288
- this.#ws();
289
- const d = this.#i < this.#n ? this.#s[this.#i] : "";
290
- if (d === ",") {
291
- this.#i++;
227
+ lex.ws();
228
+ const d = lex.peek();
229
+ if (d === COMMA) {
230
+ lex.pos++;
292
231
  continue;
293
232
  }
294
- if (d === "}") {
295
- this.#i++;
233
+ if (d === RBRACE) {
234
+ lex.pos++;
296
235
  return out;
297
236
  }
298
237
  if (this.#partial) return out;
@@ -301,34 +240,35 @@ class RelaxedJson {
301
240
  }
302
241
 
303
242
  #array(): unknown[] {
304
- this.#i++; // consume [
243
+ const lex = this.#lex;
244
+ lex.pos++; // consume [
305
245
  const out: unknown[] = [];
306
246
  for (;;) {
307
- this.#ws();
308
- if (this.#i >= this.#n) {
247
+ lex.ws();
248
+ if (lex.atEnd) {
309
249
  if (this.#partial) return out;
310
250
  throw new SyntaxError("Unterminated array");
311
251
  }
312
- const c = this.#s[this.#i];
313
- if (c === "]") {
314
- this.#i++;
252
+ const c = lex.peek();
253
+ if (c === RBRACKET) {
254
+ lex.pos++;
315
255
  return out;
316
256
  }
317
- if (c === ",") {
318
- this.#i++;
257
+ if (c === COMMA) {
258
+ lex.pos++;
319
259
  continue;
320
260
  }
321
261
  const value = this.#value(true);
322
262
  if (value === INCOMPLETE) return out;
323
263
  out.push(value);
324
- this.#ws();
325
- const d = this.#i < this.#n ? this.#s[this.#i] : "";
326
- if (d === ",") {
327
- this.#i++;
264
+ lex.ws();
265
+ const d = lex.peek();
266
+ if (d === COMMA) {
267
+ lex.pos++;
328
268
  continue;
329
269
  }
330
- if (d === "]") {
331
- this.#i++;
270
+ if (d === RBRACKET) {
271
+ lex.pos++;
332
272
  return out;
333
273
  }
334
274
  if (this.#partial) return out;
@@ -337,209 +277,12 @@ class RelaxedJson {
337
277
  }
338
278
 
339
279
  #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;
280
+ const lex = this.#lex;
281
+ const c = lex.peek();
282
+ if (c === QUOTE || c === SQUOTE) return lex.string(c).value;
283
+ const key = lex.unquotedKey();
284
+ if (key.length === 0 && !this.#partial) throw new SyntaxError("Expected object key");
285
+ return key;
543
286
  }
544
287
  }
545
288
 
package/src/tls-fetch.ts CHANGED
@@ -76,7 +76,7 @@ let cacheValue: string | undefined;
76
76
  * path, matching Node's "extensionless filename is still a path" contract.
77
77
  * `ENOENT` becomes {@link ExtraCaError}; other I/O errors bubble.
78
78
  */
79
- function resolveExtraCa(): string | undefined {
79
+ export function resolveExtraCa(): string | undefined {
80
80
  const raw = $env.NODE_EXTRA_CA_CERTS?.trim();
81
81
  if (!raw) return undefined;
82
82
 
@@ -122,7 +122,7 @@ export function __resetExtraCaCache(): void {
122
122
  * Anthropic Foundry's mTLS options, which already seed
123
123
  * `tls.rootCertificates`), only the extra CA is appended.
124
124
  */
125
- function withExtraCaInit(init: RequestInit | undefined, extraCa: string): RequestInit {
125
+ export function withExtraCaInit(init: RequestInit | undefined, extraCa: string): RequestInit {
126
126
  const existingTls = (init as BunTlsRequestInit | undefined)?.tls;
127
127
  const existingCa = existingTls?.ca;
128
128
  let mergedCa: string[];
@@ -162,17 +162,3 @@ export function wrapFetchForExtraCa(fetchImpl: FetchImpl): FetchImpl {
162
162
  );
163
163
  return wrapped;
164
164
  }
165
-
166
- /**
167
- * Convenience for options-bag composition (e.g. the stream-entry path in
168
- * `@linxiraos/pi-ai`'s `stream.ts`, which mirrors `withRequestDebugFetch` so
169
- * the proxy/debug/extra-CA wrappers compose uniformly). No-op when the env
170
- * var is unset.
171
- */
172
- export function withExtraCaFetch<T extends { fetch?: FetchImpl } | undefined>(options: T): T {
173
- if (!$env.NODE_EXTRA_CA_CERTS?.trim()) return options;
174
- const fetchImpl = options?.fetch ?? (globalThis.fetch as FetchImpl);
175
- const wrapped = wrapFetchForExtraCa(fetchImpl);
176
- if (wrapped === fetchImpl && options?.fetch !== undefined) return options;
177
- return { ...options, fetch: wrapped } as T;
178
- }