openzoo 0.18.0 → 0.18.2
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 +46 -1
- package/lib/mcp.js +10 -11
- package/lib/setup.js +28 -8
- package/package.json +1 -1
package/lib/cursorcfg.js
CHANGED
|
@@ -54,6 +54,34 @@ export function editorRunning(which) {
|
|
|
54
54
|
} catch { return false; }
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Quit the editor and wait for it to exit.
|
|
59
|
+
*
|
|
60
|
+
* WHY WE DO THIS FOR THE USER: the editor holds its settings in memory and
|
|
61
|
+
* flushes them on exit, so a write performed while it is running is silently
|
|
62
|
+
* reverted — measured: availableAPIKeyModels came back [] every launch, the
|
|
63
|
+
* model never appeared in the dropdown, and the user was told to "just pick
|
|
64
|
+
* it". Writing while it is CLOSED sticks. Telling a human to quit their editor
|
|
65
|
+
* and re-run a command is not automation, so we quit it, write, and relaunch.
|
|
66
|
+
*/
|
|
67
|
+
export async function quitEditor(which) {
|
|
68
|
+
if (!editorRunning(which)) return { wasRunning: false };
|
|
69
|
+
const app = which === 'vscode' ? 'Visual Studio Code' : 'Cursor';
|
|
70
|
+
try {
|
|
71
|
+
if (process.platform === 'darwin') {
|
|
72
|
+
// Graceful: ask the app to quit so it flushes state normally.
|
|
73
|
+
execFileSync('osascript', ['-e', `tell application "${app}" to quit`], { stdio: 'ignore' });
|
|
74
|
+
} else {
|
|
75
|
+
execFileSync('pkill', ['-TERM', '-f', which === 'vscode' ? 'Code' : 'Cursor'], { stdio: 'ignore' });
|
|
76
|
+
}
|
|
77
|
+
} catch { /* fall through to the wait; it may already be closing */ }
|
|
78
|
+
for (let i = 0; i < 40; i++) { // up to 20s
|
|
79
|
+
if (!editorRunning(which)) return { wasRunning: true, quit: true };
|
|
80
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
81
|
+
}
|
|
82
|
+
return { wasRunning: true, quit: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
/**
|
|
58
86
|
* Point the editor at the zoo: base URL, key toggle, and the models the picker
|
|
59
87
|
* should offer. Returns what changed, or null when the store is unavailable
|
|
@@ -88,5 +116,22 @@ export function writeEditorProviderConfig(which, { baseUrl, models }) {
|
|
|
88
116
|
// Single-quote escaping for the SQL literal.
|
|
89
117
|
const json = JSON.stringify(doc).replace(/'/g, "''");
|
|
90
118
|
sqlite(db, `UPDATE ItemTable SET value='${json}' WHERE key='${KEY}';`);
|
|
91
|
-
|
|
119
|
+
|
|
120
|
+
// VERIFY, DO NOT ASSUME. `availableAPIKeyModels` is SERVER-SYNCED: Cursor
|
|
121
|
+
// rebuilds it from its own account state on launch and silently discards a
|
|
122
|
+
// local write, so this function used to report "models added" for a list
|
|
123
|
+
// that was empty again seconds later — and the user was told to pick a model
|
|
124
|
+
// that never appeared in the dropdown. Read it back and say which parts
|
|
125
|
+
// actually stuck.
|
|
126
|
+
let verified = { openAIBaseUrl: null, models: [] };
|
|
127
|
+
try {
|
|
128
|
+
const raw2 = sqlite(db, `SELECT value FROM ItemTable WHERE key='${KEY}';`).trim();
|
|
129
|
+
const doc2 = JSON.parse(raw2);
|
|
130
|
+
verified = {
|
|
131
|
+
openAIBaseUrl: doc2.openAIBaseUrl,
|
|
132
|
+
models: (doc2.availableAPIKeyModels || []).map((m) => (typeof m === 'string' ? m : m?.name)).filter(Boolean),
|
|
133
|
+
};
|
|
134
|
+
} catch { /* verification is advisory */ }
|
|
135
|
+
const modelsStick = models.every((m) => verified.models.includes(m));
|
|
136
|
+
return { db, before, baseUrl, added, verified, modelsStick };
|
|
92
137
|
}
|
package/lib/mcp.js
CHANGED
|
@@ -175,17 +175,16 @@ export function buildMcpServer() {
|
|
|
175
175
|
// tool returns them live.
|
|
176
176
|
server.registerTool('zoo_status', {
|
|
177
177
|
description:
|
|
178
|
-
'
|
|
179
|
-
+ '
|
|
180
|
-
+
|
|
181
|
-
+ '
|
|
182
|
-
+ '
|
|
183
|
-
+ '
|
|
184
|
-
+ '
|
|
185
|
-
+ '
|
|
186
|
-
+ '
|
|
187
|
-
+ '
|
|
188
|
-
+ 'balance and rail state.',
|
|
178
|
+
'Report what THIS openzoo proxy is and how it is routed, by querying it live (GET /v1/info + wallet + '
|
|
179
|
+
+ 'rails). Call it when asked about the openzoo setup: its endpoint, tunnel URL, upstream, wallet, '
|
|
180
|
+
+ 'balances, or what its context/binding actually does. '
|
|
181
|
+
+ 'SCOPE — it describes the PROXY, not you: it says nothing about which provider serves your own '
|
|
182
|
+
+ 'inference or what your own attention window is, and you should not infer either from it. If your '
|
|
183
|
+
+ 'requests are not going through this proxy, say so. '
|
|
184
|
+
+ 'It is useful for correcting two common overclaims, because the returned values are checkable: '
|
|
185
|
+
+ 'openzoo does not enlarge any model\'s attention window (it binds a corpus server-side and '
|
|
186
|
+
+ 'retrieves top-k chunks, which is lossy), and calls made THROUGH it are paid per-request via x402 '
|
|
187
|
+
+ 'from a local burner wallet rather than with an API key.',
|
|
189
188
|
inputSchema: {},
|
|
190
189
|
}, async () => {
|
|
191
190
|
const rails = await liveRails().catch(() => null);
|
package/lib/setup.js
CHANGED
|
@@ -20,7 +20,7 @@ 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';
|
|
23
|
+
import { writeEditorProviderConfig, editorRunning, quitEditor } from './cursorcfg.js';
|
|
24
24
|
|
|
25
25
|
const DEFAULT_MODELS = ['anthropic/claude-opus-5', 'deepseek/deepseek-v4-pro-0813'];
|
|
26
26
|
|
|
@@ -126,12 +126,17 @@ export async function setupEditor(which, target) {
|
|
|
126
126
|
const started = await startProxy({ silent: true, autoTunnel: true });
|
|
127
127
|
publicUrl = started?.publicUrl ?? null;
|
|
128
128
|
tunnelKey = started?.tunnelToken ?? null;
|
|
129
|
-
//
|
|
130
|
-
|
|
129
|
+
// Wait for cloudflared to publish the URL. It downloads on first run and
|
|
130
|
+
// routinely takes 30-45s; a 10s wait meant the tunnel line simply never
|
|
131
|
+
// printed and the user never learned the public URL existed.
|
|
132
|
+
process.stdout.write('waiting for tunnel');
|
|
133
|
+
for (let i = 0; i < 120 && !publicUrl; i++) {
|
|
131
134
|
await new Promise((r) => setTimeout(r, 500));
|
|
132
135
|
publicUrl = started?.publicUrl ?? null;
|
|
133
136
|
tunnelKey = started?.tunnelToken ?? null;
|
|
137
|
+
if (i % 4 === 3) process.stdout.write('.');
|
|
134
138
|
}
|
|
139
|
+
process.stdout.write(publicUrl ? ' ok\n' : ' (not up yet — it will print in this terminal when ready)\n');
|
|
135
140
|
} else {
|
|
136
141
|
console.log(`proxy already running on ${base}`);
|
|
137
142
|
}
|
|
@@ -164,8 +169,11 @@ export async function setupEditor(which, target) {
|
|
|
164
169
|
// while the editor is CLOSED or it rewrites them from memory on exit.
|
|
165
170
|
const picked0 = pickEditor(which);
|
|
166
171
|
const target0 = picked0?.which || which || 'cursor';
|
|
172
|
+
// Quit it FOR the user — a running editor reverts our write on exit.
|
|
167
173
|
if (editorRunning(target0)) {
|
|
168
|
-
console.log(
|
|
174
|
+
console.log(`${target0} is running — quitting it so settings stick (it relaunches below)...`);
|
|
175
|
+
const q = await quitEditor(target0);
|
|
176
|
+
if (!q.quit) console.log(` could not close ${target0} automatically; settings may not persist`);
|
|
169
177
|
}
|
|
170
178
|
const models = [DEFAULT_MODELS[0], ...DEFAULT_MODELS.slice(1)];
|
|
171
179
|
let wrote = null;
|
|
@@ -173,10 +181,22 @@ export async function setupEditor(which, target) {
|
|
|
173
181
|
if (wrote?.error) {
|
|
174
182
|
console.log(`settings: could not write automatically (${wrote.error}) — set them in Settings → Models`);
|
|
175
183
|
} else if (wrote) {
|
|
176
|
-
console.log(`settings: openAIBaseUrl -> ${
|
|
177
|
-
console.log(
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
console.log(`settings: openAIBaseUrl -> ${wrote.verified?.openAIBaseUrl || base}`);
|
|
185
|
+
console.log(' useOpenAIKey -> true');
|
|
186
|
+
if (wrote.modelsStick) {
|
|
187
|
+
console.log(` models -> ${models.join(', ')}`);
|
|
188
|
+
console.log(' pick one in the model dropdown; built-ins bypass the zoo.');
|
|
189
|
+
} else {
|
|
190
|
+
// Cursor re-syncs this list from its account on launch, so a local write
|
|
191
|
+
// does not survive. Say so instead of promising a dropdown entry that
|
|
192
|
+
// will not be there.
|
|
193
|
+
console.log(' models -> NOT writable (Cursor re-syncs this list on launch)');
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(' ONE-TIME manual step, in Cursor → Settings → Models:');
|
|
196
|
+
console.log(` + Add model -> ${models[0]}`);
|
|
197
|
+
console.log(' then toggle OFF the built-in models, or Cursor keeps using its own backend.');
|
|
198
|
+
console.log(' (the base URL + key above are already set for you and DO persist)');
|
|
199
|
+
}
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
// 3. LAUNCH with that env. Editor resolved platform-agnostically; Cursor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
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",
|