openzoo 0.48.5 → 0.48.6
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/lib/grokui.mjs +174 -1
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { exec } from 'node:child_process';
|
|
9
9
|
import http from 'node:http';
|
|
10
10
|
import { randomUUID } from 'node:crypto';
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { brain, brainStream, PROXY } from './podagent.mjs';
|
|
@@ -122,6 +122,24 @@ workspace folder, not their real project. Same one-line-no-prose reply format:
|
|
|
122
122
|
own client. Use this directive; it
|
|
123
123
|
does the initialize handshake, holds
|
|
124
124
|
the session, and calls the tool.
|
|
125
|
+
LS: <path, or blank for the root> list a directory
|
|
126
|
+
GLOB: <pattern> find files — *.js, **/*.test.ts, src/**
|
|
127
|
+
GREP: <regex> | <optional path or glob> search file CONTENTS, with line numbers
|
|
128
|
+
EDIT: <path> | <exact old text> ||| <new text> change PART of a file. Prefer this over
|
|
129
|
+
WRITE for edits — WRITE replaces the
|
|
130
|
+
whole file, so anything you don't
|
|
131
|
+
reproduce from memory is destroyed.
|
|
132
|
+
The old text must match byte for byte
|
|
133
|
+
and appear exactly once; READ first.
|
|
134
|
+
TODO: <one item per line> set a visible checklist
|
|
135
|
+
TODO: done <n> tick an item (TODO: alone = show it)
|
|
136
|
+
|
|
137
|
+
WORK IN PARALLEL. READ, LS, GLOB, GREP, FETCH, PEEK and MCP are read-only, and if you emit
|
|
138
|
+
several of them in ONE reply the harness runs them CONCURRENTLY and returns every result
|
|
139
|
+
together. Four files in one turn costs one round trip; four turns costs four, and you pay per
|
|
140
|
+
call. Ask for everything you know you need at once instead of discovering it one file at a
|
|
141
|
+
time. Mutating directives (RUN, WRITE, EDIT, SPAWN, SEND) stay sequential on purpose — racing
|
|
142
|
+
them against each other corrupts the tree.
|
|
125
143
|
RUN: <shell command> run a REAL shell command in this
|
|
126
144
|
thread's directory — by default this
|
|
127
145
|
pauses and waits for the user to
|
|
@@ -473,7 +491,59 @@ if (!loadThreads()) newThread('openzoo', null);
|
|
|
473
491
|
// Parses a SPAWN/SEND/PING directive out of a reply, performs its side effect
|
|
474
492
|
// (creating or messaging another thread), and returns the ack text to show in
|
|
475
493
|
// place of the raw directive line — or null if the reply wasn't a directive.
|
|
494
|
+
// Directives that only READ. These are safe to run at the same time, so a
|
|
495
|
+
// reply carrying several of them costs one round trip instead of N — a model
|
|
496
|
+
// that wants four files currently spends four full turns (and four payments)
|
|
497
|
+
// fetching them one at a time.
|
|
498
|
+
//
|
|
499
|
+
// Deliberately excludes RUN / WRITE / EDIT / SPAWN / SEND: those mutate, and
|
|
500
|
+
// concurrent mutation of the same tree is a race the model cannot reason
|
|
501
|
+
// about. Reads fan out, writes stay sequential.
|
|
502
|
+
const PARALLEL_DIRECTIVE = /^[ \t>*-]*(READ|LS|GLOB|GREP|FETCH|PEEK|MCP):[ \t]*(.+)$/gm;
|
|
503
|
+
|
|
504
|
+
// Walk a thread dir once, cheaply, skipping the things nobody means to search.
|
|
505
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv']);
|
|
506
|
+
function walkDir(base, rel = '', out = [], depth = 0) {
|
|
507
|
+
if (depth > 12 || out.length > 5000) return out;
|
|
508
|
+
let entries = [];
|
|
509
|
+
try { entries = readdirSync(path.join(base, rel), { withFileTypes: true }); } catch { return out; }
|
|
510
|
+
for (const e of entries) {
|
|
511
|
+
const r = rel ? path.join(rel, e.name) : e.name;
|
|
512
|
+
if (e.isDirectory()) {
|
|
513
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
514
|
+
walkDir(base, r, out, depth + 1);
|
|
515
|
+
} else out.push(r);
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Glob -> RegExp. `**` crosses separators, `*` and `?` do not.
|
|
521
|
+
function globToRe(glob) {
|
|
522
|
+
let re = '';
|
|
523
|
+
for (let i = 0; i < glob.length; i++) {
|
|
524
|
+
const c = glob[i];
|
|
525
|
+
if (c === '*') {
|
|
526
|
+
if (glob[i + 1] === '*') { re += '.*'; i++; if (glob[i + 1] === '/') i++; }
|
|
527
|
+
else re += '[^/]*';
|
|
528
|
+
} else if (c === '?') re += '[^/]';
|
|
529
|
+
else if ('.+^${}()|[]\\'.includes(c)) re += '\\' + c;
|
|
530
|
+
else re += c;
|
|
531
|
+
}
|
|
532
|
+
return new RegExp('^' + re + '$');
|
|
533
|
+
}
|
|
534
|
+
|
|
476
535
|
async function tryDirective(reply, originId) {
|
|
536
|
+
// FAN OUT FIRST. Each line is re-entered on its own, so every branch below
|
|
537
|
+
// stays single-directive and none of them had to learn about batching.
|
|
538
|
+
const batch = [...reply.matchAll(PARALLEL_DIRECTIVE)];
|
|
539
|
+
if (batch.length > 1) {
|
|
540
|
+
const results = await Promise.all(
|
|
541
|
+
batch.map((m) => tryDirective(m[0].replace(/^[ \t>*-]*/, ''), originId)
|
|
542
|
+
.catch((e) => `${m[1]}: ${e.message}`)),
|
|
543
|
+
);
|
|
544
|
+
return results.filter(Boolean).join('\n\n');
|
|
545
|
+
}
|
|
546
|
+
|
|
477
547
|
const spawn = /^SPAWN:\s*([^|]+)\|\s*([\s\S]+)/.exec(reply);
|
|
478
548
|
if (spawn) {
|
|
479
549
|
const name = spawn[1].trim();
|
|
@@ -527,6 +597,109 @@ async function tryDirective(reply, originId) {
|
|
|
527
597
|
return `${rel}:\n${data.slice(0, 4000)}${data.length > 4000 ? '\n…(truncated)' : ''}`;
|
|
528
598
|
} catch (e) { return `Couldn't read ${rel}: ${e.message}`; }
|
|
529
599
|
}
|
|
600
|
+
// EDIT beats WRITE for changing part of a file: WRITE overwrites the whole
|
|
601
|
+
// thing, so a model that wants a one-line change has to reproduce the entire
|
|
602
|
+
// file from memory and silently drops whatever it forgot.
|
|
603
|
+
const edit = /^EDIT:\s*([^|]+)\|([\s\S]*?)\|\|\|([\s\S]*)$/.exec(reply);
|
|
604
|
+
if (edit) {
|
|
605
|
+
const rel = edit[1].trim();
|
|
606
|
+
const oldStr = edit[2].replace(/^\n/, '').replace(/\n$/, '');
|
|
607
|
+
const newStr = edit[3].replace(/^\n/, '').replace(/\n$/, '');
|
|
608
|
+
try {
|
|
609
|
+
const full = safeResolveIn(dirFor(originId), rel);
|
|
610
|
+
const before = readFileSync(full, 'utf8');
|
|
611
|
+
const hits = before.split(oldStr).length - 1;
|
|
612
|
+
if (hits === 0) return `EDIT ${rel}: that exact text isn't in the file — READ it first, the copy must match byte for byte.`;
|
|
613
|
+
if (hits > 1) return `EDIT ${rel}: that text appears ${hits} times — include more surrounding context so it matches exactly once.`;
|
|
614
|
+
writeFileSync(full, before.replace(oldStr, newStr));
|
|
615
|
+
return `Edited ${rel} (${before.length} -> ${before.replace(oldStr, newStr).length} bytes).`;
|
|
616
|
+
} catch (e) { return `Couldn't edit ${rel}: ${e.message}`; }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const ls = /^LS:\s*(.*)$/.exec(reply);
|
|
620
|
+
if (ls) {
|
|
621
|
+
const rel = ls[1].trim() || '.';
|
|
622
|
+
try {
|
|
623
|
+
const full = safeResolveIn(dirFor(originId), rel);
|
|
624
|
+
const entries = readdirSync(full, { withFileTypes: true });
|
|
625
|
+
if (!entries.length) return `${rel}: (empty)`;
|
|
626
|
+
const lines = entries.slice(0, 300).map((e) => {
|
|
627
|
+
if (e.isDirectory()) return ` ${e.name}/`;
|
|
628
|
+
let size = '';
|
|
629
|
+
try { size = ` (${statSync(path.join(full, e.name)).size}b)`; } catch { /* raced */ }
|
|
630
|
+
return ` ${e.name}${size}`;
|
|
631
|
+
});
|
|
632
|
+
return `${rel}:\n${lines.join('\n')}${entries.length > 300 ? `\n …${entries.length - 300} more` : ''}`;
|
|
633
|
+
} catch (e) { return `Couldn't list ${rel}: ${e.message}`; }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const glob = /^GLOB:\s*(.+)$/.exec(reply);
|
|
637
|
+
if (glob) {
|
|
638
|
+
const pattern = glob[1].trim();
|
|
639
|
+
try {
|
|
640
|
+
const base = dirFor(originId);
|
|
641
|
+
const re = globToRe(pattern.startsWith('./') ? pattern.slice(2) : pattern);
|
|
642
|
+
const hits = walkDir(base).filter((f) => re.test(f) || re.test(path.basename(f)));
|
|
643
|
+
if (!hits.length) return `GLOB ${pattern}: no matches`;
|
|
644
|
+
return `GLOB ${pattern} — ${hits.length} match(es):\n${hits.slice(0, 200).map((h) => ' ' + h).join('\n')}`
|
|
645
|
+
+ (hits.length > 200 ? `\n …${hits.length - 200} more` : '');
|
|
646
|
+
} catch (e) { return `GLOB ${pattern}: ${e.message}`; }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const grep = /^GREP:\s*([^|]+?)(?:\s*\|\s*(.+))?$/.exec(reply);
|
|
650
|
+
if (grep) {
|
|
651
|
+
const pattern = grep[1].trim();
|
|
652
|
+
const scope = (grep[2] || '').trim();
|
|
653
|
+
try {
|
|
654
|
+
const base = dirFor(originId);
|
|
655
|
+
let re;
|
|
656
|
+
try { re = new RegExp(pattern, 'i'); }
|
|
657
|
+
catch { return `GREP: ${pattern} isn't a valid regex.`; }
|
|
658
|
+
let files = walkDir(base);
|
|
659
|
+
if (scope) { const sre = globToRe(scope); files = files.filter((f) => sre.test(f) || f.startsWith(scope)); }
|
|
660
|
+
const out = [];
|
|
661
|
+
for (const f of files) {
|
|
662
|
+
if (out.length > 200) break;
|
|
663
|
+
let text;
|
|
664
|
+
try { text = readFileSync(path.join(base, f), 'utf8'); } catch { continue; }
|
|
665
|
+
if (text.indexOf(String.fromCharCode(0)) !== -1) continue; // binary — NUL, not whitespace
|
|
666
|
+
text.split('\n').forEach((line, i) => {
|
|
667
|
+
if (out.length <= 200 && re.test(line)) out.push(` ${f}:${i + 1}: ${line.trim().slice(0, 200)}`);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
if (!out.length) return `GREP ${pattern}: no matches`;
|
|
671
|
+
return `GREP ${pattern} — ${out.length} hit(s):\n${out.join('\n')}`;
|
|
672
|
+
} catch (e) { return `GREP: ${e.message}`; }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// A real, persisted checklist. Bots were already narrating plans; this makes
|
|
676
|
+
// the plan a thing the user can see and the model can be held to.
|
|
677
|
+
const todo = /^TODO:\s*([\s\S]*)$/.exec(reply);
|
|
678
|
+
if (todo) {
|
|
679
|
+
const t = threads.get(originId);
|
|
680
|
+
if (!t) return 'TODO: no such thread.';
|
|
681
|
+
t.todos = t.todos || [];
|
|
682
|
+
const body = todo[1].trim();
|
|
683
|
+
if (!body || /^(list|show)$/i.test(body)) {
|
|
684
|
+
if (!t.todos.length) return 'TODO: (empty)';
|
|
685
|
+
return 'TODO:\n' + t.todos.map((x, i) => ` ${i + 1}. [${x.done ? 'x' : ' '}] ${x.text}`).join('\n');
|
|
686
|
+
}
|
|
687
|
+
const done = /^done\s+(\d+)/i.exec(body);
|
|
688
|
+
if (done) {
|
|
689
|
+
const idx = Number(done[1]) - 1;
|
|
690
|
+
if (!t.todos[idx]) return `TODO: no item ${done[1]}.`;
|
|
691
|
+
t.todos[idx].done = true;
|
|
692
|
+
saveThreads();
|
|
693
|
+
return 'TODO:\n' + t.todos.map((x, i) => ` ${i + 1}. [${x.done ? 'x' : ' '}] ${x.text}`).join('\n');
|
|
694
|
+
}
|
|
695
|
+
if (/^clear$/i.test(body)) { t.todos = []; saveThreads(); return 'TODO: cleared.'; }
|
|
696
|
+
// Otherwise: replace the list with the lines given.
|
|
697
|
+
t.todos = body.split('\n').map((l) => l.replace(/^[-*\d.)\]\s]+/, '').trim())
|
|
698
|
+
.filter(Boolean).map((text) => ({ text, done: false }));
|
|
699
|
+
saveThreads();
|
|
700
|
+
return 'TODO:\n' + t.todos.map((x, i) => ` ${i + 1}. [ ] ${x.text}`).join('\n');
|
|
701
|
+
}
|
|
702
|
+
|
|
530
703
|
const serve = /^SERVE:\s*(.*)$/.exec(reply);
|
|
531
704
|
if (serve) {
|
|
532
705
|
const rel = serve[1].trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.6",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|