openzoo 0.30.2 → 0.30.4

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/cursorcfg.js CHANGED
@@ -306,3 +306,31 @@ export function unpinEditorProviderConfig(which) {
306
306
  try { sqlite(db, 'DROP TRIGGER IF EXISTS openzoo_pin;'); return { unpinned: true }; }
307
307
  catch (e) { return { error: e.message }; }
308
308
  }
309
+
310
+ /**
311
+ * Force the CACHED membership the editor stores in its own auth table to an
312
+ * entitled value. MEASURED: the working (ultra) machine held
313
+ * `cursorAuth/stripeMembershipType = "ultra"` right here in ItemTable, the free
314
+ * machine held "free" — and the model-selection gate reads this cached value,
315
+ * NOT the api2 responses (which we already answer entitled, to no effect). The
316
+ * api2 impersonation's job is then to stop the editor RE-SYNCING this back to
317
+ * free on the next focus. Best-effort; returns what it set.
318
+ */
319
+ export function forceMembership(which, type = 'pro') {
320
+ const db = storagePath(which);
321
+ if (!db || !fs.existsSync(db)) return { error: 'no editor db' };
322
+ try { execFileSync('sqlite3', ['-version'], { stdio: 'ignore' }); } catch { return { error: 'sqlite3 not available' }; }
323
+ const q = (k) => `'${String(k).replace(/'/g, "''")}'`;
324
+ const set = (key, val) => {
325
+ // These auth values are stored as raw JSON strings (quoted), e.g. "ultra".
326
+ const json = JSON.stringify(String(val));
327
+ sqlite(db, `INSERT INTO ItemTable(key,value) VALUES(${q(key)},${q(json)}) `
328
+ + `ON CONFLICT(key) DO UPDATE SET value=${q(json)};`);
329
+ };
330
+ try {
331
+ set('cursorAuth/stripeMembershipType', type);
332
+ set('cursorAuth/stripeSubscriptionStatus', 'active');
333
+ const got = sqlite(db, `SELECT value FROM ItemTable WHERE key='cursorAuth/stripeMembershipType';`).trim();
334
+ return { set: type, verified: got };
335
+ } catch (e) { return { error: e.message }; }
336
+ }
package/lib/launch.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * harness with the two env vars set. The proxy must already be running
10
10
  * (`npx openzoo` in another terminal); we check first and say so if not.
11
11
  */
12
- import { spawn } from 'node:child_process';
12
+ import { spawn, spawnSync } from 'node:child_process';
13
13
  import fs from 'node:fs';
14
14
  import os from 'node:os';
15
15
  import path from 'node:path';
@@ -84,29 +84,29 @@ export async function launchClaude(argv) {
84
84
  return;
85
85
  }
86
86
 
87
- // DESKTOP: launch via `open`, NOT by spawning the bundle binary.
87
+ // DESKTOP, ROUTED THROUGH THE ZOO.
88
88
  //
89
- // Spawning /Applications/Claude.app/Contents/MacOS/Claude directly breaks
90
- // macOS Launch Services the app detects it was not started properly and
91
- // force-quits (observed). `open -a` is the correct, reliable way to start a
92
- // .app; the tradeoff is it hands off to launchd and drops our env, so the
93
- // desktop app cannot be pointed at the zoo this way. It is a CHAT app, not
94
- // Claude Code, and does not honour ANTHROPIC_BASE_URL regardless — so the
95
- // routing was never going to happen here. For x402 routing use --terminal
96
- // (Claude Code CLI), which is why we say so loudly.
89
+ // The force-quit was NOT Launch Services — it was the single-instance lock:
90
+ // spawning the bundle binary while a Claude instance is already running makes
91
+ // the new process detect the existing one and immediately exit ("force
92
+ // quited"). `open -a` dodges that but hands off to launchd and DROPS our env,
93
+ // so ANTHROPIC_BASE_URL never reaches the app and it cannot route.
94
+ //
95
+ // To get BOTH no force-quit AND our env quit any running instance first,
96
+ // then spawn the bundle binary directly with the env set. Whether the desktop
97
+ // app honours ANTHROPIC_BASE_URL is up to the app; this at least gives it the
98
+ // variable, which `open` never could.
99
+ const app = resolveClaudeDesktop();
100
+ if (!app) { console.error('openzoo: Claude desktop app not found — install it, or use `npx openzoo claude --terminal`'); process.exit(1); }
97
101
  if (process.platform === 'darwin') {
98
- console.error('openzoo: opening the Claude DESKTOP app.');
99
- console.error(' it will NOT route through the zoo the desktop app ignores ANTHROPIC_BASE_URL.');
100
- console.error(' for x402-paid inference use: npx openzoo claude --terminal');
101
- const child = spawn('open', ['-a', 'Claude', ...(rest.length ? ['--args', ...rest] : [])], { stdio: 'ignore', detached: true });
102
- child.on('error', () => console.error('openzoo: Claude desktop app not found — install it, or use --terminal'));
103
- child.unref();
104
- return;
102
+ // Quit a running instance so the fresh one is not killed by the single-
103
+ // instance lock. `osascript quit` is graceful; pkill is the backstop.
104
+ try { spawnSync('osascript', ['-e', 'tell application "Claude" to quit'], { stdio: 'ignore', timeout: 4000 }); } catch { /* not running */ }
105
+ try { spawnSync('pkill', ['-x', 'Claude'], { stdio: 'ignore' }); } catch { /* already gone */ }
106
+ await new Promise((r) => setTimeout(r, 800));
105
107
  }
106
- // Non-macOS: spawn the resolved binary (Launch Services is a mac concept).
107
- const app = resolveClaudeDesktop();
108
- if (!app) { console.error('openzoo: Claude desktop app not found — use `npx openzoo claude --terminal` for the CLI'); process.exit(1); }
109
- console.error(`openzoo: Claude desktop (ANTHROPIC_BASE_URL=${base}); --terminal is the guaranteed-x402 path.`);
108
+ console.error(`openzoo: Claude desktop with ANTHROPIC_BASE_URL=${base} (quit any running instance first).`);
109
+ console.error(' if the desktop app ignores that variable, use --terminal for guaranteed x402.');
110
110
  const child = spawn(app, rest, { stdio: 'ignore', env, detached: true });
111
111
  child.on('error', (e) => console.error(`openzoo: could not launch Claude desktop: ${e.message}`));
112
112
  child.unref();
package/lib/setup.js CHANGED
@@ -22,7 +22,7 @@ import { spawn } from 'node:child_process';
22
22
  import { config } from './config.js';
23
23
  import {
24
24
  writeEditorProviderConfig, editorRunning, quitEditor,
25
- pinEditorProviderConfig, unpinEditorProviderConfig,
25
+ pinEditorProviderConfig, unpinEditorProviderConfig, forceMembership,
26
26
  } from './cursorcfg.js';
27
27
 
28
28
  /**
@@ -503,6 +503,14 @@ export async function setupEditor(which, target) {
503
503
  + ' The 443->8443 redirect is not delivering (pfctl/loopback). Tell me and I switch to a root-bound 443.');
504
504
  } catch (e) { console.log(`takeover: self-test error (${e.message})`); }
505
505
 
506
+ // WRITE THE CACHED MEMBERSHIP the model-gate actually reads (measured: the
507
+ // ultra machine held "ultra" here; api2 responses alone did not move the
508
+ // gate). The impersonation keeps it from re-syncing back to free.
509
+ try {
510
+ const fm = forceMembership(target0, process.env.OPENZOO_MEMBERSHIP || 'pro');
511
+ console.log(fm.error ? `takeover: membership write skipped (${fm.error})`
512
+ : `takeover: cached membership -> ${fm.verified} (was gating model selection)`);
513
+ } catch (e) { console.log(`takeover: membership write failed (${e.message})`); }
506
514
  console.log('takeover: launching with --ignore-certificate-errors so the self-signed cert is trusted (no CA install)');
507
515
  } catch (e) {
508
516
  console.log(`takeover: failed to start (${e.message}) — falling back to plain routing`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.30.2",
3
+ "version": "0.30.4",
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",