@linxiraos/pi-utils 1.1.7 → 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 (188) hide show
  1. package/CHANGELOG.md +2 -8
  2. package/README.md +18 -18
  3. package/package.json +57 -63
  4. package/src/acp/connection.ts +6 -10
  5. package/src/ar/entries.ts +1 -1
  6. package/src/ar/sevenzip.ts +17 -2
  7. package/src/async.ts +25 -4
  8. package/src/browsers.ts +11 -5
  9. package/src/dirs.ts +15 -10
  10. package/src/dom/core.ts +4 -4
  11. package/src/env.ts +12 -0
  12. package/src/fetch-retry.ts +36 -1
  13. package/src/format.ts +1 -1
  14. package/src/frontmatter.ts +1 -0
  15. package/src/incoming-json.ts +856 -0
  16. package/src/index.ts +2 -0
  17. package/src/json-lexer.ts +413 -0
  18. package/src/json-parse.ts +93 -350
  19. package/src/marked/core.ts +8 -1
  20. package/src/materialize-string.ts +10 -0
  21. package/src/path.ts +14 -0
  22. package/src/postmortem.ts +33 -6
  23. package/src/prompt.ts +1 -0
  24. package/src/ring.ts +1 -0
  25. package/src/temp.ts +16 -4
  26. package/src/template.ts +2 -2
  27. package/src/tls-fetch.ts +2 -16
  28. package/src/turndown/html.ts +1 -1
  29. package/src/vterm/query-responder.ts +76 -0
  30. package/src/vterm.ts +1 -0
  31. package/THIRD-PARTY-NOTICES.txt +0 -22909
  32. package/dist/types/abortable.d.ts +0 -32
  33. package/dist/types/acp/connection.d.ts +0 -118
  34. package/dist/types/acp/protocol.d.ts +0 -526
  35. package/dist/types/acp/schema.d.ts +0 -41
  36. package/dist/types/acp/stream.d.ts +0 -8
  37. package/dist/types/acp/transport.d.ts +0 -88
  38. package/dist/types/acp.d.ts +0 -6
  39. package/dist/types/ar/arj.d.ts +0 -5
  40. package/dist/types/ar/asar.d.ts +0 -7
  41. package/dist/types/ar/bytes.d.ts +0 -20
  42. package/dist/types/ar/cab.d.ts +0 -5
  43. package/dist/types/ar/checksums.d.ts +0 -10
  44. package/dist/types/ar/codecs/bzip2.d.ts +0 -4
  45. package/dist/types/ar/codecs/gzip.d.ts +0 -6
  46. package/dist/types/ar/codecs/lzma.d.ts +0 -6
  47. package/dist/types/ar/codecs/lzw.d.ts +0 -4
  48. package/dist/types/ar/codecs/lzx.d.ts +0 -7
  49. package/dist/types/ar/codecs/xz.d.ts +0 -4
  50. package/dist/types/ar/codecs/zstd.d.ts +0 -6
  51. package/dist/types/ar/cpio.d.ts +0 -7
  52. package/dist/types/ar/deb.d.ts +0 -5
  53. package/dist/types/ar/entries.d.ts +0 -22
  54. package/dist/types/ar/error.d.ts +0 -8
  55. package/dist/types/ar/index.d.ts +0 -11
  56. package/dist/types/ar/iso.d.ts +0 -5
  57. package/dist/types/ar/limits.d.ts +0 -33
  58. package/dist/types/ar/lzh.d.ts +0 -7
  59. package/dist/types/ar/open.d.ts +0 -45
  60. package/dist/types/ar/paths.d.ts +0 -18
  61. package/dist/types/ar/rar/rar4-decoder.d.ts +0 -6
  62. package/dist/types/ar/rar/rar5-decoder.d.ts +0 -6
  63. package/dist/types/ar/rar.d.ts +0 -5
  64. package/dist/types/ar/reader.d.ts +0 -28
  65. package/dist/types/ar/registry.d.ts +0 -17
  66. package/dist/types/ar/rpm.d.ts +0 -5
  67. package/dist/types/ar/sevenzip/decode.d.ts +0 -33
  68. package/dist/types/ar/sevenzip.d.ts +0 -5
  69. package/dist/types/ar/source.d.ts +0 -54
  70. package/dist/types/ar/tar.d.ts +0 -9
  71. package/dist/types/ar/types.d.ts +0 -99
  72. package/dist/types/ar/unix-ar.d.ts +0 -7
  73. package/dist/types/ar/write.d.ts +0 -10
  74. package/dist/types/ar/zip.d.ts +0 -10
  75. package/dist/types/async.d.ts +0 -22
  76. package/dist/types/binary.d.ts +0 -23
  77. package/dist/types/browsers.d.ts +0 -68
  78. package/dist/types/chalk.d.ts +0 -125
  79. package/dist/types/cli.d.ts +0 -134
  80. package/dist/types/color.d.ts +0 -136
  81. package/dist/types/dates.d.ts +0 -7
  82. package/dist/types/dirs.d.ts +0 -325
  83. package/dist/types/docx/converter.d.ts +0 -46
  84. package/dist/types/docx/xml.d.ts +0 -26
  85. package/dist/types/docx.d.ts +0 -11
  86. package/dist/types/dom/core.d.ts +0 -431
  87. package/dist/types/dom/parser.d.ts +0 -7
  88. package/dist/types/dom/selector.d.ts +0 -5
  89. package/dist/types/dom.d.ts +0 -5
  90. package/dist/types/env.d.ts +0 -124
  91. package/dist/types/fetch-retry.d.ts +0 -96
  92. package/dist/types/file-lock.d.ts +0 -23
  93. package/dist/types/format.d.ts +0 -37
  94. package/dist/types/frontmatter.d.ts +0 -46
  95. package/dist/types/fs-error.d.ts +0 -31
  96. package/dist/types/glob.d.ts +0 -28
  97. package/dist/types/headers.d.ts +0 -34
  98. package/dist/types/index.d.ts +0 -41
  99. package/dist/types/json-parse.d.ts +0 -83
  100. package/dist/types/json.d.ts +0 -20
  101. package/dist/types/logger/rotating-file.d.ts +0 -18
  102. package/dist/types/logger.d.ts +0 -96
  103. package/dist/types/loop-phase.d.ts +0 -10
  104. package/dist/types/lru.d.ts +0 -46
  105. package/dist/types/marked/core.d.ts +0 -445
  106. package/dist/types/marked.d.ts +0 -2
  107. package/dist/types/math-delimiters.d.ts +0 -45
  108. package/dist/types/mermaid-ascii.d.ts +0 -11
  109. package/dist/types/mime.d.ts +0 -29
  110. package/dist/types/module-timer.d.ts +0 -1
  111. package/dist/types/path-tree.d.ts +0 -76
  112. package/dist/types/path.d.ts +0 -2
  113. package/dist/types/peek-file.d.ts +0 -29
  114. package/dist/types/postmortem.d.ts +0 -174
  115. package/dist/types/process-name.d.ts +0 -7
  116. package/dist/types/procmgr.d.ts +0 -74
  117. package/dist/types/prompt.d.ts +0 -18
  118. package/dist/types/ptree.d.ts +0 -124
  119. package/dist/types/readability/readability.d.ts +0 -9
  120. package/dist/types/readability/readerable.d.ts +0 -10
  121. package/dist/types/readability/types.d.ts +0 -70
  122. package/dist/types/readability.d.ts +0 -4
  123. package/dist/types/ring.d.ts +0 -93
  124. package/dist/types/runtime-install.d.ts +0 -85
  125. package/dist/types/sanitize-text.d.ts +0 -29
  126. package/dist/types/snowflake.d.ts +0 -25
  127. package/dist/types/sqlite.d.ts +0 -26
  128. package/dist/types/stderr-guard.d.ts +0 -22
  129. package/dist/types/stream.d.ts +0 -75
  130. package/dist/types/tab-spacing.d.ts +0 -24
  131. package/dist/types/temp.d.ts +0 -17
  132. package/dist/types/template.d.ts +0 -62
  133. package/dist/types/timing-buffer.d.ts +0 -22
  134. package/dist/types/tls-fetch.d.ts +0 -37
  135. package/dist/types/turndown/gfm.d.ts +0 -11
  136. package/dist/types/turndown/html.d.ts +0 -5
  137. package/dist/types/turndown/service.d.ts +0 -21
  138. package/dist/types/turndown/types.d.ts +0 -70
  139. package/dist/types/turndown.d.ts +0 -4
  140. package/dist/types/type-guards.d.ts +0 -5
  141. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +0 -41
  142. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +0 -89
  143. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +0 -7
  144. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +0 -12
  145. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +0 -66
  146. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +0 -48
  147. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +0 -43
  148. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +0 -7
  149. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +0 -56
  150. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +0 -65
  151. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +0 -27
  152. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +0 -17
  153. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +0 -7
  154. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +0 -11
  155. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +0 -34
  156. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +0 -11
  157. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +0 -11
  158. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +0 -26
  159. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +0 -31
  160. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +0 -11
  161. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +0 -59
  162. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +0 -17
  163. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +0 -30
  164. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +0 -55
  165. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +0 -206
  166. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +0 -51
  167. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +0 -2
  168. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +0 -6
  169. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +0 -102
  170. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +0 -6
  171. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +0 -76
  172. package/dist/types/vendor/mermaid-ascii/index.d.ts +0 -1
  173. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +0 -9
  174. package/dist/types/vendor/mermaid-ascii/parser.d.ts +0 -7
  175. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +0 -6
  176. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +0 -130
  177. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +0 -23
  178. package/dist/types/vendor/mermaid-ascii/types.d.ts +0 -114
  179. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +0 -25
  180. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +0 -6
  181. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +0 -145
  182. package/dist/types/version.d.ts +0 -18
  183. package/dist/types/vterm/buffer.d.ts +0 -99
  184. package/dist/types/vterm/terminal.d.ts +0 -44
  185. package/dist/types/vterm.d.ts +0 -8
  186. package/dist/types/which.d.ts +0 -37
  187. package/dist/types/worker-host.d.ts +0 -51
  188. 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
 
@@ -343,8 +343,15 @@ const DEFAULTS: MarkedOptions = {
343
343
  };
344
344
  const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
345
345
 
346
- function tokenList(links: Links = {}): TokensList {
346
+ function tokenList(links: Links = Object.create(null)): TokensList {
347
347
  const list = [] as unknown as TokensList;
348
+ // The reference-definition map is keyed by user-controlled labels. A plain
349
+ // `{}` inherits `Object.prototype`, so a reference-style link whose label is
350
+ // an inherited member (`[x][constructor]`, `[x][__proto__]`) resolves to a
351
+ // truthy non-definition and yields a link token with `href: undefined`
352
+ // (issue #10283). A null-prototype map makes such lookups miss, so the link
353
+ // correctly falls back to literal text, and label writes cannot pollute the
354
+ // prototype.
348
355
  list.links = links;
349
356
  return list;
350
357
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copy text into independent UTF-16 backing storage before a bounded substring outlives its source.
3
+ *
4
+ * The UTF-16 round trip preserves every JavaScript code unit, including lone
5
+ * surrogates that a UTF-8 round trip would replace.
6
+ */
7
+ export function materializeString(text: string): string {
8
+ if (text.length === 0) return "";
9
+ return Buffer.from(text, "utf16le").toString("utf16le");
10
+ }
package/src/path.ts CHANGED
@@ -1,3 +1,17 @@
1
+ import * as path from "node:path";
2
+
3
+ const WINDOWS_DRIVE_PATH = /^([A-Za-z]):[\\/](.*)$/;
4
+
5
+ /** Maps an absolute Windows drive path to the drive's default WSL mount. */
6
+ export function windowsPathToWslMount(filePath: string): string | undefined {
7
+ const normalized = path.win32.normalize(filePath.trim());
8
+ const match = WINDOWS_DRIVE_PATH.exec(normalized);
9
+ if (!match) return undefined;
10
+ const [, drive, rest] = match;
11
+ const segments = rest.split("\\").filter(Boolean);
12
+ return path.posix.join("/mnt", drive!.toLowerCase(), ...segments);
13
+ }
14
+
1
15
  const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
2
16
  const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
3
17
  const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
package/src/postmortem.ts CHANGED
@@ -222,7 +222,7 @@ export function isInternalSocketClosedError(err: unknown): boolean {
222
222
  const internal = frames.every(frame => {
223
223
  const trimmed = frame.trim();
224
224
  if (trimmed === "" || trimmed === "at unknown" || trimmed === "at native") return true;
225
- if (!/\(node:[^)]*\)$/.test(trimmed) && !/^at node:/.test(trimmed)) return false;
225
+ if (!/\(node:[^)]*\)$/.test(trimmed) && !trimmed.startsWith("at node:")) return false;
226
226
  hasNetFrame ||= trimmed.includes("node:net:");
227
227
  return true;
228
228
  });
@@ -395,23 +395,40 @@ function formatFatalError(label: string, err: Error): string {
395
395
  return `\n[${label}] ${name}: ${message}${formattedStack}\n`;
396
396
  }
397
397
 
398
- async function exitAfterFatal(label: string, logMessage: string, err: Error, reason: Reason): Promise<void> {
398
+ async function exitAfterFatal(output: string, logMessage: string, err: Error, reason: Reason): Promise<never> {
399
399
  const forcedExit = setTimeout(() => exitProcess(1), CLEANUP_DEADLINE_MS);
400
400
  try {
401
+ // Cleanup callbacks are invoked synchronously before runCleanup returns its
402
+ // completion promise. TUI owners therefore hand the cursor back before the
403
+ // fatal report is written, while slower resource cleanup continues afterward.
404
+ const cleanup = runCleanup(reason);
401
405
  restoreTerminalStderr();
402
406
  // A revoked terminal can make stream writes raise another fatal error. Use
403
407
  // the descriptor directly so failure stays synchronous and contained.
404
408
  try {
405
- fs.writeSync(2, `${formatFatalError(label, err)}${formatFatalRecoveryHints()}`);
409
+ fs.writeSync(2, output);
406
410
  } catch {}
407
411
  logger.error(logMessage, { err });
408
- await runCleanup(reason);
412
+ await cleanup;
409
413
  } finally {
410
414
  clearTimeout(forcedExit);
411
415
  exitProcess(1);
412
416
  }
413
417
  }
414
418
 
419
+ /**
420
+ * Reports a caught top-level failure after terminal owners restore their display, then exits.
421
+ */
422
+ export async function fatal(error: unknown): Promise<never> {
423
+ const err = error instanceof Error ? error : new Error(String(error));
424
+ const output = `${Bun.inspect(error, { colors: process.stderr.isTTY === true })}\n${formatFatalRecoveryHints()}`;
425
+ if (!isMainThread) {
426
+ process.stderr.write(output);
427
+ process.exit(1);
428
+ }
429
+ return exitAfterFatal(output, "Fatal error", err, Reason.UNHANDLED_REJECTION);
430
+ }
431
+
415
432
  if (isMainThread) {
416
433
  process
417
434
  .on("SIGINT", async () => {
@@ -461,7 +478,12 @@ if (isMainThread) {
461
478
  });
462
479
  return;
463
480
  }
464
- await exitAfterFatal("Uncaught Exception", "Uncaught exception", err, Reason.UNCAUGHT_EXCEPTION);
481
+ await exitAfterFatal(
482
+ `${formatFatalError("Uncaught Exception", err)}${formatFatalRecoveryHints()}`,
483
+ "Uncaught exception",
484
+ err,
485
+ Reason.UNCAUGHT_EXCEPTION,
486
+ );
465
487
  })
466
488
  .on("unhandledRejection", async reason => {
467
489
  const err = reason instanceof Error ? reason : new Error(String(reason));
@@ -497,7 +519,12 @@ if (isMainThread) {
497
519
  });
498
520
  }
499
521
  }
500
- await exitAfterFatal("Unhandled Rejection", "Unhandled rejection", err, Reason.UNHANDLED_REJECTION);
522
+ await exitAfterFatal(
523
+ `${formatFatalError("Unhandled Rejection", err)}${formatFatalRecoveryHints()}`,
524
+ "Unhandled rejection",
525
+ err,
526
+ Reason.UNHANDLED_REJECTION,
527
+ );
501
528
  })
502
529
  .on("exit", async () => {
503
530
  void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
package/src/prompt.ts CHANGED
@@ -196,6 +196,7 @@ export function format(content: string, options: PromptFormatOptions = {}): stri
196
196
  } = options;
197
197
  const isPreRender = renderPhase === "pre-render";
198
198
  const lines = content.split("\n");
199
+ // [suppressed] length preallocation
199
200
  const result: string[] = new Array(lines.length);
200
201
  let n = 0; // logical length of `result` (pops are n--)
201
202
  let inCodeBlock = false;
package/src/ring.ts CHANGED
@@ -15,6 +15,7 @@ export class RingBuffer<T> {
15
15
  * @param capacity - The maximum number of elements the buffer can hold. Must be positive.
16
16
  */
17
17
  constructor(public readonly capacity: number) {
18
+ // [suppressed] length preallocation
18
19
  this.#buf = new Array(capacity);
19
20
  }
20
21