openzoo 0.50.97 → 0.50.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cursor-backend.js +1 -1
- package/bin/openzoo.js +9 -0
- package/lib/cursorapi.js +65 -0
- package/lib/cursorbackend.js +879 -77
- package/lib/grokbotAccount.js +192 -3
- package/lib/grokbotFetch.js +258 -0
- package/lib/grokbotweb.js +23 -7
- package/lib/grokcli.js +454 -44
- package/lib/grokui.mjs +6524 -0
- package/lib/launch.js +1 -1
- package/lib/models.js +11 -1
- package/lib/openzooPathShim.js +60 -0
- package/lib/ozSpendChip.js +80 -3
- package/lib/pay.js +30 -2
- package/lib/podagent.mjs +3 -1
- package/lib/proxy.js +17 -2
- package/lib/setup.js +1 -1
- package/package.json +1 -1
- package/lib/cursorbackend.js.bak-hiddenbots +0 -4072
package/lib/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 };
|
|
@@ -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
|
|
@@ -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,64 @@ 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
|
|
1090
|
+
localExecWaiters.delete(id);
|
|
1091
|
+
} else if (shellStreamOf(f)) {
|
|
1092
|
+
// The real Grok Bot lane: stdout/stderr arrive as many `kind:"client"`
|
|
1093
|
+
// frames and the turn is only over on `exit`. Resolving on the first one
|
|
1094
|
+
// (what the old client/control branch did) truncated every command to its
|
|
1095
|
+
// first chunk.
|
|
1096
|
+
const ss = shellStreamOf(f);
|
|
1097
|
+
// ShellStreamStdout/Stderr carry exactly one field, `data`.
|
|
1098
|
+
const so = oneofCase(ss, 'stdout');
|
|
1099
|
+
const se = oneofCase(ss, 'stderr');
|
|
1100
|
+
if (typeof so?.data === 'string') w.chunks += so.data;
|
|
1101
|
+
if (typeof se?.data === 'string') w.stderr += se.data;
|
|
1102
|
+
const exit = oneofCase(ss, 'exit');
|
|
1103
|
+
if (exit) {
|
|
1104
|
+
w.resolve({
|
|
1105
|
+
kind: 'exit',
|
|
1106
|
+
message: w.chunks || w.stderr,
|
|
1107
|
+
stdout: w.chunks,
|
|
1108
|
+
stderr: w.stderr,
|
|
1109
|
+
exitCode: Number(exit.code || 0),
|
|
1110
|
+
// The daemon spools a command's output to a terminal .txt under the
|
|
1111
|
+
// hello frame's terminalsFolder and only names it here. Big or
|
|
1112
|
+
// fast-finishing commands stream NO chunks at all, which is how
|
|
1113
|
+
// `echo hi; pwd` came back as a bare `(exit 0)`.
|
|
1114
|
+
outputPath: exitOutputPath(exit),
|
|
1115
|
+
});
|
|
1116
|
+
localExecWaiters.delete(id);
|
|
1117
|
+
}
|
|
1118
|
+
} else if (f.kind === 'control' && f.message && f.message.heartbeat) {
|
|
1119
|
+
// 3s keepalive while a long command runs. It is a `control` frame like
|
|
1120
|
+
// any other, so the generic branch below used to resolve the call with an
|
|
1121
|
+
// empty body the moment the daemon first breathed.
|
|
1122
|
+
continue;
|
|
1123
|
+
} else if (f.kind === 'client' || f.kind === 'control') {
|
|
1124
|
+
// The daemon's response schema is exactly: hello, client, control, file,
|
|
1125
|
+
// file-error, messages-result, messages-error, ping. `result`,
|
|
1126
|
+
// `exec-result`, `output`, `exit` were never wire kinds — reverse-
|
|
1127
|
+
// engineered from dist/local-exec-daemon/main.cjs, 0.36.0.
|
|
1128
|
+
const norm = normalizeExecFrame(f);
|
|
1129
|
+
const stdout = w.chunks + (norm.stdout || '');
|
|
1130
|
+
const stderr = w.stderr + (norm.stderr || '');
|
|
1007
1131
|
w.resolve({
|
|
1008
1132
|
kind: f.kind,
|
|
1009
|
-
message:
|
|
1010
|
-
stdout
|
|
1011
|
-
stderr:
|
|
1012
|
-
exitCode:
|
|
1133
|
+
message: norm.error || norm.message || stdout || toolResultText(f),
|
|
1134
|
+
stdout,
|
|
1135
|
+
stderr: norm.error && !stderr ? norm.error : stderr,
|
|
1136
|
+
exitCode: norm.exitCode,
|
|
1013
1137
|
});
|
|
1014
|
-
localExecWaiters.delete(
|
|
1138
|
+
localExecWaiters.delete(id);
|
|
1015
1139
|
}
|
|
1016
1140
|
}
|
|
1017
1141
|
}
|
|
@@ -1024,6 +1148,8 @@ function localExecAsk(frame, timeoutMs = 45000) {
|
|
|
1024
1148
|
reject(new Error('local-exec timeout — is Grok Bot Helper connected?'));
|
|
1025
1149
|
}, timeoutMs);
|
|
1026
1150
|
localExecWaiters.set(requestId, {
|
|
1151
|
+
chunks: '',
|
|
1152
|
+
stderr: '',
|
|
1027
1153
|
resolve: (v) => { clearTimeout(t); resolve(v); },
|
|
1028
1154
|
reject: (e) => { clearTimeout(t); reject(e); },
|
|
1029
1155
|
});
|
|
@@ -1069,9 +1195,25 @@ function extractLocalPaths(text) {
|
|
|
1069
1195
|
}
|
|
1070
1196
|
return [...new Set(out)];
|
|
1071
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* `~` is a path, not just a prefix.
|
|
1200
|
+
*
|
|
1201
|
+
* This matched `~/` ONLY, so a BARE `~` fell through untouched and every
|
|
1202
|
+
* caller then hit the literal directory named "~". MEASURED 2026-09-03 from a
|
|
1203
|
+
* user typing `ls -lha ~`: the model called `list_dir "~"` and the canvas
|
|
1204
|
+
* answered `"~": No such file or directory (os error 2)`, twice, before
|
|
1205
|
+
* giving up and running `ls -lha` in /Users instead — so the user asked for
|
|
1206
|
+
* their home directory and was shown someone else's.
|
|
1207
|
+
*
|
|
1208
|
+
* Handles `~`, `~/x`, and (on Windows, where the model still emits unix-ish
|
|
1209
|
+
* paths) `~\x`. A `~user` form is deliberately NOT expanded: guessing another
|
|
1210
|
+
* account's home is worse than leaving the path alone.
|
|
1211
|
+
*/
|
|
1072
1212
|
function expandUserPath(p) {
|
|
1073
|
-
|
|
1074
|
-
return
|
|
1213
|
+
const s = String(p ?? '');
|
|
1214
|
+
if (s === '~') return os.homedir();
|
|
1215
|
+
if (s.startsWith('~/') || s.startsWith('~\\')) return path.join(os.homedir(), s.slice(2));
|
|
1216
|
+
return s;
|
|
1075
1217
|
}
|
|
1076
1218
|
|
|
1077
1219
|
/** Per-agent transcript. sendPrompt is async: accept immediately, zooComplete
|
|
@@ -1156,6 +1298,7 @@ function appendLine(agentId, role, text, extra = {}) {
|
|
|
1156
1298
|
requestId,
|
|
1157
1299
|
...(extra.author && typeof extra.author === 'object' ? { author: extra.author } : {}),
|
|
1158
1300
|
...(extra.ephemeral ? { ephemeral: true } : {}),
|
|
1301
|
+
...(extra.sig ? { sig: String(extra.sig) } : {}),
|
|
1159
1302
|
};
|
|
1160
1303
|
}
|
|
1161
1304
|
t.entries.push(e);
|
|
@@ -1324,14 +1467,35 @@ export function restoreAgentWakeups(log = () => {}) {
|
|
|
1324
1467
|
return ids.length;
|
|
1325
1468
|
}
|
|
1326
1469
|
|
|
1470
|
+
/**
|
|
1471
|
+
* What to call the machine the bot is driving.
|
|
1472
|
+
*
|
|
1473
|
+
* Both progress strings said "your Mac" unconditionally, so a Windows or Linux
|
|
1474
|
+
* user watched the canvas narrate work on a Mac they do not own. The bot runs
|
|
1475
|
+
* wherever node runs; the noun has to follow `process.platform`.
|
|
1476
|
+
*/
|
|
1477
|
+
export function hostNoun(platform = process.platform) {
|
|
1478
|
+
if (platform === 'darwin') return 'Mac';
|
|
1479
|
+
if (platform === 'win32') return 'PC';
|
|
1480
|
+
return 'machine';
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1327
1483
|
export function formatZooProgress({ step, maxSteps, names, command } = {}) {
|
|
1328
1484
|
const tools = Array.isArray(names) ? names.filter(Boolean).join(', ') : String(names || 'tools');
|
|
1329
1485
|
const cmd = command ? ` ${JSON.stringify(String(command).slice(0, 80))}` : '';
|
|
1330
|
-
return `Working on your
|
|
1486
|
+
return `Working on your ${hostNoun()} (step ${Number(step) + 1}/${maxSteps || '?'}): ${tools}${cmd}`;
|
|
1331
1487
|
}
|
|
1332
1488
|
|
|
1333
1489
|
/** One canvas line per tool. Asar ingest is append-only — mutating a working
|
|
1334
1490
|
* bubble is still silence. Keep it short; the model history skips ephemeral. */
|
|
1491
|
+
export function toolResultText(result) {
|
|
1492
|
+
if (result == null) return '';
|
|
1493
|
+
if (typeof result === 'string') return result;
|
|
1494
|
+
if (typeof result === 'number' || typeof result === 'boolean') return String(result);
|
|
1495
|
+
if (Buffer.isBuffer(result)) return result.toString('utf8');
|
|
1496
|
+
try { return JSON.stringify(result); } catch { return String(result); }
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1335
1499
|
export function formatZooToolLine({ name, args = {}, result } = {}) {
|
|
1336
1500
|
const detail = args.command || args.path || args.name || args.window
|
|
1337
1501
|
|| args.query || args.url || args.key || args.app || args.every
|
|
@@ -1340,7 +1504,7 @@ export function formatZooToolLine({ name, args = {}, result } = {}) {
|
|
|
1340
1504
|
const head = detail
|
|
1341
1505
|
? `${name} ${JSON.stringify(String(detail).slice(0, 90))}`
|
|
1342
1506
|
: String(name || 'tool');
|
|
1343
|
-
let tail =
|
|
1507
|
+
let tail = toolResultText(result).replace(/\s+/g, ' ').trim();
|
|
1344
1508
|
if (tail.startsWith('{')) {
|
|
1345
1509
|
try {
|
|
1346
1510
|
const o = JSON.parse(String(result));
|
|
@@ -1452,6 +1616,7 @@ function kickBriefedAgent(agent, log = () => {}) {
|
|
|
1452
1616
|
|
|
1453
1617
|
function seedBriefOnCanvas(agent, log) {
|
|
1454
1618
|
if (!agent?.id || !String(agent.brief || '').trim()) return false;
|
|
1619
|
+
if (isGrokRoomAgent(agent) || isGrokRoomMember(agent)) return false;
|
|
1455
1620
|
const t = agentTranscript(agent.id);
|
|
1456
1621
|
if (!(t.entries || []).length) {
|
|
1457
1622
|
const nonce = `oz-brief-${agent.id}`;
|
|
@@ -1472,6 +1637,7 @@ let payNudged = false;
|
|
|
1472
1637
|
/** The terminal banner, on the canvas too: a new user reads the chat, not the shell. */
|
|
1473
1638
|
async function seedPayNudge(list, activeId, log = () => {}) {
|
|
1474
1639
|
if (payNudged || !activeId) return false;
|
|
1640
|
+
if (isGrokRoomAgent({ id: activeId }) || /^roombot-/.test(String(activeId))) return false;
|
|
1475
1641
|
payNudged = true;
|
|
1476
1642
|
try {
|
|
1477
1643
|
const t = agentTranscript(activeId);
|
|
@@ -1494,6 +1660,7 @@ async function seedPayNudge(list, activeId, log = () => {}) {
|
|
|
1494
1660
|
let shipNudged = false;
|
|
1495
1661
|
function seedShipNudge(list, activeId) {
|
|
1496
1662
|
if (shipNudged || !activeId) return false;
|
|
1663
|
+
if (isGrokRoomAgent({ id: activeId }) || /^roombot-/.test(String(activeId))) return false;
|
|
1497
1664
|
const roster = Array.isArray(list) ? list : [];
|
|
1498
1665
|
if (roster.some((a) => /^firstmate$/i.test(String(a?.name || '')))) { shipNudged = true; return false; }
|
|
1499
1666
|
const t = agentTranscript(activeId);
|
|
@@ -1514,6 +1681,25 @@ function groupMemberRecords(agentId) {
|
|
|
1514
1681
|
return groupMemberIds(agentId).map((mid) => {
|
|
1515
1682
|
const m = roster.find((x) => x.id === mid);
|
|
1516
1683
|
return { id: mid, name: String(m?.name || mid) };
|
|
1684
|
+
}).filter((m) => !/^roombot-/.test(m.id) && !isGrokRoomMember({ id: m.id }));
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
export function groupNormText(text) {
|
|
1688
|
+
return stripSpendFooter(String(text || ''))
|
|
1689
|
+
.replace(/^\s*[A-Za-z0-9 _.-]{1,40}:\s*/, '')
|
|
1690
|
+
.toLowerCase()
|
|
1691
|
+
.replace(/\s+/g, ' ')
|
|
1692
|
+
.trim();
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
export function groupReplyIsRepeat(text, recent = []) {
|
|
1696
|
+
const n = groupNormText(text);
|
|
1697
|
+
if (n.length < 40) return false;
|
|
1698
|
+
const head = n.slice(0, 90);
|
|
1699
|
+
return (Array.isArray(recent) ? recent : []).some((prev) => {
|
|
1700
|
+
const p = groupNormText(prev);
|
|
1701
|
+
if (p.length < 40) return false;
|
|
1702
|
+
return p.slice(0, 90) === head || p.startsWith(head) || n.startsWith(p.slice(0, 90));
|
|
1517
1703
|
});
|
|
1518
1704
|
}
|
|
1519
1705
|
|
|
@@ -1522,24 +1708,29 @@ export function groupReplyIsPass(text) {
|
|
|
1522
1708
|
if (!s) return true;
|
|
1523
1709
|
if (/^PASS\b/i.test(s)) return true;
|
|
1524
1710
|
if (s.length < 20) return true;
|
|
1711
|
+
if (/\binfinite\b.{0,48}\b(recursion|loop|chatter)\b/i.test(s)) return true;
|
|
1712
|
+
if (/\bbots? keep triggering\b/i.test(s)) return true;
|
|
1713
|
+
if (/\beach other'?s responses\b/i.test(s)) return true;
|
|
1714
|
+
if (/\bfeedback loop\b/i.test(s)) return true;
|
|
1525
1715
|
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
1716
|
}
|
|
1527
1717
|
|
|
1528
|
-
const GROUP_MAX_ROUNDS = Math.min(12, Math.max(
|
|
1529
|
-
const GROUP_MAX_CALLS = Math.min(24, Math.max(
|
|
1718
|
+
const GROUP_MAX_ROUNDS = Math.min(12, Math.max(1, Number(process.env.OZ_GROUP_MAX_ROUNDS || 2)));
|
|
1719
|
+
const GROUP_MAX_CALLS = Math.min(24, Math.max(2, Number(process.env.OZ_GROUP_MAX_CALLS || 6)));
|
|
1530
1720
|
|
|
1531
|
-
/** Members speak, then
|
|
1721
|
+
/** Members speak once, then at most a short peek. Repeats and loop-talk are PASS. */
|
|
1532
1722
|
async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
1533
1723
|
const named = groupMemberRecords(agentId);
|
|
1534
1724
|
if (!named.length) return false;
|
|
1535
1725
|
const names = named.map((m) => m.name);
|
|
1536
1726
|
let calls = 0;
|
|
1727
|
+
const painted = [];
|
|
1537
1728
|
|
|
1538
1729
|
const oneTurn = async (m, phase) => {
|
|
1539
1730
|
const others = names.filter((n) => n !== m.name).join(', ') || 'the group';
|
|
1540
1731
|
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
|
|
1732
|
+
? `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.`
|
|
1733
|
+
: `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
1734
|
let bit = '';
|
|
1544
1735
|
try {
|
|
1545
1736
|
bit = (await zooComplete(persona, log, agentId, parsed)).text;
|
|
@@ -1548,7 +1739,7 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1548
1739
|
log(`cursor-backend: group ${phase} ${m.id} failed: ${e.message}`);
|
|
1549
1740
|
}
|
|
1550
1741
|
calls += 1;
|
|
1551
|
-
const pass = groupReplyIsPass(bit);
|
|
1742
|
+
const pass = groupReplyIsPass(bit) || groupReplyIsRepeat(bit, painted);
|
|
1552
1743
|
if (!pass) {
|
|
1553
1744
|
const line = fanoutLine(agentId, 'assistant', bit, {
|
|
1554
1745
|
clientNonce: nonce,
|
|
@@ -1557,6 +1748,11 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1557
1748
|
});
|
|
1558
1749
|
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
1559
1750
|
bumpAgent(agentId, { preview: bit, notify: true });
|
|
1751
|
+
painted.push(bit);
|
|
1752
|
+
{
|
|
1753
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
1754
|
+
if (live?.room?.addr) grokroomSend(live, bit, log, { from: m.name });
|
|
1755
|
+
}
|
|
1560
1756
|
log(`cursor-backend: group ${phase} ${m.name} seq=${line.seq}`);
|
|
1561
1757
|
} else {
|
|
1562
1758
|
log(`cursor-backend: group ${phase} ${m.name} PASS`);
|
|
@@ -1569,19 +1765,28 @@ async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
|
1569
1765
|
await oneTurn(m, 'speak');
|
|
1570
1766
|
}
|
|
1571
1767
|
if (named.length < 2) return true;
|
|
1768
|
+
if (painted.length >= 2 && groupReplyIsRepeat(painted[painted.length - 1], painted.slice(0, -1))) {
|
|
1769
|
+
log(`cursor-backend: group stopped after speak (repeat) calls=${calls}`);
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1572
1772
|
|
|
1573
1773
|
let round = 0;
|
|
1574
1774
|
while (round < GROUP_MAX_ROUNDS && calls < GROUP_MAX_CALLS) {
|
|
1575
1775
|
round += 1;
|
|
1576
1776
|
let passes = 0;
|
|
1777
|
+
const before = painted.length;
|
|
1577
1778
|
for (const m of named) {
|
|
1578
1779
|
if (calls >= GROUP_MAX_CALLS) break;
|
|
1579
1780
|
if (await oneTurn(m, 'peek')) passes += 1;
|
|
1580
1781
|
}
|
|
1581
|
-
if (passes >= named.length) {
|
|
1782
|
+
if (passes >= named.length || painted.length === before) {
|
|
1582
1783
|
log(`cursor-backend: group concluded round=${round} calls=${calls}`);
|
|
1583
1784
|
break;
|
|
1584
1785
|
}
|
|
1786
|
+
if (painted.length >= 2 && groupReplyIsRepeat(painted[painted.length - 1], painted.slice(0, -1))) {
|
|
1787
|
+
log(`cursor-backend: group stopped peek (repeat) round=${round}`);
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1585
1790
|
}
|
|
1586
1791
|
log(`cursor-backend: group queue done rounds=${round} calls=${calls}`);
|
|
1587
1792
|
return true;
|
|
@@ -1603,7 +1808,7 @@ function mergeAgentLists(remote) {
|
|
|
1603
1808
|
return sortAgentsByActivity(filterDeleted(out, HOME));
|
|
1604
1809
|
}
|
|
1605
1810
|
function gatewayEntry(e) {
|
|
1606
|
-
const { seq, pulledRemote, promptRaw, ephemeral, ...rest } = e;
|
|
1811
|
+
const { seq, pulledRemote, promptRaw, ephemeral, sig, ...rest } = e;
|
|
1607
1812
|
return rest;
|
|
1608
1813
|
}
|
|
1609
1814
|
|
|
@@ -1935,6 +2140,9 @@ const MODEL_ALIASES = {
|
|
|
1935
2140
|
'x-ai/grok-4.6': 'grok-4.6',
|
|
1936
2141
|
glm: 'zai-org/glm-5.3-flash',
|
|
1937
2142
|
'glm-5': 'zai-org/glm-5.3-flash',
|
|
2143
|
+
'glm-5.2': 'zai-org/GLM-5.2',
|
|
2144
|
+
'z-ai/glm-5.2': 'zai-org/GLM-5.2',
|
|
2145
|
+
'zai-org/glm-5.2': 'zai-org/GLM-5.2',
|
|
1938
2146
|
'glm-5.3': 'zai-org/glm-5.3-flash',
|
|
1939
2147
|
'glm-5.3-flash': 'zai-org/glm-5.3-flash',
|
|
1940
2148
|
flash: 'zai-org/glm-5.3-flash',
|
|
@@ -1968,6 +2176,11 @@ async function resolveModelId(raw) {
|
|
|
1968
2176
|
if (!s) return null;
|
|
1969
2177
|
const lower = s.toLowerCase().replace(/^\/+/, '');
|
|
1970
2178
|
if (MODEL_ALIASES[lower]) return MODEL_ALIASES[lower];
|
|
2179
|
+
try {
|
|
2180
|
+
const ids = await zooModelIds();
|
|
2181
|
+
const mapped = resolveModel(s, ids);
|
|
2182
|
+
if (mapped) return mapped;
|
|
2183
|
+
} catch { /* catalog miss: keep explicit slash ids */ }
|
|
1971
2184
|
if (s.includes('/')) return s;
|
|
1972
2185
|
// No vendor prefix and no alias: the LIVE catalog decides, not this file.
|
|
1973
2186
|
// The zoo serves ids with no slash at all (abliterated-model*), and a
|
|
@@ -2062,33 +2275,131 @@ async function writeLocalBytes(abs, bytes, log) {
|
|
|
2062
2275
|
fs.writeFileSync(abs, buf);
|
|
2063
2276
|
return `wrote ${abs} (${buf.length} bytes)`;
|
|
2064
2277
|
}
|
|
2278
|
+
/** `serverMessage` is `agent.v1.ExecServerMessage` as protobuf-es JSON — a oneof,
|
|
2279
|
+
* so the key IS the case name. Two wrong shapes before this one:
|
|
2280
|
+
* `{command, cwd}` flat -> "No handler found for server message of type undefined"
|
|
2281
|
+
* `{shellArgs: {...}}` -> "No handler found for server message of type shellArgs"
|
|
2282
|
+
* `shellArgs` is a real case on the wire (field 2), but `buildLocalExecManager`
|
|
2283
|
+
* in Grok Bot 0.36.0's dist/local-exec-daemon/main.cjs registers exactly FOUR
|
|
2284
|
+
* handlers: shellStreamArgs, backgroundShellSpawnArgs, readArgs, lsArgs. The
|
|
2285
|
+
* one-shot shell case is simply not wired into the local-exec lane — the
|
|
2286
|
+
* streaming one is, and both carry the same `agent.v1.ShellArgs` payload.
|
|
2287
|
+
* `authorizedBy*` are read as `=== true` by the daemon's approval gate; without
|
|
2288
|
+
* them an unattended exec has nothing to approve it.
|
|
2289
|
+
*
|
|
2290
|
+
* `timeout` is MILLISECONDS. The daemon reads it through a function named
|
|
2291
|
+
* `resolveShellTimeoutMs` and uses the value verbatim, so the plausible-looking
|
|
2292
|
+
* `timeout: 120` (seconds, as lib/podagent.mjs had it) aborts the shell after
|
|
2293
|
+
* 120 ms: `echo hi; pwd` came back exit 0 with empty stdout and `ls -lha ~`
|
|
2294
|
+
* died mid-stream with "Cannot read properties of undefined (reading '3')".
|
|
2295
|
+
* 0 means "use the daemon's default", not "no timeout". */
|
|
2296
|
+
export function localExecFrame(command, cwd, timeoutMs = 120_000) {
|
|
2297
|
+
return {
|
|
2298
|
+
kind: 'exec',
|
|
2299
|
+
requestId: randomUUID(),
|
|
2300
|
+
approvalId: randomUUID(),
|
|
2301
|
+
authorizedByStanding: true,
|
|
2302
|
+
authorizedByApproval: true,
|
|
2303
|
+
serverMessage: {
|
|
2304
|
+
shellStreamArgs: { command: String(command), workingDirectory: cwd, timeout: timeoutMs },
|
|
2305
|
+
},
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
/** `agent.v1.ShellStream` off an ExecClientMessage: `{stdout:{data}}`,
|
|
2310
|
+
* `{stderr:{data}}`, `{exit:{code, cwd}}`. `data` is a proto string (T=9), not
|
|
2311
|
+
* bytes — no base64 decode. */
|
|
2312
|
+
/** protobuf-es serializes a oneof either as its field name (`{stdout:{…}}`,
|
|
2313
|
+
* what `toJson` emits) or as the runtime pair (`{case:"stdout",value:{…}}`)
|
|
2314
|
+
* when the object is passed straight to JSON.stringify. The daemon declares
|
|
2315
|
+
* `message` as an opaque any, so accept both at either level. */
|
|
2316
|
+
function oneofCase(node, name) {
|
|
2317
|
+
if (!node || typeof node !== 'object') return null;
|
|
2318
|
+
if (node[name] && typeof node[name] === 'object') return node[name];
|
|
2319
|
+
if (node.case === name && node.value && typeof node.value === 'object') return node.value;
|
|
2320
|
+
return null;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
export function shellStreamOf(frame) {
|
|
2324
|
+
return oneofCase(frame && frame.message, 'shellStream');
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/** `agent.v1.OutputLocation { file_path, size_bytes, line_count }`. protobuf-es
|
|
2328
|
+
* JSON is camelCase, but snake_case survives a raw passthrough. */
|
|
2329
|
+
export function exitOutputPath(exit) {
|
|
2330
|
+
const loc = exit && (exit.outputLocation || exit.output_location);
|
|
2331
|
+
const p = loc && (loc.filePath || loc.file_path);
|
|
2332
|
+
return typeof p === 'string' && p ? p : '';
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/** exec used to hardcode `/bin/zsh -lc`: on Linux that is usually ENOENT and on
|
|
2336
|
+
* Windows there is no POSIX shell at all, so every exec came back as a spawn
|
|
2337
|
+
* error instead of output. */
|
|
2338
|
+
export function execShell(command, platform = process.platform) {
|
|
2339
|
+
if (platform === 'win32') {
|
|
2340
|
+
return { file: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command] };
|
|
2341
|
+
}
|
|
2342
|
+
if (platform === 'darwin') return { file: '/bin/zsh', args: ['-lc', command] };
|
|
2343
|
+
for (const sh of ['/bin/bash', '/usr/bin/bash', '/bin/zsh']) {
|
|
2344
|
+
try { if (fs.existsSync(sh)) return { file: sh, args: ['-lc', command] }; } catch { /* keep looking */ }
|
|
2345
|
+
}
|
|
2346
|
+
return { file: '/bin/sh', args: ['-c', command] };
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/** Pull a command's spooled output back over the same SSE. The file sits under
|
|
2350
|
+
* the daemon's own terminalsFolder, so it is inside the local-exec root and
|
|
2351
|
+
* the download is not refused; a failure here is never fatal — the caller
|
|
2352
|
+
* still has the exit code. */
|
|
2353
|
+
async function localExecSpooledOutput(filePath, log = () => {}) {
|
|
2354
|
+
try {
|
|
2355
|
+
const got = await localExecAsk({ kind: 'download', path: filePath }, 20000);
|
|
2356
|
+
return toolResultText(got.text || got.message || '');
|
|
2357
|
+
} catch (e) {
|
|
2358
|
+
log(`cursor-backend: local-exec output spool ${filePath}: ${e.message}`);
|
|
2359
|
+
return '';
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2065
2363
|
async function execLocal(command, cwd, log) {
|
|
2066
2364
|
let dir = cwd ? expandUserPath(cwd) : os.homedir();
|
|
2067
2365
|
try {
|
|
2068
2366
|
if (!fs.statSync(dir).isDirectory()) dir = os.homedir();
|
|
2069
2367
|
} catch {
|
|
2070
|
-
// Node reports spawn
|
|
2368
|
+
// Node reports spawn <shell> ENOENT when cwd is missing — not a missing shell.
|
|
2071
2369
|
try { fs.mkdirSync(dir, { recursive: true }); } catch { dir = os.homedir(); }
|
|
2072
2370
|
}
|
|
2073
2371
|
if (localExecSse.size > 0) {
|
|
2074
2372
|
log(`cursor-backend: local-exec exec ${JSON.stringify(command).slice(0, 80)}`);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const
|
|
2373
|
+
// Daemon-side budget stays under our own wait so a slow command reports the
|
|
2374
|
+
// shell's error, not our generic "is Grok Bot Helper connected?".
|
|
2375
|
+
const got = await localExecAsk(localExecFrame(command, dir, 55_000), 60000);
|
|
2376
|
+
let out = toolResultText(got.stdout || got.message || '');
|
|
2377
|
+
if (!out && got.outputPath) out = await localExecSpooledOutput(got.outputPath, log);
|
|
2378
|
+
const errText = toolResultText(got.stderr);
|
|
2379
|
+
const err = errText ? `\nstderr:\n${errText}` : '';
|
|
2081
2380
|
return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
|
|
2082
2381
|
}
|
|
2083
|
-
|
|
2382
|
+
const sh = execShell(command);
|
|
2383
|
+
log(`cursor-backend: exec cwd=${dir} sh=${sh.file} ${JSON.stringify(String(command).slice(0, 80))}`);
|
|
2084
2384
|
const execMs = Math.min(180000, Math.max(15000, Number(process.env.OPENZOO_EXEC_TIMEOUT_MS || 90000)));
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2385
|
+
let stdout = '';
|
|
2386
|
+
let stderr = '';
|
|
2387
|
+
try {
|
|
2388
|
+
({ stdout, stderr } = await execFileAsync(sh.file, sh.args, {
|
|
2389
|
+
cwd: dir,
|
|
2390
|
+
timeout: execMs,
|
|
2391
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
2392
|
+
env: process.env,
|
|
2393
|
+
windowsHide: true,
|
|
2394
|
+
}));
|
|
2395
|
+
} catch (e) {
|
|
2396
|
+
// A nonzero exit (grep with no match, a failing build) still has output the
|
|
2397
|
+
// model needs — bubbling the Error loses it and paints only "ERROR".
|
|
2398
|
+
stdout = toolResultText(e.stdout);
|
|
2399
|
+
stderr = toolResultText(e.stderr) || e.message;
|
|
2400
|
+
if (!stdout && !stderr) throw e;
|
|
2401
|
+
}
|
|
2402
|
+
return `${toolResultText(stdout)}${stderr ? `\nstderr:\n${toolResultText(stderr)}` : ''}`.trim() || '(no output)';
|
|
2092
2403
|
}
|
|
2093
2404
|
|
|
2094
2405
|
const LOCAL_TOOLS = [
|
|
@@ -2600,13 +2911,19 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2600
2911
|
}
|
|
2601
2912
|
if (name === 'list_dir') {
|
|
2602
2913
|
const abs = expandUserPath(args.path || os.homedir());
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2914
|
+
try {
|
|
2915
|
+
if (localExecSse.size > 0) {
|
|
2916
|
+
// Route through execLocal so a listing that the daemon spooled to a
|
|
2917
|
+
// terminal file is fetched too, instead of painting an empty frame.
|
|
2918
|
+
const text = String(await execLocal(`ls -la ${JSON.stringify(abs)}`, os.homedir(), log)).trim();
|
|
2919
|
+
if (text && !/^\(exit \d+\)$/.test(text)) return text;
|
|
2920
|
+
}
|
|
2921
|
+
const rows = fs.readdirSync(abs, { withFileTypes: true })
|
|
2922
|
+
.map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`);
|
|
2923
|
+
return rows.join('\n') || '(empty)';
|
|
2924
|
+
} catch (e) {
|
|
2925
|
+
return `list_dir failed ${abs}: ${e.message}`;
|
|
2606
2926
|
}
|
|
2607
|
-
return fs.readdirSync(abs, { withFileTypes: true })
|
|
2608
|
-
.map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`)
|
|
2609
|
-
.join('\n');
|
|
2610
2927
|
}
|
|
2611
2928
|
if (name === 'exec') {
|
|
2612
2929
|
return await execLocal(String(args.command || ''), args.cwd, log);
|
|
@@ -2906,7 +3223,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2906
3223
|
if (mediaKind) return await mediaTurn({ model, kind: mediaKind, prompt: spoken, log, onProgress: opts.onProgress, signal: opts.signal });
|
|
2907
3224
|
}
|
|
2908
3225
|
log(`cursor-backend: zoo POST :8402 model=${model} helper=${helper ? localExecSse.size : 0} hist=${historyMessages(agentId, spoken).length}${chatOnly ? ` visitor=${visitor.shortname} chat-only` : ''} ${JSON.stringify((spoken || '').slice(0, 60))}`);
|
|
2909
|
-
if (typeof opts.onProgress === 'function') opts.onProgress(
|
|
3226
|
+
if (typeof opts.onProgress === 'function') opts.onProgress(`Working on your ${hostNoun()}…`);
|
|
2910
3227
|
|
|
2911
3228
|
const images = [];
|
|
2912
3229
|
const textFiles = [];
|
|
@@ -3159,7 +3476,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3159
3476
|
name,
|
|
3160
3477
|
path: args.path,
|
|
3161
3478
|
command: args.command ? String(args.command).slice(0, 120) : undefined,
|
|
3162
|
-
note:
|
|
3479
|
+
note: toolResultText(result).slice(0, 200),
|
|
3163
3480
|
});
|
|
3164
3481
|
if (typeof opts.onProgress === 'function') {
|
|
3165
3482
|
opts.onProgress(formatZooToolLine({ name, args, result }));
|
|
@@ -3167,7 +3484,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3167
3484
|
messages.push({
|
|
3168
3485
|
role: 'tool',
|
|
3169
3486
|
tool_call_id: c.id,
|
|
3170
|
-
content:
|
|
3487
|
+
content: toolResultText(result).slice(0, 120000),
|
|
3171
3488
|
});
|
|
3172
3489
|
}
|
|
3173
3490
|
if (pendingVision.length) {
|
|
@@ -3323,12 +3640,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3323
3640
|
jsonSend(res, { ok: true, ...state });
|
|
3324
3641
|
return true;
|
|
3325
3642
|
}
|
|
3643
|
+
if (name === 'ozShare') {
|
|
3644
|
+
const list = cachedAgentList() || [];
|
|
3645
|
+
const id = String(focusedAgentId || '');
|
|
3646
|
+
const agent = list.find((a) => a.id === id) || null;
|
|
3647
|
+
const share = groupShareState({ agent, rooms: grokroomSync._rooms });
|
|
3648
|
+
jsonSend(res, { ok: true, agentId: id, ...share });
|
|
3649
|
+
return true;
|
|
3650
|
+
}
|
|
3326
3651
|
if (name === 'ozRevive') {
|
|
3327
3652
|
let parsed = {};
|
|
3328
3653
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3329
3654
|
const every = String(parsed.every || '5m');
|
|
3330
3655
|
const wakes = readWakeups(HOME);
|
|
3331
|
-
const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup);
|
|
3656
|
+
const list = (cachedAgentList() || []).filter((a) => a?.id && !a.isGroup && !isGrokRoomAgent(a) && !isGrokRoomMember(a));
|
|
3332
3657
|
const armed = [];
|
|
3333
3658
|
const already = [];
|
|
3334
3659
|
for (const a of list) {
|
|
@@ -3345,10 +3670,48 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3345
3670
|
return true;
|
|
3346
3671
|
}
|
|
3347
3672
|
if (name === 'getHostStatus') {
|
|
3348
|
-
|
|
3673
|
+
const host = {
|
|
3674
|
+
hostVersion: '0.36.0',
|
|
3675
|
+
latestHostVersion: '0.36.0',
|
|
3676
|
+
hostUpdateAvailable: false,
|
|
3677
|
+
isBusy: false,
|
|
3678
|
+
capabilities: [],
|
|
3679
|
+
};
|
|
3680
|
+
jsonSend(res, { status: 'ok', value: host, ...host });
|
|
3349
3681
|
log('cursor-backend: -> getHostStatus ready');
|
|
3350
3682
|
return true;
|
|
3351
3683
|
}
|
|
3684
|
+
if (name === 'resolveAgentCreation') {
|
|
3685
|
+
const route = { kind: 'box' };
|
|
3686
|
+
jsonSend(res, { status: 'ok', value: route, ...route });
|
|
3687
|
+
log('cursor-backend: -> resolveAgentCreation box');
|
|
3688
|
+
return true;
|
|
3689
|
+
}
|
|
3690
|
+
if (name === 'getForeverBoxStatus' || name === 'ensureForeverBox') {
|
|
3691
|
+
let parsed = {};
|
|
3692
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3693
|
+
const box = {
|
|
3694
|
+
agentId: String(parsed.id || parsed.agentId || 'openzoo'),
|
|
3695
|
+
state: 'running',
|
|
3696
|
+
};
|
|
3697
|
+
jsonSend(res, { status: 'ok', value: box, ...box });
|
|
3698
|
+
log(`cursor-backend: -> ${name} agentId=${box.agentId} state=${box.state}`);
|
|
3699
|
+
return true;
|
|
3700
|
+
}
|
|
3701
|
+
if (name === 'getBoxSecretsStatus') {
|
|
3702
|
+
const secrets = { keys: [], isApplied: false, lastAppliedAtMs: null };
|
|
3703
|
+
jsonSend(res, { status: 'ok', value: secrets, ...secrets });
|
|
3704
|
+
return true;
|
|
3705
|
+
}
|
|
3706
|
+
if (name === 'isEgressTunnelAvailable') {
|
|
3707
|
+
jsonSend(res, { status: 'ok', value: false });
|
|
3708
|
+
return true;
|
|
3709
|
+
}
|
|
3710
|
+
if (name === 'getAgentChannels') {
|
|
3711
|
+
const channels = { manifests: [], connections: [] };
|
|
3712
|
+
jsonSend(res, { status: 'ok', value: channels, ...channels });
|
|
3713
|
+
return true;
|
|
3714
|
+
}
|
|
3352
3715
|
if (name === 'uploadAttachment' || name === 'readAttachmentImage'
|
|
3353
3716
|
|| name === 'readAttachmentText' || name === 'readAttachmentChunk') {
|
|
3354
3717
|
let parsed = {};
|
|
@@ -3412,7 +3775,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3412
3775
|
parsed = { ...src, id: undefined, name: `${src.name || 'chat'} copy` };
|
|
3413
3776
|
}
|
|
3414
3777
|
const agent = mintLocalAgent(parsed);
|
|
3415
|
-
|
|
3778
|
+
const created = { agent, id: agent.id, ...agent };
|
|
3779
|
+
jsonSend(res, { status: 'ok', value: created, ...created });
|
|
3416
3780
|
pushCreatedAgent(agent);
|
|
3417
3781
|
log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
|
|
3418
3782
|
return true;
|
|
@@ -3430,7 +3794,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3430
3794
|
let parsed = {};
|
|
3431
3795
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3432
3796
|
const id = String(parsed.id || parsed.agentId || '');
|
|
3433
|
-
|
|
3797
|
+
const kick = { id, isIntroductionInFlight: false };
|
|
3798
|
+
jsonSend(res, { status: 'ok', value: kick, ...kick });
|
|
3434
3799
|
log(`cursor-backend: kickstartAgent id=${id || '?'}`);
|
|
3435
3800
|
return true;
|
|
3436
3801
|
}
|
|
@@ -3438,15 +3803,18 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3438
3803
|
let parsed = {};
|
|
3439
3804
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3440
3805
|
const memberIds = [].concat(parsed.memberAgentIds || parsed.memberIds || []).map(String).filter(Boolean);
|
|
3441
|
-
const
|
|
3806
|
+
const raw = mintLocalAgent({
|
|
3442
3807
|
...parsed,
|
|
3443
3808
|
isGroup: true,
|
|
3444
3809
|
memberIds,
|
|
3445
3810
|
name: parsed.name || parsed.title || 'group',
|
|
3446
3811
|
});
|
|
3447
|
-
|
|
3812
|
+
const agent = mintLocalAgent(attachOnchainRoom(raw, HOME));
|
|
3813
|
+
const created = { agent, id: agent.id, ...agent };
|
|
3814
|
+
jsonSend(res, { status: 'ok', value: created, ...created });
|
|
3448
3815
|
pushCreatedAgent(agent);
|
|
3449
|
-
log(`cursor-backend: createGroup
|
|
3816
|
+
log(`cursor-backend: createGroup onchain id=${agent.id} share=${agent.room?.web || ''} members=${memberIds.length} name=${JSON.stringify(agent.name)}`);
|
|
3817
|
+
void paintGroupShareHint(agent, log);
|
|
3450
3818
|
return true;
|
|
3451
3819
|
}
|
|
3452
3820
|
if (name === 'setGroupMembers') {
|
|
@@ -3521,7 +3889,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3521
3889
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
3522
3890
|
const ids = new Set([].concat(parsed.ids || parsed.id || []).map(String));
|
|
3523
3891
|
const next = (cachedAgentList() || []).filter((a) => !ids.has(a.id));
|
|
3524
|
-
addDeletedIds(HOME, [...ids]);
|
|
3892
|
+
addDeletedIds(HOME, [...ids].filter((id) => !/^(room-|roombot-)/.test(id)));
|
|
3525
3893
|
saveAgents(next);
|
|
3526
3894
|
for (const id of ids) {
|
|
3527
3895
|
transcripts.delete(id);
|
|
@@ -3544,7 +3912,15 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3544
3912
|
return true;
|
|
3545
3913
|
}
|
|
3546
3914
|
if (name === 'listAgents' || name === 'countAgents' || name === 'searchAgents') {
|
|
3547
|
-
|
|
3915
|
+
let raw = mergeAgentLists([]);
|
|
3916
|
+
let minted = false;
|
|
3917
|
+
raw = raw.map((a) => {
|
|
3918
|
+
if (!a?.isGroup || a.hidden || /^room-/.test(String(a.id || '')) || agentShareUrl(a)) return a;
|
|
3919
|
+
minted = true;
|
|
3920
|
+
return shapeAgent(attachOnchainRoom(a, HOME));
|
|
3921
|
+
});
|
|
3922
|
+
if (minted) saveAgents(raw.map(shapeAgent));
|
|
3923
|
+
const list = rosterForEvent(raw, agentActivity);
|
|
3548
3924
|
if (name === 'countAgents') {
|
|
3549
3925
|
jsonSend(res, list.length);
|
|
3550
3926
|
log(`cursor-backend: countAgents local n=${list.length} account=${activeAccountId || 'none'}`);
|
|
@@ -3594,6 +3970,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3594
3970
|
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
3971
|
lastSendEchoId = String(nonce);
|
|
3596
3972
|
jsonSend(res, { accepted: true });
|
|
3973
|
+
const roomAgent = grokroomLookup(agentId);
|
|
3974
|
+
if (roomAgent && /^room-/.test(agentId)) {
|
|
3975
|
+
const userLine = fanoutLine(agentId, 'user', uiText, {
|
|
3976
|
+
clientNonce: nonce,
|
|
3977
|
+
requestId: nonce,
|
|
3978
|
+
richText: visitor ? prefixVisitorRichText(parsed.richText, visitor.shortname, uiText) : parsed.richText,
|
|
3979
|
+
promptRaw: prompt,
|
|
3980
|
+
});
|
|
3981
|
+
noteFocus(agentId);
|
|
3982
|
+
bumpAgent(agentId, { preview: prompt, notify: false, select: false });
|
|
3983
|
+
ssePush('transcript', { ...gatewayEntry(userLine), agentId });
|
|
3984
|
+
grokroomSend(roomAgent, prompt, log);
|
|
3985
|
+
return true;
|
|
3986
|
+
}
|
|
3597
3987
|
if (!visitor && wantsWakeupCron(prompt)) {
|
|
3598
3988
|
const cron = scheduleAgentWakeup(agentId, { every: '5m' });
|
|
3599
3989
|
log(`cursor-backend: never-stop cron agent=${agentId} every=${cron.everySec}s`);
|
|
@@ -3608,6 +3998,12 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3608
3998
|
noteFocus(agentId);
|
|
3609
3999
|
bumpAgent(agentId, { preview: prompt, notify: false });
|
|
3610
4000
|
ssePush('transcript', { ...gatewayEntry(userLine), agentId });
|
|
4001
|
+
{
|
|
4002
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
4003
|
+
if (live?.room?.addr) grokroomSend(live, prompt, log);
|
|
4004
|
+
}
|
|
4005
|
+
const joinAddr = /^\s*\/join\b/i.test(prompt) ? grokroomAddrFromShare(prompt)
|
|
4006
|
+
: (/^\s*https?:\/\/openzoo\.fun\/r\//i.test(prompt) ? grokroomAddrFromShare(prompt) : '');
|
|
3611
4007
|
const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
|
|
3612
4008
|
setImmediate(async () => {
|
|
3613
4009
|
let text = '';
|
|
@@ -3629,11 +4025,27 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3629
4025
|
text = `model set to ${id}`;
|
|
3630
4026
|
}
|
|
3631
4027
|
}
|
|
4028
|
+
} else if (joinAddr) {
|
|
4029
|
+
grouped = true;
|
|
4030
|
+
const joined = joinSharedGroup(joinAddr);
|
|
4031
|
+
if (!joined) {
|
|
4032
|
+
text = `could not join — not an openzoo.fun/r/<addr> share`;
|
|
4033
|
+
} else {
|
|
4034
|
+
noteFocus(joined.id);
|
|
4035
|
+
ssePush('agent-upserted', { agent: joined, activeAgentId: joined.id });
|
|
4036
|
+
ssePush('agents', { agents: rosterForEvent(cachedAgentList() || [], agentActivity), activeAgentId: joined.id });
|
|
4037
|
+
void paintGroupShareHint(joined, log);
|
|
4038
|
+
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}`;
|
|
4039
|
+
}
|
|
3632
4040
|
} else {
|
|
3633
|
-
const
|
|
4041
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
4042
|
+
const members = groupMemberRecords(agentId);
|
|
4043
|
+
const isSharedRoom = !!(live?.room?.addr || isGrokRoomAgent(live || { id: agentId }));
|
|
3634
4044
|
if (members.length) {
|
|
3635
4045
|
grouped = true;
|
|
3636
4046
|
await runGroupQueue({ agentId, humanPrompt: prompt, parsed, nonce, log });
|
|
4047
|
+
} else if (isSharedRoom) {
|
|
4048
|
+
grouped = true;
|
|
3637
4049
|
} else {
|
|
3638
4050
|
const z = await zooComplete(prompt, log, agentId, parsed, {
|
|
3639
4051
|
signal: turn.signal,
|
|
@@ -3664,6 +4076,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3664
4076
|
const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
3665
4077
|
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
3666
4078
|
bumpAgent(agentId, { preview: text, notify: true });
|
|
4079
|
+
{
|
|
4080
|
+
const live = (cachedAgentList() || []).find((a) => a.id === agentId);
|
|
4081
|
+
if (live?.room?.addr && text) grokroomSend(live, text, log);
|
|
4082
|
+
}
|
|
3667
4083
|
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 220))}`);
|
|
3668
4084
|
} else {
|
|
3669
4085
|
log(`cursor-backend: sendPrompt group done agent=${agentId}`);
|
|
@@ -3679,6 +4095,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3679
4095
|
noteFocus(id);
|
|
3680
4096
|
tailedAgents.add(id);
|
|
3681
4097
|
const t = agentTranscript(id);
|
|
4098
|
+
if (isGrokRoomAgent({ id }) || /^roombot-/.test(id)) t.pulledRemote = true;
|
|
3682
4099
|
if (!t.pulledRemote && realPod?.agent && !useHouseRoster()) {
|
|
3683
4100
|
const remote = await podJson('/api/getAgentTranscriptTail', {
|
|
3684
4101
|
id, agentId: id, limit: 200, beforeSeq: parsed.beforeSeq,
|
|
@@ -3718,7 +4135,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3718
4135
|
}
|
|
3719
4136
|
|
|
3720
4137
|
const stubs = {
|
|
3721
|
-
getHostStatus: {
|
|
4138
|
+
getHostStatus: {
|
|
4139
|
+
hostVersion: '0.36.0', latestHostVersion: '0.36.0',
|
|
4140
|
+
hostUpdateAvailable: false, isBusy: false, capabilities: [],
|
|
4141
|
+
},
|
|
3722
4142
|
getHostSettings: { settings: {} },
|
|
3723
4143
|
setHostSettings: { ok: true },
|
|
3724
4144
|
setBoxSecrets: { ok: true },
|
|
@@ -3729,12 +4149,16 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3729
4149
|
getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
|
|
3730
4150
|
getTeachRecordingStatus: { recording: false },
|
|
3731
4151
|
getTrays: [],
|
|
3732
|
-
isGlobalSearchEnabled:
|
|
3733
|
-
isEgressTunnelAvailable:
|
|
4152
|
+
isGlobalSearchEnabled: false,
|
|
4153
|
+
isEgressTunnelAvailable: false,
|
|
3734
4154
|
getSharingState: { sharing: false },
|
|
3735
4155
|
getBotTemplateExportPolicy: { allowed: true },
|
|
3736
4156
|
getAgentAutomations: { automations: [] },
|
|
3737
|
-
getForeverBoxStatus: {
|
|
4157
|
+
getForeverBoxStatus: { agentId: 'openzoo', state: 'running' },
|
|
4158
|
+
ensureForeverBox: { agentId: 'openzoo', state: 'running' },
|
|
4159
|
+
getBoxSecretsStatus: { keys: [], isApplied: false, lastAppliedAtMs: null },
|
|
4160
|
+
getAgentChannels: { manifests: [], connections: [] },
|
|
4161
|
+
resolveAgentCreation: { kind: 'box' },
|
|
3738
4162
|
getSubagents: { subagents: [] },
|
|
3739
4163
|
getAsyncTasks: { tasks: [] },
|
|
3740
4164
|
getAgentWorkflows: { workflows: [] },
|
|
@@ -3799,14 +4223,377 @@ function respond(req, res, method, models) {
|
|
|
3799
4223
|
res.end(bodyProto);
|
|
3800
4224
|
}
|
|
3801
4225
|
|
|
4226
|
+
// ===== GROKROOM-BRIDGE v1 (overlay) =====
|
|
4227
|
+
// Rooms from 127.0.0.1:4799 as native Grok Bot group threads.
|
|
4228
|
+
// One room = one sidebar row (# main / # bots / # random / # lobby).
|
|
4229
|
+
// Each remote sender is a hidden bubble-member (no sidebar row). Sends POST a
|
|
4230
|
+
// signed ed25519 envelope to /submit — the chain is the log. Additive: any
|
|
4231
|
+
// failure here must never break zoo chat. Interval starts from startCursorBackend
|
|
4232
|
+
// so importing this module in tests does not poll.
|
|
4233
|
+
const GROKROOM_API = process.env.OZ_GROKROOM_URL || 'http://127.0.0.1:4799';
|
|
4234
|
+
const GROKROOM_PAYMASTER = (process.env.OZ_GROKROOM_PAYMASTER || 'https://openzoo.fun').replace(/\/$/, '');
|
|
4235
|
+
const GROKROOM_ID_FILE = path.join(HOME, '.openzoo', 'grokroom-identity.json');
|
|
4236
|
+
const GROKROOM_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
4237
|
+
const GROKROOM_HANDLE = (process.env.OZ_ROOM_HANDLE || process.env.USER || 'anon')
|
|
4238
|
+
.replace(/[^A-Za-z0-9 _.'-]/g, '').trim().slice(0, 24) || 'anon';
|
|
4239
|
+
const GROKROOM_B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
4240
|
+
|
|
4241
|
+
function grokroomB58(buf) {
|
|
4242
|
+
const digits = [0];
|
|
4243
|
+
for (const byte of buf) {
|
|
4244
|
+
let carry = byte;
|
|
4245
|
+
for (let i = 0; i < digits.length; i++) { carry += digits[i] << 8; digits[i] = carry % 58; carry = (carry / 58) | 0; }
|
|
4246
|
+
while (carry) { digits.push(carry % 58); carry = (carry / 58) | 0; }
|
|
4247
|
+
}
|
|
4248
|
+
let out = '';
|
|
4249
|
+
for (const b of buf) { if (b === 0) out += '1'; else break; }
|
|
4250
|
+
return out + digits.reverse().map((d) => GROKROOM_B58[d]).join('');
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
let grokroomIdCache = null;
|
|
4254
|
+
function grokroomIdentity() {
|
|
4255
|
+
if (grokroomIdCache) return grokroomIdCache;
|
|
4256
|
+
let id = null;
|
|
4257
|
+
try {
|
|
4258
|
+
const j = JSON.parse(fs.readFileSync(GROKROOM_ID_FILE, 'utf8'));
|
|
4259
|
+
if (j.pub && j.seed) id = j;
|
|
4260
|
+
} catch { /* first run */ }
|
|
4261
|
+
if (!id) {
|
|
4262
|
+
const pair = generateKeyPairSync('ed25519');
|
|
4263
|
+
const rawPub = pair.publicKey.export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
4264
|
+
const rawSeed = pair.privateKey.export({ format: 'der', type: 'pkcs8' }).subarray(-32);
|
|
4265
|
+
id = { pub: grokroomB58(Buffer.from(rawPub)), seed: Buffer.from(rawSeed).toString('base64') };
|
|
4266
|
+
try {
|
|
4267
|
+
fs.mkdirSync(path.dirname(GROKROOM_ID_FILE), { recursive: true });
|
|
4268
|
+
fs.writeFileSync(GROKROOM_ID_FILE, JSON.stringify(id, null, 2));
|
|
4269
|
+
} catch { /* in-memory is fine */ }
|
|
4270
|
+
}
|
|
4271
|
+
grokroomIdCache = id;
|
|
4272
|
+
return id;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
async function grokroomPost(roomId, body, { from: fromRaw } = {}) {
|
|
4276
|
+
const id = grokroomIdentity();
|
|
4277
|
+
const ts = Date.now();
|
|
4278
|
+
const from = grokroomFromHandle(fromRaw || GROKROOM_HANDLE, GROKROOM_HANDLE);
|
|
4279
|
+
const pub = id.pub;
|
|
4280
|
+
const room = String(roomId || '');
|
|
4281
|
+
const canonical = JSON.stringify({ body, from, pub, room, ts });
|
|
4282
|
+
const key = createPrivateKey({ key: Buffer.concat([GROKROOM_PKCS8_PREFIX, Buffer.from(id.seed, 'base64')]), format: 'der', type: 'pkcs8' });
|
|
4283
|
+
const sig = grokroomB58(cryptoSign(null, Buffer.from(canonical, 'utf8'), key));
|
|
4284
|
+
const payload = JSON.stringify({ room, from, pub, body, ts, sig });
|
|
4285
|
+
const endpoints = [];
|
|
4286
|
+
if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(room)) {
|
|
4287
|
+
endpoints.push(GROKROOM_PAYMASTER + '/api/room/submit');
|
|
4288
|
+
}
|
|
4289
|
+
endpoints.push(GROKROOM_API + '/submit');
|
|
4290
|
+
let last = new Error('no submit endpoint');
|
|
4291
|
+
for (const url of endpoints) {
|
|
4292
|
+
try {
|
|
4293
|
+
const r = await fetch(url, {
|
|
4294
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
4295
|
+
body: payload,
|
|
4296
|
+
signal: AbortSignal.timeout(30000),
|
|
4297
|
+
});
|
|
4298
|
+
const j = await r.json().catch(() => ({}));
|
|
4299
|
+
if (r.ok && j.ok !== false) return j;
|
|
4300
|
+
last = new Error(j.error || ('submit HTTP ' + r.status));
|
|
4301
|
+
} catch (e) {
|
|
4302
|
+
last = e;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
throw last;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
function grokroomLookup(agentId) {
|
|
4309
|
+
const list = cachedAgentList() || [];
|
|
4310
|
+
return list.find((a) => a.id === agentId && isGrokRoomAgent(a)) || null;
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
function grokroomUpsertAgent(parsed, { select = false } = {}) {
|
|
4314
|
+
const id = String(parsed.id);
|
|
4315
|
+
const prev = (cachedAgentList() || []).find((a) => a.id === id) || {};
|
|
4316
|
+
const agent = shapeAgent({
|
|
4317
|
+
...prev,
|
|
4318
|
+
...parsed,
|
|
4319
|
+
id,
|
|
4320
|
+
createdAt: prev.createdAt || parsed.createdAt || Date.now(),
|
|
4321
|
+
updatedAt: Date.now(),
|
|
4322
|
+
});
|
|
4323
|
+
const list = cachedAgentList() || [];
|
|
4324
|
+
const i = list.findIndex((a) => a.id === id);
|
|
4325
|
+
if (i < 0) list.unshift(agent);
|
|
4326
|
+
else list[i] = { ...list[i], ...agent };
|
|
4327
|
+
saveAgents(list.map(shapeAgent));
|
|
4328
|
+
agentTranscript(id);
|
|
4329
|
+
if (agent.hidden) {
|
|
4330
|
+
const visible = rosterForEvent(list, agentActivity);
|
|
4331
|
+
ssePush('agents', { agents: visible, activeAgentId: focusedAgentId || visible[0]?.id || id });
|
|
4332
|
+
} else {
|
|
4333
|
+
pushCreatedAgent(agent, { select });
|
|
4334
|
+
}
|
|
4335
|
+
return agent;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
function grokroomMemberEnsure(group, name, { speak = false } = {}) {
|
|
4339
|
+
const mid = grokroomMemberId(name);
|
|
4340
|
+
const list = cachedAgentList() || [];
|
|
4341
|
+
let member = list.find((a) => a.id === mid);
|
|
4342
|
+
if (!member) {
|
|
4343
|
+
member = grokroomUpsertAgent({
|
|
4344
|
+
id: mid,
|
|
4345
|
+
name: String(name || 'anon').slice(0, 80),
|
|
4346
|
+
hidden: true,
|
|
4347
|
+
hiddenFromSidebar: true,
|
|
4348
|
+
brief: '',
|
|
4349
|
+
description: '',
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
if (!speak) return group;
|
|
4353
|
+
const have = group.memberIds || [];
|
|
4354
|
+
if (have.includes(mid)) return group;
|
|
4355
|
+
return grokroomUpsertAgent({
|
|
4356
|
+
id: group.id,
|
|
4357
|
+
name: group.name,
|
|
4358
|
+
isGroup: true,
|
|
4359
|
+
memberIds: [...have, mid],
|
|
4360
|
+
brief: '',
|
|
4361
|
+
description: '',
|
|
4362
|
+
room: group.room,
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
function joinSharedGroup(addr, { name } = {}) {
|
|
4367
|
+
const web = grokroomShareUrl(addr);
|
|
4368
|
+
if (!web) return null;
|
|
4369
|
+
const list = cachedAgentList() || [];
|
|
4370
|
+
const existing = list.find((a) => a?.room?.addr === addr);
|
|
4371
|
+
if (existing) return existing;
|
|
4372
|
+
const raw = mintLocalAgent({
|
|
4373
|
+
isGroup: true,
|
|
4374
|
+
name: String(name || `group ${addr.slice(0, 6)}`).slice(0, 80),
|
|
4375
|
+
memberIds: [],
|
|
4376
|
+
room: { id: addr, addr, web },
|
|
4377
|
+
});
|
|
4378
|
+
return mintLocalAgent(bindOnchainRoom(raw, addr, HOME));
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
function paintShareIfMissing(agent, log = () => {}) {
|
|
4382
|
+
const url = agentShareUrl(agent);
|
|
4383
|
+
if (!agent?.id || !url) return false;
|
|
4384
|
+
const t = agentTranscript(agent.id);
|
|
4385
|
+
const has = (t.entries || []).some((e) => /openzoo\.fun\/r\//.test(String(e?.content || '')));
|
|
4386
|
+
if (has) return false;
|
|
4387
|
+
const nonce = `oz-share-${agent.id}`;
|
|
4388
|
+
const line = fanoutLine(agent.id, 'assistant', 'Share this room: ' + url, {
|
|
4389
|
+
clientNonce: nonce,
|
|
4390
|
+
requestId: nonce,
|
|
4391
|
+
});
|
|
4392
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4393
|
+
log(`cursor-backend: group share painted agent=${agent.id}`);
|
|
4394
|
+
return true;
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
async function paintGroupShareHint(agent, log = () => {}) {
|
|
4398
|
+
if (!agent?.id) return;
|
|
4399
|
+
let rooms = grokroomSync._rooms;
|
|
4400
|
+
if (!Array.isArray(rooms)) {
|
|
4401
|
+
try {
|
|
4402
|
+
const st = await fetch(GROKROOM_API + '/state', { signal: AbortSignal.timeout(4000) }).then((r) => r.json());
|
|
4403
|
+
rooms = Array.isArray(st.rooms) ? st.rooms : [];
|
|
4404
|
+
grokroomSync._rooms = rooms;
|
|
4405
|
+
} catch { rooms = []; }
|
|
4406
|
+
}
|
|
4407
|
+
const share = groupShareState({ agent, rooms });
|
|
4408
|
+
let text = '';
|
|
4409
|
+
if (share.url) {
|
|
4410
|
+
text = 'Share this room on openzoo.fun: ' + share.url;
|
|
4411
|
+
} else if (share.rooms.length) {
|
|
4412
|
+
text = 'This group is local to this house. Public grokrooms — send this URL:\n'
|
|
4413
|
+
+ share.rooms.map((r) => `${r.name} ${r.url}`).join('\n');
|
|
4414
|
+
} else {
|
|
4415
|
+
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.';
|
|
4416
|
+
}
|
|
4417
|
+
const line = fanoutLine(agent.id, 'assistant', text, {});
|
|
4418
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4419
|
+
log(`cursor-backend: group share hint agent=${agent.id}`);
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
function grokroomEnsureGroup(roomId, name, addr) {
|
|
4423
|
+
const id = grokroomAgentId(roomId);
|
|
4424
|
+
if (!id) return null;
|
|
4425
|
+
const list = cachedAgentList() || [];
|
|
4426
|
+
const prev = list.find((a) => a.id === id || a.room?.id === roomId);
|
|
4427
|
+
const nextAddr = addr || prev?.room?.addr || '';
|
|
4428
|
+
const web = grokroomShareUrl(nextAddr);
|
|
4429
|
+
if (prev) {
|
|
4430
|
+
const need = (nextAddr && prev.room && prev.room.addr !== nextAddr)
|
|
4431
|
+
|| (web && prev.room && prev.room.web !== web);
|
|
4432
|
+
const agent = need
|
|
4433
|
+
? grokroomUpsertAgent({ ...prev, room: { id: roomId, addr: nextAddr, web } }, { select: false })
|
|
4434
|
+
: prev;
|
|
4435
|
+
paintShareIfMissing(agent);
|
|
4436
|
+
return agent;
|
|
4437
|
+
}
|
|
4438
|
+
const agent = grokroomUpsertAgent({
|
|
4439
|
+
id,
|
|
4440
|
+
name: name || ('# ' + roomId),
|
|
4441
|
+
isGroup: true,
|
|
4442
|
+
memberIds: [],
|
|
4443
|
+
brief: '',
|
|
4444
|
+
description: '',
|
|
4445
|
+
room: { id: roomId, addr: nextAddr, web },
|
|
4446
|
+
}, { select: false });
|
|
4447
|
+
const nonce = `oz-room-${id}`;
|
|
4448
|
+
grokroomMemberEnsure(agent, 'room');
|
|
4449
|
+
const text = 'Linked to ' + agent.name + ' on Solana testnet. Every message here is a signed memo on-chain — the chain is the log.'
|
|
4450
|
+
+ (web ? ' Share: ' + web : '')
|
|
4451
|
+
+ (nextAddr ? ' Explorer: https://solscan.io/account/' + nextAddr + '?cluster=testnet' : '');
|
|
4452
|
+
const line = fanoutLine(id, 'assistant', text, {
|
|
4453
|
+
clientNonce: nonce,
|
|
4454
|
+
requestId: nonce,
|
|
4455
|
+
author: { kind: 'agent', id: grokroomMemberId('room'), name: 'room' },
|
|
4456
|
+
});
|
|
4457
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: id });
|
|
4458
|
+
return agent;
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
function grokroomSend(agent, body, log = () => {}, { from } = {}) {
|
|
4462
|
+
const text = stripSpendFooter(String(body || '')).trim().slice(0, 400);
|
|
4463
|
+
const roomKey = agent?.room?.addr || agent?.room?.id;
|
|
4464
|
+
if (!text || !roomKey) return;
|
|
4465
|
+
if (/^PASS\b/i.test(text)) return;
|
|
4466
|
+
grokroomPost(roomKey, text, { from }).then((j) => {
|
|
4467
|
+
log(`cursor-backend: grokroom posted room=${String(roomKey).slice(0, 8)}… sig=${String(j?.sig || '').slice(0, 12)} from=${from || GROKROOM_HANDLE}`);
|
|
4468
|
+
}).catch((e) => {
|
|
4469
|
+
grokroomMemberEnsure(agent, 'room');
|
|
4470
|
+
const line = fanoutLine(agent.id, 'assistant', '⚠ could not reach the room: ' + e.message, {
|
|
4471
|
+
author: { kind: 'agent', id: grokroomMemberId('room'), name: 'room' },
|
|
4472
|
+
});
|
|
4473
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4474
|
+
log(`cursor-backend: grokroom send fail ${roomKey} ${e.message}`);
|
|
4475
|
+
});
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
function grokroomIngestMsgs(group, msgs, log = () => {}) {
|
|
4479
|
+
if (!group?.id || !Array.isArray(msgs) || !msgs.length) return 0;
|
|
4480
|
+
let agent = group;
|
|
4481
|
+
const names = new Set();
|
|
4482
|
+
for (const m of msgs) {
|
|
4483
|
+
const from = String(m.from || m.node || '').slice(0, 24);
|
|
4484
|
+
if (from) names.add(from);
|
|
4485
|
+
}
|
|
4486
|
+
for (const from of names) agent = grokroomMemberEnsure(agent, from);
|
|
4487
|
+
const t = agentTranscript(agent.id);
|
|
4488
|
+
const seen = new Set((t.entries || []).filter((h) => h.sig).map((h) => h.sig));
|
|
4489
|
+
const recent = (t.entries || []).slice(-80).map((h) => String(h.content || ''));
|
|
4490
|
+
let dirty = 0;
|
|
4491
|
+
for (const m of msgs) {
|
|
4492
|
+
const msig = m.sig || m.txSig || m.envsig || (m.pub && m.ts ? m.pub + ':' + m.ts : null);
|
|
4493
|
+
if (!msig || seen.has(msig)) continue;
|
|
4494
|
+
seen.add(msig);
|
|
4495
|
+
const from = String(m.from || m.node || 'anon').slice(0, 24);
|
|
4496
|
+
const body = String(m.body || m.text || '');
|
|
4497
|
+
if (!body) continue;
|
|
4498
|
+
if (recent.includes(body)) continue;
|
|
4499
|
+
agent = grokroomMemberEnsure(agent, from);
|
|
4500
|
+
const mid = grokroomMemberId(from);
|
|
4501
|
+
const line = fanoutLine(agent.id, 'assistant', body, {
|
|
4502
|
+
author: { kind: 'agent', id: mid, name: from },
|
|
4503
|
+
sig: msig,
|
|
4504
|
+
});
|
|
4505
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: agent.id });
|
|
4506
|
+
dirty += 1;
|
|
4507
|
+
}
|
|
4508
|
+
if (dirty) {
|
|
4509
|
+
bumpAgent(agent.id, { preview: msgs[msgs.length - 1]?.body || '', notify: true, select: false });
|
|
4510
|
+
log(`cursor-backend: grokroom ${agent.room?.id || agent.id} +${dirty} bubbles`);
|
|
4511
|
+
}
|
|
4512
|
+
return dirty;
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
async function grokroomSyncPaymaster(log = () => {}) {
|
|
4516
|
+
const list = cachedAgentList() || [];
|
|
4517
|
+
const seen = new Set();
|
|
4518
|
+
for (const a of list) {
|
|
4519
|
+
const addr = a?.room?.addr;
|
|
4520
|
+
if (!grokroomShareUrl(addr) || seen.has(addr)) continue;
|
|
4521
|
+
seen.add(addr);
|
|
4522
|
+
try {
|
|
4523
|
+
const r = await fetch(`${GROKROOM_PAYMASTER}/api/room/history?addr=${encodeURIComponent(addr)}`, {
|
|
4524
|
+
signal: AbortSignal.timeout(8000),
|
|
4525
|
+
});
|
|
4526
|
+
if (!r.ok) continue;
|
|
4527
|
+
const j = await r.json().catch(() => ({}));
|
|
4528
|
+
const msgs = Array.isArray(j.msgs) ? j.msgs : [];
|
|
4529
|
+
grokroomIngestMsgs(a, msgs, log);
|
|
4530
|
+
} catch (e) {
|
|
4531
|
+
log(`cursor-backend: grokroom history ${String(addr).slice(0, 8)}… ${e.message}`);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4536
|
+
let grokroomBusy = false;
|
|
4537
|
+
async function grokroomSync(log = () => {}) {
|
|
4538
|
+
if (grokroomBusy) return;
|
|
4539
|
+
grokroomBusy = true;
|
|
4540
|
+
try {
|
|
4541
|
+
try {
|
|
4542
|
+
const stRes = await fetch(GROKROOM_API + '/state', { signal: AbortSignal.timeout(4000) });
|
|
4543
|
+
if (stRes.ok) {
|
|
4544
|
+
const st = await stRes.json();
|
|
4545
|
+
const rooms = Array.isArray(st.rooms) ? st.rooms : [];
|
|
4546
|
+
grokroomSync._rooms = rooms;
|
|
4547
|
+
if (!grokroomSync._logged) {
|
|
4548
|
+
grokroomSync._logged = true;
|
|
4549
|
+
log(`cursor-backend: grokroom sync tick: rooms=${rooms.map((r) => r.id).join(',')}`);
|
|
4550
|
+
}
|
|
4551
|
+
for (const r of rooms) {
|
|
4552
|
+
const roomId = r.id || r.name;
|
|
4553
|
+
if (!roomId) continue;
|
|
4554
|
+
const group = grokroomEnsureGroup(roomId, r.name, r.addr || r.address);
|
|
4555
|
+
if (!group) continue;
|
|
4556
|
+
const feedRes = await fetch(GROKROOM_API + '/feed?room=' + encodeURIComponent(roomId), { signal: AbortSignal.timeout(4000) });
|
|
4557
|
+
if (!feedRes.ok) continue;
|
|
4558
|
+
const feed = await feedRes.json();
|
|
4559
|
+
grokroomIngestMsgs(group, Array.isArray(feed.messages) ? feed.messages : [], log);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
} catch (e) {
|
|
4563
|
+
if (!grokroomSync._localMiss) {
|
|
4564
|
+
grokroomSync._localMiss = true;
|
|
4565
|
+
log(`cursor-backend: grokroom local :4799 down (${e.message}); paymaster only`);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
await grokroomSyncPaymaster(log);
|
|
4569
|
+
} catch (e) {
|
|
4570
|
+
log(`cursor-backend: grokroom sync: ${e.message}`);
|
|
4571
|
+
} finally {
|
|
4572
|
+
grokroomBusy = false;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
function startGrokroomBridge(log = () => {}) {
|
|
4577
|
+
if (startGrokroomBridge._on) return;
|
|
4578
|
+
if (process.env.OZ_GROKROOM === '0') {
|
|
4579
|
+
log('cursor-backend: grokroom bridge off (OZ_GROKROOM=0)');
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4582
|
+
startGrokroomBridge._on = true;
|
|
4583
|
+
const tick = () => grokroomSync(log);
|
|
4584
|
+
setTimeout(tick, 800);
|
|
4585
|
+
setInterval(tick, 4000);
|
|
4586
|
+
}
|
|
4587
|
+
|
|
3802
4588
|
/**
|
|
3803
4589
|
* Start the impersonation server. Binds `port` (default 8443 — a privileged
|
|
3804
4590
|
* 443->port redirect is installed alongside the /etc/hosts entry, so this stays
|
|
3805
4591
|
* unprivileged). Returns { server, port }.
|
|
3806
4592
|
*/
|
|
3807
|
-
export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
4593
|
+
export async function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
3808
4594
|
restoreAgentWakeups(log);
|
|
3809
|
-
|
|
4595
|
+
startGrokroomBridge(log);
|
|
4596
|
+
const { cert, key } = await ensureCert(log);
|
|
3810
4597
|
const certPem = fs.readFileSync(cert);
|
|
3811
4598
|
const keyPem = fs.readFileSync(key);
|
|
3812
4599
|
const secureContext = tls.createSecureContext({ cert: certPem, key: keyPem });
|
|
@@ -3895,6 +4682,21 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
3895
4682
|
return;
|
|
3896
4683
|
}
|
|
3897
4684
|
log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
|
|
4685
|
+
// HIJACK LOGIN. Measured Linux 2026-09-03: GET /auth/poll -> empty-ok
|
|
4686
|
+
// (`{}`) so SandBackendLoginManager threw bad_200 and the UI stayed on
|
|
4687
|
+
// "Log in with Cursor". Poll must return accessToken+refreshToken JWT.
|
|
4688
|
+
// /oauth/token refresh is the same session — do NOT passthrough fake
|
|
4689
|
+
// tokens to real api2. Sniff/passthru keeps the real issuer.
|
|
4690
|
+
{
|
|
4691
|
+
const fakeAuth = fakeCursorAuthReply(full, { passthrough: sniffOn() || process.env.OPENZOO_PASSTHRU === '1' });
|
|
4692
|
+
if (fakeAuth) {
|
|
4693
|
+
if (req.method === 'OPTIONS') { res.writeHead(204, CORS); res.end(); return; }
|
|
4694
|
+
res.writeHead(200, { 'content-type': fakeAuth.contentType, ...CORS });
|
|
4695
|
+
res.end(fakeAuth.body);
|
|
4696
|
+
log(`cursor-backend: -> FAKE ${fakeAuth.kind} logged-in (no Cursor account)`);
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
3898
4700
|
// OAUTH MUST HIT THE REAL ISSUER. empty-ok on /oauth/token is the
|
|
3899
4701
|
// "Reconnecting to your computer" / edge/handler-failed loop: the app
|
|
3900
4702
|
// retries the same 509b body forever and never reaches AvailableModels
|