tune-basic-toolset 0.1.10 → 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/README.md CHANGED
@@ -13,8 +13,10 @@ Basic toolset for [Tune](https://github.com/iovdin/tune).
13
13
  - [sh](#sh) execute shell command
14
14
  - [cmd](#cmd) execute Windows cmd command
15
15
  - [powershell](#powershell) execute PowerShell command
16
+ - [grep](#grep) search for patterns in text or files
16
17
  - [osa](#osa) manage reminders/notes/calendar (AppleScript/macOS)
17
18
  - [jina_r](#jina_r) fetch webpage content
19
+ - [websearch](#websearch) search the web with web-enabled llms
18
20
  - [list](#list) keep list of tasks todo (loops for LLM)
19
21
  - [sqlite](#sqlite) execute sqlite queries
20
22
  - [py](#py) run python code
@@ -175,6 +177,22 @@ TotalPhysicalMemory : 17179869184
175
177
  CsProcessors : {Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz}
176
178
  ```
177
179
 
180
+ ### `grep`
181
+ Search for patterns in text or files using regular expressions
182
+ ```chat
183
+ user: @grep
184
+ find all lines containing "TODO" in myfile.js
185
+ tool_call: grep {"filename":"myfile.js","regex":"TODO"}
186
+ tool_result:
187
+ // TODO: refactor this function
188
+ // TODO: add error handling
189
+
190
+ system:
191
+ TODOS:
192
+ @{ myfile.js | proc grep regex=TODO }
193
+
194
+ ```
195
+
178
196
  ### `osa`
179
197
  AppleScript tool, manage reminders, notes, calendar etc on osx
180
198
  ```chat
@@ -219,7 +237,25 @@ Tune is a versatile toolkit designed for developers and users to effectively int
219
237
  <cut for brevity>
220
238
  ```
221
239
 
240
+ ### `websearch`
241
+ Search the web with web enabled llms
242
+ Supports search with `perplexity/sonar`, `perplexity/sonar-pro`, `gpt-4o-search-preview`, `gpt-4o-mini-search-preview` models via the `model` parameter (defaults to `perplexity/sonar`).
243
+
244
+ ```chat
245
+ user: @websearch
246
+ latest ai news
247
+
248
+ assistant:
249
+
250
+ tool_call: websearch {"model":"perplexity/sonar"}
251
+ latest AI news
252
+ tool_result:
253
+ The latest AI news in October 2025 highlights significant investments, new projects, policy developments, and advances across various sectors:
254
+
255
+ - Major companies including Microsoft, Google, Nvidia, OpenAI, Salesforce, and CoreWeave have pledged over £31 billion in capital expenditure focused on AI data centers and infrastructure upgrades[1].
256
+ ```
222
257
 
258
+ The websearch tool provides up-to-date information by querying the web through AI-powered search models. You can specify different Perplexity models like `perplexity/sonar-pro` for more advanced searches.
223
259
 
224
260
  ### `list`
225
261
  Keep list of tasks to do
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tune-basic-toolset",
3
- "version": "0.1.10",
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-sdk": "latest",
29
- "tune-fs": "latest"
28
+ "tune-fs": "latest",
29
+ "tune-sdk": "latest"
30
30
  },
31
31
  "devDependencies": {
32
- "tune-sdk": "latest",
33
- "tune-fs": "latest"
32
+ "tune-fs": "latest",
33
+ "tune-sdk": "latest"
34
34
  },
35
35
  "dependencies": {
36
36
  "escodegen": "^2.1.0",
@@ -0,0 +1,25 @@
1
+ {
2
+ "description": "filter lines with regex",
3
+ "parameters": {
4
+ "type": "object",
5
+ "properties": {
6
+ "text": {
7
+ "type": "string",
8
+ "description": "Text to be filtered"
9
+ },
10
+ "filename": {
11
+ "type": "string",
12
+ "description": "if text is not set, filename will be read and filtered"
13
+ },
14
+ "regex": {
15
+ "type": "string",
16
+ "description": "js regex to filter lines in file or text. passed as first arguments to new RegExp "
17
+ },
18
+ "regex_flags": {
19
+ "type": "string",
20
+ "description": "flags to be passed to new RegExp constructor"
21
+ }
22
+ },
23
+ "required": ["regex"]
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ module.exports = async function grep({filename, text, regex, regex_flags}, ctx) {
2
+ if (!text && filename) {
3
+ const n = await ctx.resolve(filename)
4
+ if (!n)
5
+ return `${filename} not found`
6
+
7
+ text = await n.read()
8
+ }
9
+
10
+ if (!text) {
11
+ return "content is empty"
12
+ }
13
+
14
+ const r = new RegExp(regex, regex_flags)
15
+ return text.split(/\r?\n/).filter(line => r.test(line)).join("\n").replaceAll("@", "\\@")
16
+ }
@@ -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
+ }
@@ -16,6 +16,6 @@
16
16
  "description": "User message to send"
17
17
  }
18
18
  },
19
- "required": ["filename", "text"]
19
+ "required": ["text"]
20
20
  }
21
21
  }
package/src/patch.tool.js CHANGED
@@ -5,7 +5,9 @@ const fs = require('fs').promises;
5
5
 
6
6
  module.exports = async function patch({ text, filename }, ctx) {
7
7
  // Regex to match each patch block
8
- const patchRegex = /<<<<<<< ORIGINAL[^\n]*\n([\s\S]*?)=======\n([\s\S]*?)>>>>>>> UPDATED[^\n]*(?:\n|$)/g;
8
+ // Be lenient about the number of conflict marker characters because some
9
+ // environments may trim one or more > or < characters.
10
+ const patchRegex = /<{6,}\s*ORIGINAL[^\n]*\n([\s\S]*?)=+\n([\s\S]*?)>{6,}\s*UPDATED[^\n]*(?:\n|$)/g;
9
11
  const patches = [];
10
12
  let match;
11
13
 
package/src/proc.proc.js CHANGED
@@ -14,8 +14,6 @@ module.exports = async function proc(node, args, ctx) {
14
14
 
15
15
  const tool = await ctx.resolve(toolName, { "type": "tool" })
16
16
 
17
- console.log(params)
18
-
19
17
  if (!tool || tool.type !== "tool") {
20
18
  throw Error(`tool '${toolName}' not found`)
21
19
  }
@@ -46,7 +44,7 @@ function parseCommandLine(input) {
46
44
  // 1) Parse leading alphanumeric command
47
45
  skipWS();
48
46
  const cmdStart = i;
49
- while (i < len && /[A-Za-z0-9]/.test(s[i])) i++;
47
+ while (i < len && /[A-Za-z0-9_\-]/.test(s[i])) i++;
50
48
  const command = s.slice(cmdStart, i);
51
49
  if (!command) return [null, {}];
52
50
 
@@ -0,0 +1,24 @@
1
+ {
2
+ "description": "Does a websearch using llm",
3
+ "parameters": {
4
+ "type": "object",
5
+ "properties": {
6
+ "text": {
7
+ "type": "string",
8
+ "description": "web search query"
9
+ },
10
+ "model": {
11
+ "type": "string",
12
+ "enum": [
13
+ "perplexity/sonar",
14
+ "perplexity/sonar-pro",
15
+ "gpt-5-search-api",
16
+ "gpt-4o-search-preview",
17
+ "gpt-4o-mini-search-preview"
18
+ ],
19
+ "description": "model to do websearch, default is perplexity/sonar"
20
+ }
21
+ },
22
+ "required": ["text"]
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ user:
2
+ @{ model | init perplexity/sonar | resolve }
3
+ @text