openzoo 0.50.0 → 0.50.2
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 +26 -0
- package/lib/dotenv.js +37 -0
- package/lib/pay.js +16 -2
- package/lib/proxy.js +1 -8
- package/lib/receipts.js +139 -0
- package/lib/sonar.js +1879 -0
- package/lib/voice.js +714 -0
- package/lib/voiceserve.js +84 -0
- package/lib/voicewatch.js +127 -0
- package/lib/x402.js +21 -4
- package/lib/xbot.js +553 -14
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -113,6 +113,15 @@ usage:
|
|
|
113
113
|
and launches Grok Bot with Node TLS override (sudo required;
|
|
114
114
|
ctrl-c restores /etc/hosts).
|
|
115
115
|
--no-takeover plain launch · --no-launch config only
|
|
116
|
+
npx openzoo voice write like YOU — your own exports, paid per call
|
|
117
|
+
voice ingest --telegram <telegram_messages.txt> --twitter <archive dir>
|
|
118
|
+
parse your history into turns and bind the tier cascade
|
|
119
|
+
(cream / all telegram / all twitter) to the gateway + leCore
|
|
120
|
+
voice card --telegram <file> distil the style card (one paid call)
|
|
121
|
+
voice say "draft" rewrite a draft in your voice
|
|
122
|
+
voice login / voice watch PREHOOK your own outgoing Telegram
|
|
123
|
+
messages: type raw, the watcher revises in place (userbot;
|
|
124
|
+
"." prefix sends raw). X has no edit API — no equivalent.
|
|
116
125
|
npx openzoo openclaw write the zoo into ~/.openclaw/openclaw.json as a model
|
|
117
126
|
provider WITH REAL PRICES (OpenClaw's own custom-provider
|
|
118
127
|
path hard-codes $0.00 and ignores /v1/models pricing)
|
|
@@ -171,6 +180,23 @@ async function main() {
|
|
|
171
180
|
case 'openclaw':
|
|
172
181
|
await (await import('../lib/openclaw.js')).setupOpenClaw(process.argv.slice(3));
|
|
173
182
|
break;
|
|
183
|
+
case 'voice':
|
|
184
|
+
await (await import('../lib/voice.js')).runVoice(process.argv.slice(3));
|
|
185
|
+
break;
|
|
186
|
+
case 'sonar':
|
|
187
|
+
await (await import('../lib/sonar.js')).runSonar(process.argv.slice(3));
|
|
188
|
+
break;
|
|
189
|
+
// @openzoobot. runXBot was only ever reachable as a library export, so the
|
|
190
|
+
// obvious `openzoo xbot` printed usage and did nothing.
|
|
191
|
+
case 'xbot': {
|
|
192
|
+
const a = process.argv.slice(3);
|
|
193
|
+
await (await import('../lib/xbot.js')).runXBot({
|
|
194
|
+
once: a.includes('--once'),
|
|
195
|
+
dryRun: a.includes('--dry-run') || a.includes('--dry'),
|
|
196
|
+
seed: a.includes('--seed'),
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
174
200
|
case 'mcp':
|
|
175
201
|
await (await import('../lib/mcp.js')).startMcp();
|
|
176
202
|
break;
|
package/lib/dotenv.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The smallest possible .env loader — no dependency, no magic.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT EXISTS: credentialed URLs and tokens were being pasted onto
|
|
5
|
+
* every command line, which puts them in shell history, in process
|
|
6
|
+
* listings, and eventually in a screenshot. A file the repo ignores is
|
|
7
|
+
* the right place for them.
|
|
8
|
+
*
|
|
9
|
+
* NEVER OVERRIDES a value already in the environment: an explicit
|
|
10
|
+
* `FOO=bar node ...` must win over a stale line in a file, or debugging
|
|
11
|
+
* becomes guesswork about which value is live.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
export function loadDotenv(file = path.join(HERE, '..', '.env')) {
|
|
21
|
+
let raw;
|
|
22
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch { return {}; }
|
|
23
|
+
const loaded = {};
|
|
24
|
+
for (const line of raw.split('\n')) {
|
|
25
|
+
const t = line.trim();
|
|
26
|
+
if (!t || t.startsWith('#')) continue;
|
|
27
|
+
const eq = t.indexOf('=');
|
|
28
|
+
if (eq < 1) continue;
|
|
29
|
+
const key = t.slice(0, eq).trim();
|
|
30
|
+
let val = t.slice(eq + 1).trim();
|
|
31
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
32
|
+
val = val.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
if (process.env[key] === undefined) { process.env[key] = val; loaded[key] = true; }
|
|
35
|
+
}
|
|
36
|
+
return loaded;
|
|
37
|
+
}
|
package/lib/pay.js
CHANGED
|
@@ -305,8 +305,22 @@ export class PayClient {
|
|
|
305
305
|
...init,
|
|
306
306
|
headers: { ...stripAuthorization(init.headers || {}), ...paymentHeaders(payment.header) },
|
|
307
307
|
});
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
// NEVER PRESENT OUR OWN SIGNATURE AS THE TRANSACTION.
|
|
309
|
+
//
|
|
310
|
+
// This fell back to `{ signature: payment.ownerSignature }`, and on Solana
|
|
311
|
+
// that signature CANNOT EXIST ON CHAIN: the facilitator is fee payer, so it
|
|
312
|
+
// re-signs the payload before submitting and the resulting transaction has
|
|
313
|
+
// a different signature entirely. MEASURED 2026-08-25 — the receipt printed
|
|
314
|
+
// `tx 4wJ42z5dtnt2h7NZpKM5…`, which getTransaction reports as NOT FOUND,
|
|
315
|
+
// for a payment that really settled as `2YgJg97DnK4cuvhE1jhCiwYS…`
|
|
316
|
+
// (slot 441794097, 0.147837 TOKEN moved). The gateway's chat path did not
|
|
317
|
+
// send `x-payment-response` at the time, so this fired on EVERY call: every
|
|
318
|
+
// Solana receipt a customer could check came back "not found", which reads
|
|
319
|
+
// as "I was never charged" when they were.
|
|
320
|
+
//
|
|
321
|
+
// No settle header now means no tx line, not a plausible-looking wrong one.
|
|
322
|
+
// An absent id is honest; an unverifiable id is worse than useless.
|
|
323
|
+
const settle = decodeSettleHeader(response.headers.get('x-payment-response')) || null;
|
|
310
324
|
const receipt = {
|
|
311
325
|
at: new Date().toISOString(),
|
|
312
326
|
line: receiptLine(accept, settle),
|
package/lib/proxy.js
CHANGED
|
@@ -794,14 +794,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
794
794
|
);
|
|
795
795
|
console.log(`openzoo v${version} -> ${config.apiBase}`);
|
|
796
796
|
console.log(`listening on http://localhost:${config.port}/v1`);
|
|
797
|
-
//
|
|
798
|
-
// with OPENZOO_NO_OPEN=1.
|
|
799
|
-
if (process.stdout.isTTY && !process.env.OPENZOO_NO_OPEN) {
|
|
800
|
-
const opener = process.platform === 'darwin' ? 'open'
|
|
801
|
-
: process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
802
|
-
import('node:child_process').then(({ exec }) =>
|
|
803
|
-
exec(`${opener} http://localhost:${config.port}/`, () => {}));
|
|
804
|
-
}
|
|
797
|
+
// The chat GUI still lives at GET / — but it is never auto-opened.
|
|
805
798
|
console.log('');
|
|
806
799
|
if (client.walletCreated) console.log(`new burner wallet created at ${client.walletPath} (chmod 600)`);
|
|
807
800
|
console.log(`wallet (fund me) · solana: ${client.address}`);
|
package/lib/receipts.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The receipt ledger — every paid call, kept.
|
|
3
|
+
*
|
|
4
|
+
* WHY ON DISK: the receipt is the product. A line that scrolls past in a
|
|
5
|
+
* terminal proves nothing an hour later, and "59x cheaper than OpenRouter"
|
|
6
|
+
* is a claim anyone can ask you to back up. This is the backing: an
|
|
7
|
+
* append-only JSONL of every call the shim has paid for, with both the
|
|
8
|
+
* billed price and what the SAME tokens on the SAME model would have cost
|
|
9
|
+
* buying direct, so the multiple is arithmetic rather than marketing.
|
|
10
|
+
*
|
|
11
|
+
* Append-only and never rewritten: a ledger you can edit is not evidence.
|
|
12
|
+
* 0600, local, never uploaded — it contains what you wrote and what it
|
|
13
|
+
* cost, which is nobody's business but yours.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
export const RECEIPTS_FILE = process.env.OPENZOO_RECEIPTS
|
|
21
|
+
|| path.join(os.homedir(), '.openzoo', 'receipts.jsonl');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Record one paid call. Never throws: a ledger write must not be able to
|
|
25
|
+
* fail the thing it is recording.
|
|
26
|
+
*
|
|
27
|
+
* `input`/`output` are kept because a before/after pair is the only part
|
|
28
|
+
* of a receipt anyone actually enjoys reading. OPENZOO_RECEIPTS_NO_TEXT=1
|
|
29
|
+
* stores lengths only.
|
|
30
|
+
*/
|
|
31
|
+
export function recordReceipt(rec) {
|
|
32
|
+
try {
|
|
33
|
+
const keepText = process.env.OPENZOO_RECEIPTS_NO_TEXT !== '1';
|
|
34
|
+
const row = {
|
|
35
|
+
at: new Date().toISOString(),
|
|
36
|
+
kind: rec.kind || 'call',
|
|
37
|
+
model: rec.model || 'unknown',
|
|
38
|
+
billedUsd: Number(rec.billedUsd || 0),
|
|
39
|
+
directUsd: Number(rec.directUsd || 0),
|
|
40
|
+
seconds: Number(rec.seconds || 0),
|
|
41
|
+
inChars: Number(rec.inChars || (rec.input ? rec.input.length : 0)),
|
|
42
|
+
outChars: Number(rec.outChars || (rec.output ? rec.output.length : 0)),
|
|
43
|
+
...(rec.stage ? { stage: rec.stage } : {}),
|
|
44
|
+
...(rec.tool ? { tool: rec.tool } : {}),
|
|
45
|
+
...(keepText && rec.input ? { input: rec.input } : {}),
|
|
46
|
+
...(keepText && rec.output ? { output: rec.output } : {}),
|
|
47
|
+
};
|
|
48
|
+
fs.mkdirSync(path.dirname(RECEIPTS_FILE), { recursive: true, mode: 0o700 });
|
|
49
|
+
fs.appendFileSync(RECEIPTS_FILE, JSON.stringify(row) + '\n', { mode: 0o600 });
|
|
50
|
+
return row;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readReceipts(file = RECEIPTS_FILE) {
|
|
57
|
+
try {
|
|
58
|
+
return fs.readFileSync(file, 'utf8')
|
|
59
|
+
.split('\n')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function usd(n) {
|
|
69
|
+
if (!(n > 0)) return '$0';
|
|
70
|
+
if (n >= 0.01) return `$${n.toFixed(4)}`;
|
|
71
|
+
if (n >= 0.000001) return `$${n.toFixed(6)}`;
|
|
72
|
+
return `$${n.toExponential(1)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Roll the ledger up into the numbers worth quoting.
|
|
77
|
+
*
|
|
78
|
+
* The headline multiple is TOTAL direct over TOTAL billed, not the mean of
|
|
79
|
+
* per-call multiples: averaging ratios lets one cheap call with a big
|
|
80
|
+
* multiple dominate a hundred expensive ones, which would be the exact
|
|
81
|
+
* kind of flattering-but-wrong number this ledger exists to avoid.
|
|
82
|
+
* Calls the gateway reported no direct price for are excluded from the
|
|
83
|
+
* comparison and counted separately, never silently treated as 1x.
|
|
84
|
+
*/
|
|
85
|
+
export function summarizeReceipts(rows = readReceipts()) {
|
|
86
|
+
const comparable = rows.filter((r) => r.billedUsd > 0 && r.directUsd > 0);
|
|
87
|
+
const billed = rows.reduce((n, r) => n + r.billedUsd, 0);
|
|
88
|
+
const cmpBilled = comparable.reduce((n, r) => n + r.billedUsd, 0);
|
|
89
|
+
const cmpDirect = comparable.reduce((n, r) => n + r.directUsd, 0);
|
|
90
|
+
const best = comparable
|
|
91
|
+
.map((r) => ({ ...r, x: r.directUsd / r.billedUsd }))
|
|
92
|
+
.sort((a, b) => b.x - a.x)[0] || null;
|
|
93
|
+
const byKind = {};
|
|
94
|
+
for (const r of rows) {
|
|
95
|
+
const k = byKind[r.kind] ||= { calls: 0, billed: 0, direct: 0, seconds: 0 };
|
|
96
|
+
k.calls += 1; k.billed += r.billedUsd; k.direct += r.directUsd; k.seconds += r.seconds;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
calls: rows.length,
|
|
100
|
+
since: rows[0]?.at ?? null,
|
|
101
|
+
billedUsd: billed,
|
|
102
|
+
comparable: comparable.length,
|
|
103
|
+
freeOrUnpriced: rows.length - comparable.length,
|
|
104
|
+
directUsd: cmpDirect,
|
|
105
|
+
savedUsd: Math.max(0, cmpDirect - cmpBilled),
|
|
106
|
+
multiple: cmpBilled > 0 ? cmpDirect / cmpBilled : 0,
|
|
107
|
+
avgSeconds: rows.length ? rows.reduce((n, r) => n + r.seconds, 0) / rows.length : 0,
|
|
108
|
+
best,
|
|
109
|
+
byKind,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The boast, printable. */
|
|
114
|
+
export function formatSummary(s = summarizeReceipts()) {
|
|
115
|
+
if (!s.calls) return 'no receipts yet — nothing has been paid for.';
|
|
116
|
+
const lines = [
|
|
117
|
+
`openzoo receipts — ${s.calls} paid call${s.calls === 1 ? '' : 's'} since ${String(s.since).slice(0, 10)}`,
|
|
118
|
+
'',
|
|
119
|
+
` spent ${usd(s.billedUsd)}`,
|
|
120
|
+
` same calls direct on OpenRouter ${usd(s.directUsd)} (over ${s.comparable} comparable call${s.comparable === 1 ? '' : 's'})`,
|
|
121
|
+
` saved ${usd(s.savedUsd)}${s.multiple >= 1.05 ? ` — ${s.multiple.toFixed(1)}× cheaper overall` : ''}`,
|
|
122
|
+
` avg latency ${s.avgSeconds.toFixed(1)}s`,
|
|
123
|
+
];
|
|
124
|
+
if (s.freeOrUnpriced) {
|
|
125
|
+
lines.push(` (${s.freeOrUnpriced} call${s.freeOrUnpriced === 1 ? '' : 's'} had no comparable direct price — cached or unpriced, excluded above)`);
|
|
126
|
+
}
|
|
127
|
+
const kinds = Object.entries(s.byKind).sort((a, b) => b[1].calls - a[1].calls);
|
|
128
|
+
if (kinds.length > 1) {
|
|
129
|
+
lines.push('', ' by kind:');
|
|
130
|
+
for (const [k, v] of kinds) lines.push(` ${k.padEnd(10)} ${String(v.calls).padStart(4)} calls ${usd(v.billed)}`);
|
|
131
|
+
}
|
|
132
|
+
if (s.best) {
|
|
133
|
+
lines.push('', ` best single call: ${s.best.x.toFixed(1)}× — ${s.best.model} ${usd(s.best.billedUsd)} vs ${usd(s.best.directUsd)} direct`);
|
|
134
|
+
if (s.best.input) lines.push(` in: ${String(s.best.input).replace(/\s+/g, ' ').slice(0, 100)}`);
|
|
135
|
+
if (s.best.output) lines.push(` out: ${String(s.best.output).replace(/\s+/g, ' ').slice(0, 100)}`);
|
|
136
|
+
}
|
|
137
|
+
lines.push('', ` ledger: ${RECEIPTS_FILE}`);
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|