openzoo 0.33.3 → 0.34.0
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 +41 -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
|
@@ -93,12 +93,16 @@ export async function launchClaude(argv) {
|
|
|
93
93
|
try {
|
|
94
94
|
const scriptPath = path.join(os.homedir(), '.openzoo', 'statusline.sh');
|
|
95
95
|
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
96
|
+
// node JSON.parse, not awk — /v1/info is pretty-printed (space after the
|
|
97
|
+
// colon) and a fixed-offset awk regex silently read 0. This is robust.
|
|
96
98
|
fs.writeFileSync(scriptPath,
|
|
97
99
|
'#!/bin/sh\n'
|
|
98
100
|
+ `curl -s --max-time 1 ${base}/info 2>/dev/null | `
|
|
99
|
-
+
|
|
100
|
-
+ '
|
|
101
|
-
+ '
|
|
101
|
+
+ `${JSON.stringify(process.execPath)} -e `
|
|
102
|
+
+ '\'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{'
|
|
103
|
+
+ 'try{const j=JSON.parse(s);'
|
|
104
|
+
+ 'process.stdout.write("\\x1b[38;5;208m\\u25cf\\x1b[0m openzoo $"+(Number(j.spendUsd)||0).toFixed(4)+" "+(j.paidCalls||0)+" call"+((j.paidCalls||0)===1?"":"s")+" \\u00b7 x402")}'
|
|
105
|
+
+ 'catch{process.stdout.write("\\x1b[38;5;208m\\u25cf\\x1b[0m openzoo \\u00b7 x402")}})\'\n');
|
|
102
106
|
fs.chmodSync(scriptPath, 0o755);
|
|
103
107
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
104
108
|
let settings = {};
|
|
@@ -148,16 +152,46 @@ export async function launchClaude(argv) {
|
|
|
148
152
|
// variable, which `open` never could.
|
|
149
153
|
const app = resolveClaudeDesktop();
|
|
150
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
|
+
|
|
151
182
|
if (process.platform === 'darwin') {
|
|
152
183
|
// Quit a running instance so the fresh one is not killed by the single-
|
|
153
|
-
// instance lock
|
|
184
|
+
// instance lock, and so it re-resolves api.anthropic.com to us on launch.
|
|
154
185
|
try { spawnSync('osascript', ['-e', 'tell application "Claude" to quit'], { stdio: 'ignore', timeout: 4000 }); } catch { /* not running */ }
|
|
155
186
|
try { spawnSync('pkill', ['-x', 'Claude'], { stdio: 'ignore' }); } catch { /* already gone */ }
|
|
156
187
|
await new Promise((r) => setTimeout(r, 800));
|
|
157
188
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
189
|
+
// Chromium flags: accept the self-signed cert, and MAP the Anthropic hosts to
|
|
190
|
+
// us at the resolver level (defeats DoH, which ignores /etc/hosts).
|
|
191
|
+
const flags = argv.includes('--no-intercept') ? []
|
|
192
|
+
: ['--ignore-certificate-errors', '--host-resolver-rules=MAP api.anthropic.com 127.0.0.1,MAP api-staging.anthropic.com 127.0.0.1'];
|
|
193
|
+
console.error('openzoo: launching Claude desktop — inference routes through the zoo (pays x402).');
|
|
194
|
+
const child = spawn(app, [...flags, ...rest], { stdio: 'ignore', env, detached: true });
|
|
161
195
|
child.on('error', (e) => console.error(`openzoo: could not launch Claude desktop: ${e.message}`));
|
|
162
196
|
child.unref();
|
|
163
197
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
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",
|