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.
- package/lib/cursorbackend.js +26 -0
- package/lib/hosts.js +4 -0
- package/lib/launch.js +44 -7
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -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
|
|
157
|
-
// instance lock
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
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.
|
|
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",
|