openzoo 0.33.4 → 0.34.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.
@@ -272,6 +272,32 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
272
272
  const method = full.split('/').filter(Boolean).pop() || '';
273
273
  const ct = req.headers['content-type'] || '?';
274
274
  const body = await readBody(req);
275
+ // CLAUDE DESKTOP: api.anthropic.com speaks the JSON Messages API our proxy
276
+ // already handles. If this is an Anthropic host / messages call, forward it
277
+ // verbatim to the local paying proxy and relay the response — reusing the
278
+ // whole translate+x402 path. (SNI is checked because the Host header may be
279
+ // absent on h2.)
280
+ const host = String(req.headers[':authority'] || req.headers.host || req.socket?.servername || '');
281
+ if (/anthropic\.com$/.test(host) || /\/v1\/messages\b/.test(full)) {
282
+ try {
283
+ const up = await fetch(`http://127.0.0.1:8402${full}`, {
284
+ method: req.method,
285
+ headers: { 'content-type': req.headers['content-type'] || 'application/json' },
286
+ body: (req.method === 'GET' || req.method === 'HEAD') ? undefined : body,
287
+ });
288
+ const buf = Buffer.from(await up.arrayBuffer());
289
+ const h = { ...CORS };
290
+ up.headers.forEach((v, k) => { if (!['content-encoding', 'transfer-encoding', 'connection'].includes(k)) h[k] = v; });
291
+ res.writeHead(up.status, h);
292
+ res.end(buf);
293
+ log(`cursor-backend: #${conns} ${req.method} ${full} ANTHROPIC -> proxy (${up.status}, ${buf.length}b)`);
294
+ } catch (e) {
295
+ res.writeHead(502, { 'content-type': 'application/json', ...CORS });
296
+ res.end(JSON.stringify({ type: 'error', error: { type: 'api_error', message: `openzoo forward failed: ${e.message}` } }));
297
+ log(`cursor-backend: anthropic forward failed: ${e.message}`);
298
+ }
299
+ return;
300
+ }
275
301
  log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
276
302
  // CAPTURE the chat inference request so its schema can be decoded from
277
303
  // REAL bytes (Auto's StreamUnifiedChat). Written once; inspect then build.
package/lib/hosts.js CHANGED
@@ -47,6 +47,10 @@ const MARK = '# openzoo: force the editor onto the local proxy';
47
47
  */
48
48
  export const BACKEND_HOSTS = ['api2.cursor.sh'];
49
49
 
50
+ // Claude DESKTOP inference host — impersonated + forwarded to the local proxy
51
+ // (Messages API is JSON; the proxy already translates + pays x402).
52
+ export const CLAUDE_HOSTS = ['api.anthropic.com', 'api-staging.anthropic.com'];
53
+
50
54
  export const AGENT_HOSTS = [
51
55
  'agent.api5.cursor.sh', 'agentn.api5.cursor.sh',
52
56
  'agent-gcpp-uswest.api5.cursor.sh', 'agentn-gcpp-uswest.api5.cursor.sh',
package/lib/launch.js CHANGED
@@ -152,16 +152,53 @@ export async function launchClaude(argv) {
152
152
  // variable, which `open` never could.
153
153
  const app = resolveClaudeDesktop();
154
154
  if (!app) { console.error('openzoo: Claude desktop app not found — install it, or use `npx openzoo claude --terminal`'); process.exit(1); }
155
+
156
+ // ROUTE THE DESKTOP APP BY IMPERSONATING api.anthropic.com.
157
+ //
158
+ // The desktop app is OAuth/subscription-bound and ignores ANTHROPIC_BASE_URL,
159
+ // so env cannot point it at us. Instead we ARE api.anthropic.com: block it in
160
+ // /etc/hosts, bind our backend on 443 (it forwards /v1/messages to the local
161
+ // proxy, which translates + pays x402), and launch the app with the Chromium
162
+ // flags that (a) accept our self-signed cert and (b) override DNS past DoH.
163
+ // Opt out with --no-intercept (then it just opens, on the subscription).
164
+ if (process.platform !== 'win32' && !argv.includes('--no-intercept')) {
165
+ try {
166
+ const { blockBackend, bindBackend443, CLAUDE_HOSTS } = await import('./hosts.js');
167
+ const { ensureCert } = await import('./cursorbackend.js');
168
+ ensureCert(console.error);
169
+ const r = blockBackend(CLAUDE_HOSTS);
170
+ console.error(r.already ? 'openzoo: api.anthropic.com already routed to us'
171
+ : `openzoo: routing ${CLAUDE_HOSTS.join(', ')} -> 127.0.0.1 (sudo)`);
172
+ const modelsFile = path.join(os.tmpdir(), 'openzoo-claude-models.json');
173
+ fs.writeFileSync(modelsFile, '[]');
174
+ const backendLog = path.join(os.homedir(), '.openzoo', 'cursor-backend.log');
175
+ bindBackend443(modelsFile, backendLog, console.error);
176
+ await new Promise((res) => setTimeout(res, 800));
177
+ console.error('openzoo: backend bound on :443 — Claude desktop inference now forwards to the zoo.');
178
+ console.error(' undo any time with: npx openzoo unblock');
179
+ } catch (e) { console.error(`openzoo: desktop interception setup failed (${e.message}) — opening app plain`); }
180
+ }
181
+
155
182
  if (process.platform === 'darwin') {
156
- // Quit a running instance so the fresh one is not killed by the single-
157
- // instance lock. `osascript quit` is graceful; pkill is the backstop.
183
+ // Quit a running instance and WAIT until it is actually gone. A fixed 800ms
184
+ // wait raced the single-instance lock Claude was still releasing its file
185
+ // locks (LOCK errors), so the fresh spawn saw a live instance and exited
186
+ // immediately ("didn't launch at all"). Poll pgrep until clear, up to ~6s.
158
187
  try { spawnSync('osascript', ['-e', 'tell application "Claude" to quit'], { stdio: 'ignore', timeout: 4000 }); } catch { /* not running */ }
159
- try { spawnSync('pkill', ['-x', 'Claude'], { stdio: 'ignore' }); } catch { /* already gone */ }
160
- await new Promise((r) => setTimeout(r, 800));
188
+ for (let i = 0; i < 24; i++) {
189
+ const r = spawnSync('pgrep', ['-x', 'Claude'], { encoding: 'utf8' });
190
+ if (!r.stdout || !r.stdout.trim()) break; // gone
191
+ if (i === 8) { try { spawnSync('pkill', ['-9', '-x', 'Claude'], { stdio: 'ignore' }); } catch { /* */ } }
192
+ await new Promise((res) => setTimeout(res, 250));
193
+ }
194
+ await new Promise((r) => setTimeout(r, 400)); // let the lock file release
161
195
  }
162
- console.error(`openzoo: Claude desktop with ANTHROPIC_BASE_URL=${base} (quit any running instance first).`);
163
- console.error(' if the desktop app ignores that variable, use --terminal for guaranteed x402.');
164
- const child = spawn(app, rest, { stdio: 'ignore', env, detached: true });
196
+ // Chromium flags: accept the self-signed cert, and MAP the Anthropic hosts to
197
+ // us at the resolver level (defeats DoH, which ignores /etc/hosts).
198
+ const flags = argv.includes('--no-intercept') ? []
199
+ : ['--ignore-certificate-errors', '--host-resolver-rules=MAP api.anthropic.com 127.0.0.1,MAP api-staging.anthropic.com 127.0.0.1'];
200
+ console.error('openzoo: launching Claude desktop — inference routes through the zoo (pays x402).');
201
+ const child = spawn(app, [...flags, ...rest], { stdio: 'ignore', env, detached: true });
165
202
  child.on('error', (e) => console.error(`openzoo: could not launch Claude desktop: ${e.message}`));
166
203
  child.unref();
167
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.33.4",
3
+ "version": "0.34.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",