openzoo 0.50.91 → 0.50.93
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/claude-zoo.js +28 -3
- package/bin/openzoo.js +2 -26
- package/lib/aoe.js +8 -2
- package/lib/cursorbackend.js +2 -0
- package/lib/launch.js +17 -73
- package/lib/models.js +0 -30
- package/lib/ozSpendChip.js +67 -17
- package/lib/proxy.js +96 -27
- package/lib/setup.js +12 -13
- package/package.json +2 -2
- package/lib/websearch.js +0 -31
package/bin/claude-zoo.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the local openzoo proxy on :8402 (x402 per-call payment, no Anthropic
|
|
7
7
|
* subscription, no login):
|
|
8
8
|
*
|
|
9
|
-
* - starts the proxy if it is not already
|
|
9
|
+
* - starts the proxy if it is not already THIS version on :8402 (steals stale)
|
|
10
10
|
* - applies claudeZooEnv(): ANTHROPIC_BASE_URL=localhost:PORT/v1,
|
|
11
11
|
* AUTH_TOKEN=sk-openzoo, ANTHROPIC_API_KEY deleted, compaction disabled
|
|
12
12
|
* (the proxy binds the prefix), 1M-token ceiling restored
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* `claude-code-cli` (same directory, symlinked to occ).
|
|
17
17
|
*/
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
|
-
import { existsSync, readdirSync as fsReaddir } from 'node:fs';
|
|
19
|
+
import { existsSync, readdirSync as fsReaddir, readFileSync } from 'node:fs';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
20
21
|
import { homedir, platform } from 'node:os';
|
|
21
22
|
import { dirname, join, sep } from 'node:path';
|
|
22
23
|
import { fileURLToPath } from 'node:url';
|
|
@@ -50,10 +51,34 @@ if (!occ) {
|
|
|
50
51
|
const PROXY_PORT = Number(process.env.OPENZOO_PROXY_PORT || 8402);
|
|
51
52
|
const PROXY_URL = `http://localhost:${PROXY_PORT}/v1`;
|
|
52
53
|
|
|
54
|
+
function mineVersion() {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readFileSync(join(shimRoot, 'package.json'), 'utf8')).version;
|
|
57
|
+
} catch { return ''; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stealPort(port) {
|
|
61
|
+
const n = Number(port);
|
|
62
|
+
for (const cmd of [
|
|
63
|
+
`lsof -t -iTCP:${n} -sTCP:LISTEN | xargs kill -9`,
|
|
64
|
+
`fuser -k ${n}/tcp`,
|
|
65
|
+
]) {
|
|
66
|
+
try { execSync(cmd, { stdio: 'ignore', timeout: 2000, shell: true }); } catch { /* missing */ }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
async function proxyUp() {
|
|
54
71
|
try {
|
|
55
72
|
const r = await fetch(`http://localhost:${PROXY_PORT}/v1/info`, { signal: AbortSignal.timeout(1500) });
|
|
56
|
-
|
|
73
|
+
if (!r.ok) return false;
|
|
74
|
+
const j = await r.json().catch(() => ({}));
|
|
75
|
+
const mine = mineVersion();
|
|
76
|
+
if (mine && String(j.version || '') !== mine) {
|
|
77
|
+
console.error(`claude: stale proxy v${j.version || '?'} on :${PROXY_PORT} — stealing for v${mine}`);
|
|
78
|
+
stealPort(PROXY_PORT);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
57
82
|
} catch { return false; }
|
|
58
83
|
}
|
|
59
84
|
|
package/bin/openzoo.js
CHANGED
|
@@ -330,7 +330,7 @@ async function main() {
|
|
|
330
330
|
}
|
|
331
331
|
case 'ask': {
|
|
332
332
|
const question = process.argv[3];
|
|
333
|
-
if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>]
|
|
333
|
+
if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>]');
|
|
334
334
|
const ci = process.argv.indexOf('--context');
|
|
335
335
|
const mi = process.argv.indexOf('--model');
|
|
336
336
|
// A BARE QUESTION IS A DIFFERENT PRODUCT FROM A BRIEFED ONE.
|
|
@@ -344,20 +344,7 @@ async function main() {
|
|
|
344
344
|
// DHH, Hyprland and theming. Same gateway, same product, one had context.
|
|
345
345
|
// A caller that knows where it is running can now say so.
|
|
346
346
|
const si = process.argv.indexOf('--system');
|
|
347
|
-
|
|
348
|
-
// --web: a keyless DuckDuckGo search, top results injected into THIS
|
|
349
|
-
// call's system prompt. The x402 rail strips OpenRouter's `plugins`
|
|
350
|
-
// field, so search-then-inject has to happen here, on the caller's
|
|
351
|
-
// side. EGRESS: the question text goes to duckduckgo.com. Also on with
|
|
352
|
-
// OPENZOO_ASK_WEB=1; --web-results N caps the count (default 5).
|
|
353
|
-
const wantWeb = process.argv.includes('--web') || process.env.OPENZOO_ASK_WEB === '1';
|
|
354
|
-
if (wantWeb) {
|
|
355
|
-
const wi = process.argv.indexOf('--web-results');
|
|
356
|
-
const n = wi !== -1 ? Number(process.argv[wi + 1]) || 5 : 5;
|
|
357
|
-
const { webSearch, formatWebResults } = await import('../lib/websearch.js');
|
|
358
|
-
const hits = await webSearch(question, n).catch((e) => { console.error(`web search failed: ${e.message}`); return []; });
|
|
359
|
-
if (hits.length) system = (system ? system + '\n\n' : '') + formatWebResults(question, hits);
|
|
360
|
-
}
|
|
347
|
+
const system = si !== -1 ? process.argv[si + 1] : '';
|
|
361
348
|
const { PayClient } = await import('../lib/pay.js');
|
|
362
349
|
const { config } = await import('../lib/config.js');
|
|
363
350
|
const client = new PayClient();
|
|
@@ -402,17 +389,6 @@ async function main() {
|
|
|
402
389
|
case '-h':
|
|
403
390
|
console.log(HELP);
|
|
404
391
|
break;
|
|
405
|
-
case 'version':
|
|
406
|
-
case '--version':
|
|
407
|
-
case '-v':
|
|
408
|
-
case '-V': {
|
|
409
|
-
// Every installer, mise shim and shell script that probes a CLI asks
|
|
410
|
-
// this first; answering "unknown command" to it failed real installs.
|
|
411
|
-
const { readFileSync } = await import('fs');
|
|
412
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
413
|
-
console.log(pkg.version);
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
392
|
default:
|
|
417
393
|
console.error(`unknown command: ${cmd}\n`);
|
|
418
394
|
console.log(HELP);
|
package/lib/aoe.js
CHANGED
|
@@ -287,9 +287,15 @@ export async function setupAoe(argv = []) {
|
|
|
287
287
|
|
|
288
288
|
const base = `http://localhost:${config.port}/v1`;
|
|
289
289
|
if (!flags.has('--no-proxy')) {
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
const { oursOn, packageVersion, killListen } = await import('./proxy.js');
|
|
291
|
+
if (await oursOn(config.port)) {
|
|
292
|
+
console.error(`openzoo: proxy v${packageVersion()} already on ${base}`);
|
|
292
293
|
} else {
|
|
294
|
+
if (await proxyUp(base)) {
|
|
295
|
+
console.error(`openzoo: stale proxy on ${base} — stealing :${config.port}`);
|
|
296
|
+
killListen(config.port);
|
|
297
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
298
|
+
}
|
|
293
299
|
const { pid, logPath } = startDetachedProxy({ tunnel: flags.has('--tunnel') });
|
|
294
300
|
let up = false;
|
|
295
301
|
for (let i = 0; i < 40 && !up; i++) {
|
package/lib/cursorbackend.js
CHANGED
|
@@ -3080,6 +3080,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3080
3080
|
data.error.message = raw;
|
|
3081
3081
|
try {
|
|
3082
3082
|
const { withOnrampLink, isFundInstruction } = await import('./stripeOnramp.js');
|
|
3083
|
+
// Settle/upstream 402s keep the real message. Whop copy-paste only
|
|
3084
|
+
// when this is a genuine empty-wallet / fund-me instruction.
|
|
3083
3085
|
if (isFundInstruction(raw)) {
|
|
3084
3086
|
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
3085
3087
|
const w = loadOrCreateWallet();
|
package/lib/launch.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* harness with the two env vars set. The proxy must already be running
|
|
10
10
|
* (`npx openzoo` in another terminal); we check first and say so if not.
|
|
11
11
|
*/
|
|
12
|
-
import { spawn, spawnSync
|
|
12
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
@@ -191,67 +191,20 @@ export function claudeZooEnv(baseEnv = process.env, { base, port } = {}) {
|
|
|
191
191
|
* Both get ANTHROPIC_BASE_URL so inference pays x402.
|
|
192
192
|
*/
|
|
193
193
|
|
|
194
|
-
/** Omarchy often has no lsof. Kill whatever is bound to the proxy port. */
|
|
195
|
-
function killPort(port) {
|
|
196
|
-
const n = Number(port);
|
|
197
|
-
for (const cmd of [
|
|
198
|
-
`lsof -t -iTCP:${n} -sTCP:LISTEN | xargs kill -9`,
|
|
199
|
-
`fuser -k ${n}/tcp`,
|
|
200
|
-
]) {
|
|
201
|
-
try { execSync(cmd, { stdio: 'ignore', timeout: 2000, shell: true }); } catch { /* tool missing */ }
|
|
202
|
-
}
|
|
203
|
-
// /proc fallback — no lsof/fuser required (Omarchy).
|
|
204
|
-
try {
|
|
205
|
-
execSync(`python3 - ${n}`, {
|
|
206
|
-
stdio: 'ignore', timeout: 2500, input: `import os, glob, sys
|
|
207
|
-
port=int(sys.argv[1]); hx=f'{port:04X}'
|
|
208
|
-
inodes=set()
|
|
209
|
-
for path in ('/proc/net/tcp','/proc/net/tcp6'):
|
|
210
|
-
try:
|
|
211
|
-
for line in open(path):
|
|
212
|
-
p=line.split()
|
|
213
|
-
if len(p)<10: continue
|
|
214
|
-
if p[1].split(':')[-1].upper()==hx: inodes.add(p[9])
|
|
215
|
-
except FileNotFoundError:
|
|
216
|
-
pass
|
|
217
|
-
for fd in glob.glob('/proc/[0-9]*/fd/[0-9]*'):
|
|
218
|
-
try: t=os.readlink(fd)
|
|
219
|
-
except OSError: continue
|
|
220
|
-
if any(ino and ino!='0' and ino in t for ino in inodes):
|
|
221
|
-
try: os.kill(int(fd.split('/')[2]), 9)
|
|
222
|
-
except OSError: pass
|
|
223
|
-
`,
|
|
224
|
-
});
|
|
225
|
-
} catch { /* no python or not linux */ }
|
|
226
|
-
}
|
|
227
194
|
|
|
228
195
|
export async function launchClaude(argv) {
|
|
229
|
-
//
|
|
230
|
-
// below must follow the port we actually bound.
|
|
196
|
+
// NEVER hop. :8402 is the product. startProxy steals a stale listener.
|
|
231
197
|
let base = `http://localhost:${config.port}/v1`;
|
|
232
|
-
// AUTO-START THE PROXY. One command should just work — if nothing is listening
|
|
233
|
-
//
|
|
234
|
-
// foreground below), rather than making the user run `npx openzoo` first.
|
|
198
|
+
// AUTO-START THE PROXY. One command should just work — if nothing is listening
|
|
199
|
+
// (or a leftover npx cache is), boot THIS version on :8402 in this process.
|
|
235
200
|
let up = false;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const info = await r.json().catch(() => ({}));
|
|
244
|
-
const theirs = String(info.version || '');
|
|
245
|
-
if (!theirs || theirs !== mine) {
|
|
246
|
-
process.stderr.write(`openzoo: replacing stale proxy ${theirs ? 'v'+theirs : '(no version)'} with v${mine} on :${config.port}\n`);
|
|
247
|
-
killPort(config.port);
|
|
248
|
-
await new Promise((ok) => setTimeout(ok, 300));
|
|
249
|
-
up = false;
|
|
250
|
-
} else {
|
|
251
|
-
up = true;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} catch { up = false; }
|
|
201
|
+
const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
|
|
202
|
+
const mine = packageVersion();
|
|
203
|
+
if (await oursOn(config.port)) {
|
|
204
|
+
up = true;
|
|
205
|
+
} else {
|
|
206
|
+
process.stderr.write(`openzoo: claiming :${config.port} for v${mine}\n`);
|
|
207
|
+
}
|
|
255
208
|
if (!up) {
|
|
256
209
|
// NEVER GO SILENT DURING STARTUP. silent:true routes the proxy's own lines
|
|
257
210
|
// to ~/.openzoo/proxy.log so payment receipts cannot corrupt Claude Code's
|
|
@@ -268,7 +221,6 @@ export async function launchClaude(argv) {
|
|
|
268
221
|
tick.unref?.();
|
|
269
222
|
const done = (msg) => { clearInterval(tick); process.stderr.write(`\r\x1b[2Kopenzoo: ${msg}\n`); };
|
|
270
223
|
try {
|
|
271
|
-
const { startProxy } = await import('./proxy.js');
|
|
272
224
|
await startProxy({ silent: true, autoTunnel: true });
|
|
273
225
|
} catch (err) {
|
|
274
226
|
// An exception here used to surface as an eternal spinner. Say what broke.
|
|
@@ -277,9 +229,6 @@ export async function launchClaude(argv) {
|
|
|
277
229
|
console.error(' try: OPENZOO_NO_TUNNEL=1 npx openzoo claude (skips the cloudflared download)');
|
|
278
230
|
process.exit(1);
|
|
279
231
|
}
|
|
280
|
-
// The proxy may have healed onto a different port (8402 busy). config.port
|
|
281
|
-
// is the one it ACTUALLY bound, so re-derive every URL from it — the old
|
|
282
|
-
// code kept polling the port it wished for and timed out on a live proxy.
|
|
283
232
|
base = `http://localhost:${config.port}/v1`;
|
|
284
233
|
// PROBE /v1/info, NOT /v1/models. `models` is PROXIED UPSTREAM, so on a
|
|
285
234
|
// network with a bad path to the gateway the local proxy is listening and
|
|
@@ -630,17 +579,12 @@ export async function launchClaude(argv) {
|
|
|
630
579
|
}
|
|
631
580
|
|
|
632
581
|
export async function launchHarness(cmd, args) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (!r.ok) throw new Error(String(r.status));
|
|
640
|
-
} catch {
|
|
641
|
-
console.error(`openzoo: no proxy reachable at ${base}`);
|
|
642
|
-
console.error('start it first in another terminal: npx openzoo');
|
|
643
|
-
process.exit(1);
|
|
582
|
+
let base = `http://localhost:${config.port}/v1`;
|
|
583
|
+
const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
|
|
584
|
+
if (!(await oursOn(config.port))) {
|
|
585
|
+
process.stderr.write(`openzoo: claiming :${config.port} for v${packageVersion()}\n`);
|
|
586
|
+
await startProxy({ silent: true, autoTunnel: true });
|
|
587
|
+
base = `http://localhost:${config.port}/v1`;
|
|
644
588
|
}
|
|
645
589
|
|
|
646
590
|
const env = claudeZooEnv(process.env, { base });
|
package/lib/models.js
CHANGED
|
@@ -168,39 +168,9 @@ const tokensOf = (id) => id.toLowerCase().split(/[^a-z0-9.]+/).filter((t) => t &
|
|
|
168
168
|
* the id is already servable (no rewrite), otherwise the closest zoo id.
|
|
169
169
|
* OPENZOO_DEFAULT_MODEL is an explicit user override, not a fallback tier.
|
|
170
170
|
*/
|
|
171
|
-
/**
|
|
172
|
-
* UNOPENROUTER. OpenRouter is not an upstream any more (gateway, 2026-09-02):
|
|
173
|
-
* every completion is bought from an x402 door, and doors publish BARE ids
|
|
174
|
-
* (`grok-4.3`, `claude-sonnet-5`, `gemini-2.5-flash`). A harness that still
|
|
175
|
-
* sends the OpenRouter spelling (`x-ai/grok-4.3`, `anthropic/claude-sonnet-5`)
|
|
176
|
-
* is rewritten to the bare id whenever the live catalog serves that bare id.
|
|
177
|
-
*
|
|
178
|
-
* OPENZOO_UNOPENROUTER=1 strip the vendor prefix ALWAYS, catalog or not
|
|
179
|
-
* OPENZOO_UNOPENROUTER=0 never strip
|
|
180
|
-
* unset strip when the bare id is in the catalog
|
|
181
|
-
*
|
|
182
|
-
* Router aliases and `openzoo-` twins are never touched: their slash is not a
|
|
183
|
-
* vendor.
|
|
184
|
-
*/
|
|
185
|
-
export function unopenrouter(requested, ids) {
|
|
186
|
-
const mode = process.env.OPENZOO_UNOPENROUTER;
|
|
187
|
-
if (mode === '0') return null;
|
|
188
|
-
const s = String(requested || '');
|
|
189
|
-
if (!s || isAutoModel(s) || /^openzoo[-/]/i.test(s)) return null;
|
|
190
|
-
const slash = s.indexOf('/');
|
|
191
|
-
if (slash <= 0) return null;
|
|
192
|
-
const bare = s.slice(slash + 1);
|
|
193
|
-
if (!bare || bare.includes('/')) return null;
|
|
194
|
-
if (mode === '1') return bare;
|
|
195
|
-
return Array.isArray(ids) && ids.includes(bare) ? bare : null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
171
|
export function resolveModel(requested, ids) {
|
|
199
172
|
// Virtual router id — never family-match, never steal via OPENZOO_DEFAULT_MODEL.
|
|
200
173
|
if (isAutoModel(requested)) return null;
|
|
201
|
-
// Vendor-prefixed OpenRouter spelling → the bare id the doors serve.
|
|
202
|
-
const bareVendor = unopenrouter(requested, ids);
|
|
203
|
-
if (bareVendor) return bareVendor;
|
|
204
174
|
// Bare Anthropic / Claude Code ids are never live on Fly/OpenRouter
|
|
205
175
|
// (`claude-opus-5` → 500 unknown model). Rewrite even on a catalog miss
|
|
206
176
|
// or if a gateway row lists the bare name — the request must not leave
|
package/lib/ozSpendChip.js
CHANGED
|
@@ -14,7 +14,7 @@ import { spendChipLabel } from './spendProof.js';
|
|
|
14
14
|
|
|
15
15
|
export const GROKBOT_CDP_PORT = Number(process.env.OZ_GROKBOT_CDP_PORT || 9444);
|
|
16
16
|
|
|
17
|
-
/** Session totals for
|
|
17
|
+
/** Session totals for the always-on HUD pill. Message chips still fold the footer. */
|
|
18
18
|
export function sessionSpendLabel(home = os.homedir()) {
|
|
19
19
|
try {
|
|
20
20
|
const s = JSON.parse(fs.readFileSync(path.join(home, '.openzoo', 'session.json'), 'utf8'));
|
|
@@ -159,7 +159,9 @@ function ozEnsureSpendCss() {
|
|
|
159
159
|
}
|
|
160
160
|
s.textContent = [
|
|
161
161
|
'.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
|
|
162
|
-
'#oz-spend-
|
|
162
|
+
'#oz-spend-hud{opacity:1}',
|
|
163
|
+
'#oz-spend-hud>summary{cursor:grab;background:rgba(28,28,30,.94);color:#f4f4f5;',
|
|
164
|
+
'border-color:rgba(255,255,255,.28);box-shadow:0 1px 8px rgba(0,0,0,.35)}',
|
|
163
165
|
'.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
|
|
164
166
|
'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18);white-space:nowrap;',
|
|
165
167
|
'max-width:100%;overflow:hidden;text-overflow:ellipsis}',
|
|
@@ -213,11 +215,19 @@ export function stripSpendFromText(s) {
|
|
|
213
215
|
return t;
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
function ozInComposer(el) {
|
|
219
|
+
if (!el) return false;
|
|
220
|
+
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT' || el.isContentEditable) return true;
|
|
221
|
+
return !!(el.closest && el.closest('textarea, input, [contenteditable="true"]'));
|
|
222
|
+
}
|
|
223
|
+
|
|
216
224
|
function ozBlankSpendIn(root) {
|
|
225
|
+
if (ozInComposer(root)) return;
|
|
217
226
|
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
218
227
|
while (w.nextNode()) {
|
|
219
228
|
const tn = w.currentNode;
|
|
220
229
|
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
230
|
+
if (ozInComposer(tn.parentElement)) continue;
|
|
221
231
|
const next = stripSpendFromText(tn.nodeValue || '');
|
|
222
232
|
if (next !== tn.nodeValue) tn.nodeValue = next;
|
|
223
233
|
}
|
|
@@ -228,6 +238,7 @@ function ozHideSpendLeftovers() {
|
|
|
228
238
|
for (let i = 0; i < nodes.length; i += 1) {
|
|
229
239
|
const el = nodes[i];
|
|
230
240
|
if (el.closest && el.closest('.oz-spend')) continue;
|
|
241
|
+
if (ozInComposer(el)) continue;
|
|
231
242
|
if (el.querySelector && el.querySelector('.oz-spend')) continue;
|
|
232
243
|
const vis = ozVisibleSpendText(el);
|
|
233
244
|
if (!spendOnlyText(vis)) continue;
|
|
@@ -290,10 +301,11 @@ function ozCollapseSpend() {
|
|
|
290
301
|
for (let i = 0; i < nodes.length; i += 1) {
|
|
291
302
|
const el = nodes[i];
|
|
292
303
|
if (el.closest && el.closest('.oz-spend')) continue;
|
|
304
|
+
if (ozInComposer(el)) continue;
|
|
293
305
|
const t = ozVisibleSpendText(el);
|
|
294
306
|
if (!/::oz-spend::|this call \$|spent \$/i.test(t)) continue;
|
|
295
307
|
const host = ozSpendHost(el);
|
|
296
|
-
if (!host || seen.has(host)) continue;
|
|
308
|
+
if (!host || seen.has(host) || ozInComposer(host)) continue;
|
|
297
309
|
seen.add(host);
|
|
298
310
|
hosts.push(host);
|
|
299
311
|
}
|
|
@@ -302,10 +314,11 @@ function ozCollapseSpend() {
|
|
|
302
314
|
const vis = ozVisibleSpendText(host);
|
|
303
315
|
const split = splitSpendText(vis);
|
|
304
316
|
if (!split || !split.body || split.body.length < 12) continue;
|
|
317
|
+
if (split.summary) window.__OZ_SPEND_LAST__ = split.summary;
|
|
305
318
|
ozEnsureSpendCss();
|
|
306
319
|
if (spendOnlyText(vis)) {
|
|
307
320
|
const prev = ozPreviousMessageCard(host);
|
|
308
|
-
if (prev && prev !== host) {
|
|
321
|
+
if (prev && prev !== host && !ozInComposer(prev)) {
|
|
309
322
|
ozAttachSpendChip(prev, split);
|
|
310
323
|
host.setAttribute('data-oz-spend-hide', '1');
|
|
311
324
|
continue;
|
|
@@ -318,18 +331,46 @@ function ozCollapseSpend() {
|
|
|
318
331
|
ozEnsureFloatSpend();
|
|
319
332
|
}
|
|
320
333
|
|
|
334
|
+
/** Live HUD label: latest folded chip, then a read-only canvas scan, then inject snapshot. */
|
|
335
|
+
function ozFloatLabel() {
|
|
336
|
+
const pills = document.querySelectorAll('.oz-spend:not(#oz-spend-hud):not(#oz-spend-float) summary');
|
|
337
|
+
if (pills.length) {
|
|
338
|
+
const lab = String(pills[pills.length - 1].textContent || '').replace(/^ⓘ\s*/, '').trim();
|
|
339
|
+
if (lab) {
|
|
340
|
+
window.__OZ_SPEND_LAST__ = lab;
|
|
341
|
+
return lab;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const nodes = document.querySelectorAll('[class*="sand-message"]');
|
|
346
|
+
for (let i = nodes.length - 1; i >= 0; i -= 1) {
|
|
347
|
+
if (ozInComposer(nodes[i])) continue;
|
|
348
|
+
const vis = ozVisibleSpendText(nodes[i]);
|
|
349
|
+
if (!/::oz-spend::|this call \$|spent \$/i.test(vis)) continue;
|
|
350
|
+
const split = splitSpendText(vis);
|
|
351
|
+
if (split && split.summary) {
|
|
352
|
+
window.__OZ_SPEND_LAST__ = split.summary;
|
|
353
|
+
return split.summary;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {}
|
|
357
|
+
if (window.__OZ_SPEND_LAST__) return String(window.__OZ_SPEND_LAST__);
|
|
358
|
+
return String(window.__OZ_SESSION_SPEND__ || '').trim();
|
|
359
|
+
}
|
|
360
|
+
|
|
321
361
|
function ozEnsureFloatSpend() {
|
|
322
|
-
const label =
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
362
|
+
const label = ozFloatLabel();
|
|
363
|
+
const leftover = document.getElementById('oz-spend-float');
|
|
364
|
+
if (leftover) leftover.remove();
|
|
365
|
+
let el = document.getElementById('oz-spend-hud');
|
|
366
|
+
if (!label) {
|
|
326
367
|
if (el) el.remove();
|
|
327
368
|
return;
|
|
328
369
|
}
|
|
329
370
|
ozEnsureSpendCss();
|
|
330
371
|
if (!el) {
|
|
331
372
|
el = document.createElement('details');
|
|
332
|
-
el.id = 'oz-spend-
|
|
373
|
+
el.id = 'oz-spend-hud';
|
|
333
374
|
el.className = 'oz-spend';
|
|
334
375
|
const sum = document.createElement('summary');
|
|
335
376
|
const body = document.createElement('div');
|
|
@@ -357,7 +398,7 @@ function ozPlaceFloat(el) {
|
|
|
357
398
|
if (!el) return;
|
|
358
399
|
el.style.position = 'fixed';
|
|
359
400
|
el.style.zIndex = '2147483646';
|
|
360
|
-
el.style.opacity = '
|
|
401
|
+
el.style.opacity = '1';
|
|
361
402
|
const saved = ozSavedFloatPos();
|
|
362
403
|
if (saved) {
|
|
363
404
|
el.style.left = Math.max(8, saved.left) + 'px';
|
|
@@ -416,29 +457,36 @@ function ozDragFloat(el) {
|
|
|
416
457
|
|
|
417
458
|
function ozWatchSpend() {
|
|
418
459
|
let t = 0;
|
|
460
|
+
const inComposer = () => ozInComposer(document.activeElement);
|
|
419
461
|
const run = () => {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
462
|
+
try {
|
|
463
|
+
if (inComposer()) {
|
|
464
|
+
ozEnsureSpendCss();
|
|
465
|
+
ozEnsureFloatSpend();
|
|
466
|
+
} else {
|
|
467
|
+
ozCollapseSpend();
|
|
468
|
+
}
|
|
469
|
+
} catch (e) {}
|
|
423
470
|
};
|
|
424
|
-
const debounced = () => { clearTimeout(t); t = setTimeout(run,
|
|
471
|
+
const debounced = () => { clearTimeout(t); t = setTimeout(run, 400); };
|
|
425
472
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
|
|
426
473
|
else run();
|
|
427
474
|
try {
|
|
428
475
|
const mo = new MutationObserver(debounced);
|
|
429
|
-
const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree:
|
|
476
|
+
const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree: true }); };
|
|
430
477
|
if (document.body) start();
|
|
431
478
|
else document.addEventListener('DOMContentLoaded', start);
|
|
432
479
|
} catch (e) {}
|
|
433
480
|
try { window.addEventListener('resize', debounced); } catch (e) {}
|
|
481
|
+
try { setInterval(run, 2000); } catch (e) {}
|
|
434
482
|
}
|
|
435
483
|
|
|
436
484
|
export function spendChipSource() {
|
|
437
485
|
return [
|
|
438
486
|
'(function ozSpendChip(){',
|
|
439
487
|
"'use strict';",
|
|
440
|
-
'if (window.__OZ_SPEND_CHIP__ ===
|
|
441
|
-
'window.__OZ_SPEND_CHIP__ =
|
|
488
|
+
'if (window.__OZ_SPEND_CHIP__ === 13) return;',
|
|
489
|
+
'window.__OZ_SPEND_CHIP__ = 13;',
|
|
442
490
|
chipUsd.toString(),
|
|
443
491
|
labelFromSpendBody.toString(),
|
|
444
492
|
spendLinesOnly.toString(),
|
|
@@ -448,11 +496,13 @@ export function spendChipSource() {
|
|
|
448
496
|
ozSpendHost.toString(),
|
|
449
497
|
ozVisibleSpendText.toString(),
|
|
450
498
|
stripSpendFromText.toString(),
|
|
499
|
+
ozInComposer.toString(),
|
|
451
500
|
ozBlankSpendIn.toString(),
|
|
452
501
|
ozHideSpendLeftovers.toString(),
|
|
453
502
|
ozPreviousMessageCard.toString(),
|
|
454
503
|
ozAttachSpendChip.toString(),
|
|
455
504
|
ozCollapseSpend.toString(),
|
|
505
|
+
ozFloatLabel.toString(),
|
|
456
506
|
ozEnsureFloatSpend.toString(),
|
|
457
507
|
ozSavedFloatPos.toString(),
|
|
458
508
|
ozPlaceFloat.toString(),
|
package/lib/proxy.js
CHANGED
|
@@ -38,19 +38,89 @@ import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettl
|
|
|
38
38
|
import { fetchHeaders } from './fetch.js';
|
|
39
39
|
import { attachX402Proof } from './spendProof.js';
|
|
40
40
|
|
|
41
|
-
/** Kill whatever is LISTEN on this port except this process.
|
|
41
|
+
/** Kill whatever is LISTEN on this port except this process.
|
|
42
|
+
* lsof first (macOS), then fuser, then /proc (Omarchy/Arch with neither).
|
|
43
|
+
* Every openzoo subcommand that binds :8402 goes through this. Hopping to
|
|
44
|
+
* 8403 was the second-burner bug. */
|
|
42
45
|
export function killListen(port, run = execSync) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const n = Number(port);
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const addFrom = (out) => {
|
|
49
|
+
for (const tok of String(out || '').split(/[^\d]+/)) {
|
|
50
|
+
const pid = Number(tok);
|
|
51
|
+
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid) seen.add(pid);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const tryRun = (cmd, opts = {}) => {
|
|
55
|
+
try {
|
|
56
|
+
return run(cmd, { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
57
|
+
} catch { return ''; }
|
|
58
|
+
};
|
|
59
|
+
addFrom(tryRun(`lsof -nP -iTCP:${n} -sTCP:LISTEN -t`));
|
|
60
|
+
addFrom(tryRun(`fuser -n tcp ${n}`));
|
|
61
|
+
if (process.platform === 'linux' && seen.size === 0) {
|
|
62
|
+
addFrom(tryRun(`python3 - ${n}`, {
|
|
63
|
+
timeout: 2500,
|
|
64
|
+
input: `import os, glob, sys
|
|
65
|
+
port=int(sys.argv[1]); hx=f'{port:04X}'
|
|
66
|
+
inodes=set(); pids=set()
|
|
67
|
+
for path in ('/proc/net/tcp','/proc/net/tcp6'):
|
|
68
|
+
try:
|
|
69
|
+
for line in open(path):
|
|
70
|
+
p=line.split()
|
|
71
|
+
if len(p)<10: continue
|
|
72
|
+
if p[1].split(':')[-1].upper()==hx: inodes.add(p[9])
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
pass
|
|
75
|
+
for fd in glob.glob('/proc/[0-9]*/fd/[0-9]*'):
|
|
76
|
+
try: t=os.readlink(fd)
|
|
77
|
+
except OSError: continue
|
|
78
|
+
if any(ino and ino!='0' and ino in t for ino in inodes):
|
|
79
|
+
try: pids.add(int(fd.split('/')[2]))
|
|
80
|
+
except (OSError, ValueError): pass
|
|
81
|
+
print('\\n'.join(str(p) for p in pids if p != os.getpid()))
|
|
82
|
+
`,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
for (const pid of seen) {
|
|
86
|
+
try { run(`kill ${pid}`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
87
|
+
try { run(`kill -9 ${pid}`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
88
|
+
}
|
|
89
|
+
return [...seen];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** This package.json version. Same one /v1/info and /v1/session publish. */
|
|
93
|
+
export function packageVersion() {
|
|
94
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True when the listener on this port is THIS openzoo version or newer.
|
|
98
|
+
* Missing / unparseable version is stale (0.49.x answered with no version).
|
|
99
|
+
* Older than us is stale. Newer we leave alone so we never downgrade. */
|
|
100
|
+
export async function oursOn(port = config.port) {
|
|
101
|
+
const mine = packageVersion();
|
|
102
|
+
const parse = (v) => {
|
|
103
|
+
const m = String(v || '').trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
104
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
105
|
+
};
|
|
106
|
+
const attachable = (theirs) => {
|
|
107
|
+
const a = parse(theirs), b = parse(mine);
|
|
108
|
+
if (!a || !b) return false;
|
|
109
|
+
for (let i = 0; i < 3; i++) {
|
|
110
|
+
if (a[i] > b[i]) return true;
|
|
111
|
+
if (a[i] < b[i]) return false;
|
|
49
112
|
}
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
for (const path of ['/v1/info', '/v1/session']) {
|
|
116
|
+
try {
|
|
117
|
+
const r = await fetch(`http://127.0.0.1:${Number(port)}${path}`, { signal: AbortSignal.timeout(1500) });
|
|
118
|
+
if (!r.ok) continue;
|
|
119
|
+
const j = await r.json().catch(() => ({}));
|
|
120
|
+
if (attachable(j.version)) return true;
|
|
121
|
+
} catch { /* try next */ }
|
|
53
122
|
}
|
|
123
|
+
return false;
|
|
54
124
|
}
|
|
55
125
|
|
|
56
126
|
// THE SHIM IS A FACILITATOR, NOT A MIDDLEBOX.
|
|
@@ -904,11 +974,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
904
974
|
// OPENZOO_BIND=0.0.0.0 AND a tunnel token, so that port stays gated exactly
|
|
905
975
|
// like the public tunnel path.
|
|
906
976
|
const bindHost = process.env.OPENZOO_BIND || '127.0.0.1';
|
|
907
|
-
// SELF-HEAL
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
977
|
+
// SELF-HEAL :8402. Kill whoever is on THIS port, then bind it. Never hop to
|
|
978
|
+
// 8403 — that was the second-burner. Every subcommand that calls startProxy
|
|
979
|
+
// (openzoo, claude, bot, web, cursor, aoe, grok, tunnel) goes through here.
|
|
980
|
+
// Reusing a leftover listener left a stale PayClient serving $0 after a
|
|
981
|
+
// TOKEN top-up, and an old npx cache answering /v1 with no version.
|
|
982
|
+
{
|
|
983
|
+
const pids = killListen(config.port);
|
|
984
|
+
if (pids.length) {
|
|
985
|
+
say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
986
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
912
989
|
for (let attempt = 0; ; attempt++) {
|
|
913
990
|
try {
|
|
914
991
|
await new Promise((resolve, reject) => {
|
|
@@ -918,20 +995,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
918
995
|
});
|
|
919
996
|
break;
|
|
920
997
|
} catch (e) {
|
|
921
|
-
if (e?.code !== 'EADDRINUSE' || attempt >=
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
926
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
config.port += 1;
|
|
931
|
-
say(`openzoo: :${config.port - 1} busy — trying :${config.port}`);
|
|
998
|
+
if (e?.code !== 'EADDRINUSE' || attempt >= 8) throw e;
|
|
999
|
+
const pids = killListen(config.port);
|
|
1000
|
+
if (pids.length) say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
1001
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
932
1002
|
}
|
|
933
1003
|
}
|
|
934
|
-
if (config.port !== wanted) say(`openzoo: listening on :${config.port} (:${wanted} was busy)`);
|
|
935
1004
|
|
|
936
1005
|
// AUTO-PREPAY. Paying on-chain per call is where the latency lives: credit
|
|
937
1006
|
// is applied automatically server-side whenever a balance covers the quote,
|
package/lib/setup.js
CHANGED
|
@@ -214,7 +214,7 @@ async function printStartupDiagnostic(base, which) {
|
|
|
214
214
|
|
|
215
215
|
console.log('openzoo diagnostic');
|
|
216
216
|
console.log(` version : ${version} node ${process.version} ${process.platform}/${process.arch}`);
|
|
217
|
-
console.log(` port ${config.port} : ${portBusy ? '
|
|
217
|
+
console.log(` port ${config.port} : ${portBusy ? 'occupied (steal unless this exact version)' : 'free'}`);
|
|
218
218
|
console.log(` editor : ${picked ? `${picked.which} @ ${picked.cmd}` : 'NONE FOUND'}`);
|
|
219
219
|
console.log(` running : ${picked ? (q(() => editorRunning(picked.which), false) ? 'yes — will be quit so settings stick' : 'no') : '-'}`);
|
|
220
220
|
console.log(` backend : ${hosts}`);
|
|
@@ -245,9 +245,17 @@ export async function setupEditor(which, target) {
|
|
|
245
245
|
// Declared out here: the tunnel-rebind hook is registered further down, well
|
|
246
246
|
// outside the block that starts the proxy.
|
|
247
247
|
let started = null;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
|
|
249
|
+
if (await oursOn(config.port)) {
|
|
250
|
+
console.log(`proxy v${packageVersion()} already on ${base} — keeping it`);
|
|
251
|
+
try {
|
|
252
|
+
const info = await (await fetch(`${base}/info`)).json();
|
|
253
|
+
publicUrl = (info?.publicTunnel || '').replace(/\/v1$/, '') || null;
|
|
254
|
+
tunnelKey = info?.tunnelToken ?? tunnelKey;
|
|
255
|
+
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 (from the running proxy)`);
|
|
256
|
+
} catch { /* no /info */ }
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`${(await proxyUp(base)) ? 'stale proxy — stealing' : 'starting proxy on'} ${base} (+ public tunnel)...`);
|
|
251
259
|
started = await startProxy({ silent: true, autoTunnel: true });
|
|
252
260
|
publicUrl = started?.publicUrl ?? null;
|
|
253
261
|
tunnelKey = started?.tunnelToken ?? null;
|
|
@@ -298,15 +306,6 @@ export async function setupEditor(which, target) {
|
|
|
298
306
|
console.log(' most common cause here: no working IPv6 route (we already force');
|
|
299
307
|
console.log(' --edge-ip-version 4). check: npx openzoo tunnel for the raw log.');
|
|
300
308
|
}
|
|
301
|
-
} else {
|
|
302
|
-
console.log(`proxy already running on ${base}`);
|
|
303
|
-
// A proxy someone else started owns the tunnel; ask it for the public URL.
|
|
304
|
-
try {
|
|
305
|
-
const info = await (await fetch(`${base}/info`)).json();
|
|
306
|
-
publicUrl = (info?.publicTunnel || '').replace(/\/v1$/, '') || null;
|
|
307
|
-
tunnelKey = info?.tunnelToken ?? tunnelKey;
|
|
308
|
-
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 (from the running proxy)`);
|
|
309
|
-
} catch { /* no /info — fall through to the localhost warning below */ }
|
|
310
309
|
}
|
|
311
310
|
// What the EDITOR is configured with. Localhost only as a last resort, and
|
|
312
311
|
// said out loud, because it will fail with the private-networks error.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
4
|
-
"description": "Local x402-paying proxy + MCP server for openzoo.fun
|
|
3
|
+
"version": "0.50.93",
|
|
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",
|
|
7
7
|
"bin": {
|
package/lib/websearch.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Keyless web search for `openzoo ask --web`: DuckDuckGo's HTML endpoint,
|
|
2
|
-
// scraped for title / url / snippet. No API key, no account, one GET. The
|
|
3
|
-
// only thing that leaves is the question text, to duckduckgo.com.
|
|
4
|
-
const strip = (s) => String(s || '')
|
|
5
|
-
.replace(/<[^>]+>/g, '')
|
|
6
|
-
.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>')
|
|
7
|
-
.replace(/\s+/g, ' ').trim();
|
|
8
|
-
|
|
9
|
-
export async function webSearch(query, max = 5) {
|
|
10
|
-
const res = await fetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), {
|
|
11
|
-
headers: { 'user-agent': 'Mozilla/5.0 openzoo-ask/1.0' },
|
|
12
|
-
signal: AbortSignal.timeout(12_000),
|
|
13
|
-
});
|
|
14
|
-
if (!res.ok) throw new Error(`duckduckgo HTTP ${res.status}`);
|
|
15
|
-
const html = await res.text();
|
|
16
|
-
const out = [];
|
|
17
|
-
const re = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
18
|
-
let m;
|
|
19
|
-
while ((m = re.exec(html)) && out.length < Math.max(1, Math.min(10, max))) {
|
|
20
|
-
let url = m[1];
|
|
21
|
-
const redirected = url.match(/uddg=([^&]+)/);
|
|
22
|
-
if (redirected) url = decodeURIComponent(redirected[1]);
|
|
23
|
-
out.push({ title: strip(m[2]), url, snippet: strip(m[3]).slice(0, 400) });
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function formatWebResults(query, hits) {
|
|
29
|
-
const lines = hits.map((h, i) => `${i + 1}. ${h.title} — ${h.url}\n ${h.snippet}`);
|
|
30
|
-
return `Web search results for "${query}" (DuckDuckGo, fetched just now; cite the url when you rely on one):\n${lines.join('\n')}`;
|
|
31
|
-
}
|