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.
- package/lib/grokui.mjs +177 -120
- 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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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, #
|
|
2606
|
-
#inp, #search, #composeInp, .
|
|
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">↻</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.
|
|
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",
|