openzoo 0.21.4 → 0.21.6

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/setup.js CHANGED
@@ -203,7 +203,7 @@ export async function setupEditor(which, target) {
203
203
  // does not, we say so plainly and still launch.
204
204
  if (!publicUrl) {
205
205
  process.stdout.write('waiting for the public tunnel');
206
- for (let i = 0; i < 60 && !started?.publicUrl; i++) { // up to 30s
206
+ for (let i = 0; i < 180 && !started?.publicUrl; i++) { // up to 90s
207
207
  await new Promise((r) => setTimeout(r, 500));
208
208
  if (i % 4 === 3) process.stdout.write('.');
209
209
  }
@@ -212,9 +212,7 @@ export async function setupEditor(which, target) {
212
212
  tunnelKey = started?.tunnelToken ?? tunnelKey;
213
213
  }
214
214
  if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 api_key ${tunnelKey}`);
215
- else console.log('tunnel: did not come up in 30s — launching anyway on localhost;\n'
216
- + ' the editor will say "Access to private networks is forbidden"\n'
217
- + ' until you re-run this command.');
215
+ else console.log('tunnel: did not come up in 90s.');
218
216
  } else {
219
217
  console.log(`proxy already running on ${base}`);
220
218
  // A proxy someone else started owns the tunnel; ask it for the public URL.
@@ -227,8 +225,13 @@ export async function setupEditor(which, target) {
227
225
  }
228
226
  // What the EDITOR is configured with. Localhost only as a last resort, and
229
227
  // said out loud, because it will fail with the private-networks error.
230
- const editorBase = publicUrl ? `${publicUrl}/v1` : base;
231
- if (!publicUrl) console.log('settings: falling back to localhost expect "Access to private networks is forbidden"');
228
+ // NEVER WRITE LOCALHOST INTO THE EDITOR. Its server makes the request, so a
229
+ // private address is unreachable from there and every call returns "Access to
230
+ // private networks is forbidden". Writing it anyway does not degrade
231
+ // gracefully — it overwrites a config that may have been working with one
232
+ // that provably cannot. With no tunnel we leave the existing settings alone
233
+ // and say why.
234
+ const editorBase = publicUrl ? `${publicUrl}/v1` : null;
232
235
 
233
236
  // 2. ENV INTO THE EDITOR. Both vendor shapes, so an OpenAI-compatible pane
234
237
  // and an Anthropic-shaped one (Claude Code extension) both route here.
@@ -256,6 +259,12 @@ export async function setupEditor(which, target) {
256
259
  // globalStorage sqlite — not an encrypted store, as previously assumed — so
257
260
  // the "paste four things into Settings" ritual is unnecessary. Must happen
258
261
  // while the editor is CLOSED or it rewrites them from memory on exit.
262
+ if (!editorBase) {
263
+ console.log('settings: NOT touched — a tunnel is required for the editor path and none came up.');
264
+ console.log(' (localhost cannot work here: the editor calls the endpoint from ITS server,');
265
+ console.log(' which answers "Access to private networks is forbidden".)');
266
+ console.log(' your previous settings are left as they were — re-run when the network settles.');
267
+ }
259
268
  const picked0 = pickEditor(which);
260
269
  const target0 = picked0?.which || which || 'cursor';
261
270
  // Quit it FOR the user — a running editor reverts our write on exit.
@@ -266,7 +275,9 @@ export async function setupEditor(which, target) {
266
275
  }
267
276
  const models = await catalogModels(base);
268
277
  let wrote = null;
269
- try { wrote = writeEditorProviderConfig(target0, { baseUrl: editorBase, models, apiKey: tunnelKey }); } catch (e) { wrote = { error: e.message }; }
278
+ if (editorBase) {
279
+ try { wrote = writeEditorProviderConfig(target0, { baseUrl: editorBase, models, apiKey: tunnelKey }); } catch (e) { wrote = { error: e.message }; }
280
+ }
270
281
  if (wrote?.error) {
271
282
  console.log(`settings: could not write automatically (${wrote.error}) — set them in Settings → Models`);
272
283
  } else if (wrote) {
package/lib/tunnel.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * OPENZOO_MAX_USD_PER_CALL and/or OPENZOO_TUNNEL_MAX_USD to add them;
21
21
  * - every tunnel-served request is logged with its running spend.
22
22
  */
23
- import { spawn } from 'node:child_process';
23
+ import { spawn, execFileSync } from 'node:child_process';
24
24
  import { createWriteStream } from 'node:fs';
25
25
  import fs from 'node:fs';
26
26
  import path from 'node:path';
@@ -91,8 +91,27 @@ export async function ensureCloudflared(log = console.log) {
91
91
  return cached;
92
92
  }
93
93
 
94
+ /**
95
+ * Kill cloudflared processes left over from previous runs against OUR port.
96
+ *
97
+ * WHY: the proxy exits with its terminal but cloudflared does not always go with
98
+ * it, so repeated `openzoo cursor` runs stack tunnels. Observed: two orphaned
99
+ * cloudflared processes, the editor configured with a third hostname, and every
100
+ * request answering 530/1033 — the editor was pointed at a tunnel whose origin
101
+ * had died while other live tunnels sat there serving nothing.
102
+ */
103
+ function killStaleTunnels(port, log) {
104
+ try {
105
+ const out = execFileSync('pgrep', ['-f', `cloudflared tunnel --url http://localhost:${port}`], { encoding: 'utf8' });
106
+ const pids = out.split('\n').map((x) => x.trim()).filter(Boolean).filter((x) => Number(x) !== process.pid);
107
+ for (const pid of pids) { try { process.kill(Number(pid), 'SIGTERM'); } catch { /* already gone */ } }
108
+ if (pids.length) log(`tunnel: cleared ${pids.length} stale cloudflared process(es) from an earlier run`);
109
+ } catch { /* pgrep exits non-zero when nothing matches — that is the good case */ }
110
+ }
111
+
94
112
  /** Start a quick tunnel to localhost:<port>; resolve with its public URL. */
95
113
  export function startCloudflared(bin, port, log) {
114
+ killStaleTunnels(port, log);
96
115
  return new Promise((resolve, reject) => {
97
116
  // --edge-ip-version 4: cloudflared prefers IPv6+QUIC to reach the edge
98
117
  // whenever the interface merely HAS an IPv6 address — it never checks the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.21.4",
3
+ "version": "0.21.6",
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",