openzoo 0.50.37 → 0.50.39
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/bin/claude-zoo.js +100 -0
- package/bin/openzoo.js +8 -0
- package/lib/cursorbackend.js +464 -99
- package/lib/grokbotAccount.js +134 -8
- package/lib/grokbotUploads.js +168 -0
- package/lib/grokbotweb-shim.js +766 -0
- package/lib/grokbotweb.js +933 -0
- package/lib/grokcli.js +47 -22
- package/lib/ozSpendChip.js +482 -0
- package/lib/pay.js +17 -24
- package/lib/proxy.js +33 -11
- package/lib/spendProof.js +307 -0
- package/lib/x402.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve the Grok Bot renderer in a normal browser.
|
|
3
|
+
*
|
|
4
|
+
* The .app is Electron: main + preload + a Vite-built renderer in app.asar.
|
|
5
|
+
* Chat never lived in Chromium — it goes through a coordinator MessagePort to
|
|
6
|
+
* the same /api/sendPrompt hijack `npx openzoo bot` already runs. This process
|
|
7
|
+
* extracts nothing into git: it reads the asar in place, injects a preload
|
|
8
|
+
* shim, and speaks the coordinator protocol over WebSocket.
|
|
9
|
+
*/
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { exec } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
import { config } from './config.js';
|
|
19
|
+
import { readHouseRoster } from './grokbotAccount.js';
|
|
20
|
+
import { ingestUpload, lookupUpload } from './grokbotUploads.js';
|
|
21
|
+
import { spendChipSource } from './ozSpendChip.js';
|
|
22
|
+
|
|
23
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const SHIM_PATH = path.join(HERE, 'grokbotweb-shim.js');
|
|
25
|
+
const PROTOCOL_VERSION = 1;
|
|
26
|
+
const DEFAULT_PORT = Number(process.env.OZ_GROKBOT_WEB_PORT || 4174);
|
|
27
|
+
/** LAN/tunnel visitors need all-interfaces. Hijack :8443 bind is unchanged. */
|
|
28
|
+
export const DEFAULT_WEB_BIND = process.env.OZ_GROKBOT_WEB_BIND || process.env.OPENZOO_BIND || '0.0.0.0';
|
|
29
|
+
const BIND = DEFAULT_WEB_BIND;
|
|
30
|
+
const HIJACK = process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443';
|
|
31
|
+
const OZ_WHO_COOKIE = 'oz_who';
|
|
32
|
+
const OZ_WHO_MAX_AGE = 365 * 24 * 3600;
|
|
33
|
+
|
|
34
|
+
/** Word-list shortnames — never sequential "User-3". Paired 1:1 with colors. */
|
|
35
|
+
export const VISITOR_SHORTNAMES = [
|
|
36
|
+
'maya', 'rex', 'jun', 'pio', 'nia', 'kai', 'zo', 'ash', 'ivy', 'lux',
|
|
37
|
+
'neo', 'ora', 'sol', 'vem', 'kit', 'wynn', 'joss', 'lark', 'nico', 'rafi',
|
|
38
|
+
'tess', 'uma', 'voss', 'wren', 'yara', 'zed', 'bea', 'cal', 'drew', 'eve',
|
|
39
|
+
'finn', 'gia', 'hugo', 'ida', 'jem', 'liv', 'moe', 'nils', 'opal', 'quin',
|
|
40
|
+
];
|
|
41
|
+
export const VISITOR_COLORS = [
|
|
42
|
+
'#e85d4c', '#3db8e8', '#e8c14c', '#7c5ce8', '#3dd68a',
|
|
43
|
+
'#e84c9a', '#4c7ae8', '#e88b4c', '#4ce0d0', '#d44ce8',
|
|
44
|
+
'#8ee84c', '#e85d7a', '#5c9ae8', '#e8a33b', '#6e4ce8',
|
|
45
|
+
'#4ce89a', '#e86b4c', '#4caee8', '#c8e84c', '#e84cc8',
|
|
46
|
+
'#4c58e8', '#e8d44c', '#a44ce8', '#4ce86e', '#e8486e',
|
|
47
|
+
'#3ec4e8', '#e89a4c', '#745ce8', '#9ae84c', '#e84c7a',
|
|
48
|
+
'#4c8ee8', '#e8bc4c', '#c44ce8', '#4ce8b6', '#e8704c',
|
|
49
|
+
'#5cb0e8', '#d8e84c', '#8c4ce8', '#4ce878', '#e85c9c',
|
|
50
|
+
];
|
|
51
|
+
const usedShortnames = new Set();
|
|
52
|
+
|
|
53
|
+
export function visitorPaletteMap() {
|
|
54
|
+
const o = {};
|
|
55
|
+
for (let i = 0; i < VISITOR_SHORTNAMES.length; i += 1) {
|
|
56
|
+
o[VISITOR_SHORTNAMES[i]] = VISITOR_COLORS[i % VISITOR_COLORS.length];
|
|
57
|
+
}
|
|
58
|
+
return o;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isVisitorShortname(name) {
|
|
62
|
+
return VISITOR_SHORTNAMES.includes(String(name || '').toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function colorForShortname(name) {
|
|
66
|
+
const i = VISITOR_SHORTNAMES.indexOf(String(name || '').toLowerCase());
|
|
67
|
+
if (i >= 0) return VISITOR_COLORS[i % VISITOR_COLORS.length];
|
|
68
|
+
let h = 0;
|
|
69
|
+
const s = String(name || '');
|
|
70
|
+
for (let k = 0; k < s.length; k += 1) h = (h * 33 + s.charCodeAt(k)) >>> 0;
|
|
71
|
+
return VISITOR_COLORS[h % VISITOR_COLORS.length];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeVisitor(raw) {
|
|
75
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
76
|
+
const id = String(raw.id || '').trim().toLowerCase();
|
|
77
|
+
const shortname = String(raw.shortname || raw.name || '').trim().toLowerCase();
|
|
78
|
+
let color = String(raw.color || '').trim().toLowerCase();
|
|
79
|
+
if (!/^[a-f0-9-]{8,36}$/.test(id)) return null;
|
|
80
|
+
if (!/^[a-z][a-z0-9]{1,15}$/.test(shortname)) return null;
|
|
81
|
+
if (/^[0-9a-f]{6}$/.test(color)) color = `#${color}`;
|
|
82
|
+
if (!/^#[0-9a-f]{6}$/.test(color)) color = colorForShortname(shortname);
|
|
83
|
+
return { id, shortname, color };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function mintVisitor(used = usedShortnames) {
|
|
87
|
+
const taken = used instanceof Set ? used : usedShortnames;
|
|
88
|
+
const pool = VISITOR_SHORTNAMES.filter((n) => !taken.has(n));
|
|
89
|
+
const pickFrom = pool.length ? pool : VISITOR_SHORTNAMES;
|
|
90
|
+
const shortname = pickFrom[crypto.randomInt(pickFrom.length)];
|
|
91
|
+
taken.add(shortname);
|
|
92
|
+
return {
|
|
93
|
+
id: crypto.randomBytes(4).toString('hex'),
|
|
94
|
+
shortname,
|
|
95
|
+
color: colorForShortname(shortname),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Compact cookie value: `<8-char-id>.<shortname>.<rrggbb>` (no #). */
|
|
100
|
+
export function serializeOzWho(visitor) {
|
|
101
|
+
const v = normalizeVisitor(visitor);
|
|
102
|
+
if (!v) return '';
|
|
103
|
+
return `${v.id}.${v.shortname}.${v.color.replace(/^#/, '')}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function parseOzWhoValue(value) {
|
|
107
|
+
const s = String(value || '').trim();
|
|
108
|
+
if (!s) return null;
|
|
109
|
+
if (s.startsWith('{')) {
|
|
110
|
+
try { return normalizeVisitor(JSON.parse(s)); } catch { return null; }
|
|
111
|
+
}
|
|
112
|
+
const decoded = (() => {
|
|
113
|
+
try { return decodeURIComponent(s); } catch { return s; }
|
|
114
|
+
})();
|
|
115
|
+
const m = decoded.match(/^([a-f0-9-]{8,36})\.([a-z][a-z0-9]{1,15})\.([0-9a-f]{6})$/i);
|
|
116
|
+
if (!m) return null;
|
|
117
|
+
return normalizeVisitor({ id: m[1], shortname: m[2], color: m[3] });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseOzWhoCookie(header) {
|
|
121
|
+
const raw = String(header || '');
|
|
122
|
+
const parts = raw.split(';');
|
|
123
|
+
for (const part of parts) {
|
|
124
|
+
const eq = part.indexOf('=');
|
|
125
|
+
if (eq < 0) continue;
|
|
126
|
+
const k = part.slice(0, eq).trim();
|
|
127
|
+
if (k !== OZ_WHO_COOKIE) continue;
|
|
128
|
+
return parseOzWhoValue(part.slice(eq + 1).trim());
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function ozWhoSetCookie(visitor) {
|
|
134
|
+
const value = serializeOzWho(visitor);
|
|
135
|
+
return `${OZ_WHO_COOKIE}=${value}; Path=/; Max-Age=${OZ_WHO_MAX_AGE}; SameSite=Lax`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function ensureVisitorFromRequest(req) {
|
|
139
|
+
const existing = parseOzWhoCookie(req?.headers?.cookie);
|
|
140
|
+
if (existing) {
|
|
141
|
+
usedShortnames.add(existing.shortname);
|
|
142
|
+
return { visitor: existing, setCookie: null };
|
|
143
|
+
}
|
|
144
|
+
const visitor = mintVisitor(usedShortnames);
|
|
145
|
+
return { visitor, setCookie: ozWhoSetCookie(visitor) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function stripVisitorPrompt(text) {
|
|
149
|
+
const s = String(text || '');
|
|
150
|
+
const m = s.match(/^([a-z][a-z0-9]{1,15}):\s+/);
|
|
151
|
+
if (m && isVisitorShortname(m[1])) return s.slice(m[0].length);
|
|
152
|
+
return s;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function formatVisitorPrompt(shortname, prompt) {
|
|
156
|
+
const p = String(prompt || '');
|
|
157
|
+
const n = String(shortname || '').toLowerCase();
|
|
158
|
+
if (!n) return p;
|
|
159
|
+
const prefix = `${n}: `;
|
|
160
|
+
if (p.startsWith(prefix)) return p;
|
|
161
|
+
return prefix + p;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Grok Bot paints `richText`, not `content`. Unprefixed richText is why every
|
|
165
|
+
* visitor looked like the same grey "you" pill. */
|
|
166
|
+
export function prefixVisitorRichText(richText, shortname, labeledText) {
|
|
167
|
+
const labeled = String(labeledText || '');
|
|
168
|
+
const fallback = JSON.stringify({
|
|
169
|
+
type: 'doc',
|
|
170
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: labeled }] }],
|
|
171
|
+
});
|
|
172
|
+
const n = String(shortname || '').toLowerCase();
|
|
173
|
+
if (!labeled) return typeof richText === 'string' ? richText : (richText ? JSON.stringify(richText) : fallback);
|
|
174
|
+
if (!n) return fallback;
|
|
175
|
+
const prefix = `${n}: `;
|
|
176
|
+
let doc = richText;
|
|
177
|
+
if (doc == null || doc === '') return fallback;
|
|
178
|
+
if (typeof doc === 'string') {
|
|
179
|
+
try { doc = JSON.parse(doc); } catch { return fallback; }
|
|
180
|
+
}
|
|
181
|
+
if (!doc || typeof doc !== 'object') return fallback;
|
|
182
|
+
let clone;
|
|
183
|
+
try { clone = JSON.parse(JSON.stringify(doc)); } catch { return fallback; }
|
|
184
|
+
const blocks = Array.isArray(clone.content) ? clone.content : null;
|
|
185
|
+
const para = blocks ? blocks.find((c) => c && c.type === 'paragraph') : null;
|
|
186
|
+
if (!para) return fallback;
|
|
187
|
+
if (!Array.isArray(para.content)) para.content = [];
|
|
188
|
+
const first = para.content.find((c) => c && c.type === 'text' && typeof c.text === 'string');
|
|
189
|
+
if (first) {
|
|
190
|
+
if (!first.text.startsWith(prefix)) first.text = prefix + String(first.text).replace(/^\s+/, '');
|
|
191
|
+
} else {
|
|
192
|
+
para.content.unshift({ type: 'text', text: prefix });
|
|
193
|
+
}
|
|
194
|
+
return JSON.stringify(clone);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const MIME = {
|
|
198
|
+
html: 'text/html; charset=utf-8',
|
|
199
|
+
js: 'application/javascript; charset=utf-8',
|
|
200
|
+
mjs: 'application/javascript; charset=utf-8',
|
|
201
|
+
css: 'text/css; charset=utf-8',
|
|
202
|
+
json: 'application/json',
|
|
203
|
+
svg: 'image/svg+xml',
|
|
204
|
+
png: 'image/png',
|
|
205
|
+
jpg: 'image/jpeg',
|
|
206
|
+
jpeg: 'image/jpeg',
|
|
207
|
+
gif: 'image/gif',
|
|
208
|
+
webp: 'image/webp',
|
|
209
|
+
woff: 'font/woff',
|
|
210
|
+
woff2: 'font/woff2',
|
|
211
|
+
ttf: 'font/ttf',
|
|
212
|
+
wasm: 'application/wasm',
|
|
213
|
+
map: 'application/json',
|
|
214
|
+
txt: 'text/plain; charset=utf-8',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const HOST_STATUS = {
|
|
218
|
+
hostVersion: '0.30.0',
|
|
219
|
+
latestHostVersion: '0.30.0',
|
|
220
|
+
hostUpdateAvailable: false,
|
|
221
|
+
isBusy: false,
|
|
222
|
+
capabilities: [],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const COORD_DEFAULTS = {
|
|
226
|
+
listAgents: [],
|
|
227
|
+
countAgents: 0,
|
|
228
|
+
searchAgents: [],
|
|
229
|
+
searchMedia: [],
|
|
230
|
+
getTrays: [],
|
|
231
|
+
getHostStatus: HOST_STATUS,
|
|
232
|
+
sendPrompt: { accepted: true },
|
|
233
|
+
promptAcceptanceStatus: { outcome: 'found', record: { status: 'accepted' } },
|
|
234
|
+
getAgentTranscriptTail: { entries: [] },
|
|
235
|
+
openAgentTail: { entries: [] },
|
|
236
|
+
getForeverBoxStatus: { agentId: 'openzoo', state: 'ready' },
|
|
237
|
+
ensureForeverBox: { agentId: 'openzoo', state: 'ready' },
|
|
238
|
+
getBoxSecretsStatus: { keys: [], isApplied: false, lastAppliedAtMs: null },
|
|
239
|
+
isGlobalSearchEnabled: false,
|
|
240
|
+
isEgressTunnelAvailable: false,
|
|
241
|
+
getSharingState: { isEnabled: false, selfAuthId: null, pendingJoinRequests: [], rooms: [], typingUsers: [] },
|
|
242
|
+
getTeachRecordingStatus: { recording: false },
|
|
243
|
+
getBotTemplateExportPolicy: { allowed: true },
|
|
244
|
+
listBotTemplates: [],
|
|
245
|
+
skillsCatalog: [],
|
|
246
|
+
syncPluginSkills: [],
|
|
247
|
+
getPluginSyncStatus: { ok: true },
|
|
248
|
+
getMcpState: { servers: [] },
|
|
249
|
+
getMcpCatalog: [],
|
|
250
|
+
getEffectiveMcpPlugins: [],
|
|
251
|
+
getSubagents: [],
|
|
252
|
+
getAsyncTasks: [],
|
|
253
|
+
getAgentWorkflows: [],
|
|
254
|
+
getAgentAutomations: [],
|
|
255
|
+
listAllAutomations: [],
|
|
256
|
+
getListenerIntegrations: {},
|
|
257
|
+
getAgentChannels: { manifests: [], connections: [] },
|
|
258
|
+
getConversationOutline: [],
|
|
259
|
+
readVoiceCallSentMessages: [],
|
|
260
|
+
getVoiceCall: null,
|
|
261
|
+
getHostSettings: { settings: {} },
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const VOID_METHODS = new Set([
|
|
265
|
+
'resolveAutoReviewApproval', 'resolveLocalToolPermission', 'submitSecret',
|
|
266
|
+
'submitUserForm', 'dismissUserForm', 'reactToMessage', 'voteFeedback',
|
|
267
|
+
'deleteBotTemplate', 'setAgentUnread', 'setAgentHiddenFromSidebar',
|
|
268
|
+
'setAgentNotificationsEnabled', 'setAgentNotifyOnUpdates', 'runAgentWorkflowNow',
|
|
269
|
+
'recordVoiceCall', 'handBackForeverBox', 'dismissTray', 'clearTrays',
|
|
270
|
+
'setSharedRoomTyping',
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
export function findGrokBotAsar() {
|
|
274
|
+
if (process.env.GROK_BOT_ASAR && fs.existsSync(process.env.GROK_BOT_ASAR)) {
|
|
275
|
+
return process.env.GROK_BOT_ASAR;
|
|
276
|
+
}
|
|
277
|
+
const names = [
|
|
278
|
+
path.join(process.cwd(), 'Grok Bot.app', 'Contents', 'Resources', 'app.asar'),
|
|
279
|
+
path.join(HERE, '..', 'Grok Bot.app', 'Contents', 'Resources', 'app.asar'),
|
|
280
|
+
'/Applications/Grok Bot.app/Contents/Resources/app.asar',
|
|
281
|
+
path.join(os.homedir(), 'Applications', 'Grok Bot.app', 'Contents', 'Resources', 'app.asar'),
|
|
282
|
+
];
|
|
283
|
+
return names.find((p) => fs.existsSync(p)) || null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function loadAsar(asarPath) {
|
|
287
|
+
const fd = fs.openSync(asarPath, 'r');
|
|
288
|
+
const sizeBuf = Buffer.alloc(8);
|
|
289
|
+
if (fs.readSync(fd, sizeBuf, 0, 8, 0) !== 8) {
|
|
290
|
+
fs.closeSync(fd);
|
|
291
|
+
throw new Error('asar: short size header');
|
|
292
|
+
}
|
|
293
|
+
const headerPickleSize = sizeBuf.readUInt32LE(4);
|
|
294
|
+
const headerBuf = Buffer.alloc(headerPickleSize);
|
|
295
|
+
if (fs.readSync(fd, headerBuf, 0, headerPickleSize, 8) !== headerPickleSize) {
|
|
296
|
+
fs.closeSync(fd);
|
|
297
|
+
throw new Error('asar: short header pickle');
|
|
298
|
+
}
|
|
299
|
+
const jsonLen = headerBuf.readUInt32LE(4);
|
|
300
|
+
const json = JSON.parse(headerBuf.slice(8, 8 + jsonLen).toString('utf8'));
|
|
301
|
+
return {
|
|
302
|
+
fd,
|
|
303
|
+
asarPath,
|
|
304
|
+
json,
|
|
305
|
+
payloadOffset: 8 + headerPickleSize,
|
|
306
|
+
unpackedDir: asarPath + '.unpacked',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function walkAsar(node, parts) {
|
|
311
|
+
let cur = node;
|
|
312
|
+
for (const part of parts) {
|
|
313
|
+
const files = cur?.files || cur;
|
|
314
|
+
cur = files?.[part];
|
|
315
|
+
if (!cur) return null;
|
|
316
|
+
}
|
|
317
|
+
return cur;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function readAsarFile(archive, rel) {
|
|
321
|
+
const parts = String(rel || '').split('/').filter(Boolean);
|
|
322
|
+
const node = walkAsar(archive.json, parts);
|
|
323
|
+
if (!node || node.files) return null;
|
|
324
|
+
if (node.unpacked) {
|
|
325
|
+
const full = path.join(archive.unpackedDir, ...parts);
|
|
326
|
+
try { return fs.readFileSync(full); } catch { return null; }
|
|
327
|
+
}
|
|
328
|
+
const size = Number(node.size) || 0;
|
|
329
|
+
const offset = Number(node.offset) || 0;
|
|
330
|
+
const buf = Buffer.alloc(size);
|
|
331
|
+
const n = fs.readSync(archive.fd, buf, 0, size, archive.payloadOffset + offset);
|
|
332
|
+
return n === size ? buf : buf.subarray(0, n);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export const MOBILE_CSS = `/* cafe phone: canvas first, chrome out of the way.
|
|
336
|
+
min-width is always 0 so iOS does not zoom the layout past the 720px
|
|
337
|
+
breakpoint (asar --sand-chat-min-width:424px + sidebar 280px did that). */
|
|
338
|
+
html, body, #root, .sand-shell {
|
|
339
|
+
min-width: 0 !important;
|
|
340
|
+
max-width: 100%;
|
|
341
|
+
}
|
|
342
|
+
:root {
|
|
343
|
+
--sand-chat-min-width: 0px !important;
|
|
344
|
+
}
|
|
345
|
+
@media (max-width: 900px), (pointer: coarse) {
|
|
346
|
+
html, body, #root, .sand-shell {
|
|
347
|
+
width: 100%;
|
|
348
|
+
max-width: 100vw;
|
|
349
|
+
height: 100dvh;
|
|
350
|
+
overflow-x: hidden;
|
|
351
|
+
overflow-y: auto;
|
|
352
|
+
}
|
|
353
|
+
:root {
|
|
354
|
+
--sand-titlebar-block: env(safe-area-inset-top, 0px) !important;
|
|
355
|
+
}
|
|
356
|
+
html.oz-narrow [data-agent-id] {
|
|
357
|
+
min-width: 44px;
|
|
358
|
+
min-height: 44px;
|
|
359
|
+
}
|
|
360
|
+
html.oz-narrow textarea,
|
|
361
|
+
html.oz-narrow input:not([type="checkbox"]):not([type="radio"]) {
|
|
362
|
+
font-size: 16px !important;
|
|
363
|
+
}
|
|
364
|
+
html.oz-narrow .sand-new-chat-bar,
|
|
365
|
+
html.oz-narrow .sand-new-chat-menu,
|
|
366
|
+
html.oz-narrow .sand-kit-message-input-frame {
|
|
367
|
+
z-index: 40;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
#oz-new-chat {
|
|
371
|
+
display: flex;
|
|
372
|
+
position: fixed;
|
|
373
|
+
top: max(10px, env(safe-area-inset-top, 0px));
|
|
374
|
+
right: max(10px, env(safe-area-inset-right, 0px));
|
|
375
|
+
z-index: 2147483000;
|
|
376
|
+
width: 44px;
|
|
377
|
+
height: 44px;
|
|
378
|
+
margin: 0;
|
|
379
|
+
padding: 0;
|
|
380
|
+
align-items: center;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
border-radius: 999px;
|
|
383
|
+
border: 1px solid rgba(255,255,255,.4);
|
|
384
|
+
background: #2a2a2a;
|
|
385
|
+
color: #f3f3f3;
|
|
386
|
+
font: 600 28px/1 ui-sans-serif, system-ui, sans-serif;
|
|
387
|
+
box-shadow: 0 2px 10px rgba(0,0,0,.45);
|
|
388
|
+
-webkit-tap-highlight-color: transparent;
|
|
389
|
+
touch-action: manipulation;
|
|
390
|
+
}
|
|
391
|
+
#oz-new-chat:active { background: #3a3a3a; }
|
|
392
|
+
#oz-new-chat[hidden] { display: none !important; }
|
|
393
|
+
[data-oz-spend-hide] { display: none !important; }
|
|
394
|
+
`;
|
|
395
|
+
|
|
396
|
+
export function injectIndexHtml(html) {
|
|
397
|
+
let out = String(html);
|
|
398
|
+
out = out.replace(
|
|
399
|
+
/content="default-src 'self';[^"]*"/,
|
|
400
|
+
'content="default-src \'self\'; style-src \'self\' \'unsafe-inline\'; script-src \'self\'; worker-src \'self\' blob:; font-src \'self\' data:; img-src \'self\' data: blob: https:; media-src \'self\' blob:; connect-src \'self\' ws: wss: blob:;"',
|
|
401
|
+
);
|
|
402
|
+
if (!out.includes('viewport-fit')) {
|
|
403
|
+
out = out.replace(
|
|
404
|
+
/<meta name="viewport" content="width=device-width, initial-scale=1\.0"\s*\/>/,
|
|
405
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />',
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!out.includes('oz-shim.js')) {
|
|
409
|
+
out = out.replace('<head>', '<head>\n <script src="./oz-shim.js"></script>');
|
|
410
|
+
}
|
|
411
|
+
if (!out.includes('oz-mobile.css')) {
|
|
412
|
+
out = out.replace('</head>', ' <link rel="stylesheet" href="./oz-mobile.css" />\n </head>');
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function unwrapPodJson(j) {
|
|
418
|
+
if (Array.isArray(j)) return j;
|
|
419
|
+
if (j && typeof j === 'object' && j.status === 'ok' && 'value' in j) return j.value;
|
|
420
|
+
return j;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function podApi(method, args = {}, { hijack = HIJACK, fetchImpl = fetch } = {}) {
|
|
424
|
+
const url = `${hijack.replace(/\/$/, '')}/api/${method}`;
|
|
425
|
+
const r = await fetchImpl(url, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
428
|
+
body: JSON.stringify(args && typeof args === 'object' ? args : {}),
|
|
429
|
+
});
|
|
430
|
+
const text = await r.text();
|
|
431
|
+
if (!text) return undefined;
|
|
432
|
+
try { return unwrapPodJson(JSON.parse(text)); } catch { return text; }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function defaultFor(method) {
|
|
436
|
+
if (Object.prototype.hasOwnProperty.call(COORD_DEFAULTS, method)) return COORD_DEFAULTS[method];
|
|
437
|
+
if (VOID_METHODS.has(method)) return null;
|
|
438
|
+
if (/^is[A-Z]/.test(method)) return false;
|
|
439
|
+
if (/^(list|search|get.*s)$/.test(method)) return [];
|
|
440
|
+
return {};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function handleCoordinatorRequest(method, args, opts = {}) {
|
|
444
|
+
const hijack = opts.hijack || HIJACK;
|
|
445
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
446
|
+
if (method === 'sendPrompt' && opts.visitor) {
|
|
447
|
+
const v = normalizeVisitor(opts.visitor);
|
|
448
|
+
if (v) args = { ...(args && typeof args === 'object' ? args : {}), visitor: v };
|
|
449
|
+
}
|
|
450
|
+
if (method === 'getHostStatus') return HOST_STATUS;
|
|
451
|
+
if (method === 'countAgents') {
|
|
452
|
+
try {
|
|
453
|
+
const list = await podApi('listAgents', {}, { hijack, fetchImpl });
|
|
454
|
+
if (Array.isArray(list) && list.length) return list.length;
|
|
455
|
+
} catch { /* fall through */ }
|
|
456
|
+
return readHouseRoster(os.homedir()).length;
|
|
457
|
+
}
|
|
458
|
+
if (method === 'searchAgents') {
|
|
459
|
+
try {
|
|
460
|
+
let list = await podApi('listAgents', {}, { hijack, fetchImpl });
|
|
461
|
+
if (!Array.isArray(list) || !list.length) list = readHouseRoster(os.homedir());
|
|
462
|
+
const q = String(args?.query || args?.q || '').toLowerCase();
|
|
463
|
+
if (!q) return list;
|
|
464
|
+
return list.filter((a) => String(a?.name || a?.title || a?.id || '').toLowerCase().includes(q));
|
|
465
|
+
} catch { return readHouseRoster(os.homedir()); }
|
|
466
|
+
}
|
|
467
|
+
if (method === 'promptAcceptanceStatus') {
|
|
468
|
+
try {
|
|
469
|
+
const got = await podApi(method, args, { hijack, fetchImpl });
|
|
470
|
+
if (got && typeof got === 'object' && typeof got.outcome === 'string') return got;
|
|
471
|
+
} catch { /* */ }
|
|
472
|
+
const nonce = String(args?.clientNonce || '');
|
|
473
|
+
return {
|
|
474
|
+
outcome: 'found',
|
|
475
|
+
record: {
|
|
476
|
+
status: 'accepted',
|
|
477
|
+
acceptedAtMs: Date.now(),
|
|
478
|
+
echoEntryId: nonce,
|
|
479
|
+
clientNonce: nonce,
|
|
480
|
+
agentId: String(args?.agentId || ''),
|
|
481
|
+
inputDigest: '',
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const proxied = new Set([
|
|
487
|
+
'listAgents', 'sendPrompt', 'getAgentTranscriptTail', 'openAgentTail',
|
|
488
|
+
'createAgent', 'createAgentFromTemplate', 'duplicateAgent', 'updateAgent',
|
|
489
|
+
'deleteAgents', 'getTrays', 'createGroup', 'setGroupMembers',
|
|
490
|
+
'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
|
|
491
|
+
'setAgentNotifyOnUpdates', 'getAgentAvatar', 'setAgentAvatarBytes',
|
|
492
|
+
'interruptAgentRun', 'kickstartAgent',
|
|
493
|
+
'uploadAttachment', 'readAttachmentImage', 'readAttachmentText', 'readAttachmentChunk',
|
|
494
|
+
]);
|
|
495
|
+
if (proxied.has(method) || !Object.prototype.hasOwnProperty.call(COORD_DEFAULTS, method)) {
|
|
496
|
+
try {
|
|
497
|
+
const got = await podApi(method, args, { hijack, fetchImpl });
|
|
498
|
+
if (got !== undefined) {
|
|
499
|
+
if (method === 'getHostStatus') return HOST_STATUS;
|
|
500
|
+
if (method === 'getAgentTranscriptTail' || method === 'openAgentTail') {
|
|
501
|
+
if (got && Array.isArray(got.entries)) return got;
|
|
502
|
+
}
|
|
503
|
+
if (method === 'listAgents' && Array.isArray(got)) {
|
|
504
|
+
return got.length ? got : readHouseRoster(os.homedir());
|
|
505
|
+
}
|
|
506
|
+
if (method === 'sendPrompt' && got && typeof got === 'object') {
|
|
507
|
+
return got.accepted === true ? got : { ...got, accepted: true };
|
|
508
|
+
}
|
|
509
|
+
if (method === 'uploadAttachment' && got && typeof got === 'object' && got.path) {
|
|
510
|
+
return got;
|
|
511
|
+
}
|
|
512
|
+
return got;
|
|
513
|
+
}
|
|
514
|
+
} catch { /* defaults below */ }
|
|
515
|
+
}
|
|
516
|
+
return defaultFor(method);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function coordinatorReply(requestId, value) {
|
|
520
|
+
return { kind: 'reply', requestId, outcome: { status: 'ok', value: value === undefined ? null : value } };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function coordinatorFail(requestId, message) {
|
|
524
|
+
return {
|
|
525
|
+
kind: 'reply',
|
|
526
|
+
requestId,
|
|
527
|
+
outcome: { status: 'failed', failure: { code: 'gateway/unknown-method', message: String(message || 'failed') } },
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function lifecycleReady() {
|
|
532
|
+
return { kind: 'lifecycle', phase: 'ready', protocolVersion: PROTOCOL_VERSION };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function transportConnected() {
|
|
536
|
+
return { kind: 'event', family: 'coordinator-transport-state', payload: { state: 'connected' } };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function transcriptSourceGateway() {
|
|
540
|
+
return { kind: 'event', family: 'coordinator-transcript-source', payload: { source: 'gateway' } };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export async function handleCoordinatorFrame(frame, send, opts = {}) {
|
|
544
|
+
if (!frame || typeof frame !== 'object') return;
|
|
545
|
+
if (frame.kind === 'lifecycle' && frame.phase === 'hello') {
|
|
546
|
+
send(lifecycleReady());
|
|
547
|
+
send(transportConnected());
|
|
548
|
+
send(transcriptSourceGateway());
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (frame.kind === 'lifecycle' && frame.phase === 'shutdown') return;
|
|
552
|
+
if (frame.kind === 'cancel') return;
|
|
553
|
+
if (frame.kind !== 'request') return;
|
|
554
|
+
const id = frame.requestId;
|
|
555
|
+
try {
|
|
556
|
+
const value = await handleCoordinatorRequest(frame.method, frame.args || {}, opts);
|
|
557
|
+
send(coordinatorReply(id, value));
|
|
558
|
+
} catch (err) {
|
|
559
|
+
send(coordinatorFail(id, err?.message || err));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
564
|
+
|
|
565
|
+
function wsAccept(key) {
|
|
566
|
+
return crypto.createHash('sha1').update(String(key) + WS_GUID).digest('base64');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function wsEncode(payload) {
|
|
570
|
+
const data = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload));
|
|
571
|
+
const len = data.length;
|
|
572
|
+
let header;
|
|
573
|
+
if (len < 126) {
|
|
574
|
+
header = Buffer.alloc(2);
|
|
575
|
+
header[0] = 0x81;
|
|
576
|
+
header[1] = len;
|
|
577
|
+
} else if (len < 65536) {
|
|
578
|
+
header = Buffer.alloc(4);
|
|
579
|
+
header[0] = 0x81;
|
|
580
|
+
header[1] = 126;
|
|
581
|
+
header.writeUInt16BE(len, 2);
|
|
582
|
+
} else {
|
|
583
|
+
header = Buffer.alloc(10);
|
|
584
|
+
header[0] = 0x81;
|
|
585
|
+
header[1] = 127;
|
|
586
|
+
header.writeUInt32BE(0, 2);
|
|
587
|
+
header.writeUInt32BE(len, 6);
|
|
588
|
+
}
|
|
589
|
+
return Buffer.concat([header, data]);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function wsPong(payload) {
|
|
593
|
+
const data = Buffer.isBuffer(payload) ? payload : Buffer.alloc(0);
|
|
594
|
+
const header = Buffer.alloc(2);
|
|
595
|
+
header[0] = 0x8a;
|
|
596
|
+
header[1] = data.length;
|
|
597
|
+
return Buffer.concat([header, data]);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function attachWs(socket, onText) {
|
|
601
|
+
let buf = Buffer.alloc(0);
|
|
602
|
+
const send = (obj) => {
|
|
603
|
+
if (socket.destroyed) return;
|
|
604
|
+
try { socket.write(wsEncode(JSON.stringify(obj))); } catch { /* */ }
|
|
605
|
+
};
|
|
606
|
+
socket.on('data', (chunk) => {
|
|
607
|
+
buf = Buffer.concat([buf, chunk]);
|
|
608
|
+
while (buf.length >= 2) {
|
|
609
|
+
const opcode = buf[0] & 0x0f;
|
|
610
|
+
const masked = (buf[1] & 0x80) !== 0;
|
|
611
|
+
let len = buf[1] & 0x7f;
|
|
612
|
+
let offset = 2;
|
|
613
|
+
if (len === 126) {
|
|
614
|
+
if (buf.length < 4) return;
|
|
615
|
+
len = buf.readUInt16BE(2);
|
|
616
|
+
offset = 4;
|
|
617
|
+
} else if (len === 127) {
|
|
618
|
+
if (buf.length < 10) return;
|
|
619
|
+
len = Number(buf.readBigUInt64BE(2));
|
|
620
|
+
offset = 10;
|
|
621
|
+
}
|
|
622
|
+
const maskLen = masked ? 4 : 0;
|
|
623
|
+
if (buf.length < offset + maskLen + len) return;
|
|
624
|
+
let payload = buf.subarray(offset + maskLen, offset + maskLen + len);
|
|
625
|
+
if (masked) {
|
|
626
|
+
const mask = buf.subarray(offset, offset + 4);
|
|
627
|
+
const decoded = Buffer.alloc(len);
|
|
628
|
+
for (let i = 0; i < len; i += 1) decoded[i] = payload[i] ^ mask[i % 4];
|
|
629
|
+
payload = decoded;
|
|
630
|
+
}
|
|
631
|
+
buf = buf.subarray(offset + maskLen + len);
|
|
632
|
+
if (opcode === 0x8) { socket.end(); return; }
|
|
633
|
+
if (opcode === 0x9) { socket.write(wsPong(payload)); continue; }
|
|
634
|
+
if (opcode === 0xa) continue;
|
|
635
|
+
if (opcode === 0x1) {
|
|
636
|
+
let frame;
|
|
637
|
+
try { frame = JSON.parse(payload.toString('utf8')); } catch { continue; }
|
|
638
|
+
onText(frame, send);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
return send;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function ensureProxy(log) {
|
|
646
|
+
const base = `http://localhost:${config.port}/v1`;
|
|
647
|
+
try {
|
|
648
|
+
if ((await fetch(`${base}/models`, { signal: AbortSignal.timeout(1500) })).ok) return;
|
|
649
|
+
} catch { /* start it */ }
|
|
650
|
+
log(`openzoo-web: starting proxy on ${base}`);
|
|
651
|
+
const { startProxy } = await import('./proxy.js');
|
|
652
|
+
await startProxy({ silent: true, autoTunnel: process.env.OPENZOO_NO_TUNNEL === '1' ? false : true });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function ensureHijack(log) {
|
|
656
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
657
|
+
process.env.OPENZOO_BYOK = '1';
|
|
658
|
+
if (!process.env.OZ_HIJACK_POD) {
|
|
659
|
+
const url = HIJACK;
|
|
660
|
+
process.env.OZ_HIJACK_POD = JSON.stringify({
|
|
661
|
+
region: 'local',
|
|
662
|
+
accountId: 'openzoo',
|
|
663
|
+
podId: 'openzoo-local',
|
|
664
|
+
token: 'openzoo',
|
|
665
|
+
agent: url,
|
|
666
|
+
vnc: url,
|
|
667
|
+
p1340: url,
|
|
668
|
+
p6081: url,
|
|
669
|
+
});
|
|
670
|
+
process.env.OZ_SNIFF_SELF = url;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const r = await fetch(`${HIJACK}/health`, { signal: AbortSignal.timeout(1500) });
|
|
674
|
+
if (r.ok) {
|
|
675
|
+
log(`openzoo-web: reusing hijack at ${HIJACK}`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
} catch { /* start */ }
|
|
679
|
+
const { startCursorBackend } = await import('./cursorbackend.js');
|
|
680
|
+
const models = ['gpt-4o', 'gpt-4o-mini'].map((n) => ({ name: n, label: n }));
|
|
681
|
+
try {
|
|
682
|
+
startCursorBackend({
|
|
683
|
+
port: 8443,
|
|
684
|
+
models,
|
|
685
|
+
log: (m) => log(` backend: ${m}`),
|
|
686
|
+
});
|
|
687
|
+
log(`openzoo-web: aiserver on ${HIJACK}`);
|
|
688
|
+
} catch (e) {
|
|
689
|
+
log(`openzoo-web: 8443 already bound — reusing (${e.message})`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function hijackSse(onEvent, log) {
|
|
694
|
+
const ctrl = new AbortController();
|
|
695
|
+
(async () => {
|
|
696
|
+
while (!ctrl.signal.aborted) {
|
|
697
|
+
try {
|
|
698
|
+
const r = await fetch(`${HIJACK}/events`, {
|
|
699
|
+
headers: { accept: 'text/event-stream' },
|
|
700
|
+
signal: ctrl.signal,
|
|
701
|
+
});
|
|
702
|
+
if (!r.ok || !r.body) throw new Error(`events ${r.status}`);
|
|
703
|
+
const reader = r.body.getReader();
|
|
704
|
+
const dec = new TextDecoder();
|
|
705
|
+
let acc = '';
|
|
706
|
+
for (;;) {
|
|
707
|
+
const { done, value } = await reader.read();
|
|
708
|
+
if (done) break;
|
|
709
|
+
acc += dec.decode(value, { stream: true });
|
|
710
|
+
const parts = acc.split('\n\n');
|
|
711
|
+
acc = parts.pop() || '';
|
|
712
|
+
for (const block of parts) {
|
|
713
|
+
const line = block.split('\n').find((l) => l.startsWith('data:'));
|
|
714
|
+
if (!line) continue;
|
|
715
|
+
try {
|
|
716
|
+
const msg = JSON.parse(line.slice(5).trim());
|
|
717
|
+
if (msg && msg.channel && msg.channel !== 'ping') {
|
|
718
|
+
onEvent({ kind: 'event', family: msg.channel, payload: msg.payload ?? {} });
|
|
719
|
+
}
|
|
720
|
+
} catch { /* */ }
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
} catch (e) {
|
|
724
|
+
if (ctrl.signal.aborted) return;
|
|
725
|
+
log(`openzoo-web: events reconnect (${e.message})`);
|
|
726
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
})();
|
|
730
|
+
return () => ctrl.abort();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function startGrokBotWeb(opts = {}) {
|
|
734
|
+
const log = opts.log || ((m) => console.error(m));
|
|
735
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
736
|
+
const bind = opts.bind || BIND;
|
|
737
|
+
const asarPath = opts.asarPath || findGrokBotAsar();
|
|
738
|
+
if (!asarPath) {
|
|
739
|
+
throw new Error('Grok Bot.app not found (looked in ./Grok Bot.app and /Applications).');
|
|
740
|
+
}
|
|
741
|
+
const archive = loadAsar(asarPath);
|
|
742
|
+
const shim = fs.readFileSync(SHIM_PATH, 'utf8') + '\n' + spendChipSource();
|
|
743
|
+
|
|
744
|
+
if (!opts.skipBackend) {
|
|
745
|
+
await ensureProxy(log);
|
|
746
|
+
await ensureHijack(log);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const crashes = [];
|
|
750
|
+
const sockets = new Set();
|
|
751
|
+
const stopSse = opts.skipBackend ? () => {} : hijackSse((ev) => {
|
|
752
|
+
for (const send of sockets) send(ev);
|
|
753
|
+
}, log);
|
|
754
|
+
|
|
755
|
+
function writeHead(res, status, headers = {}, reqForWho = null) {
|
|
756
|
+
const out = { ...headers };
|
|
757
|
+
if (reqForWho) {
|
|
758
|
+
const { setCookie } = ensureVisitorFromRequest(reqForWho);
|
|
759
|
+
if (setCookie) out['set-cookie'] = setCookie;
|
|
760
|
+
}
|
|
761
|
+
res.writeHead(status, out);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const server = http.createServer((req, res) => {
|
|
765
|
+
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
766
|
+
if (urlPath === '/oz-health') {
|
|
767
|
+
writeHead(res, 200, { 'content-type': 'application/json' }, req);
|
|
768
|
+
res.end(JSON.stringify({
|
|
769
|
+
ok: true,
|
|
770
|
+
asar: asarPath,
|
|
771
|
+
hijack: HIJACK,
|
|
772
|
+
crashes: crashes.length,
|
|
773
|
+
agents: readHouseRoster(os.homedir()).length,
|
|
774
|
+
}));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (urlPath === '/oz-who') {
|
|
778
|
+
const { visitor, setCookie } = ensureVisitorFromRequest(req);
|
|
779
|
+
const headers = { 'content-type': 'application/json', 'cache-control': 'no-store' };
|
|
780
|
+
if (setCookie) headers['set-cookie'] = setCookie;
|
|
781
|
+
res.writeHead(200, headers);
|
|
782
|
+
res.end(JSON.stringify(visitor));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (urlPath === '/oz-upload' && req.method === 'POST') {
|
|
786
|
+
const chunks = [];
|
|
787
|
+
req.on('data', (c) => chunks.push(c));
|
|
788
|
+
req.on('end', () => {
|
|
789
|
+
let parsed = {};
|
|
790
|
+
try { parsed = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); } catch { parsed = {}; }
|
|
791
|
+
const got = ingestUpload({
|
|
792
|
+
filename: parsed.filename,
|
|
793
|
+
bytesBase64: parsed.bytesBase64,
|
|
794
|
+
});
|
|
795
|
+
writeHead(res, got.ok ? 200 : 413, { 'content-type': 'application/json' }, req);
|
|
796
|
+
res.end(JSON.stringify(got));
|
|
797
|
+
if (got.ok) log(`openzoo-web: upload ${got.path} ${got.bytes}b`);
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (urlPath.startsWith('/oz-upload/') && req.method === 'GET') {
|
|
802
|
+
const rec = lookupUpload(decodeURIComponent(urlPath.slice('/oz-upload'.length)));
|
|
803
|
+
if (!rec) {
|
|
804
|
+
writeHead(res, 404, { 'content-type': 'text/plain' }, req);
|
|
805
|
+
res.end('not found');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
writeHead(res, 200, {
|
|
809
|
+
'content-type': rec.mime || 'application/octet-stream',
|
|
810
|
+
'cache-control': 'private, max-age=3600',
|
|
811
|
+
}, req);
|
|
812
|
+
res.end(rec.buf);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (urlPath === '/oz-crash' && req.method === 'POST') {
|
|
816
|
+
const chunks = [];
|
|
817
|
+
req.on('data', (c) => chunks.push(c));
|
|
818
|
+
req.on('end', () => {
|
|
819
|
+
try {
|
|
820
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
821
|
+
crashes.push(body);
|
|
822
|
+
if (crashes.length > 40) crashes.shift();
|
|
823
|
+
log(`openzoo-web: crash ${body.type || ''} ${String(body.message || '').slice(0, 240)}`);
|
|
824
|
+
} catch { /* */ }
|
|
825
|
+
writeHead(res, 204, {}, req);
|
|
826
|
+
res.end();
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (urlPath === '/oz-crash' && req.method === 'GET') {
|
|
831
|
+
writeHead(res, 200, { 'content-type': 'application/json' }, req);
|
|
832
|
+
res.end(JSON.stringify(crashes, null, 2));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (urlPath === '/oz-mobile.css') {
|
|
836
|
+
writeHead(res, 200, { 'content-type': MIME.css, 'cache-control': 'no-store' }, req);
|
|
837
|
+
res.end(MOBILE_CSS);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (urlPath === '/oz-shim.js') {
|
|
841
|
+
const { visitor, setCookie } = ensureVisitorFromRequest(req);
|
|
842
|
+
const headers = { 'content-type': MIME.js, 'cache-control': 'no-store' };
|
|
843
|
+
if (setCookie) headers['set-cookie'] = setCookie;
|
|
844
|
+
const prelude = `window.__OZ_WHO_PALETTE__=${JSON.stringify(visitorPaletteMap())};\n`
|
|
845
|
+
+ `window.__OZ_WHO__=${JSON.stringify(visitor)};\n`;
|
|
846
|
+
res.writeHead(200, headers);
|
|
847
|
+
res.end(prelude + shim);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
let rel = urlPath === '/' ? 'dist/renderer/index.html' : path.posix.normalize(urlPath).replace(/^\/+/, '');
|
|
851
|
+
if (rel === 'index.html' || !rel.startsWith('dist/')) rel = `dist/renderer/${rel}`;
|
|
852
|
+
const data = readAsarFile(archive, rel);
|
|
853
|
+
if (!data) {
|
|
854
|
+
writeHead(res, 404, { 'content-type': 'text/plain' }, req);
|
|
855
|
+
res.end('not found');
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const ext = rel.split('.').pop();
|
|
859
|
+
let body = data;
|
|
860
|
+
const headers = {
|
|
861
|
+
'content-type': MIME[ext] || 'application/octet-stream',
|
|
862
|
+
'cache-control': rel.endsWith('index.html') ? 'no-store' : 'public, max-age=86400',
|
|
863
|
+
};
|
|
864
|
+
if (rel.endsWith('index.html')) {
|
|
865
|
+
const { setCookie } = ensureVisitorFromRequest(req);
|
|
866
|
+
if (setCookie) headers['set-cookie'] = setCookie;
|
|
867
|
+
body = Buffer.from(injectIndexHtml(data.toString('utf8')), 'utf8');
|
|
868
|
+
}
|
|
869
|
+
res.writeHead(200, headers);
|
|
870
|
+
res.end(body);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
server.on('upgrade', (req, socket) => {
|
|
874
|
+
if ((req.url || '').split('?')[0] !== '/oz-coord') {
|
|
875
|
+
socket.destroy();
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const key = req.headers['sec-websocket-key'];
|
|
879
|
+
if (!key) { socket.destroy(); return; }
|
|
880
|
+
const { visitor, setCookie } = ensureVisitorFromRequest(req);
|
|
881
|
+
socket.write(
|
|
882
|
+
'HTTP/1.1 101 Switching Protocols\r\n'
|
|
883
|
+
+ 'Upgrade: websocket\r\n'
|
|
884
|
+
+ 'Connection: Upgrade\r\n'
|
|
885
|
+
+ `Sec-WebSocket-Accept: ${wsAccept(key)}\r\n`
|
|
886
|
+
+ (setCookie ? `Set-Cookie: ${setCookie}\r\n` : '')
|
|
887
|
+
+ '\r\n',
|
|
888
|
+
);
|
|
889
|
+
const send = attachWs(socket, (frame, s) => {
|
|
890
|
+
if (frame?.kind === 'request') {
|
|
891
|
+
const who = frame.method === 'sendPrompt' && visitor ? ` visitor=${visitor.shortname}` : '';
|
|
892
|
+
log(`openzoo-web: coord ${frame.method}${who}`);
|
|
893
|
+
}
|
|
894
|
+
handleCoordinatorFrame(frame, s, { hijack: HIJACK, visitor }).catch((e) => {
|
|
895
|
+
log(`openzoo-web: coord ${e.message}`);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
sockets.add(send);
|
|
899
|
+
socket.on('close', () => sockets.delete(send));
|
|
900
|
+
socket.on('error', () => sockets.delete(send));
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
await new Promise((resolve, reject) => {
|
|
904
|
+
server.once('error', reject);
|
|
905
|
+
server.listen(port, bind, () => resolve());
|
|
906
|
+
});
|
|
907
|
+
const url = `http://${bind === '0.0.0.0' ? 'localhost' : bind}:${port}/`;
|
|
908
|
+
log(`openzoo-web: Grok Bot renderer at ${url}`);
|
|
909
|
+
log(` asar ${asarPath}`);
|
|
910
|
+
return {
|
|
911
|
+
url,
|
|
912
|
+
server,
|
|
913
|
+
asarPath,
|
|
914
|
+
close() {
|
|
915
|
+
stopSse();
|
|
916
|
+
try { fs.closeSync(archive.fd); } catch { /* */ }
|
|
917
|
+
server.close();
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export async function runGrokBotWeb(argv = []) {
|
|
923
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
924
|
+
const log = (m) => console.error(m);
|
|
925
|
+
const started = await startGrokBotWeb({ log });
|
|
926
|
+
if (!argv.includes('--no-open') && process.platform === 'darwin') {
|
|
927
|
+
exec(`open ${JSON.stringify(started.url)}`);
|
|
928
|
+
}
|
|
929
|
+
log('openzoo-web: leave this running. ctrl-c stops it.');
|
|
930
|
+
if (argv.includes('--once')) return started;
|
|
931
|
+
await new Promise(() => {});
|
|
932
|
+
return started;
|
|
933
|
+
}
|