openzoo 0.50.88 → 0.50.90
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 +15 -2
- package/lib/cursorbackend.js +13 -10
- package/lib/proxy.js +13 -14
- package/lib/stripeOnramp.js +50 -0
- package/lib/websearch.js +31 -0
- package/package.json +2 -2
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>] [--web [--web-results N]]');
|
|
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,7 +344,20 @@ 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
|
-
|
|
347
|
+
let system = si !== -1 ? process.argv[si + 1] : '';
|
|
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
|
+
}
|
|
348
361
|
const { PayClient } = await import('../lib/pay.js');
|
|
349
362
|
const { config } = await import('../lib/config.js');
|
|
350
363
|
const client = new PayClient();
|
package/lib/cursorbackend.js
CHANGED
|
@@ -3075,17 +3075,20 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3075
3075
|
if (r.status === 402) {
|
|
3076
3076
|
const raw = zooTextFromMessage(data?.choices?.[0]?.message, data)
|
|
3077
3077
|
|| data?.error?.message
|
|
3078
|
-
|| 'openzoo
|
|
3078
|
+
|| 'openzoo payment required (HTTP 402).';
|
|
3079
|
+
data.error = data.error || {};
|
|
3080
|
+
data.error.message = raw;
|
|
3079
3081
|
try {
|
|
3080
|
-
const { withOnrampLink } = await import('./stripeOnramp.js');
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3082
|
+
const { withOnrampLink, isFundInstruction } = await import('./stripeOnramp.js');
|
|
3083
|
+
if (isFundInstruction(raw)) {
|
|
3084
|
+
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
3085
|
+
const w = loadOrCreateWallet();
|
|
3086
|
+
const usd = Number(String(raw).match(/≈\$([0-9.]+)/)?.[1]);
|
|
3087
|
+
data.error.message = await withOnrampLink(raw, {
|
|
3088
|
+
solana: w.keypair.publicKey.toBase58(),
|
|
3089
|
+
usd,
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3089
3092
|
} catch { /* keep proxy copy */ }
|
|
3090
3093
|
}
|
|
3091
3094
|
return { r, data };
|
package/lib/proxy.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from './config.js';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
11
|
import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
|
|
12
|
-
import { withOnrampLink } from './stripeOnramp.js';
|
|
12
|
+
import { withOnrampLink, settleFailCopy, isFundInstruction } from './stripeOnramp.js';
|
|
13
13
|
import { tokenBalance } from './x402.js';
|
|
14
14
|
import { evmTokenBalance } from './evm.js';
|
|
15
15
|
import { autoContext } from './autobind.js';
|
|
@@ -854,7 +854,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
854
854
|
// half-kilobyte accepts[] blob at the user instead of the one thing they
|
|
855
855
|
// need: the price, what the wallet holds, and where to send funds.
|
|
856
856
|
if (response.status === 402) {
|
|
857
|
-
let quoted = '';
|
|
858
857
|
let usd;
|
|
859
858
|
let q402 = null;
|
|
860
859
|
try { q402 = await response.clone().json(); } catch { q402 = null; }
|
|
@@ -871,18 +870,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
871
870
|
return;
|
|
872
871
|
}
|
|
873
872
|
try {
|
|
874
|
-
|
|
875
|
-
usd =
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
{ solana: client.address, usd }
|
|
883
|
-
|
|
884
|
-
log(/ties to your account/i.test(msg) ? 'onramp: whop + copy-paste solana' :
|
|
885
|
-
jsonErr(res, 402, msg);
|
|
873
|
+
usd = Number(q402?.accepts?.[0]?.extra?.billedUsd);
|
|
874
|
+
if (!Number.isFinite(usd)) usd = undefined;
|
|
875
|
+
} catch { usd = undefined; }
|
|
876
|
+
const copy = settleFailCopy(q402);
|
|
877
|
+
let msg = copy.message;
|
|
878
|
+
const wantOnramp = copy.code === 'insufficient_funds'
|
|
879
|
+
|| (!paid && isFundInstruction(copy.reason, copy));
|
|
880
|
+
if (wantOnramp) {
|
|
881
|
+
msg = await withOnrampLink(msg, { solana: client.address, usd, code: copy.code });
|
|
882
|
+
}
|
|
883
|
+
log(/ties to your account/i.test(msg) ? 'onramp: whop + copy-paste solana' : `402: ${msg.slice(0, 140)}`);
|
|
884
|
+
jsonErr(res, paid ? copy.status : 402, msg);
|
|
886
885
|
return;
|
|
887
886
|
}
|
|
888
887
|
await relay(res, response, meterStreamed);
|
package/lib/stripeOnramp.js
CHANGED
|
@@ -103,8 +103,58 @@ export function whopFundBlurb(solana) {
|
|
|
103
103
|
].join('\n');
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Genuine empty-wallet / fund-me copy. A post-pay settle failure
|
|
108
|
+
* ("payment did not settle" with a gateway reason and no underfunded
|
|
109
|
+
* wording) is NOT this — those wallets are often funded; the 402 is
|
|
110
|
+
* the facilitator or upstream.
|
|
111
|
+
*/
|
|
112
|
+
export function isFundInstruction(text, extra = {}) {
|
|
113
|
+
const code = extra.code ?? extra.advice?.code;
|
|
114
|
+
if (String(code || '') === 'insufficient_funds') return true;
|
|
115
|
+
const s = String(text || '');
|
|
116
|
+
if (!s) return false;
|
|
117
|
+
if (/\b(?:wallet underfunded|empty wallet|wallet is empty|needs more than the wallet holds|insufficient[_\s]funds)\b/i.test(s)) return true;
|
|
118
|
+
if (/\bunderfunded\b/i.test(s)) return true;
|
|
119
|
+
if (/\bsend (?:usdc|a few cents)\b/i.test(s)) return true;
|
|
120
|
+
if (/\bno offered payment row is affordable/i.test(s)) return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function gatewayReason(q402) {
|
|
125
|
+
if (!q402 || typeof q402 !== 'object') return '';
|
|
126
|
+
const err = q402.error;
|
|
127
|
+
const advice = q402.advice;
|
|
128
|
+
if (typeof err?.message === 'string' && err.message.trim()) return err.message.trim();
|
|
129
|
+
if (typeof err === 'string' && err.trim()) return err.trim();
|
|
130
|
+
if (typeof advice?.message === 'string' && advice.message.trim()) return advice.message.trim();
|
|
131
|
+
if (typeof advice === 'string' && advice.trim()) return advice.trim();
|
|
132
|
+
if (advice && typeof advice === 'object') {
|
|
133
|
+
const bits = [advice.code, advice.reason, advice.detail].filter((x) => typeof x === 'string' && x.trim());
|
|
134
|
+
if (bits.length) return bits.join(': ');
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Copy for a 402 AFTER PayClient already signed and retried (paid:true).
|
|
141
|
+
* Never "wallet underfunded" — that string is reserved for preflight
|
|
142
|
+
* empty-wallet errors. Prefix stays greppable as "payment did not settle".
|
|
143
|
+
*/
|
|
144
|
+
export function settleFailCopy(q402) {
|
|
145
|
+
const reason = gatewayReason(q402);
|
|
146
|
+
const code = q402?.advice?.code || q402?.error?.code || '';
|
|
147
|
+
const fund = isFundInstruction(reason, { code, advice: q402?.advice });
|
|
148
|
+
const message = reason
|
|
149
|
+
? `openzoo payment did not settle: ${reason}`
|
|
150
|
+
: 'openzoo payment did not settle';
|
|
151
|
+
const upstreamish = /upstream|facilitator|internal(?: server)? error|settle(?:ment)? (?:failed|error)/i.test(reason) && !fund;
|
|
152
|
+
return { message, status: upstreamish ? 502 : 402, fund, reason, code: String(code || '') };
|
|
153
|
+
}
|
|
154
|
+
|
|
106
155
|
export async function withOnrampLink(text, dest) {
|
|
107
156
|
const body = String(text || '').trim();
|
|
157
|
+
if (!isFundInstruction(body, dest)) return body;
|
|
108
158
|
const blurb = whopFundBlurb(dest?.solana);
|
|
109
159
|
if (!blurb) return body;
|
|
110
160
|
if (/ties to your account/i.test(body) && body.includes(String(dest.solana))) return body;
|
package/lib/websearch.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
}
|
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.90",
|
|
4
|
+
"description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 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": {
|