openzoo 0.16.0 → 0.18.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/cursorcfg.js +92 -0
- package/lib/mcp.js +59 -0
- package/lib/proxy.js +36 -0
- package/lib/setup.js +22 -12
- package/package.json +1 -1
package/lib/cursorcfg.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
// MATCH THE APP BINARY, NOT THE WORD. `pgrep -f Cursor` matched 19
|
|
43
|
+
// processes on a machine with Cursor CLOSED: this very command
|
|
44
|
+
// (`node openzoo.js cursor` contains the word), leftover crashpad helpers,
|
|
45
|
+
// and macOS's own CursorUIViewService — so it always warned "already
|
|
46
|
+
// running" and told the user to quit an editor that was not open.
|
|
47
|
+
const needle = which === 'vscode'
|
|
48
|
+
? 'Visual Studio Code.app/Contents/MacOS/'
|
|
49
|
+
: 'Cursor.app/Contents/MacOS/Cursor';
|
|
50
|
+
const out = execFileSync('pgrep', ['-f', needle], { encoding: 'utf8' });
|
|
51
|
+
const pids = out.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
52
|
+
.filter((p) => Number(p) !== process.pid && Number(p) !== process.ppid);
|
|
53
|
+
return pids.length > 0;
|
|
54
|
+
} catch { return false; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Point the editor at the zoo: base URL, key toggle, and the models the picker
|
|
59
|
+
* should offer. Returns what changed, or null when the store is unavailable
|
|
60
|
+
* (a fresh install with no globalStorage yet).
|
|
61
|
+
*/
|
|
62
|
+
export function writeEditorProviderConfig(which, { baseUrl, models }) {
|
|
63
|
+
const db = storagePath(which);
|
|
64
|
+
if (!fs.existsSync(db)) return null;
|
|
65
|
+
try { execFileSync('sqlite3', ['-version'], { stdio: 'ignore' }); } catch { return { error: 'sqlite3 not available' }; }
|
|
66
|
+
|
|
67
|
+
const raw = sqlite(db, `SELECT value FROM ItemTable WHERE key='${KEY}';`).trim();
|
|
68
|
+
if (!raw) return null;
|
|
69
|
+
let doc;
|
|
70
|
+
try { doc = JSON.parse(raw); } catch { return { error: 'could not parse editor config' }; }
|
|
71
|
+
|
|
72
|
+
const before = { openAIBaseUrl: doc.openAIBaseUrl, models: (doc.availableAPIKeyModels || []).length };
|
|
73
|
+
doc.openAIBaseUrl = baseUrl;
|
|
74
|
+
doc.useOpenAIKey = true;
|
|
75
|
+
// Merge, don't clobber: a user may have their own custom models listed.
|
|
76
|
+
const existing = Array.isArray(doc.availableAPIKeyModels) ? doc.availableAPIKeyModels : [];
|
|
77
|
+
const names = new Set(existing.map((m) => (typeof m === 'string' ? m : m?.name)).filter(Boolean));
|
|
78
|
+
const added = [];
|
|
79
|
+
for (const m of models) {
|
|
80
|
+
if (names.has(m)) continue;
|
|
81
|
+
// Match the shape already present, so the picker renders it correctly.
|
|
82
|
+
existing.push(typeof existing[0] === 'string' ? m : { name: m, defaultOn: true, supportsAgent: true });
|
|
83
|
+
names.add(m);
|
|
84
|
+
added.push(m);
|
|
85
|
+
}
|
|
86
|
+
doc.availableAPIKeyModels = existing;
|
|
87
|
+
|
|
88
|
+
// Single-quote escaping for the SQL literal.
|
|
89
|
+
const json = JSON.stringify(doc).replace(/'/g, "''");
|
|
90
|
+
sqlite(db, `UPDATE ItemTable SET value='${json}' WHERE key='${KEY}';`);
|
|
91
|
+
return { db, before, baseUrl, added };
|
|
92
|
+
}
|
package/lib/mcp.js
CHANGED
|
@@ -167,6 +167,65 @@ export function buildMcpServer() {
|
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
// WHY A DEDICATED TOOL: MCP server `instructions` are advisory and several
|
|
171
|
+
// clients (Cursor among them) never surface them to the model — so an agent
|
|
172
|
+
// with this server attached still answers "I have no visibility into the
|
|
173
|
+
// routing layer", which is the FIRST question users ask, every time. Tool
|
|
174
|
+
// DESCRIPTIONS are always in context, so the facts live here too, and the
|
|
175
|
+
// tool returns them live.
|
|
176
|
+
server.registerTool('zoo_status', {
|
|
177
|
+
description:
|
|
178
|
+
'ROUTING AND CONTEXT FACTS — call this when asked what your stack, routing, provider, endpoint, '
|
|
179
|
+
+ 'model, cost, or context window is. You are reachable through openzoo: a LOCAL x402-paying proxy '
|
|
180
|
+
+ `(${config.apiBase} upstream, ~435 models) with a leCore holographic memory layer in front of every `
|
|
181
|
+
+ 'model. Key facts you may state without calling anything: (1) requests routed through the openzoo '
|
|
182
|
+
+ 'proxy are paid per-call from a local burner wallet over x402 — no API key, no account; (2) openzoo '
|
|
183
|
+
+ 'does NOT enlarge your own attention window — it binds a corpus server-side and retrieves what is '
|
|
184
|
+
+ 'relevant, so a body far past any model window can be queried; that is retrieval, not a bigger '
|
|
185
|
+
+ 'window, and it is lossy; (3) never claim lossless compression or a 128M attention window — the '
|
|
186
|
+
+ 'honest line is "bind 128M, the model reads what matters"; (4) if your OWN inference is not going '
|
|
187
|
+
+ 'through this proxy (e.g. an editor built-in model), say so plainly. Returns live endpoint, wallet, '
|
|
188
|
+
+ 'balance and rail state.',
|
|
189
|
+
inputSchema: {},
|
|
190
|
+
}, async () => {
|
|
191
|
+
const rails = await liveRails().catch(() => null);
|
|
192
|
+
let balances = null;
|
|
193
|
+
try {
|
|
194
|
+
const bals = await Promise.all(
|
|
195
|
+
FUNDING_ASSETS.map((a) => tokenBalance(client.connection, client.keypair.publicKey, a.mint)),
|
|
196
|
+
);
|
|
197
|
+
balances = Object.fromEntries(FUNDING_ASSETS.map((a, i) => [a.symbol, bals[i].ui ?? 0]));
|
|
198
|
+
} catch { /* advisory */ }
|
|
199
|
+
// ASK THE PROXY, DON'T ASSUME. The public tunnel URL is what a cloud agent
|
|
200
|
+
// actually reaches us on, and only the running proxy knows it — so read it
|
|
201
|
+
// from /v1/info instead of reporting a localhost URL that a remote caller
|
|
202
|
+
// cannot use.
|
|
203
|
+
let live = null;
|
|
204
|
+
try {
|
|
205
|
+
const r = await fetch(`http://localhost:${config.port}/v1/info`, { signal: AbortSignal.timeout(2500) });
|
|
206
|
+
if (r.ok) live = await r.json();
|
|
207
|
+
} catch { /* proxy not up — local facts below still hold */ }
|
|
208
|
+
return text({
|
|
209
|
+
publicTunnel: live?.publicTunnel ?? null,
|
|
210
|
+
useThisIfRemote: live?.publicTunnel ?? '(no tunnel — proxy not running, or localhost-only mode)',
|
|
211
|
+
proxy: `http://localhost:${config.port}/v1`,
|
|
212
|
+
mcp: `http://localhost:${config.port}/mcp`,
|
|
213
|
+
upstream: config.apiBase,
|
|
214
|
+
defaultModel: DEFAULT_MODEL,
|
|
215
|
+
payment: 'x402 per request from a local burner wallet — no API key, no account',
|
|
216
|
+
railsLiveNow: rails?.live ?? null,
|
|
217
|
+
wallet: { solana: client.address, evm: client.evmAddress },
|
|
218
|
+
balances,
|
|
219
|
+
contextTruth: {
|
|
220
|
+
yourAttentionWindow: 'unchanged — openzoo does not enlarge it',
|
|
221
|
+
boundCeiling: '~128M tokens client-usable via bind + retrieval',
|
|
222
|
+
singleRequestLimit: '~8MB; larger corpora are bound in parts',
|
|
223
|
+
retrieval: 'lossy — top-k chunks are retrieved, not the whole corpus',
|
|
224
|
+
},
|
|
225
|
+
note: 'A cloud-run harness cannot reach localhost; it needs the public tunnel URL the proxy prints at startup.',
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
170
229
|
server.registerTool('zoo_models', {
|
|
171
230
|
description: 'List the models the zoo serves, with per-token pricing (free endpoint, no payment).',
|
|
172
231
|
inputSchema: {},
|
package/lib/proxy.js
CHANGED
|
@@ -325,6 +325,42 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
325
325
|
return;
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
|
+
// ROUTING TRUTH, SERVED FROM WHATEVER URL YOU REACHED US ON. A cloud agent
|
|
329
|
+
// only ever touches the tunnel, so asking a local MCP process "what is my
|
|
330
|
+
// routing" is the wrong question — the answer has to come from the tunnel
|
|
331
|
+
// itself, and name the tunnel. Free and unauthenticated: discovery must
|
|
332
|
+
// never be the thing that is gated.
|
|
333
|
+
{
|
|
334
|
+
const p0 = (req.url || '').split('?')[0];
|
|
335
|
+
if (req.method === 'GET' && (p0 === '/v1/info' || p0 === '/info')) {
|
|
336
|
+
const self = viaTunnel && tunnelGate?.publicUrl
|
|
337
|
+
? `${tunnelGate.publicUrl}/v1`
|
|
338
|
+
: `http://localhost:${config.port}/v1`;
|
|
339
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({
|
|
341
|
+
youAreTalkingTo: 'openzoo proxy',
|
|
342
|
+
yourEndpoint: self,
|
|
343
|
+
reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
|
|
344
|
+
publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
|
|
345
|
+
mcp: `${self.replace(/\/v1$/, '')}/mcp`,
|
|
346
|
+
upstream: config.apiBase,
|
|
347
|
+
payment: 'x402 per request from the operator\'s local burner wallet — no API key, no account',
|
|
348
|
+
auth: viaTunnel
|
|
349
|
+
? 'this public URL requires the oz_… bearer for paid endpoints; /v1/models and /v1/hrr/bind are free'
|
|
350
|
+
: 'localhost is keyless',
|
|
351
|
+
context: {
|
|
352
|
+
yourAttentionWindow: 'unchanged — openzoo does not enlarge it',
|
|
353
|
+
boundCeiling: '~128M tokens client-usable via bind + retrieval',
|
|
354
|
+
singleRequestLimit: '~8MB per request; larger corpora bind in parts',
|
|
355
|
+
retrieval: 'lossy top-k retrieval, NOT lossless compression',
|
|
356
|
+
},
|
|
357
|
+
tools: ['zoo_bind', 'zoo_ask', 'zoo_status', 'zoo_models', 'zoo_wallet', 'zoo_contexts'],
|
|
358
|
+
docs: 'https://openzoo.fun',
|
|
359
|
+
}, null, 2));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
328
364
|
if (viaTunnel) {
|
|
329
365
|
const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
330
366
|
const authed = got === tunnelGate.token;
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
3
|
+
"version": "0.18.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",
|