openzoo 0.48.83 → 0.48.85

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.
Files changed (2) hide show
  1. package/lib/grokui.mjs +177 -120
  2. package/package.json +1 -1
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;
@@ -2757,8 +2751,25 @@ const APP_HTML = `<!doctype html>
2757
2751
  font-size: 12px; }
2758
2752
  #composeFoot kbd { margin-right: 4px; }
2759
2753
  .mention { background: #3a3a3c; border-radius: 10px; padding: 1px 8px; font-size: 0.92em; }
2754
+ /* Sidebar names and header titles are readable text — they must be
2755
+ selectable so select-to-copy works there, not only in bubbles. */
2756
+ .tname, .tprev, .pname, .hname, .hdir, .hdr, .wbal, .wnote, .wsub, #chatHeader {
2757
+ -webkit-user-select: text; user-select: text; }
2758
+ /* SELECT-TO-COPY. A blink, not a dialog: selection landed on the clipboard. */
2759
+ #copiedToast {
2760
+ position: fixed; z-index: 4000; pointer-events: none;
2761
+ padding: 7px 14px; border-radius: 999px;
2762
+ background: #b8f240; color: #0b0b0d;
2763
+ font: 700 12px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2764
+ letter-spacing: .06em;
2765
+ box-shadow: 0 8px 24px rgba(0,0,0,.5);
2766
+ opacity: 0; transform: translate(-50%, -6px);
2767
+ transition: opacity .14s ease, transform .14s ease;
2768
+ }
2769
+ #copiedToast.show { opacity: 1; transform: translate(-50%, 0); }
2760
2770
  </style></head>
2761
2771
  <body>
2772
+ <div id="copiedToast" role="status" aria-live="polite">copied</div>
2762
2773
  <div id="dragbar"></div>
2763
2774
  <div id="sidebar">
2764
2775
  <div id="sideTop">
@@ -2783,16 +2794,6 @@ const APP_HTML = `<!doctype html>
2783
2794
  <div id="composeFoot"><span><kbd>Tab</kbd> add</span><span><kbd>Enter</kbd> open</span></div>
2784
2795
  </div>
2785
2796
  </div>
2786
- <div id="assetsOverlay" data-component="assets-modal">
2787
- <div id="walletBox">
2788
- <h3>Unlock baked assets</h3>
2789
- <div class="wsub">ProofFront ships encrypted inside this box's image. Your passphrase decrypts it here; it is never stored.</div>
2790
- <input class="wpass" id="assetsPass" type="password" autocomplete="off" spellcheck="false" placeholder="passphrase">
2791
- <button class="wgo" id="assetsGo" data-testid="assets-decrypt">decrypt</button>
2792
- <div class="wnote" id="assetsMsg"></div>
2793
- <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>
2794
- </div>
2795
- </div>
2796
2797
  <div id="walletOverlay" data-component="wallet-modal">
2797
2798
  <div id="walletBox">
2798
2799
  <h3>Your wallet</h3>
@@ -2831,8 +2832,6 @@ const APP_HTML = `<!doctype html>
2831
2832
  <option value="4 4">best 4 of 4</option>
2832
2833
  </optgroup>
2833
2834
  </select>
2834
- <button class="dial" id="assetsBtn" data-component="assets-unlock"
2835
- title="Unlock the encrypted assets baked into this box's image">assets</button>
2836
2835
  <button class="dial" id="walletBtn" data-component="wallet-open"
2837
2836
  title="Your local burner wallet — deposit addresses and live balances">wallet</button>
2838
2837
  <button class="icon-btn" id="reloadBtn" title="Restart grokui on this box">&#8635;</button>
@@ -3152,53 +3151,6 @@ const APP_HTML = `<!doctype html>
3152
3151
  if (w.funded === false) note.classList.add('wempty');
3153
3152
  if (note.textContent.trim()) walletBody.appendChild(note);
3154
3153
  }
3155
- // ASSETS UNLOCK. The passphrase lives in the input and in one fetch body and
3156
- // nowhere else — not in a variable that outlives the call, not in
3157
- // localStorage, and the field is cleared the moment it succeeds.
3158
- const assetsOverlay = document.getElementById('assetsOverlay');
3159
- const assetsPass = document.getElementById('assetsPass');
3160
- const assetsGo = document.getElementById('assetsGo');
3161
- const assetsMsg = document.getElementById('assetsMsg');
3162
- async function unlockAssets() {
3163
- const pass = assetsPass.value;
3164
- if (!pass) { assetsMsg.textContent = 'enter the passphrase'; return; }
3165
- assetsGo.disabled = true;
3166
- assetsGo.textContent = 'decrypting…';
3167
- assetsMsg.textContent = 'this takes a minute — 800MB';
3168
- try {
3169
- const r = await fetch(API + '/decrypt-assets', {
3170
- method: 'POST', headers: { 'content-type': 'application/json' },
3171
- body: JSON.stringify({ pass }),
3172
- });
3173
- const j = await r.json().catch(() => ({}));
3174
- if (j.ok) {
3175
- assetsPass.value = ''; // do not leave it sitting in the DOM
3176
- assetsMsg.textContent = j.already ? 'already unlocked' : ('unlocked — ' + j.files + ' files in /workspace/prooffront');
3177
- assetsMsg.className = 'wnote';
3178
- } else {
3179
- assetsMsg.textContent = j.error || ('failed (HTTP ' + r.status + ')');
3180
- assetsMsg.className = 'wnote wempty';
3181
- }
3182
- } catch (e) {
3183
- assetsMsg.textContent = 'could not reach the box';
3184
- assetsMsg.className = 'wnote wempty';
3185
- }
3186
- assetsGo.disabled = false;
3187
- assetsGo.textContent = 'decrypt';
3188
- }
3189
- assetsGo.addEventListener('click', unlockAssets);
3190
- assetsPass.addEventListener('keydown', (e) => { if (e.key === 'Enter') unlockAssets(); });
3191
- document.getElementById('assetsBtn').addEventListener('click', () => {
3192
- assetsOverlay.classList.add('show');
3193
- assetsMsg.textContent = '';
3194
- assetsMsg.className = 'wnote';
3195
- requestAnimationFrame(() => { try { assetsPass.focus(); } catch (e) {} });
3196
- });
3197
- assetsOverlay.addEventListener('click', (e) => {
3198
- // Clear on dismiss so a passphrase never lingers behind a closed modal.
3199
- if (e.target === assetsOverlay) { assetsPass.value = ''; assetsOverlay.classList.remove('show'); }
3200
- });
3201
-
3202
3154
  document.getElementById('walletBtn').addEventListener('click', openWallet);
3203
3155
  // First launch: if the burner is empty, open the wallet once so they see
3204
3156
  // addresses they can copy — and whose wallet this is. localStorage so a
@@ -3228,7 +3180,6 @@ const APP_HTML = `<!doctype html>
3228
3180
  });
3229
3181
  document.addEventListener('keydown', (e) => {
3230
3182
  if (e.key === 'Escape' && walletOverlay.classList.contains('show')) walletOverlay.classList.remove('show');
3231
- if (e.key === 'Escape' && assetsOverlay.classList.contains('show')) { assetsPass.value = ''; assetsOverlay.classList.remove('show'); }
3232
3183
  });
3233
3184
 
3234
3185
  document.getElementById('tierSel').addEventListener('change', (e) => setDial('tier', e.target.value));
@@ -3338,6 +3289,140 @@ const APP_HTML = `<!doctype html>
3338
3289
  return b;
3339
3290
  }
3340
3291
 
3292
+ /* SELECT → CLIPBOARD. A finished selection copies itself. No ⌘C, no hunting
3293
+ for a 10px button. Empty click / caret-only is a no-op so we do not spam
3294
+ "copied". The highlight stays put: copy must not steal focus or collapse
3295
+ the range. */
3296
+ function selectedText() {
3297
+ const el = document.activeElement;
3298
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
3299
+ if (el.type === 'password') return '';
3300
+ const a = el.selectionStart;
3301
+ const b = el.selectionEnd;
3302
+ if (typeof a === 'number' && typeof b === 'number' && b > a) {
3303
+ return String(el.value || '').slice(a, b);
3304
+ }
3305
+ return '';
3306
+ }
3307
+ const sel = window.getSelection();
3308
+ return sel ? sel.toString() : '';
3309
+ }
3310
+ function selectedRect() {
3311
+ const el = document.activeElement;
3312
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') && el.type !== 'password') {
3313
+ const a = el.selectionStart;
3314
+ const b = el.selectionEnd;
3315
+ if (typeof a === 'number' && typeof b === 'number' && b > a) return el.getBoundingClientRect();
3316
+ }
3317
+ const sel = window.getSelection();
3318
+ if (!sel || !sel.rangeCount) return null;
3319
+ const r = sel.getRangeAt(0).getBoundingClientRect();
3320
+ if (!r || (r.width === 0 && r.height === 0)) return null;
3321
+ return r;
3322
+ }
3323
+ function snapshotSelection() {
3324
+ const el = document.activeElement;
3325
+ if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
3326
+ return { kind: 'field', el: el, start: el.selectionStart, end: el.selectionEnd };
3327
+ }
3328
+ const sel = window.getSelection();
3329
+ if (!sel || !sel.rangeCount) return { kind: 'none' };
3330
+ const ranges = [];
3331
+ for (let i = 0; i < sel.rangeCount; i++) ranges.push(sel.getRangeAt(i).cloneRange());
3332
+ return { kind: 'dom', ranges: ranges };
3333
+ }
3334
+ function restoreSelection(snap) {
3335
+ if (!snap) return;
3336
+ if (snap.kind === 'field' && snap.el && document.contains(snap.el)) {
3337
+ try {
3338
+ snap.el.focus({ preventScroll: true });
3339
+ snap.el.setSelectionRange(snap.start, snap.end);
3340
+ } catch (e) {}
3341
+ return;
3342
+ }
3343
+ if (snap.kind === 'dom') {
3344
+ const sel = window.getSelection();
3345
+ sel.removeAllRanges();
3346
+ for (let i = 0; i < snap.ranges.length; i++) {
3347
+ try { sel.addRange(snap.ranges[i]); } catch (e) {}
3348
+ }
3349
+ }
3350
+ }
3351
+ let copiedToastTimer = 0;
3352
+ function showCopiedToast(rect) {
3353
+ const el = document.getElementById('copiedToast');
3354
+ if (!el) return;
3355
+ let x = window.innerWidth / 2;
3356
+ let y = 40;
3357
+ if (rect && (rect.width || rect.height)) {
3358
+ x = rect.left + rect.width / 2;
3359
+ y = rect.top - 38;
3360
+ if (y < 8) y = rect.bottom + 10;
3361
+ }
3362
+ if (x < 48) x = 48;
3363
+ if (x > window.innerWidth - 48) x = window.innerWidth - 48;
3364
+ if (y < 8) y = 8;
3365
+ if (y > window.innerHeight - 36) y = window.innerHeight - 36;
3366
+ el.style.left = x + 'px';
3367
+ el.style.top = y + 'px';
3368
+ el.classList.add('show');
3369
+ clearTimeout(copiedToastTimer);
3370
+ copiedToastTimer = setTimeout(function () { el.classList.remove('show'); }, 1200);
3371
+ }
3372
+ let selectPointerDown = false;
3373
+ let selectShiftHeld = false;
3374
+ let selectCopying = false;
3375
+ let selectTimer = 0;
3376
+ let selectLastText = '';
3377
+ let selectLastAt = 0;
3378
+ async function copySettledSelection() {
3379
+ if (selectCopying) return;
3380
+ const text = selectedText();
3381
+ if (!text) return;
3382
+ const now = Date.now();
3383
+ if (text === selectLastText && (now - selectLastAt) < 500) return;
3384
+ selectLastText = text;
3385
+ selectLastAt = now;
3386
+ const rect = selectedRect();
3387
+ const snap = snapshotSelection();
3388
+ selectCopying = true;
3389
+ let ok = false;
3390
+ try { ok = await copyText(text); }
3391
+ finally { restoreSelection(snap); selectCopying = false; }
3392
+ if (ok) showCopiedToast(rect);
3393
+ }
3394
+ function scheduleCopySelection() {
3395
+ if (selectPointerDown || selectCopying) return;
3396
+ clearTimeout(selectTimer);
3397
+ selectTimer = setTimeout(copySettledSelection, 40);
3398
+ }
3399
+ document.addEventListener('pointerdown', function (e) {
3400
+ if (e.button !== undefined && e.button !== 0) return;
3401
+ selectPointerDown = true;
3402
+ });
3403
+ document.addEventListener('pointerup', function (e) {
3404
+ if (e.button !== undefined && e.button !== 0) return;
3405
+ selectPointerDown = false;
3406
+ scheduleCopySelection();
3407
+ });
3408
+ document.addEventListener('pointercancel', function () { selectPointerDown = false; });
3409
+ document.addEventListener('keydown', function (e) {
3410
+ if (e.key === 'Shift') selectShiftHeld = true;
3411
+ });
3412
+ document.addEventListener('keyup', function (e) {
3413
+ if (e.key === 'Shift') selectShiftHeld = false;
3414
+ const withMod = e.metaKey || e.ctrlKey;
3415
+ const selectish = e.key === 'Shift'
3416
+ || e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown'
3417
+ || e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown'
3418
+ || ((e.key === 'a' || e.key === 'A') && withMod);
3419
+ if (selectish) scheduleCopySelection();
3420
+ });
3421
+ document.addEventListener('selectionchange', function () {
3422
+ if (selectPointerDown || selectCopying || selectShiftHeld) return;
3423
+ scheduleCopySelection();
3424
+ });
3425
+
3341
3426
  function renderMentions(text) {
3342
3427
  const fences = [];
3343
3428
  const src = String(text).replace(/\`\`\`([\\w-]*)\\n?([\\s\\S]*?)\`\`\`/g, (m, lang, code) => {
@@ -3833,6 +3918,16 @@ const APP_HTML = `<!doctype html>
3833
3918
 
3834
3919
  const hudBtn = document.getElementById('hudBtn');
3835
3920
  const hud = document.getElementById('hud');
3921
+ const chatHeader = document.getElementById('chatHeader');
3922
+ const mainEl = document.getElementById('main');
3923
+ // Pin the cost card just under the header. #chatHeader wraps, so a fixed
3924
+ // 40px sat on top of the spend dials. Measure the live bottom each time
3925
+ // it opens or the header changes height.
3926
+ function placeHud() {
3927
+ if (!hud || !chatHeader || !mainEl) return;
3928
+ const top = chatHeader.getBoundingClientRect().bottom - mainEl.getBoundingClientRect().top + 8;
3929
+ hud.style.top = Math.max(8, Math.round(top)) + 'px';
3930
+ }
3836
3931
  function usd(n) {
3837
3932
  if (n === null || n === undefined) return '—';
3838
3933
  if (n === 0) return '$0';
@@ -3888,12 +3983,17 @@ const APP_HTML = `<!doctype html>
3888
3983
  e.stopPropagation();
3889
3984
  hud.classList.toggle('show');
3890
3985
  if (hud.classList.contains('show')) {
3986
+ placeHud();
3891
3987
  refreshHud();
3892
3988
  hudTimer = setInterval(refreshHud, 30000);
3893
3989
  } else if (hudTimer) {
3894
3990
  clearInterval(hudTimer); hudTimer = null;
3895
3991
  }
3896
3992
  });
3993
+ window.addEventListener('resize', () => { if (hud.classList.contains('show')) placeHud(); });
3994
+ if (typeof ResizeObserver !== 'undefined' && chatHeader) {
3995
+ new ResizeObserver(() => { if (hud.classList.contains('show')) placeHud(); }).observe(chatHeader);
3996
+ }
3897
3997
  document.addEventListener('click', (e) => { if (!hud.contains(e.target)) hud.classList.remove('show'); });
3898
3998
  </script>
3899
3999
  </body></html>`;
@@ -3914,49 +4014,6 @@ const server = http.createServer((req, res) => {
3914
4014
  return;
3915
4015
  }
3916
4016
 
3917
- // UNLOCK THE BAKED ASSETS. The image ships ProofFront as ciphertext; this is
3918
- // where the operator supplies the passphrase that opens it.
3919
- //
3920
- // The passphrase is used and dropped: passed to openssl via an ENV VAR (never
3921
- // argv — a command line is world-readable in /proc), never written to disk,
3922
- // never logged, never stored on the thread. A wrong one is reported as wrong
3923
- // and nothing else happens.
3924
- //
3925
- // SAY WHAT THIS DOES: once decrypted the files are served by /api/files and
3926
- // /site like anything else in the workspace, and :8080 has no authentication.
3927
- // The lock protects the IMAGE, which is public. It does not protect a running
3928
- // box from anyone who knows its pod id.
3929
- if (req.method === 'POST' && req.url === '/decrypt-assets') {
3930
- const chunks = [];
3931
- req.on('data', (d) => chunks.push(d));
3932
- req.on('end', () => {
3933
- let pass = '';
3934
- try { pass = String(JSON.parse(Buffer.concat(chunks).toString('utf8')).pass || ''); } catch { /* ignore */ }
3935
- const done = (code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
3936
- if (!pass) return done(400, { ok: false, error: 'no passphrase' });
3937
- if (!existsSync('/opt/prooffront.enc')) return done(404, { ok: false, error: 'this image has no encrypted assets baked in' });
3938
- if (existsSync('/workspace/prooffront')) return done(200, { ok: true, already: true, note: 'already unlocked' });
3939
- // Same shape as box-boot: decrypt to temp, verify it is really a zip,
3940
- // then unpack and swap. Never unzip unverified plaintext into /workspace.
3941
- const script = 'set -e; '
3942
- + '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; '
3943
- + 'head -c 2 /tmp/pf.zip | grep -q PK; '
3944
- + 'rm -rf /workspace/.pf.tmp; mkdir -p /workspace/.pf.tmp; '
3945
- + 'unzip -q /tmp/pf.zip -d /workspace/.pf.tmp; '
3946
- + 'mv /workspace/.pf.tmp /workspace/prooffront; '
3947
- + 'rm -f /tmp/pf.zip; '
3948
- + 'find /workspace/prooffront -type f | wc -l';
3949
- execFile('bash', ['-lc', script], { env: { ...process.env, OZ_PF_PASS: pass }, timeout: 600000, maxBuffer: 1 << 20 },
3950
- (err, stdout) => {
3951
- // Wipe the copy we were handed as soon as openssl is done with it.
3952
- pass = '';
3953
- if (err) return done(200, { ok: false, error: 'wrong passphrase, or the blob is corrupt' });
3954
- done(200, { ok: true, files: Number(String(stdout).trim()) || 0 });
3955
- });
3956
- });
3957
- return;
3958
- }
3959
-
3960
4017
  // Restart grokui in place — and ACTUALLY PICK UP THE NEW BUILD.
3961
4018
  //
3962
4019
  // Exiting is the restart: on a production box, box-server's ensureOz() poll
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.48.83",
3
+ "version": "0.48.85",
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",