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 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
- const settle = decodeSettleHeader(response.headers.get('x-payment-response'))
309
- || { signature: payment.ownerSignature };
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
- // LAND THEM IN THE APP. Never in CI/agents (no TTY), never twice, opt out
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}`);
@@ -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
+ }