openzoo 0.15.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.
package/bin/openzoo.js CHANGED
@@ -134,7 +134,7 @@ async function main() {
134
134
  method: 'POST',
135
135
  headers,
136
136
  body: JSON.stringify({
137
- model: (mi !== -1 && process.argv[mi + 1]) || process.env.OPENZOO_DEFAULT_MODEL || 'deepseek/deepseek-v4-pro-0813',
137
+ model: (mi !== -1 && process.argv[mi + 1]) || process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
138
138
  messages: [{ role: 'user', content: question }],
139
139
  max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 1024),
140
140
  }),
@@ -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/mcp.js CHANGED
@@ -9,7 +9,11 @@ import { askWithContext, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
9
9
  import { listContexts } from './contexts.js';
10
10
 
11
11
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
12
- const DEFAULT_MODEL = process.env.OPENZOO_DEMO_MODEL || 'nvidia/nemotron-3.5-lightning';
12
+ // The model zoo_ask uses when the caller does not name one. Opus 5 by default:
13
+ // an agent calling zoo_ask is usually doing real reasoning over a bound corpus,
14
+ // and a tiny model there produces answers that look cheap because they are.
15
+ // OPENZOO_DEFAULT_MODEL (or the tool's own `model` arg) overrides.
16
+ const DEFAULT_MODEL = process.env.OPENZOO_DEFAULT_MODEL || process.env.OPENZOO_DEMO_MODEL || 'anthropic/claude-opus-5';
13
17
 
14
18
  function text(obj) {
15
19
  return { content: [{ type: 'text', text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }] };
@@ -35,6 +39,23 @@ export function buildMcpServer() {
35
39
  '',
36
40
  'Never search the machine for API keys or endpoints — there are none to find. zoo_wallet shows funding',
37
41
  'state, zoo_models lists what is served with live pricing.',
42
+ '',
43
+ // ROUTING, STATED UNCONDITIONALLY. An agent whose OWN inference is not
44
+ // routed through the proxy never receives lib/brief.js, so it answers
45
+ // "I have no visibility into the routing layer" while this very server
46
+ // is attached to it. These facts hold whether or not its own tokens
47
+ // flow through the zoo, so they belong here.
48
+ 'ROUTING — you can state these as fact:',
49
+ ` local proxy : ${config.apiBase ? `http://localhost:${config.port}/v1` : 'http://localhost:8402/v1'} (api key: any string; x402 pays, not keys)`,
50
+ ` upstream : ${config.apiBase}`,
51
+ ` default model for zoo_ask : ${DEFAULT_MODEL}`,
52
+ ' a cloud-run harness cannot reach localhost — it needs the public tunnel URL the',
53
+ ' proxy prints at startup. Call zoo_models for the live catalog and real pricing.',
54
+ '',
55
+ 'CONTEXT, PRECISELY: openzoo does NOT enlarge your own attention window. It binds a corpus',
56
+ 'server-side and retrieves what is relevant, so a body far past any model\'s window can be',
57
+ 'queried — that is retrieval, not a bigger window. Do not claim lossless compression and do',
58
+ 'not claim a 128M attention window; the honest line is "bind 128M, the model reads what matters".',
38
59
  ].join('\n'),
39
60
  });
40
61
 
@@ -103,8 +124,44 @@ export function buildMcpServer() {
103
124
  ...(reuse ? { context: reuse } : {}),
104
125
  });
105
126
  } catch (err) {
106
- if (err instanceof UnderfundedError || err instanceof QuoteTooHighError) {
107
- return { ...text(err.message), isError: true };
127
+ // PAYMENT FAILURES MUST REACH THE CHAT, ACTIONABLY. A bare throw shows an
128
+ // agent "tool failed" with no reason and no fix, so it retries blindly or
129
+ // gives up; the human never learns the wallet is empty. Every failure
130
+ // returns WHY it failed and exactly how to fund, with live addresses.
131
+ const fail = async (why, hint) => {
132
+ let balances = null; let addr = null; let evm = null;
133
+ try {
134
+ addr = client.address;
135
+ evm = client.evmAddress;
136
+ const bals = await Promise.all(
137
+ FUNDING_ASSETS.map((a) => tokenBalance(client.connection, client.keypair.publicKey, a.mint)),
138
+ );
139
+ balances = Object.fromEntries(FUNDING_ASSETS.map((a, i) => [a.symbol, bals[i].ui ?? 0]));
140
+ } catch { /* advisory — never let the diagnostic itself fail */ }
141
+ const rails = await liveRails().catch(() => null);
142
+ return {
143
+ ...text({
144
+ error: why,
145
+ x402: 'this call was NOT paid and NOT served',
146
+ detail: hint,
147
+ wallet: { solana: addr, evm },
148
+ balances,
149
+ fundWith: rails ? railFundingHint(rails.live) : 'USDC or TOKEN on Solana',
150
+ solanaMints: Object.fromEntries(FUNDING_ASSETS.map((a) => [a.symbol, a.mint])),
151
+ tellTheUser: 'Report this verbatim — the operator must fund the wallet above; you cannot fix it yourself, and retrying will fail identically until they do.',
152
+ }),
153
+ isError: true,
154
+ };
155
+ };
156
+ if (err instanceof UnderfundedError) {
157
+ return fail('x402 payment failed: wallet underfunded', err.message);
158
+ }
159
+ if (err instanceof QuoteTooHighError) {
160
+ return fail('x402 payment refused: quote above the local per-call cap', `${err.message} (raise OPENZOO_MAX_USD_PER_CALL, or ask a cheaper model)`);
161
+ }
162
+ // Anything else that mentions payment/402 still deserves the funding card.
163
+ if (/402|payment|underfunded|insufficient/i.test(err?.message || '')) {
164
+ return fail('x402 payment failed', err.message);
108
165
  }
109
166
  throw err;
110
167
  }
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'),
@@ -147,11 +150,34 @@ export async function setupEditor(which, target) {
147
150
  console.log('');
148
151
  console.log(`mcp: ${mcpFile} (openzoo: zoo_bind, zoo_ask, zoo_models, zoo_wallet, zoo_contexts)`);
149
152
  console.log(`local: ${base} api_key sk-openzoo`);
150
- if (publicUrl) {
151
- console.log(`tunnel: ${publicUrl}/v1 api_key ${tunnelKey}`);
152
- console.log(' (use the tunnel for any cloud-run harness — it cannot reach localhost)');
153
- }
154
153
  console.log('');
154
+ // THE ONE THING THE EDITOR WILL NOT INHERIT. Cursor's BUILT-IN models
155
+ // (Opus 5, GPT, Composer) go to Cursor's own backend and ignore
156
+ // ANTHROPIC_BASE_URL — Cursor has no Anthropic base-URL override, only an
157
+ // OpenAI one. So routing a Claude model through the zoo means adding it as a
158
+ // CUSTOM model under the OpenAI override, where the proxy serves it and maps
159
+ // the name. Env alone cannot do this; say so plainly instead of implying the
160
+ // launch handled everything.
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.');
180
+ }
155
181
 
156
182
  // 3. LAUNCH with that env. Editor resolved platform-agnostically; Cursor
157
183
  // wins when both are installed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.15.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",