openzoo 0.43.0 → 0.43.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/lib/grokui.mjs +1124 -0
- package/lib/podagent.mjs +121 -31
- package/lib/proxy.js +27 -0
- package/package.json +1 -1
package/lib/podagent.mjs
CHANGED
|
@@ -26,15 +26,15 @@ import { randomUUID } from 'node:crypto';
|
|
|
26
26
|
const PORTS = (process.env.OZ_AGENT_PORTS || '1337,6080,1340,6081')
|
|
27
27
|
.split(',').map((s) => Number(s.trim())).filter(Boolean);
|
|
28
28
|
const LOG = process.env.OZ_AGENT_LOG || '/var/log/openzoo/agent.jsonl';
|
|
29
|
-
const PROXY = process.env.OZ_PROXY || 'http://127.0.0.1:8402/v1';
|
|
30
|
-
const MODEL = process.env.OZ_BRAIN_MODEL || '
|
|
29
|
+
export const PROXY = process.env.OZ_PROXY || 'http://127.0.0.1:8402/v1';
|
|
30
|
+
const MODEL = process.env.OZ_BRAIN_MODEL || 'deepseek/deepseek-v4-pro-0813';
|
|
31
31
|
const MAX_STEPS = Number(process.env.OZ_MAX_STEPS || 10);
|
|
32
32
|
|
|
33
33
|
// Matches the Grok Bot chat surface itself (dark canvas, right-aligned grey
|
|
34
34
|
// user pills, pink-avatar left-aligned replies, pill input bar with + / mic) —
|
|
35
35
|
// this renders INSIDE Grok Bot's own sandbox panel (its sidebar/titlebar are
|
|
36
36
|
// the app's own chrome, not ours), so only the message canvas needs to match.
|
|
37
|
-
const VNC_CHAT_HTML = `<!doctype html>
|
|
37
|
+
export const VNC_CHAT_HTML = `<!doctype html>
|
|
38
38
|
<html><head><meta charset="utf-8"><title>openzoo box</title>
|
|
39
39
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
40
40
|
<style>
|
|
@@ -43,23 +43,28 @@ const VNC_CHAT_HTML = `<!doctype html>
|
|
|
43
43
|
html, body { margin: 0; height: 100%; background: #000; }
|
|
44
44
|
body { color: #ececec; font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
45
45
|
display: flex; flex-direction: column; }
|
|
46
|
-
#log { flex: 1; overflow-y: auto; padding: 28px 24px 12px; display: flex; flex-direction: column; gap:
|
|
47
|
-
.
|
|
48
|
-
|
|
46
|
+
#log { flex: 1; overflow-y: auto; padding: 28px 24px 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
47
|
+
.hdr { align-self: flex-start; display: flex; align-items: center; gap: 6px; margin: 12px 0 4px;
|
|
48
|
+
color: #8e8e93; font-size: 13px; }
|
|
49
|
+
.hdr .avatar { width: 18px; height: 18px; border-radius: 5px; }
|
|
50
|
+
.hdr .avatar svg { width: 10px; height: 10px; }
|
|
51
|
+
.row { display: flex; max-width: 78%; margin: 2px 0; }
|
|
52
|
+
.row.user { align-self: flex-end; }
|
|
49
53
|
.row.bot { align-self: flex-start; }
|
|
50
54
|
.avatar { width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px; background: #e91e8c;
|
|
51
55
|
display: flex; align-items: center; justify-content: center; }
|
|
52
56
|
.avatar svg { width: 16px; height: 16px; }
|
|
53
|
-
.bubble { padding: 11px
|
|
54
|
-
.row.user .bubble { background: #
|
|
55
|
-
.row.bot .bubble { background:
|
|
57
|
+
.bubble { padding: 11px 16px; border-radius: 20px; white-space: pre-wrap; word-break: break-word; }
|
|
58
|
+
.row.user .bubble { background: #57575c; }
|
|
59
|
+
.row.bot .bubble { background: #262626; color: #ececec; }
|
|
56
60
|
.row.bot.pending .bubble { color: #8e8e93; }
|
|
57
61
|
.dots span { display: inline-block; width: 5px; height: 5px; margin-right: 3px; border-radius: 50%;
|
|
58
62
|
background: #8e8e93; animation: blink 1.2s infinite ease-in-out; }
|
|
59
63
|
.dots span:nth-child(2) { animation-delay: .2s; } .dots span:nth-child(3) { animation-delay: .4s; }
|
|
60
64
|
@keyframes blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
|
|
61
65
|
#bar { padding: 10px 16px 18px; }
|
|
62
|
-
#
|
|
66
|
+
#row-input { display: flex; align-items: center; gap: 8px; }
|
|
67
|
+
#pill { flex: 1; display: flex; align-items: center; gap: 6px; background: #2c2c2e; border-radius: 26px;
|
|
63
68
|
padding: 8px 10px 8px 14px; }
|
|
64
69
|
.icon-btn { width: 32px; height: 32px; border-radius: 50%; border: none; background: transparent;
|
|
65
70
|
color: #ececec; display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
@@ -70,17 +75,26 @@ const VNC_CHAT_HTML = `<!doctype html>
|
|
|
70
75
|
padding: 6px 0; min-width: 0; }
|
|
71
76
|
#inp::placeholder { color: #8e8e93; }
|
|
72
77
|
#inp:focus { outline: none; }
|
|
78
|
+
#send { width: 34px; height: 34px; border-radius: 50%; border: none; background: #fff; color: #000;
|
|
79
|
+
display: none; align-items: center; justify-content: center; cursor: pointer; flex: 0 0 34px; }
|
|
80
|
+
#send.show { display: flex; }
|
|
81
|
+
#send svg { width: 16px; height: 16px; }
|
|
73
82
|
</style></head>
|
|
74
83
|
<body>
|
|
75
84
|
<div id="log"></div>
|
|
76
85
|
<div id="bar">
|
|
77
|
-
<div id="
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
86
|
+
<div id="row-input">
|
|
87
|
+
<div id="pill">
|
|
88
|
+
<button class="icon-btn" tabindex="-1">
|
|
89
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
90
|
+
</button>
|
|
91
|
+
<input id="inp" placeholder="Message openzoo" autofocus>
|
|
92
|
+
<button class="icon-btn" tabindex="-1">
|
|
93
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v1a7 7 0 0 1-14 0v-1"/><line x1="12" y1="18" x2="12" y2="22"/></svg>
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
<button id="send">
|
|
97
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M6 11l6-6 6 6"/></svg>
|
|
84
98
|
</button>
|
|
85
99
|
</div>
|
|
86
100
|
</div>
|
|
@@ -89,15 +103,17 @@ const VNC_CHAT_HTML = `<!doctype html>
|
|
|
89
103
|
const inp = document.getElementById('inp');
|
|
90
104
|
const send = document.getElementById('send');
|
|
91
105
|
const AVATAR = '<svg viewBox="0 0 24 24" fill="none"><path d="M6 10c0-1 1-2 2-1l2 2 2-2c1-1 2 0 2 1v2c0 2-2 3-4 3s-4-1-4-3v-2z" fill="#fff"/></svg>';
|
|
106
|
+
let lastWho = null;
|
|
92
107
|
function addRow(who, text) {
|
|
108
|
+
if (who === 'bot' && lastWho !== 'bot') {
|
|
109
|
+
const hdr = document.createElement('div');
|
|
110
|
+
hdr.className = 'hdr';
|
|
111
|
+
hdr.innerHTML = '<span class="avatar">' + AVATAR + '</span><span>openzoo</span>';
|
|
112
|
+
log.appendChild(hdr);
|
|
113
|
+
}
|
|
114
|
+
lastWho = who;
|
|
93
115
|
const row = document.createElement('div');
|
|
94
116
|
row.className = 'row ' + who + (text === null ? ' pending' : '');
|
|
95
|
-
if (who === 'bot') {
|
|
96
|
-
const av = document.createElement('div');
|
|
97
|
-
av.className = 'avatar';
|
|
98
|
-
av.innerHTML = AVATAR;
|
|
99
|
-
row.appendChild(av);
|
|
100
|
-
}
|
|
101
117
|
const bubble = document.createElement('div');
|
|
102
118
|
bubble.className = 'bubble';
|
|
103
119
|
bubble.innerHTML = text === null ? '<span class="dots"><span></span><span></span><span></span></span>' : '';
|
|
@@ -111,7 +127,8 @@ const VNC_CHAT_HTML = `<!doctype html>
|
|
|
111
127
|
const task = inp.value.trim();
|
|
112
128
|
if (!task) return;
|
|
113
129
|
inp.value = '';
|
|
114
|
-
|
|
130
|
+
send.classList.remove('show');
|
|
131
|
+
inp.disabled = true;
|
|
115
132
|
addRow('user', task);
|
|
116
133
|
const pending = addRow('bot', null);
|
|
117
134
|
try {
|
|
@@ -126,8 +143,9 @@ const VNC_CHAT_HTML = `<!doctype html>
|
|
|
126
143
|
pending.row.classList.remove('pending');
|
|
127
144
|
pending.bubble.textContent = 'error: ' + e.message;
|
|
128
145
|
}
|
|
129
|
-
inp.disabled = false;
|
|
146
|
+
inp.disabled = false; inp.focus();
|
|
130
147
|
}
|
|
148
|
+
inp.addEventListener('input', () => { send.classList.toggle('show', inp.value.trim().length > 0); });
|
|
131
149
|
send.addEventListener('click', submit);
|
|
132
150
|
inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
|
|
133
151
|
</script>
|
|
@@ -163,16 +181,68 @@ function execFrame(command, cwd = '/tmp') {
|
|
|
163
181
|
|
|
164
182
|
/** One openzoo chat turn. Paid per call by the box's own wallet via the local
|
|
165
183
|
* proxy — no key, no account. */
|
|
166
|
-
async function brain(messages) {
|
|
184
|
+
export async function brain(messages, contextId) {
|
|
167
185
|
const r = await fetch(`${PROXY}/chat/completions`, {
|
|
168
186
|
method: 'POST',
|
|
169
|
-
headers: {
|
|
170
|
-
|
|
187
|
+
headers: {
|
|
188
|
+
'content-type': 'application/json', authorization: 'Bearer sk-openzoo',
|
|
189
|
+
// real leCore memory for this thread, bound via POST /v1/hrr/bind — NOT
|
|
190
|
+
// a fabricated mechanism. Retrieval runs automatically once this header
|
|
191
|
+
// is set; nothing more for the model to invent or explain.
|
|
192
|
+
...(contextId ? { 'x-hrr-context': contextId } : {}),
|
|
193
|
+
},
|
|
194
|
+
// explicit, not relying on the gateway's "inject when caller said nothing"
|
|
195
|
+
// default — an explicit plugins array is always respected as-is, so this
|
|
196
|
+
// guarantees every bot on every model actually has web search, instead of
|
|
197
|
+
// hoping nothing upstream (local proxy, gateway config) already set one.
|
|
198
|
+
// 900 was cutting real (especially web-search-backed) answers off mid-sentence
|
|
199
|
+
body: JSON.stringify({ model: MODEL, max_tokens: 4096, messages, plugins: [{ id: 'web' }] }),
|
|
171
200
|
});
|
|
172
201
|
const j = await r.json().catch(() => ({}));
|
|
173
202
|
return j?.choices?.[0]?.message?.content ?? '';
|
|
174
203
|
}
|
|
175
204
|
|
|
205
|
+
/** Same call, but streamed — invokes onDelta(text) as tokens arrive (for a
|
|
206
|
+
* live-typing UI) and resolves with the full accumulated text at the end, so
|
|
207
|
+
* callers that need to parse a directive out of the complete reply still can. */
|
|
208
|
+
export async function brainStream(messages, onDelta, contextId) {
|
|
209
|
+
const r = await fetch(`${PROXY}/chat/completions`, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: {
|
|
212
|
+
'content-type': 'application/json', authorization: 'Bearer sk-openzoo',
|
|
213
|
+
...(contextId ? { 'x-hrr-context': contextId } : {}),
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify({ model: MODEL, max_tokens: 4096, messages, plugins: [{ id: 'web' }], stream: true }),
|
|
216
|
+
});
|
|
217
|
+
if (!r.ok || !r.body) {
|
|
218
|
+
// fall back to the non-streaming path rather than fail outright
|
|
219
|
+
const text = await r.json().then((j) => j?.choices?.[0]?.message?.content ?? '').catch(() => '');
|
|
220
|
+
if (text) onDelta(text);
|
|
221
|
+
return text;
|
|
222
|
+
}
|
|
223
|
+
const reader = r.body.getReader();
|
|
224
|
+
const decoder = new TextDecoder();
|
|
225
|
+
let buf = '', full = '';
|
|
226
|
+
for (;;) {
|
|
227
|
+
const { value, done } = await reader.read();
|
|
228
|
+
if (done) break;
|
|
229
|
+
buf += decoder.decode(value, { stream: true });
|
|
230
|
+
const lines = buf.split('\n');
|
|
231
|
+
buf = lines.pop(); // last line may be incomplete — keep it for next chunk
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
const s = line.trim();
|
|
234
|
+
if (!s.startsWith('data:')) continue;
|
|
235
|
+
const payload = s.slice(5).trim();
|
|
236
|
+
if (payload === '[DONE]') continue;
|
|
237
|
+
try {
|
|
238
|
+
const delta = JSON.parse(payload)?.choices?.[0]?.delta?.content;
|
|
239
|
+
if (delta) { full += delta; onDelta(delta); }
|
|
240
|
+
} catch { /* keep-alive line or partial JSON — ignore */ }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return full;
|
|
244
|
+
}
|
|
245
|
+
|
|
176
246
|
const SYSTEM = `You are the brain of a Grok-Bot-style coding/ops agent. The polished chat UI
|
|
177
247
|
the user sees is Grok Bot (Anysphere's app); its "sandbox" has been pointed at THIS box, and
|
|
178
248
|
your reasoning is served by openzoo (pay-per-call access to ~435 models over x402 — no API key,
|
|
@@ -186,9 +256,29 @@ be careful, never destructive, prefer read-before-write, and explain nothing to
|
|
|
186
256
|
PROTOCOL — reply with EXACTLY one line, no prose, no code fences:
|
|
187
257
|
RUN: <a single shell command> to execute a step
|
|
188
258
|
DONE: <a short natural-language answer> when the task is complete (this text is shown in the UI)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
259
|
+
SPAWN: <name> | <task> to delegate an independent subtask to a fresh subagent
|
|
260
|
+
SEND: <name> | <message> to message a subagent you (or the user) already spawned
|
|
261
|
+
PING: <name> to check whether a subagent is still working or has finished
|
|
262
|
+
You are given each command's output before your next line. SPAWN does not block you — the subagent
|
|
263
|
+
runs independently and you continue your own reasoning immediately; check on it later with PING or
|
|
264
|
+
wait for it to SEND you its result. All subagents share the ONE real exec channel (the user's Mac),
|
|
265
|
+
so their shell commands queue safely behind each other automatically — you never need to worry about
|
|
266
|
+
that part. If the task needs no shell (a question, an explanation), answer it directly with a single
|
|
267
|
+
DONE: line. Keep DONE summaries human and useful — they are the assistant's reply to the user, not a
|
|
268
|
+
log.`;
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------- subagents --
|
|
271
|
+
|
|
272
|
+
// name -> { mailbox: string[], done: boolean, result: string|null }. In-process
|
|
273
|
+
// registry — every agent (root task + SPAWNed subagents) shares the ONE real
|
|
274
|
+
// exec channel (queueDrive), but reason (call the brain) independently and
|
|
275
|
+
// concurrently, so SPAWN genuinely doesn't block the spawning agent.
|
|
276
|
+
const agents = new Map();
|
|
277
|
+
|
|
278
|
+
function ensureAgent(name) {
|
|
279
|
+
if (!agents.has(name)) agents.set(name, { mailbox: [], done: false, result: null });
|
|
280
|
+
return agents.get(name);
|
|
281
|
+
}
|
|
192
282
|
|
|
193
283
|
/** The agent loop for one task, executed through the connected daemon. Each
|
|
194
284
|
* RUN is pushed as an exec frame; the daemon's result frames (captured in
|
package/lib/proxy.js
CHANGED
|
@@ -315,6 +315,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
315
315
|
console.log(line);
|
|
316
316
|
};
|
|
317
317
|
let sessionSpent = 0;
|
|
318
|
+
let sessionCogs = 0;
|
|
319
|
+
let sessionDirect = 0;
|
|
320
|
+
const MARKUP = 3; // confirmed constant, see .claude/wiki.md "Margin needs a like-for-like denominator"
|
|
318
321
|
let tunnelSpent = 0;
|
|
319
322
|
// Live balance refresh state — the real implementation is assigned in the
|
|
320
323
|
// banner section below; the handler only ever calls scheduleRefresh().
|
|
@@ -342,6 +345,16 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
342
345
|
// both surfaces: point base_url at /v1 for transparent context spilling,
|
|
343
346
|
// or add /mcp for tools (zoo_bind, zoo_ask...). Running two commands to
|
|
344
347
|
// get both was friction nobody should pay.
|
|
348
|
+
// This wallet's own running total for THIS proxy process — every paid
|
|
349
|
+
// call through this port counts (GUI, MCP, CLI, any harness), not just
|
|
350
|
+
// whichever surface happens to be asking. Local-only, no auth needed:
|
|
351
|
+
// it's a number, not a capability.
|
|
352
|
+
if (req.method === 'GET' && (req.url || '').split('?')[0] === '/v1/session') {
|
|
353
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
354
|
+
res.end(JSON.stringify({ spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls }));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
345
358
|
if ((req.url || '').split('?')[0] === '/mcp') {
|
|
346
359
|
try {
|
|
347
360
|
const { handleMcpRequest } = await import('./mcphttp.js');
|
|
@@ -643,6 +656,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
643
656
|
if (paid && receipt) {
|
|
644
657
|
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
|
645
658
|
sessionSpent += receipt.billedUsd;
|
|
659
|
+
// cogs: no per-call field for it, but MARKUP is a known constant
|
|
660
|
+
// (3x — confirmed against the gateway's own margin math), and
|
|
661
|
+
// billedUsd = cogs * markup on a straight-markup call. Close enough
|
|
662
|
+
// on a counterfactual (leCore-discounted) call too since markup is
|
|
663
|
+
// still the ceiling those get capped against.
|
|
664
|
+
sessionCogs += receipt.billedUsd / MARKUP;
|
|
665
|
+
// direct: savesVsDirect = direct / billedUsd is on the receipt when
|
|
666
|
+
// leCore compression engaged (server derives it from real token
|
|
667
|
+
// counts) — exact, not estimated. When absent, nothing was
|
|
668
|
+
// compressed, so direct === what was paid (same reasoning as the
|
|
669
|
+
// like-for-like fix: no compression, no saving, not zero).
|
|
670
|
+
sessionDirect += typeof receipt.savesVsDirect === 'number'
|
|
671
|
+
? receipt.savesVsDirect * receipt.billedUsd
|
|
672
|
+
: receipt.billedUsd;
|
|
646
673
|
// The public-URL ceiling meters only public-origin spend — your own
|
|
647
674
|
// local calls never eat into it.
|
|
648
675
|
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.2",
|
|
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",
|