openzoo 0.48.84 → 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 +26 -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;
|
|
@@ -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">↻</button>
|
|
@@ -3169,53 +3151,6 @@ const APP_HTML = `<!doctype html>
|
|
|
3169
3151
|
if (w.funded === false) note.classList.add('wempty');
|
|
3170
3152
|
if (note.textContent.trim()) walletBody.appendChild(note);
|
|
3171
3153
|
}
|
|
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
3154
|
document.getElementById('walletBtn').addEventListener('click', openWallet);
|
|
3220
3155
|
// First launch: if the burner is empty, open the wallet once so they see
|
|
3221
3156
|
// addresses they can copy — and whose wallet this is. localStorage so a
|
|
@@ -3245,7 +3180,6 @@ const APP_HTML = `<!doctype html>
|
|
|
3245
3180
|
});
|
|
3246
3181
|
document.addEventListener('keydown', (e) => {
|
|
3247
3182
|
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
3183
|
});
|
|
3250
3184
|
|
|
3251
3185
|
document.getElementById('tierSel').addEventListener('change', (e) => setDial('tier', e.target.value));
|
|
@@ -3984,6 +3918,16 @@ const APP_HTML = `<!doctype html>
|
|
|
3984
3918
|
|
|
3985
3919
|
const hudBtn = document.getElementById('hudBtn');
|
|
3986
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
|
+
}
|
|
3987
3931
|
function usd(n) {
|
|
3988
3932
|
if (n === null || n === undefined) return '—';
|
|
3989
3933
|
if (n === 0) return '$0';
|
|
@@ -4039,12 +3983,17 @@ const APP_HTML = `<!doctype html>
|
|
|
4039
3983
|
e.stopPropagation();
|
|
4040
3984
|
hud.classList.toggle('show');
|
|
4041
3985
|
if (hud.classList.contains('show')) {
|
|
3986
|
+
placeHud();
|
|
4042
3987
|
refreshHud();
|
|
4043
3988
|
hudTimer = setInterval(refreshHud, 30000);
|
|
4044
3989
|
} else if (hudTimer) {
|
|
4045
3990
|
clearInterval(hudTimer); hudTimer = null;
|
|
4046
3991
|
}
|
|
4047
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
|
+
}
|
|
4048
3997
|
document.addEventListener('click', (e) => { if (!hud.contains(e.target)) hud.classList.remove('show'); });
|
|
4049
3998
|
</script>
|
|
4050
3999
|
</body></html>`;
|
|
@@ -4065,49 +4014,6 @@ const server = http.createServer((req, res) => {
|
|
|
4065
4014
|
return;
|
|
4066
4015
|
}
|
|
4067
4016
|
|
|
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
4017
|
// Restart grokui in place — and ACTUALLY PICK UP THE NEW BUILD.
|
|
4112
4018
|
//
|
|
4113
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",
|