openzoo 0.50.37 → 0.50.39
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/claude-zoo.js +100 -0
- package/bin/openzoo.js +8 -0
- package/lib/cursorbackend.js +464 -99
- package/lib/grokbotAccount.js +134 -8
- package/lib/grokbotUploads.js +168 -0
- package/lib/grokbotweb-shim.js +766 -0
- package/lib/grokbotweb.js +933 -0
- package/lib/grokcli.js +47 -22
- package/lib/ozSpendChip.js +482 -0
- package/lib/pay.js +17 -24
- package/lib/proxy.js +33 -11
- package/lib/spendProof.js +307 -0
- package/lib/x402.js +3 -1
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -38,8 +38,14 @@ import tls from 'node:tls';
|
|
|
38
38
|
import { randomUUID } from 'node:crypto';
|
|
39
39
|
import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
|
|
40
40
|
import {
|
|
41
|
-
accountPodPath, accountAgentsPath, rosterForAccount,
|
|
41
|
+
accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
|
|
42
|
+
readHouseRoster, houseAgentsPath, shapeAgent,
|
|
42
43
|
} from './grokbotAccount.js';
|
|
44
|
+
import { formatSpendFooter, mergeTurnProof } from './spendProof.js';
|
|
45
|
+
import { prefixVisitorRichText } from './grokbotweb.js';
|
|
46
|
+
import {
|
|
47
|
+
ingestUpload, lookupUpload, readUploadChunk, readUploadImage, readUploadText,
|
|
48
|
+
} from './grokbotUploads.js';
|
|
43
49
|
|
|
44
50
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
45
51
|
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
@@ -343,31 +349,30 @@ activeAccountId = realPod?.accountId || null;
|
|
|
343
349
|
if (activeAccountId) migrateLegacyPod(activeAccountId);
|
|
344
350
|
|
|
345
351
|
function agentsPath() {
|
|
346
|
-
if (!activeAccountId) return
|
|
347
|
-
return accountAgentsPath(HOME, activeAccountId) ||
|
|
352
|
+
if (!activeAccountId) return houseAgentsPath(HOME);
|
|
353
|
+
return accountAgentsPath(HOME, activeAccountId) || houseAgentsPath(HOME);
|
|
354
|
+
}
|
|
355
|
+
/** Cafe/web hijack: one house tray for every visitor. Electron sniff still
|
|
356
|
+
* talks to the live 1340 so a second Cursor login stays isolated. */
|
|
357
|
+
function useHouseRoster() {
|
|
358
|
+
return Boolean(process.env.OZ_HIJACK_POD) && !sniffOn();
|
|
348
359
|
}
|
|
349
360
|
function loadAgents() {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (!activeAccountId) return null;
|
|
353
|
-
const scoped = readJsonFile(accountAgentsPath(HOME, activeAccountId));
|
|
354
|
-
if (Array.isArray(scoped)) return scoped;
|
|
355
|
-
const legacyPod = readJsonFile(POD_FILE);
|
|
356
|
-
if (legacyPod?.accountId === activeAccountId) {
|
|
357
|
-
const a = readJsonFile(AGENTS_FILE);
|
|
358
|
-
return Array.isArray(a) ? a : null;
|
|
359
|
-
}
|
|
360
|
-
return null;
|
|
361
|
+
const house = readHouseRoster(HOME, activeAccountId);
|
|
362
|
+
return house.length ? house : [];
|
|
361
363
|
}
|
|
362
364
|
function saveAgents(a) {
|
|
363
|
-
if (!Array.isArray(a)
|
|
364
|
-
|
|
365
|
-
|
|
365
|
+
if (!Array.isArray(a)) return;
|
|
366
|
+
// Persist [] too — otherwise delete-all is a no-op and the 65 land back.
|
|
367
|
+
writeJsonFile(houseAgentsPath(HOME), a);
|
|
368
|
+
if (activeAccountId) {
|
|
369
|
+
const p = accountAgentsPath(HOME, activeAccountId);
|
|
370
|
+
if (p) writeJsonFile(p, a);
|
|
371
|
+
}
|
|
366
372
|
}
|
|
367
373
|
function cachedAgentList() {
|
|
368
374
|
const a = loadAgents();
|
|
369
|
-
|
|
370
|
-
return null;
|
|
375
|
+
return a.length ? a : null;
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
function sniffOn() { return process.env.OPENZOO_SNIFF === '1'; }
|
|
@@ -621,10 +626,10 @@ async function fetchEnsureSandBox(req, body, log) {
|
|
|
621
626
|
try {
|
|
622
627
|
const remote = await podJson('/api/listAgents', {}, log);
|
|
623
628
|
if (Array.isArray(remote) && remote.length) {
|
|
624
|
-
const merged = mergeAgentLists(remote);
|
|
629
|
+
const merged = rosterForEvent(mergeAgentLists(remote), agentActivity);
|
|
625
630
|
saveAgents(merged);
|
|
626
631
|
const active = focusedAgentId || merged[0]?.id;
|
|
627
|
-
ssePush('agents', { agents: merged
|
|
632
|
+
ssePush('agents', { agents: merged, activeAgentId: active });
|
|
628
633
|
log(`cursor-backend: discovered roster n=${merged.length} account=${realPod.accountId}`);
|
|
629
634
|
}
|
|
630
635
|
} catch (e) {
|
|
@@ -781,23 +786,32 @@ async function proxyPodHttp(req, res, full, body, log) {
|
|
|
781
786
|
podStale = true;
|
|
782
787
|
log(`cursor-backend: 1340 ${cap.status} ${path0} — pod stale, local`);
|
|
783
788
|
if (path0 === '/api/listAgents') {
|
|
789
|
+
if (useHouseRoster()) {
|
|
790
|
+
jsonSend(res, rosterForEvent(loadAgents(), agentActivity));
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
784
793
|
const cached = rosterForAccount({
|
|
785
794
|
liveAccountId: activeAccountId,
|
|
786
795
|
cachedAccountId: activeAccountId,
|
|
787
796
|
cached: loadAgents(),
|
|
797
|
+
fallback: loadAgents(),
|
|
788
798
|
});
|
|
789
799
|
if (cached.length) {
|
|
790
|
-
jsonSend(res, mergeAgentLists(cached));
|
|
800
|
+
jsonSend(res, rosterForEvent(mergeAgentLists(cached), agentActivity));
|
|
791
801
|
return true;
|
|
792
802
|
}
|
|
793
803
|
}
|
|
794
804
|
return false;
|
|
795
805
|
}
|
|
796
806
|
if (path0 === '/api/listAgents' && cap.status === 200) {
|
|
807
|
+
if (useHouseRoster()) {
|
|
808
|
+
jsonSend(res, rosterForEvent(loadAgents(), agentActivity));
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
797
811
|
try {
|
|
798
812
|
const parsed = JSON.parse(String(inflateBody(cap.buf, cap.respHeaders)));
|
|
799
813
|
if (Array.isArray(parsed)) {
|
|
800
|
-
const merged = mergeAgentLists(parsed);
|
|
814
|
+
const merged = rosterForEvent(mergeAgentLists(parsed), agentActivity);
|
|
801
815
|
saveAgents(merged);
|
|
802
816
|
jsonSend(res, merged);
|
|
803
817
|
log(`cursor-backend: listAgents 200 merged n=${merged.length} account=${activeAccountId || '?'}`);
|
|
@@ -810,9 +824,10 @@ async function proxyPodHttp(req, res, full, body, log) {
|
|
|
810
824
|
liveAccountId: activeAccountId,
|
|
811
825
|
cachedAccountId: activeAccountId,
|
|
812
826
|
cached: loadAgents(),
|
|
827
|
+
fallback: loadAgents(),
|
|
813
828
|
});
|
|
814
|
-
if (cached.length) {
|
|
815
|
-
jsonSend(res, mergeAgentLists(cached));
|
|
829
|
+
if (cached.length || useHouseRoster()) {
|
|
830
|
+
jsonSend(res, rosterForEvent(mergeAgentLists(cached), agentActivity));
|
|
816
831
|
log(`cursor-backend: listAgents ${cap.status} — cached ${cached.length} named agents account=${activeAccountId}`);
|
|
817
832
|
return true;
|
|
818
833
|
}
|
|
@@ -877,6 +892,13 @@ function noteFocus(id) {
|
|
|
877
892
|
a.unreadCount = 0;
|
|
878
893
|
agentActivity.set(id, a);
|
|
879
894
|
}
|
|
895
|
+
function topConversationId(list) {
|
|
896
|
+
const arr = Array.isArray(list) ? list : [];
|
|
897
|
+
return focusedAgentId && arr.some((a) => a.id === focusedAgentId)
|
|
898
|
+
? focusedAgentId
|
|
899
|
+
: (arr[0]?.id || null);
|
|
900
|
+
}
|
|
901
|
+
|
|
880
902
|
function stampActivity(agent) {
|
|
881
903
|
const a = agentActivity.get(agent.id) || {};
|
|
882
904
|
const updatedAt = a.updatedAt || agent.updatedAt || agent.createdAt || 0;
|
|
@@ -908,13 +930,23 @@ function bumpAgent(id, { preview = '', notify = true } = {}) {
|
|
|
908
930
|
const list = cachedAgentList() || [];
|
|
909
931
|
const idx = list.findIndex((x) => x.id === id);
|
|
910
932
|
const base = idx >= 0 ? list[idx] : { id, name: id };
|
|
911
|
-
const agent = stampActivity({ ...base, updatedAt: now });
|
|
933
|
+
const agent = shapeAgent(stampActivity({ ...base, updatedAt: now }));
|
|
912
934
|
if (idx >= 0) list[idx] = agent;
|
|
913
935
|
else list.unshift(agent);
|
|
914
|
-
saveAgents(sortAgentsByActivity(list));
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
936
|
+
saveAgents(sortAgentsByActivity(list.map(shapeAgent)));
|
|
937
|
+
pushCreatedAgent(agent);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** asar ingestAgentsEvent does Ae.agents.map — `{action:"created"}` has no
|
|
941
|
+
* agents[] so the picker/tray throws and looks empty. ingestAgentUpserted
|
|
942
|
+
* needs {agent, activeAgentId}. Same payload bumpAgent already sends. */
|
|
943
|
+
function pushCreatedAgent(agent, { select = true } = {}) {
|
|
944
|
+
if (!agent || !agent.id) return;
|
|
945
|
+
if (select) focusedAgentId = agent.id;
|
|
946
|
+
const active = focusedAgentId || agent.id;
|
|
947
|
+
const list = rosterForEvent(cachedAgentList() || [], agentActivity);
|
|
948
|
+
ssePush('agent-upserted', { agent, activeAgentId: active });
|
|
949
|
+
ssePush('agents', { agents: list, activeAgentId: active });
|
|
918
950
|
}
|
|
919
951
|
|
|
920
952
|
/** Grok Bot Helper daemon: GET /local-exec/requests is SSE, POST /local-exec/responses
|
|
@@ -1086,6 +1118,7 @@ function appendLine(agentId, role, text, extra = {}) {
|
|
|
1086
1118
|
timestampMs: ts,
|
|
1087
1119
|
...(nonce ? { clientNonce: nonce } : {}),
|
|
1088
1120
|
requestId,
|
|
1121
|
+
...(extra.promptRaw != null ? { promptRaw: String(extra.promptRaw) } : {}),
|
|
1089
1122
|
};
|
|
1090
1123
|
} else {
|
|
1091
1124
|
e = {
|
|
@@ -1095,6 +1128,7 @@ function appendLine(agentId, role, text, extra = {}) {
|
|
|
1095
1128
|
message: { type: 'text', content: String(text || '') },
|
|
1096
1129
|
timestampMs: ts,
|
|
1097
1130
|
requestId,
|
|
1131
|
+
...(extra.author && typeof extra.author === 'object' ? { author: extra.author } : {}),
|
|
1098
1132
|
};
|
|
1099
1133
|
}
|
|
1100
1134
|
t.entries.push(e);
|
|
@@ -1108,29 +1142,106 @@ function fanoutLine(primaryId, role, text, extra = {}) {
|
|
|
1108
1142
|
}
|
|
1109
1143
|
function mintLocalAgent(parsed = {}) {
|
|
1110
1144
|
const id = String(parsed.id || randomUUID());
|
|
1111
|
-
const
|
|
1112
|
-
const agent = {
|
|
1145
|
+
const prev = (cachedAgentList() || []).find((a) => a.id === id) || {};
|
|
1146
|
+
const agent = shapeAgent({
|
|
1147
|
+
...prev,
|
|
1148
|
+
...parsed,
|
|
1113
1149
|
id,
|
|
1114
|
-
|
|
1115
|
-
description: String(parsed.description || ''),
|
|
1116
|
-
title: String(parsed.title || name),
|
|
1117
|
-
origin: parsed.origin || 'user',
|
|
1118
|
-
createdAt: Date.now(),
|
|
1150
|
+
createdAt: prev.createdAt || parsed.createdAt || Date.now(),
|
|
1119
1151
|
updatedAt: Date.now(),
|
|
1120
|
-
|
|
1121
|
-
avatarColor: parsed.avatarColor || null,
|
|
1122
|
-
path: `/local/${id}`,
|
|
1123
|
-
};
|
|
1152
|
+
});
|
|
1124
1153
|
const list = cachedAgentList() || [];
|
|
1125
1154
|
if (!list.some((a) => a.id === id)) list.unshift(agent);
|
|
1126
1155
|
else {
|
|
1127
1156
|
const i = list.findIndex((a) => a.id === id);
|
|
1128
1157
|
list[i] = { ...list[i], ...agent };
|
|
1129
1158
|
}
|
|
1130
|
-
saveAgents(list);
|
|
1159
|
+
saveAgents(list.map(shapeAgent));
|
|
1131
1160
|
agentTranscript(id);
|
|
1132
1161
|
return agent;
|
|
1133
1162
|
}
|
|
1163
|
+
function groupMemberIds(agentId) {
|
|
1164
|
+
const a = (cachedAgentList() || []).find((x) => x.id === agentId);
|
|
1165
|
+
if (!a?.isGroup) return [];
|
|
1166
|
+
return (a.memberIds || []).map(String).filter(Boolean);
|
|
1167
|
+
}
|
|
1168
|
+
function groupMemberRecords(agentId) {
|
|
1169
|
+
const roster = cachedAgentList() || [];
|
|
1170
|
+
return groupMemberIds(agentId).map((mid) => {
|
|
1171
|
+
const m = roster.find((x) => x.id === mid);
|
|
1172
|
+
return { id: mid, name: String(m?.name || mid) };
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export function groupReplyIsPass(text) {
|
|
1177
|
+
const s = stripSpendFooter(String(text || '')).replace(/^\s*[A-Za-z0-9 _.-]{1,40}:\s*/, '').trim();
|
|
1178
|
+
if (!s) return true;
|
|
1179
|
+
if (/^PASS\b/i.test(s)) return true;
|
|
1180
|
+
if (s.length < 20) return true;
|
|
1181
|
+
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);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const GROUP_MAX_ROUNDS = Math.min(12, Math.max(2, Number(process.env.OZ_GROUP_MAX_ROUNDS || 8)));
|
|
1185
|
+
const GROUP_MAX_CALLS = Math.min(24, Math.max(4, Number(process.env.OZ_GROUP_MAX_CALLS || 16)));
|
|
1186
|
+
|
|
1187
|
+
/** Members speak, then keep peek/ponging until they PASS or hit the cap. */
|
|
1188
|
+
async function runGroupQueue({ agentId, humanPrompt, parsed, nonce, log }) {
|
|
1189
|
+
const named = groupMemberRecords(agentId);
|
|
1190
|
+
if (!named.length) return false;
|
|
1191
|
+
const names = named.map((m) => m.name);
|
|
1192
|
+
let calls = 0;
|
|
1193
|
+
|
|
1194
|
+
const oneTurn = async (m, phase) => {
|
|
1195
|
+
const others = names.filter((n) => n !== m.name).join(', ') || 'the group';
|
|
1196
|
+
const persona = phase === 'peek'
|
|
1197
|
+
? `You are ${m.name} in a group with ${others}. Continue ping/pong with the other members using the prior turns. Do not speak as the human. Do not prefix with visitor shortnames. If the exchange has reached a natural conclusion or you have nothing to add, reply with exactly PASS and nothing else. Otherwise one short in-character turn.`
|
|
1198
|
+
: `You are ${m.name} in a group with ${others}. Answer the human as yourself and leave a hook the others can ping. Do not speak as the human. Do not prefix with visitor shortnames. A few sentences.\n\nThe human said: ${humanPrompt}`;
|
|
1199
|
+
let bit = '';
|
|
1200
|
+
try {
|
|
1201
|
+
bit = (await zooComplete(persona, log, agentId, parsed)).text;
|
|
1202
|
+
} catch (e) {
|
|
1203
|
+
bit = `openzoo error (${m.name}): ${e.message}`;
|
|
1204
|
+
log(`cursor-backend: group ${phase} ${m.id} failed: ${e.message}`);
|
|
1205
|
+
}
|
|
1206
|
+
calls += 1;
|
|
1207
|
+
const pass = groupReplyIsPass(bit);
|
|
1208
|
+
if (!pass) {
|
|
1209
|
+
const line = fanoutLine(agentId, 'assistant', bit, {
|
|
1210
|
+
clientNonce: nonce,
|
|
1211
|
+
requestId: nonce,
|
|
1212
|
+
author: { kind: 'agent', id: m.id, name: m.name },
|
|
1213
|
+
});
|
|
1214
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
1215
|
+
bumpAgent(agentId, { preview: bit, notify: true });
|
|
1216
|
+
log(`cursor-backend: group ${phase} ${m.name} seq=${line.seq}`);
|
|
1217
|
+
} else {
|
|
1218
|
+
log(`cursor-backend: group ${phase} ${m.name} PASS`);
|
|
1219
|
+
}
|
|
1220
|
+
return pass;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
for (const m of named) {
|
|
1224
|
+
if (calls >= GROUP_MAX_CALLS) break;
|
|
1225
|
+
await oneTurn(m, 'speak');
|
|
1226
|
+
}
|
|
1227
|
+
if (named.length < 2) return true;
|
|
1228
|
+
|
|
1229
|
+
let round = 0;
|
|
1230
|
+
while (round < GROUP_MAX_ROUNDS && calls < GROUP_MAX_CALLS) {
|
|
1231
|
+
round += 1;
|
|
1232
|
+
let passes = 0;
|
|
1233
|
+
for (const m of named) {
|
|
1234
|
+
if (calls >= GROUP_MAX_CALLS) break;
|
|
1235
|
+
if (await oneTurn(m, 'peek')) passes += 1;
|
|
1236
|
+
}
|
|
1237
|
+
if (passes >= named.length) {
|
|
1238
|
+
log(`cursor-backend: group concluded round=${round} calls=${calls}`);
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
log(`cursor-backend: group queue done rounds=${round} calls=${calls}`);
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1134
1245
|
function mergeAgentLists(remote) {
|
|
1135
1246
|
const local = cachedAgentList() || [];
|
|
1136
1247
|
const seen = new Set();
|
|
@@ -1143,12 +1254,15 @@ function mergeAgentLists(remote) {
|
|
|
1143
1254
|
return sortAgentsByActivity(out);
|
|
1144
1255
|
}
|
|
1145
1256
|
function gatewayEntry(e) {
|
|
1146
|
-
const { seq, pulledRemote, ...rest } = e;
|
|
1257
|
+
const { seq, pulledRemote, promptRaw, ...rest } = e;
|
|
1147
1258
|
return rest;
|
|
1148
1259
|
}
|
|
1149
1260
|
|
|
1150
1261
|
function stripSpendFooter(s) {
|
|
1151
|
-
return String(s || '')
|
|
1262
|
+
return String(s || '')
|
|
1263
|
+
.replace(/\n{2,}::oz-spend::[\s\S]*$/i, '')
|
|
1264
|
+
.replace(/\n{2,}this call \$[\d.]+[\s\S]*$/i, '')
|
|
1265
|
+
.trimEnd();
|
|
1152
1266
|
}
|
|
1153
1267
|
|
|
1154
1268
|
function entryPlainText(v) {
|
|
@@ -1176,16 +1290,26 @@ function historyMessages(agentId, currentPrompt) {
|
|
|
1176
1290
|
text = entryPlainText(e.message ?? e.content ?? e.text);
|
|
1177
1291
|
role = 'assistant';
|
|
1178
1292
|
text = stripSpendFooter(text);
|
|
1293
|
+
const who = e.author && e.author.name ? String(e.author.name).trim() : '';
|
|
1294
|
+
if (who && text && !text.toLowerCase().startsWith(`${who.toLowerCase()}:`)) {
|
|
1295
|
+
text = `${who}: ${text}`;
|
|
1296
|
+
}
|
|
1179
1297
|
} else if (e.kind === 'message' || e.role === 'user' || e.kind === 'user') {
|
|
1298
|
+
// Keep `shortname: ` on the turn the model sees -- stripping it made every
|
|
1299
|
+
// visitor look like one anonymous "you" (rex asked "am I still rex").
|
|
1180
1300
|
text = entryPlainText(e.content ?? e.message ?? e.text ?? e.prompt);
|
|
1301
|
+
if (!String(text || '').trim() && e.promptRaw != null) text = String(e.promptRaw);
|
|
1181
1302
|
role = 'user';
|
|
1182
1303
|
}
|
|
1183
1304
|
text = String(text || '').trim();
|
|
1184
1305
|
if (!role || !text) continue;
|
|
1185
1306
|
out.push({ role, content: text });
|
|
1186
1307
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1308
|
+
const curBare = stripVisitorLabel(cur).trim();
|
|
1309
|
+
while (out.length && out[out.length - 1].role === 'user') {
|
|
1310
|
+
const last = String(out[out.length - 1].content).trim();
|
|
1311
|
+
if (last === cur || stripVisitorLabel(last).trim() === curBare) out.pop();
|
|
1312
|
+
else break;
|
|
1189
1313
|
}
|
|
1190
1314
|
let chars = 0;
|
|
1191
1315
|
const kept = [];
|
|
@@ -1268,6 +1392,33 @@ function ingestRemoteEntries(agentId, entries) {
|
|
|
1268
1392
|
return incoming.length;
|
|
1269
1393
|
}
|
|
1270
1394
|
|
|
1395
|
+
function visitorFromSend(parsed) {
|
|
1396
|
+
const v = parsed && parsed.visitor;
|
|
1397
|
+
if (!v || typeof v !== 'object') return null;
|
|
1398
|
+
const id = String(v.id || '').trim();
|
|
1399
|
+
const shortname = String(v.shortname || v.name || '').trim().toLowerCase();
|
|
1400
|
+
const color = String(v.color || '').trim();
|
|
1401
|
+
if (!id || !/^[a-z][a-z0-9]{1,15}$/.test(shortname)) return null;
|
|
1402
|
+
return { id, shortname, color };
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/** UI stores `shortname: prompt`; zooComplete / history must see the raw line. */
|
|
1406
|
+
function stripVisitorLabel(text) {
|
|
1407
|
+
const s = String(text || '');
|
|
1408
|
+
const m = s.match(/^([a-z][a-z0-9]{1,15}):\s+/);
|
|
1409
|
+
if (!m) return s;
|
|
1410
|
+
if (/^(https?|ftp|mailto|file|data)$/i.test(m[1])) return s;
|
|
1411
|
+
return s.slice(m[0].length);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function labeledVisitorPrompt(visitor, prompt) {
|
|
1415
|
+
const p = String(prompt || '');
|
|
1416
|
+
if (!visitor?.shortname) return p;
|
|
1417
|
+
const prefix = `${visitor.shortname}: `;
|
|
1418
|
+
if (p.startsWith(prefix)) return p;
|
|
1419
|
+
return prefix + p;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1271
1422
|
function promptFromSendBody(raw) {
|
|
1272
1423
|
let obj = raw;
|
|
1273
1424
|
if (Buffer.isBuffer(raw) || typeof raw === 'string') {
|
|
@@ -1292,7 +1443,7 @@ function promptFromSendBody(raw) {
|
|
|
1292
1443
|
|
|
1293
1444
|
let walletUsdCache = { usd: null, at: 0 };
|
|
1294
1445
|
async function walletUsdCached() {
|
|
1295
|
-
if (walletUsdCache.usd != null && Date.now() - walletUsdCache.at <
|
|
1446
|
+
if (walletUsdCache.usd != null && Date.now() - walletUsdCache.at < 8_000) return walletUsdCache.usd;
|
|
1296
1447
|
try {
|
|
1297
1448
|
const { affordableUsd } = await import('./info.js');
|
|
1298
1449
|
const n = await Promise.race([
|
|
@@ -1327,13 +1478,16 @@ async function zooSpendOverlay(data) {
|
|
|
1327
1478
|
const bal = Number.isFinite(wallet) && wallet > 0.004
|
|
1328
1479
|
? wallet
|
|
1329
1480
|
: (Number.isFinite(credit) && credit > 0.004 ? credit : null);
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1481
|
+
return formatSpendFooter({
|
|
1482
|
+
billedUsd: x.billedUsd,
|
|
1483
|
+
directUsd: x.directUsd,
|
|
1484
|
+
spent,
|
|
1485
|
+
would,
|
|
1486
|
+
saved,
|
|
1487
|
+
pct,
|
|
1488
|
+
balance: bal,
|
|
1489
|
+
x402: x,
|
|
1490
|
+
});
|
|
1337
1491
|
}
|
|
1338
1492
|
|
|
1339
1493
|
const MODELS_PATH = path.join(os.homedir(), '.openzoo', 'grokbot-models.json');
|
|
@@ -1567,12 +1721,34 @@ function zooTextFromMessage(msg, data) {
|
|
|
1567
1721
|
async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
1568
1722
|
const model = currentModel(agentId);
|
|
1569
1723
|
const helper = localExecSse.size > 0;
|
|
1724
|
+
const visitor = visitorFromSend(parsed);
|
|
1725
|
+
const chatOnly = Boolean(visitor);
|
|
1726
|
+
const spoken = visitor && !/^You are /.test(String(prompt || ''))
|
|
1727
|
+
? labeledVisitorPrompt(visitor, prompt)
|
|
1728
|
+
: prompt;
|
|
1570
1729
|
await ensureTranscriptHydrated(agentId, log);
|
|
1571
|
-
log(`cursor-backend: zoo POST :8402 model=${model} helper=${helper ? localExecSse.size : 0} hist=${historyMessages(agentId,
|
|
1730
|
+
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))}`);
|
|
1572
1731
|
|
|
1573
1732
|
const images = [];
|
|
1574
1733
|
const textFiles = [];
|
|
1575
1734
|
for (const { raw, name } of attachmentList(parsed, prompt)) {
|
|
1735
|
+
const uploaded = lookupUpload(raw);
|
|
1736
|
+
if (uploaded) {
|
|
1737
|
+
if (uploaded.mime && uploaded.mime.startsWith('image/')) {
|
|
1738
|
+
images.push({
|
|
1739
|
+
path: raw,
|
|
1740
|
+
mime: uploaded.mime,
|
|
1741
|
+
dataUrl: `data:${uploaded.mime};base64,${uploaded.buf.toString('base64')}`,
|
|
1742
|
+
});
|
|
1743
|
+
log(`cursor-backend: attached image ${raw} ${uploaded.mime} ${uploaded.bytes}b`);
|
|
1744
|
+
} else {
|
|
1745
|
+
textFiles.push({ path: raw, abs: uploaded.abs, text: uploaded.buf.toString('utf8') });
|
|
1746
|
+
}
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
// Public visitors must not trigger local file reads on this Mac.
|
|
1750
|
+
// Uploads above are bytes they posted to us, not a host path.
|
|
1751
|
+
if (chatOnly) continue;
|
|
1576
1752
|
const abs = expandUserPath(raw);
|
|
1577
1753
|
try {
|
|
1578
1754
|
const buf = await readLocalBytes(abs, log);
|
|
@@ -1595,20 +1771,29 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1595
1771
|
const messages = [
|
|
1596
1772
|
{
|
|
1597
1773
|
role: 'system',
|
|
1598
|
-
content:
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
'
|
|
1607
|
-
|
|
1608
|
-
|
|
1774
|
+
content: chatOnly
|
|
1775
|
+
? [
|
|
1776
|
+
`You are ${model} served through openzoo inside Grok Bot.`,
|
|
1777
|
+
'This is a public visitor chat. You do not have filesystem, shell, or local-exec access on the host Mac.',
|
|
1778
|
+
'Reply in chat only. Do not claim you will write files, run commands, or use local tools.',
|
|
1779
|
+
'Each human line is prefixed with that visitor\'s shortname and a colon, like "rex: hello". Different shortnames are different people. Address them by that name.',
|
|
1780
|
+
'A spend footer is appended after your reply by the host -- ignore it.',
|
|
1781
|
+
'Prior turns of THIS Grok Bot chat are in the messages below. Do not claim the thread starts blank or that earlier questions did not arrive.',
|
|
1782
|
+
].join(' ')
|
|
1783
|
+
: [
|
|
1784
|
+
`You are ${model} served through openzoo inside Grok Bot.`,
|
|
1785
|
+
`You HAVE local tools on the user's computer via ${via}.`,
|
|
1786
|
+
'Tools: read_file, write_file, exec, list_dir. USE THEM. Write files to disk instead of pasting giant HTML into chat.',
|
|
1787
|
+
'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
|
|
1788
|
+
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
1789
|
+
'After using tools you MUST still write a normal chat reply: what you did, file paths written, and what to open. Empty content is a bug.',
|
|
1790
|
+
'Do not stop mid-task. Do not write "Stopped on research", "nothing to open yet", "no app files written this turn", or "say go again". Keep using tools until the files the user asked for exist on disk THIS turn. Summarize only after those writes succeed.',
|
|
1791
|
+
'A spend footer is appended after your reply by the host — ignore it.',
|
|
1792
|
+
'Prior turns of THIS Grok Bot chat are in the messages below. Do not claim the thread starts blank or that earlier questions did not arrive.',
|
|
1793
|
+
].join(' '),
|
|
1609
1794
|
},
|
|
1610
1795
|
];
|
|
1611
|
-
for (const m of historyMessages(agentId,
|
|
1796
|
+
for (const m of historyMessages(agentId, spoken)) messages.push(m);
|
|
1612
1797
|
if (textFiles.length) {
|
|
1613
1798
|
const bits = textFiles.map((a) => (
|
|
1614
1799
|
a.error
|
|
@@ -1622,7 +1807,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1622
1807
|
for (const img of images) {
|
|
1623
1808
|
userContent.push({ type: 'image_url', image_url: { url: img.dataUrl } });
|
|
1624
1809
|
}
|
|
1625
|
-
userContent.push({ type: 'text', text:
|
|
1810
|
+
userContent.push({ type: 'text', text: spoken || (images.length ? '(see attached image)' : 'hello') });
|
|
1626
1811
|
messages.push({
|
|
1627
1812
|
role: 'user',
|
|
1628
1813
|
content: userContent.length === 1 && userContent[0].type === 'text'
|
|
@@ -1633,6 +1818,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1633
1818
|
const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 8192);
|
|
1634
1819
|
const maxSteps = Math.min(64, Math.max(8, Number(process.env.OPENZOO_ASK_TOOL_STEPS || 32)));
|
|
1635
1820
|
let lastData = {};
|
|
1821
|
+
let turnX402 = {};
|
|
1636
1822
|
let text = '';
|
|
1637
1823
|
const usedTools = [];
|
|
1638
1824
|
const KEEP_GOING = 'Do not stop. Do not wait for another message. Write the files to disk now with write_file / exec. Keep going until the requested paths exist. A summary is only allowed after the files are written.';
|
|
@@ -1687,17 +1873,29 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1687
1873
|
};
|
|
1688
1874
|
let keepGoingNudge = 0;
|
|
1689
1875
|
for (let step = 0; step < maxSteps; step++) {
|
|
1690
|
-
const {
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
});
|
|
1876
|
+
const payload = { model, messages, max_tokens: maxTok };
|
|
1877
|
+
if (!chatOnly) {
|
|
1878
|
+
payload.tools = LOCAL_TOOLS;
|
|
1879
|
+
payload.tool_choice = 'auto';
|
|
1880
|
+
}
|
|
1881
|
+
const { r, data } = await zooPost(payload);
|
|
1697
1882
|
lastData = data;
|
|
1883
|
+
turnX402 = mergeTurnProof(turnX402, data);
|
|
1698
1884
|
const msg = data.choices?.[0]?.message || {};
|
|
1699
1885
|
const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
1700
1886
|
if (calls.length) {
|
|
1887
|
+
if (chatOnly) {
|
|
1888
|
+
log(`cursor-backend: visitor chat-only ignored ${calls.length} tool calls`);
|
|
1889
|
+
messages.push(msg);
|
|
1890
|
+
for (const c of calls) {
|
|
1891
|
+
messages.push({
|
|
1892
|
+
role: 'tool',
|
|
1893
|
+
tool_call_id: c.id,
|
|
1894
|
+
content: 'local tools are not available to web visitors',
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1701
1899
|
log(`cursor-backend: zoo tools step=${step} n=${calls.length} ${calls.map((c) => c.function?.name || c.name).join(',')}`);
|
|
1702
1900
|
messages.push(msg);
|
|
1703
1901
|
for (const c of calls) {
|
|
@@ -1724,7 +1922,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1724
1922
|
log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${finish}`);
|
|
1725
1923
|
// Model likes to park after research ("Stopped on research", "say go again")
|
|
1726
1924
|
// instead of writing files. Measured 2026-08-30 on volume track00r.
|
|
1727
|
-
if ((finish === 'length' || looksStoppedReply(text)) && keepGoingNudge < 6) {
|
|
1925
|
+
if (!chatOnly && (finish === 'length' || looksStoppedReply(text)) && keepGoingNudge < 6) {
|
|
1728
1926
|
keepGoingNudge += 1;
|
|
1729
1927
|
log(`cursor-backend: keep-going nudge=${keepGoingNudge} finish=${finish}`);
|
|
1730
1928
|
messages.push(msg);
|
|
@@ -1747,6 +1945,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1747
1945
|
max_tokens: Math.max(800, Math.min(maxTok, 2048)),
|
|
1748
1946
|
});
|
|
1749
1947
|
lastData = data;
|
|
1948
|
+
turnX402 = mergeTurnProof(turnX402, data);
|
|
1750
1949
|
text = zooTextFromMessage(data.choices?.[0]?.message, data);
|
|
1751
1950
|
log(`cursor-backend: << zoo summary ${r.status} ${text.length}c`);
|
|
1752
1951
|
}
|
|
@@ -1763,6 +1962,9 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1763
1962
|
text = '(empty zoo reply)';
|
|
1764
1963
|
}
|
|
1765
1964
|
}
|
|
1965
|
+
if (lastData && typeof lastData === 'object') {
|
|
1966
|
+
lastData.x402 = mergeTurnProof(turnX402, lastData);
|
|
1967
|
+
}
|
|
1766
1968
|
try { text += await zooSpendOverlay(lastData); } catch { /* overlay must never eat the reply */ }
|
|
1767
1969
|
return { text, data: lastData };
|
|
1768
1970
|
}
|
|
@@ -1796,6 +1998,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1796
1998
|
});
|
|
1797
1999
|
res.write('data: {"channel":"ping","payload":{}}\n\n');
|
|
1798
2000
|
sseClients.add(res);
|
|
2001
|
+
try {
|
|
2002
|
+
const list = rosterForEvent(loadAgents() || [], agentActivity);
|
|
2003
|
+
const active = topConversationId(list);
|
|
2004
|
+
if (active) {
|
|
2005
|
+
focusedAgentId = focusedAgentId || active;
|
|
2006
|
+
res.write(`data: ${JSON.stringify({
|
|
2007
|
+
channel: 'agents',
|
|
2008
|
+
payload: {
|
|
2009
|
+
agents: list.map((a) => ({ ...a, isActive: a.id === active })),
|
|
2010
|
+
activeAgentId: active,
|
|
2011
|
+
},
|
|
2012
|
+
})}\n\n`);
|
|
2013
|
+
}
|
|
2014
|
+
} catch { /* first paint must not break SSE */ }
|
|
1799
2015
|
const iv = setInterval(() => {
|
|
1800
2016
|
try { res.write('data: {"channel":"ping","payload":{}}\n\n'); } catch { clearInterval(iv); sseClients.delete(res); }
|
|
1801
2017
|
}, 15000);
|
|
@@ -1816,6 +2032,46 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1816
2032
|
log('cursor-backend: -> getHostStatus ready');
|
|
1817
2033
|
return true;
|
|
1818
2034
|
}
|
|
2035
|
+
if (name === 'uploadAttachment' || name === 'readAttachmentImage'
|
|
2036
|
+
|| name === 'readAttachmentText' || name === 'readAttachmentChunk') {
|
|
2037
|
+
let parsed = {};
|
|
2038
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
2039
|
+
if (name === 'uploadAttachment') {
|
|
2040
|
+
const got = ingestUpload({
|
|
2041
|
+
filename: parsed.filename,
|
|
2042
|
+
bytesBase64: parsed.bytesBase64,
|
|
2043
|
+
});
|
|
2044
|
+
if (!got.ok) {
|
|
2045
|
+
jsonSend(res, { status: 'ok', value: null, ok: false, reason: got.reason });
|
|
2046
|
+
log(`cursor-backend: uploadAttachment ${got.reason || 'failed'}`);
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
// Dual shape: sendPrompt-style raw `.path` AND CVr `{status,value}`.
|
|
2050
|
+
jsonSend(res, { status: 'ok', value: { path: got.path }, path: got.path, ok: true });
|
|
2051
|
+
log(`cursor-backend: uploadAttachment ${got.path} ${got.bytes}b ${got.mime}`);
|
|
2052
|
+
return true;
|
|
2053
|
+
}
|
|
2054
|
+
if (name === 'readAttachmentChunk') {
|
|
2055
|
+
const chunk = readUploadChunk({
|
|
2056
|
+
path: parsed.path,
|
|
2057
|
+
offset: parsed.offset,
|
|
2058
|
+
length: parsed.length,
|
|
2059
|
+
});
|
|
2060
|
+
jsonSend(res, chunk
|
|
2061
|
+
? { status: 'ok', value: chunk, ...chunk }
|
|
2062
|
+
: { status: 'ok', value: null });
|
|
2063
|
+
log(`cursor-backend: readAttachmentChunk ${parsed.path || ''} ${chunk ? chunk.totalSize : 'miss'}b`);
|
|
2064
|
+
return true;
|
|
2065
|
+
}
|
|
2066
|
+
if (name === 'readAttachmentImage') {
|
|
2067
|
+
const img = readUploadImage(parsed.path);
|
|
2068
|
+
jsonSend(res, img ? { status: 'ok', value: img, ...img } : { status: 'ok', value: null });
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
2071
|
+
const text = readUploadText(parsed.path);
|
|
2072
|
+
jsonSend(res, text ? { status: 'ok', value: text, ...text } : { status: 'ok', value: null });
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
1819
2075
|
// Roster/settings come from the REAL 1340 gateway (names, avatars, trays).
|
|
1820
2076
|
// Chat stays local so inference is zoo. Discovered on EnsureSandBox rewrite.
|
|
1821
2077
|
const roster = new Set([
|
|
@@ -1825,8 +2081,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1825
2081
|
'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
|
|
1826
2082
|
'isEgressTunnelAvailable', 'listBoxMcpServers',
|
|
1827
2083
|
'setWindowFocused', 'getAgentAutomations',
|
|
1828
|
-
'
|
|
1829
|
-
'interruptAgentRun', 'requestDiskSaverAudit', 'broadcastToAgents',
|
|
2084
|
+
'interruptAgentRun', 'requestDiskSaverAudit',
|
|
1830
2085
|
'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
|
|
1831
2086
|
'setAgentNotifyOnUpdates', 'setAgentAvatarBytes', 'getAgentAvatar',
|
|
1832
2087
|
]);
|
|
@@ -1841,15 +2096,82 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1841
2096
|
}
|
|
1842
2097
|
const agent = mintLocalAgent(parsed);
|
|
1843
2098
|
jsonSend(res, { agent, id: agent.id, ...agent });
|
|
1844
|
-
|
|
2099
|
+
pushCreatedAgent(agent);
|
|
1845
2100
|
log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
|
|
1846
2101
|
return true;
|
|
1847
2102
|
}
|
|
2103
|
+
if (name === 'kickstartAgent') {
|
|
2104
|
+
let parsed = {};
|
|
2105
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
2106
|
+
const id = String(parsed.id || parsed.agentId || '');
|
|
2107
|
+
jsonSend(res, { id, isIntroductionInFlight: false });
|
|
2108
|
+
log(`cursor-backend: kickstartAgent id=${id || '?'}`);
|
|
2109
|
+
return true;
|
|
2110
|
+
}
|
|
2111
|
+
if (name === 'createGroup') {
|
|
2112
|
+
let parsed = {};
|
|
2113
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
2114
|
+
const memberIds = [].concat(parsed.memberAgentIds || parsed.memberIds || []).map(String).filter(Boolean);
|
|
2115
|
+
const agent = mintLocalAgent({
|
|
2116
|
+
...parsed,
|
|
2117
|
+
isGroup: true,
|
|
2118
|
+
memberIds,
|
|
2119
|
+
name: parsed.name || parsed.title || 'group',
|
|
2120
|
+
});
|
|
2121
|
+
jsonSend(res, { agent, id: agent.id, ...agent });
|
|
2122
|
+
pushCreatedAgent(agent);
|
|
2123
|
+
log(`cursor-backend: createGroup local id=${agent.id} members=${memberIds.length} name=${JSON.stringify(agent.name)}`);
|
|
2124
|
+
return true;
|
|
2125
|
+
}
|
|
2126
|
+
if (name === 'setGroupMembers') {
|
|
2127
|
+
let parsed = {};
|
|
2128
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
2129
|
+
const id = String(parsed.id || parsed.agentId || '');
|
|
2130
|
+
const memberIds = [].concat(parsed.memberAgentIds || parsed.memberIds || []).map(String).filter(Boolean);
|
|
2131
|
+
const list = cachedAgentList() || [];
|
|
2132
|
+
const i = list.findIndex((a) => a.id === id);
|
|
2133
|
+
if (!id || i < 0) {
|
|
2134
|
+
jsonSend(res, null);
|
|
2135
|
+
log(`cursor-backend: setGroupMembers miss id=${id || '?'}`);
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
const agent = shapeAgent({ ...list[i], isGroup: true, memberIds, updatedAt: Date.now() });
|
|
2139
|
+
list[i] = agent;
|
|
2140
|
+
saveAgents(list.map(shapeAgent));
|
|
2141
|
+
jsonSend(res, { agent, ...agent });
|
|
2142
|
+
ssePush('agent-upserted', { agent, activeAgentId: focusedAgentId || id });
|
|
2143
|
+
ssePush('agents', { agents: rosterForEvent(list, agentActivity), activeAgentId: focusedAgentId || id });
|
|
2144
|
+
log(`cursor-backend: setGroupMembers id=${id} n=${memberIds.length}`);
|
|
2145
|
+
return true;
|
|
2146
|
+
}
|
|
2147
|
+
if (name === 'broadcastToAgents') {
|
|
2148
|
+
let parsed = {};
|
|
2149
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
2150
|
+
const ids = [].concat(parsed.ids || parsed.agentIds || []).map(String).filter(Boolean);
|
|
2151
|
+
const prompt = String(parsed.prompt || parsed.text || parsed.message || '').trim();
|
|
2152
|
+
jsonSend(res, { ok: true, accepted: true, ids });
|
|
2153
|
+
const target = ids[0] && groupMemberIds(ids[0]).length ? ids[0] : (focusedAgentId || ids[0]);
|
|
2154
|
+
if (target && prompt) {
|
|
2155
|
+
const nonce = parsed.clientNonce || `oz-bc-${Date.now()}`;
|
|
2156
|
+
setImmediate(() => {
|
|
2157
|
+
runGroupQueue({
|
|
2158
|
+
agentId: target,
|
|
2159
|
+
humanPrompt: prompt,
|
|
2160
|
+
parsed,
|
|
2161
|
+
nonce,
|
|
2162
|
+
log,
|
|
2163
|
+
}).catch((e) => log(`cursor-backend: broadcast queue ${e.message}`));
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
log(`cursor-backend: broadcastToAgents ids=${ids.length} target=${target || 'none'}`);
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
1848
2169
|
if (name === 'updateAgent') {
|
|
1849
2170
|
let parsed = {};
|
|
1850
2171
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
1851
2172
|
const agent = mintLocalAgent(parsed);
|
|
1852
2173
|
jsonSend(res, { agent, ...agent });
|
|
2174
|
+
pushCreatedAgent(agent, { select: false });
|
|
1853
2175
|
log(`cursor-backend: updateAgent local id=${agent.id}`);
|
|
1854
2176
|
return true;
|
|
1855
2177
|
}
|
|
@@ -1880,17 +2202,43 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1880
2202
|
return true;
|
|
1881
2203
|
}
|
|
1882
2204
|
if (name === 'listAgents' || name === 'getTrays' || name === 'countAgents' || name === 'searchAgents') {
|
|
1883
|
-
if (!sniffOn()) {
|
|
2205
|
+
if (!sniffOn() && !useHouseRoster()) {
|
|
1884
2206
|
const pod = await waitForAccountPod(log);
|
|
1885
2207
|
if (pod?.agent) {
|
|
1886
2208
|
const proxied = await proxyPodHttp(req, res, full, body, log);
|
|
1887
2209
|
if (proxied) return true;
|
|
1888
2210
|
}
|
|
1889
2211
|
}
|
|
1890
|
-
if (name === '
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2212
|
+
if (name === 'getTrays') {
|
|
2213
|
+
jsonSend(res, []);
|
|
2214
|
+
return true;
|
|
2215
|
+
}
|
|
2216
|
+
if (name === 'listAgents' || name === 'countAgents' || name === 'searchAgents') {
|
|
2217
|
+
const list = rosterForEvent(mergeAgentLists([]), agentActivity);
|
|
2218
|
+
if (name === 'countAgents') {
|
|
2219
|
+
jsonSend(res, list.length);
|
|
2220
|
+
log(`cursor-backend: countAgents local n=${list.length} account=${activeAccountId || 'none'}`);
|
|
2221
|
+
return true;
|
|
2222
|
+
}
|
|
2223
|
+
if (name === 'searchAgents') {
|
|
2224
|
+
let parsed = {};
|
|
2225
|
+
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
2226
|
+
const q = String(parsed.query || parsed.q || '').toLowerCase();
|
|
2227
|
+
const out = !q ? list : list.filter((a) => String(a?.name || a?.title || a?.id || '').toLowerCase().includes(q));
|
|
2228
|
+
jsonSend(res, out);
|
|
2229
|
+
log(`cursor-backend: searchAgents local n=${out.length}/${list.length} account=${activeAccountId || 'none'}`);
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
const active = topConversationId(list);
|
|
2233
|
+
if (active && !focusedAgentId) focusedAgentId = active;
|
|
2234
|
+
jsonSend(res, list.map((a) => ({ ...a, isActive: !!(active && a.id === active) })));
|
|
2235
|
+
if (active) {
|
|
2236
|
+
ssePush('agents', {
|
|
2237
|
+
agents: list.map((a) => ({ ...a, isActive: a.id === active })),
|
|
2238
|
+
activeAgentId: active,
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
log(`cursor-backend: listAgents local n=${list.length} account=${activeAccountId || 'none'} active=${active || 'none'}`);
|
|
1894
2242
|
return true;
|
|
1895
2243
|
}
|
|
1896
2244
|
}
|
|
@@ -1905,17 +2253,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1905
2253
|
if (name === 'sendPrompt') {
|
|
1906
2254
|
let parsed = {};
|
|
1907
2255
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
1908
|
-
const prompt = promptFromSendBody(parsed);
|
|
2256
|
+
const prompt = stripVisitorLabel(promptFromSendBody(parsed));
|
|
2257
|
+
const visitor = visitorFromSend(parsed);
|
|
1909
2258
|
const agentId = String(parsed.agentId || parsed.id || 'openzoo');
|
|
1910
2259
|
const nonce = parsed.clientNonce || `oz-${Date.now()}`;
|
|
1911
2260
|
const attN = Array.isArray(parsed.attachmentPaths) ? parsed.attachmentPaths.length : 0;
|
|
1912
|
-
|
|
2261
|
+
const uiText = visitor ? labeledVisitorPrompt(visitor, prompt) : prompt;
|
|
2262
|
+
log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN}${visitor ? ` visitor=${visitor.shortname}` : ''} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
|
|
1913
2263
|
lastSendEchoId = String(nonce);
|
|
1914
2264
|
jsonSend(res, { accepted: true });
|
|
1915
|
-
const userLine = fanoutLine(agentId, 'user',
|
|
2265
|
+
const userLine = fanoutLine(agentId, 'user', uiText, {
|
|
1916
2266
|
clientNonce: nonce,
|
|
1917
2267
|
requestId: nonce,
|
|
1918
|
-
richText: parsed.richText,
|
|
2268
|
+
richText: visitor ? prefixVisitorRichText(parsed.richText, visitor.shortname, uiText) : parsed.richText,
|
|
2269
|
+
promptRaw: prompt,
|
|
1919
2270
|
});
|
|
1920
2271
|
noteFocus(agentId);
|
|
1921
2272
|
bumpAgent(agentId, { preview: prompt, notify: false });
|
|
@@ -1923,6 +2274,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1923
2274
|
const modelCmd = /^\s*\/model(?:\s+(\S+))?\s*$/i.exec(prompt || '');
|
|
1924
2275
|
setImmediate(async () => {
|
|
1925
2276
|
let text = '';
|
|
2277
|
+
let grouped = false;
|
|
1926
2278
|
try {
|
|
1927
2279
|
if (modelCmd) {
|
|
1928
2280
|
const want = modelCmd[1];
|
|
@@ -1941,17 +2293,28 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1941
2293
|
}
|
|
1942
2294
|
try { text += await zooSpendOverlay({}); } catch { /* */ }
|
|
1943
2295
|
} else {
|
|
1944
|
-
const
|
|
1945
|
-
|
|
2296
|
+
const members = groupMemberIds(agentId);
|
|
2297
|
+
if (members.length) {
|
|
2298
|
+
grouped = true;
|
|
2299
|
+
await runGroupQueue({ agentId, humanPrompt: prompt, parsed, nonce, log });
|
|
2300
|
+
} else {
|
|
2301
|
+
const z = await zooComplete(prompt, log, agentId, parsed);
|
|
2302
|
+
text = z.text;
|
|
2303
|
+
}
|
|
1946
2304
|
}
|
|
1947
2305
|
} catch (e) {
|
|
2306
|
+
grouped = false;
|
|
1948
2307
|
text = `openzoo error: ${e.message}`;
|
|
1949
2308
|
log(`cursor-backend: sendPrompt zoo failed: ${e.message}`);
|
|
1950
2309
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2310
|
+
if (!grouped) {
|
|
2311
|
+
const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
2312
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
2313
|
+
bumpAgent(agentId, { preview: text, notify: true });
|
|
2314
|
+
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 220))}`);
|
|
2315
|
+
} else {
|
|
2316
|
+
log(`cursor-backend: sendPrompt group done agent=${agentId}`);
|
|
2317
|
+
}
|
|
1955
2318
|
});
|
|
1956
2319
|
return true;
|
|
1957
2320
|
}
|
|
@@ -1962,7 +2325,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1962
2325
|
noteFocus(id);
|
|
1963
2326
|
tailedAgents.add(id);
|
|
1964
2327
|
const t = agentTranscript(id);
|
|
1965
|
-
if (!t.pulledRemote && realPod?.agent) {
|
|
2328
|
+
if (!t.pulledRemote && realPod?.agent && !useHouseRoster()) {
|
|
1966
2329
|
const remote = await podJson('/api/getAgentTranscriptTail', {
|
|
1967
2330
|
id, agentId: id, limit: 200, beforeSeq: parsed.beforeSeq,
|
|
1968
2331
|
}, log);
|
|
@@ -2005,11 +2368,13 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
2005
2368
|
getHostSettings: { settings: {} },
|
|
2006
2369
|
setHostSettings: { ok: true },
|
|
2007
2370
|
setBoxSecrets: { ok: true },
|
|
2008
|
-
listAgents: cachedAgentList()
|
|
2009
|
-
|| [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
|
|
2371
|
+
listAgents: rosterForEvent(cachedAgentList()
|
|
2372
|
+
|| [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })), agentActivity),
|
|
2373
|
+
countAgents: (cachedAgentList() || [...new Set([...transcripts.keys(), ...tailedAgents])]).length,
|
|
2374
|
+
searchAgents: rosterForEvent(cachedAgentList() || [], agentActivity),
|
|
2010
2375
|
getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
|
|
2011
2376
|
getTeachRecordingStatus: { recording: false },
|
|
2012
|
-
getTrays:
|
|
2377
|
+
getTrays: [],
|
|
2013
2378
|
isGlobalSearchEnabled: { enabled: false },
|
|
2014
2379
|
isEgressTunnelAvailable: { available: false },
|
|
2015
2380
|
getSharingState: { sharing: false },
|
|
@@ -2023,7 +2388,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
2023
2388
|
};
|
|
2024
2389
|
const payload = stubs[name] !== undefined ? stubs[name] : { ok: true };
|
|
2025
2390
|
// Live 1340 listAgents is a RAW array, not CVr (measured 104674b 2026-08-29).
|
|
2026
|
-
if (name === 'listAgents') jsonSend(res, payload);
|
|
2391
|
+
if (name === 'listAgents' || name === 'countAgents' || name === 'searchAgents') jsonSend(res, payload);
|
|
2027
2392
|
else jsonApi(res, payload);
|
|
2028
2393
|
log(`cursor-backend: -> pod /api/${name}`);
|
|
2029
2394
|
return true;
|