openzoo 0.50.96 → 0.50.98
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 +35 -2
- package/lib/cursorapi.js +65 -0
- package/lib/cursorbackend.js +817 -78
- package/lib/cursorbackend.js.bak-hiddenbots +4072 -0
- package/lib/grokbotAccount.js +195 -4
- package/lib/grokbotAccount.js.bak-hiddenbots +386 -0
- package/lib/grokbotFetch.js +258 -0
- package/lib/grokbotweb.js +23 -7
- package/lib/grokcli.js +527 -35
- package/lib/grokui.mjs +6524 -0
- package/lib/launch.js +37 -6
- package/lib/modelroute/README.md +1 -0
- package/lib/modelroute/catalog.json +1 -0
- package/lib/modelroute/outcomes.json +1566 -0
- package/lib/modelroute/router.json +1 -0
- package/lib/modelroute.js +737 -0
- package/lib/models.js +40 -0
- package/lib/openzooPathShim.js +60 -0
- package/lib/ozSpendChip.js +80 -3
- package/lib/pay.js +30 -2
- package/lib/podagent.mjs +1427 -0
- package/lib/proxy.js +89 -4
- package/lib/runguard.js +31 -0
- package/lib/setup.js +1 -1
- package/lib/spill.js +2031 -0
- package/lib/subscription.js +207 -0
- package/lib/websearch.js +31 -0
- package/lib/worktree.mjs +424 -0
- package/package.json +1 -1
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,6 +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,
|
|
433
|
+
hidden, // GROKROOM-HIDDEN-BOTS
|
|
434
|
+
hiddenFromSidebar: hidden,
|
|
253
435
|
notificationsEnabled: a.notificationsEnabled !== false,
|
|
254
436
|
notifyOnUpdatesEnabled: a.notifyOnUpdatesEnabled !== false,
|
|
255
437
|
isGroup,
|
|
@@ -260,6 +442,7 @@ export function shapeAgent(raw = {}) {
|
|
|
260
442
|
? a.awaitingUserResponse : null,
|
|
261
443
|
avatarShape: a.avatarShape ?? null,
|
|
262
444
|
avatarColor: a.avatarColor ?? null,
|
|
445
|
+
...(room ? { room } : {}),
|
|
263
446
|
};
|
|
264
447
|
}
|
|
265
448
|
|
|
@@ -267,8 +450,9 @@ export function shapeAgent(raw = {}) {
|
|
|
267
450
|
* Full sidebar roster for SSE `agents` / listAgents. Sorts by activity and
|
|
268
451
|
* stamps unread -- does not slice. The old 80-cap hid agents past the tray.
|
|
269
452
|
*/
|
|
270
|
-
export function rosterForEvent(list, activity) {
|
|
271
|
-
|
|
453
|
+
export function rosterForEvent(list, activity, opts) {
|
|
454
|
+
// GROKROOM-HIDDEN-BOTS: client-facing rosters never include hidden bots; persistence passes { includeHidden: true }
|
|
455
|
+
const arr = (Array.isArray(list) ? [...list] : []).filter((a) => (opts && opts.includeHidden) || !a?.hidden);
|
|
272
456
|
arr.sort((x, y) => activityTs(y, activity) - activityTs(x, activity));
|
|
273
457
|
return arr.map((agent) => {
|
|
274
458
|
const rec = activityRec(agent, activity) || {};
|
|
@@ -363,7 +547,14 @@ export function addDeletedIds(home, ids) {
|
|
|
363
547
|
export function filterDeleted(agents, home) {
|
|
364
548
|
const del = readDeletedIds(home);
|
|
365
549
|
if (!del.size || !Array.isArray(agents)) return agents || [];
|
|
366
|
-
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
|
+
});
|
|
367
558
|
}
|
|
368
559
|
|
|
369
560
|
export function writeWakeups(home, map) {
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Bot tray/roster.
|
|
3
|
+
*
|
|
4
|
+
* Electron hijack: a second Cursor login on the same Mac must not inherit the
|
|
5
|
+
* previous account's 1340 / tray (rosterForAccount still isolates that).
|
|
6
|
+
*
|
|
7
|
+
* Cafe/web: every visitor is a cookie identity on ONE house. They share the
|
|
8
|
+
* operator tray at ~/.openzoo/grokbot-agents.json plus any account agents.json
|
|
9
|
+
* under ~/.openzoo/grokbot/<id>/ -- not per-browser localStorage, not empty
|
|
10
|
+
* just because no Cursor account has logged in.
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export function accountSlug(accountId) {
|
|
16
|
+
const s = String(accountId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
|
|
17
|
+
return s || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function accountDir(home, accountId) {
|
|
21
|
+
const slug = accountSlug(accountId);
|
|
22
|
+
if (!slug) return null;
|
|
23
|
+
return path.join(home, '.openzoo', 'grokbot', slug);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function accountPodPath(home, accountId) {
|
|
27
|
+
const dir = accountDir(home, accountId);
|
|
28
|
+
return dir ? path.join(dir, 'pod.json') : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function accountAgentsPath(home, accountId) {
|
|
32
|
+
const dir = accountDir(home, accountId);
|
|
33
|
+
return dir ? path.join(dir, 'agents.json') : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function houseAgentsPath(home) {
|
|
37
|
+
return path.join(home, '.openzoo', 'grokbot-agents.json');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJsonFile(p) {
|
|
41
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** First-seen id wins. A later pile can only *upgrade* a UUID name/stub brief. */
|
|
45
|
+
export function mergeAgentRecords(piles) {
|
|
46
|
+
const seen = new Map();
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const pile of piles) {
|
|
49
|
+
if (!Array.isArray(pile)) continue;
|
|
50
|
+
for (const a of pile) {
|
|
51
|
+
if (!a?.id) continue;
|
|
52
|
+
const idx = seen.get(a.id);
|
|
53
|
+
if (idx == null) {
|
|
54
|
+
seen.set(a.id, out.length);
|
|
55
|
+
out.push(a);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
out[idx] = preferNamedAgent(out[idx], a);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Shared house tray from disk. Cafe visitors have no Cursor account — they
|
|
66
|
+
* still get this list. A live account's file is preferred, never required.
|
|
67
|
+
*/
|
|
68
|
+
export function readHouseRoster(home, liveAccountId) {
|
|
69
|
+
const piles = [];
|
|
70
|
+
const livePath = liveAccountId ? accountAgentsPath(home, liveAccountId) : null;
|
|
71
|
+
if (livePath) {
|
|
72
|
+
const scoped = readJsonFile(livePath);
|
|
73
|
+
if (Array.isArray(scoped)) piles.push(scoped);
|
|
74
|
+
}
|
|
75
|
+
const global = readJsonFile(houseAgentsPath(home));
|
|
76
|
+
if (Array.isArray(global)) piles.push(global);
|
|
77
|
+
try {
|
|
78
|
+
const dir = path.join(home, '.openzoo', 'grokbot');
|
|
79
|
+
for (const name of fs.readdirSync(dir)) {
|
|
80
|
+
const p = path.join(dir, name, 'agents.json');
|
|
81
|
+
if (livePath && p === livePath) continue;
|
|
82
|
+
const a = readJsonFile(p);
|
|
83
|
+
if (Array.isArray(a) && a.length) piles.push(a);
|
|
84
|
+
}
|
|
85
|
+
} catch { /* no grokbot dir yet */ }
|
|
86
|
+
return mergeAgentRecords(piles);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Isolate two Cursor logins. Cafe (no live account) gets the house fallback.
|
|
91
|
+
* `fallback` is the merged disk roster; never return [] just because oauth
|
|
92
|
+
* hasn't run — that emptied the public site's sidebar.
|
|
93
|
+
*/
|
|
94
|
+
export function rosterForAccount({ liveAccountId, cachedAccountId, cached, fallback }) {
|
|
95
|
+
if (liveAccountId && cachedAccountId && liveAccountId !== cachedAccountId) return [];
|
|
96
|
+
if (liveAccountId && cachedAccountId && liveAccountId === cachedAccountId) {
|
|
97
|
+
return Array.isArray(cached) ? cached : [];
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(fallback) && fallback.length) return fallback;
|
|
100
|
+
return Array.isArray(cached) ? cached : [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function activityRec(agent, activity) {
|
|
104
|
+
if (!activity || !agent?.id) return null;
|
|
105
|
+
if (typeof activity.get === 'function') return activity.get(agent.id) || null;
|
|
106
|
+
return activity[agent.id] || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function activityTs(agent, activity) {
|
|
110
|
+
const rec = activityRec(agent, activity);
|
|
111
|
+
return (rec && rec.updatedAt) || agent.updatedAt || agent.createdAt || 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Grok Bot client persistence (Xkn) DROPS any row missing these booleans /
|
|
116
|
+
* nulls, then clears the whole persisted tray. That is how a group vanished
|
|
117
|
+
* after the first send: bumpAgent wrote a partial row, restore returned null.
|
|
118
|
+
*/
|
|
119
|
+
const AGENT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
120
|
+
|
|
121
|
+
/** bumpAgent used `{id, name:id}` and 1340 listAgents often echoes that — sidebar then paints UUIDs. */
|
|
122
|
+
export function looksLikeAgentId(s, id) {
|
|
123
|
+
const n = String(s || '').trim();
|
|
124
|
+
if (!n) return true;
|
|
125
|
+
if (id && n === String(id)) return true;
|
|
126
|
+
return AGENT_UUID_RE.test(n);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isStubBrief(brief, id) {
|
|
130
|
+
const b = String(brief || '').trim();
|
|
131
|
+
if (!b) return true;
|
|
132
|
+
if (id && b.startsWith(`You are ${id}. Your job is ${id}`)) return true;
|
|
133
|
+
return /^You are [0-9a-f-]{36}\. Your job is [0-9a-f-]{36}/i.test(b);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Pull a human name out of a standing brief: "You are 6 · Content Studio…" / "Job: Product Simplification". */
|
|
137
|
+
export function nameFromBrief(brief, id) {
|
|
138
|
+
const b = String(brief || '').replace(/^\[brief\]\s*/i, '').trim();
|
|
139
|
+
if (!b) return '';
|
|
140
|
+
const job = b.match(/\bJob:\s*([^\n.]+)/i);
|
|
141
|
+
if (job) {
|
|
142
|
+
const n = job[1].trim();
|
|
143
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
144
|
+
}
|
|
145
|
+
const numbered = b.match(/^You are\s+(\d+\s*[·.•.\-—–]+\s*[^\n.]+)/i);
|
|
146
|
+
if (numbered) {
|
|
147
|
+
const n = numbered[1].replace(/\s+for\s+.*$/i, '').trim();
|
|
148
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
149
|
+
}
|
|
150
|
+
const you = b.match(/^You are\s+(.+?)(?:\.\s|$)/i);
|
|
151
|
+
if (you) {
|
|
152
|
+
const n = you[1].replace(/\s+for\s+Stacc(?:'s)?(?:\s+LLC)?$/i, '').trim();
|
|
153
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function nameQuality(a) {
|
|
159
|
+
const n = String(a?.name || a?.title || '').trim();
|
|
160
|
+
if (!n || looksLikeAgentId(n, a?.id)) return 0;
|
|
161
|
+
if (/^(chat|group)$/i.test(n)) return 1;
|
|
162
|
+
if (/^new bot$/i.test(n)) return 2;
|
|
163
|
+
return 3;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Keep the record that still has a human name / real brief. */
|
|
167
|
+
export function preferNamedAgent(keep, incoming) {
|
|
168
|
+
if (!keep) return incoming;
|
|
169
|
+
if (!incoming) return keep;
|
|
170
|
+
const kq = nameQuality(keep);
|
|
171
|
+
const iq = nameQuality(incoming);
|
|
172
|
+
const keepBrief = String(keep.brief || keep.instructions || '').trim();
|
|
173
|
+
const inBrief = String(incoming.brief || incoming.instructions || '').trim();
|
|
174
|
+
const keepStub = isStubBrief(keepBrief, keep.id);
|
|
175
|
+
const inStub = isStubBrief(inBrief, incoming.id);
|
|
176
|
+
if (iq <= kq && !(keepStub && !inStub)) return keep;
|
|
177
|
+
const nameSrc = iq > kq ? incoming : keep;
|
|
178
|
+
const briefSrc = (!inStub && keepStub) ? incoming : keep;
|
|
179
|
+
return {
|
|
180
|
+
...keep,
|
|
181
|
+
...incoming,
|
|
182
|
+
name: nameSrc.name || nameSrc.title,
|
|
183
|
+
title: nameSrc.title || nameSrc.name,
|
|
184
|
+
brief: briefSrc.brief || briefSrc.instructions || keepBrief || inBrief,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function displayName(raw = {}) {
|
|
189
|
+
const a = raw && typeof raw === 'object' ? raw : {};
|
|
190
|
+
const id = String(a.id || '');
|
|
191
|
+
const isGroup = a.isGroup === true
|
|
192
|
+
|| (Array.isArray(a.memberIds) && a.memberIds.length > 0)
|
|
193
|
+
|| (Array.isArray(a.memberAgentIds) && a.memberAgentIds.length > 0);
|
|
194
|
+
for (const cand of [a.name, a.title]) {
|
|
195
|
+
const n = String(cand || '').trim();
|
|
196
|
+
if (n && !looksLikeAgentId(n, id)) return n.slice(0, 80);
|
|
197
|
+
}
|
|
198
|
+
const fromBrief = nameFromBrief(a.brief || a.instructions || a.description, id);
|
|
199
|
+
if (fromBrief) return fromBrief;
|
|
200
|
+
return isGroup ? 'group' : 'chat';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Standing job from a sidebar name like "6 · Content Studio" or "Bot 1 — Marketing (X)". */
|
|
204
|
+
export function briefFromName(name) {
|
|
205
|
+
const n = String(name || '').trim();
|
|
206
|
+
if (!n || /^(new bot|chat|group)$/i.test(n) || looksLikeAgentId(n)) return '';
|
|
207
|
+
const role = n.replace(/^\d+\s*[·.•.\-—–]+\s*/, '').trim() || n;
|
|
208
|
+
if (role.length < 2) return '';
|
|
209
|
+
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
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Standing job text. shapeAgent used to drop this, so every restart was amnesia. */
|
|
213
|
+
export function agentBrief(raw = {}) {
|
|
214
|
+
const a = raw && typeof raw === 'object' ? raw : {};
|
|
215
|
+
const id = String(a.id || '');
|
|
216
|
+
for (const k of ['brief', 'instructions', 'customInstructions', 'systemPrompt']) {
|
|
217
|
+
const v = a[k];
|
|
218
|
+
if (typeof v === 'string' && v.trim() && !isStubBrief(v, id)) {
|
|
219
|
+
const name = displayName(a);
|
|
220
|
+
const text = v.trim();
|
|
221
|
+
if (id && name && !looksLikeAgentId(name, id) && text.startsWith(`You are ${id}`)) {
|
|
222
|
+
return (`You are ${name}` + text.slice(`You are ${id}`.length)).slice(0, 8000);
|
|
223
|
+
}
|
|
224
|
+
return text.slice(0, 8000);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const d = String(a.description || '').trim();
|
|
228
|
+
if (d && !isStubBrief(d, id)) return d.slice(0, 8000);
|
|
229
|
+
return briefFromName(displayName(a)).slice(0, 8000);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function shapeAgent(raw = {}) {
|
|
233
|
+
const a = raw && typeof raw === 'object' ? raw : {};
|
|
234
|
+
const id = String(a.id || '');
|
|
235
|
+
const memberIds = Array.isArray(a.memberIds)
|
|
236
|
+
? a.memberIds.map((x) => String(x)).filter(Boolean)
|
|
237
|
+
: (Array.isArray(a.memberAgentIds) ? a.memberAgentIds.map((x) => String(x)).filter(Boolean) : []);
|
|
238
|
+
const isGroup = a.isGroup === true || memberIds.length > 0;
|
|
239
|
+
const name = displayName({ ...a, isGroup, memberIds });
|
|
240
|
+
const brief = agentBrief({ ...a, name, title: a.title || name });
|
|
241
|
+
return {
|
|
242
|
+
id,
|
|
243
|
+
name,
|
|
244
|
+
brief,
|
|
245
|
+
description: String((a.description && !looksLikeAgentId(a.description, id) && a.description) || brief || ''),
|
|
246
|
+
title: String((!looksLikeAgentId(a.title, id) && a.title) || name),
|
|
247
|
+
origin: String(a.origin || 'user'),
|
|
248
|
+
path: String(a.path || (id ? `/local/${id}` : '/local')),
|
|
249
|
+
createdAt: Number(a.createdAt) || Date.now(),
|
|
250
|
+
updatedAt: Number(a.updatedAt) || Date.now(),
|
|
251
|
+
hasUnread: !!a.hasUnread,
|
|
252
|
+
unreadCount: Number(a.unreadCount) || 0,
|
|
253
|
+
notificationsEnabled: a.notificationsEnabled !== false,
|
|
254
|
+
notifyOnUpdatesEnabled: a.notifyOnUpdatesEnabled !== false,
|
|
255
|
+
isGroup,
|
|
256
|
+
memberIds: isGroup ? memberIds : [],
|
|
257
|
+
lastMessageId: a.lastMessageId == null ? null : String(a.lastMessageId),
|
|
258
|
+
lastEntry: a.lastEntry && typeof a.lastEntry === 'object' ? a.lastEntry : null,
|
|
259
|
+
awaitingUserResponse: a.awaitingUserResponse && typeof a.awaitingUserResponse === 'object'
|
|
260
|
+
? a.awaitingUserResponse : null,
|
|
261
|
+
avatarShape: a.avatarShape ?? null,
|
|
262
|
+
avatarColor: a.avatarColor ?? null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Full sidebar roster for SSE `agents` / listAgents. Sorts by activity and
|
|
268
|
+
* stamps unread -- does not slice. The old 80-cap hid agents past the tray.
|
|
269
|
+
*/
|
|
270
|
+
export function rosterForEvent(list, activity) {
|
|
271
|
+
const arr = Array.isArray(list) ? [...list] : [];
|
|
272
|
+
arr.sort((x, y) => activityTs(y, activity) - activityTs(x, activity));
|
|
273
|
+
return arr.map((agent) => {
|
|
274
|
+
const rec = activityRec(agent, activity) || {};
|
|
275
|
+
return shapeAgent({
|
|
276
|
+
...agent,
|
|
277
|
+
updatedAt: rec.updatedAt || agent.updatedAt || agent.createdAt || 0,
|
|
278
|
+
hasUnread: !!rec.hasUnread,
|
|
279
|
+
unreadCount: rec.unreadCount || 0,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const WAKEUP_MIN_SEC = 60;
|
|
285
|
+
export const WAKEUP_MAX_SEC = 6 * 3600;
|
|
286
|
+
export const WAKEUP_DEFAULT_SEC = 5 * 60;
|
|
287
|
+
export const DEFAULT_WAKEUP_PROMPT = 'Wakeup. Continue your brief. Do the next real action (write a file, click, or message a worker). Do not only re-read sitrep. Do not spawn more bots. Do not exec sysctl/uptime.';
|
|
288
|
+
|
|
289
|
+
export function wakeupsPath(home) {
|
|
290
|
+
return path.join(home, '.openzoo', 'grokbot-wakeups.json');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** "5m" / "1h" / "90" / "30s". Floor 60s so a never-stop cron cannot storm. */
|
|
294
|
+
export function parseWakeupEvery(raw) {
|
|
295
|
+
const s = String(raw ?? '').trim().toLowerCase();
|
|
296
|
+
if (!s) return WAKEUP_DEFAULT_SEC;
|
|
297
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*(s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?)?$/);
|
|
298
|
+
if (!m) return WAKEUP_DEFAULT_SEC;
|
|
299
|
+
const n = Number(m[1]);
|
|
300
|
+
if (!Number.isFinite(n) || n <= 0) return WAKEUP_DEFAULT_SEC;
|
|
301
|
+
const u = m[2] || 's';
|
|
302
|
+
let sec = n;
|
|
303
|
+
if (/^m/.test(u)) sec = n * 60;
|
|
304
|
+
else if (/^h/.test(u)) sec = n * 3600;
|
|
305
|
+
return Math.min(WAKEUP_MAX_SEC, Math.max(WAKEUP_MIN_SEC, Math.round(sec)));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function wantsWakeupCron(prompt) {
|
|
309
|
+
const s = String(prompt || '');
|
|
310
|
+
if (/\bcron\b.{0,48}\bwakeups?\b/i.test(s)) return true;
|
|
311
|
+
if (/\bwakeups?\b.{0,48}\bcron\b/i.test(s)) return true;
|
|
312
|
+
if (/\bnever stop\b/i.test(s)) return true;
|
|
313
|
+
if (/\bschedule\b.{0,24}\bwakeups?\b/i.test(s)) return true;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function shapeWakeup(agentId, rec = {}, now = Date.now()) {
|
|
318
|
+
const everySec = parseWakeupEvery(rec.everySec ?? rec.every ?? rec.interval);
|
|
319
|
+
const prompt = String(rec.prompt || DEFAULT_WAKEUP_PROMPT).trim().slice(0, 2000)
|
|
320
|
+
|| DEFAULT_WAKEUP_PROMPT;
|
|
321
|
+
const lastAt = Number(rec.lastAt) || 0;
|
|
322
|
+
let nextAt = Number(rec.nextAt) || 0;
|
|
323
|
+
if (!Number.isFinite(nextAt) || nextAt <= 0) nextAt = now + everySec * 1000;
|
|
324
|
+
return {
|
|
325
|
+
agentId: String(agentId || rec.agentId || ''),
|
|
326
|
+
everySec,
|
|
327
|
+
prompt,
|
|
328
|
+
lastAt,
|
|
329
|
+
nextAt,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function readWakeups(home) {
|
|
334
|
+
const raw = readJsonFile(wakeupsPath(home));
|
|
335
|
+
const src = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
336
|
+
const out = {};
|
|
337
|
+
for (const [id, rec] of Object.entries(src)) {
|
|
338
|
+
const w = shapeWakeup(id, rec);
|
|
339
|
+
if (w.agentId) out[w.agentId] = w;
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function deletedAgentsPath(home) {
|
|
345
|
+
return path.join(home, '.openzoo', 'grokbot-deleted.json');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function readDeletedIds(home) {
|
|
349
|
+
const raw = readJsonFile(deletedAgentsPath(home));
|
|
350
|
+
const ids = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.ids) ? raw.ids : []);
|
|
351
|
+
return new Set(ids.map(String).filter(Boolean));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function addDeletedIds(home, ids) {
|
|
355
|
+
const s = readDeletedIds(home);
|
|
356
|
+
for (const id of ids) if (id) s.add(String(id));
|
|
357
|
+
const dir = path.dirname(deletedAgentsPath(home));
|
|
358
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
359
|
+
fs.writeFileSync(deletedAgentsPath(home), JSON.stringify([...s], null, 2));
|
|
360
|
+
return s;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function filterDeleted(agents, home) {
|
|
364
|
+
const del = readDeletedIds(home);
|
|
365
|
+
if (!del.size || !Array.isArray(agents)) return agents || [];
|
|
366
|
+
return agents.filter((a) => a && !del.has(String(a.id)));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function writeWakeups(home, map) {
|
|
370
|
+
const dir = path.dirname(wakeupsPath(home));
|
|
371
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
372
|
+
fs.writeFileSync(wakeupsPath(home), JSON.stringify(map || {}, null, 2));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function callerKeyFromAuth(authorization) {
|
|
376
|
+
const a = String(authorization || '').trim();
|
|
377
|
+
if (!a) return '';
|
|
378
|
+
// Not a secret store — a session-scoped equality key so a second oauth
|
|
379
|
+
// does not inherit the previous account's 1340 / tray cache.
|
|
380
|
+
let h = 2166136261;
|
|
381
|
+
for (let i = 0; i < a.length; i++) {
|
|
382
|
+
h ^= a.charCodeAt(i);
|
|
383
|
+
h = Math.imul(h, 16777619);
|
|
384
|
+
}
|
|
385
|
+
return (h >>> 0).toString(16);
|
|
386
|
+
}
|