openzoo 0.29.9 → 0.30.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,6 +19,8 @@ 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
23
+ by default, --terminal for the Claude Code CLI
22
24
  npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
23
25
  (claude, aider...) already pointed at the zoo
24
26
  npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_bind, zoo_models, zoo_wallet, zoo_contexts)
@@ -69,6 +71,9 @@ async function main() {
69
71
  case 'mcp':
70
72
  await (await import('../lib/mcp.js')).startMcp();
71
73
  break;
74
+ case 'claude':
75
+ await (await import('../lib/launch.js')).launchClaude(process.argv.slice(3));
76
+ break;
72
77
  case 'launch': {
73
78
  const rest = process.argv.slice(3);
74
79
  const [harness, hargs] = [rest[0], rest.slice(1)];
package/lib/cursorapi.js CHANGED
@@ -92,6 +92,59 @@ export function encodeAvailableModels(models) {
92
92
  return b.done();
93
93
  }
94
94
 
95
+ /**
96
+ * ENTITLEMENT RESPONSES. Measured from the backend log: once the transport works
97
+ * (NODE_TLS_REJECT_UNAUTHORIZED=0), the editor calls a set of DashboardService/
98
+ * AiService gRPC methods to decide the plan. Returning EMPTY reads as free tier —
99
+ * the "upgrade" prompt. These populate the exact protobuf shapes (read from the
100
+ * editor's own bundle) with an active pro plan and a real identity.
101
+ */
102
+
103
+ // GetPlanInfoResponse{ 1 plan_info: PlanInfo{ 1 plan_name, 2 included_amount_cents, 5 plan_owner } }
104
+ export function encodeGetPlanInfo() {
105
+ const planInfo = new Buf()
106
+ .str(1, 'pro') // plan_name
107
+ .int(2, 99999900) // included_amount_cents (huge headroom)
108
+ .int(5, 1); // plan_owner = PLAN_OWNER_STRIPE
109
+ return new Buf().msg(1, planInfo).done();
110
+ }
111
+
112
+ // GetMeResponse{ 1 auth_id, 2 user_id, 3 email, 9 is_enterprise_user }
113
+ export function encodeGetMe() {
114
+ return new Buf()
115
+ .str(1, 'openzoo-user')
116
+ .int(2, 1)
117
+ .str(3, 'user@openzoo.local')
118
+ .bool(9, false)
119
+ .done();
120
+ }
121
+
122
+ // GetDefaultModelResponse{ 1 model, 2 thinking_model, 3 max_mode, 4 next_default_set_date }
123
+ export function encodeGetDefaultModel(models) {
124
+ const m = (models && models[0] && models[0].name) || 'gpt-4o';
125
+ return new Buf().str(1, m).str(2, m).bool(3, false).str(4, '').done();
126
+ }
127
+
128
+ // IsOnNewPricingResponse{ 1 is_on_new_pricing, 2 is_opted_out, 3 has_auto_spillover, 5 ... }
129
+ export function encodeIsOnNewPricing() {
130
+ return new Buf().bool(1, false).bool(2, false).bool(3, false).bool(5, false).done();
131
+ }
132
+
133
+ /**
134
+ * The bare proto body for a method, or null to fall through to empty-ok.
135
+ * Only the methods that gate entitlement or the model list are populated.
136
+ */
137
+ export function encodeForMethod(method, models) {
138
+ switch (method) {
139
+ case 'AvailableModels': return encodeAvailableModels(models);
140
+ case 'GetPlanInfo': return encodeGetPlanInfo();
141
+ case 'GetMe': return encodeGetMe();
142
+ case 'GetDefaultModel': return encodeGetDefaultModel(models);
143
+ case 'IsOnNewPricing': return encodeIsOnNewPricing();
144
+ default: return null;
145
+ }
146
+ }
147
+
95
148
  /** Connect unary framing: 5-byte prefix (flags + big-endian length) + payload. */
96
149
  export function connectFrame(payload) {
97
150
  const head = Buffer.alloc(5);
@@ -31,7 +31,7 @@ import fs from 'node:fs';
31
31
  import os from 'node:os';
32
32
  import path from 'node:path';
33
33
  import { execFileSync } from 'node:child_process';
34
- import { encodeAvailableModels } from './cursorapi.js';
34
+ import { encodeAvailableModels, encodeForMethod } from './cursorapi.js';
35
35
 
36
36
  const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
37
37
  const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
@@ -134,7 +134,7 @@ function respond(req, res, method, models) {
134
134
  return;
135
135
  }
136
136
 
137
- const bodyProto = method === 'AvailableModels' ? encodeAvailableModels(models) : Buffer.alloc(0);
137
+ const bodyProto = encodeForMethod(method, models) || Buffer.alloc(0);
138
138
 
139
139
  if (isJson) {
140
140
  // Connect unary JSON. AvailableModels as JSON; everything else an empty object.
@@ -196,8 +196,10 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
196
196
  log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
197
197
  try {
198
198
  respond(req, res, method, models);
199
+ const populated = ['GetPlanInfo', 'GetMe', 'GetDefaultModel', 'IsOnNewPricing'];
199
200
  const what = /stripe|membership|subscription/i.test(full) ? 'ENTITLED stripe profile'
200
201
  : method === 'AvailableModels' ? `AvailableModels (${models.length} models, gates open)`
202
+ : populated.includes(method) ? `${method} (PRO/entitled)`
201
203
  : req.method === 'OPTIONS' ? 'CORS preflight 204'
202
204
  : 'empty-ok';
203
205
  log(`cursor-backend: -> ${what}`);
package/lib/launch.js CHANGED
@@ -10,8 +10,90 @@
10
10
  * (`npx openzoo` in another terminal); we check first and say so if not.
11
11
  */
12
12
  import { spawn } from 'node:child_process';
13
+ import fs from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
13
16
  import { config } from './config.js';
14
17
 
18
+ /** Resolve the Claude DESKTOP app binary, platform-agnostically. Spawn the
19
+ * binary directly (not `open -a`) so the env — ANTHROPIC_BASE_URL — survives;
20
+ * macOS `open` hands off to launchd and drops it. */
21
+ function resolveClaudeDesktop() {
22
+ const bundles = [
23
+ '/Applications/Claude.app/Contents/MacOS/Claude',
24
+ path.join(os.homedir(), 'Applications', 'Claude.app', 'Contents', 'MacOS', 'Claude'),
25
+ ];
26
+ for (const b of bundles) { try { fs.accessSync(b, fs.constants.X_OK); return b; } catch { /* next */ } }
27
+ // Linux/Windows or PATH install.
28
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
29
+ for (const n of ['claude-desktop', 'Claude']) {
30
+ const f = path.join(dir, n + (process.platform === 'win32' ? '.exe' : ''));
31
+ try { fs.accessSync(f, fs.constants.X_OK); return f; } catch { /* next */ }
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /** Resolve the Claude Code TERMINAL CLI. */
38
+ function resolveClaudeCli() {
39
+ for (const dir of (process.env.PATH || '').split(path.delimiter)) {
40
+ const f = path.join(dir, 'claude' + (process.platform === 'win32' ? '.cmd' : ''));
41
+ try { fs.accessSync(f, fs.constants.X_OK); return f; } catch { /* next */ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * `npx openzoo claude [dir] [--terminal]` — launch Claude on the zoo.
48
+ * DEFAULT is the desktop app; `--terminal` (or `-t`) runs the Claude Code CLI.
49
+ * Both get ANTHROPIC_BASE_URL so inference pays x402.
50
+ */
51
+ export async function launchClaude(argv) {
52
+ const base = `http://localhost:${config.port}/v1`;
53
+ // AUTO-START THE PROXY. One command should just work — if nothing is listening,
54
+ // boot the proxy in THIS process (it stays alive because claude runs in the
55
+ // foreground below), rather than making the user run `npx openzoo` first.
56
+ let up = false;
57
+ try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(3000) })).ok; } catch { up = false; }
58
+ if (!up) {
59
+ console.error('openzoo: starting the proxy in the background...');
60
+ const { startProxy } = await import('./proxy.js');
61
+ await startProxy({ silent: true, autoTunnel: true });
62
+ for (let i = 0; i < 20 && !up; i++) {
63
+ await new Promise((r) => setTimeout(r, 300));
64
+ try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* keep waiting */ }
65
+ }
66
+ if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
67
+ }
68
+ const terminal = argv.includes('--terminal') || argv.includes('-t');
69
+ const rest = argv.filter((a) => a !== '--terminal' && a !== '-t');
70
+ const env = {
71
+ ...process.env,
72
+ ANTHROPIC_BASE_URL: base,
73
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || 'sk-openzoo',
74
+ ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || 'sk-openzoo',
75
+ };
76
+
77
+ if (terminal) {
78
+ const cli = resolveClaudeCli();
79
+ if (!cli) { console.error('openzoo: `claude` CLI not found on PATH — install Claude Code, or drop --terminal for the desktop app'); process.exit(1); }
80
+ console.error(`openzoo: Claude Code (terminal) on the zoo — every turn pays x402`);
81
+ const child = spawn(cli, rest, { stdio: 'inherit', env });
82
+ child.on('exit', (c) => process.exit(c ?? 0));
83
+ child.on('error', (e) => { console.error(`openzoo: could not launch claude: ${e.message}`); process.exit(1); });
84
+ return;
85
+ }
86
+
87
+ const app = resolveClaudeDesktop();
88
+ if (!app) { console.error('openzoo: Claude desktop app not found — use `npx openzoo claude --terminal` for the CLI'); process.exit(1); }
89
+ console.error(`openzoo: Claude desktop on the zoo (ANTHROPIC_BASE_URL=${base})`);
90
+ console.error(' note: the desktop app routes through the zoo only if it honours ANTHROPIC_BASE_URL;');
91
+ console.error(' --terminal (Claude Code CLI) is the guaranteed-x402 path.');
92
+ const child = spawn(app, rest, { stdio: 'ignore', env, detached: true });
93
+ child.on('error', (e) => console.error(`openzoo: could not launch Claude desktop: ${e.message}`));
94
+ child.unref();
95
+ }
96
+
15
97
  export async function launchHarness(cmd, args) {
16
98
  const base = `http://localhost:${config.port}/v1`;
17
99
  // Fail early with a clear message rather than letting the harness spew
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.29.9",
3
+ "version": "0.30.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",