openzoo 0.14.1 → 0.16.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 +7 -5
- package/lib/mcp.js +60 -3
- package/lib/proxy.js +11 -1
- package/lib/setup.js +159 -53
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -6,9 +6,10 @@ const HELP = `openzoo — local x402-paying proxy + MCP server for openzoo.fun
|
|
|
6
6
|
usage:
|
|
7
7
|
npx openzoo start the proxy: http://localhost:8402/v1 (keyless) PLUS a
|
|
8
8
|
public HTTPS url for cloud IDEs (key required, printed at start)
|
|
9
|
-
npx openzoo cursor
|
|
10
|
-
|
|
11
|
-
npx openzoo vscode
|
|
9
|
+
npx openzoo cursor [path] start proxy+tunnel, write MCP config, and LAUNCH
|
|
10
|
+
Cursor already pointed at the zoo (env inherited)
|
|
11
|
+
npx openzoo vscode [path] same, for VS Code
|
|
12
|
+
npx openzoo editor [path] whichever is installed (Cursor wins if both)
|
|
12
13
|
npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
|
|
13
14
|
(claude, aider...) already pointed at the zoo
|
|
14
15
|
npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_bind, zoo_models, zoo_wallet, zoo_contexts)
|
|
@@ -49,10 +50,11 @@ async function main() {
|
|
|
49
50
|
case 'start':
|
|
50
51
|
await (await import('../lib/proxy.js')).startProxy({ autoTunnel: true });
|
|
51
52
|
break;
|
|
53
|
+
case 'editor':
|
|
52
54
|
case 'cursor':
|
|
53
55
|
case 'vscode':
|
|
54
56
|
// GUI editors read config files, not env vars — see lib/setup.js.
|
|
55
|
-
(await import('../lib/setup.js')).setupEditor(cmd);
|
|
57
|
+
await (await import('../lib/setup.js')).setupEditor(cmd === 'editor' ? undefined : cmd, process.argv[3]);
|
|
56
58
|
break;
|
|
57
59
|
case 'mcp':
|
|
58
60
|
await (await import('../lib/mcp.js')).startMcp();
|
|
@@ -132,7 +134,7 @@ async function main() {
|
|
|
132
134
|
method: 'POST',
|
|
133
135
|
headers,
|
|
134
136
|
body: JSON.stringify({
|
|
135
|
-
model: (mi !== -1 && process.argv[mi + 1]) || process.env.OPENZOO_DEFAULT_MODEL || '
|
|
137
|
+
model: (mi !== -1 && process.argv[mi + 1]) || process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
|
|
136
138
|
messages: [{ role: 'user', content: question }],
|
|
137
139
|
max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 1024),
|
|
138
140
|
}),
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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/proxy.js
CHANGED
|
@@ -691,5 +691,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
691
691
|
}
|
|
692
692
|
})();
|
|
693
693
|
}
|
|
694
|
-
|
|
694
|
+
// Expose live tunnel details so a caller that starts the proxy in-process
|
|
695
|
+
// (openzoo cursor/vscode) can surface the public URL + key instead of the
|
|
696
|
+
// user hunting for them. Getters, because the tunnel resolves ASYNC after
|
|
697
|
+
// this returns — a snapshot would always be null.
|
|
698
|
+
return {
|
|
699
|
+
server,
|
|
700
|
+
client,
|
|
701
|
+
spent: () => sessionSpent,
|
|
702
|
+
get publicUrl() { return tunnelGate?.publicUrl ?? null; },
|
|
703
|
+
get tunnelToken() { return tunnelGate?.token ?? null; },
|
|
704
|
+
};
|
|
695
705
|
}
|
package/lib/setup.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `npx openzoo cursor
|
|
2
|
+
* `npx openzoo cursor|vscode [path]` — one command: proxy + tunnel up, config
|
|
3
|
+
* written, editor launched already pointed at the zoo.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* THREE THINGS THIS MUST DO, because the user should configure nothing:
|
|
6
|
+
* 1. ANTHROPIC_BASE_URL (and OPENAI_BASE_URL) exported INTO the editor, so
|
|
7
|
+
* its embedded terminals and the Claude Code extension bill through x402.
|
|
8
|
+
* 2. Config written for them — MCP server registered in the editor's own
|
|
9
|
+
* mcp.json, no hand-editing.
|
|
10
|
+
* 3. The TUNNEL, because a cloud-run harness cannot reach localhost.
|
|
11
|
+
*
|
|
12
|
+
* WHY LAUNCH THE BINARY, NOT `open -a`: macOS `open` hands the app to launchd,
|
|
13
|
+
* which does NOT pass the caller's environment. `open -a Cursor` therefore
|
|
14
|
+
* configures nothing — the editor comes up with no idea the zoo exists.
|
|
15
|
+
* Spawning Contents/MacOS/Cursor directly keeps the env, which is the entire
|
|
16
|
+
* point of this command.
|
|
12
17
|
*/
|
|
13
18
|
import fs from 'node:fs';
|
|
14
19
|
import os from 'node:os';
|
|
@@ -16,71 +21,172 @@ import path from 'node:path';
|
|
|
16
21
|
import { spawn } from 'node:child_process';
|
|
17
22
|
import { config } from './config.js';
|
|
18
23
|
|
|
19
|
-
const
|
|
20
|
-
|
|
24
|
+
const MCP_FILES = {
|
|
25
|
+
cursor: path.join(os.homedir(), '.cursor', 'mcp.json'),
|
|
26
|
+
vscode: path.join(os.homedir(), '.vscode', 'mcp.json'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find a launchable editor binary, platform-agnostically.
|
|
31
|
+
*
|
|
32
|
+
* Hardcoding /Applications broke every non-mac install and any mac install
|
|
33
|
+
* that is not in /Applications (~/Applications, Setapp, a homebrew cask on a
|
|
34
|
+
* different volume). Order per editor: PATH first (works everywhere and is
|
|
35
|
+
* what a Linux/Windows user has), then the known app-bundle locations.
|
|
36
|
+
*
|
|
37
|
+
* The BUNDLE BINARY is preferred over `open -a` on macOS because `open` hands
|
|
38
|
+
* the app to launchd, which drops our environment — and the environment IS the
|
|
39
|
+
* configuration here.
|
|
40
|
+
*/
|
|
41
|
+
const EDITORS = {
|
|
42
|
+
cursor: {
|
|
43
|
+
cli: ['cursor'],
|
|
44
|
+
bundles: [
|
|
45
|
+
'/Applications/Cursor.app/Contents/MacOS/Cursor',
|
|
46
|
+
path.join(os.homedir(), 'Applications', 'Cursor.app', 'Contents', 'MacOS', 'Cursor'),
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
vscode: {
|
|
50
|
+
cli: ['code', 'code-insiders', 'codium'],
|
|
51
|
+
bundles: [
|
|
52
|
+
'/Applications/Visual Studio Code.app/Contents/MacOS/Electron',
|
|
53
|
+
path.join(os.homedir(), 'Applications', 'Visual Studio Code.app', 'Contents', 'MacOS', 'Electron'),
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function onPath(bin) {
|
|
59
|
+
const exts = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
|
|
60
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
61
|
+
for (const ext of exts) {
|
|
62
|
+
const f = path.join(dir, bin + ext);
|
|
63
|
+
try { fs.accessSync(f, fs.constants.X_OK); return f; } catch { /* keep looking */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve one editor to a runnable command, or null if it is not installed. */
|
|
70
|
+
function resolveEditor(which) {
|
|
71
|
+
const spec = EDITORS[which];
|
|
72
|
+
if (!spec) return null;
|
|
73
|
+
for (const c of spec.cli) { const f = onPath(c); if (f) return f; }
|
|
74
|
+
for (const b of spec.bundles) { try { fs.accessSync(b, fs.constants.X_OK); return b; } catch { /* next */ } }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Which editor to use: the one asked for, else Cursor if present, else VS Code. */
|
|
79
|
+
export function pickEditor(requested) {
|
|
80
|
+
if (requested && EDITORS[requested]) {
|
|
81
|
+
const found = resolveEditor(requested);
|
|
82
|
+
if (found) return { which: requested, cmd: found };
|
|
83
|
+
}
|
|
84
|
+
for (const which of ['cursor', 'vscode']) { // cursor wins when both exist
|
|
85
|
+
const cmd = resolveEditor(which);
|
|
86
|
+
if (cmd) return { which, cmd };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
21
90
|
|
|
22
91
|
/** Merge our server into an existing mcp.json without clobbering the user's. */
|
|
23
92
|
function addMcpServer(file) {
|
|
24
93
|
let doc = {};
|
|
25
94
|
try { doc = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { doc = {}; }
|
|
26
|
-
const key = doc.
|
|
95
|
+
const key = doc.servers && !doc.mcpServers ? 'servers' : 'mcpServers';
|
|
27
96
|
doc[key] = doc[key] || {};
|
|
28
|
-
const existed = Boolean(doc[key].openzoo);
|
|
29
97
|
doc[key].openzoo = { command: 'npx', args: ['-y', 'openzoo@latest', 'mcp'] };
|
|
30
98
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
99
|
fs.writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
32
|
-
return
|
|
100
|
+
return file;
|
|
33
101
|
}
|
|
34
102
|
|
|
35
|
-
|
|
103
|
+
/** Is a proxy already listening? Returns its /v1 base or null. */
|
|
104
|
+
async function proxyUp(base) {
|
|
105
|
+
try {
|
|
106
|
+
const r = await fetch(`${base}/models`, { signal: AbortSignal.timeout(2500) });
|
|
107
|
+
return r.ok;
|
|
108
|
+
} catch { return false; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function setupEditor(which, target) {
|
|
36
112
|
const base = `http://localhost:${config.port}/v1`;
|
|
37
|
-
const
|
|
38
|
-
const { existed } = addMcpServer(file);
|
|
113
|
+
const mcpFile = addMcpServer(MCP_FILES[which] || MCP_FILES.cursor);
|
|
39
114
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
console.log('');
|
|
115
|
+
// 1. PROXY + TUNNEL. Start in-process if nothing is listening, so the user
|
|
116
|
+
// does not need a second terminal. The tunnel URL is what a cloud-run
|
|
117
|
+
// harness must use; we surface it rather than leaving them to find it.
|
|
118
|
+
let publicUrl = null;
|
|
119
|
+
let tunnelKey = null;
|
|
120
|
+
if (!(await proxyUp(base))) {
|
|
121
|
+
console.log(`starting proxy on ${base} (+ public tunnel)...`);
|
|
122
|
+
const { startProxy } = await import('./proxy.js');
|
|
123
|
+
const started = await startProxy({ silent: true, autoTunnel: true });
|
|
124
|
+
publicUrl = started?.publicUrl ?? null;
|
|
125
|
+
tunnelKey = started?.tunnelToken ?? null;
|
|
126
|
+
// give the tunnel a moment to publish its URL
|
|
127
|
+
for (let i = 0; i < 20 && !publicUrl; i++) {
|
|
128
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
129
|
+
publicUrl = started?.publicUrl ?? null;
|
|
130
|
+
tunnelKey = started?.tunnelToken ?? null;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`proxy already running on ${base}`);
|
|
134
|
+
}
|
|
61
135
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// themselves. Env vars are set for the child so any terminal the editor
|
|
65
|
-
// spawns inherits the zoo, and `open -a` is used on macOS because Cursor is
|
|
66
|
-
// a GUI .app, not a binary on PATH.
|
|
136
|
+
// 2. ENV INTO THE EDITOR. Both vendor shapes, so an OpenAI-compatible pane
|
|
137
|
+
// and an Anthropic-shaped one (Claude Code extension) both route here.
|
|
67
138
|
const env = {
|
|
68
139
|
...process.env,
|
|
69
140
|
OPENAI_BASE_URL: base,
|
|
70
141
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY || 'sk-openzoo',
|
|
71
142
|
ANTHROPIC_BASE_URL: base,
|
|
72
143
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || 'sk-openzoo',
|
|
144
|
+
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || 'sk-openzoo',
|
|
73
145
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
console.log(`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
146
|
+
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(`mcp: ${mcpFile} (openzoo: zoo_bind, zoo_ask, zoo_models, zoo_wallet, zoo_contexts)`);
|
|
149
|
+
console.log(`local: ${base} api_key sk-openzoo`);
|
|
150
|
+
console.log('');
|
|
151
|
+
// THE ONE THING THE EDITOR WILL NOT INHERIT. Cursor's BUILT-IN models
|
|
152
|
+
// (Opus 5, GPT, Composer) go to Cursor's own backend and ignore
|
|
153
|
+
// ANTHROPIC_BASE_URL — Cursor has no Anthropic base-URL override, only an
|
|
154
|
+
// OpenAI one. So routing a Claude model through the zoo means adding it as a
|
|
155
|
+
// CUSTOM model under the OpenAI override, where the proxy serves it and maps
|
|
156
|
+
// the name. Env alone cannot do this; say so plainly instead of implying the
|
|
157
|
+
// 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)');
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
171
|
+
|
|
172
|
+
// 3. LAUNCH with that env. Editor resolved platform-agnostically; Cursor
|
|
173
|
+
// wins when both are installed.
|
|
174
|
+
const cwd = target && !target.startsWith('-') ? target : '.';
|
|
175
|
+
const picked = pickEditor(which);
|
|
176
|
+
if (!picked) {
|
|
177
|
+
console.error('no editor found — install Cursor or VS Code, or put `cursor`/`code` on PATH');
|
|
178
|
+
console.error(`(config is written either way: ${mcpFile})`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(`launching ${picked.which}...`);
|
|
182
|
+
const child = spawn(picked.cmd, [cwd], { stdio: 'ignore', env, detached: true });
|
|
80
183
|
child.on('error', (e) => {
|
|
81
|
-
console.error(`could not
|
|
82
|
-
console.error(process.platform === 'darwin'
|
|
83
|
-
? `try: open -a "${app}" .`
|
|
84
|
-
: `is the \`${which === 'vscode' ? 'code' : 'cursor'}\` command on PATH? (editor: Shell Command: Install '${which === 'vscode' ? 'code' : 'cursor'}' command)`);
|
|
184
|
+
console.error(`could not launch ${picked.which}: ${e.message}`);
|
|
85
185
|
});
|
|
186
|
+
child.unref();
|
|
187
|
+
// Keep this process alive when we own the proxy — killing it would kill the
|
|
188
|
+
// zoo the editor was just pointed at.
|
|
189
|
+
if (publicUrl || !(await proxyUp(base))) {
|
|
190
|
+
console.log('proxy is running in this terminal — Ctrl-C when done.');
|
|
191
|
+
}
|
|
86
192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|