openzoo 0.50.97 → 0.50.99
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/cursor-backend.js +1 -1
- package/bin/openzoo.js +9 -0
- package/lib/cursorapi.js +65 -0
- package/lib/cursorbackend.js +879 -77
- package/lib/grokbotAccount.js +192 -3
- package/lib/grokbotFetch.js +258 -0
- package/lib/grokbotweb.js +23 -7
- package/lib/grokcli.js +454 -44
- package/lib/grokui.mjs +6524 -0
- package/lib/launch.js +1 -1
- package/lib/models.js +11 -1
- package/lib/openzooPathShim.js +60 -0
- package/lib/ozSpendChip.js +80 -3
- package/lib/pay.js +30 -2
- package/lib/podagent.mjs +3 -1
- package/lib/proxy.js +17 -2
- package/lib/setup.js +1 -1
- package/package.json +1 -1
- package/lib/cursorbackend.js.bak-hiddenbots +0 -4072
package/lib/grokbotAccount.js
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* just because no Cursor account has logged in.
|
|
11
11
|
*/
|
|
12
12
|
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
13
14
|
import path from 'node:path';
|
|
15
|
+
import { generateKeyPairSync } from 'node:crypto';
|
|
14
16
|
|
|
15
17
|
export function accountSlug(accountId) {
|
|
16
18
|
const s = String(accountId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
|
|
@@ -204,11 +206,179 @@ export function displayName(raw = {}) {
|
|
|
204
206
|
export function briefFromName(name) {
|
|
205
207
|
const n = String(name || '').trim();
|
|
206
208
|
if (!n || /^(new bot|chat|group)$/i.test(n) || looksLikeAgentId(n)) return '';
|
|
209
|
+
// grokroom threads (# main) and bubble-members are not jobs — a brief would
|
|
210
|
+
// kick a zoo turn on a gossip canvas.
|
|
211
|
+
if (/^#\s/.test(n)) return '';
|
|
207
212
|
const role = n.replace(/^\d+\s*[·.•.\-—–]+\s*/, '').trim() || n;
|
|
208
213
|
if (role.length < 2) return '';
|
|
209
214
|
return `You are ${n}. Your job is ${role}. Do that job. Do not ask the human to re-brief you. Coordinate with list_agents and message_agent.`;
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
/** Overlay sidebar id for a grokroom gossip room. */
|
|
218
|
+
export function grokroomAgentId(roomId) {
|
|
219
|
+
const id = String(roomId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 40);
|
|
220
|
+
return id ? `room-${id}` : '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Paymaster `from` field: 1–24 chars, letters/numbers/space/._'- */
|
|
224
|
+
export function grokroomFromHandle(name, fallback = 'anon') {
|
|
225
|
+
const fb = String(fallback || 'anon').replace(/[^A-Za-z0-9 _.'-]/g, '').trim().slice(0, 24) || 'anon';
|
|
226
|
+
const s = String(name || fb).replace(/[^A-Za-z0-9 _.'-]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 24);
|
|
227
|
+
return s || fb;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Public page on openzoo.fun for a grokroom account (paymaster + browser signer). */
|
|
231
|
+
export function grokroomShareUrl(addr, { origin = 'https://openzoo.fun' } = {}) {
|
|
232
|
+
const a = String(addr || '').trim();
|
|
233
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a)) return '';
|
|
234
|
+
return `${String(origin || 'https://openzoo.fun').replace(/\/$/, '')}/r/${a}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Extract a paymaster room addr from a share URL, `/join …`, or a bare pubkey. */
|
|
238
|
+
export function grokroomAddrFromShare(s) {
|
|
239
|
+
const t = String(s || '').trim();
|
|
240
|
+
if (!t) return '';
|
|
241
|
+
const m = t.match(/openzoo\.fun\/r\/([1-9A-HJ-NP-Za-km-z]{32,44})/i)
|
|
242
|
+
|| t.match(/(?:^|\s)\/join\s+(\S+)/i);
|
|
243
|
+
const cand = m ? m[1] : t;
|
|
244
|
+
const addr = String(cand).replace(/^https?:\/\/[^/]+\/r\//i, '').replace(/[/?#].*$/, '').trim();
|
|
245
|
+
return grokroomShareUrl(addr) ? addr : '';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Bind an existing public room onto a local group (join). Does not mint a new key. */
|
|
249
|
+
export function bindOnchainRoom(agent, addr, home = os.homedir()) {
|
|
250
|
+
if (!agent || typeof agent !== 'object') return agent;
|
|
251
|
+
const web = grokroomShareUrl(addr);
|
|
252
|
+
if (!web) return agent;
|
|
253
|
+
const rec = {
|
|
254
|
+
agentId: String(agent.id || ''),
|
|
255
|
+
name: String(agent.name || 'group'),
|
|
256
|
+
addr,
|
|
257
|
+
web,
|
|
258
|
+
createdAt: Date.now(),
|
|
259
|
+
};
|
|
260
|
+
const all = readOnchainRooms(home).filter((r) => r && r.agentId !== rec.agentId && r.addr !== addr);
|
|
261
|
+
all.push(rec);
|
|
262
|
+
writeOnchainRooms(all, home);
|
|
263
|
+
return {
|
|
264
|
+
...agent,
|
|
265
|
+
isGroup: true,
|
|
266
|
+
room: { id: String(agent.room?.id || addr), addr, web },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Share URL already on an overlay group (room.web or derived from addr). */
|
|
271
|
+
export function agentShareUrl(a) {
|
|
272
|
+
if (!a || typeof a !== 'object') return '';
|
|
273
|
+
return grokroomShareUrl(a.room?.addr || a.room?.address) || String(a.room?.web || '').trim();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Overlay share payload for a focused tray row + grokroom /state rooms.
|
|
278
|
+
* Grokroom groups have their own /r/<addr>. Local createGroup has no pubkey;
|
|
279
|
+
* the HUD then lists the public grokroom URLs.
|
|
280
|
+
*/
|
|
281
|
+
export function groupShareState({ agent, rooms } = {}) {
|
|
282
|
+
const url = agentShareUrl(agent);
|
|
283
|
+
const list = (Array.isArray(rooms) ? rooms : []).map((r) => {
|
|
284
|
+
const u = r && (r.web || grokroomShareUrl(r.addr || r.address));
|
|
285
|
+
if (!u) return null;
|
|
286
|
+
return { id: String(r.id || r.name || ''), name: String(r.name || r.id || ''), url: u };
|
|
287
|
+
}).filter(Boolean);
|
|
288
|
+
const isGroup = !!(agent && (agent.isGroup || agent.room));
|
|
289
|
+
return { url: url || '', isGroup, rooms: list };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const GROKROOM_B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
293
|
+
function grokroomB58(buf) {
|
|
294
|
+
const digits = [0];
|
|
295
|
+
for (const byte of buf) {
|
|
296
|
+
let carry = byte;
|
|
297
|
+
for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8; digits[i] = carry % 58; carry = (carry / 58) | 0; }
|
|
298
|
+
while (carry) { digits.push(carry % 58); carry = (carry / 58) | 0; }
|
|
299
|
+
}
|
|
300
|
+
let out = '';
|
|
301
|
+
for (const b of buf) { if (b === 0) out += '1'; else break; }
|
|
302
|
+
return out + digits.reverse().map((d) => GROKROOM_B58[d]).join('');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function onchainRoomsPath(home = os.homedir()) {
|
|
306
|
+
return path.join(home, '.openzoo', 'grokbot-onchain-rooms.json');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function readOnchainRooms(home = os.homedir()) {
|
|
310
|
+
try {
|
|
311
|
+
const j = JSON.parse(fs.readFileSync(onchainRoomsPath(home), 'utf8'));
|
|
312
|
+
return Array.isArray(j) ? j : [];
|
|
313
|
+
} catch { return []; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function writeOnchainRooms(list, home = os.homedir()) {
|
|
317
|
+
const p = onchainRoomsPath(home);
|
|
318
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
319
|
+
fs.writeFileSync(p, JSON.stringify(Array.isArray(list) ? list : [], null, 2));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Mint a Solana ed25519 pubkey for a Grok Bot groupchat. The paymaster
|
|
324
|
+
* indexes memos by that account; https://openzoo.fun/r/<addr> is the public page.
|
|
325
|
+
*/
|
|
326
|
+
export function mintOnchainRoom({ agentId, name, home = os.homedir() } = {}) {
|
|
327
|
+
const pair = generateKeyPairSync('ed25519');
|
|
328
|
+
const rawPub = pair.publicKey.export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
329
|
+
const addr = grokroomB58(Buffer.from(rawPub));
|
|
330
|
+
const rec = {
|
|
331
|
+
agentId: String(agentId || ''),
|
|
332
|
+
name: String(name || 'group'),
|
|
333
|
+
addr,
|
|
334
|
+
web: grokroomShareUrl(addr),
|
|
335
|
+
createdAt: Date.now(),
|
|
336
|
+
};
|
|
337
|
+
const all = readOnchainRooms(home).filter((r) => r && r.agentId !== rec.agentId);
|
|
338
|
+
all.push(rec);
|
|
339
|
+
writeOnchainRooms(all, home);
|
|
340
|
+
return rec;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Reuse a stored room for this agent, or mint one. */
|
|
344
|
+
export function ensureOnchainRoom(agent, home = os.homedir()) {
|
|
345
|
+
const addr = agent?.room?.addr || agent?.room?.address || '';
|
|
346
|
+
const web = grokroomShareUrl(addr);
|
|
347
|
+
if (web) return { agentId: String(agent.id || ''), name: String(agent.name || ''), addr, web };
|
|
348
|
+
const hit = readOnchainRooms(home).find((r) => r && r.agentId === agent?.id && grokroomShareUrl(r.addr));
|
|
349
|
+
if (hit) return { ...hit, web: grokroomShareUrl(hit.addr) };
|
|
350
|
+
return mintOnchainRoom({ agentId: agent?.id, name: agent?.name, home });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function attachOnchainRoom(agent, home = os.homedir()) {
|
|
354
|
+
if (!agent || typeof agent !== 'object') return agent;
|
|
355
|
+
if (!agent.isGroup && !(agent.room && agent.room.id)) return agent;
|
|
356
|
+
if (agent.hidden) return agent;
|
|
357
|
+
const rec = ensureOnchainRoom(agent, home);
|
|
358
|
+
const id = String(agent.room?.id || rec.agentId || agent.id || '');
|
|
359
|
+
return {
|
|
360
|
+
...agent,
|
|
361
|
+
room: { id, addr: rec.addr, web: rec.web },
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Hidden bubble-member id for a grokroom sender. One row, many rooms. */
|
|
366
|
+
export function grokroomMemberId(name) {
|
|
367
|
+
const slug = String(name || 'anon').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'anon';
|
|
368
|
+
return `roombot-${slug}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function isGrokRoomAgent(a) {
|
|
372
|
+
if (!a || typeof a !== 'object') return false;
|
|
373
|
+
if (a.room && a.room.id) return true;
|
|
374
|
+
return /^room-/.test(String(a.id || ''));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function isGrokRoomMember(a) {
|
|
378
|
+
if (!a || typeof a !== 'object') return false;
|
|
379
|
+
return a.hidden === true || a.hiddenFromSidebar === true || /^roombot-/.test(String(a.id || ''));
|
|
380
|
+
}
|
|
381
|
+
|
|
212
382
|
/** Standing job text. shapeAgent used to drop this, so every restart was amnesia. */
|
|
213
383
|
export function agentBrief(raw = {}) {
|
|
214
384
|
const a = raw && typeof raw === 'object' ? raw : {};
|
|
@@ -237,7 +407,17 @@ export function shapeAgent(raw = {}) {
|
|
|
237
407
|
: (Array.isArray(a.memberAgentIds) ? a.memberAgentIds.map((x) => String(x)).filter(Boolean) : []);
|
|
238
408
|
const isGroup = a.isGroup === true || memberIds.length > 0;
|
|
239
409
|
const name = displayName({ ...a, isGroup, memberIds });
|
|
240
|
-
const
|
|
410
|
+
const hidden = a.hidden === true || a.hiddenFromSidebar === true;
|
|
411
|
+
let room = null;
|
|
412
|
+
if (a.room && typeof a.room === 'object' && a.room.id) {
|
|
413
|
+
const addr = String(a.room.addr || a.room.address || '');
|
|
414
|
+
const web = grokroomShareUrl(addr) || String(a.room.web || '').trim();
|
|
415
|
+
room = { id: String(a.room.id), addr, ...(web ? { web } : {}) };
|
|
416
|
+
} else if (/^room-/.test(id)) {
|
|
417
|
+
room = { id: id.slice(5), addr: '' };
|
|
418
|
+
}
|
|
419
|
+
const skipJob = hidden || !!room || /^#\s/.test(name);
|
|
420
|
+
const brief = skipJob ? String(a.brief || '').slice(0, 8000) : agentBrief({ ...a, name, title: a.title || name });
|
|
241
421
|
return {
|
|
242
422
|
id,
|
|
243
423
|
name,
|
|
@@ -250,7 +430,8 @@ export function shapeAgent(raw = {}) {
|
|
|
250
430
|
updatedAt: Number(a.updatedAt) || Date.now(),
|
|
251
431
|
hasUnread: !!a.hasUnread,
|
|
252
432
|
unreadCount: Number(a.unreadCount) || 0,
|
|
253
|
-
hidden
|
|
433
|
+
hidden, // GROKROOM-HIDDEN-BOTS
|
|
434
|
+
hiddenFromSidebar: hidden,
|
|
254
435
|
notificationsEnabled: a.notificationsEnabled !== false,
|
|
255
436
|
notifyOnUpdatesEnabled: a.notifyOnUpdatesEnabled !== false,
|
|
256
437
|
isGroup,
|
|
@@ -261,6 +442,7 @@ export function shapeAgent(raw = {}) {
|
|
|
261
442
|
? a.awaitingUserResponse : null,
|
|
262
443
|
avatarShape: a.avatarShape ?? null,
|
|
263
444
|
avatarColor: a.avatarColor ?? null,
|
|
445
|
+
...(room ? { room } : {}),
|
|
264
446
|
};
|
|
265
447
|
}
|
|
266
448
|
|
|
@@ -365,7 +547,14 @@ export function addDeletedIds(home, ids) {
|
|
|
365
547
|
export function filterDeleted(agents, home) {
|
|
366
548
|
const del = readDeletedIds(home);
|
|
367
549
|
if (!del.size || !Array.isArray(agents)) return agents || [];
|
|
368
|
-
return agents.filter((a) =>
|
|
550
|
+
return agents.filter((a) => {
|
|
551
|
+
if (!a) return false;
|
|
552
|
+
const id = String(a.id || '');
|
|
553
|
+
// grokroom threads + bubble-members are gossip identities, not user bots.
|
|
554
|
+
// The old sidebar roombots were tombstoned; native rooms must still load.
|
|
555
|
+
if (/^(room-|roombot-)/.test(id)) return true;
|
|
556
|
+
return !del.has(id);
|
|
557
|
+
});
|
|
369
558
|
}
|
|
370
559
|
|
|
371
560
|
export function writeWakeups(home, map) {
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Fetch + install the official Grok Bot binary for this platform/arch.
|
|
2
|
+
//
|
|
3
|
+
// Source order:
|
|
4
|
+
// 1. GitHub release grokbot-v* on staccDOTsol/openzoo (our multiarch re-host)
|
|
5
|
+
// 2. Vendor feed api2.cursor.sh/updates/.../sand (same as scripts/release-grokbot.sh)
|
|
6
|
+
//
|
|
7
|
+
// A Finder-launched OpenZoo Bot.app has no npx and no Node on PATH. This is
|
|
8
|
+
// the grokui sidecar trick applied to the vendor Grok Bot: download the
|
|
9
|
+
// matching Grok_Bot_<ver>_<plat>-<arch> artifact, unpack it, then `openzoo bot`
|
|
10
|
+
// can spawn it. GROKBOT_NO_FETCH=1 skips the network (tests / airgap).
|
|
11
|
+
|
|
12
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
13
|
+
import {
|
|
14
|
+
chmodSync,
|
|
15
|
+
copyFileSync,
|
|
16
|
+
cpSync,
|
|
17
|
+
createWriteStream,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
} from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { Readable } from 'node:stream';
|
|
27
|
+
import { pipeline } from 'node:stream/promises';
|
|
28
|
+
|
|
29
|
+
export const GROKBOT_GITHUB_REPO = 'staccDOTsol/openzoo';
|
|
30
|
+
|
|
31
|
+
export const FEED_TARGETS = {
|
|
32
|
+
'darwin-arm64': { feed: 'darwin-arm64', plat: 'darwin-arm64', ext: 'zip' },
|
|
33
|
+
'darwin-x64': { feed: 'darwin-x64', plat: 'darwin-x64', ext: 'zip' },
|
|
34
|
+
'linux-arm64': { feed: 'linux-arm64', plat: 'linux-arm64', ext: 'AppImage' },
|
|
35
|
+
'linux-x64': { feed: 'linux-x64', plat: 'linux-x64', ext: 'AppImage' },
|
|
36
|
+
'win32-arm64': { feed: 'win32-arm64-user', plat: 'win-arm64', ext: 'exe' },
|
|
37
|
+
'win32-x64': { feed: 'win32-x64-user', plat: 'win-x64', ext: 'exe' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function machineArch(arch = process.arch) {
|
|
41
|
+
// nvm's default node on this Mac is x86_64 under Rosetta. Grok Bot is a
|
|
42
|
+
// native app — fetching darwin-x64 because process.arch is x64 would ship
|
|
43
|
+
// the Intel build onto Apple Silicon. hw.optional.arm64 is 1 on M-series
|
|
44
|
+
// even when the current process is translated.
|
|
45
|
+
if (process.platform === 'darwin') {
|
|
46
|
+
try {
|
|
47
|
+
const out = execFileSync('sysctl', ['-n', 'hw.optional.arm64'], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
timeout: 2000,
|
|
50
|
+
}).trim();
|
|
51
|
+
if (out === '1') return 'arm64';
|
|
52
|
+
} catch { /* Intel Mac, or sysctl blocked */ }
|
|
53
|
+
}
|
|
54
|
+
return arch === 'arm64' ? 'arm64' : 'x64';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hostKey(platform = process.platform, arch = machineArch()) {
|
|
58
|
+
const a = arch === 'arm64' ? 'arm64' : 'x64';
|
|
59
|
+
if (platform === 'linux') return `linux-${a}`;
|
|
60
|
+
if (platform === 'win32') return `win32-${a}`;
|
|
61
|
+
return `darwin-${a}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function githubAssetRegex(key) {
|
|
65
|
+
const t = FEED_TARGETS[key];
|
|
66
|
+
if (!t) return null;
|
|
67
|
+
const plat = t.plat.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
return new RegExp(`^Grok_Bot_.+_${plat}\\.${t.ext}$`, 'i');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function matchGithubAsset(assets, key) {
|
|
72
|
+
const re = githubAssetRegex(key);
|
|
73
|
+
if (!re || !Array.isArray(assets)) return null;
|
|
74
|
+
return assets.find((a) => a && re.test(String(a.name || ''))) || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function installDest(home = homedir(), platform = process.platform) {
|
|
78
|
+
if (platform === 'linux') return join(home, '.local', 'bin', 'Grok_Bot.AppImage');
|
|
79
|
+
if (platform === 'win32') {
|
|
80
|
+
return join(home, 'AppData', 'Local', 'Programs', 'Grok Bot', 'Grok Bot.exe');
|
|
81
|
+
}
|
|
82
|
+
return join(home, 'Applications', 'Grok Bot.app');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function installedBinary(home = homedir(), platform = process.platform) {
|
|
86
|
+
const dest = installDest(home, platform);
|
|
87
|
+
if (platform === 'darwin') return join(dest, 'Contents', 'MacOS', 'Grok Bot');
|
|
88
|
+
return dest;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function distDir(home = homedir()) {
|
|
92
|
+
return join(home, '.openzoo', 'grokbot-dist');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function findAppBundle(dir, depth = 0) {
|
|
96
|
+
if (!dir || !existsSync(dir) || depth > 4) return null;
|
|
97
|
+
let entries;
|
|
98
|
+
try { entries = readdirSync(dir); } catch { return null; }
|
|
99
|
+
for (const name of entries) {
|
|
100
|
+
const p = join(dir, name);
|
|
101
|
+
if (/\.app$/i.test(name) && existsSync(join(p, 'Contents', 'MacOS'))) return p;
|
|
102
|
+
try {
|
|
103
|
+
if (statSync(p).isDirectory() && name !== 'Contents' && name !== 'node_modules') {
|
|
104
|
+
const hit = findAppBundle(p, depth + 1);
|
|
105
|
+
if (hit) return hit;
|
|
106
|
+
}
|
|
107
|
+
} catch { /* skip */ }
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function uaHeaders() {
|
|
113
|
+
return { 'User-Agent': 'openzoo-bot', Accept: 'application/vnd.github+json' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function githubGrokbotRelease({ fetchImpl = fetch, repo = GROKBOT_GITHUB_REPO } = {}) {
|
|
117
|
+
const r = await fetchImpl(`https://api.github.com/repos/${repo}/releases?per_page=40`, {
|
|
118
|
+
headers: uaHeaders(),
|
|
119
|
+
signal: AbortSignal.timeout(20000),
|
|
120
|
+
});
|
|
121
|
+
if (!r.ok) throw new Error(`github releases ${r.status}`);
|
|
122
|
+
const list = await r.json();
|
|
123
|
+
if (!Array.isArray(list)) throw new Error('github releases: not a list');
|
|
124
|
+
const hit = list.find((x) => x && typeof x.tag_name === 'string' && /^grokbot-v/.test(x.tag_name));
|
|
125
|
+
if (!hit) throw new Error('no grokbot-v* GitHub release');
|
|
126
|
+
return hit;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function vendorFeedAsset(key, { fetchImpl = fetch } = {}) {
|
|
130
|
+
const t = FEED_TARGETS[key];
|
|
131
|
+
if (!t) throw new Error(`unknown host key ${key}`);
|
|
132
|
+
const machine = process.env.GROKBOT_MACHINE_ID || '11111111-2222-3333-4444-555555555555';
|
|
133
|
+
const base = process.env.GROKBOT_BASE_VERSION || '0.30.0';
|
|
134
|
+
const url = `https://api2.cursor.sh/updates/api/update/${t.feed}/sand/${base}/${machine}/stable`;
|
|
135
|
+
const r = await fetchImpl(url, { signal: AbortSignal.timeout(20000) });
|
|
136
|
+
if (!r.ok) throw new Error(`vendor feed ${r.status}`);
|
|
137
|
+
const j = await r.json();
|
|
138
|
+
const version = j.version || j.name || j.productVersion;
|
|
139
|
+
let fileUrl = j.url;
|
|
140
|
+
if (typeof fileUrl === 'string' && /\/linux\//.test(fileUrl) && version) {
|
|
141
|
+
fileUrl = `${fileUrl.replace(/\/[^/]+$/, '')}/Grok_Bot_${version}.AppImage`;
|
|
142
|
+
}
|
|
143
|
+
if (!version || !fileUrl) throw new Error(`vendor feed missing url/version for ${key}`);
|
|
144
|
+
return { version: String(version), url: fileUrl, name: `Grok_Bot_${version}_${t.plat}.${t.ext}`, source: 'vendor' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function resolveGrokBotAsset(key, { fetchImpl = fetch, repo = GROKBOT_GITHUB_REPO } = {}) {
|
|
148
|
+
if (!FEED_TARGETS[key]) throw new Error(`unknown host key ${key}`);
|
|
149
|
+
try {
|
|
150
|
+
const rel = await githubGrokbotRelease({ fetchImpl, repo });
|
|
151
|
+
const asset = matchGithubAsset(rel.assets || [], key);
|
|
152
|
+
if (asset && asset.browser_download_url) {
|
|
153
|
+
const version = String(rel.tag_name || '').replace(/^grokbot-v/, '') || 'unknown';
|
|
154
|
+
return {
|
|
155
|
+
version,
|
|
156
|
+
url: asset.browser_download_url,
|
|
157
|
+
name: asset.name,
|
|
158
|
+
source: 'github',
|
|
159
|
+
tag: rel.tag_name,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
} catch { /* fall through to vendor */ }
|
|
163
|
+
return vendorFeedAsset(key, { fetchImpl });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function downloadTo(url, dest, { fetchImpl = fetch, log = () => {} } = {}) {
|
|
167
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
168
|
+
const r = await fetchImpl(url, {
|
|
169
|
+
headers: uaHeaders(),
|
|
170
|
+
redirect: 'follow',
|
|
171
|
+
signal: AbortSignal.timeout(15 * 60 * 1000),
|
|
172
|
+
});
|
|
173
|
+
if (!r.ok) throw new Error(`download ${r.status} ${url}`);
|
|
174
|
+
const total = Number(r.headers.get('content-length') || 0);
|
|
175
|
+
log(`openzoo: downloading Grok Bot${total ? ` (${Math.round(total / 1024 / 1024)} MB)` : ''}…`);
|
|
176
|
+
if (!r.body) throw new Error('download: empty body');
|
|
177
|
+
await pipeline(Readable.fromWeb(r.body), createWriteStream(dest));
|
|
178
|
+
const sz = statSync(dest).size;
|
|
179
|
+
if (sz < 50 * 1024 * 1024) throw new Error(`download too small (${sz} bytes) — ${url}`);
|
|
180
|
+
log(`openzoo: downloaded ${Math.round(sz / 1024 / 1024)} MB -> ${dest}`);
|
|
181
|
+
return dest;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function run(cmd, args, opts = {}) {
|
|
185
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: opts.timeout || 120000, stdio: opts.stdio || 'pipe' });
|
|
186
|
+
if (r.error) throw r.error;
|
|
187
|
+
if (r.status !== 0) {
|
|
188
|
+
throw new Error(`${cmd} ${args.join(' ')} failed (${r.status}): ${(r.stderr || r.stdout || '').slice(0, 400)}`);
|
|
189
|
+
}
|
|
190
|
+
return r;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function installGrokBotArtifact({ archivePath, key, home = homedir(), log = () => {} }) {
|
|
194
|
+
const platform = key.startsWith('linux') ? 'linux' : key.startsWith('win32') ? 'win32' : 'darwin';
|
|
195
|
+
const dest = installDest(home, platform);
|
|
196
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
197
|
+
|
|
198
|
+
if (platform === 'darwin') {
|
|
199
|
+
const tmp = join(distDir(home), 'extract');
|
|
200
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
201
|
+
mkdirSync(tmp, { recursive: true });
|
|
202
|
+
run('unzip', ['-o', archivePath, '-d', tmp]);
|
|
203
|
+
const app = findAppBundle(tmp);
|
|
204
|
+
if (!app) throw new Error(`zip has no Grok Bot.app: ${archivePath}`);
|
|
205
|
+
rmSync(dest, { recursive: true, force: true });
|
|
206
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
207
|
+
cpSync(app, dest, { recursive: true });
|
|
208
|
+
const bin = join(dest, 'Contents', 'MacOS', 'Grok Bot');
|
|
209
|
+
if (!existsSync(bin)) throw new Error(`installed app missing binary: ${bin}`);
|
|
210
|
+
log(`openzoo: installed Grok Bot.app -> ${dest}`);
|
|
211
|
+
return bin;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (platform === 'linux') {
|
|
215
|
+
copyFileSync(archivePath, dest);
|
|
216
|
+
chmodSync(dest, 0o755);
|
|
217
|
+
log(`openzoo: installed AppImage -> ${dest}`);
|
|
218
|
+
return dest;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// NSIS user installer (vendor + our re-host). /S is silent; the default
|
|
222
|
+
// dest is %LOCALAPPDATA%\Programs\Grok Bot\Grok Bot.exe.
|
|
223
|
+
log('openzoo: running Grok Bot installer (silent)');
|
|
224
|
+
run(archivePath, ['/S'], { timeout: 300000, stdio: 'ignore' });
|
|
225
|
+
const deadline = Date.now() + 60000;
|
|
226
|
+
while (Date.now() < deadline) {
|
|
227
|
+
if (existsSync(dest)) return dest;
|
|
228
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
|
|
229
|
+
}
|
|
230
|
+
if (!existsSync(dest)) {
|
|
231
|
+
throw new Error(`installer finished but ${dest} is missing — run ${archivePath} yourself`);
|
|
232
|
+
}
|
|
233
|
+
return dest;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function ensureGrokBot({
|
|
237
|
+
existing,
|
|
238
|
+
platform = process.platform,
|
|
239
|
+
arch = machineArch(),
|
|
240
|
+
home = homedir(),
|
|
241
|
+
fetchImpl = fetch,
|
|
242
|
+
log = (m) => console.error(m),
|
|
243
|
+
} = {}) {
|
|
244
|
+
if (existing && existsSync(existing)) return existing;
|
|
245
|
+
if (process.env.GROKBOT_NO_FETCH === '1') {
|
|
246
|
+
throw new Error('Grok Bot missing and GROKBOT_NO_FETCH=1');
|
|
247
|
+
}
|
|
248
|
+
const key = hostKey(platform, arch);
|
|
249
|
+
const asset = await resolveGrokBotAsset(key, { fetchImpl });
|
|
250
|
+
log(`openzoo: Grok Bot ${asset.version} from ${asset.source} (${asset.name})`);
|
|
251
|
+
const destFile = join(distDir(home), asset.name);
|
|
252
|
+
if (!existsSync(destFile) || statSync(destFile).size < 50 * 1024 * 1024) {
|
|
253
|
+
await downloadTo(asset.url, destFile, { fetchImpl, log });
|
|
254
|
+
} else {
|
|
255
|
+
log(`openzoo: using cached ${destFile}`);
|
|
256
|
+
}
|
|
257
|
+
return installGrokBotArtifact({ archivePath: destFile, key, home, log });
|
|
258
|
+
}
|
package/lib/grokbotweb.js
CHANGED
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
import { exec } from 'node:child_process';
|
|
17
17
|
|
|
18
18
|
import { config } from './config.js';
|
|
19
|
-
import { readHouseRoster } from './grokbotAccount.js';
|
|
19
|
+
import { readHouseRoster, groupShareState } from './grokbotAccount.js';
|
|
20
20
|
import { ingestUpload, lookupUpload } from './grokbotUploads.js';
|
|
21
21
|
import { spendChipSource, sessionSpendState } from './ozSpendChip.js';
|
|
22
22
|
|
|
@@ -215,8 +215,8 @@ const MIME = {
|
|
|
215
215
|
};
|
|
216
216
|
|
|
217
217
|
export const HOST_STATUS = {
|
|
218
|
-
hostVersion: '0.
|
|
219
|
-
latestHostVersion: '0.
|
|
218
|
+
hostVersion: '0.36.0',
|
|
219
|
+
latestHostVersion: '0.36.0',
|
|
220
220
|
hostUpdateAvailable: false,
|
|
221
221
|
isBusy: false,
|
|
222
222
|
capabilities: [],
|
|
@@ -233,9 +233,10 @@ export const COORD_DEFAULTS = {
|
|
|
233
233
|
promptAcceptanceStatus: { outcome: 'found', record: { status: 'accepted' } },
|
|
234
234
|
getAgentTranscriptTail: { entries: [] },
|
|
235
235
|
openAgentTail: { entries: [] },
|
|
236
|
-
getForeverBoxStatus: { agentId: 'openzoo', state: '
|
|
237
|
-
ensureForeverBox: { agentId: 'openzoo', state: '
|
|
236
|
+
getForeverBoxStatus: { agentId: 'openzoo', state: 'running' },
|
|
237
|
+
ensureForeverBox: { agentId: 'openzoo', state: 'running' },
|
|
238
238
|
getBoxSecretsStatus: { keys: [], isApplied: false, lastAppliedAtMs: null },
|
|
239
|
+
resolveAgentCreation: { kind: 'box' },
|
|
239
240
|
isGlobalSearchEnabled: false,
|
|
240
241
|
isEgressTunnelAvailable: false,
|
|
241
242
|
getSharingState: { isEnabled: false, selfAuthId: null, pendingJoinRequests: [], rooms: [], typingUsers: [] },
|
|
@@ -679,7 +680,7 @@ async function ensureHijack(log) {
|
|
|
679
680
|
const { startCursorBackend } = await import('./cursorbackend.js');
|
|
680
681
|
const models = ['gpt-4o', 'gpt-4o-mini'].map((n) => ({ name: n, label: n }));
|
|
681
682
|
try {
|
|
682
|
-
startCursorBackend({
|
|
683
|
+
await startCursorBackend({
|
|
683
684
|
port: 8443,
|
|
684
685
|
models,
|
|
685
686
|
log: (m) => log(` backend: ${m}`),
|
|
@@ -761,7 +762,7 @@ export async function startGrokBotWeb(opts = {}) {
|
|
|
761
762
|
res.writeHead(status, out);
|
|
762
763
|
}
|
|
763
764
|
|
|
764
|
-
const server = http.createServer((req, res) => {
|
|
765
|
+
const server = http.createServer(async (req, res) => {
|
|
765
766
|
const urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
766
767
|
if (urlPath === '/oz-spend') {
|
|
767
768
|
writeHead(res, 200, {
|
|
@@ -772,6 +773,21 @@ export async function startGrokBotWeb(opts = {}) {
|
|
|
772
773
|
res.end(JSON.stringify({ ok: true, ...sessionSpendState() }));
|
|
773
774
|
return;
|
|
774
775
|
}
|
|
776
|
+
if (urlPath === '/oz-share') {
|
|
777
|
+
writeHead(res, 200, {
|
|
778
|
+
'content-type': 'application/json',
|
|
779
|
+
'cache-control': 'no-store',
|
|
780
|
+
'access-control-allow-origin': '*',
|
|
781
|
+
}, req);
|
|
782
|
+
let rooms = [];
|
|
783
|
+
try {
|
|
784
|
+
const st = await fetch('http://127.0.0.1:4799/state', { signal: AbortSignal.timeout(1500) }).then((r) => r.json());
|
|
785
|
+
rooms = Array.isArray(st.rooms) ? st.rooms : [];
|
|
786
|
+
} catch { /* grokroom down */ }
|
|
787
|
+
const share = groupShareState({ agent: { isGroup: rooms.length > 0 }, rooms });
|
|
788
|
+
res.end(JSON.stringify({ ok: true, ...share }));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
775
791
|
if (urlPath === '/oz-health') {
|
|
776
792
|
writeHead(res, 200, { 'content-type': 'application/json' }, req);
|
|
777
793
|
res.end(JSON.stringify({
|