openzoo 0.48.92 → 0.48.94
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 +93 -47
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -221,10 +221,12 @@ subtasks), you may instead reply with EXACTLY one line, no prose, using one of:
|
|
|
221
221
|
independent agent and give it a task
|
|
222
222
|
SEND: <name> | <message> message an agent thread that already
|
|
223
223
|
exists (yours or one you spawned)
|
|
224
|
-
PING: <name>
|
|
225
|
-
|
|
224
|
+
PING: <name> wake that agent to take a turn now
|
|
225
|
+
(* / all / everyone = the whole project).
|
|
226
|
+
You get back "pinged, working" — that is
|
|
227
|
+
an ack, not the child's result
|
|
226
228
|
PEEK: <name> a fuller look — its last few messages,
|
|
227
|
-
not just the latest one
|
|
229
|
+
not just the latest one. Read-only.
|
|
228
230
|
You are given the result before your next line, so none of these block you — check back
|
|
229
231
|
later if it's still working.
|
|
230
232
|
|
|
@@ -500,7 +502,7 @@ spawn/delegate/create agents AND no other bot has already done it this round, re
|
|
|
500
502
|
EXACTLY one line, no prose, using one of:
|
|
501
503
|
SPAWN: <short name> | <task for the new agent> create a new thread with its own agent
|
|
502
504
|
SEND: <name> | <message> message an existing agent thread
|
|
503
|
-
PING: <name>
|
|
505
|
+
PING: <name> wake that agent (* / all = the project)
|
|
504
506
|
PEEK: <name> a fuller look at its last few messages
|
|
505
507
|
|
|
506
508
|
You ALSO have real (sandboxed) filesystem access, scoped to THIS group's own directory —
|
|
@@ -661,6 +663,32 @@ const NUDGE = 'That reply announced work instead of doing it — no directive li
|
|
|
661
663
|
const AUTO_CONTINUE = 'AUTO is still on — do not stop and do not ask the user to type continue. '
|
|
662
664
|
+ 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
|
|
663
665
|
+ 'or DONE: if the job is actually finished.';
|
|
666
|
+
// PING used to be a read: last-line status, no turn. Idle children stayed idle
|
|
667
|
+
// while the parent treated the dump as evidence they had acted. A ping is a
|
|
668
|
+
// wake — the same harness continue AUTO already uses, unless a custom message
|
|
669
|
+
// was given. Thinking threads are left alone; pendingRun stays on the human.
|
|
670
|
+
let runTurnOverride = null;
|
|
671
|
+
function setRunTurnForTest(fn) {
|
|
672
|
+
runTurnOverride = typeof fn === 'function' ? fn : null;
|
|
673
|
+
}
|
|
674
|
+
function kickTurn(threadId, userText, onEvent, images) {
|
|
675
|
+
return (runTurnOverride || runTurn)(threadId, userText, onEvent, images);
|
|
676
|
+
}
|
|
677
|
+
function pingWakeText(extra) {
|
|
678
|
+
const msg = String(extra || '').trim();
|
|
679
|
+
// A restated spawn brief is not a nudge. MEASURED live: existing-SPAWN
|
|
680
|
+
// wrapped children in childKickoff({fresh:false}) → "CONTEXT REFRESH —
|
|
681
|
+
// you already exist" and they thought, then refused to redo the job.
|
|
682
|
+
if (!msg || /CONTEXT REFRESH|--- your specific job ---|ROOT ASK —/.test(msg)) return AUTO_CONTINUE;
|
|
683
|
+
return msg;
|
|
684
|
+
}
|
|
685
|
+
function pingCanWake(x) {
|
|
686
|
+
return Boolean(x) && !x.pendingRun && x.status !== 'thinking';
|
|
687
|
+
}
|
|
688
|
+
function wakeOnPing(x, extra) {
|
|
689
|
+
// Never childKickoff. Ping is a short continue, not a first-day re-brief.
|
|
690
|
+
kickTurn(x.id, pingWakeText(extra)).catch(() => {});
|
|
691
|
+
}
|
|
664
692
|
// Ceiling on subagents per thread. Spawning is fire-and-forget and each child
|
|
665
693
|
// can spawn too, so without a count it is unbounded — MEASURED as 15+ threads
|
|
666
694
|
// all named tetris-contract, every one of them a live agent making paid calls.
|
|
@@ -927,7 +955,7 @@ const SLASH_COMMANDS = [
|
|
|
927
955
|
{ name: '/memory', args: '[text|clear]', help: 'facts injected into every turn' },
|
|
928
956
|
{ name: '/sessions', args: '', help: 'list all threads' },
|
|
929
957
|
{ name: '/all', args: '<message>', help: 'send a message to every bot in this project' },
|
|
930
|
-
{ name: '/ping', args: '', help: '
|
|
958
|
+
{ name: '/ping', args: '', help: 'wake idle bots below you to take a turn now' },
|
|
931
959
|
{ name: '/cron', args: '<mins> | <message>', help: 'repeat a message on a timer' },
|
|
932
960
|
{ name: '/crons', args: '', help: 'list timers (/cron del <id> removes one)' },
|
|
933
961
|
{ name: '/dir', args: '<path>', help: 'set this thread’s working directory' },
|
|
@@ -1037,7 +1065,8 @@ async function handleSlash(task, t) {
|
|
|
1037
1065
|
+ ' SPAWN: <name> | <task> a NEW subagent (names are unique —\n'
|
|
1038
1066
|
+ ' spawning an existing one sends to it)\n'
|
|
1039
1067
|
+ ' SEND: <name> | <msg> more work for an EXISTING subagent\n'
|
|
1040
|
-
+ ' PING
|
|
1068
|
+
+ ' PING: <name> wake that bot (* wakes the project)\n'
|
|
1069
|
+
+ ' PEEK: <name> read-only look at another bot\n\n'
|
|
1041
1070
|
+ 'READ, LS, GLOB, GREP, FETCH, PEEK and MCP run CONCURRENTLY when several\n'
|
|
1042
1071
|
+ 'appear in one reply — four files cost one round trip, not four.';
|
|
1043
1072
|
}
|
|
@@ -1210,19 +1239,26 @@ async function handleSlash(task, t) {
|
|
|
1210
1239
|
return `Sent down your branch to ${crew.length} bot(s): ${crew.map((x) => x.name).join(', ')}`;
|
|
1211
1240
|
}
|
|
1212
1241
|
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1242
|
+
// Wake the room. Used to be a free last-line dump — idle children stayed
|
|
1243
|
+
// idle, and a parent reading "kid: <old reply>" thought they had acted.
|
|
1244
|
+
// Empty extra is a nudge (AUTO_CONTINUE), not a cancel. Thinking stays
|
|
1245
|
+
// thinking; pendingRun stays on the human. Same branch scope as /all.
|
|
1215
1246
|
if (cmd === 'ping') {
|
|
1216
|
-
// Same scoping as /all: your branch, not the whole project.
|
|
1217
1247
|
const crew = subtreeOf(t.id, true);
|
|
1218
1248
|
if (crew.length < 2) return 'You have no subagents yet.';
|
|
1219
1249
|
return crew.map((x) => {
|
|
1220
1250
|
const mark = x.id === t.id ? ' (here)' : '';
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1251
|
+
if (x.id === t.id) {
|
|
1252
|
+
const last = x.history[x.history.length - 1];
|
|
1253
|
+
return x.pendingRun ? ` ${x.name}${mark}: BLOCKED — waiting for your approval`
|
|
1254
|
+
: x.status === 'thinking' ? ` ${x.name}${mark}: working`
|
|
1255
|
+
: last ? ` ${x.name}${mark}: ${String(last.text).replace(/\s+/g, ' ').slice(0, 90)}`
|
|
1256
|
+
: ` ${x.name}${mark}: nothing yet`;
|
|
1257
|
+
}
|
|
1258
|
+
if (x.pendingRun) return ` ${x.name}${mark}: BLOCKED — waiting for your approval`;
|
|
1259
|
+
if (x.status === 'thinking') return ` ${x.name}${mark}: working`;
|
|
1260
|
+
wakeOnPing(x, arg);
|
|
1261
|
+
return ` ${x.name}${mark}: pinged, working`;
|
|
1226
1262
|
}).join('\n');
|
|
1227
1263
|
}
|
|
1228
1264
|
|
|
@@ -1767,14 +1803,15 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
1767
1803
|
const notes = [];
|
|
1768
1804
|
for (const { name, task } of parsed) {
|
|
1769
1805
|
const existing = findByName(name);
|
|
1770
|
-
if (existing) { notes.push(`${name} already exists —
|
|
1806
|
+
if (existing) { notes.push(`${name} already exists — woke it to keep working.`); made.push({ t: existing, task, fresh: false }); continue; }
|
|
1771
1807
|
const siblings = [...threads.values()].filter((x) => x.parent === originId).length;
|
|
1772
1808
|
if (siblings >= SPAWN_MAX_CHILDREN) { notes.push(`Not spawning "${name}": already at ${SPAWN_MAX_CHILDREN} subagents.`); continue; }
|
|
1773
1809
|
made.push({ t: newThread(name, originId), task, fresh: true });
|
|
1774
1810
|
}
|
|
1775
1811
|
// Every thread now exists, so spawnPosition sees the COMPLETE cohort.
|
|
1776
1812
|
for (const { t: sub, task, fresh } of made) {
|
|
1777
|
-
runTurn(sub.id, childKickoff(parent, sub.name, task, { fresh })).catch(() => {});
|
|
1813
|
+
if (fresh) runTurn(sub.id, childKickoff(parent, sub.name, task, { fresh })).catch(() => {});
|
|
1814
|
+
else wakeOnPing(sub);
|
|
1778
1815
|
}
|
|
1779
1816
|
const fresh = made.filter((m) => m.fresh).map((m) => m.t.name);
|
|
1780
1817
|
return [fresh.length ? `Spawned ${fresh.length} together (they can each see the full crew): ${fresh.join(', ')}` : '', ...notes]
|
|
@@ -1793,8 +1830,11 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
1793
1830
|
// is what SEND already does.
|
|
1794
1831
|
const existing = findByName(name);
|
|
1795
1832
|
if (existing) {
|
|
1796
|
-
|
|
1797
|
-
|
|
1833
|
+
// Repeat SPAWN is a wake, not a CONTEXT REFRESH. childKickoff({fresh:false})
|
|
1834
|
+
// restates the original job and tells the child it already exists — MEASURED,
|
|
1835
|
+
// the crew flipped to that preview, thought once, and sat.
|
|
1836
|
+
wakeOnPing(existing);
|
|
1837
|
+
return `${name} already exists — woke it to keep working.`;
|
|
1798
1838
|
}
|
|
1799
1839
|
// Storm guard. Fire-and-forget spawning is unbounded by construction: each
|
|
1800
1840
|
// child can spawn, and nothing above it is counting.
|
|
@@ -1861,27 +1901,29 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
1861
1901
|
const ping = pingAll.length === 1 ? [null, pingAll[0]] : null;
|
|
1862
1902
|
if (ping) {
|
|
1863
1903
|
const name = ping[1].trim();
|
|
1864
|
-
// PING: * (or 'all' / 'project')
|
|
1904
|
+
// PING: * (or 'all' / 'project') WAKES every other bot in this project.
|
|
1865
1905
|
// Coordinating a spawn tree by naming siblings one at a time is a chore
|
|
1866
1906
|
// the parent should not have to do, and it cannot know who else exists.
|
|
1907
|
+
// The return is an ack ("pinged, working"), not a last-line dump that
|
|
1908
|
+
// lets the parent think the child already acted.
|
|
1867
1909
|
if (/^(\*|all|project|everyone)$/i.test(name)) {
|
|
1868
1910
|
const me = threads.get(originId);
|
|
1869
1911
|
const root = me ? rootOf(me).rootId : null;
|
|
1870
1912
|
const crew = [...threads.values()].filter((x) => x.id !== originId && rootOf(x).rootId === root);
|
|
1871
1913
|
if (!crew.length) return 'No other bots in this project yet.';
|
|
1872
1914
|
return crew.map((x) => {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
: x.name + ': no reply yet';
|
|
1915
|
+
if (x.pendingRun) return x.name + ': BLOCKED — waiting for approval';
|
|
1916
|
+
if (x.status === 'thinking') return x.name + ': still working';
|
|
1917
|
+
wakeOnPing(x);
|
|
1918
|
+
return x.name + ': pinged, working';
|
|
1878
1919
|
}).join('\n');
|
|
1879
1920
|
}
|
|
1880
1921
|
const target = findByName(name);
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1922
|
+
if (!target) return `No thread named "${name}".`;
|
|
1923
|
+
if (target.pendingRun) return `${name}: BLOCKED — waiting for approval`;
|
|
1924
|
+
if (target.status === 'thinking') return `${name} is still working.`;
|
|
1925
|
+
wakeOnPing(target);
|
|
1926
|
+
return `${name}: pinged, working`;
|
|
1885
1927
|
}
|
|
1886
1928
|
const peek = /^[ \t>*-]*PEEK:\s*(.+)/m.exec(reply);
|
|
1887
1929
|
if (peek) {
|
|
@@ -3121,12 +3163,9 @@ const APP_HTML = `<!doctype html>
|
|
|
3121
3163
|
</div>
|
|
3122
3164
|
<div id="walletOverlay" data-component="wallet-modal">
|
|
3123
3165
|
<div id="walletBox">
|
|
3124
|
-
<h3>
|
|
3125
|
-
<div class="wsub">
|
|
3126
|
-
<div
|
|
3127
|
-
<div class="wsub">This is <b>your</b> local burner on this machine (or this box). Keys stay in ~/.openzoo/wallet.json. It is not openzoo’s wallet, not a shared zoo account, not the model’s. You fund these deposit addresses; the app pays x402 per call from this wallet. Public addresses only — the UI never shows the key.</div>
|
|
3128
|
-
<div id="walletBody">loading…</div>
|
|
3129
|
-
<div class="wlane" id="subLane" data-component="subscribe-lane">
|
|
3166
|
+
<h3>Pay with a card</h3>
|
|
3167
|
+
<div class="wsub">Pay with a card. Basic, Pro, and Ultra are first. Wallet/x402 is the other option below — it stays; it is not the lead.</div>
|
|
3168
|
+
<div id="subLane" data-component="subscribe-lane">
|
|
3130
3169
|
<div class="wlanetitle">Subscribe with a card</div>
|
|
3131
3170
|
<div class="wtag">Subscription key · no x402</div>
|
|
3132
3171
|
<div class="wsub">Same plans as the public page. Checkout opens in the system browser — never an in-app Stripe window. After Stripe, this app polls the site’s key endpoint with the checkout session, or you paste the key from the success page.</div>
|
|
@@ -3142,6 +3181,11 @@ const APP_HTML = `<!doctype html>
|
|
|
3142
3181
|
<div class="wnote" id="subNote"></div>
|
|
3143
3182
|
<div class="wquiet"><a id="subPageLink" href="${SUBSCRIPTIONS_PAGE}" target="_blank" rel="noopener">Full subscriptions page</a></div>
|
|
3144
3183
|
</div>
|
|
3184
|
+
<div class="wlane" id="x402Lane" data-component="x402-lane">
|
|
3185
|
+
<div class="wlanetitle">Wallet / x402</div>
|
|
3186
|
+
<div class="wsub">This is <b>your</b> local burner on this machine (or this box). Keys stay in ~/.openzoo/wallet.json. It is not openzoo’s wallet, not a shared zoo account, not the model’s. You fund these deposit addresses; the app pays x402 per call from this wallet. Public addresses only — the UI never shows the key.</div>
|
|
3187
|
+
<div id="walletBody">loading…</div>
|
|
3188
|
+
</div>
|
|
3145
3189
|
</div>
|
|
3146
3190
|
</div>
|
|
3147
3191
|
<div id="main">
|
|
@@ -3176,7 +3220,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3176
3220
|
</optgroup>
|
|
3177
3221
|
</select>
|
|
3178
3222
|
<button class="dial" id="walletBtn" data-component="wallet-open"
|
|
3179
|
-
title="
|
|
3223
|
+
title="Pay with a card, or use wallet/x402">pay</button>
|
|
3180
3224
|
<button class="icon-btn" id="reloadBtn" title="Restart grokui on this box">↻</button>
|
|
3181
3225
|
<button class="icon-btn" id="hudBtn">◎</button>
|
|
3182
3226
|
</div>
|
|
@@ -3486,7 +3530,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3486
3530
|
// subagents without retyping — and pressing it addressed the WHOLE
|
|
3487
3531
|
// project, cousins included. Now every thread with descendants gets
|
|
3488
3532
|
// one, scoped to its own branch.
|
|
3489
|
-
(t.kids ? '<button class="pingall trow-ping" data-testid="ping-all" title="
|
|
3533
|
+
(t.kids ? '<button class="pingall trow-ping" data-testid="ping-all" title="Wake all '
|
|
3490
3534
|
+ t.kids + ' bot(s) below ' + escapeHtml(t.name) + '">\u21f2 ' + t.kids + '</button>' : '') +
|
|
3491
3535
|
'<button class="tclose" title="Remove">✕</button>';
|
|
3492
3536
|
row.addEventListener('click', () => {
|
|
@@ -3501,18 +3545,17 @@ const APP_HTML = `<!doctype html>
|
|
|
3501
3545
|
if (pingBtn) {
|
|
3502
3546
|
pingBtn.addEventListener('click', async (e) => {
|
|
3503
3547
|
e.stopPropagation();
|
|
3504
|
-
|
|
3505
|
-
//
|
|
3506
|
-
//
|
|
3507
|
-
|
|
3548
|
+
// Default click = wake with the harness continue. window.prompt is
|
|
3549
|
+
// missing or blocked in Electron, so a modal here silently no-op'd
|
|
3550
|
+
// the only UI path that tried to reach the crew. /all still sends
|
|
3551
|
+
// exact text; ping is "poke them to work".
|
|
3508
3552
|
pingBtn.disabled = true;
|
|
3509
3553
|
const was = pingBtn.textContent;
|
|
3510
3554
|
pingBtn.textContent = '…';
|
|
3511
3555
|
try {
|
|
3512
|
-
// Routed through THIS thread, so /all scopes to its own subtree.
|
|
3513
3556
|
await fetch(API + '/drive', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
3514
|
-
body: JSON.stringify({ threadId: t.id, task: '/
|
|
3515
|
-
pingBtn.textContent = '
|
|
3557
|
+
body: JSON.stringify({ threadId: t.id, task: '/ping' }) });
|
|
3558
|
+
pingBtn.textContent = 'pinged';
|
|
3516
3559
|
} catch (err) { pingBtn.textContent = 'failed'; }
|
|
3517
3560
|
setTimeout(() => { pingBtn.disabled = false; pingBtn.textContent = was; }, 1500);
|
|
3518
3561
|
await loadThreads();
|
|
@@ -3623,7 +3666,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3623
3666
|
if (!w || (!w.solana && !w.evm && w.creditUsd == null)) {
|
|
3624
3667
|
const p = document.createElement('div');
|
|
3625
3668
|
p.className = 'wnote wempty';
|
|
3626
|
-
p.textContent = 'Could not reach the local openzoo proxy on :8402. It may still be starting — try again in a few seconds. You can still subscribe with a card
|
|
3669
|
+
p.textContent = 'Could not reach the local openzoo proxy on :8402. It may still be starting — try again in a few seconds. You can still subscribe with a card above.';
|
|
3627
3670
|
walletBody.appendChild(p);
|
|
3628
3671
|
renderSubLane(w && w.subscription ? w.subscription : null);
|
|
3629
3672
|
return;
|
|
@@ -3665,7 +3708,7 @@ const APP_HTML = `<!doctype html>
|
|
|
3665
3708
|
note.textContent = subOn
|
|
3666
3709
|
? ('Wallet is optional while a subscription is active. ' + (w.funding || ''))
|
|
3667
3710
|
: (w.funded === false
|
|
3668
|
-
? 'This wallet is EMPTY — wallet/x402 calls will fail with HTTP 402 until you fund the addresses above, or subscribe with a card
|
|
3711
|
+
? 'This wallet is EMPTY — wallet/x402 calls will fail with HTTP 402 until you fund the addresses above, or subscribe with a card at the top. ' + (w.funding || '')
|
|
3669
3712
|
: (w.funding || ''));
|
|
3670
3713
|
if (w.funded === false && !subOn) note.classList.add('wempty');
|
|
3671
3714
|
if (note.textContent.trim()) walletBody.appendChild(note);
|
|
@@ -3832,8 +3875,9 @@ const APP_HTML = `<!doctype html>
|
|
|
3832
3875
|
if (e.key === 'Enter') { e.preventDefault(); savePastedSub(); }
|
|
3833
3876
|
});
|
|
3834
3877
|
// First launch: if the burner is empty AND there is no subscription, open
|
|
3835
|
-
// the
|
|
3836
|
-
// so a funded session, a saved key, or a dismiss does
|
|
3878
|
+
// the pay modal once so they see card plans first — and burner addresses
|
|
3879
|
+
// below. localStorage so a funded session, a saved key, or a dismiss does
|
|
3880
|
+
// not keep popping it.
|
|
3837
3881
|
(async function maybeOpenWalletOnce() {
|
|
3838
3882
|
if (localStorage.getItem('openzoo.wallet.seen')) return;
|
|
3839
3883
|
for (let i = 0; i < 8; i++) {
|
|
@@ -5237,4 +5281,6 @@ server.listen(PORT, BIND, () => console.log(`[grokui] http://${BIND === '0.0.0.0
|
|
|
5237
5281
|
export {
|
|
5238
5282
|
tryDirective, ensureWorkspacePort, isPreviewableRel, previewAck, workspaceFileUrl,
|
|
5239
5283
|
parseRun, looksLikeMcpAsBash, stripThinkTags, safeResolveIn, inDir, listDir,
|
|
5284
|
+
handleSlash, newThread, setRunTurnForTest, AUTO_CONTINUE, pingWakeText, pingCanWake,
|
|
5285
|
+
childKickoff,
|
|
5240
5286
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.94",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 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",
|