openzoo 0.50.24 → 0.50.26
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/cursorbackend.js +118 -11
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -663,6 +663,56 @@ function ssePush(channel, payload) {
|
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
let focusedAgentId = null;
|
|
667
|
+
const agentActivity = new Map(); // id -> { updatedAt, unreadCount, hasUnread, preview }
|
|
668
|
+
function noteFocus(id) {
|
|
669
|
+
if (!id) return;
|
|
670
|
+
focusedAgentId = id;
|
|
671
|
+
const a = agentActivity.get(id) || {};
|
|
672
|
+
a.hasUnread = false;
|
|
673
|
+
a.unreadCount = 0;
|
|
674
|
+
agentActivity.set(id, a);
|
|
675
|
+
}
|
|
676
|
+
function stampActivity(agent) {
|
|
677
|
+
const a = agentActivity.get(agent.id) || {};
|
|
678
|
+
const updatedAt = a.updatedAt || agent.updatedAt || agent.createdAt || 0;
|
|
679
|
+
return {
|
|
680
|
+
...agent,
|
|
681
|
+
updatedAt,
|
|
682
|
+
hasUnread: !!a.hasUnread,
|
|
683
|
+
unreadCount: a.unreadCount || 0,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function sortAgentsByActivity(list) {
|
|
687
|
+
return [...list].sort((x, y) => {
|
|
688
|
+
const ax = agentActivity.get(x.id)?.updatedAt || x.updatedAt || x.createdAt || 0;
|
|
689
|
+
const ay = agentActivity.get(y.id)?.updatedAt || y.updatedAt || y.createdAt || 0;
|
|
690
|
+
return ay - ax;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function bumpAgent(id, { preview = '', notify = true } = {}) {
|
|
694
|
+
const now = Date.now();
|
|
695
|
+
const a = agentActivity.get(id) || { unreadCount: 0 };
|
|
696
|
+
a.updatedAt = now;
|
|
697
|
+
if (preview) a.preview = String(preview).slice(0, 120);
|
|
698
|
+
const other = notify && focusedAgentId && focusedAgentId !== id;
|
|
699
|
+
if (other) {
|
|
700
|
+
a.hasUnread = true;
|
|
701
|
+
a.unreadCount = (a.unreadCount || 0) + 1;
|
|
702
|
+
}
|
|
703
|
+
agentActivity.set(id, a);
|
|
704
|
+
const list = cachedAgentList() || [];
|
|
705
|
+
const idx = list.findIndex((x) => x.id === id);
|
|
706
|
+
const base = idx >= 0 ? list[idx] : { id, name: id };
|
|
707
|
+
const agent = stampActivity({ ...base, updatedAt: now });
|
|
708
|
+
if (idx >= 0) list[idx] = agent;
|
|
709
|
+
else list.unshift(agent);
|
|
710
|
+
saveAgents(sortAgentsByActivity(list));
|
|
711
|
+
// asar handleGatewaySseEvent: channel agent-upserted → {agent, activeAgentId}
|
|
712
|
+
ssePush('agent-upserted', { agent, activeAgentId: focusedAgentId || id });
|
|
713
|
+
ssePush('agents', { agents: sortAgentsByActivity(cachedAgentList() || []).slice(0, 80).map(stampActivity), activeAgentId: focusedAgentId || id });
|
|
714
|
+
}
|
|
715
|
+
|
|
666
716
|
/** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
|
|
667
717
|
* is `{providerId, frames}`. We used to JSON-[] the GET which is "disconnected"
|
|
668
718
|
* (measured: Grok Bot "can't see files on your local computer"). */
|
|
@@ -874,9 +924,9 @@ function mergeAgentLists(remote) {
|
|
|
874
924
|
for (const a of [...local, ...(Array.isArray(remote) ? remote : [])]) {
|
|
875
925
|
if (!a?.id || seen.has(a.id)) continue;
|
|
876
926
|
seen.add(a.id);
|
|
877
|
-
out.push(a);
|
|
927
|
+
out.push(stampActivity(a));
|
|
878
928
|
}
|
|
879
|
-
return out;
|
|
929
|
+
return sortAgentsByActivity(out);
|
|
880
930
|
}
|
|
881
931
|
function gatewayEntry(e) {
|
|
882
932
|
const { seq, pulledRemote, ...rest } = e;
|
|
@@ -1257,6 +1307,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1257
1307
|
'Tools: read_file, write_file, exec, list_dir. USE THEM. Write files to disk instead of pasting giant HTML into chat.',
|
|
1258
1308
|
'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
|
|
1259
1309
|
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
1310
|
+
'After using tools you MUST still write a normal chat reply: what you did, file paths written, and what to open. Empty content is a bug.',
|
|
1260
1311
|
'A spend footer is appended after your reply by the host — ignore it.',
|
|
1261
1312
|
].join(' '),
|
|
1262
1313
|
},
|
|
@@ -1285,14 +1336,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1285
1336
|
const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 4096);
|
|
1286
1337
|
let lastData = {};
|
|
1287
1338
|
let text = '';
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
model,
|
|
1291
|
-
messages,
|
|
1292
|
-
tools: LOCAL_TOOLS,
|
|
1293
|
-
tool_choice: 'auto',
|
|
1294
|
-
max_tokens: maxTok,
|
|
1295
|
-
};
|
|
1339
|
+
const usedTools = [];
|
|
1340
|
+
const zooPost = async (payload) => {
|
|
1296
1341
|
const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
1297
1342
|
method: 'POST',
|
|
1298
1343
|
headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
|
|
@@ -1306,6 +1351,16 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1306
1351
|
r = await post();
|
|
1307
1352
|
}
|
|
1308
1353
|
const data = await r.json();
|
|
1354
|
+
return { r, data };
|
|
1355
|
+
};
|
|
1356
|
+
for (let step = 0; step < 8; step++) {
|
|
1357
|
+
const { r, data } = await zooPost({
|
|
1358
|
+
model,
|
|
1359
|
+
messages,
|
|
1360
|
+
tools: LOCAL_TOOLS,
|
|
1361
|
+
tool_choice: 'auto',
|
|
1362
|
+
max_tokens: maxTok,
|
|
1363
|
+
});
|
|
1309
1364
|
lastData = data;
|
|
1310
1365
|
const msg = data.choices?.[0]?.message || {};
|
|
1311
1366
|
const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
@@ -1317,6 +1372,12 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1317
1372
|
try { args = JSON.parse(c.function?.arguments || c.arguments || '{}'); } catch { args = {}; }
|
|
1318
1373
|
const name = c.function?.name || c.name || '';
|
|
1319
1374
|
const result = await runLocalTool(name, args, log);
|
|
1375
|
+
usedTools.push({
|
|
1376
|
+
name,
|
|
1377
|
+
path: args.path,
|
|
1378
|
+
command: args.command ? String(args.command).slice(0, 120) : undefined,
|
|
1379
|
+
note: String(result).slice(0, 200),
|
|
1380
|
+
});
|
|
1320
1381
|
messages.push({
|
|
1321
1382
|
role: 'tool',
|
|
1322
1383
|
tool_call_id: c.id,
|
|
@@ -1325,10 +1386,37 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1325
1386
|
}
|
|
1326
1387
|
continue;
|
|
1327
1388
|
}
|
|
1328
|
-
text = zooTextFromMessage(msg, data)
|
|
1389
|
+
text = zooTextFromMessage(msg, data);
|
|
1329
1390
|
log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${data.choices?.[0]?.finish_reason || '?'}`);
|
|
1330
1391
|
break;
|
|
1331
1392
|
}
|
|
1393
|
+
if (usedTools.length && !String(text || '').trim()) {
|
|
1394
|
+
messages.push({
|
|
1395
|
+
role: 'user',
|
|
1396
|
+
content: 'Stop calling tools. Write a short chat reply summarizing what you did: files written (full paths), commands that mattered, and what the user should open. No empty message.',
|
|
1397
|
+
});
|
|
1398
|
+
const { r, data } = await zooPost({
|
|
1399
|
+
model,
|
|
1400
|
+
messages,
|
|
1401
|
+
max_tokens: Math.max(800, Math.min(maxTok, 2048)),
|
|
1402
|
+
});
|
|
1403
|
+
lastData = data;
|
|
1404
|
+
text = zooTextFromMessage(data.choices?.[0]?.message, data);
|
|
1405
|
+
log(`cursor-backend: << zoo summary ${r.status} ${text.length}c`);
|
|
1406
|
+
}
|
|
1407
|
+
if (!String(text || '').trim()) {
|
|
1408
|
+
if (usedTools.length) {
|
|
1409
|
+
const writes = usedTools.filter((t) => t.name === 'write_file' && t.path).map((t) => t.path);
|
|
1410
|
+
const execs = usedTools.filter((t) => t.name === 'exec' && t.command).map((t) => t.command);
|
|
1411
|
+
const lines = ['Did local work (no model chat text came back):'];
|
|
1412
|
+
if (writes.length) lines.push(`wrote: ${[...new Set(writes)].join(', ')}`);
|
|
1413
|
+
if (execs.length) lines.push(`ran: ${execs.slice(-6).join(' · ')}`);
|
|
1414
|
+
if (lines.length === 1) lines.push(`${usedTools.length} tool calls.`);
|
|
1415
|
+
text = lines.join('\n');
|
|
1416
|
+
} else {
|
|
1417
|
+
text = '(empty zoo reply)';
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1332
1420
|
try { text += await zooSpendOverlay(lastData); } catch { /* overlay must never eat the reply */ }
|
|
1333
1421
|
return { text, data: lastData };
|
|
1334
1422
|
}
|
|
@@ -1414,6 +1502,21 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1414
1502
|
log(`cursor-backend: updateAgent local id=${agent.id}`);
|
|
1415
1503
|
return true;
|
|
1416
1504
|
}
|
|
1505
|
+
if (name === 'setAgentUnread') {
|
|
1506
|
+
let parsed = {};
|
|
1507
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
1508
|
+
const id = String(parsed.id || '');
|
|
1509
|
+
const unread = parsed.isUnread === true;
|
|
1510
|
+
if (id) {
|
|
1511
|
+
const a = agentActivity.get(id) || {};
|
|
1512
|
+
a.hasUnread = unread;
|
|
1513
|
+
a.unreadCount = unread ? Math.max(1, a.unreadCount || 1) : 0;
|
|
1514
|
+
agentActivity.set(id, a);
|
|
1515
|
+
if (!unread) noteFocus(id);
|
|
1516
|
+
}
|
|
1517
|
+
jsonSend(res, { ok: true });
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1417
1520
|
if (name === 'deleteAgents') {
|
|
1418
1521
|
let parsed = {};
|
|
1419
1522
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
@@ -1457,6 +1560,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1457
1560
|
requestId: nonce,
|
|
1458
1561
|
richText: parsed.richText,
|
|
1459
1562
|
});
|
|
1563
|
+
noteFocus(agentId);
|
|
1564
|
+
bumpAgent(agentId, { preview: prompt, notify: false });
|
|
1460
1565
|
ssePush('transcript', { ...gatewayEntry(userLine), agentId });
|
|
1461
1566
|
jsonSend(res, { accepted: true });
|
|
1462
1567
|
const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
|
|
@@ -1489,6 +1594,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1489
1594
|
}
|
|
1490
1595
|
const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
1491
1596
|
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
1597
|
+
bumpAgent(agentId, { preview: text, notify: true });
|
|
1492
1598
|
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 80))}`);
|
|
1493
1599
|
});
|
|
1494
1600
|
return true;
|
|
@@ -1497,6 +1603,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1497
1603
|
let parsed = {};
|
|
1498
1604
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
1499
1605
|
const id = String(parsed.id || parsed.agentId || 'openzoo');
|
|
1606
|
+
noteFocus(id);
|
|
1500
1607
|
tailedAgents.add(id);
|
|
1501
1608
|
const t = agentTranscript(id);
|
|
1502
1609
|
if (!t.pulledRemote && realPod?.agent) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.26",
|
|
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",
|