openzoo 0.41.1 → 0.43.0

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.
@@ -175,11 +175,14 @@ function encodeChatResponse(text) {
175
175
  }
176
176
 
177
177
  /**
178
- * ROUTE AUTO THROUGH THE ZOO. Cursor's chat inference is ChatService/
179
- * StreamUnifiedChat to the agent host (now impersonated). Parse the prompt, pay
180
- * the zoo via the local proxy, stream the answer back as StreamUnifiedChatResponse
181
- * frames. Legitimate: Auto is free-allowed; we redirect the operator's OWN
182
- * inference to their OWN paid proxy.
178
+ * THE TRIGGER. Cursor's chat inference is ChatService/StreamUnifiedChat to the
179
+ * agent host (now impersonated). Whatever the user typed into Grok Bot's UI is
180
+ * in this request pull it out and hand it to OUR box (OZ_BOX_AGENT, set by
181
+ * grokbot.js to the box's :1337 podagent). The box's own openzoo brain drives
182
+ * the local-exec loop against the daemon running on this Mac (approval-gated),
183
+ * and its DONE: answer is what streams back into the UI. If no box is wired
184
+ * (OZ_BOX_AGENT unset — e.g. running the backend standalone), fall back to a
185
+ * plain one-shot zoo call so the app still gets a reply.
183
186
  */
184
187
  async function handleStreamChat(req, res, body, log) {
185
188
  const ct = String(req.headers['content-type'] || '');
@@ -187,20 +190,34 @@ async function handleStreamChat(req, res, body, log) {
187
190
  const prompt = extractPromptText(body) || 'hello';
188
191
  log(`cursor-backend: >> StreamUnifiedChat prompt: ${JSON.stringify(prompt.slice(0, 80))}`);
189
192
  let text = '';
193
+ const boxAgent = process.env.OZ_BOX_AGENT;
190
194
  try {
191
- const zoo = await fetch('http://127.0.0.1:8402/v1/chat/completions', {
192
- method: 'POST',
193
- headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
194
- body: JSON.stringify({
195
- model: process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
196
- messages: [{ role: 'user', content: prompt }],
197
- max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
198
- }),
199
- });
200
- const data = await zoo.json();
201
- text = data.choices?.[0]?.message?.content || '(no content)';
202
- log(`cursor-backend: << zoo replied ${text.length} chars (paid x402)`);
203
- } catch (e) { text = `openzoo error: ${e.message}`; log(`cursor-backend: zoo call failed: ${e.message}`); }
195
+ if (boxAgent) {
196
+ const drive = await fetch(`${boxAgent}/drive`, {
197
+ method: 'POST',
198
+ headers: { 'content-type': 'application/json' },
199
+ body: JSON.stringify({ task: prompt }),
200
+ // worst case: MAX_STEPS(10) x 60s exec waits + brain latency headroom
201
+ signal: AbortSignal.timeout(11 * 60_000),
202
+ });
203
+ const data = await drive.json();
204
+ text = data.text || '(box returned nothing)';
205
+ log(`cursor-backend: << box drove task -> ${text.length} chars`);
206
+ } else {
207
+ const zoo = await fetch('http://127.0.0.1:8402/v1/chat/completions', {
208
+ method: 'POST',
209
+ headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
210
+ body: JSON.stringify({
211
+ model: process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
212
+ messages: [{ role: 'user', content: prompt }],
213
+ max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
214
+ }),
215
+ });
216
+ const data = await zoo.json();
217
+ text = data.choices?.[0]?.message?.content || '(no content)';
218
+ log(`cursor-backend: << zoo replied ${text.length} chars (paid x402, no box wired)`);
219
+ }
220
+ } catch (e) { text = `openzoo error: ${e.message}`; log(`cursor-backend: drive call failed: ${e.message}`); }
204
221
 
205
222
  res.writeHead(200, {
206
223
  'content-type': isGrpcWeb ? 'application/grpc-web+proto' : 'application/connect+proto',
package/lib/grokbot.js CHANGED
@@ -20,10 +20,10 @@ import { existsSync } from 'node:fs';
20
20
  import { homedir } from 'node:os';
21
21
  import { join } from 'node:path';
22
22
 
23
- import { config } from './config.js';
23
+ import { config, USDC_MINT, TOKEN_MINT } from './config.js';
24
24
  import { PayClient } from './pay.js';
25
25
  import {
26
- podEndpoints, readyBox, runpodConfigured, spawnBox, listBoxes,
26
+ podEndpoints, readyBox, runpodConfigured, spawnBox, listBoxes, execInBox,
27
27
  } from './boxes.js';
28
28
 
29
29
  const APP = '/Applications/Grok Bot.app';
@@ -46,19 +46,15 @@ export async function runGrokBot(argv = []) {
46
46
  }
47
47
 
48
48
  const days = Number((argv.find((a) => /^--days=/.test(a)) || '').split('=')[1]) || 1;
49
- const shareWallet = argv.includes('--share-wallet');
50
- const client = new PayClient();
51
49
 
52
50
  // 1) a box, ready, BEFORE we touch the app — so the first EnsureSandBox we
53
- // hijack already has a live pod to point at.
51
+ // hijack already has a live pod to point at. The box mints its OWN wallet
52
+ // (never the operator's secret key — that never leaves this Mac). There is
53
+ // NO --task and NO --share-wallet: the task is whatever the user types
54
+ // into the Grok Bot UI, captured from the MITM'd chat request and driven
55
+ // through to the box over POST /drive.
54
56
  console.error('openzoo grokbot: spawning your box (CPU, x402-funded)…');
55
- const taskArg = (argv.find((a) => a.startsWith('--task=')) || '').slice('--task='.length)
56
- || (argv.includes('--task') ? argv[argv.indexOf('--task') + 1] : '');
57
- const spawned = await spawnBox({
58
- name: 'grokbot', days,
59
- ...(shareWallet ? { walletJson: JSON.stringify([...client.keypair.secretKey]) } : {}),
60
- ...(taskArg ? { task: taskArg } : {}),
61
- });
57
+ const spawned = await spawnBox({ name: 'grokbot', days });
62
58
  if (!spawned.ok) { console.error(` spawn failed: ${spawned.error}`); process.exit(1); }
63
59
  const box = spawned.box;
64
60
  console.error(` ${box.name} ${box.id} expires ${spawned.expiresAt.slice(0, 16)}`);
@@ -66,6 +62,18 @@ export async function runGrokBot(argv = []) {
66
62
  if (!ready.ok) console.error(` ${ready.error} — continuing; the capture agent may still bind`);
67
63
  else console.error(` box ready in ${ready.seconds}s`);
68
64
 
65
+ // the box minted its OWN wallet on boot — print its funding address so it
66
+ // can be topped up directly. Its brain pays per call from THIS wallet, not
67
+ // the operator's; a fresh box starts at $0 until funded.
68
+ if (box.ssh) {
69
+ const addr = await execInBox(box, 'npx --yes openzoo address 2>/dev/null');
70
+ if (addr.ok && addr.stdout.trim()) {
71
+ console.error(' box wallet (top up to give it inference budget):');
72
+ console.error(addr.stdout.trim().split('\n').map((l) => ` ${l}`).join('\n'));
73
+ }
74
+ console.error(` mints: USDC ${USDC_MINT} · TOKEN ${TOKEN_MINT} (Solana)`);
75
+ }
76
+
69
77
  const ep = podEndpoints(box.id);
70
78
  process.env.OZ_HIJACK_POD = JSON.stringify({
71
79
  region: 'us1',
@@ -74,6 +82,8 @@ export async function runGrokBot(argv = []) {
74
82
  token: `nto-${Math.random().toString(36).slice(2, 12)}`,
75
83
  agent: ep.agent, vnc: ep.vnc, p1340: ep.p1340, p6081: ep.p6081,
76
84
  });
85
+ // the backend (handleStreamChat) forwards each UI prompt here to be driven
86
+ process.env.OZ_BOX_AGENT = ep.agent;
77
87
  // other methods must still reach the REAL backend, or the app can't load
78
88
  process.env.OPENZOO_PASSTHRU = '1';
79
89
  process.env.OPENZOO_FORCE_BLOCK = '1';
package/lib/podagent.mjs CHANGED
@@ -30,6 +30,109 @@ const PROXY = process.env.OZ_PROXY || 'http://127.0.0.1:8402/v1';
30
30
  const MODEL = process.env.OZ_BRAIN_MODEL || 'x-ai/grok-4.6';
31
31
  const MAX_STEPS = Number(process.env.OZ_MAX_STEPS || 10);
32
32
 
33
+ // Matches the Grok Bot chat surface itself (dark canvas, right-aligned grey
34
+ // user pills, pink-avatar left-aligned replies, pill input bar with + / mic) —
35
+ // this renders INSIDE Grok Bot's own sandbox panel (its sidebar/titlebar are
36
+ // the app's own chrome, not ours), so only the message canvas needs to match.
37
+ const VNC_CHAT_HTML = `<!doctype html>
38
+ <html><head><meta charset="utf-8"><title>openzoo box</title>
39
+ <meta name="viewport" content="width=device-width,initial-scale=1">
40
+ <style>
41
+ :root { color-scheme: dark; }
42
+ * { box-sizing: border-box; }
43
+ html, body { margin: 0; height: 100%; background: #000; }
44
+ body { color: #ececec; font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
45
+ display: flex; flex-direction: column; }
46
+ #log { flex: 1; overflow-y: auto; padding: 28px 24px 12px; display: flex; flex-direction: column; gap: 18px; }
47
+ .row { display: flex; align-items: flex-start; gap: 10px; max-width: 78%; }
48
+ .row.user { align-self: flex-end; flex-direction: row-reverse; }
49
+ .row.bot { align-self: flex-start; }
50
+ .avatar { width: 30px; height: 30px; border-radius: 8px; flex: 0 0 30px; background: #e91e8c;
51
+ display: flex; align-items: center; justify-content: center; }
52
+ .avatar svg { width: 16px; height: 16px; }
53
+ .bubble { padding: 11px 15px; border-radius: 18px; white-space: pre-wrap; word-break: break-word; }
54
+ .row.user .bubble { background: #3a3a3c; border-bottom-right-radius: 5px; }
55
+ .row.bot .bubble { background: transparent; padding: 6px 0; color: #ececec; }
56
+ .row.bot.pending .bubble { color: #8e8e93; }
57
+ .dots span { display: inline-block; width: 5px; height: 5px; margin-right: 3px; border-radius: 50%;
58
+ background: #8e8e93; animation: blink 1.2s infinite ease-in-out; }
59
+ .dots span:nth-child(2) { animation-delay: .2s; } .dots span:nth-child(3) { animation-delay: .4s; }
60
+ @keyframes blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }
61
+ #bar { padding: 10px 16px 18px; }
62
+ #pill { display: flex; align-items: center; gap: 10px; background: #2c2c2e; border-radius: 26px;
63
+ padding: 8px 10px 8px 14px; }
64
+ .icon-btn { width: 32px; height: 32px; border-radius: 50%; border: none; background: transparent;
65
+ color: #ececec; display: flex; align-items: center; justify-content: center; cursor: pointer;
66
+ flex: 0 0 32px; }
67
+ .icon-btn:hover { background: #3a3a3c; }
68
+ .icon-btn svg { width: 18px; height: 18px; }
69
+ #inp { flex: 1; background: transparent; border: none; color: #ececec; font: inherit;
70
+ padding: 6px 0; min-width: 0; }
71
+ #inp::placeholder { color: #8e8e93; }
72
+ #inp:focus { outline: none; }
73
+ </style></head>
74
+ <body>
75
+ <div id="log"></div>
76
+ <div id="bar">
77
+ <div id="pill">
78
+ <button class="icon-btn" tabindex="-1">
79
+ <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>
80
+ </button>
81
+ <input id="inp" placeholder="Message pods" autofocus>
82
+ <button class="icon-btn" id="send">
83
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M6 11l6-6 6 6"/></svg>
84
+ </button>
85
+ </div>
86
+ </div>
87
+ <script>
88
+ const log = document.getElementById('log');
89
+ const inp = document.getElementById('inp');
90
+ const send = document.getElementById('send');
91
+ 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>';
92
+ function addRow(who, text) {
93
+ const row = document.createElement('div');
94
+ 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
+ const bubble = document.createElement('div');
102
+ bubble.className = 'bubble';
103
+ bubble.innerHTML = text === null ? '<span class="dots"><span></span><span></span><span></span></span>' : '';
104
+ if (text !== null) bubble.textContent = text;
105
+ row.appendChild(bubble);
106
+ log.appendChild(row);
107
+ log.scrollTop = log.scrollHeight;
108
+ return { row, bubble };
109
+ }
110
+ async function submit() {
111
+ const task = inp.value.trim();
112
+ if (!task) return;
113
+ inp.value = '';
114
+ inp.disabled = true; send.disabled = true;
115
+ addRow('user', task);
116
+ const pending = addRow('bot', null);
117
+ try {
118
+ const r = await fetch('/drive', {
119
+ method: 'POST', headers: { 'content-type': 'application/json' },
120
+ body: JSON.stringify({ task }),
121
+ });
122
+ const j = await r.json();
123
+ pending.row.classList.remove('pending');
124
+ pending.bubble.textContent = j.text || '(no response)';
125
+ } catch (e) {
126
+ pending.row.classList.remove('pending');
127
+ pending.bubble.textContent = 'error: ' + e.message;
128
+ }
129
+ inp.disabled = false; send.disabled = false; inp.focus();
130
+ }
131
+ send.addEventListener('click', submit);
132
+ inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit(); });
133
+ </script>
134
+ </body></html>`;
135
+
33
136
  function record(entry) {
34
137
  try { appendFileSync(LOG, JSON.stringify(entry) + '\n'); } catch { /* best effort */ }
35
138
  console.log(`[agent:${entry.port ?? '-'}] ${entry.method || entry.ev} ${entry.path || ''} ${entry.bodyBytes ?? ''}${entry.frames ? ' frames=' + entry.frames : ''}`);
@@ -70,25 +173,42 @@ async function brain(messages) {
70
173
  return j?.choices?.[0]?.message?.content ?? '';
71
174
  }
72
175
 
73
- const SYSTEM = `You drive a Linux/macOS shell to accomplish a task. Reply with EXACTLY one line:
74
- either RUN: <a single shell command>
75
- or DONE: <one-line summary> when the task is complete.
76
- No prose, no fences. You are given each command's output before your next line.`;
176
+ const SYSTEM = `You are the brain of a Grok-Bot-style coding/ops agent. The polished chat UI
177
+ the user sees is Grok Bot (Anysphere's app); its "sandbox" has been pointed at THIS box, and
178
+ your reasoning is served by openzoo (pay-per-call access to ~435 models over x402 — no API key,
179
+ no account, each call paid from a burner wallet). You are the substitute agent server.
180
+
181
+ WHERE YOUR COMMANDS RUN: each shell command you emit is pushed to a local-exec daemon running on
182
+ the USER'S OWN Mac (a supervised "sand" sandbox rooted at their home dir). Every command is
183
+ APPROVAL-GATED — the user sees and approves it before it runs. So: act on the user's real machine,
184
+ be careful, never destructive, prefer read-before-write, and explain nothing to the shell.
185
+
186
+ PROTOCOL — reply with EXACTLY one line, no prose, no code fences:
187
+ RUN: <a single shell command> to execute a step
188
+ DONE: <a short natural-language answer> when the task is complete (this text is shown in the UI)
189
+ You are given each command's output before your next line. If the task needs no shell (a question,
190
+ an explanation), answer it directly with a single DONE: line. Keep DONE summaries human and useful —
191
+ they are the assistant's reply to the user, not a log.`;
77
192
 
78
193
  /** The agent loop for one task, executed through the connected daemon. Each
79
194
  * RUN is pushed as an exec frame; the daemon's result frames (captured in
80
195
  * `pendingResults`) feed the next turn. */
81
196
  async function runTask(task, stream, ctx) {
82
197
  const messages = [{ role: 'system', content: SYSTEM }, { role: 'user', content: task }];
198
+ let answer = '';
83
199
  for (let step = 0; step < MAX_STEPS; step++) {
84
200
  const line = (await brain(messages)).trim();
85
201
  record({ ev: 'brain', step, line });
202
+ const done = /^DONE:\s*([\s\S]+)/.exec(line);
203
+ if (done) { answer = done[1].trim(); record({ ev: 'task-done', step, answer }); return answer; }
86
204
  const run = /^RUN:\s*([\s\S]+)/.exec(line);
87
- if (!run) { record({ ev: 'task-done', step, line }); return; }
205
+ // no RUN and no DONE treat the whole line as a direct answer to the user
206
+ if (!run) { answer = line.replace(/^DONE:\s*/i, ''); record({ ev: 'task-freeform', step, answer }); return answer; }
88
207
  const cmd = run[1].trim();
89
208
  const frame = execFrame(cmd);
90
209
  ctx.awaiting = frame.requestId;
91
210
  ctx.output = '';
211
+ ctx.done = false;
92
212
  sseSend(stream, frame);
93
213
  record({ ev: 'exec-push', step, requestId: frame.requestId, cmd });
94
214
  // wait for the daemon's result frames for this requestId (or timeout)
@@ -96,6 +216,7 @@ async function runTask(task, stream, ctx) {
96
216
  messages.push({ role: 'assistant', content: line });
97
217
  messages.push({ role: 'user', content: `output:\n${out || '(none)'}` });
98
218
  }
219
+ return answer || '(reached step limit without finishing)';
99
220
  }
100
221
 
101
222
  function waitResult(ctx, timeoutMs) {
@@ -113,36 +234,70 @@ function waitResult(ctx, timeoutMs) {
113
234
 
114
235
  // per-connection context so a result frame can be matched to its exec
115
236
  const ctxByToken = new Map();
237
+ // the single live daemon (Grok Bot's local-exec on the user's Mac). Set when it
238
+ // opens its SSE; the /drive endpoint pushes exec frames down THIS stream.
239
+ let activeDaemon = null;
240
+ // one exec loop at a time against the shared daemon ctx — two concurrent
241
+ // runTask calls would interleave result frames onto the same ctx.output.
242
+ let driveQueue = Promise.resolve();
243
+ function queueDrive(fn) {
244
+ const next = driveQueue.then(fn, fn);
245
+ driveQueue = next.catch(() => {});
246
+ return next;
247
+ }
116
248
 
117
249
  for (const port of PORTS) {
118
250
  const server = http.createServer((req, res) => {
119
- // The daemon's receive-channel: hold the SSE open; when it connects, if a
120
- // task is queued (OZ_TASK) run the agent loop against it.
251
+ // The daemon's receive-channel: hold the SSE open. The TASK does NOT come
252
+ // from here or from any env var it arrives via POST /drive, forwarded by
253
+ // the backend from whatever the user typed into the Grok Bot UI.
121
254
  if (req.method === 'GET' && req.url && req.url.startsWith('/local-exec/requests')) {
122
255
  const token = (req.headers['x-anyrun-network-token'] || 'default');
123
256
  const ctx = { awaiting: null, output: '', done: false };
124
257
  ctxByToken.set(token, ctx);
125
- record({ port, method: 'GET', path: req.url, note: 'SSE opened', bodyBytes: 0 });
258
+ activeDaemon = { stream: res, ctx };
259
+ record({ port, method: 'GET', path: req.url, note: 'SSE opened — daemon connected', bodyBytes: 0 });
126
260
  res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
127
261
  res.write(':ok\n\n');
128
- // announce ourselves, then drive the task
262
+ // announce ourselves; then wait for the UI to drive us
129
263
  sseSend(res, { kind: 'welcome', providerId: 'openzoo' });
130
- req.on('close', () => ctxByToken.delete(token));
131
- if (process.env.OZ_TASK) {
132
- setTimeout(() => runTask(process.env.OZ_TASK, res, ctx).catch((e) => record({ ev: 'task-error', err: String(e) })), 1200);
133
- }
264
+ req.on('close', () => {
265
+ ctxByToken.delete(token);
266
+ if (activeDaemon && activeDaemon.stream === res) activeDaemon = null;
267
+ });
134
268
  return;
135
269
  }
136
270
 
137
271
  const chunks = [];
138
272
  req.on('data', (d) => chunks.push(d));
139
- req.on('end', () => {
273
+ req.on('end', async () => {
140
274
  const body = Buffer.concat(chunks);
141
275
  let parsed, kinds;
142
276
  try { parsed = JSON.parse(body.toString('utf8')); kinds = parsed.frames?.map((f) => f.kind).join(','); } catch { /* not json */ }
143
277
  record({ port, method: req.method, path: req.url, bodyBytes: body.length, frames: kinds,
144
278
  bodyUtf8: body.slice(0, 1024).toString('utf8').replace(/[^\x20-\x7e]/g, '.') });
145
279
 
280
+ // THE TRIGGER: the backend forwards the user's Grok Bot prompt here. Drive
281
+ // the agent loop against the connected daemon and return the answer text.
282
+ if (req.method === 'POST' && req.url && req.url.startsWith('/drive')) {
283
+ let task = '';
284
+ try { task = (JSON.parse(body.toString('utf8')).task || '').toString(); }
285
+ catch { task = body.toString('utf8'); }
286
+ record({ ev: 'drive', task: task.slice(0, 160) });
287
+ if (!activeDaemon) {
288
+ res.writeHead(200, { 'content-type': 'application/json' });
289
+ res.end(JSON.stringify({ ok: false, text: '(sandbox not connected yet — give the app a moment and retry)' }));
290
+ return;
291
+ }
292
+ let text;
293
+ try {
294
+ text = await queueDrive(() => runTask(task, activeDaemon.stream, activeDaemon.ctx));
295
+ } catch (e) { text = `agent error: ${e.message}`; record({ ev: 'drive-error', err: String(e) }); }
296
+ res.writeHead(200, { 'content-type': 'application/json' });
297
+ res.end(JSON.stringify({ ok: true, text }));
298
+ return;
299
+ }
300
+
146
301
  // the daemon posts result frames here — accumulate output for the loop
147
302
  if (parsed?.frames && req.url?.includes('/local-exec/responses')) {
148
303
  const token = (req.headers['x-anyrun-network-token'] || 'default');
@@ -153,9 +308,16 @@ for (const port of PORTS) {
153
308
  }
154
309
  }
155
310
 
311
+ // THE REAL TRIGGER. Grok Bot's own chat (StreamUnifiedChat) never reaches
312
+ // us — inference happens server-side inside whatever pod EnsureSandBox
313
+ // named, and since we hijacked that to a pod that doesn't exist on
314
+ // Cursor's side, the UI's chat box just sits silent forever. But the app
315
+ // ALSO opens this vnc.html URL — OUR box — inside its own sandbox panel.
316
+ // Serve a real chat page here instead of a stub: the user types inside
317
+ // Grok Bot's own window, it POSTs straight to /drive on this box.
156
318
  if (req.url && req.url.includes('/vnc')) {
157
319
  res.writeHead(200, { 'content-type': 'text/html' });
158
- res.end('<!doctype html><title>openzoo box</title>');
320
+ res.end(VNC_CHAT_HTML);
159
321
  } else {
160
322
  res.writeHead(200, { 'content-type': 'application/json' });
161
323
  res.end('{"ok":true}');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.41.1",
3
+ "version": "0.43.0",
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",