openzoo 0.16.0 → 0.17.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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Write Cursor / VS Code-fork provider settings directly.
3
+ *
4
+ * These live as PLAIN JSON in the editor's globalStorage SQLite (`state.vscdb`,
5
+ * table ItemTable), under the reactive-storage `applicationUser` blob — NOT in
6
+ * an encrypted store. The fields that matter:
7
+ * openAIBaseUrl the "Override OpenAI Base URL" box
8
+ * useOpenAIKey the toggle next to it
9
+ * availableAPIKeyModels the custom model names the picker offers
10
+ *
11
+ * So the whole "paste these four things into Settings" ritual is scriptable,
12
+ * and `openzoo cursor` should just do it. The editor must be CLOSED while we
13
+ * write, or it will overwrite us from memory on exit.
14
+ */
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { execFileSync } from 'node:child_process';
19
+
20
+ const KEY = 'src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser';
21
+
22
+ const STORAGE = {
23
+ cursor: path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
24
+ vscode: path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'state.vscdb'),
25
+ };
26
+
27
+ /** Linux/Windows put globalStorage elsewhere; resolve per platform. */
28
+ function storagePath(which) {
29
+ if (process.platform === 'darwin') return STORAGE[which];
30
+ const dir = which === 'vscode' ? 'Code' : 'Cursor';
31
+ if (process.platform === 'win32') {
32
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), dir, 'User', 'globalStorage', 'state.vscdb');
33
+ }
34
+ return path.join(os.homedir(), '.config', dir, 'User', 'globalStorage', 'state.vscdb');
35
+ }
36
+
37
+ const sqlite = (db, sql) => execFileSync('sqlite3', [db, sql], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
38
+
39
+ /** True when the editor process is running — writing under it gets clobbered. */
40
+ export function editorRunning(which) {
41
+ try {
42
+ const name = which === 'vscode' ? 'Visual Studio Code' : 'Cursor';
43
+ const out = execFileSync('pgrep', ['-f', name], { encoding: 'utf8' });
44
+ return out.trim().length > 0;
45
+ } catch { return false; }
46
+ }
47
+
48
+ /**
49
+ * Point the editor at the zoo: base URL, key toggle, and the models the picker
50
+ * should offer. Returns what changed, or null when the store is unavailable
51
+ * (a fresh install with no globalStorage yet).
52
+ */
53
+ export function writeEditorProviderConfig(which, { baseUrl, models }) {
54
+ const db = storagePath(which);
55
+ if (!fs.existsSync(db)) return null;
56
+ try { execFileSync('sqlite3', ['-version'], { stdio: 'ignore' }); } catch { return { error: 'sqlite3 not available' }; }
57
+
58
+ const raw = sqlite(db, `SELECT value FROM ItemTable WHERE key='${KEY}';`).trim();
59
+ if (!raw) return null;
60
+ let doc;
61
+ try { doc = JSON.parse(raw); } catch { return { error: 'could not parse editor config' }; }
62
+
63
+ const before = { openAIBaseUrl: doc.openAIBaseUrl, models: (doc.availableAPIKeyModels || []).length };
64
+ doc.openAIBaseUrl = baseUrl;
65
+ doc.useOpenAIKey = true;
66
+ // Merge, don't clobber: a user may have their own custom models listed.
67
+ const existing = Array.isArray(doc.availableAPIKeyModels) ? doc.availableAPIKeyModels : [];
68
+ const names = new Set(existing.map((m) => (typeof m === 'string' ? m : m?.name)).filter(Boolean));
69
+ const added = [];
70
+ for (const m of models) {
71
+ if (names.has(m)) continue;
72
+ // Match the shape already present, so the picker renders it correctly.
73
+ existing.push(typeof existing[0] === 'string' ? m : { name: m, defaultOn: true, supportsAgent: true });
74
+ names.add(m);
75
+ added.push(m);
76
+ }
77
+ doc.availableAPIKeyModels = existing;
78
+
79
+ // Single-quote escaping for the SQL literal.
80
+ const json = JSON.stringify(doc).replace(/'/g, "''");
81
+ sqlite(db, `UPDATE ItemTable SET value='${json}' WHERE key='${KEY}';`);
82
+ return { db, before, baseUrl, added };
83
+ }
package/lib/setup.js CHANGED
@@ -20,6 +20,9 @@ import os from 'node:os';
20
20
  import path from 'node:path';
21
21
  import { spawn } from 'node:child_process';
22
22
  import { config } from './config.js';
23
+ import { writeEditorProviderConfig, editorRunning } from './cursorcfg.js';
24
+
25
+ const DEFAULT_MODELS = ['anthropic/claude-opus-5', 'deepseek/deepseek-v4-pro-0813'];
23
26
 
24
27
  const MCP_FILES = {
25
28
  cursor: path.join(os.homedir(), '.cursor', 'mcp.json'),
@@ -155,19 +158,26 @@ export async function setupEditor(which, target) {
155
158
  // CUSTOM model under the OpenAI override, where the proxy serves it and maps
156
159
  // the name. Env alone cannot do this; say so plainly instead of implying the
157
160
  // launch handled everything.
158
- console.log('one manual step Cursor Settings Models (built-ins bypass the zoo):');
159
- console.log(` 1. Override OpenAI Base URL -> ${base}`);
160
- console.log(' 2. OpenAI API Key -> sk-openzoo (any value; x402 pays)');
161
- console.log(' 3. Add model -> anthropic/claude-opus-5');
162
- console.log(' (or deepseek/deepseek-v4-pro-0813 ~34x cheaper output)');
163
- console.log(' 4. Toggle OFF the built-in models (Opus 5 / GPT / Composer) — those');
164
- console.log(' resolve against Cursor\'s backend and never touch the zoo.');
165
- console.log(' The embedded terminal + Claude Code extension DO inherit the env above.');
166
- if (publicUrl) {
167
- console.log(`tunnel: ${publicUrl}/v1 api_key ${tunnelKey}`);
168
- console.log(' (use the tunnel for any cloud-run harness — it cannot reach localhost)');
161
+ // WRITE THE PROVIDER SETTINGS. These are plain JSON in the editor's
162
+ // globalStorage sqlite not an encrypted store, as previously assumed — so
163
+ // the "paste four things into Settings" ritual is unnecessary. Must happen
164
+ // while the editor is CLOSED or it rewrites them from memory on exit.
165
+ const picked0 = pickEditor(which);
166
+ const target0 = picked0?.which || which || 'cursor';
167
+ if (editorRunning(target0)) {
168
+ console.log(`NOTE: ${target0} is already running quit it and re-run so settings stick.`);
169
+ }
170
+ const models = [DEFAULT_MODELS[0], ...DEFAULT_MODELS.slice(1)];
171
+ let wrote = null;
172
+ try { wrote = writeEditorProviderConfig(target0, { baseUrl: base, models }); } catch (e) { wrote = { error: e.message }; }
173
+ if (wrote?.error) {
174
+ console.log(`settings: could not write automatically (${wrote.error}) — set them in Settings → Models`);
175
+ } else if (wrote) {
176
+ console.log(`settings: openAIBaseUrl -> ${base} (was ${wrote.before.openAIBaseUrl || 'unset'})`);
177
+ console.log(` useOpenAIKey -> true`);
178
+ console.log(` models added -> ${wrote.added.length ? wrote.added.join(', ') : '(already present)'}`);
179
+ console.log(' pick one of those in the model dropdown; built-ins bypass the zoo.');
169
180
  }
170
- console.log('');
171
181
 
172
182
  // 3. LAUNCH with that env. Editor resolved platform-agnostically; Cursor
173
183
  // wins when both are installed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",