openzoo 0.20.3 → 0.20.5
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 +11 -0
- package/lib/hosts.js +3 -2
- package/lib/setup.js +44 -16
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -12,11 +12,16 @@ usage:
|
|
|
12
12
|
a missing dir is created)
|
|
13
13
|
--profile use an isolated editor profile the
|
|
14
14
|
vendor account cannot re-sync over
|
|
15
|
+
(also points cursor's own backend at 127.0.0.1 in
|
|
16
|
+
the hosts file so it cannot re-sync over your model
|
|
17
|
+
list or route inference around the proxy — asks for
|
|
18
|
+
your password; --no-block skips it)
|
|
15
19
|
npx openzoo vscode [path] same, for VS Code
|
|
16
20
|
npx openzoo editor [path] whichever is installed (Cursor wins if both)
|
|
17
21
|
npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
|
|
18
22
|
(claude, aider...) already pointed at the zoo
|
|
19
23
|
npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_bind, zoo_models, zoo_wallet, zoo_contexts)
|
|
24
|
+
npx openzoo unblock restore the editor's own backend in the hosts file
|
|
20
25
|
npx openzoo tunnel public-url-only mode (everything key-gated, no keyless localhost)
|
|
21
26
|
npx openzoo demo ~1M-token needle demo: direct refuses, the zoo answers
|
|
22
27
|
(run it twice — the second run reuses the bound corpus and is near-free)
|
|
@@ -70,6 +75,12 @@ async function main() {
|
|
|
70
75
|
await (await import('../lib/launch.js')).launchHarness(harness, hargs);
|
|
71
76
|
break;
|
|
72
77
|
}
|
|
78
|
+
case 'unblock': {
|
|
79
|
+
const { unblockBackend, isBlocked } = await import('../lib/hosts.js');
|
|
80
|
+
const r = unblockBackend();
|
|
81
|
+
console.log(r.already ? 'not blocked — nothing to undo' : (isBlocked() ? 'still blocked (sudo declined?)' : 'restored: the editor can reach its own backend again'));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
73
84
|
case 'tunnel':
|
|
74
85
|
await (await import('../lib/tunnel.js')).runTunnel();
|
|
75
86
|
break;
|
package/lib/hosts.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* COLLATERAL, STATED PLAINLY: that host also carries auth and usage reporting.
|
|
15
15
|
* The editor may report being signed out or degraded. This is a system-wide
|
|
16
|
-
* change
|
|
17
|
-
*
|
|
16
|
+
* change needing a password, so it always backs up the hosts file first and
|
|
17
|
+
* `npx openzoo unblock` restores it. `--no-block` skips it entirely.
|
|
18
18
|
*/
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
@@ -73,6 +73,7 @@ export function blockBackend() {
|
|
|
73
73
|
console.log(` backup : ${BACKUP}`);
|
|
74
74
|
console.log(' NOTE : that host also carries the editor\'s auth/usage — it may report');
|
|
75
75
|
console.log(' being signed out. Undo any time with: npx openzoo unblock');
|
|
76
|
+
console.log(' skip this next time with: --no-block');
|
|
76
77
|
console.log(' sudo will ask for your password now.');
|
|
77
78
|
if (WIN) {
|
|
78
79
|
console.log(' windows: run this in an ADMINISTRATOR PowerShell, then relaunch:');
|
package/lib/setup.js
CHANGED
|
@@ -175,24 +175,39 @@ export async function setupEditor(which, target) {
|
|
|
175
175
|
// the user staring at "waiting for tunnel.." while nothing else happened —
|
|
176
176
|
// and if it never came up, the editor never launched at all. Announce it
|
|
177
177
|
// when it arrives instead.
|
|
178
|
+
// THE EDITOR CONFIG CANNOT USE LOCALHOST. Cursor does not call the custom
|
|
179
|
+
// endpoint from your machine — its SERVER makes the request, so a private
|
|
180
|
+
// address is unreachable from there and it answers, verbatim:
|
|
181
|
+
// "Provider returned error: Access to private networks is forbidden"
|
|
182
|
+
// (see the header of lib/tunnel.js). So the public URL is a HARD dependency
|
|
183
|
+
// of the editor path, and we wait for it — bounded, with progress, because
|
|
184
|
+
// an unbounded wait once left the editor never launching at all.
|
|
178
185
|
if (!publicUrl) {
|
|
179
|
-
|
|
180
|
-
(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
console.log('tunnel: still not up — localhost is unaffected; OPENZOO_NO_TUNNEL=1 to skip it');
|
|
191
|
-
})();
|
|
186
|
+
process.stdout.write('waiting for the public tunnel (the editor\'s server cannot reach localhost)');
|
|
187
|
+
for (let i = 0; i < 120 && !started?.publicUrl; i++) { // up to 60s
|
|
188
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
189
|
+
if (i % 4 === 3) process.stdout.write('.');
|
|
190
|
+
}
|
|
191
|
+
console.log('');
|
|
192
|
+
publicUrl = started?.publicUrl ?? null;
|
|
193
|
+
tunnelKey = started?.tunnelToken ?? tunnelKey;
|
|
192
194
|
}
|
|
195
|
+
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 api_key ${tunnelKey}`);
|
|
196
|
+
else console.log('tunnel: NOT up — the editor will not be able to reach the proxy\n (OPENZOO_NO_TUNNEL=1 to skip; terminal harnesses still work on localhost)');
|
|
193
197
|
} else {
|
|
194
198
|
console.log(`proxy already running on ${base}`);
|
|
199
|
+
// A proxy someone else started owns the tunnel; ask it for the public URL.
|
|
200
|
+
try {
|
|
201
|
+
const info = await (await fetch(`${base}/info`)).json();
|
|
202
|
+
publicUrl = (info?.publicTunnel || '').replace(/\/v1$/, '') || null;
|
|
203
|
+
tunnelKey = info?.tunnelToken ?? tunnelKey;
|
|
204
|
+
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 (from the running proxy)`);
|
|
205
|
+
} catch { /* no /info — fall through to the localhost warning below */ }
|
|
195
206
|
}
|
|
207
|
+
// What the EDITOR is configured with. Localhost only as a last resort, and
|
|
208
|
+
// said out loud, because it will fail with the private-networks error.
|
|
209
|
+
const editorBase = publicUrl ? `${publicUrl}/v1` : base;
|
|
210
|
+
if (!publicUrl) console.log('settings: falling back to localhost — expect "Access to private networks is forbidden"');
|
|
196
211
|
|
|
197
212
|
// 2. ENV INTO THE EDITOR. Both vendor shapes, so an OpenAI-compatible pane
|
|
198
213
|
// and an Anthropic-shaped one (Claude Code extension) both route here.
|
|
@@ -230,14 +245,14 @@ export async function setupEditor(which, target) {
|
|
|
230
245
|
}
|
|
231
246
|
const models = await catalogModels(base);
|
|
232
247
|
let wrote = null;
|
|
233
|
-
try { wrote = writeEditorProviderConfig(target0, { baseUrl:
|
|
248
|
+
try { wrote = writeEditorProviderConfig(target0, { baseUrl: editorBase, models, apiKey: tunnelKey }); } catch (e) { wrote = { error: e.message }; }
|
|
234
249
|
if (wrote?.error) {
|
|
235
250
|
console.log(`settings: could not write automatically (${wrote.error}) — set them in Settings → Models`);
|
|
236
251
|
} else if (wrote) {
|
|
237
|
-
console.log(`settings: openAIBaseUrl -> ${wrote.verified?.openAIBaseUrl ||
|
|
252
|
+
console.log(`settings: openAIBaseUrl -> ${wrote.verified?.openAIBaseUrl || editorBase}`);
|
|
238
253
|
console.log(' useOpenAIKey -> true');
|
|
239
254
|
// PIN so the editor's account-sync cannot revert it on launch.
|
|
240
|
-
const pin = pinEditorProviderConfig(target0, { baseUrl:
|
|
255
|
+
const pin = pinEditorProviderConfig(target0, { baseUrl: editorBase, models });
|
|
241
256
|
console.log(` models -> ${models.join(', ')}`);
|
|
242
257
|
console.log(` selected -> ${models[0]}`);
|
|
243
258
|
console.log(pin?.pinned
|
|
@@ -246,6 +261,19 @@ export async function setupEditor(which, target) {
|
|
|
246
261
|
console.log(' verify: send one message, watch for "paid $0.0… · rail solana · tx …" here.');
|
|
247
262
|
}
|
|
248
263
|
|
|
264
|
+
// 2b. THE BACKEND BLOCK. Pinning the database is not sufficient on its own:
|
|
265
|
+
// the editor re-syncs its model list from its own backend into MEMORY on
|
|
266
|
+
// window focus and repaints the picker empty, and that same host is the
|
|
267
|
+
// route its own inference takes. Blackholing it in the hosts file is what
|
|
268
|
+
// leaves the configured base URL as the only way out. System-wide and
|
|
269
|
+
// needs a password, so it is opt-in and reversible.
|
|
270
|
+
if (target0 === 'cursor' && !process.argv.includes('--no-block')) {
|
|
271
|
+
const { blockBackend, isBlocked } = await import('./hosts.js');
|
|
272
|
+
const r = blockBackend();
|
|
273
|
+
if (r.already) console.log('backend: already blocked (npx openzoo unblock to restore)');
|
|
274
|
+
else console.log(`backend: ${isBlocked() ? 'blocked -> 127.0.0.1' : 'NOT blocked — the editor will re-sync over your model list'}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
249
277
|
// 3. LAUNCH with that env. Editor resolved platform-agnostically; Cursor
|
|
250
278
|
// wins when both are installed.
|
|
251
279
|
// Open the directory you ran this from, or the one you named. The earlier
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.5",
|
|
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",
|