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.
@@ -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 brief = agentBrief({ ...a, name, title: a.title || name });
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: a.hidden === true || a.hiddenFromSidebar === true, // GROKROOM-HIDDEN-BOTS
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) => a && !del.has(String(a.id)));
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.30.0',
219
- latestHostVersion: '0.30.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: 'ready' },
237
- ensureForeverBox: { agentId: 'openzoo', state: 'ready' },
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({