openzoo 0.50.95 → 0.50.97

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/bin/claude-zoo.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * the local openzoo proxy on :8402 (x402 per-call payment, no Anthropic
7
7
  * subscription, no login):
8
8
  *
9
- * - starts the proxy if it is not already listening
9
+ * - starts the proxy if it is not already THIS version on :8402 (steals stale)
10
10
  * - applies claudeZooEnv(): ANTHROPIC_BASE_URL=localhost:PORT/v1,
11
11
  * AUTH_TOKEN=sk-openzoo, ANTHROPIC_API_KEY deleted, compaction disabled
12
12
  * (the proxy binds the prefix), 1M-token ceiling restored
@@ -16,7 +16,8 @@
16
16
  * `claude-code-cli` (same directory, symlinked to occ).
17
17
  */
18
18
  import { spawn } from 'node:child_process';
19
- import { existsSync, readdirSync as fsReaddir } from 'node:fs';
19
+ import { existsSync, readdirSync as fsReaddir, readFileSync } from 'node:fs';
20
+ import { execSync } from 'node:child_process';
20
21
  import { homedir, platform } from 'node:os';
21
22
  import { dirname, join, sep } from 'node:path';
22
23
  import { fileURLToPath } from 'node:url';
@@ -50,10 +51,34 @@ if (!occ) {
50
51
  const PROXY_PORT = Number(process.env.OPENZOO_PROXY_PORT || 8402);
51
52
  const PROXY_URL = `http://localhost:${PROXY_PORT}/v1`;
52
53
 
54
+ function mineVersion() {
55
+ try {
56
+ return JSON.parse(readFileSync(join(shimRoot, 'package.json'), 'utf8')).version;
57
+ } catch { return ''; }
58
+ }
59
+
60
+ function stealPort(port) {
61
+ const n = Number(port);
62
+ for (const cmd of [
63
+ `lsof -t -iTCP:${n} -sTCP:LISTEN | xargs kill -9`,
64
+ `fuser -k ${n}/tcp`,
65
+ ]) {
66
+ try { execSync(cmd, { stdio: 'ignore', timeout: 2000, shell: true }); } catch { /* missing */ }
67
+ }
68
+ }
69
+
53
70
  async function proxyUp() {
54
71
  try {
55
72
  const r = await fetch(`http://localhost:${PROXY_PORT}/v1/info`, { signal: AbortSignal.timeout(1500) });
56
- return r.ok;
73
+ if (!r.ok) return false;
74
+ const j = await r.json().catch(() => ({}));
75
+ const mine = mineVersion();
76
+ if (mine && String(j.version || '') !== mine) {
77
+ console.error(`claude: stale proxy v${j.version || '?'} on :${PROXY_PORT} — stealing for v${mine}`);
78
+ stealPort(PROXY_PORT);
79
+ return false;
80
+ }
81
+ return true;
57
82
  } catch { return false; }
58
83
  }
59
84
 
package/lib/aoe.js CHANGED
@@ -287,9 +287,15 @@ export async function setupAoe(argv = []) {
287
287
 
288
288
  const base = `http://localhost:${config.port}/v1`;
289
289
  if (!flags.has('--no-proxy')) {
290
- if (await proxyUp(base)) {
291
- console.error(`openzoo: proxy already up on ${base}`);
290
+ const { oursOn, packageVersion, killListen } = await import('./proxy.js');
291
+ if (await oursOn(config.port)) {
292
+ console.error(`openzoo: proxy v${packageVersion()} already on ${base}`);
292
293
  } else {
294
+ if (await proxyUp(base)) {
295
+ console.error(`openzoo: stale proxy on ${base} — stealing :${config.port}`);
296
+ killListen(config.port);
297
+ await new Promise((r) => setTimeout(r, 400));
298
+ }
293
299
  const { pid, logPath } = startDetachedProxy({ tunnel: flags.has('--tunnel') });
294
300
  let up = false;
295
301
  for (let i = 0; i < 40 && !up; i++) {
@@ -44,7 +44,8 @@ import {
44
44
  readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
45
45
  DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
46
46
  } from './grokbotAccount.js';
47
- import { formatSpendFooter, mergeTurnProof } from './spendProof.js';
47
+ import { spendChipLabel, formatSpendFooter, mergeTurnProof } from './spendProof.js';
48
+ import { spendLinesOnly, writeSpendHud } from './ozSpendChip.js';
48
49
  import { zooModelIds, zooModelRow, mediaKindOf } from './models.js';
49
50
  import { prefixVisitorRichText } from './grokbotweb.js';
50
51
  import {
@@ -651,10 +652,10 @@ async function fetchEnsureSandBox(req, body, log) {
651
652
  try {
652
653
  const remote = await podJson('/api/listAgents', {}, log);
653
654
  if (Array.isArray(remote) && remote.length) {
654
- const merged = rosterForEvent(mergeAgentLists(remote), agentActivity);
655
+ const merged = rosterForEvent(mergeAgentLists(remote), agentActivity, { includeHidden: true }); // GROKROOM-HIDDEN-BOTS
655
656
  saveAgents(merged);
656
657
  const active = focusedAgentId || merged[0]?.id;
657
- ssePush('agents', { agents: merged, activeAgentId: active });
658
+ ssePush('agents', { agents: merged.filter((a) => !a.hidden), activeAgentId: active }); // GROKROOM-HIDDEN-BOTS
658
659
  log(`cursor-backend: discovered roster n=${merged.length} account=${realPod.accountId}`);
659
660
  }
660
661
  } catch (e) {
@@ -836,9 +837,9 @@ async function proxyPodHttp(req, res, full, body, log) {
836
837
  try {
837
838
  const parsed = JSON.parse(String(inflateBody(cap.buf, cap.respHeaders)));
838
839
  if (Array.isArray(parsed)) {
839
- const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity);
840
+ const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity, { includeHidden: true }); // GROKROOM-HIDDEN-BOTS
840
841
  saveAgents(merged);
841
- jsonSend(res, merged);
842
+ jsonSend(res, merged.filter((a) => !a.hidden)); // GROKROOM-HIDDEN-BOTS
842
843
  log(`cursor-backend: listAgents 200 merged n=${merged.length} account=${activeAccountId || '?'}`);
843
844
  return true;
844
845
  }
@@ -971,7 +972,7 @@ function pushCreatedAgent(agent, { select = true } = {}) {
971
972
  const active = focusedAgentId || agent.id;
972
973
  const list = rosterForEvent(cachedAgentList() || [], agentActivity);
973
974
  ssePush('agent-upserted', { agent, activeAgentId: active });
974
- ssePush('agents', { agents: list, activeAgentId: active });
975
+ ssePush('agents', { agents: list.filter((a) => !a.hidden), activeAgentId: active }); // GROKROOM-HIDDEN-BOTS
975
976
  }
976
977
 
977
978
  /** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
@@ -1880,7 +1881,15 @@ async function zooSpendOverlay(data) {
1880
1881
  const bal = Number.isFinite(wallet) && wallet > 0.004
1881
1882
  ? wallet
1882
1883
  : (Number.isFinite(credit) && credit > 0.004 ? credit : null);
1883
- return formatSpendFooter({
1884
+ const label = spent > 0.00005 ? spendChipLabel({
1885
+ billedUsd: x.billedUsd,
1886
+ directUsd: x.directUsd,
1887
+ spent,
1888
+ would,
1889
+ saved,
1890
+ pct,
1891
+ }) : '';
1892
+ const footer = formatSpendFooter({
1884
1893
  billedUsd: x.billedUsd,
1885
1894
  directUsd: x.directUsd,
1886
1895
  spent,
@@ -1890,6 +1899,12 @@ async function zooSpendOverlay(data) {
1890
1899
  balance: bal,
1891
1900
  x402: x,
1892
1901
  });
1902
+ const body = spendLinesOnly(footer) || (spent > 0.00005
1903
+ ? `spent $${spent.toFixed(4)} · OpenRouter would $${would.toFixed(4)} · saved $${saved.toFixed(4)} (${pct.toFixed(0)}%)`
1904
+ : '');
1905
+ const state = { spent, would, saved, pct, label, billedUsd: x.billedUsd, directUsd: x.directUsd, balance: bal, body };
1906
+ try { writeSpendHud(state); } catch { /* */ }
1907
+ return state;
1893
1908
  }
1894
1909
 
1895
1910
  const MODELS_PATH = path.join(os.homedir(), '.openzoo', 'grokbot-models.json');
@@ -2941,7 +2956,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2941
2956
  'This is a public visitor chat. You do not have filesystem, shell, or local-exec access on the host Mac.',
2942
2957
  'Reply in chat only. Do not claim you will write files, run commands, or use local tools.',
2943
2958
  'Each human line is prefixed with that visitor\'s shortname and a colon, like "rex: hello". Different shortnames are different people. Address them by that name.',
2944
- 'A spend footer is appended after your reply by the host -- ignore it.',
2959
+ 'Do not write spend totals, solscan links, memos, or ::oz-spend:: into the reply. The host paints a HUD pill.',
2945
2960
  'Prior turns of THIS Grok Bot chat are in the messages below. Do not claim the thread starts blank or that earlier questions did not arrive.',
2946
2961
  ].join(' ')
2947
2962
  : [
@@ -2984,8 +2999,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2984
2999
  'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
2985
3000
  '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. A new user message cancels this turn — leave a visible reply before that happens.',
2986
3001
  'Do not stop mid-task. Do not write "Stopped on research", "nothing to open yet", "no app files written this turn", or "say go again". Keep using tools until the files the user asked for exist on disk THIS turn. Summarize only after those writes succeed.',
2987
- 'A spend footer is appended after your reply by the host ignore it.',
2988
- 'The operator wallet pays every zoo call via x402 (TOKEN/USDC on Solana; SOL gas is sponsored). If this thread has a spend footer or solscan/basescan tx, the call WAS paid. Do not say you have no money, cannot afford tools, or need the human to fund you. A 402/underfunded tool error is a host retry — continue the job.',
3002
+ 'Do not write spend totals, solscan links, memos, or ::oz-spend:: into the reply. The host paints a HUD pill.',
3003
+ 'The operator wallet pays every zoo call via x402 (TOKEN/USDC on Solana; SOL gas is sponsored). If the spend HUD is visible or a prior turn paid, the call WAS paid. Do not say you have no money, cannot afford tools, or need the human to fund you. A 402/underfunded tool error is a host retry — continue the job.',
2989
3004
  'Prior turns of THIS Grok Bot chat are in the messages below. Do not claim the thread starts blank or that earlier questions did not arrive.',
2990
3005
  ].join(' '),
2991
3006
  },
@@ -3075,17 +3090,22 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
3075
3090
  if (r.status === 402) {
3076
3091
  const raw = zooTextFromMessage(data?.choices?.[0]?.message, data)
3077
3092
  || data?.error?.message
3078
- || 'openzoo wallet underfunded.';
3093
+ || 'openzoo payment required (HTTP 402).';
3094
+ data.error = data.error || {};
3095
+ data.error.message = raw;
3079
3096
  try {
3080
- const { withOnrampLink } = await import('./stripeOnramp.js');
3081
- const { loadOrCreateWallet } = await import('./wallet.js');
3082
- const w = loadOrCreateWallet();
3083
- const usd = Number(String(raw).match(/≈\$([0-9.]+)/)?.[1]);
3084
- data.error = data.error || {};
3085
- data.error.message = await withOnrampLink(raw, {
3086
- solana: w.keypair.publicKey.toBase58(),
3087
- usd,
3088
- });
3097
+ const { withOnrampLink, isFundInstruction } = await import('./stripeOnramp.js');
3098
+ // Settle/upstream 402s keep the real message. Whop copy-paste only
3099
+ // when this is a genuine empty-wallet / fund-me instruction.
3100
+ if (isFundInstruction(raw)) {
3101
+ const { loadOrCreateWallet } = await import('./wallet.js');
3102
+ const w = loadOrCreateWallet();
3103
+ const usd = Number(String(raw).match(/≈\$([0-9.]+)/)?.[1]);
3104
+ data.error.message = await withOnrampLink(raw, {
3105
+ solana: w.keypair.publicKey.toBase58(),
3106
+ usd,
3107
+ });
3108
+ }
3089
3109
  } catch { /* keep proxy copy */ }
3090
3110
  }
3091
3111
  return { r, data };
@@ -3232,7 +3252,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
3232
3252
  if (lastData && typeof lastData === 'object') {
3233
3253
  lastData.x402 = mergeTurnProof(turnX402, lastData);
3234
3254
  }
3235
- try { text += await zooSpendOverlay(lastData); } catch { /* overlay must never eat the reply */ }
3255
+ try { await zooSpendOverlay(lastData); } catch { /* HUD dump; never append to the reply */ }
3236
3256
  return { text, data: lastData };
3237
3257
  }
3238
3258
 
@@ -3297,6 +3317,12 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3297
3317
  // Revive: the minutely cron (scripts/revive-bots.sh) calls this after it has
3298
3318
  // made sure the hijack + app are alive. Every non-group bot gets a wakeup
3299
3319
  // timer if it lost one; nothing is spawned, nothing already armed is touched.
3320
+ if (name === 'ozSpend') {
3321
+ let state = { spent: 0, would: 0, saved: 0, pct: 0, label: '' };
3322
+ try { state = await zooSpendOverlay(null); } catch { /* */ }
3323
+ jsonSend(res, { ok: true, ...state });
3324
+ return true;
3325
+ }
3300
3326
  if (name === 'ozRevive') {
3301
3327
  let parsed = {};
3302
3328
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
@@ -3603,7 +3629,6 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3603
3629
  text = `model set to ${id}`;
3604
3630
  }
3605
3631
  }
3606
- try { text += await zooSpendOverlay({}); } catch { /* */ }
3607
3632
  } else {
3608
3633
  const members = groupMemberIds(agentId);
3609
3634
  if (members.length) {