openzoo 0.43.7 → 0.43.9
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/grokui.mjs +106 -10
- package/lib/namespace.js +67 -2
- package/lib/proxy.js +16 -9
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -529,7 +529,7 @@ const APP_HTML = `<!doctype html>
|
|
|
529
529
|
.tname { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
530
530
|
.tprev { font-size: 12px; color: #8e8e93; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
531
531
|
.tdot { width: 8px; height: 8px; border-radius: 50%; background: #0a84ff; flex: 0 0 8px; }
|
|
532
|
-
#main { flex: 1; display: flex; flex-direction: column; height: 100vh; }
|
|
532
|
+
#main { flex: 1; min-width: 0; display: flex; flex-direction: column; height: 100vh; }
|
|
533
533
|
#chatHeader { padding: 14px 20px; border-bottom: 1px solid #1c1c1e; display: flex; align-items: center; gap: 10px;
|
|
534
534
|
font-weight: 600; }
|
|
535
535
|
#chatHeader .tavatar { width: 26px; height: 26px; border-radius: 7px; font-size: 11px; flex: 0 0 26px; }
|
|
@@ -549,21 +549,48 @@ const APP_HTML = `<!doctype html>
|
|
|
549
549
|
#hud .hember { color: #f28c4d; }
|
|
550
550
|
#hud .hfoot { border-top: 1px solid #333340; margin-top: 10px; padding-top: 8px; color: #999aa8; font-size: 10px; }
|
|
551
551
|
#sidebar, #main { -webkit-app-region: no-drag; }
|
|
552
|
-
#log { flex: 1;
|
|
552
|
+
#log { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 20px 24px 12px;
|
|
553
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
553
554
|
-webkit-user-select: text; user-select: text; }
|
|
554
555
|
.hdr { align-self: flex-start; display: flex; align-items: center; gap: 6px; margin: 12px 0 4px;
|
|
555
556
|
color: #8e8e93; font-size: 13px; }
|
|
556
557
|
.hdr .avatar { width: 18px; height: 18px; border-radius: 5px; display: flex; align-items: center;
|
|
557
558
|
justify-content: center; color: #fff; font-size: 9px; font-weight: 700; }
|
|
558
|
-
.
|
|
559
|
+
/* min-width:0 is load-bearing. A flex item defaults to min-width:auto, so it
|
|
560
|
+
refuses to shrink below its content's intrinsic width — one long
|
|
561
|
+
unbreakable line (a curl command, a JSON blob) in a <pre> then stretches
|
|
562
|
+
the row past max-width, widens the whole column, and pushes the header's
|
|
563
|
+
cost button off-screen. */
|
|
564
|
+
.row { display: flex; max-width: 78%; min-width: 0; margin: 2px 0; }
|
|
559
565
|
.row.user { align-self: flex-end; }
|
|
560
566
|
.row.bot { align-self: flex-start; }
|
|
561
567
|
.bubble { padding: 11px 16px; border-radius: 20px; white-space: pre-wrap; word-break: break-word;
|
|
568
|
+
min-width: 0; max-width: 100%; overflow-x: hidden;
|
|
562
569
|
-webkit-user-select: text; user-select: text; cursor: text; }
|
|
563
570
|
.bubble a { color: #6ab0ff; text-decoration: underline; cursor: pointer; }
|
|
571
|
+
/* rendered markdown. The bubble is pre-wrap for plain text, but block
|
|
572
|
+
elements carry their own spacing — leaving pre-wrap on would add the
|
|
573
|
+
source newlines back on top of it and double every gap. */
|
|
574
|
+
.bubble:has(> p, > .md-h, > .md-table, > .md-list, > .md-pre) { white-space: normal; }
|
|
575
|
+
.bubble > p { margin: 0 0 10px; }
|
|
576
|
+
.bubble > p:last-child { margin-bottom: 0; }
|
|
577
|
+
.md-h { margin: 14px 0 8px; font-size: 15px; font-weight: 600; line-height: 1.3; }
|
|
578
|
+
.md-h:first-child { margin-top: 0; }
|
|
579
|
+
.md-hr { border: 0; border-top: 1px solid #3a3a3c; margin: 14px 0; }
|
|
580
|
+
.md-list { margin: 0 0 10px; padding-left: 22px; }
|
|
581
|
+
.md-list li { margin: 3px 0; }
|
|
582
|
+
.bubble code { background: #1c1c1e; border: 1px solid #333; border-radius: 5px;
|
|
583
|
+
padding: 1px 5px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
|
|
584
|
+
.md-pre { background: #1c1c1e; border: 1px solid #333; border-radius: 10px; padding: 10px 12px;
|
|
585
|
+
overflow-x: auto; margin: 0 0 10px; }
|
|
586
|
+
.md-pre code { background: none; border: 0; padding: 0; font-size: 12.5px; line-height: 1.45; }
|
|
587
|
+
.md-table { border-collapse: collapse; margin: 0 0 10px; font-size: 13px; display: block; overflow-x: auto; }
|
|
588
|
+
.md-table th, .md-table td { border: 1px solid #3a3a3c; padding: 5px 10px; text-align: left; vertical-align: top; }
|
|
589
|
+
.md-table th { background: #1c1c1e; font-weight: 600; }
|
|
564
590
|
.bubble-images { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
|
|
565
591
|
.bubble-images img { max-width: 160px; max-height: 160px; border-radius: 12px; display: block; }
|
|
566
|
-
.runcard { background: #1c1c1e; border: 1px solid #333; border-radius: 14px; padding: 12px 14px;
|
|
592
|
+
.runcard { background: #1c1c1e; border: 1px solid #333; border-radius: 14px; padding: 12px 14px;
|
|
593
|
+
max-width: 100%; min-width: 0; overflow: hidden; }
|
|
567
594
|
.runcmd { font-family: Menlo, monospace; font-size: 12.5px; color: #ececec; white-space: pre-wrap;
|
|
568
595
|
word-break: break-word; margin-bottom: 8px; }
|
|
569
596
|
.runactions { display: flex; gap: 8px; }
|
|
@@ -754,11 +781,78 @@ const APP_HTML = `<!doctype html>
|
|
|
754
781
|
}
|
|
755
782
|
|
|
756
783
|
function escapeHtml(s) { return s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); }
|
|
784
|
+
// Inline span-level markdown. Runs AFTER escapeHtml, so every tag below is
|
|
785
|
+
// one we created — model output can never inject its own.
|
|
786
|
+
function mdInline(s) {
|
|
787
|
+
let o = escapeHtml(s);
|
|
788
|
+
o = o.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
789
|
+
o = o.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
790
|
+
o = o.replace(/(^|[^*])\\*([^*\\n]+)\\*/g, '$1<em>$2</em>');
|
|
791
|
+
o = o.replace(/\\[([^\\]]+)\\]\\((https?:[^)\\s]+)\\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
792
|
+
// bare URLs, but only at a boundary — inside href="..." the preceding
|
|
793
|
+
// char is a quote, so links we just built are left alone
|
|
794
|
+
o = o.replace(/(^|[\\s(])(https?:\\/\\/[^\\s<)]+)/g, '$1<a href="$2" target="_blank" rel="noopener">$2</a>');
|
|
795
|
+
o = o.replace(/@(\\w+)/g, '<span class="mention">\u{1F465} $1</span>');
|
|
796
|
+
return o;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Block-level markdown: fenced code, tables, headings, lists. Models answer
|
|
800
|
+
// in markdown by default, and rendering it as literal "## " and "| --- |"
|
|
801
|
+
// made every structured answer unreadable.
|
|
757
802
|
function renderMentions(text) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
803
|
+
const fences = [];
|
|
804
|
+
const src = String(text).replace(/\`\`\`([\\w-]*)\\n?([\\s\\S]*?)\`\`\`/g, (m, lang, code) => {
|
|
805
|
+
fences.push('<pre class="md-pre"><code>' + escapeHtml(code.replace(/\\n$/, '')) + '</code></pre>');
|
|
806
|
+
return '\\u0000F' + (fences.length - 1) + '\\u0000';
|
|
807
|
+
});
|
|
808
|
+
const lines = src.split('\\n');
|
|
809
|
+
const out = [];
|
|
810
|
+
let list = null, tbl = null, para = [];
|
|
811
|
+
const flushPara = () => {
|
|
812
|
+
if (!para.length) return;
|
|
813
|
+
out.push('<p>' + para.map(mdInline).join('<br>') + '</p>');
|
|
814
|
+
para = [];
|
|
815
|
+
};
|
|
816
|
+
const closeList = () => { if (list) { out.push('</' + list + '>'); list = null; } };
|
|
817
|
+
const closeTbl = () => {
|
|
818
|
+
if (!tbl) return;
|
|
819
|
+
let h = '<table class="md-table"><thead><tr>'
|
|
820
|
+
+ tbl[0].map((c) => '<th>' + mdInline(c) + '</th>').join('') + '</tr></thead><tbody>';
|
|
821
|
+
for (const r of tbl.slice(1)) h += '<tr>' + r.map((c) => '<td>' + mdInline(c) + '</td>').join('') + '</tr>';
|
|
822
|
+
tbl = null;
|
|
823
|
+
out.push(h + '</tbody></table>');
|
|
824
|
+
};
|
|
825
|
+
const cells = (l) => l.replace(/^\\s*\\|/, '').replace(/\\|\\s*$/, '').split('|').map((c) => c.trim());
|
|
826
|
+
for (const line of lines) {
|
|
827
|
+
if (/^\\s*\\|.*\\|\\s*$/.test(line)) { // table row
|
|
828
|
+
flushPara(); closeList();
|
|
829
|
+
if (/^[\\s|:-]+$/.test(line)) continue; // |---|---| separator
|
|
830
|
+
(tbl = tbl || []).push(cells(line));
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
closeTbl();
|
|
834
|
+
const h = /^(#{1,4})\\s+(.*)$/.exec(line);
|
|
835
|
+
if (h) { flushPara(); closeList(); out.push('<h' + (h[1].length + 2) + ' class="md-h">' + mdInline(h[2]) + '</h' + (h[1].length + 2) + '>'); continue; }
|
|
836
|
+
if (/^\\s*([-*_])\\s*\\1\\s*\\1[\\s\\-*_]*$/.test(line)) { flushPara(); closeList(); out.push('<hr class="md-hr">'); continue; }
|
|
837
|
+
const ul = /^\\s*[-*]\\s+(.*)$/.exec(line);
|
|
838
|
+
const ol = /^\\s*\\d+[.)]\\s+(.*)$/.exec(line);
|
|
839
|
+
if (ul || ol) {
|
|
840
|
+
flushPara();
|
|
841
|
+
const want = ul ? 'ul' : 'ol';
|
|
842
|
+
if (list && list !== want) closeList();
|
|
843
|
+
if (!list) { list = want; out.push('<' + want + ' class="md-list">'); }
|
|
844
|
+
out.push('<li>' + mdInline((ul || ol)[1]) + '</li>');
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
closeList();
|
|
848
|
+
if (!line.trim()) { flushPara(); continue; }
|
|
849
|
+
// a fence placeholder is a BLOCK — letting it fall into a paragraph
|
|
850
|
+
// emits <p><pre>…</pre></p>, which browsers silently split apart
|
|
851
|
+
if (/^\\u0000F\\d+\\u0000$/.test(line.trim())) { flushPara(); out.push(line.trim()); continue; }
|
|
852
|
+
para.push(line);
|
|
853
|
+
}
|
|
854
|
+
flushPara(); closeList(); closeTbl();
|
|
855
|
+
return out.join('').replace(/\\u0000F(\\d+)\\u0000/g, (m, i) => fences[Number(i)]);
|
|
762
856
|
}
|
|
763
857
|
|
|
764
858
|
let lastSpeaker = null;
|
|
@@ -1077,8 +1171,10 @@ const APP_HTML = `<!doctype html>
|
|
|
1077
1171
|
const mult = direct / spent;
|
|
1078
1172
|
// honest either way: >=1x is a real saving vs a naked direct call,
|
|
1079
1173
|
// <1x means you're currently paying MORE than direct would cost —
|
|
1080
|
-
// don't dress that up as green when it isn't one
|
|
1081
|
-
|
|
1174
|
+
// don't dress that up as green when it isn't one.
|
|
1175
|
+
// Asking a bound corpus makes this genuinely large (the counterfactual
|
|
1176
|
+
// is shipping the WHOLE corpus), so 2dp would read as noise up there.
|
|
1177
|
+
savedEl.textContent = (mult >= 100 ? Math.round(mult) : mult.toFixed(mult >= 10 ? 1 : 2)) + 'x';
|
|
1082
1178
|
savedEl.className = mult >= 1 ? 'hlime' : 'hember';
|
|
1083
1179
|
} else {
|
|
1084
1180
|
savedEl.textContent = '—';
|
package/lib/namespace.js
CHANGED
|
@@ -47,8 +47,73 @@ export function namespaceHeaderValue() {
|
|
|
47
47
|
return cached;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* PROVE the namespace claim instead of merely asserting it.
|
|
52
|
+
*
|
|
53
|
+
* The header above is derived from the PUBLIC key, so on its own it is not
|
|
54
|
+
* authentication: anyone who knows this wallet's address can recompute the
|
|
55
|
+
* same value and land in the same sidecar tenant — and since the sidecar gates
|
|
56
|
+
* context access purely on tenant match, that is the whole door. The gateway
|
|
57
|
+
* (nsauth.ts) verifies a signature over `openzoo-namespace:<ns>:<ts>` and
|
|
58
|
+
* derives the tenant from sha256(chain:signer:namespace), which binds the
|
|
59
|
+
* PROVEN signer in, so another wallet signing an identical label lands
|
|
60
|
+
* somewhere else.
|
|
61
|
+
*
|
|
62
|
+
* Signed with the same Solana key x402 already pays from — no new secret, and
|
|
63
|
+
* nothing leaves the machine but a public key and a signature. The timestamp
|
|
64
|
+
* is what bounds replay (the gateway enforces its own window), so it is minted
|
|
65
|
+
* per call and these headers are deliberately NOT cached.
|
|
66
|
+
*/
|
|
67
|
+
const PKCS8_ED25519_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
68
|
+
|
|
69
|
+
function signNamespace(namespace) {
|
|
70
|
+
const w = loadOrCreateWallet();
|
|
71
|
+
const timestamp = String(Date.now());
|
|
72
|
+
// Node has no raw-ed25519 signer, but it will build one from a PKCS8 DER
|
|
73
|
+
// wrapper around the 32-byte seed (a Solana secretKey is seed||pubkey).
|
|
74
|
+
const der = Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(w.keypair.secretKey.slice(0, 32))]);
|
|
75
|
+
const key = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
|
|
76
|
+
const sig = crypto.sign(null, Buffer.from(`openzoo-namespace:${namespace}:${timestamp}`), key);
|
|
77
|
+
return {
|
|
78
|
+
'x-openzoo-namespace-sig': base58encode(sig),
|
|
79
|
+
'x-openzoo-namespace-signer': w.keypair.publicKey.toBase58(),
|
|
80
|
+
'x-openzoo-namespace-ts': timestamp,
|
|
81
|
+
'x-openzoo-namespace-chain': 'solana',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Merge the namespace header — and its proof — into any headers object. */
|
|
51
86
|
export function withNamespace(headers = {}) {
|
|
52
87
|
const ns = namespaceHeaderValue();
|
|
53
|
-
|
|
88
|
+
if (!ns) return headers;
|
|
89
|
+
let proof = {};
|
|
90
|
+
try {
|
|
91
|
+
proof = signNamespace(ns);
|
|
92
|
+
} catch {
|
|
93
|
+
// Unsigned still works while the gateway is in soft launch. Sending the
|
|
94
|
+
// bare namespace is strictly better than sending nothing: it keeps the
|
|
95
|
+
// caller in their existing tenant instead of silently relocating every
|
|
96
|
+
// bound corpus to the shared one.
|
|
97
|
+
}
|
|
98
|
+
return { ...headers, 'x-openzoo-namespace': ns, ...proof };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Bitcoin-alphabet base58, matching what the gateway's bs58 decode expects.
|
|
102
|
+
// Local rather than a dependency for the same reason lib/x402.js keeps its own.
|
|
103
|
+
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
104
|
+
function base58encode(bytes) {
|
|
105
|
+
const digits = [0];
|
|
106
|
+
for (const byte of bytes) {
|
|
107
|
+
let carry = byte;
|
|
108
|
+
for (let j = 0; j < digits.length; j++) {
|
|
109
|
+
carry += digits[j] << 8;
|
|
110
|
+
digits[j] = carry % 58;
|
|
111
|
+
carry = (carry / 58) | 0;
|
|
112
|
+
}
|
|
113
|
+
while (carry > 0) { digits.push(carry % 58); carry = (carry / 58) | 0; }
|
|
114
|
+
}
|
|
115
|
+
let out = '';
|
|
116
|
+
for (const b of bytes) { if (b === 0) out += '1'; else break; } // leading zeros
|
|
117
|
+
for (let i = digits.length - 1; i >= 0; i--) out += B58[digits[i]];
|
|
118
|
+
return out;
|
|
54
119
|
}
|
package/lib/proxy.js
CHANGED
|
@@ -678,15 +678,22 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
678
678
|
// billedUsd = cogs * markup on a straight-markup call. Close enough
|
|
679
679
|
// on a counterfactual (leCore-discounted) call too since markup is
|
|
680
680
|
// still the ceiling those get capped against.
|
|
681
|
-
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
681
|
+
// Prefer the gateway's own cogsUsd. Deriving it as billedUsd/MARKUP
|
|
682
|
+
// is only correct on a straight-markup call: under counterfactual
|
|
683
|
+
// pricing billedUsd is min(direct×discount, markupUsd), so the
|
|
684
|
+
// division understates cost and overstates margin.
|
|
685
|
+
sessionCogs += typeof receipt.cogsUsd === 'number'
|
|
686
|
+
? receipt.cogsUsd
|
|
687
|
+
: receipt.billedUsd / MARKUP;
|
|
688
|
+
// direct = what answering this WITHOUT the zoo would have cost. On an
|
|
689
|
+
// attach call that is the whole bound corpus, which is why it can be
|
|
690
|
+
// orders of magnitude above what was billed. directUsd is exact and
|
|
691
|
+
// always present; savesVsDirect is the same number as a ratio.
|
|
692
|
+
sessionDirect += typeof receipt.directUsd === 'number'
|
|
693
|
+
? receipt.directUsd
|
|
694
|
+
: typeof receipt.savesVsDirect === 'number'
|
|
695
|
+
? receipt.savesVsDirect * receipt.billedUsd
|
|
696
|
+
: receipt.billedUsd;
|
|
690
697
|
// The public-URL ceiling meters only public-origin spend — your own
|
|
691
698
|
// local calls never eat into it.
|
|
692
699
|
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.9",
|
|
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",
|