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/cursorbackend.js
CHANGED
|
@@ -33,20 +33,24 @@ import os from 'node:os';
|
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
import zlib from 'node:zlib';
|
|
35
35
|
import { execFileSync, execFile } from 'node:child_process';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
36
37
|
import { promisify } from 'node:util';
|
|
37
38
|
import tls from 'node:tls';
|
|
38
|
-
import { randomUUID } from 'node:crypto';
|
|
39
|
-
import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
|
|
39
|
+
import { randomUUID, generateKeyPairSync, createPrivateKey, sign as cryptoSign } from 'node:crypto';
|
|
40
|
+
import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect, fakeCursorAuthReply } from './cursorapi.js';
|
|
40
41
|
import {
|
|
41
42
|
accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
|
|
42
43
|
readHouseRoster, houseAgentsPath, shapeAgent, agentBrief, briefFromName,
|
|
43
44
|
preferNamedAgent, looksLikeAgentId,
|
|
45
|
+
grokroomAgentId, grokroomMemberId, grokroomShareUrl, grokroomFromHandle, grokroomAddrFromShare,
|
|
46
|
+
agentShareUrl, groupShareState, attachOnchainRoom, bindOnchainRoom,
|
|
47
|
+
isGrokRoomAgent, isGrokRoomMember,
|
|
44
48
|
readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
|
|
45
49
|
DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
|
|
46
50
|
} from './grokbotAccount.js';
|
|
47
51
|
import { spendChipLabel, formatSpendFooter, mergeTurnProof } from './spendProof.js';
|
|
48
52
|
import { spendLinesOnly, writeSpendHud } from './ozSpendChip.js';
|
|
49
|
-
import { zooModelIds, zooModelRow, mediaKindOf } from './models.js';
|
|
53
|
+
import { zooModelIds, zooModelRow, mediaKindOf, resolveModel } from './models.js';
|
|
50
54
|
import { prefixVisitorRichText } from './grokbotweb.js';
|
|
51
55
|
import {
|
|
52
56
|
ingestUpload, lookupUpload, readUploadChunk, readUploadImage, readUploadText,
|
|
@@ -66,11 +70,36 @@ const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'rep
|
|
|
66
70
|
// handshake before ALPN — logged as `HANDSHAKE FAILED ECONNRESET alpn=?`, with
|
|
67
71
|
// no request ever reaching us. Same cert, one more pair of names.
|
|
68
72
|
const ANTHROPIC_TLS_HOSTS = ['api.anthropic.com', 'api-staging.anthropic.com'];
|
|
69
|
-
const
|
|
73
|
+
const TLS_DNS = [...CURSOR_HOSTS, ...ANTHROPIC_TLS_HOSTS, 'localhost'];
|
|
74
|
+
const TLS_IPS = ['127.0.0.1'];
|
|
75
|
+
const TLS_HOSTS = [...TLS_DNS, ...TLS_IPS];
|
|
70
76
|
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
export function
|
|
77
|
+
/** Node-only self-signed PEM. Windows has no openssl in PATH (measured 2026-09-03).
|
|
78
|
+
* selfsigned@5 `generate` is async. */
|
|
79
|
+
export async function mintTlsPems() {
|
|
80
|
+
const selfsigned = createRequire(import.meta.url)('selfsigned');
|
|
81
|
+
const pems = await selfsigned.generate(
|
|
82
|
+
[{ name: 'commonName', value: 'api2.cursor.sh' }],
|
|
83
|
+
{
|
|
84
|
+
keySize: 2048,
|
|
85
|
+
days: 3650,
|
|
86
|
+
algorithm: 'sha256',
|
|
87
|
+
extensions: [{
|
|
88
|
+
name: 'subjectAltName',
|
|
89
|
+
altNames: [
|
|
90
|
+
...TLS_DNS.map((value) => ({ type: 2, value })),
|
|
91
|
+
...TLS_IPS.map((ip) => ({ type: 7, ip })),
|
|
92
|
+
],
|
|
93
|
+
}],
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
if (!pems?.cert || !pems?.private) throw new Error('selfsigned returned empty pem');
|
|
97
|
+
return { cert: pems.cert, key: pems.private };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Generate (once) a self-signed cert covering every host we impersonate.
|
|
101
|
+
* Mint with the `selfsigned` package (no openssl). openssl is a fallback only. */
|
|
102
|
+
export async function ensureCert(log = () => {}) {
|
|
74
103
|
const cert = path.join(TLS_DIR, 'cert.pem');
|
|
75
104
|
const key = path.join(TLS_DIR, 'key.pem');
|
|
76
105
|
// STALE-CERT INVALIDATION. A plain existence check meant anyone who had ever
|
|
@@ -85,12 +114,19 @@ export function ensureCert(log = () => {}) {
|
|
|
85
114
|
log('cursor-tls: cert predates the current host list — re-minting');
|
|
86
115
|
} catch { /* make it */ }
|
|
87
116
|
fs.mkdirSync(TLS_DIR, { recursive: true });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
117
|
+
try {
|
|
118
|
+
const pems = await mintTlsPems();
|
|
119
|
+
fs.writeFileSync(key, pems.key);
|
|
120
|
+
fs.writeFileSync(cert, pems.cert);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
const san = `subjectAltName=${TLS_DNS.map((h) => `DNS:${h}`).join(',')},${TLS_IPS.map((h) => `IP:${h}`).join(',')}`;
|
|
123
|
+
execFileSync('openssl', [
|
|
124
|
+
'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
|
|
125
|
+
'-keyout', key, '-out', cert, '-days', '3650',
|
|
126
|
+
'-subj', '/CN=api2.cursor.sh', '-addext', san,
|
|
127
|
+
], { stdio: 'ignore' });
|
|
128
|
+
log(`cursor-tls: openssl fallback (${e.message})`);
|
|
129
|
+
}
|
|
94
130
|
fs.writeFileSync(stamp, want);
|
|
95
131
|
log(`cursor-tls: self-signed cert minted at ${TLS_DIR} covering ${TLS_HOSTS.length} hosts (no CA, no trust prompt)`);
|
|
96
132
|
return { cert, key };
|
|
@@ -652,10 +688,10 @@ async function fetchEnsureSandBox(req, body, log) {
|
|
|
652
688
|
try {
|
|
653
689
|
const remote = await podJson('/api/listAgents', {}, log);
|
|
654
690
|
if (Array.isArray(remote) && remote.length) {
|
|
655
|
-
const merged = rosterForEvent(mergeAgentLists(remote), agentActivity);
|
|
691
|
+
const merged = rosterForEvent(mergeAgentLists(remote), agentActivity, { includeHidden: true }); // GROKROOM-HIDDEN-BOTS
|
|
656
692
|
saveAgents(merged);
|
|
657
693
|
const active = focusedAgentId || merged[0]?.id;
|
|
658
|
-
ssePush('agents', { agents: merged, activeAgentId: active });
|
|
694
|
+
ssePush('agents', { agents: merged.filter((a) => !a.hidden), activeAgentId: active }); // GROKROOM-HIDDEN-BOTS
|
|
659
695
|
log(`cursor-backend: discovered roster n=${merged.length} account=${realPod.accountId}`);
|
|
660
696
|
}
|
|
661
697
|
} catch (e) {
|
|
@@ -837,9 +873,9 @@ async function proxyPodHttp(req, res, full, body, log) {
|
|
|
837
873
|
try {
|
|
838
874
|
const parsed = JSON.parse(String(inflateBody(cap.buf, cap.respHeaders)));
|
|
839
875
|
if (Array.isArray(parsed)) {
|
|
840
|
-
const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity);
|
|
876
|
+
const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity, { includeHidden: true }); // GROKROOM-HIDDEN-BOTS
|
|
841
877
|
saveAgents(merged);
|
|
842
|
-
jsonSend(res, merged);
|
|
878
|
+
jsonSend(res, merged.filter((a) => !a.hidden)); // GROKROOM-HIDDEN-BOTS
|
|
843
879
|
log(`cursor-backend: listAgents 200 merged n=${merged.length} account=${activeAccountId || '?'}`);
|
|
844
880
|
return true;
|
|
845
881
|
}
|
|
@@ -942,7 +978,7 @@ function sortAgentsByActivity(list) {
|
|
|
942
978
|
return ay - ax;
|
|
943
979
|
});
|
|
944
980
|
}
|
|
945
|
-
function bumpAgent(id, { preview = '', notify = true } = {}) {
|
|
981
|
+
function bumpAgent(id, { preview = '', notify = true, select = true } = {}) {
|
|
946
982
|
const now = Date.now();
|
|
947
983
|
const a = agentActivity.get(id) || { unreadCount: 0 };
|
|
948
984
|
a.updatedAt = now;
|
|
@@ -960,7 +996,7 @@ function bumpAgent(id, { preview = '', notify = true } = {}) {
|
|
|
960
996
|
if (idx >= 0) list[idx] = agent;
|
|
961
997
|
else list.unshift(agent);
|
|
962
998
|
saveAgents(sortAgentsByActivity(list.map(shapeAgent)));
|
|
963
|
-
pushCreatedAgent(agent);
|
|
999
|
+
pushCreatedAgent(agent, { select });
|
|
964
1000
|
}
|
|
965
1001
|
|
|
966
1002
|
/** asar ingestAgentsEvent does Ae.agents.map — `{action:"created"}` has no
|
|
@@ -972,7 +1008,7 @@ function pushCreatedAgent(agent, { select = true } = {}) {
|
|
|
972
1008
|
const active = focusedAgentId || agent.id;
|
|
973
1009
|
const list = rosterForEvent(cachedAgentList() || [], agentActivity);
|
|
974
1010
|
ssePush('agent-upserted', { agent, activeAgentId: active });
|
|
975
|
-
ssePush('agents', { agents: list, activeAgentId: active });
|
|
1011
|
+
ssePush('agents', { agents: list.filter((a) => !a.hidden), activeAgentId: active }); // GROKROOM-HIDDEN-BOTS
|
|
976
1012
|
}
|
|
977
1013
|
|
|
978
1014
|
/** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
|
|
@@ -987,6 +1023,54 @@ function localExecPush(frame) {
|
|
|
987
1023
|
try { c.write(chunk); } catch { localExecSse.delete(c); }
|
|
988
1024
|
}
|
|
989
1025
|
}
|
|
1026
|
+
/** Helper builds disagree about where the shell output lives: top-level
|
|
1027
|
+
* `stdout` on some, `message: {stdout, exitCode}` or `clientMessage` on
|
|
1028
|
+
* others. Dig it out as TEXT — an object that reaches the canvas paints as
|
|
1029
|
+
* [object Object] (measured: `→ exec "ls" [object Object]` on Linux). */
|
|
1030
|
+
export function normalizeExecFrame(frame) {
|
|
1031
|
+
const out = { stdout: '', stderr: '', exitCode: undefined, message: '', error: '' };
|
|
1032
|
+
const seen = new Set();
|
|
1033
|
+
const pick = (node, keys) => {
|
|
1034
|
+
for (const k of keys) if (typeof node[k] === 'string' && node[k]) return node[k];
|
|
1035
|
+
return '';
|
|
1036
|
+
};
|
|
1037
|
+
const visit = (node, depth) => {
|
|
1038
|
+
if (!node || typeof node !== 'object' || depth > 4 || seen.has(node)) return;
|
|
1039
|
+
seen.add(node);
|
|
1040
|
+
if (!out.stdout) out.stdout = pick(node, ['stdout', 'stdOut', 'output']);
|
|
1041
|
+
if (!out.stderr) out.stderr = pick(node, ['stderr', 'stdErr']);
|
|
1042
|
+
if (!out.message) out.message = pick(node, ['message', 'text', 'content']);
|
|
1043
|
+
if (!out.error) out.error = pick(node, ['error', 'errorMessage']);
|
|
1044
|
+
if (out.exitCode == null) {
|
|
1045
|
+
for (const k of ['exitCode', 'exit_code', 'code']) {
|
|
1046
|
+
if (typeof node[k] === 'number') { out.exitCode = node[k]; break; }
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
for (const k of ['message', 'clientMessage', 'serverMessage', 'result', 'data', 'payload', 'body', 'throw']) {
|
|
1050
|
+
visit(node[k], depth + 1);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
visit(frame, 0);
|
|
1054
|
+
if (out.error) out.error = `local-exec: ${out.error}`;
|
|
1055
|
+
if (!out.stdout && !out.stderr && !out.message && !out.error) out.message = toolResultText(frame);
|
|
1056
|
+
return out;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** The daemon streams `output`/`stdout` chunks and only then closes with
|
|
1060
|
+
* `result`/`exit`, and those streaming frames do not always echo requestId.
|
|
1061
|
+
* Keying purely on requestId dropped the body on the floor and waited out the
|
|
1062
|
+
* 45s timeout, so fall back to the sole in-flight request. */
|
|
1063
|
+
function localExecWaiterFor(f) {
|
|
1064
|
+
if (f.requestId && localExecWaiters.has(f.requestId)) {
|
|
1065
|
+
return { id: f.requestId, w: localExecWaiters.get(f.requestId) };
|
|
1066
|
+
}
|
|
1067
|
+
if (!f.requestId && localExecWaiters.size === 1) {
|
|
1068
|
+
const [id, w] = localExecWaiters.entries().next().value;
|
|
1069
|
+
return { id, w };
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
990
1074
|
function handleLocalExecFrames(frames, log) {
|
|
991
1075
|
for (const f of frames || []) {
|
|
992
1076
|
if (!f || typeof f !== 'object') continue;
|
|
@@ -994,24 +1078,59 @@ function handleLocalExecFrames(frames, log) {
|
|
|
994
1078
|
localExecHello = f;
|
|
995
1079
|
log(`cursor-backend: local-exec hello computer=${f.computerId || f.label || '?'} root=${f.localRoot || '?'}`);
|
|
996
1080
|
}
|
|
997
|
-
const
|
|
998
|
-
if (!
|
|
1081
|
+
const hit = localExecWaiterFor(f);
|
|
1082
|
+
if (!hit) continue;
|
|
1083
|
+
const { id, w } = hit;
|
|
999
1084
|
if (f.kind === 'file') {
|
|
1000
1085
|
const buf = Buffer.from(f.bytesBase64 || '', 'base64');
|
|
1001
1086
|
w.resolve({ kind: 'file', bytes: buf, text: buf.toString('utf8') });
|
|
1002
|
-
localExecWaiters.delete(
|
|
1087
|
+
localExecWaiters.delete(id);
|
|
1003
1088
|
} else if (f.kind === 'file-error' || f.kind === 'messages-error') {
|
|
1004
1089
|
w.reject(new Error(f.error || 'local-exec error'));
|
|
1005
|
-
localExecWaiters.delete(
|
|
1006
|
-
} else if (f.kind === '
|
|
1090
|
+
localExecWaiters.delete(id);
|
|
1091
|
+
} else if (f.kind === 'output' || f.kind === 'stdout' || f.kind === 'stderr') {
|
|
1092
|
+
const norm = normalizeExecFrame(f);
|
|
1093
|
+
const chunk = norm.stdout || norm.message
|
|
1094
|
+
|| (typeof f.data === 'string' ? f.data : '')
|
|
1095
|
+
|| (typeof f.chunk === 'string' ? f.chunk : '');
|
|
1096
|
+
if (f.kind === 'stderr') w.stderr += toolResultText(chunk);
|
|
1097
|
+
else w.chunks += toolResultText(chunk);
|
|
1098
|
+
} else if (shellStreamOf(f)) {
|
|
1099
|
+
// The real Grok Bot lane: stdout/stderr arrive as many `kind:"client"`
|
|
1100
|
+
// frames and the turn is only over on `exit`. Resolving on the first one
|
|
1101
|
+
// (what the old client/control branch did) truncated every command to its
|
|
1102
|
+
// first chunk.
|
|
1103
|
+
const ss = shellStreamOf(f);
|
|
1104
|
+
if (typeof ss.stdout?.data === 'string') w.chunks += ss.stdout.data;
|
|
1105
|
+
if (typeof ss.stderr?.data === 'string') w.stderr += ss.stderr.data;
|
|
1106
|
+
if (ss.exit) {
|
|
1107
|
+
w.resolve({
|
|
1108
|
+
kind: 'exit',
|
|
1109
|
+
message: w.chunks || w.stderr,
|
|
1110
|
+
stdout: w.chunks,
|
|
1111
|
+
stderr: w.stderr,
|
|
1112
|
+
exitCode: Number(ss.exit.code || 0),
|
|
1113
|
+
});
|
|
1114
|
+
localExecWaiters.delete(id);
|
|
1115
|
+
}
|
|
1116
|
+
} else if (f.kind === 'control' && f.message && f.message.heartbeat) {
|
|
1117
|
+
// 3s keepalive while a long command runs. It is a `control` frame like
|
|
1118
|
+
// any other, so the generic branch below used to resolve the call with an
|
|
1119
|
+
// empty body the moment the daemon first breathed.
|
|
1120
|
+
continue;
|
|
1121
|
+
} else if (f.kind === 'client' || f.kind === 'control' || f.kind === 'result'
|
|
1122
|
+
|| f.kind === 'exec-result' || f.kind === 'exit') {
|
|
1123
|
+
const norm = normalizeExecFrame(f);
|
|
1124
|
+
const stdout = w.chunks + (norm.stdout || '');
|
|
1125
|
+
const stderr = w.stderr + (norm.stderr || '');
|
|
1007
1126
|
w.resolve({
|
|
1008
1127
|
kind: f.kind,
|
|
1009
|
-
message:
|
|
1010
|
-
stdout
|
|
1011
|
-
stderr:
|
|
1012
|
-
exitCode:
|
|
1128
|
+
message: norm.error || norm.message || stdout || toolResultText(f),
|
|
1129
|
+
stdout,
|
|
1130
|
+
stderr: norm.error && !stderr ? norm.error : stderr,
|
|
1131
|
+
exitCode: norm.exitCode,
|
|
1013
1132
|
});
|
|
1014
|
-
localExecWaiters.delete(
|
|
1133
|
+
localExecWaiters.delete(id);
|
|
1015
1134
|
}
|
|
1016
1135
|
}
|
|
1017
1136
|
}
|
|
@@ -1024,6 +1143,8 @@ function localExecAsk(frame, timeoutMs = 45000) {
|
|
|
1024
1143
|
reject(new Error('local-exec timeout — is Grok Bot Helper connected?'));
|
|
1025
1144
|
}, timeoutMs);
|
|
1026
1145
|
localExecWaiters.set(requestId, {
|
|
1146
|
+
chunks: '',
|
|
1147
|
+
stderr: '',
|
|
1027
1148
|
resolve: (v) => { clearTimeout(t); resolve(v); },
|
|
1028
1149
|
reject: (e) => { clearTimeout(t); reject(e); },
|
|
1029
1150
|
});
|
|
@@ -1156,6 +1277,7 @@ function appendLine(agentId, role, text, extra = {}) {
|
|
|
1156
1277
|
requestId,
|
|
1157
1278
|
...(extra.author && typeof extra.author === 'object' ? { author: extra.author } : {}),
|
|
1158
1279
|
...(extra.ephemeral ? { ephemeral: true } : {}),
|
|
1280
|
+
...(extra.sig ? { sig: String(extra.sig) } : {}),
|
|
1159
1281
|
};
|
|
1160
1282
|
}
|
|
1161
1283
|
t.entries.push(e);
|
|
@@ -1332,6 +1454,14 @@ export function formatZooProgress({ step, maxSteps, names, command } = {}) {
|
|
|
1332
1454
|
|
|
1333
1455
|
/** One canvas line per tool. Asar ingest is append-only — mutating a working
|
|
1334
1456
|
* bubble is still silence. Keep it short; the model history skips ephemeral. */
|
|
1457
|
+
export function toolResultText(result) {
|
|
1458
|
+
if (result == null) return '';
|
|
1459
|
+
if (typeof result === 'string') return result;
|
|
1460
|
+
if (typeof result === 'number' || typeof result === 'boolean') return String(result);
|
|
1461
|
+
if (Buffer.isBuffer(result)) return result.toString('utf8');
|
|
1462
|
+
try { return JSON.stringify(result); } catch { return String(result); }
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1335
1465
|
export function formatZooToolLine({ name, args = {}, result } = {}) {
|
|
1336
1466
|
const detail = args.command || args.path || args.name || args.window
|
|
1337
1467
|
|| args.query || args.url || args.key || args.app || args.every
|
|
@@ -1340,7 +1470,7 @@ export function formatZooToolLine({ name, args = {}, result } = {}) {
|
|
|
1340
1470
|
const head = detail
|
|
1341
1471
|
? `${name} ${JSON.stringify(String(detail).slice(0, 90))}`
|
|
1342
1472
|
: String(name || 'tool');
|
|
1343
|
-
let tail =
|
|
1473
|
+
let tail = toolResultText(result).replace(/\s+/g, ' ').trim();
|
|
1344
1474
|
if (tail.startsWith('{')) {
|
|
1345
1475
|
try {
|
|
1346
1476
|
const o = JSON.parse(String(result));
|
|
@@ -1452,6 +1582,7 @@ function kickBriefedAgent(agent, log = () => {}) {
|
|
|
1452
1582
|
|
|
1453
1583
|
function seedBriefOnCanvas(agent, log) {
|
|
1454
1584
|
if (!agent?.id || !String(agent.brief || '').trim()) return false;
|
|
1585
|
+
if (isGrokRoomAgent(agent) || isGrokRoomMember(agent)) return false;
|
|
1455
1586
|
const t = agentTranscript(agent.id);
|
|
1456
1587
|
if (!(t.entries || []).length) {
|
|
1457
1588
|
const nonce = `oz-brief-${agent.id}`;
|
|
@@ -1472,6 +1603,7 @@ let payNudged = false;
|
|
|
1472
1603
|
/** The terminal banner, on the canvas too: a new user reads the chat, not the shell. */
|
|
1473
1604
|
async function seedPayNudge(list, activeId, log = () => {}) {
|
|
1474
1605
|
if (payNudged || !activeId) return false;
|
|
1606
|
+
if (isGrokRoomAgent({ id: activeId }) || /^roombot-/.test(String(activeId))) return false;
|
|
1475
1607
|
payNudged = true;
|
|
1476
1608
|
try {
|
|
1477
1609
|
const t = agentTranscript(activeId);
|
|
@@ -1494,6 +1626,7 @@ async function seedPayNudge(list, activeId, log = () => {}) {
|
|
|
1494
1626
|
let shipNudged = false;
|
|
1495
1627
|
function seedShipNudge(list, activeId) {
|
|
1496
1628
|
if (shipNudged || !activeId) return false;
|
|
1629
|
+
if (isGrokRoomAgent({ id: activeId }) || /^roombot-/.test(String(activeId))) return false;
|
|
1497
1630
|
const roster = Array.isArray(list) ? list : [];
|
|
1498
1631
|
if (roster.some((a) => /^firstmate$/i.test(String(a?.name || '')))) { shipNudged = true; return false; }
|
|
1499
1632
|
const t = agentTranscript(activeId);
|
|
@@ -1514,6 +1647,25 @@ function groupMemberRecords(agentId) {
|
|
|
1514
1647
|
return groupMemberIds(agentId).map((mid) => {
|
|
1515
1648
|
const m = roster.find((x) => x.id === mid);
|
|
1516
1649
|
return { id: mid, name: String(m?.name || mid) };
|
|
1650
|
+
}).filter((m) => !/^roombot-/.test(m.id) && !isGrokRoomMember({ id: m.id }));
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
export function groupNormText(text) {
|
|
1654
|
+
return stripSpendFooter(String(text || ''))
|
|
1655
|
+
.replace(/^\s*[A-Za-z0-9 _.-]{1,40}:\s*/, '')
|
|
1656
|
+
.toLowerCase()
|
|
1657
|
+
.replace(/\s+/g, ' ')
|
|
1658
|
+
.trim();
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
export function groupReplyIsRepeat(text, recent = []) {
|
|
1662
|
+
const n = groupNormText(text);
|
|
1663
|
+
if (n.length < 40) return false;
|
|
1664
|
+
const head = n.slice(0, 90);
|
|
1665
|
+
return (Array.isArray(recent) ? recent : []).some((prev) => {
|
|
1666
|
+
const p = groupNormText(prev);
|
|
1667
|
+
if (p.length < 40) return false;
|
|
1668
|
+
return p.slice(0, 90) === head || p.startsWith(head) || n.startsWith(p.slice(0, 90));
|
|
1517
1669
|
});
|
|
1518
1670
|
}
|
|
1519
1671
|
|
|
@@ -1522,24 +1674,29 @@ export function groupReplyIsPass(text) {
|
|
|
1522
1674
|
if (!s) return true;
|
|
1523
1675
|
if (/^PASS\b/i.test(s)) return true;
|
|
1524
1676
|
if (s.length < 20) return true;
|
|
1677
|
+
if (/\binfinite\b.{0,48}\b(recursion|loop|chatter)\b/i.test(s)) return true;
|
|
1678
|
+
if (/\bbots? keep triggering\b/i.test(s)) return true;
|
|
1679
|
+
if (/\beach other'?s responses\b/i.test(s)) return true;
|
|
1680
|
+
if (/\bfeedback loop\b/i.test(s)) return true;
|
|
1525
1681
|
return /\b(nothing (more|else) to add|i('m| am) done|that('s| is) (all|my last word)|we (are|'re) (agreed|aligned|done)|i'?ll (stop|leave it)|no further|conversation is over|natural conclusion|i pass)\b/i.test(s);
|
|
1526
1682
|
}
|
|
1527
1683
|
|
|
1528
|
-
const GROUP_MAX_ROUNDS = Math.min(12, Math.max(
|
|
1529
|
-
const GROUP_MAX_CALLS = Math.min(24, Math.max(
|
|
1684
|
+
const GROUP_MAX_ROUNDS = Math.min(12, Math.max(1, Number(process.env.OZ_GROUP_MAX_ROUNDS || 2)));
|
|
1685
|
+
const GROUP_MAX_CALLS = Math.min(24, Math.max(2, Number(process.env.OZ_GROUP_MAX_CALLS || 6)));
|
|
1530
1686
|
|
|
1531
|
-
/** Members speak, then
|
|
1687
|
+
/** Members speak once, then at most a short peek. Repeats and loop-talk are PASS. */
|
|
1532
1688
|
async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
1533
1689
|
const named = groupMemberRecords(agentId);
|
|
1534
1690
|
if (!named.length) return false;
|
|
1535
1691
|
const names = named.map((m) => m.name);
|
|
1536
1692
|
let calls = 0;
|
|
1693
|
+
const painted = [];
|
|
1537
1694
|
|
|
1538
1695
|
const oneTurn = async (m, phase) => {
|
|
1539
1696
|
const others = names.filter((n) => n !== m.name).join(', ') || 'the group';
|
|
1540
1697
|
const persona = phase === 'peek'
|
|
1541
|
-
? `You are ${m.name} in a group with ${others}.
|
|
1542
|
-
: `You are ${m.name} in a group with ${others}. Answer the human as yourself
|
|
1698
|
+
? `You are ${m.name} in a group with ${others}. If you have nothing new, or would repeat another member, reply with exactly PASS. Do not discuss loops, recursion, or that bots are talking to each other. One short in-character line or PASS.`
|
|
1699
|
+
: `You are ${m.name} in a group with ${others}. Answer the human as yourself in a few sentences. Do not speak as the human. Do not prefix with visitor shortnames. Do not discuss infinite loops or recursion unless the human asked about that.\n\nThe human said: ${humanPrompt}`;
|
|
1543
1700
|
let bit = '';
|
|
1544
1701
|
try {
|
|
1545
1702
|
bit = (await zooComplete(persona, log, agentId, parsed)).text;
|
|
@@ -1548,7 +1705,7 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1548
1705
|
log(`cursor-backend: group ${phase} ${m.id} failed: ${e.message}`);
|
|
1549
1706
|
}
|
|
1550
1707
|
calls += 1;
|
|
1551
|
-
const pass = groupReplyIsPass(bit);
|
|
1708
|
+
const pass = groupReplyIsPass(bit) || groupReplyIsRepeat(bit, painted);
|
|
1552
1709
|
if (!pass) {
|
|
1553
1710
|
const line = fanoutLine(agentId, 'assistant', bit, {
|
|
1554
1711
|
clientNonce: nonce,
|
|
@@ -1557,6 +1714,11 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1557
1714
|
});
|
|
1558
1715
|
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
1559
1716
|
bumpAgent(agentId, { preview: bit, notify: true });
|
|
1717
|
+
painted.push(bit);
|
|
1718
|
+
{
|
|
1719
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
1720
|
+
if (live?.room?.addr) grokroomSend(live, bit, log, { from: m.name });
|
|
1721
|
+
}
|
|
1560
1722
|
log(`cursor-backend: group ${phase} ${m.name} seq=${line.seq}`);
|
|
1561
1723
|
} else {
|
|
1562
1724
|
log(`cursor-backend: group ${phase} ${m.name} PASS`);
|
|
@@ -1569,19 +1731,28 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1569
1731
|
await oneTurn(m, 'speak');
|
|
1570
1732
|
}
|
|
1571
1733
|
if (named.length < 2) return true;
|
|
1734
|
+
if (painted.length >= 2 && groupReplyIsRepeat(painted[painted.length - 1], painted.slice(0, -1))) {
|
|
1735
|
+
log(`cursor-backend: group stopped after speak (repeat) calls=${calls}`);
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1572
1738
|
|
|
1573
1739
|
let round = 0;
|
|
1574
1740
|
while (round < GROUP_MAX_ROUNDS && calls < GROUP_MAX_CALLS) {
|
|
1575
1741
|
round += 1;
|
|
1576
1742
|
let passes = 0;
|
|
1743
|
+
const before = painted.length;
|
|
1577
1744
|
for (const m of named) {
|
|
1578
1745
|
if (calls >= GROUP_MAX_CALLS) break;
|
|
1579
1746
|
if (await oneTurn(m, 'peek')) passes += 1;
|
|
1580
1747
|
}
|
|
1581
|
-
if (passes >= named.length) {
|
|
1748
|
+
if (passes >= named.length || painted.length === before) {
|
|
1582
1749
|
log(`cursor-backend: group concluded round=${round} calls=${calls}`);
|
|
1583
1750
|
break;
|
|
1584
1751
|
}
|
|
1752
|
+
if (painted.length >= 2 && groupReplyIsRepeat(painted[painted.length - 1], painted.slice(0, -1))) {
|
|
1753
|
+
log(`cursor-backend: group stopped peek (repeat) round=${round}`);
|
|
1754
|
+
break;
|
|
1755
|
+
}
|
|
1585
1756
|
}
|
|
1586
1757
|
log(`cursor-backend: group queue done rounds=${round} calls=${calls}`);
|
|
1587
1758
|
return true;
|
|
@@ -1603,7 +1774,7 @@ function mergeAgentLists(remote) {
|
|
|
1603
1774
|
return sortAgentsByActivity(filterDeleted(out, HOME));
|
|
1604
1775
|
}
|
|
1605
1776
|
function gatewayEntry(e) {
|
|
1606
|
-
const { seq, pulledRemote, promptRaw, ephemeral, ...rest } = e;
|
|
1777
|
+
const { seq, pulledRemote, promptRaw, ephemeral, sig, ...rest } = e;
|
|
1607
1778
|
return rest;
|
|
1608
1779
|
}
|
|
1609
1780
|
|
|
@@ -1935,6 +2106,9 @@ const MODEL_ALIASES = {
|
|
|
1935
2106
|
'x-ai/grok-4.6': 'grok-4.6',
|
|
1936
2107
|
glm: 'zai-org/glm-5.3-flash',
|
|
1937
2108
|
'glm-5': 'zai-org/glm-5.3-flash',
|
|
2109
|
+
'glm-5.2': 'zai-org/GLM-5.2',
|
|
2110
|
+
'z-ai/glm-5.2': 'zai-org/GLM-5.2',
|
|
2111
|
+
'zai-org/glm-5.2': 'zai-org/GLM-5.2',
|
|
1938
2112
|
'glm-5.3': 'zai-org/glm-5.3-flash',
|
|
1939
2113
|
'glm-5.3-flash': 'zai-org/glm-5.3-flash',
|
|
1940
2114
|
flash: 'zai-org/glm-5.3-flash',
|
|
@@ -1968,6 +2142,11 @@ async function resolveModelId(raw) {
|
|
|
1968
2142
|
if (!s) return null;
|
|
1969
2143
|
const lower = s.toLowerCase().replace(/^\/+/, '');
|
|
1970
2144
|
if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
|
|
2145
|
+
try {
|
|
2146
|
+
const ids = await zooModelIds();
|
|
2147
|
+
const mapped = resolveModel(s, ids);
|
|
2148
|
+
if (mapped) return mapped;
|
|
2149
|
+
} catch { /* catalog miss: keep explicit slash ids */ }
|
|
1971
2150
|
if (s.includes('/')) return s;
|
|
1972
2151
|
// No vendor prefix and no alias: the LIVE catalog decides, not this file.
|
|
1973
2152
|
// The zoo serves ids with no slash at all (abliterated-model*), and a
|
|
@@ -2062,33 +2241,100 @@ async function writeLocalBytes(abs, bytes, log) {
|
|
|
2062
2241
|
fs.writeFileSync(abs, buf);
|
|
2063
2242
|
return `wrote ${abs} (${buf.length} bytes)`;
|
|
2064
2243
|
}
|
|
2244
|
+
/** `serverMessage` is `agent.v1.ExecServerMessage` as protobuf-es JSON — a oneof,
|
|
2245
|
+
* so the key IS the case name. Two wrong shapes before this one:
|
|
2246
|
+
* `{command, cwd}` flat -> "No handler found for server message of type undefined"
|
|
2247
|
+
* `{shellArgs: {...}}` -> "No handler found for server message of type shellArgs"
|
|
2248
|
+
* `shellArgs` is a real case on the wire (field 2), but `buildLocalExecManager`
|
|
2249
|
+
* in Grok Bot 0.36.0's dist/local-exec-daemon/main.cjs registers exactly FOUR
|
|
2250
|
+
* handlers: shellStreamArgs, backgroundShellSpawnArgs, readArgs, lsArgs. The
|
|
2251
|
+
* one-shot shell case is simply not wired into the local-exec lane — the
|
|
2252
|
+
* streaming one is, and both carry the same `agent.v1.ShellArgs` payload.
|
|
2253
|
+
* `authorizedBy*` are read as `=== true` by the daemon's approval gate; without
|
|
2254
|
+
* them an unattended exec has nothing to approve it.
|
|
2255
|
+
*
|
|
2256
|
+
* `timeout` is MILLISECONDS. The daemon reads it through a function named
|
|
2257
|
+
* `resolveShellTimeoutMs` and uses the value verbatim, so the plausible-looking
|
|
2258
|
+
* `timeout: 120` (seconds, as lib/podagent.mjs had it) aborts the shell after
|
|
2259
|
+
* 120 ms: `echo hi; pwd` came back exit 0 with empty stdout and `ls -lha ~`
|
|
2260
|
+
* died mid-stream with "Cannot read properties of undefined (reading '3')".
|
|
2261
|
+
* 0 means "use the daemon's default", not "no timeout". */
|
|
2262
|
+
export function localExecFrame(command, cwd, timeoutMs = 120_000) {
|
|
2263
|
+
return {
|
|
2264
|
+
kind: 'exec',
|
|
2265
|
+
requestId: randomUUID(),
|
|
2266
|
+
approvalId: randomUUID(),
|
|
2267
|
+
authorizedByStanding: true,
|
|
2268
|
+
authorizedByApproval: true,
|
|
2269
|
+
serverMessage: {
|
|
2270
|
+
shellStreamArgs: { command: String(command), workingDirectory: cwd, timeout: timeoutMs },
|
|
2271
|
+
},
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/** `agent.v1.ShellStream` off an ExecClientMessage: `{stdout:{data}}`,
|
|
2276
|
+
* `{stderr:{data}}`, `{exit:{code, cwd}}`. `data` is a proto string (T=9), not
|
|
2277
|
+
* bytes — no base64 decode. */
|
|
2278
|
+
export function shellStreamOf(frame) {
|
|
2279
|
+
const m = frame && frame.message;
|
|
2280
|
+
if (!m || typeof m !== 'object') return null;
|
|
2281
|
+
const ss = m.shellStream;
|
|
2282
|
+
return ss && typeof ss === 'object' ? ss : null;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/** exec used to hardcode `/bin/zsh -lc`: on Linux that is usually ENOENT and on
|
|
2286
|
+
* Windows there is no POSIX shell at all, so every exec came back as a spawn
|
|
2287
|
+
* error instead of output. */
|
|
2288
|
+
export function execShell(command, platform = process.platform) {
|
|
2289
|
+
if (platform === 'win32') {
|
|
2290
|
+
return { file: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command] };
|
|
2291
|
+
}
|
|
2292
|
+
if (platform === 'darwin') return { file: '/bin/zsh', args: ['-lc', command] };
|
|
2293
|
+
for (const sh of ['/bin/bash', '/usr/bin/bash', '/bin/zsh']) {
|
|
2294
|
+
try { if (fs.existsSync(sh)) return { file: sh, args: ['-lc', command] }; } catch { /* keep looking */ }
|
|
2295
|
+
}
|
|
2296
|
+
return { file: '/bin/sh', args: ['-c', command] };
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2065
2299
|
async function execLocal(command, cwd, log) {
|
|
2066
2300
|
let dir = cwd ? expandUserPath(cwd) : os.homedir();
|
|
2067
2301
|
try {
|
|
2068
2302
|
if (!fs.statSync(dir).isDirectory()) dir = os.homedir();
|
|
2069
2303
|
} catch {
|
|
2070
|
-
// Node reports spawn
|
|
2304
|
+
// Node reports spawn <shell> ENOENT when cwd is missing — not a missing shell.
|
|
2071
2305
|
try { fs.mkdirSync(dir, { recursive: true }); } catch { dir = os.homedir(); }
|
|
2072
2306
|
}
|
|
2073
2307
|
if (localExecSse.size > 0) {
|
|
2074
2308
|
log(`cursor-backend: local-exec exec ${JSON.stringify(command).slice(0, 80)}`);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
const
|
|
2080
|
-
const err =
|
|
2309
|
+
// Daemon-side budget stays under our own wait so a slow command reports the
|
|
2310
|
+
// shell's error, not our generic "is Grok Bot Helper connected?".
|
|
2311
|
+
const got = await localExecAsk(localExecFrame(command, dir, 55_000), 60000);
|
|
2312
|
+
const out = toolResultText(got.stdout || got.message || '');
|
|
2313
|
+
const errText = toolResultText(got.stderr);
|
|
2314
|
+
const err = errText ? `\nstderr:\n${errText}` : '';
|
|
2081
2315
|
return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
|
|
2082
2316
|
}
|
|
2083
|
-
|
|
2317
|
+
const sh = execShell(command);
|
|
2318
|
+
log(`cursor-backend: exec cwd=${dir} sh=${sh.file} ${JSON.stringify(String(command).slice(0, 80))}`);
|
|
2084
2319
|
const execMs = Math.min(180000, Math.max(15000, Number(process.env.OPENZOO_EXEC_TIMEOUT_MS || 90000)));
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2320
|
+
let stdout = '';
|
|
2321
|
+
let stderr = '';
|
|
2322
|
+
try {
|
|
2323
|
+
({ stdout, stderr } = await execFileAsync(sh.file, sh.args, {
|
|
2324
|
+
cwd: dir,
|
|
2325
|
+
timeout: execMs,
|
|
2326
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
2327
|
+
env: process.env,
|
|
2328
|
+
windowsHide: true,
|
|
2329
|
+
}));
|
|
2330
|
+
} catch (e) {
|
|
2331
|
+
// A nonzero exit (grep with no match, a failing build) still has output the
|
|
2332
|
+
// model needs — bubbling the Error loses it and paints only "ERROR".
|
|
2333
|
+
stdout = toolResultText(e.stdout);
|
|
2334
|
+
stderr = toolResultText(e.stderr) || e.message;
|
|
2335
|
+
if (!stdout && !stderr) throw e;
|
|
2336
|
+
}
|
|
2337
|
+
return `${toolResultText(stdout)}${stderr ? `\nstderr:\n${toolResultText(stderr)}` : ''}`.trim() || '(no output)';
|
|
2092
2338
|
}
|
|
2093
2339
|
|
|
2094
2340
|
const LOCAL_TOOLS = [
|
|
@@ -2600,13 +2846,21 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2600
2846
|
}
|
|
2601
2847
|
if (name === 'list_dir') {
|
|
2602
2848
|
const abs = expandUserPath(args.path || os.homedir());
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2849
|
+
try {
|
|
2850
|
+
if (localExecSse.size > 0) {
|
|
2851
|
+
const got = await localExecAsk(localExecFrame(`ls -la ${JSON.stringify(abs)}`, os.homedir(), 40_000));
|
|
2852
|
+
// `|| got` used to dump the whole resolved frame onto the canvas as
|
|
2853
|
+
// JSON ({"kind":"exit","stdout":"", …}) whenever the listing came back
|
|
2854
|
+
// empty. Fall back to the local read instead — it is the same answer.
|
|
2855
|
+
const text = toolResultText(got.stdout || got.stderr || got.message).trim();
|
|
2856
|
+
if (text) return text;
|
|
2857
|
+
}
|
|
2858
|
+
const rows = fs.readdirSync(abs, { withFileTypes: true })
|
|
2859
|
+
.map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`);
|
|
2860
|
+
return rows.join('\n') || '(empty)';
|
|
2861
|
+
} catch (e) {
|
|
2862
|
+
return `list_dir failed ${abs}: ${e.message}`;
|
|
2606
2863
|
}
|
|
2607
|
-
return fs.readdirSync(abs, { withFileTypes: true })
|
|
2608
|
-
.map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`)
|
|
2609
|
-
.join('\n');
|
|
2610
2864
|
}
|
|
2611
2865
|
if (name === 'exec') {
|
|
2612
2866
|
return await execLocal(String(args.command || ''), args.cwd, log);
|
|
@@ -3159,7 +3413,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3159
3413
|
name,
|
|
3160
3414
|
path: args.path,
|
|
3161
3415
|
command: args.command ? String(args.command).slice(0, 120) : undefined,
|
|
3162
|
-
note:
|
|
3416
|
+
note: toolResultText(result).slice(0, 200),
|
|
3163
3417
|
});
|
|
3164
3418
|
if (typeof opts.onProgress === 'function') {
|
|
3165
3419
|
opts.onProgress(formatZooToolLine({ name, args, result }));
|
|
@@ -3167,7 +3421,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3167
3421
|
messages.push({
|
|
3168
3422
|
role: 'tool',
|
|
3169
3423
|
tool_call_id: c.id,
|
|
3170
|
-
content:
|
|
3424
|
+
content: toolResultText(result).slice(0, 120000),
|
|
3171
3425
|
});
|
|
3172
3426
|
}
|
|
3173
3427
|
if (pendingVision.length) {
|
|
@@ -3323,12 +3577,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3323
3577
|
jsonSend(res, { ok: true, ...state });
|
|
3324
3578
|
return true;
|
|
3325
3579
|
}
|
|
3580
|
+
if (name === 'ozShare') {
|
|
3581
|
+
const list = cachedAgentList() || [];
|
|
3582
|
+
const id = String(focusedAgentId || '');
|
|
3583
|
+
const agent = list.find((a) => a.id === id) || null;
|
|
3584
|
+
const share = groupShareState({ agent, rooms: grokroomSync._rooms });
|
|
3585
|
+
jsonSend(res, { ok: true, agentId: id, ...share });
|
|
3586
|
+
return true;
|
|
3587
|
+
}
|
|
3326
3588
|
if (name === 'ozRevive') {
|
|
3327
3589
|
let parsed = {};
|
|
3328
3590
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3329
3591
|
const every = String(parsed.every || '5m');
|
|
3330
3592
|
const wakes = readWakeups(HOME);
|
|
3331
|
-
const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup);
|
|
3593
|
+
const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup && !isGrokRoomAgent(a) && !isGrokRoomMember(a));
|
|
3332
3594
|
const armed = [];
|
|
3333
3595
|
const already = [];
|
|
3334
3596
|
for (const a of list) {
|
|
@@ -3345,10 +3607,48 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3345
3607
|
return true;
|
|
3346
3608
|
}
|
|
3347
3609
|
if (name === 'getHostStatus') {
|
|
3348
|
-
|
|
3610
|
+
const host = {
|
|
3611
|
+
hostVersion: '0.36.0',
|
|
3612
|
+
latestHostVersion: '0.36.0',
|
|
3613
|
+
hostUpdateAvailable: false,
|
|
3614
|
+
isBusy: false,
|
|
3615
|
+
capabilities: [],
|
|
3616
|
+
};
|
|
3617
|
+
jsonSend(res, { status: 'ok', value: host, ...host });
|
|
3349
3618
|
log('cursor-backend: -> getHostStatus ready');
|
|
3350
3619
|
return true;
|
|
3351
3620
|
}
|
|
3621
|
+
if (name === 'resolveAgentCreation') {
|
|
3622
|
+
const route = { kind: 'box' };
|
|
3623
|
+
jsonSend(res, { status: 'ok', value: route, ...route });
|
|
3624
|
+
log('cursor-backend: -> resolveAgentCreation box');
|
|
3625
|
+
return true;
|
|
3626
|
+
}
|
|
3627
|
+
if (name === 'getForeverBoxStatus' || name === 'ensureForeverBox') {
|
|
3628
|
+
let parsed = {};
|
|
3629
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3630
|
+
const box = {
|
|
3631
|
+
agentId: String(parsed.id || parsed.agentId || 'openzoo'),
|
|
3632
|
+
state: 'running',
|
|
3633
|
+
};
|
|
3634
|
+
jsonSend(res, { status: 'ok', value: box, ...box });
|
|
3635
|
+
log(`cursor-backend: -> ${name} agentId=${box.agentId} state=${box.state}`);
|
|
3636
|
+
return true;
|
|
3637
|
+
}
|
|
3638
|
+
if (name === 'getBoxSecretsStatus') {
|
|
3639
|
+
const secrets = { keys: [], isApplied: false, lastAppliedAtMs: null };
|
|
3640
|
+
jsonSend(res, { status: 'ok', value: secrets, ...secrets });
|
|
3641
|
+
return true;
|
|
3642
|
+
}
|
|
3643
|
+
if (name === 'isEgressTunnelAvailable') {
|
|
3644
|
+
jsonSend(res, { status: 'ok', value: false });
|
|
3645
|
+
return true;
|
|
3646
|
+
}
|
|
3647
|
+
if (name === 'getAgentChannels') {
|
|
3648
|
+
const channels = { manifests: [], connections: [] };
|
|
3649
|
+
jsonSend(res, { status: 'ok', value: channels, ...channels });
|
|
3650
|
+
return true;
|
|
3651
|
+
}
|
|
3352
3652
|
if (name === 'uploadAttachment' || name === 'readAttachmentImage'
|
|
3353
3653
|
|| name === 'readAttachmentText' || name === 'readAttachmentChunk') {
|
|
3354
3654
|
let parsed = {};
|
|
@@ -3412,7 +3712,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3412
3712
|
parsed = { ...src, id: undefined, name: `${src.name || 'chat'} copy` };
|
|
3413
3713
|
}
|
|
3414
3714
|
const agent = mintLocalAgent(parsed);
|
|
3415
|
-
|
|
3715
|
+
const created = { agent, id: agent.id, ...agent };
|
|
3716
|
+
jsonSend(res, { status: 'ok', value: created, ...created });
|
|
3416
3717
|
pushCreatedAgent(agent);
|
|
3417
3718
|
log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
|
|
3418
3719
|
return true;
|
|
@@ -3430,7 +3731,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3430
3731
|
let parsed = {};
|
|
3431
3732
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3432
3733
|
const id = String(parsed.id || parsed.agentId || '');
|
|
3433
|
-
|
|
3734
|
+
const kick = { id, isIntroductionInFlight: false };
|
|
3735
|
+
jsonSend(res, { status: 'ok', value: kick, ...kick });
|
|
3434
3736
|
log(`cursor-backend: kickstartAgent id=${id || '?'}`);
|
|
3435
3737
|
return true;
|
|
3436
3738
|
}
|
|
@@ -3438,15 +3740,18 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3438
3740
|
let parsed = {};
|
|
3439
3741
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3440
3742
|
const memberIds = [].concat(parsed.memberAgentIds || parsed.memberIds || []).map(String).filter(Boolean);
|
|
3441
|
-
const
|
|
3743
|
+
const raw = mintLocalAgent({
|
|
3442
3744
|
...parsed,
|
|
3443
3745
|
isGroup: true,
|
|
3444
3746
|
memberIds,
|
|
3445
3747
|
name: parsed.name || parsed.title || 'group',
|
|
3446
3748
|
});
|
|
3447
|
-
|
|
3749
|
+
const agent = mintLocalAgent(attachOnchainRoom(raw, HOME));
|
|
3750
|
+
const created = { agent, id: agent.id, ...agent };
|
|
3751
|
+
jsonSend(res, { status: 'ok', value: created, ...created });
|
|
3448
3752
|
pushCreatedAgent(agent);
|
|
3449
|
-
log(`cursor-backend: createGroup
|
|
3753
|
+
log(`cursor-backend: createGroup onchain id=${agent.id} share=${agent.room?.web || ''} members=${memberIds.length} name=${JSON.stringify(agent.name)}`);
|
|
3754
|
+
void paintGroupShareHint(agent, log);
|
|
3450
3755
|
return true;
|
|
3451
3756
|
}
|
|
3452
3757
|
if (name === 'setGroupMembers') {
|
|
@@ -3521,7 +3826,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3521
3826
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3522
3827
|
const ids = new Set([].concat(parsed.ids || parsed.id || []).map(String));
|
|
3523
3828
|
const next = (cachedAgentList() || []).filter((a) => !ids.has(a.id));
|
|
3524
|
-
addDeletedIds(HOME, [...ids]);
|
|
3829
|
+
addDeletedIds(HOME, [...ids].filter((id) => !/^(room-|roombot-)/.test(id)));
|
|
3525
3830
|
saveAgents(next);
|
|
3526
3831
|
for (const id of ids) {
|
|
3527
3832
|
transcripts.delete(id);
|
|
@@ -3544,7 +3849,15 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3544
3849
|
return true;
|
|
3545
3850
|
}
|
|
3546
3851
|
if (name === 'listAgents' || name === 'countAgents' || name === 'searchAgents') {
|
|
3547
|
-
|
|
3852
|
+
let raw = mergeAgentLists([]);
|
|
3853
|
+
let minted = false;
|
|
3854
|
+
raw = raw.map((a) => {
|
|
3855
|
+
if (!a?.isGroup || a.hidden || /^room-/.test(String(a.id || '')) || agentShareUrl(a)) return a;
|
|
3856
|
+
minted = true;
|
|
3857
|
+
return shapeAgent(attachOnchainRoom(a, HOME));
|
|
3858
|
+
});
|
|
3859
|
+
if (minted) saveAgents(raw.map(shapeAgent));
|
|
3860
|
+
const list = rosterForEvent(raw, agentActivity);
|
|
3548
3861
|
if (name === 'countAgents') {
|
|
3549
3862
|
jsonSend(res, list.length);
|
|
3550
3863
|
log(`cursor-backend: countAgents local n=${list.length} account=${activeAccountId || 'none'}`);
|
|
@@ -3594,6 +3907,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3594
3907
|
log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN}${visitor ? ` visitor=${visitor.shortname}` : ''} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
|
|
3595
3908
|
lastSendEchoId = String(nonce);
|
|
3596
3909
|
jsonSend(res, { accepted: true });
|
|
3910
|
+
const roomAgent = grokroomLookup(agentId);
|
|
3911
|
+
if (roomAgent && /^room-/.test(agentId)) {
|
|
3912
|
+
const userLine = fanoutLine(agentId, 'user', uiText, {
|
|
3913
|
+
clientNonce: nonce,
|
|
3914
|
+
requestId: nonce,
|
|
3915
|
+
richText: visitor ? prefixVisitorRichText(parsed.richText, visitor.shortname, uiText) : parsed.richText,
|
|
3916
|
+
promptRaw: prompt,
|
|
3917
|
+
});
|
|
3918
|
+
noteFocus(agentId);
|
|
3919
|
+
bumpAgent(agentId, { preview: prompt, notify: false, select: false });
|
|
3920
|
+
ssePush('transcript', { ...gatewayEntry(userLine), agentId });
|
|
3921
|
+
grokroomSend(roomAgent, prompt, log);
|
|
3922
|
+
return true;
|
|
3923
|
+
}
|
|
3597
3924
|
if (!visitor && wantsWakeupCron(prompt)) {
|
|
3598
3925
|
const cron = scheduleAgentWakeup(agentId, { every: '5m' });
|
|
3599
3926
|
log(`cursor-backend: never-stop cron agent=${agentId} every=${cron.everySec}s`);
|
|
@@ -3608,6 +3935,12 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3608
3935
|
noteFocus(agentId);
|
|
3609
3936
|
bumpAgent(agentId, { preview: prompt, notify: false });
|
|
3610
3937
|
ssePush('transcript', { ...gatewayEntry(userLine), agentId });
|
|
3938
|
+
{
|
|
3939
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
3940
|
+
if (live?.room?.addr) grokroomSend(live, prompt, log);
|
|
3941
|
+
}
|
|
3942
|
+
const joinAddr = /^\s*\/join\b/i.test(prompt) ? grokroomAddrFromShare(prompt)
|
|
3943
|
+
: (/^\s*https?:\/\/openzoo\.fun\/r\//i.test(prompt) ? grokroomAddrFromShare(prompt) : '');
|
|
3611
3944
|
const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
|
|
3612
3945
|
setImmediate(async () => {
|
|
3613
3946
|
let text = '';
|
|
@@ -3629,11 +3962,27 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3629
3962
|
text = `model set to ${id}`;
|
|
3630
3963
|
}
|
|
3631
3964
|
}
|
|
3965
|
+
} else if (joinAddr) {
|
|
3966
|
+
grouped = true;
|
|
3967
|
+
const joined = joinSharedGroup(joinAddr);
|
|
3968
|
+
if (!joined) {
|
|
3969
|
+
text = `could not join — not an openzoo.fun/r/<addr> share`;
|
|
3970
|
+
} else {
|
|
3971
|
+
noteFocus(joined.id);
|
|
3972
|
+
ssePush('agent-upserted', { agent: joined, activeAgentId: joined.id });
|
|
3973
|
+
ssePush('agents', { agents: rosterForEvent(cachedAgentList() || [], agentActivity), activeAgentId: joined.id });
|
|
3974
|
+
void paintGroupShareHint(joined, log);
|
|
3975
|
+
text = `joined ${joined.name}\n${joined.room?.web || ''}\nIn your OpenZoo Bot: add your bots to this group. They speak here; remote nicks only show up as people, they are not local bots.\nAnyone else: /join ${joined.room?.web || joinAddr}`;
|
|
3976
|
+
}
|
|
3632
3977
|
} else {
|
|
3633
|
-
const
|
|
3978
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
3979
|
+
const members = groupMemberRecords(agentId);
|
|
3980
|
+
const isSharedRoom = !!(live?.room?.addr || isGrokRoomAgent(live || { id: agentId }));
|
|
3634
3981
|
if (members.length) {
|
|
3635
3982
|
grouped = true;
|
|
3636
3983
|
await runGroupQueue({ agentId, humanPrompt: prompt, parsed, nonce, log });
|
|
3984
|
+
} else if (isSharedRoom) {
|
|
3985
|
+
grouped = true;
|
|
3637
3986
|
} else {
|
|
3638
3987
|
const z = await zooComplete(prompt, log, agentId, parsed, {
|
|
3639
3988
|
signal: turn.signal,
|
|
@@ -3664,6 +4013,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3664
4013
|
const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
3665
4014
|
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
3666
4015
|
bumpAgent(agentId, { preview: text, notify: true });
|
|
4016
|
+
{
|
|
4017
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
4018
|
+
if (live?.room?.addr && text) grokroomSend(live, text, log);
|
|
4019
|
+
}
|
|
3667
4020
|
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 220))}`);
|
|
3668
4021
|
} else {
|
|
3669
4022
|
log(`cursor-backend: sendPrompt group done agent=${agentId}`);
|
|
@@ -3679,6 +4032,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3679
4032
|
noteFocus(id);
|
|
3680
4033
|
tailedAgents.add(id);
|
|
3681
4034
|
const t = agentTranscript(id);
|
|
4035
|
+
if (isGrokRoomAgent({ id }) || /^roombot-/.test(id)) t.pulledRemote = true;
|
|
3682
4036
|
if (!t.pulledRemote && realPod?.agent && !useHouseRoster()) {
|
|
3683
4037
|
const remote = await podJson('/api/getAgentTranscriptTail', {
|
|
3684
4038
|
id, agentId: id, limit: 200, beforeSeq: parsed.beforeSeq,
|
|
@@ -3718,7 +4072,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3718
4072
|
}
|
|
3719
4073
|
|
|
3720
4074
|
const stubs = {
|
|
3721
|
-
getHostStatus: {
|
|
4075
|
+
getHostStatus: {
|
|
4076
|
+
hostVersion: '0.36.0', latestHostVersion: '0.36.0',
|
|
4077
|
+
hostUpdateAvailable: false, isBusy: false, capabilities: [],
|
|
4078
|
+
},
|
|
3722
4079
|
getHostSettings: { settings: {} },
|
|
3723
4080
|
setHostSettings: { ok: true },
|
|
3724
4081
|
setBoxSecrets: { ok: true },
|
|
@@ -3729,12 +4086,16 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3729
4086
|
getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
|
|
3730
4087
|
getTeachRecordingStatus: { recording: false },
|
|
3731
4088
|
getTrays: [],
|
|
3732
|
-
isGlobalSearchEnabled:
|
|
3733
|
-
isEgressTunnelAvailable:
|
|
4089
|
+
isGlobalSearchEnabled: false,
|
|
4090
|
+
isEgressTunnelAvailable: false,
|
|
3734
4091
|
getSharingState: { sharing: false },
|
|
3735
4092
|
getBotTemplateExportPolicy: { allowed: true },
|
|
3736
4093
|
getAgentAutomations: { automations: [] },
|
|
3737
|
-
getForeverBoxStatus: {
|
|
4094
|
+
getForeverBoxStatus: { agentId: 'openzoo', state: 'running' },
|
|
4095
|
+
ensureForeverBox: { agentId: 'openzoo', state: 'running' },
|
|
4096
|
+
getBoxSecretsStatus: { keys: [], isApplied: false, lastAppliedAtMs: null },
|
|
4097
|
+
getAgentChannels: { manifests: [], connections: [] },
|
|
4098
|
+
resolveAgentCreation: { kind: 'box' },
|
|
3738
4099
|
getSubagents: { subagents: [] },
|
|
3739
4100
|
getAsyncTasks: { tasks: [] },
|
|
3740
4101
|
getAgentWorkflows: { workflows: [] },
|
|
@@ -3799,14 +4160,377 @@ function respond(req, res, method, models) {
|
|
|
3799
4160
|
res.end(bodyProto);
|
|
3800
4161
|
}
|
|
3801
4162
|
|
|
4163
|
+
// ===== GROKROOM-BRIDGE v1 (overlay) =====
|
|
4164
|
+
// Rooms from 127.0.0.1:4799 as native Grok Bot group threads.
|
|
4165
|
+
// One room = one sidebar row (# main / # bots / # random / # lobby).
|
|
4166
|
+
// Each remote sender is a hidden bubble-member (no sidebar row). Sends POST a
|
|
4167
|
+
// signed ed25519 envelope to /submit — the chain is the log. Additive: any
|
|
4168
|
+
// failure here must never break zoo chat. Interval starts from startCursorBackend
|
|
4169
|
+
// so importing this module in tests does not poll.
|
|
4170
|
+
const GROKROOM_API = process.env.OZ_GROKROOM_URL || 'http://127.0.0.1:4799';
|
|
4171
|
+
const GROKROOM_PAYMASTER = (process.env.OZ_GROKROOM_PAYMASTER || 'https://openzoo.fun').replace(/\/$/, '');
|
|
4172
|
+
const GROKROOM_ID_FILE = path.join(HOME, '.openzoo', 'grokroom-identity.json');
|
|
4173
|
+
const GROKROOM_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
4174
|
+
const GROKROOM_HANDLE = (process.env.OZ_ROOM_HANDLE || process.env.USER || 'anon')
|
|
4175
|
+
.replace(/[^A-Za-z0-9 _.'-]/g, '').trim().slice(0, 24) || 'anon';
|
|
4176
|
+
const GROKROOM_B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
4177
|
+
|
|
4178
|
+
function grokroomB58(buf) {
|
|
4179
|
+
const digits = [0];
|
|
4180
|
+
for (const byte of buf) {
|
|
4181
|
+
let carry = byte;
|
|
4182
|
+
for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8; digits[i] = carry % 58; carry = (carry / 58) | 0; }
|
|
4183
|
+
while (carry) { digits.push(carry % 58); carry = (carry / 58) | 0; }
|
|
4184
|
+
}
|
|
4185
|
+
let out = '';
|
|
4186
|
+
for (const b of buf) { if (b === 0) out += '1'; else break; }
|
|
4187
|
+
return out + digits.reverse().map((d) => GROKROOM_B58[d]).join('');
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
let grokroomIdCache = null;
|
|
4191
|
+
function grokroomIdentity() {
|
|
4192
|
+
if (grokroomIdCache) return grokroomIdCache;
|
|
4193
|
+
let id = null;
|
|
4194
|
+
try {
|
|
4195
|
+
const j = JSON.parse(fs.readFileSync(GROKROOM_ID_FILE, 'utf8'));
|
|
4196
|
+
if (j.pub && j.seed) id = j;
|
|
4197
|
+
} catch { /* first run */ }
|
|
4198
|
+
if (!id) {
|
|
4199
|
+
const pair = generateKeyPairSync('ed25519');
|
|
4200
|
+
const rawPub = pair.publicKey.export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
4201
|
+
const rawSeed = pair.privateKey.export({ format: 'der', type: 'pkcs8' }).subarray(-32);
|
|
4202
|
+
id = { pub: grokroomB58(Buffer.from(rawPub)), seed: Buffer.from(rawSeed).toString('base64') };
|
|
4203
|
+
try {
|
|
4204
|
+
fs.mkdirSync(path.dirname(GROKROOM_ID_FILE), { recursive: true });
|
|
4205
|
+
fs.writeFileSync(GROKROOM_ID_FILE, JSON.stringify(id, null, 2));
|
|
4206
|
+
} catch { /* in-memory is fine */ }
|
|
4207
|
+
}
|
|
4208
|
+
grokroomIdCache = id;
|
|
4209
|
+
return id;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
async function grokroomPost(roomId, body, { from: fromRaw } = {}) {
|
|
4213
|
+
const id = grokroomIdentity();
|
|
4214
|
+
const ts = Date.now();
|
|
4215
|
+
const from = grokroomFromHandle(fromRaw || GROKROOM_HANDLE, GROKROOM_HANDLE);
|
|
4216
|
+
const pub = id.pub;
|
|
4217
|
+
const room = String(roomId || '');
|
|
4218
|
+
const canonical = JSON.stringify({ body, from, pub, room, ts });
|
|
4219
|
+
const key = createPrivateKey({ key: Buffer.concat([GROKROOM_PKCS8_PREFIX, Buffer.from(id.seed, 'base64')]), format: 'der', type: 'pkcs8' });
|
|
4220
|
+
const sig = grokroomB58(cryptoSign(null, Buffer.from(canonical, 'utf8'), key));
|
|
4221
|
+
const payload = JSON.stringify({ room, from, pub, body, ts, sig });
|
|
4222
|
+
const endpoints = [];
|
|
4223
|
+
if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(room)) {
|
|
4224
|
+
endpoints.push(GROKROOM_PAYMASTER + '/api/room/submit');
|
|
4225
|
+
}
|
|
4226
|
+
endpoints.push(GROKROOM_API + '/submit');
|
|
4227
|
+
let last = new Error('no submit endpoint');
|
|
4228
|
+
for (const url of endpoints) {
|
|
4229
|
+
try {
|
|
4230
|
+
const r = await fetch(url, {
|
|
4231
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
4232
|
+
body: payload,
|
|
4233
|
+
signal: AbortSignal.timeout(30000),
|
|
4234
|
+
});
|
|
4235
|
+
const j = await r.json().catch(() => ({}));
|
|
4236
|
+
if (r.ok && j.ok !== false) return j;
|
|
4237
|
+
last = new Error(j.error || ('submit HTTP ' + r.status));
|
|
4238
|
+
} catch (e) {
|
|
4239
|
+
last = e;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
throw last;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
function grokroomLookup(agentId) {
|
|
4246
|
+
const list = cachedAgentList() || [];
|
|
4247
|
+
return list.find((a) => a.id === agentId && isGrokRoomAgent(a)) || null;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
function grokroomUpsertAgent(parsed, { select = false } = {}) {
|
|
4251
|
+
const id = String(parsed.id);
|
|
4252
|
+
const prev = (cachedAgentList() || []).find((a) => a.id === id) || {};
|
|
4253
|
+
const agent = shapeAgent({
|
|
4254
|
+
...prev,
|
|
4255
|
+
...parsed,
|
|
4256
|
+
id,
|
|
4257
|
+
createdAt: prev.createdAt || parsed.createdAt || Date.now(),
|
|
4258
|
+
updatedAt: Date.now(),
|
|
4259
|
+
});
|
|
4260
|
+
const list = cachedAgentList() || [];
|
|
4261
|
+
const i = list.findIndex((a) => a.id === id);
|
|
4262
|
+
if (i < 0) list.unshift(agent);
|
|
4263
|
+
else list[i] = { ...list[i], ...agent };
|
|
4264
|
+
saveAgents(list.map(shapeAgent));
|
|
4265
|
+
agentTranscript(id);
|
|
4266
|
+
if (agent.hidden) {
|
|
4267
|
+
const visible = rosterForEvent(list, agentActivity);
|
|
4268
|
+
ssePush('agents', { agents: visible, activeAgentId: focusedAgentId || visible[0]?.id || id });
|
|
4269
|
+
} else {
|
|
4270
|
+
pushCreatedAgent(agent, { select });
|
|
4271
|
+
}
|
|
4272
|
+
return agent;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
function grokroomMemberEnsure(group, name, { speak = false } = {}) {
|
|
4276
|
+
const mid = grokroomMemberId(name);
|
|
4277
|
+
const list = cachedAgentList() || [];
|
|
4278
|
+
let member = list.find((a) => a.id === mid);
|
|
4279
|
+
if (!member) {
|
|
4280
|
+
member = grokroomUpsertAgent({
|
|
4281
|
+
id: mid,
|
|
4282
|
+
name: String(name || 'anon').slice(0, 80),
|
|
4283
|
+
hidden: true,
|
|
4284
|
+
hiddenFromSidebar: true,
|
|
4285
|
+
brief: '',
|
|
4286
|
+
description: '',
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
if (!speak) return group;
|
|
4290
|
+
const have = group.memberIds || [];
|
|
4291
|
+
if (have.includes(mid)) return group;
|
|
4292
|
+
return grokroomUpsertAgent({
|
|
4293
|
+
id: group.id,
|
|
4294
|
+
name: group.name,
|
|
4295
|
+
isGroup: true,
|
|
4296
|
+
memberIds: [...have, mid],
|
|
4297
|
+
brief: '',
|
|
4298
|
+
description: '',
|
|
4299
|
+
room: group.room,
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
function joinSharedGroup(addr, { name } = {}) {
|
|
4304
|
+
const web = grokroomShareUrl(addr);
|
|
4305
|
+
if (!web) return null;
|
|
4306
|
+
const list = cachedAgentList() || [];
|
|
4307
|
+
const existing = list.find((a) => a?.room?.addr === addr);
|
|
4308
|
+
if (existing) return existing;
|
|
4309
|
+
const raw = mintLocalAgent({
|
|
4310
|
+
isGroup: true,
|
|
4311
|
+
name: String(name || `group ${addr.slice(0, 6)}`).slice(0, 80),
|
|
4312
|
+
memberIds: [],
|
|
4313
|
+
room: { id: addr, addr, web },
|
|
4314
|
+
});
|
|
4315
|
+
return mintLocalAgent(bindOnchainRoom(raw, addr, HOME));
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
function paintShareIfMissing(agent, log = () => {}) {
|
|
4319
|
+
const url = agentShareUrl(agent);
|
|
4320
|
+
if (!agent?.id || !url) return false;
|
|
4321
|
+
const t = agentTranscript(agent.id);
|
|
4322
|
+
const has = (t.entries || []).some((e) => /openzoo\.fun\/r\//.test(String(e?.content || '')));
|
|
4323
|
+
if (has) return false;
|
|
4324
|
+
const nonce = `oz-share-${agent.id}`;
|
|
4325
|
+
const line = fanoutLine(agent.id, 'assistant', 'Share this room: ' + url, {
|
|
4326
|
+
clientNonce: nonce,
|
|
4327
|
+
requestId: nonce,
|
|
4328
|
+
});
|
|
4329
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4330
|
+
log(`cursor-backend: group share painted agent=${agent.id}`);
|
|
4331
|
+
return true;
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
async function paintGroupShareHint(agent, log = () => {}) {
|
|
4335
|
+
if (!agent?.id) return;
|
|
4336
|
+
let rooms = grokroomSync._rooms;
|
|
4337
|
+
if (!Array.isArray(rooms)) {
|
|
4338
|
+
try {
|
|
4339
|
+
const st = await fetch(GROKROOM_API + '/state', { signal: AbortSignal.timeout(4000) }).then((r) => r.json());
|
|
4340
|
+
rooms = Array.isArray(st.rooms) ? st.rooms : [];
|
|
4341
|
+
grokroomSync._rooms = rooms;
|
|
4342
|
+
} catch { rooms = []; }
|
|
4343
|
+
}
|
|
4344
|
+
const share = groupShareState({ agent, rooms });
|
|
4345
|
+
let text = '';
|
|
4346
|
+
if (share.url) {
|
|
4347
|
+
text = 'Share this room on openzoo.fun: ' + share.url;
|
|
4348
|
+
} else if (share.rooms.length) {
|
|
4349
|
+
text = 'This group is local to this house. Public grokrooms — send this URL:\n'
|
|
4350
|
+
+ share.rooms.map((r) => `${r.name} ${r.url}`).join('\n');
|
|
4351
|
+
} else {
|
|
4352
|
+
text = 'This group is local to this house. To share on openzoo.fun, use a grokroom (# main / # bots / # random / # lobby). Start ~/grokroom on :4799 if those rows are missing.';
|
|
4353
|
+
}
|
|
4354
|
+
const line = fanoutLine(agent.id, 'assistant', text, {});
|
|
4355
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4356
|
+
log(`cursor-backend: group share hint agent=${agent.id}`);
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
function grokroomEnsureGroup(roomId, name, addr) {
|
|
4360
|
+
const id = grokroomAgentId(roomId);
|
|
4361
|
+
if (!id) return null;
|
|
4362
|
+
const list = cachedAgentList() || [];
|
|
4363
|
+
const prev = list.find((a) => a.id === id || a.room?.id === roomId);
|
|
4364
|
+
const nextAddr = addr || prev?.room?.addr || '';
|
|
4365
|
+
const web = grokroomShareUrl(nextAddr);
|
|
4366
|
+
if (prev) {
|
|
4367
|
+
const need = (nextAddr && prev.room && prev.room.addr !== nextAddr)
|
|
4368
|
+
|| (web && prev.room && prev.room.web !== web);
|
|
4369
|
+
const agent = need
|
|
4370
|
+
? grokroomUpsertAgent({ ...prev, room: { id: roomId, addr: nextAddr, web } }, { select: false })
|
|
4371
|
+
: prev;
|
|
4372
|
+
paintShareIfMissing(agent);
|
|
4373
|
+
return agent;
|
|
4374
|
+
}
|
|
4375
|
+
const agent = grokroomUpsertAgent({
|
|
4376
|
+
id,
|
|
4377
|
+
name: name || ('# ' + roomId),
|
|
4378
|
+
isGroup: true,
|
|
4379
|
+
memberIds: [],
|
|
4380
|
+
brief: '',
|
|
4381
|
+
description: '',
|
|
4382
|
+
room: { id: roomId, addr: nextAddr, web },
|
|
4383
|
+
}, { select: false });
|
|
4384
|
+
const nonce = `oz-room-${id}`;
|
|
4385
|
+
grokroomMemberEnsure(agent, 'room');
|
|
4386
|
+
const text = 'Linked to ' + agent.name + ' on Solana testnet. Every message here is a signed memo on-chain — the chain is the log.'
|
|
4387
|
+
+ (web ? ' Share: ' + web : '')
|
|
4388
|
+
+ (nextAddr ? ' Explorer: https://solscan.io/account/' + nextAddr + '?cluster=testnet' : '');
|
|
4389
|
+
const line = fanoutLine(id, 'assistant', text, {
|
|
4390
|
+
clientNonce: nonce,
|
|
4391
|
+
requestId: nonce,
|
|
4392
|
+
author: { kind: 'agent', id: grokroomMemberId('room'), name: 'room' },
|
|
4393
|
+
});
|
|
4394
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: id });
|
|
4395
|
+
return agent;
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
function grokroomSend(agent, body, log = () => {}, { from } = {}) {
|
|
4399
|
+
const text = stripSpendFooter(String(body || '')).trim().slice(0, 400);
|
|
4400
|
+
const roomKey = agent?.room?.addr || agent?.room?.id;
|
|
4401
|
+
if (!text || !roomKey) return;
|
|
4402
|
+
if (/^PASS\b/i.test(text)) return;
|
|
4403
|
+
grokroomPost(roomKey, text, { from }).then((j) => {
|
|
4404
|
+
log(`cursor-backend: grokroom posted room=${String(roomKey).slice(0, 8)}… sig=${String(j?.sig || '').slice(0, 12)} from=${from || GROKROOM_HANDLE}`);
|
|
4405
|
+
}).catch((e) => {
|
|
4406
|
+
grokroomMemberEnsure(agent, 'room');
|
|
4407
|
+
const line = fanoutLine(agent.id, 'assistant', '⚠ could not reach the room: ' + e.message, {
|
|
4408
|
+
author: { kind: 'agent', id: grokroomMemberId('room'), name: 'room' },
|
|
4409
|
+
});
|
|
4410
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4411
|
+
log(`cursor-backend: grokroom send fail ${roomKey} ${e.message}`);
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
function grokroomIngestMsgs(group, msgs, log = () => {}) {
|
|
4416
|
+
if (!group?.id || !Array.isArray(msgs) || !msgs.length) return 0;
|
|
4417
|
+
let agent = group;
|
|
4418
|
+
const names = new Set();
|
|
4419
|
+
for (const m of msgs) {
|
|
4420
|
+
const from = String(m.from || m.node || '').slice(0, 24);
|
|
4421
|
+
if (from) names.add(from);
|
|
4422
|
+
}
|
|
4423
|
+
for (const from of names) agent = grokroomMemberEnsure(agent, from);
|
|
4424
|
+
const t = agentTranscript(agent.id);
|
|
4425
|
+
const seen = new Set((t.entries || []).filter((h) => h.sig).map((h) => h.sig));
|
|
4426
|
+
const recent = (t.entries || []).slice(-80).map((h) => String(h.content || ''));
|
|
4427
|
+
let dirty = 0;
|
|
4428
|
+
for (const m of msgs) {
|
|
4429
|
+
const msig = m.sig || m.txSig || m.envsig || (m.pub && m.ts ? m.pub + ':' + m.ts : null);
|
|
4430
|
+
if (!msig || seen.has(msig)) continue;
|
|
4431
|
+
seen.add(msig);
|
|
4432
|
+
const from = String(m.from || m.node || 'anon').slice(0, 24);
|
|
4433
|
+
const body = String(m.body || m.text || '');
|
|
4434
|
+
if (!body) continue;
|
|
4435
|
+
if (recent.includes(body)) continue;
|
|
4436
|
+
agent = grokroomMemberEnsure(agent, from);
|
|
4437
|
+
const mid = grokroomMemberId(from);
|
|
4438
|
+
const line = fanoutLine(agent.id, 'assistant', body, {
|
|
4439
|
+
author: { kind: 'agent', id: mid, name: from },
|
|
4440
|
+
sig: msig,
|
|
4441
|
+
});
|
|
4442
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4443
|
+
dirty += 1;
|
|
4444
|
+
}
|
|
4445
|
+
if (dirty) {
|
|
4446
|
+
bumpAgent(agent.id, { preview: msgs[msgs.length - 1]?.body || '', notify: true, select: false });
|
|
4447
|
+
log(`cursor-backend: grokroom ${agent.room?.id || agent.id} +${dirty} bubbles`);
|
|
4448
|
+
}
|
|
4449
|
+
return dirty;
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
async function grokroomSyncPaymaster(log = () => {}) {
|
|
4453
|
+
const list = cachedAgentList() || [];
|
|
4454
|
+
const seen = new Set();
|
|
4455
|
+
for (const a of list) {
|
|
4456
|
+
const addr = a?.room?.addr;
|
|
4457
|
+
if (!grokroomShareUrl(addr) || seen.has(addr)) continue;
|
|
4458
|
+
seen.add(addr);
|
|
4459
|
+
try {
|
|
4460
|
+
const r = await fetch(`${GROKROOM_PAYMASTER}/api/room/history?addr=${encodeURIComponent(addr)}`, {
|
|
4461
|
+
signal: AbortSignal.timeout(8000),
|
|
4462
|
+
});
|
|
4463
|
+
if (!r.ok) continue;
|
|
4464
|
+
const j = await r.json().catch(() => ({}));
|
|
4465
|
+
const msgs = Array.isArray(j.msgs) ? j.msgs : [];
|
|
4466
|
+
grokroomIngestMsgs(a, msgs, log);
|
|
4467
|
+
} catch (e) {
|
|
4468
|
+
log(`cursor-backend: grokroom history ${String(addr).slice(0, 8)}… ${e.message}`);
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
let grokroomBusy = false;
|
|
4474
|
+
async function grokroomSync(log = () => {}) {
|
|
4475
|
+
if (grokroomBusy) return;
|
|
4476
|
+
grokroomBusy = true;
|
|
4477
|
+
try {
|
|
4478
|
+
try {
|
|
4479
|
+
const stRes = await fetch(GROKROOM_API + '/state', { signal: AbortSignal.timeout(4000) });
|
|
4480
|
+
if (stRes.ok) {
|
|
4481
|
+
const st = await stRes.json();
|
|
4482
|
+
const rooms = Array.isArray(st.rooms) ? st.rooms : [];
|
|
4483
|
+
grokroomSync._rooms = rooms;
|
|
4484
|
+
if (!grokroomSync._logged) {
|
|
4485
|
+
grokroomSync._logged = true;
|
|
4486
|
+
log(`cursor-backend: grokroom sync tick: rooms=${rooms.map((r) => r.id).join(',')}`);
|
|
4487
|
+
}
|
|
4488
|
+
for (const r of rooms) {
|
|
4489
|
+
const roomId = r.id || r.name;
|
|
4490
|
+
if (!roomId) continue;
|
|
4491
|
+
const group = grokroomEnsureGroup(roomId, r.name, r.addr || r.address);
|
|
4492
|
+
if (!group) continue;
|
|
4493
|
+
const feedRes = await fetch(GROKROOM_API + '/feed?room=' + encodeURIComponent(roomId), { signal: AbortSignal.timeout(4000) });
|
|
4494
|
+
if (!feedRes.ok) continue;
|
|
4495
|
+
const feed = await feedRes.json();
|
|
4496
|
+
grokroomIngestMsgs(group, Array.isArray(feed.messages) ? feed.messages : [], log);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
} catch (e) {
|
|
4500
|
+
if (!grokroomSync._localMiss) {
|
|
4501
|
+
grokroomSync._localMiss = true;
|
|
4502
|
+
log(`cursor-backend: grokroom local :4799 down (${e.message}); paymaster only`);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
await grokroomSyncPaymaster(log);
|
|
4506
|
+
} catch (e) {
|
|
4507
|
+
log(`cursor-backend: grokroom sync: ${e.message}`);
|
|
4508
|
+
} finally {
|
|
4509
|
+
grokroomBusy = false;
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function startGrokroomBridge(log = () => {}) {
|
|
4514
|
+
if (startGrokroomBridge._on) return;
|
|
4515
|
+
if (process.env.OZ_GROKROOM === '0') {
|
|
4516
|
+
log('cursor-backend: grokroom bridge off (OZ_GROKROOM=0)');
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
startGrokroomBridge._on = true;
|
|
4520
|
+
const tick = () => grokroomSync(log);
|
|
4521
|
+
setTimeout(tick, 800);
|
|
4522
|
+
setInterval(tick, 4000);
|
|
4523
|
+
}
|
|
4524
|
+
|
|
3802
4525
|
/**
|
|
3803
4526
|
* Start the impersonation server. Binds `port` (default 8443 — a privileged
|
|
3804
4527
|
* 443->port redirect is installed alongside the /etc/hosts entry, so this stays
|
|
3805
4528
|
* unprivileged). Returns { server, port }.
|
|
3806
4529
|
*/
|
|
3807
|
-
export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
4530
|
+
export async function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
3808
4531
|
restoreAgentWakeups(log);
|
|
3809
|
-
|
|
4532
|
+
startGrokroomBridge(log);
|
|
4533
|
+
const { cert, key } = await ensureCert(log);
|
|
3810
4534
|
const certPem = fs.readFileSync(cert);
|
|
3811
4535
|
const keyPem = fs.readFileSync(key);
|
|
3812
4536
|
const secureContext = tls.createSecureContext({ cert: certPem, key: keyPem });
|
|
@@ -3895,6 +4619,21 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
3895
4619
|
return;
|
|
3896
4620
|
}
|
|
3897
4621
|
log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
|
|
4622
|
+
// HIJACK LOGIN. Measured Linux 2026-09-03: GET /auth/poll -> empty-ok
|
|
4623
|
+
// (`{}`) so SandBackendLoginManager threw bad_200 and the UI stayed on
|
|
4624
|
+
// "Log in with Cursor". Poll must return accessToken+refreshToken JWT.
|
|
4625
|
+
// /oauth/token refresh is the same session — do NOT passthrough fake
|
|
4626
|
+
// tokens to real api2. Sniff/passthru keeps the real issuer.
|
|
4627
|
+
{
|
|
4628
|
+
const fakeAuth = fakeCursorAuthReply(full, { passthrough: sniffOn() || process.env.OPENZOO_PASSTHRU === '1' });
|
|
4629
|
+
if (fakeAuth) {
|
|
4630
|
+
if (req.method === 'OPTIONS') { res.writeHead(204, CORS); res.end(); return; }
|
|
4631
|
+
res.writeHead(200, { 'content-type': fakeAuth.contentType, ...CORS });
|
|
4632
|
+
res.end(fakeAuth.body);
|
|
4633
|
+
log(`cursor-backend: -> FAKE ${fakeAuth.kind} logged-in (no Cursor account)`);
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
3898
4637
|
// OAUTH MUST HIT THE REAL ISSUER. empty-ok on /oauth/token is the
|
|
3899
4638
|
// "Reconnecting to your computer" / edge/handler-failed loop: the app
|
|
3900
4639
|
// retries the same 509b body forever and never reaches AvailableModels
|