@oh-my-pi/pi-utils 16.3.0 → 16.3.2
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.
- package/CHANGELOG.md +6 -0
- package/package.json +2 -2
- package/src/json-parse.ts +61 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.1] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `parseJsonWithRepair` failing tool calls whose streamed arguments contain an unquoted string value (e.g. `{"paths": packages/foo/*, "i": "…"}`). Final parsing now recovers such barewords in object/array value position as strings, terminating at `,` / `}` / `]` / newline. Recovery deliberately refuses anything that could mask real structure or bad data — truncated values, tokens containing `"` / `{` / `[` or a key-like `:` (URL `://` and Windows `:\` colons stay literal), and non-finite atoms (`NaN`, `Infinity`, `undefined`) — and streaming partial parses still roll back unfinished barewords instead of committing them.
|
|
10
|
+
|
|
5
11
|
## [16.3.0] - 2026-07-02
|
|
6
12
|
|
|
7
13
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.2",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "16.3.
|
|
34
|
+
"@oh-my-pi/pi-natives": "16.3.2",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "^5.0.0"
|
package/src/json-parse.ts
CHANGED
|
@@ -49,6 +49,18 @@ const KEYWORDS: readonly (readonly [string, unknown])[] = [
|
|
|
49
49
|
["None", null],
|
|
50
50
|
];
|
|
51
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
|
+
|
|
52
64
|
/**
|
|
53
65
|
* Sentinel returned by partial-mode value parsing when an atomic value
|
|
54
66
|
* (number / keyword) is incomplete at the streaming edge, so the enclosing
|
|
@@ -157,7 +169,10 @@ export function repairJson(json: string): string {
|
|
|
157
169
|
* - Python literals `True` / `False` / `None` and JS `NaN` / `Infinity`;
|
|
158
170
|
* - raw control characters and invalid `\x` escapes inside strings (kept literally);
|
|
159
171
|
* - unescaped quotes inside strings — a quote only closes a string when followed
|
|
160
|
-
* by a value terminator, recovering apostrophes such as `'it's'
|
|
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.
|
|
161
176
|
*
|
|
162
177
|
* In `partial` mode an unterminated string/object/array (or a value cut off at
|
|
163
178
|
* end-of-input) is auto-closed with whatever was parsed so far — for streaming.
|
|
@@ -182,7 +197,7 @@ class RelaxedJson {
|
|
|
182
197
|
if (this.#partial) return undefined;
|
|
183
198
|
throw new SyntaxError("Unexpected end of JSON input");
|
|
184
199
|
}
|
|
185
|
-
const value = this.#value();
|
|
200
|
+
const value = this.#value(false);
|
|
186
201
|
if (value === INCOMPLETE) return undefined;
|
|
187
202
|
this.#ws();
|
|
188
203
|
if (!this.#partial && this.#i < this.#n) {
|
|
@@ -218,7 +233,7 @@ class RelaxedJson {
|
|
|
218
233
|
}
|
|
219
234
|
}
|
|
220
235
|
|
|
221
|
-
#value(): unknown {
|
|
236
|
+
#value(allowBareword: boolean): unknown {
|
|
222
237
|
const s = this.#s;
|
|
223
238
|
const c = s[this.#i];
|
|
224
239
|
if (c === "{") return this.#object();
|
|
@@ -231,7 +246,7 @@ class RelaxedJson {
|
|
|
231
246
|
// NaN guard (strict throw / partial rollback) like other bad tokens.
|
|
232
247
|
return this.#number();
|
|
233
248
|
}
|
|
234
|
-
return this.#keyword();
|
|
249
|
+
return this.#keyword(allowBareword);
|
|
235
250
|
}
|
|
236
251
|
|
|
237
252
|
#object(): Record<string, unknown> {
|
|
@@ -267,7 +282,7 @@ class RelaxedJson {
|
|
|
267
282
|
if (this.#partial) return out;
|
|
268
283
|
throw new SyntaxError("Expected value after ':'");
|
|
269
284
|
}
|
|
270
|
-
const value = this.#value();
|
|
285
|
+
const value = this.#value(true);
|
|
271
286
|
if (value === INCOMPLETE) return out;
|
|
272
287
|
out[key] = value;
|
|
273
288
|
this.#ws();
|
|
@@ -303,7 +318,7 @@ class RelaxedJson {
|
|
|
303
318
|
this.#i++;
|
|
304
319
|
continue;
|
|
305
320
|
}
|
|
306
|
-
const value = this.#value();
|
|
321
|
+
const value = this.#value(true);
|
|
307
322
|
if (value === INCOMPLETE) return out;
|
|
308
323
|
out.push(value);
|
|
309
324
|
this.#ws();
|
|
@@ -468,7 +483,7 @@ class RelaxedJson {
|
|
|
468
483
|
return num;
|
|
469
484
|
}
|
|
470
485
|
|
|
471
|
-
#keyword(): unknown {
|
|
486
|
+
#keyword(allowBareword: boolean): unknown {
|
|
472
487
|
const s = this.#s;
|
|
473
488
|
const i = this.#i;
|
|
474
489
|
for (const [word, value] of KEYWORDS) {
|
|
@@ -485,8 +500,47 @@ class RelaxedJson {
|
|
|
485
500
|
this.#i = this.#n;
|
|
486
501
|
return INCOMPLETE;
|
|
487
502
|
}
|
|
503
|
+
if (allowBareword) return this.#bareword();
|
|
488
504
|
throw new SyntaxError(`Unexpected token at position ${this.#i}`);
|
|
489
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
|
+
}
|
|
490
544
|
}
|
|
491
545
|
|
|
492
546
|
/**
|