openzoo 0.32.3 → 0.33.1

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/openzoo.js CHANGED
@@ -19,7 +19,7 @@ usage:
19
19
  only "Auto". Undo: npx openzoo unblock
20
20
  npx openzoo vscode [path] same, for VS Code
21
21
  npx openzoo editor [path] whichever is installed (Cursor wins if both)
22
- npx openzoo claude [dir] [--terminal] launch Claude on the zoo desktop app
22
+ npx openzoo claude [dir] Claude Code CLI on the zoo (x402 per turn); --desktop for the app
23
23
  by default, --terminal for the Claude Code CLI
24
24
  npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
25
25
  (claude, aider...) already pointed at the zoo
package/lib/hosts.js CHANGED
@@ -35,19 +35,24 @@ const HOSTS = WIN
35
35
  const BACKUP = `${HOSTS}.openzoo-backup`;
36
36
  const MARK = '# openzoo: force the editor onto the local proxy';
37
37
 
38
- /** Hosts the editor uses for model sync + its own inference proxy. */
39
- export const BACKEND_HOSTS = [
40
- 'api2.cursor.sh',
41
- // Chat inference (StreamUnifiedChat) hosts EVERY region x privacy variant,
42
- // because /etc/hosts cannot wildcard *.api5.cursor.sh and Auto picks one per
43
- // the account's region + privacy mode. Missing any = the chat escapes to
44
- // Cursor's servers and never reaches us (observed: only api2 blocked -> no
45
- // cursor-backend lines on chat).
38
+ /**
39
+ * The DEFAULT block — the model-list re-sync host only. Blocking this forces a
40
+ * subscribed account's editor onto our model list + endpoint without severing
41
+ * chat, so plain `openzoo cursor` works for subbed users.
42
+ *
43
+ * The chat-inference hosts (agent.api5.*) are NOT here: blocking them without a
44
+ * working StreamUnifiedChat translator severs chat entirely ("Reconnecting...",
45
+ * observed on a working ultra account). They live in AGENT_HOSTS, blocked only
46
+ * under --takeover where the impersonation backend actually answers chat.
47
+ */
48
+ export const BACKEND_HOSTS = ['api2.cursor.sh'];
49
+
50
+ export const AGENT_HOSTS = [
46
51
  'agent.api5.cursor.sh', 'agentn.api5.cursor.sh',
47
52
  'agent-gcpp-uswest.api5.cursor.sh', 'agentn-gcpp-uswest.api5.cursor.sh',
48
53
  'agent-gcpp-eucentral.api5.cursor.sh', 'agentn-gcpp-eucentral.api5.cursor.sh',
49
54
  'agent-gcpp-apsoutheast.api5.cursor.sh', 'agentn-gcpp-apsoutheast.api5.cursor.sh',
50
- ]
55
+ ];
51
56
 
52
57
  export function isBlocked() {
53
58
  try {
@@ -77,17 +82,17 @@ function flushDnsCmd() {
77
82
  + ' || service nscd restart 2>/dev/null; true';
78
83
  }
79
84
 
80
- export function blockBackend() {
85
+ export function blockBackend(hosts = BACKEND_HOSTS) {
81
86
  // Add only the hosts NOT already present, so a prior api2-only block still gets
82
87
  // the agent/chat hosts appended (early-returning on isBlocked left them out).
83
88
  let current = '';
84
89
  try { current = fs.readFileSync(HOSTS, 'utf8'); } catch { /* new */ }
85
- const missing = BACKEND_HOSTS.filter((h) => !new RegExp(`^\\s*127\\.0\\.0\\.1\\s+${h.replace(/\./g, '\\.')}\\b`, 'm').test(current));
90
+ const missing = hosts.filter((h) => !new RegExp(`^\\s*127\\.0\\.0\\.1\\s+${h.replace(/\./g, '\\.')}\\b`, 'm').test(current));
86
91
  if (!missing.length) return { already: true };
87
92
  const entries = missing.map((h) => `127.0.0.1 ${h}`).join('\\n');
88
93
  console.log('');
89
94
  console.log('blocking the editor\'s backend so it cannot re-sync over your model list.');
90
- console.log(` hosts : ${BACKEND_HOSTS.join(', ')} -> 127.0.0.1`);
95
+ console.log(` hosts : ${hosts.join(', ')} -> 127.0.0.1`);
91
96
  console.log(` backup : ${BACKUP}`);
92
97
  console.log(' NOTE : that host also carries the editor\'s auth/usage — it may report');
93
98
  console.log(' being signed out. Undo any time with: npx openzoo unblock');
@@ -96,7 +101,7 @@ export function blockBackend() {
96
101
  if (WIN) {
97
102
  console.log(' windows: run this in an ADMINISTRATOR PowerShell, then relaunch:');
98
103
  console.log(` Copy-Item "${HOSTS}" "${BACKUP}" -ErrorAction SilentlyContinue`);
99
- for (const h of BACKEND_HOSTS) console.log(` Add-Content "${HOSTS}" "127.0.0.1 ${h}"`);
104
+ for (const h of hosts) console.log(` Add-Content "${HOSTS}" "127.0.0.1 ${h}"`);
100
105
  console.log(' ipconfig /flushdns');
101
106
  return { ok: false, manual: true, blocked: false };
102
107
  }
@@ -112,7 +117,7 @@ export function unblockBackend() {
112
117
  if (!isBlocked()) return { already: true };
113
118
  // Remove only OUR lines, never restore wholesale — the user may have edited
114
119
  // /etc/hosts for unrelated reasons since the backup was taken.
115
- const pattern = BACKEND_HOSTS.map((h) => h.replace('.', '\\.')).join('|');
120
+ const pattern = [...BACKEND_HOSTS, ...AGENT_HOSTS].map((h) => h.replace(/\./g, '\\.')).join('|');
116
121
  if (WIN) {
117
122
  console.log('windows: run in an ADMINISTRATOR PowerShell:');
118
123
  console.log(` Copy-Item "${BACKUP}" "${HOSTS}" -Force; ipconfig /flushdns`);
package/lib/launch.js CHANGED
@@ -65,8 +65,12 @@ export async function launchClaude(argv) {
65
65
  }
66
66
  if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
67
67
  }
68
- const terminal = argv.includes('--terminal') || argv.includes('-t');
69
- const rest = argv.filter((a) => a !== '--terminal' && a !== '-t');
68
+ // TERMINAL (Claude Code CLI) IS THE DEFAULT — it is the guaranteed-x402 path
69
+ // and honours ANTHROPIC_BASE_URL. --desktop explicitly opens the desktop app
70
+ // (which is OAuth/subscription-bound and may ignore our endpoint).
71
+ const wantDesktop = argv.includes('--desktop');
72
+ const terminal = !wantDesktop;
73
+ const rest = argv.filter((a) => !['--terminal', '-t', '--desktop'].includes(a));
70
74
  const env = {
71
75
  ...process.env,
72
76
  ANTHROPIC_BASE_URL: base,
package/lib/proxy.js CHANGED
@@ -280,6 +280,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
280
280
  // lines / payment receipts there corrupts that program's output (observed: the
281
281
  // Solana receipt leaking into the Claude Code CLI). When silent, route this
282
282
  // channel to a log file instead; only print to the console when we own it.
283
+ let paidCalls = 0;
283
284
  let sayFile = null;
284
285
  if (silent) {
285
286
  try {
@@ -611,6 +612,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
611
612
  if (requireToken) say(`${line} · session $${sessionSpent.toFixed(6)}`);
612
613
  else if (viaTunnel) say(`${line} · public-url session $${tunnelSpent.toFixed(6)}`);
613
614
  else say(line);
615
+ // ALWAYS-ON SPEND, TUI-SAFE. When a harness owns the terminal (silent),
616
+ // the receipt lines go to a file (they corrupt a TUI). But the running
617
+ // total should still be visible — so write it to the terminal TITLE via
618
+ // an OSC escape, which updates the window/tab title without touching the
619
+ // TUI's content. `openzoo ● $0.0042 · 12 calls` in the title bar, live.
620
+ if (receipt.ok && typeof receipt.billedUsd === 'number') { paidCalls += 1; }
621
+ if (sayFile) {
622
+ try { process.stderr.write(`]0;openzoo ● $${sessionSpent.toFixed(4)} · ${paidCalls} call${paidCalls === 1 ? '' : 's'}`); } catch { /* no tty */ }
623
+ }
614
624
  scheduleRefresh(4000); // settlement lands on-chain in a few seconds
615
625
  }
616
626
  // Chat completions come back as one JSON object (settle-before-serve).
package/lib/setup.js CHANGED
@@ -443,8 +443,14 @@ export async function setupEditor(which, target) {
443
443
  //
444
444
  // --no-block opts out for anyone who would rather keep the vendor catalog.
445
445
  if (target0 === 'cursor' && !process.argv.includes('--no-block')) {
446
- const { blockBackend, isBlocked } = await import('./hosts.js');
447
- const r = blockBackend();
446
+ const { blockBackend, isBlocked, BACKEND_HOSTS, AGENT_HOSTS } = await import('./hosts.js');
447
+ // Default: block ONLY the model-list re-sync host (safe for subbed users).
448
+ // Under --takeover we also block the chat-inference hosts, because only then
449
+ // does the impersonation backend answer their chat — blocking them otherwise
450
+ // just severs Cursor ("Reconnecting...").
451
+ const hostsToBlock = process.argv.includes('--takeover')
452
+ ? [...BACKEND_HOSTS, ...AGENT_HOSTS] : BACKEND_HOSTS;
453
+ const r = blockBackend(hostsToBlock);
448
454
  if (r.already) console.log('backend: already blocked (required for routing; npx openzoo unblock to undo)');
449
455
  else if (isBlocked()) console.log('backend: blocked -> 127.0.0.1 (this is what forces the editor onto the zoo)');
450
456
  else console.log('backend: NOT blocked — the editor will keep using its own backend and\n nothing will reach the zoo. Re-run and enter your password.');
@@ -556,7 +562,18 @@ export async function setupEditor(which, target) {
556
562
  const args = [cwd];
557
563
  // Trust our self-signed impersonation cert without any CA install — this flag
558
564
  // is the entire reason no trust prompt is needed. Only added under --takeover.
559
- if (doTakeover) args.unshift('--ignore-certificate-errors');
565
+ if (doTakeover) {
566
+ // DEFEAT DNS-OVER-HTTPS. /etc/hosts is ignored by Chromium's Secure DNS, so
567
+ // the agent chat connection (network-service, DoH) escaped even with every
568
+ // host blocked — measured: 0 backend lines on chat while api2 (renderer
569
+ // fetch, OS resolver) hit us fine. --host-resolver-rules overrides Chromium's
570
+ // resolver directly, DoH included, and takes a WILDCARD so every region /
571
+ // privacy variant of *.api5.cursor.sh maps to us without enumerating them.
572
+ args.unshift(
573
+ '--ignore-certificate-errors',
574
+ '--host-resolver-rules=MAP api2.cursor.sh 127.0.0.1,MAP *.api5.cursor.sh 127.0.0.1',
575
+ );
576
+ }
560
577
  if (useProfile) {
561
578
  fs.mkdirSync(PROFILE_DIR, { recursive: true });
562
579
  args.unshift(`--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${path.join(PROFILE_DIR, 'extensions')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.32.3",
3
+ "version": "0.33.1",
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",