tune-basic-toolset 0.1.11 → 0.1.12
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/package.json +5 -5
- package/src/json_format.proc.js +716 -2
- package/src/message.schema.json +1 -1
- package/src/websearch.schema.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tune-basic-toolset",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Basic toolset for tune",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"tune-
|
|
29
|
-
"tune-
|
|
28
|
+
"tune-fs": "latest",
|
|
29
|
+
"tune-sdk": "latest"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"tune-
|
|
33
|
-
"tune-
|
|
32
|
+
"tune-fs": "latest",
|
|
33
|
+
"tune-sdk": "latest"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"escodegen": "^2.1.0",
|
package/src/json_format.proc.js
CHANGED
|
@@ -17,6 +17,720 @@ module.exports = async function json_format(node, args, ctx) {
|
|
|
17
17
|
}
|
|
18
18
|
return {
|
|
19
19
|
...node,
|
|
20
|
-
exec: async (payload, ctx) => node.exec({ ...payload, response_format }, ctx)
|
|
20
|
+
exec: async (payload, ctx) => node.exec({ ...payload, response_format }, ctx),
|
|
21
|
+
hookMsg: (msg) => {
|
|
22
|
+
if (msg.content) {
|
|
23
|
+
msg.content = JSON.stringify(text2json(msg.content), null, " ")
|
|
24
|
+
}
|
|
25
|
+
return msg
|
|
26
|
+
}
|
|
21
27
|
};
|
|
22
|
-
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// text2json.js (CommonJS)
|
|
31
|
+
// Lightweight JSON extraction from LLM output.
|
|
32
|
+
//
|
|
33
|
+
// Exports: function text2json(text)
|
|
34
|
+
//
|
|
35
|
+
// Strategy:
|
|
36
|
+
// 1) Extract candidates from:
|
|
37
|
+
// - Markdown code blocks (```json and others)
|
|
38
|
+
// - Balanced { ... } or [ ... ] segments in text (even if incomplete)
|
|
39
|
+
// - Whole text as fallback
|
|
40
|
+
// 2) For each candidate, sanitize JSON-ish into valid JSON:
|
|
41
|
+
// - Strip comments (//, /* */) respecting strings
|
|
42
|
+
// - Convert single-quoted strings to double-quoted
|
|
43
|
+
// - Quote unquoted object keys
|
|
44
|
+
// - Quote unquoted values with spaces or path-like tokens
|
|
45
|
+
// - Remove trailing commas
|
|
46
|
+
// - Auto-close unbalanced braces/brackets
|
|
47
|
+
// 3) Try JSON.parse. Collect all successful parses.
|
|
48
|
+
// - If multiple parses succeed, return array of results
|
|
49
|
+
// - If one succeeds, return it
|
|
50
|
+
// - Else return null
|
|
51
|
+
|
|
52
|
+
function text2json(text) {
|
|
53
|
+
if (typeof text !== 'string') return null;
|
|
54
|
+
|
|
55
|
+
// Quick path
|
|
56
|
+
const direct = tryParseJSON(text);
|
|
57
|
+
if (direct.ok) return direct.value;
|
|
58
|
+
|
|
59
|
+
const candidates = [
|
|
60
|
+
...extractMarkdownBlocks(text),
|
|
61
|
+
...findBalancedJsonSegments(text),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Add entire text as last resort
|
|
65
|
+
candidates.push({ snippet: text, reason: 'entire_text_fallback', complete: false });
|
|
66
|
+
|
|
67
|
+
const results = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
|
|
70
|
+
for (const c of prioritizeCandidates(candidates)) {
|
|
71
|
+
const attempts = generateSanitizedAttempts(c.snippet);
|
|
72
|
+
|
|
73
|
+
for (const attempt of attempts) {
|
|
74
|
+
const parsed = tryParseJSON(attempt);
|
|
75
|
+
if (parsed.ok) {
|
|
76
|
+
const key = stableStringify(parsed.value);
|
|
77
|
+
if (!seen.has(key)) {
|
|
78
|
+
seen.add(key);
|
|
79
|
+
results.push(parsed.value);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Try with auto-closing unbalanced braces/brackets and trailing comma cleanup
|
|
83
|
+
const closed = autoCloseAndClean(attempt);
|
|
84
|
+
const parsed2 = tryParseJSON(closed);
|
|
85
|
+
if (parsed2.ok) {
|
|
86
|
+
const key = stableStringify(parsed2.value);
|
|
87
|
+
if (!seen.has(key)) {
|
|
88
|
+
seen.add(key);
|
|
89
|
+
results.push(parsed2.value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (results.length > 0) break; // Prefer first success per candidate
|
|
94
|
+
}
|
|
95
|
+
if (results.length > 0) break; // Prefer first successful candidate
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (results.length === 0) return null;
|
|
99
|
+
if (results.length === 1) return results[0];
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --------------------------- Candidate extraction ---------------------------
|
|
104
|
+
|
|
105
|
+
function extractMarkdownBlocks(text) {
|
|
106
|
+
const results = [];
|
|
107
|
+
|
|
108
|
+
// ```lang\n...``` blocks (handles unclosed too)
|
|
109
|
+
const blockRe = /```([a-zA-Z0-9 _-]+)?\n([\s\S]*?)```/g;
|
|
110
|
+
let match;
|
|
111
|
+
while ((match = blockRe.exec(text)) !== null) {
|
|
112
|
+
const lang = (match[1] || '').trim().toLowerCase();
|
|
113
|
+
const content = match[2] || '';
|
|
114
|
+
results.push({
|
|
115
|
+
snippet: content,
|
|
116
|
+
reason: `codeblock:${lang || 'unknown'}`,
|
|
117
|
+
complete: true,
|
|
118
|
+
lang,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle unclosed block at end: ```json\n...EOF
|
|
123
|
+
const openRe = /```([a-zA-Z0-9 _-]+)?\n([\s\S]*)$/;
|
|
124
|
+
const openMatch = text.match(openRe);
|
|
125
|
+
if (openMatch && !/```/.test(openMatch[2])) {
|
|
126
|
+
const lang = (openMatch[1] || '').trim().toLowerCase();
|
|
127
|
+
const content = openMatch[2] || '';
|
|
128
|
+
results.push({
|
|
129
|
+
snippet: content,
|
|
130
|
+
reason: `codeblock_unclosed:${lang || 'unknown'}`,
|
|
131
|
+
complete: false,
|
|
132
|
+
lang,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function findBalancedJsonSegments(text) {
|
|
140
|
+
const results = [];
|
|
141
|
+
|
|
142
|
+
let stack = [];
|
|
143
|
+
let start = -1;
|
|
144
|
+
let inDouble = false;
|
|
145
|
+
let inSingle = false;
|
|
146
|
+
let inLineComment = false;
|
|
147
|
+
let inBlockComment = false;
|
|
148
|
+
let escape = false;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < text.length; i++) {
|
|
151
|
+
const c = text[i];
|
|
152
|
+
const next = i + 1 < text.length ? text[i + 1] : '';
|
|
153
|
+
|
|
154
|
+
if (inLineComment) {
|
|
155
|
+
if (c === '\n') {
|
|
156
|
+
inLineComment = false;
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (inBlockComment) {
|
|
161
|
+
if (c === '*' && next === '/') {
|
|
162
|
+
inBlockComment = false;
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!inSingle && !inDouble) {
|
|
169
|
+
if (c === '/' && next === '/') {
|
|
170
|
+
inLineComment = true;
|
|
171
|
+
i++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (c === '/' && next === '*') {
|
|
175
|
+
inBlockComment = true;
|
|
176
|
+
i++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (inDouble) {
|
|
182
|
+
if (!escape && c === '"') inDouble = false;
|
|
183
|
+
escape = c === '\\' ? !escape : false;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (inSingle) {
|
|
187
|
+
if (!escape && c === "'") inSingle = false;
|
|
188
|
+
escape = c === '\\' ? !escape : false;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (c === '"') {
|
|
193
|
+
inDouble = true;
|
|
194
|
+
escape = false;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (c === "'") {
|
|
198
|
+
inSingle = true;
|
|
199
|
+
escape = false;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (c === '{' || c === '[') {
|
|
204
|
+
if (stack.length === 0) start = i;
|
|
205
|
+
stack.push(c);
|
|
206
|
+
} else if (c === '}' || c === ']') {
|
|
207
|
+
if (stack.length > 0) {
|
|
208
|
+
const last = stack[stack.length - 1];
|
|
209
|
+
const expectedOpen = c === '}' ? '{' : '[';
|
|
210
|
+
if (last === expectedOpen) stack.pop();
|
|
211
|
+
}
|
|
212
|
+
if (stack.length === 0 && start !== -1) {
|
|
213
|
+
results.push({
|
|
214
|
+
snippet: text.slice(start, i + 1),
|
|
215
|
+
reason: 'balanced_segment',
|
|
216
|
+
complete: true,
|
|
217
|
+
});
|
|
218
|
+
start = -1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (stack.length > 0 && start !== -1) {
|
|
224
|
+
results.push({
|
|
225
|
+
snippet: text.slice(start),
|
|
226
|
+
reason: 'balanced_segment_incomplete',
|
|
227
|
+
complete: false,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function prioritizeCandidates(candidates) {
|
|
235
|
+
// Prefer json-tagged code blocks, then any code blocks, then balanced segments, then fallback
|
|
236
|
+
return candidates.sort((a, b) => scoreCandidate(b) - scoreCandidate(a));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function scoreCandidate(c) {
|
|
240
|
+
let score = 0;
|
|
241
|
+
if (c.reason.startsWith('codeblock')) score += 5;
|
|
242
|
+
if (c.lang === 'json') score += 5;
|
|
243
|
+
if (c.reason.includes('balanced_segment')) score += 3;
|
|
244
|
+
if (c.complete) score += 2;
|
|
245
|
+
return score;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ----------------------------- Sanitization --------------------------------
|
|
249
|
+
|
|
250
|
+
function generateSanitizedAttempts(snippet) {
|
|
251
|
+
const trimmed = snippet.trim().replace(/^\uFEFF/, '');
|
|
252
|
+
const attempts = [];
|
|
253
|
+
|
|
254
|
+
// Attempt 1: minimal cleanup (comments + trailing commas)
|
|
255
|
+
{
|
|
256
|
+
let s = stripComments(trimmed);
|
|
257
|
+
s = removeTrailingCommas(s);
|
|
258
|
+
attempts.push(s);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Attempt 2: full jsonish fixing
|
|
262
|
+
{
|
|
263
|
+
let s = jsonishFix(trimmed);
|
|
264
|
+
s = removeTrailingCommas(s);
|
|
265
|
+
attempts.push(s);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Attempt 3: full jsonish + autoclose
|
|
269
|
+
{
|
|
270
|
+
let s = jsonishFix(trimmed);
|
|
271
|
+
s = autoCloseAndClean(s);
|
|
272
|
+
attempts.push(s);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return attempts;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stripComments(input) {
|
|
279
|
+
let out = '';
|
|
280
|
+
let inDouble = false;
|
|
281
|
+
let inSingle = false;
|
|
282
|
+
let inLineComment = false;
|
|
283
|
+
let inBlockComment = false;
|
|
284
|
+
let escape = false;
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < input.length; i++) {
|
|
287
|
+
const c = input[i];
|
|
288
|
+
const next = input[i + 1];
|
|
289
|
+
|
|
290
|
+
if (inLineComment) {
|
|
291
|
+
if (c === '\n') {
|
|
292
|
+
inLineComment = false;
|
|
293
|
+
out += c;
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (inBlockComment) {
|
|
298
|
+
if (c === '*' && next === '/') {
|
|
299
|
+
inBlockComment = false;
|
|
300
|
+
i++;
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!inSingle && !inDouble) {
|
|
306
|
+
if (c === '/' && next === '/') {
|
|
307
|
+
inLineComment = true;
|
|
308
|
+
i++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (c === '/' && next === '*') {
|
|
312
|
+
inBlockComment = true;
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
out += c;
|
|
319
|
+
|
|
320
|
+
if (inDouble) {
|
|
321
|
+
if (!escape && c === '"') inDouble = false;
|
|
322
|
+
escape = c === '\\' ? !escape : false;
|
|
323
|
+
} else if (inSingle) {
|
|
324
|
+
if (!escape && c === "'") inSingle = false;
|
|
325
|
+
escape = c === '\\' ? !escape : false;
|
|
326
|
+
} else {
|
|
327
|
+
if (c === '"') {
|
|
328
|
+
inDouble = true;
|
|
329
|
+
escape = false;
|
|
330
|
+
} else if (c === "'") {
|
|
331
|
+
inSingle = true;
|
|
332
|
+
escape = false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function removeTrailingCommas(s) {
|
|
340
|
+
// Remove trailing comma before } or ]
|
|
341
|
+
return s.replace(/,(\s*[}\]])/g, '$1');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function jsonishFix(input) {
|
|
345
|
+
// Full pass: convert single quotes, quote unquoted keys, quote unquoted values, strip comments.
|
|
346
|
+
const noComments = stripComments(input);
|
|
347
|
+
// Convert single-quoted strings to double-quoted strings
|
|
348
|
+
const singlesFixed = convertSingleQuotedStrings(noComments);
|
|
349
|
+
|
|
350
|
+
// One pass state machine to quote keys and values where needed
|
|
351
|
+
const normalized = quoteKeysAndValues(singlesFixed);
|
|
352
|
+
|
|
353
|
+
return normalized;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function convertSingleQuotedStrings(s) {
|
|
357
|
+
let out = '';
|
|
358
|
+
let inDouble = false;
|
|
359
|
+
let inSingle = false;
|
|
360
|
+
let escape = false;
|
|
361
|
+
|
|
362
|
+
for (let i = 0; i < s.length; i++) {
|
|
363
|
+
const c = s[i];
|
|
364
|
+
|
|
365
|
+
if (inDouble) {
|
|
366
|
+
out += c;
|
|
367
|
+
if (!escape && c === '"') inDouble = false;
|
|
368
|
+
escape = c === '\\' ? !escape : false;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (inSingle) {
|
|
372
|
+
if (!escape && c === "'") {
|
|
373
|
+
inSingle = false;
|
|
374
|
+
out += '"';
|
|
375
|
+
} else if (!escape && c === '"') {
|
|
376
|
+
out += '\\"';
|
|
377
|
+
} else if (c === '\\') {
|
|
378
|
+
// Keep escapes inside single quotes; next char is escaped
|
|
379
|
+
out += '\\';
|
|
380
|
+
} else {
|
|
381
|
+
out += c;
|
|
382
|
+
}
|
|
383
|
+
escape = c === '\\' ? !escape : false;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (c === '"') {
|
|
388
|
+
inDouble = true;
|
|
389
|
+
out += c;
|
|
390
|
+
escape = false;
|
|
391
|
+
} else if (c === "'") {
|
|
392
|
+
inSingle = true;
|
|
393
|
+
out += '"';
|
|
394
|
+
escape = false;
|
|
395
|
+
} else {
|
|
396
|
+
out += c;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// If dangling single-quoted string (unlikely), close it
|
|
401
|
+
if (inSingle) out += '"';
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function quoteKeysAndValues(s) {
|
|
406
|
+
// State machine through objects/arrays to:
|
|
407
|
+
// - Quote unquoted keys in objects
|
|
408
|
+
// - Quote unquoted values with spaces or path-like tokens
|
|
409
|
+
let out = '';
|
|
410
|
+
const ctxStack = []; // 'object' | 'array'
|
|
411
|
+
let inString = false;
|
|
412
|
+
let escape = false;
|
|
413
|
+
let expectingKey = false; // valid only when top ctx is object
|
|
414
|
+
let expectingValue = false;
|
|
415
|
+
let i = 0;
|
|
416
|
+
|
|
417
|
+
function top() {
|
|
418
|
+
return ctxStack.length ? ctxStack[ctxStack.length - 1] : null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function skipWhitespace(idx) {
|
|
422
|
+
while (idx < s.length && /\s/.test(s[idx])) idx++;
|
|
423
|
+
return idx;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function readUntilColon(idx) {
|
|
427
|
+
// Read raw key until first colon at this nesting level (ignores quotes)
|
|
428
|
+
let buf = '';
|
|
429
|
+
let inD = false, esc = false;
|
|
430
|
+
for (; idx < s.length; idx++) {
|
|
431
|
+
const ch = s[idx];
|
|
432
|
+
if (inD) {
|
|
433
|
+
buf += ch;
|
|
434
|
+
if (!esc && ch === '"') inD = false;
|
|
435
|
+
esc = ch === '\\' ? !esc : false;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (ch === '"') {
|
|
439
|
+
inD = true;
|
|
440
|
+
buf += ch;
|
|
441
|
+
esc = false;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (ch === ':') {
|
|
445
|
+
return { keyRaw: buf, nextIdx: idx + 1 };
|
|
446
|
+
}
|
|
447
|
+
// guard: if we hit { or [ or } or ] or comma/newline before colon, abort
|
|
448
|
+
if (ch === '{' || ch === '[' || ch === '}' || ch === ']' || ch === ','
|
|
449
|
+
|| ch === '\n') {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
buf += ch;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function emitQuotedString(str) {
|
|
458
|
+
return JSON.stringify(str);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function quoteKeyIfNeeded(idx) {
|
|
462
|
+
// Assumes s[idx] at start of key position
|
|
463
|
+
let j = skipWhitespace(idx);
|
|
464
|
+
const ch = s[j];
|
|
465
|
+
|
|
466
|
+
if (ch === '"') {
|
|
467
|
+
// Already quoted
|
|
468
|
+
// Copy through quoted string
|
|
469
|
+
let buf = '';
|
|
470
|
+
let inD = true, esc = false;
|
|
471
|
+
for (; j < s.length; j++) {
|
|
472
|
+
const c = s[j];
|
|
473
|
+
buf += c;
|
|
474
|
+
if (inD) {
|
|
475
|
+
if (!esc && c === '"') { inD = false; j++; break; }
|
|
476
|
+
esc = c === '\\' ? !esc : false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
out += buf;
|
|
480
|
+
// Expect colon next (copy it and move on)
|
|
481
|
+
let k = skipWhitespace(j);
|
|
482
|
+
if (s[k] === ':') {
|
|
483
|
+
out += s.slice(j, k + 1);
|
|
484
|
+
return k + 1;
|
|
485
|
+
} else {
|
|
486
|
+
// If colon missing, just return next
|
|
487
|
+
return j;
|
|
488
|
+
}
|
|
489
|
+
} else if (ch === '}' || ch === undefined) {
|
|
490
|
+
// Empty object or invalid
|
|
491
|
+
out += s[idx];
|
|
492
|
+
return idx + 1;
|
|
493
|
+
} else {
|
|
494
|
+
// Unquoted key: read until colon
|
|
495
|
+
const res = readUntilColon(j);
|
|
496
|
+
if (!res) {
|
|
497
|
+
// Fallback: pass-through char and move on
|
|
498
|
+
out += s[idx];
|
|
499
|
+
return idx + 1;
|
|
500
|
+
}
|
|
501
|
+
const keyRaw = res.keyRaw;
|
|
502
|
+
// Trim whitespace
|
|
503
|
+
const key = keyRaw.trim();
|
|
504
|
+
// If key already looks like "something", retain inner; else quote raw key text
|
|
505
|
+
let quoted = '';
|
|
506
|
+
if (key.startsWith('"') && key.endsWith('"')) {
|
|
507
|
+
quoted = key;
|
|
508
|
+
} else {
|
|
509
|
+
// Strip any trailing commas/spaces in raw accumulation
|
|
510
|
+
const cleaned = key.replace(/\s+$/g, '');
|
|
511
|
+
quoted = emitQuotedString(unquoteIfQuoted(cleaned));
|
|
512
|
+
}
|
|
513
|
+
out += quoted + ':';
|
|
514
|
+
return res.nextIdx;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function unquoteIfQuoted(k) {
|
|
519
|
+
const t = k.trim();
|
|
520
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
521
|
+
return t.slice(1, -1);
|
|
522
|
+
}
|
|
523
|
+
return t;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function quoteValueIfNeeded(idx) {
|
|
527
|
+
let j = skipWhitespace(idx);
|
|
528
|
+
const ch = s[j];
|
|
529
|
+
|
|
530
|
+
if (ch === '"') {
|
|
531
|
+
// Already a string
|
|
532
|
+
// copy until end of string
|
|
533
|
+
let inD = true, esc = false;
|
|
534
|
+
for (; j < s.length; j++) {
|
|
535
|
+
const c = s[j];
|
|
536
|
+
out += c;
|
|
537
|
+
if (inD) {
|
|
538
|
+
if (!esc && c === '"') { inD = false; j++; break; }
|
|
539
|
+
esc = c === '\\' ? !esc : false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return j;
|
|
543
|
+
}
|
|
544
|
+
if (ch === '{' || ch === '[') {
|
|
545
|
+
// Nested structure, let main loop handle
|
|
546
|
+
out += ch;
|
|
547
|
+
return j + 1;
|
|
548
|
+
}
|
|
549
|
+
if (ch === 't' && s.slice(j, j + 4) === 'true') { out += 'true'; return j + 4; }
|
|
550
|
+
if (ch === 'f' && s.slice(j, j + 5) === 'false') { out += 'false'; return j + 5; }
|
|
551
|
+
if (ch === 'n' && s.slice(j, j + 4) === 'null') { out += 'null'; return j + 4; }
|
|
552
|
+
|
|
553
|
+
// Number?
|
|
554
|
+
const numMatch = s.slice(j).match(/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/);
|
|
555
|
+
if (numMatch) {
|
|
556
|
+
out += numMatch[0];
|
|
557
|
+
return j + numMatch[0].length;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Bareword/path-like or with spaces: read until comma or } or ]
|
|
561
|
+
// Respect basic string quoting inside by stopping at quotes (we'll leave them for main loop)
|
|
562
|
+
let k = j;
|
|
563
|
+
let buf = '';
|
|
564
|
+
while (k < s.length) {
|
|
565
|
+
const c = s[k];
|
|
566
|
+
if (c === ',' || c === '}' || c === ']' || c === '\n') break;
|
|
567
|
+
if (c === '"' || c === "'") break;
|
|
568
|
+
buf += c;
|
|
569
|
+
k++;
|
|
570
|
+
}
|
|
571
|
+
const val = buf.trim();
|
|
572
|
+
if (val.length > 0) {
|
|
573
|
+
out += emitQuotedString(val);
|
|
574
|
+
return k;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Fallback: output the character and advance
|
|
578
|
+
out += s[j] || '';
|
|
579
|
+
return j + 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
while (i < s.length) {
|
|
583
|
+
const c = s[i];
|
|
584
|
+
|
|
585
|
+
if (inString) {
|
|
586
|
+
out += c;
|
|
587
|
+
if (!escape && c === '"') {
|
|
588
|
+
inString = false;
|
|
589
|
+
}
|
|
590
|
+
escape = c === '\\' ? !escape : false;
|
|
591
|
+
i++;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (c === '"') {
|
|
596
|
+
inString = true;
|
|
597
|
+
escape = false;
|
|
598
|
+
out += c;
|
|
599
|
+
i++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (c === '{') {
|
|
604
|
+
ctxStack.push('object');
|
|
605
|
+
expectingKey = true;
|
|
606
|
+
out += c;
|
|
607
|
+
i++;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (c === '[') {
|
|
611
|
+
ctxStack.push('array');
|
|
612
|
+
expectingValue = true;
|
|
613
|
+
out += c;
|
|
614
|
+
i++;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (c === '}') {
|
|
618
|
+
ctxStack.pop();
|
|
619
|
+
expectingKey = (top() === 'object'); // next in outer object we expect key after comma
|
|
620
|
+
out += c;
|
|
621
|
+
i++;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (c === ']') {
|
|
625
|
+
ctxStack.pop();
|
|
626
|
+
expectingValue = (top() === 'array');
|
|
627
|
+
out += c;
|
|
628
|
+
i++;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (c === ':') {
|
|
632
|
+
expectingKey = false;
|
|
633
|
+
expectingValue = true;
|
|
634
|
+
out += c;
|
|
635
|
+
i++;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (c === ',') {
|
|
639
|
+
if (top() === 'object') {
|
|
640
|
+
expectingKey = true;
|
|
641
|
+
expectingValue = false;
|
|
642
|
+
} else if (top() === 'array') {
|
|
643
|
+
expectingValue = true;
|
|
644
|
+
}
|
|
645
|
+
out += c;
|
|
646
|
+
i++;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (top() === 'object' && expectingKey) {
|
|
651
|
+
i = quoteKeyIfNeeded(i);
|
|
652
|
+
// After quoteKeyIfNeeded, we are positioned after colon or advanced minimally
|
|
653
|
+
// expectingValue should be true if colon was handled
|
|
654
|
+
// Heuristic: if last char written was ':', set expectingValue
|
|
655
|
+
if (out.length > 0 && out[out.length - 1] === ':') expectingValue = true;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if ((top() === 'object' && expectingValue) || (top() === 'array' && expectingValue)) {
|
|
660
|
+
i = quoteValueIfNeeded(i);
|
|
661
|
+
// After value, we wait for comma or close
|
|
662
|
+
expectingValue = false;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Default: copy char
|
|
667
|
+
out += c;
|
|
668
|
+
i++;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function autoCloseAndClean(s) {
|
|
675
|
+
// Remove trailing commas before attempting to close
|
|
676
|
+
s = removeTrailingCommas(s);
|
|
677
|
+
|
|
678
|
+
// Auto-close brackets/braces
|
|
679
|
+
const closers = [];
|
|
680
|
+
let inString = false;
|
|
681
|
+
let escape = false;
|
|
682
|
+
|
|
683
|
+
for (let i = 0; i < s.length; i++) {
|
|
684
|
+
const c = s[i];
|
|
685
|
+
if (inString) {
|
|
686
|
+
if (!escape && c === '"') inString = false;
|
|
687
|
+
escape = c === '\\' ? !escape : false;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (c === '"') {
|
|
691
|
+
inString = true;
|
|
692
|
+
escape = false;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (c === '{') closers.push('}');
|
|
696
|
+
else if (c === '[') closers.push(']');
|
|
697
|
+
else if (c === '}' || c === ']') {
|
|
698
|
+
const last = closers[closers.length - 1];
|
|
699
|
+
if ((c === '}' && last === '}') || (c === ']' && last === ']')) {
|
|
700
|
+
closers.pop();
|
|
701
|
+
} else {
|
|
702
|
+
// Mismatch; ignore
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Remove trailing comma before closing we will append
|
|
708
|
+
s = s.replace(/,\s*$/, '');
|
|
709
|
+
|
|
710
|
+
return s + closers.reverse().join('');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ------------------------------ Utilities ----------------------------------
|
|
714
|
+
|
|
715
|
+
function tryParseJSON(s) {
|
|
716
|
+
try {
|
|
717
|
+
return { ok: true, value: JSON.parse(s) };
|
|
718
|
+
} catch {
|
|
719
|
+
return { ok: false, value: null };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function stableStringify(v) {
|
|
724
|
+
// Basic stable stringify for dedupe
|
|
725
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
726
|
+
const keys = Object.keys(v).sort();
|
|
727
|
+
const obj = {};
|
|
728
|
+
for (const k of keys) obj[k] = v[k];
|
|
729
|
+
return JSON.stringify(obj, (_, val) =>
|
|
730
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
731
|
+
? Object.keys(val).sort().reduce((acc, kk) => (acc[kk] = val[kk], acc), {})
|
|
732
|
+
: val
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
return JSON.stringify(v);
|
|
736
|
+
}
|
package/src/message.schema.json
CHANGED