openzoo 0.48.84 → 0.48.86

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  // own independent agent (and that agent can spawn further threads too) —
6
6
  // reusing the same SPAWN/SEND pattern podagent.mjs built for shell delegation,
7
7
  // adapted here for plain chat.
8
- import { exec, execFile } from 'node:child_process';
8
+ import { exec } from 'node:child_process';
9
9
  import http from 'node:http';
10
10
  import { randomUUID } from 'node:crypto';
11
11
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
@@ -2495,7 +2495,7 @@ const APP_HTML = `<!doctype html>
2495
2495
  }
2496
2496
  @keyframes twarnpulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } }
2497
2497
  @media (prefers-reduced-motion: reduce) { .twarn { animation: none; } }
2498
- #main { flex: 1; min-width: 0; display: flex; flex-direction: column; height: 100vh; }
2498
+ #main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; height: 100vh; }
2499
2499
  #chatHeader { padding: 14px 20px; border-bottom: 1px solid #1c1c1e; display: flex; align-items: center;
2500
2500
  flex-wrap: wrap; gap: 8px 10px; font-weight: 600; }
2501
2501
  #chatHeader .tavatar { width: 26px; height: 26px; border-radius: 7px; font-size: 11px; flex: 0 0 26px; }
@@ -2537,9 +2537,9 @@ const APP_HTML = `<!doctype html>
2537
2537
  whose entire premise is that the box pays for itself. Click-to-copy is a
2538
2538
  shortcut; the address is selectable so Cmd/Ctrl+C still works if copy
2539
2539
  fails. */
2540
- #walletOverlay, #assetsOverlay { position: fixed; inset: 0; background: rgba(0,0,0,.66); z-index: 1200;
2540
+ #walletOverlay { position: fixed; inset: 0; background: rgba(0,0,0,.66); z-index: 1200;
2541
2541
  display: none; align-items: center; justify-content: center; padding: 24px; }
2542
- #walletOverlay.show, #assetsOverlay.show { display: flex; }
2542
+ #walletOverlay.show { display: flex; }
2543
2543
  #walletBox { width: 100%; max-width: 560px; max-height: 82vh; overflow-y: auto; background: #111113;
2544
2544
  border: 1px solid #2c2c2e; border-radius: 16px; padding: 20px 22px; }
2545
2545
  #walletBox h3 { margin: 0 0 2px; font-size: 15px; font-weight: 600; }
@@ -2561,16 +2561,6 @@ const APP_HTML = `<!doctype html>
2561
2561
  font-size: 12px; color: #ececec; line-height: 1.7; word-break: break-word; }
2562
2562
  .wnote { color: #6f7080; font-size: 11px; line-height: 1.6; margin-top: 12px; }
2563
2563
  .wempty { color: #f28c4d; }
2564
- /* The passphrase field. type=password so it never renders in clear, and
2565
- autocomplete off so a browser is not tempted to remember a key that
2566
- unlocks a public blob. */
2567
- .wpass { width: 100%; margin-top: 4px; padding: 9px 12px; background: #0d0d0f; color: #ececec;
2568
- border: 1px solid #2c2c2e; border-radius: 10px; font: inherit; font-size: 13px;
2569
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .12em; }
2570
- .wpass:focus { outline: none; border-color: #6ab0ff; }
2571
- .wgo { margin-top: 10px; width: 100%; padding: 9px; border: 0; border-radius: 10px;
2572
- background: #b8f240; color: #000; font: inherit; font-weight: 600; cursor: pointer; }
2573
- .wgo[disabled] { background: #2c2c2e; color: #6f7080; cursor: default; }
2574
2564
  /* Slash autocomplete. Anchored above the composer because the composer sits
2575
2565
  at the bottom of the viewport — a dropdown BELOW it would render off
2576
2566
  screen. */
@@ -2585,7 +2575,11 @@ const APP_HTML = `<!doctype html>
2585
2575
  .scmd i { color: #6f7080; font-style: normal; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
2586
2576
  .scmd span { color: #999aa8; margin-left: auto; text-align: right; }
2587
2577
  #hudBtn { margin-left: 2px; }
2588
- #hud { position: fixed; top: 40px; right: 14px; width: 250px; background: rgba(14,14,17,.94);
2578
+ /* Below #chatHeader, not a hardcoded 40px the header wraps, so 40px lands
2579
+ inside the dials (race / wallet / ◎) once they take a second row. top is
2580
+ set from the header's live bottom in placeHud(). z-index stays high; the
2581
+ bug was geometry, not stacking. */
2582
+ #hud { position: absolute; right: 14px; width: 250px; background: rgba(14,14,17,.94);
2589
2583
  border: 1px solid #333340; border-radius: 10px; padding: 12px 14px; font: 11px/1.5 Menlo, monospace;
2590
2584
  display: none; z-index: 300; box-shadow: 0 12px 30px rgba(0,0,0,.5); }
2591
2585
  #hud.show { display: block; }
@@ -2602,8 +2596,8 @@ const APP_HTML = `<!doctype html>
2602
2596
  color: #f0c9a8; font-size: 10.5px; line-height: 1.45; }
2603
2597
  #hud .hhint.show { display: block; }
2604
2598
  #hud .hhint b { color: #f28c4d; font-weight: 600; }
2605
- #sidebar, #main, #walletOverlay, #assetsOverlay, #composeOverlay,
2606
- #inp, #search, #composeInp, .wpass, .bubble, .md-pre, .runoutput, .runcmd {
2599
+ #sidebar, #main, #walletOverlay, #composeOverlay,
2600
+ #inp, #search, #composeInp, .bubble, .md-pre, .runoutput, .runcmd {
2607
2601
  -webkit-app-region: no-drag; }
2608
2602
  #log { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 20px 24px 12px;
2609
2603
  display: flex; flex-direction: column; gap: 6px;
@@ -2800,16 +2794,6 @@ const APP_HTML = `<!doctype html>
2800
2794
  <div id="composeFoot"><span><kbd>Tab</kbd> add</span><span><kbd>Enter</kbd> open</span></div>
2801
2795
  </div>
2802
2796
  </div>
2803
- <div id="assetsOverlay" data-component="assets-modal">
2804
- <div id="walletBox">
2805
- <h3>Unlock baked assets</h3>
2806
- <div class="wsub">ProofFront ships encrypted inside this box's image. Your passphrase decrypts it here; it is never stored.</div>
2807
- <input class="wpass" id="assetsPass" type="password" autocomplete="off" spellcheck="false" placeholder="passphrase">
2808
- <button class="wgo" id="assetsGo" data-testid="assets-decrypt">decrypt</button>
2809
- <div class="wnote" id="assetsMsg"></div>
2810
- <div class="wnote">Once unlocked the files are served like anything else in the workspace, and this box's :8080 has no authentication — the lock protects the public image, not a running box whose id someone knows.</div>
2811
- </div>
2812
- </div>
2813
2797
  <div id="walletOverlay" data-component="wallet-modal">
2814
2798
  <div id="walletBox">
2815
2799
  <h3>Your wallet</h3>
@@ -2848,8 +2832,6 @@ const APP_HTML = `<!doctype html>
2848
2832
  <option value="4 4">best 4 of 4</option>
2849
2833
  </optgroup>
2850
2834
  </select>
2851
- <button class="dial" id="assetsBtn" data-component="assets-unlock"
2852
- title="Unlock the encrypted assets baked into this box's image">assets</button>
2853
2835
  <button class="dial" id="walletBtn" data-component="wallet-open"
2854
2836
  title="Your local burner wallet — deposit addresses and live balances">wallet</button>
2855
2837
  <button class="icon-btn" id="reloadBtn" title="Restart grokui on this box">&#8635;</button>
@@ -2858,6 +2840,7 @@ const APP_HTML = `<!doctype html>
2858
2840
  </div>
2859
2841
  <div id="hud">
2860
2842
  <div class="htitle">YOUR WALLET · THIS SESSION</div>
2843
+ <div class="hrow"><span>prepaid credit</span><span id="hCredit" class="hlime">—</span></div>
2861
2844
  <div class="hrow"><span>you've paid</span><span id="hYouSpent">—</span></div>
2862
2845
  <div class="hrow"><span>our cost (cogs)</span><span id="hYouCogs">—</span></div>
2863
2846
  <div class="hrow"><span>margin</span><span id="hYouMargin" class="hlime">—</span></div>
@@ -3150,6 +3133,15 @@ const APP_HTML = `<!doctype html>
3150
3133
  walletBody.appendChild(p);
3151
3134
  return;
3152
3135
  }
3136
+ if (w.creditUsd != null && w.creditUsd !== '') {
3137
+ const c = document.createElement('div');
3138
+ c.className = 'wbal';
3139
+ c.style.color = '#b8f240';
3140
+ c.style.fontSize = '18px';
3141
+ c.style.fontWeight = '700';
3142
+ c.textContent = 'Prepaid credit $' + (Number(w.creditUsd) || 0).toFixed(2);
3143
+ walletBody.appendChild(c);
3144
+ }
3153
3145
  if (w.solana) walletBody.appendChild(walletRow('Solana', w.solana));
3154
3146
  if (w.evm) walletBody.appendChild(walletRow('Base / RH', w.evm));
3155
3147
  if (w.balances) {
@@ -3169,53 +3161,6 @@ const APP_HTML = `<!doctype html>
3169
3161
  if (w.funded === false) note.classList.add('wempty');
3170
3162
  if (note.textContent.trim()) walletBody.appendChild(note);
3171
3163
  }
3172
- // ASSETS UNLOCK. The passphrase lives in the input and in one fetch body and
3173
- // nowhere else — not in a variable that outlives the call, not in
3174
- // localStorage, and the field is cleared the moment it succeeds.
3175
- const assetsOverlay = document.getElementById('assetsOverlay');
3176
- const assetsPass = document.getElementById('assetsPass');
3177
- const assetsGo = document.getElementById('assetsGo');
3178
- const assetsMsg = document.getElementById('assetsMsg');
3179
- async function unlockAssets() {
3180
- const pass = assetsPass.value;
3181
- if (!pass) { assetsMsg.textContent = 'enter the passphrase'; return; }
3182
- assetsGo.disabled = true;
3183
- assetsGo.textContent = 'decrypting…';
3184
- assetsMsg.textContent = 'this takes a minute — 800MB';
3185
- try {
3186
- const r = await fetch(API + '/decrypt-assets', {
3187
- method: 'POST', headers: { 'content-type': 'application/json' },
3188
- body: JSON.stringify({ pass }),
3189
- });
3190
- const j = await r.json().catch(() => ({}));
3191
- if (j.ok) {
3192
- assetsPass.value = ''; // do not leave it sitting in the DOM
3193
- assetsMsg.textContent = j.already ? 'already unlocked' : ('unlocked — ' + j.files + ' files in /workspace/prooffront');
3194
- assetsMsg.className = 'wnote';
3195
- } else {
3196
- assetsMsg.textContent = j.error || ('failed (HTTP ' + r.status + ')');
3197
- assetsMsg.className = 'wnote wempty';
3198
- }
3199
- } catch (e) {
3200
- assetsMsg.textContent = 'could not reach the box';
3201
- assetsMsg.className = 'wnote wempty';
3202
- }
3203
- assetsGo.disabled = false;
3204
- assetsGo.textContent = 'decrypt';
3205
- }
3206
- assetsGo.addEventListener('click', unlockAssets);
3207
- assetsPass.addEventListener('keydown', (e) => { if (e.key === 'Enter') unlockAssets(); });
3208
- document.getElementById('assetsBtn').addEventListener('click', () => {
3209
- assetsOverlay.classList.add('show');
3210
- assetsMsg.textContent = '';
3211
- assetsMsg.className = 'wnote';
3212
- requestAnimationFrame(() => { try { assetsPass.focus(); } catch (e) {} });
3213
- });
3214
- assetsOverlay.addEventListener('click', (e) => {
3215
- // Clear on dismiss so a passphrase never lingers behind a closed modal.
3216
- if (e.target === assetsOverlay) { assetsPass.value = ''; assetsOverlay.classList.remove('show'); }
3217
- });
3218
-
3219
3164
  document.getElementById('walletBtn').addEventListener('click', openWallet);
3220
3165
  // First launch: if the burner is empty, open the wallet once so they see
3221
3166
  // addresses they can copy — and whose wallet this is. localStorage so a
@@ -3245,7 +3190,6 @@ const APP_HTML = `<!doctype html>
3245
3190
  });
3246
3191
  document.addEventListener('keydown', (e) => {
3247
3192
  if (e.key === 'Escape' && walletOverlay.classList.contains('show')) walletOverlay.classList.remove('show');
3248
- if (e.key === 'Escape' && assetsOverlay.classList.contains('show')) { assetsPass.value = ''; assetsOverlay.classList.remove('show'); }
3249
3193
  });
3250
3194
 
3251
3195
  document.getElementById('tierSel').addEventListener('change', (e) => setDial('tier', e.target.value));
@@ -3984,6 +3928,16 @@ const APP_HTML = `<!doctype html>
3984
3928
 
3985
3929
  const hudBtn = document.getElementById('hudBtn');
3986
3930
  const hud = document.getElementById('hud');
3931
+ const chatHeader = document.getElementById('chatHeader');
3932
+ const mainEl = document.getElementById('main');
3933
+ // Pin the cost card just under the header. #chatHeader wraps, so a fixed
3934
+ // 40px sat on top of the spend dials. Measure the live bottom each time
3935
+ // it opens or the header changes height.
3936
+ function placeHud() {
3937
+ if (!hud || !chatHeader || !mainEl) return;
3938
+ const top = chatHeader.getBoundingClientRect().bottom - mainEl.getBoundingClientRect().top + 8;
3939
+ hud.style.top = Math.max(8, Math.round(top)) + 'px';
3940
+ }
3987
3941
  function usd(n) {
3988
3942
  if (n === null || n === undefined) return '—';
3989
3943
  if (n === 0) return '$0';
@@ -3996,6 +3950,8 @@ const APP_HTML = `<!doctype html>
3996
3950
  // straight to localhost:8402 would work fine, but routing it through
3997
3951
  // our own backend keeps one fetch path if that ever needs to change.
3998
3952
  const you = await (await fetch(API + '/hud-summary')).json();
3953
+ const creditEl = document.getElementById('hCredit');
3954
+ if (creditEl) creditEl.textContent = (you.creditUsd == null) ? '—' : usd(Number(you.creditUsd) || 0);
3999
3955
  const spent = Number(you.spentUsd) || 0;
4000
3956
  const cogs = Number(you.cogsUsd) || 0;
4001
3957
  const direct = Number(you.directUsd) || 0;
@@ -4039,12 +3995,17 @@ const APP_HTML = `<!doctype html>
4039
3995
  e.stopPropagation();
4040
3996
  hud.classList.toggle('show');
4041
3997
  if (hud.classList.contains('show')) {
3998
+ placeHud();
4042
3999
  refreshHud();
4043
4000
  hudTimer = setInterval(refreshHud, 30000);
4044
4001
  } else if (hudTimer) {
4045
4002
  clearInterval(hudTimer); hudTimer = null;
4046
4003
  }
4047
4004
  });
4005
+ window.addEventListener('resize', () => { if (hud.classList.contains('show')) placeHud(); });
4006
+ if (typeof ResizeObserver !== 'undefined' && chatHeader) {
4007
+ new ResizeObserver(() => { if (hud.classList.contains('show')) placeHud(); }).observe(chatHeader);
4008
+ }
4048
4009
  document.addEventListener('click', (e) => { if (!hud.contains(e.target)) hud.classList.remove('show'); });
4049
4010
  </script>
4050
4011
  </body></html>`;
@@ -4059,55 +4020,16 @@ const server = http.createServer((req, res) => {
4059
4020
  let w = { error: 'proxy unreachable' };
4060
4021
  try { w = await (await fetch(`${PROXY}/wallet`)).json(); }
4061
4022
  catch { /* proxy not up yet — say so rather than render an empty modal */ }
4023
+ try {
4024
+ const { creditBalance } = await import('./info.js');
4025
+ w.creditUsd = await creditBalance();
4026
+ } catch { /* leave credit off if the gateway is down */ }
4062
4027
  res.writeHead(200, { 'content-type': 'application/json' });
4063
4028
  res.end(JSON.stringify(w));
4064
4029
  })();
4065
4030
  return;
4066
4031
  }
4067
4032
 
4068
- // UNLOCK THE BAKED ASSETS. The image ships ProofFront as ciphertext; this is
4069
- // where the operator supplies the passphrase that opens it.
4070
- //
4071
- // The passphrase is used and dropped: passed to openssl via an ENV VAR (never
4072
- // argv — a command line is world-readable in /proc), never written to disk,
4073
- // never logged, never stored on the thread. A wrong one is reported as wrong
4074
- // and nothing else happens.
4075
- //
4076
- // SAY WHAT THIS DOES: once decrypted the files are served by /api/files and
4077
- // /site like anything else in the workspace, and :8080 has no authentication.
4078
- // The lock protects the IMAGE, which is public. It does not protect a running
4079
- // box from anyone who knows its pod id.
4080
- if (req.method === 'POST' && req.url === '/decrypt-assets') {
4081
- const chunks = [];
4082
- req.on('data', (d) => chunks.push(d));
4083
- req.on('end', () => {
4084
- let pass = '';
4085
- try { pass = String(JSON.parse(Buffer.concat(chunks).toString('utf8')).pass || ''); } catch { /* ignore */ }
4086
- const done = (code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
4087
- if (!pass) return done(400, { ok: false, error: 'no passphrase' });
4088
- if (!existsSync('/opt/prooffront.enc')) return done(404, { ok: false, error: 'this image has no encrypted assets baked in' });
4089
- if (existsSync('/workspace/prooffront')) return done(200, { ok: true, already: true, note: 'already unlocked' });
4090
- // Same shape as box-boot: decrypt to temp, verify it is really a zip,
4091
- // then unpack and swap. Never unzip unverified plaintext into /workspace.
4092
- const script = 'set -e; '
4093
- + 'openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in /opt/prooffront.enc -out /tmp/pf.zip -pass env:OZ_PF_PASS 2>/dev/null; '
4094
- + 'head -c 2 /tmp/pf.zip | grep -q PK; '
4095
- + 'rm -rf /workspace/.pf.tmp; mkdir -p /workspace/.pf.tmp; '
4096
- + 'unzip -q /tmp/pf.zip -d /workspace/.pf.tmp; '
4097
- + 'mv /workspace/.pf.tmp /workspace/prooffront; '
4098
- + 'rm -f /tmp/pf.zip; '
4099
- + 'find /workspace/prooffront -type f | wc -l';
4100
- execFile('bash', ['-lc', script], { env: { ...process.env, OZ_PF_PASS: pass }, timeout: 600000, maxBuffer: 1 << 20 },
4101
- (err, stdout) => {
4102
- // Wipe the copy we were handed as soon as openssl is done with it.
4103
- pass = '';
4104
- if (err) return done(200, { ok: false, error: 'wrong passphrase, or the blob is corrupt' });
4105
- done(200, { ok: true, files: Number(String(stdout).trim()) || 0 });
4106
- });
4107
- });
4108
- return;
4109
- }
4110
-
4111
4033
  // Restart grokui in place — and ACTUALLY PICK UP THE NEW BUILD.
4112
4034
  //
4113
4035
  // Exiting is the restart: on a production box, box-server's ensureOz() poll
@@ -4157,9 +4079,13 @@ const server = http.createServer((req, res) => {
4157
4079
 
4158
4080
  if (req.method === 'GET' && req.url === '/hud-summary') {
4159
4081
  (async () => {
4160
- let you = { spentUsd: 0, cogsUsd: 0, directUsd: 0, paidCalls: 0 };
4161
- try { you = await (await fetch('http://127.0.0.1:8402/v1/session')).json(); }
4082
+ let you = { spentUsd: 0, cogsUsd: 0, directUsd: 0, paidCalls: 0, creditUsd: null };
4083
+ try { you = { ...you, ...(await (await fetch('http://127.0.0.1:8402/v1/session')).json()) }; }
4162
4084
  catch { /* local proxy not running — HUD shows zeros rather than guessing */ }
4085
+ try {
4086
+ const { creditBalance } = await import('./info.js');
4087
+ you.creditUsd = await creditBalance();
4088
+ } catch { /* credit is advisory */ }
4163
4089
  res.writeHead(200, { 'content-type': 'application/json' });
4164
4090
  res.end(JSON.stringify(you));
4165
4091
  })();
package/lib/proxy.js CHANGED
@@ -856,8 +856,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
856
856
  // whichever surface happens to be asking. Local-only, no auth needed:
857
857
  // it's a number, not a capability.
858
858
  if (req.method === 'GET' && (req.url || '').split('?')[0] === '/v1/session') {
859
+ refreshCredit();
859
860
  res.writeHead(200, { 'content-type': 'application/json' });
860
- res.end(JSON.stringify({ spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls }));
861
+ res.end(JSON.stringify({ spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls, creditUsd }));
861
862
  return;
862
863
  }
863
864
 
@@ -874,6 +875,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
874
875
  // caller can tell a genuinely empty wallet from a transient 402
875
876
  balances: balanceLine(lastSnap || []) || null,
876
877
  funded: (lastSnap || []).some((b) => b.ui > 0),
878
+ creditUsd,
877
879
  }));
878
880
  return;
879
881
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.84",
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.",
3
+ "version": "0.48.86",
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": {