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.
Files changed (2) hide show
  1. package/lib/cursorbackend.js +118 -11
  2. package/package.json +1 -1
@@ -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
- for (let step = 0; step < 8; step++) {
1289
- const payload = {
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) || (step ? '(tool loop ended with empty content)' : '(empty zoo reply)');
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.24",
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",