openzoo 0.50.48 → 0.50.50
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/lib/cursorbackend.js +178 -3
- package/lib/grokbotAccount.js +66 -0
- package/lib/grokcli.js +6 -6
- package/lib/ozSpendChip.js +122 -12
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -40,6 +40,8 @@ import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetG
|
|
|
40
40
|
import {
|
|
41
41
|
accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
|
|
42
42
|
readHouseRoster, houseAgentsPath, shapeAgent, agentBrief, briefFromName,
|
|
43
|
+
readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
|
|
44
|
+
DEFAULT_WAKEUP_PROMPT,
|
|
43
45
|
} from './grokbotAccount.js';
|
|
44
46
|
import { formatSpendFooter, mergeTurnProof } from './spendProof.js';
|
|
45
47
|
import { prefixVisitorRichText } from './grokbotweb.js';
|
|
@@ -1181,10 +1183,132 @@ export function createZooTurnQueue() {
|
|
|
1181
1183
|
inflight.delete(id);
|
|
1182
1184
|
return 1;
|
|
1183
1185
|
},
|
|
1186
|
+
busy(agentId) {
|
|
1187
|
+
return inflight.has(String(agentId || ''));
|
|
1188
|
+
},
|
|
1184
1189
|
};
|
|
1185
1190
|
}
|
|
1186
1191
|
const zooTurns = createZooTurnQueue();
|
|
1187
1192
|
|
|
1193
|
+
const wakeupTimers = new Map();
|
|
1194
|
+
let wakeupLog = () => {};
|
|
1195
|
+
let wakeupsRestored = false;
|
|
1196
|
+
|
|
1197
|
+
function persistWakeups(map) {
|
|
1198
|
+
try { writeWakeups(HOME, map); } catch (e) {
|
|
1199
|
+
wakeupLog(`cursor-backend: wakeups save failed: ${e.message}`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
export function listAgentWakeups() {
|
|
1204
|
+
return readWakeups(HOME);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
export function cancelAgentWakeup(agentId) {
|
|
1208
|
+
const id = String(agentId || '');
|
|
1209
|
+
const t = wakeupTimers.get(id);
|
|
1210
|
+
if (t) {
|
|
1211
|
+
clearTimeout(t);
|
|
1212
|
+
wakeupTimers.delete(id);
|
|
1213
|
+
}
|
|
1214
|
+
const map = readWakeups(HOME);
|
|
1215
|
+
if (!map[id]) return { ok: true, cancelled: false };
|
|
1216
|
+
delete map[id];
|
|
1217
|
+
persistWakeups(map);
|
|
1218
|
+
return { ok: true, cancelled: true, id };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function armWakeup(rec) {
|
|
1222
|
+
const id = rec.agentId;
|
|
1223
|
+
const prev = wakeupTimers.get(id);
|
|
1224
|
+
if (prev) clearTimeout(prev);
|
|
1225
|
+
const delay = Math.max(1000, Number(rec.nextAt) - Date.now());
|
|
1226
|
+
const t = setTimeout(() => {
|
|
1227
|
+
fireAgentWakeup(id).catch((e) => wakeupLog(`cursor-backend: wakeup fire ${id}: ${e.message}`));
|
|
1228
|
+
}, delay);
|
|
1229
|
+
wakeupTimers.set(id, t);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export function scheduleAgentWakeup(agentId, opts = {}) {
|
|
1233
|
+
const id = String(agentId || '').trim();
|
|
1234
|
+
if (!id) return { ok: false, error: 'no agent' };
|
|
1235
|
+
const now = Date.now();
|
|
1236
|
+
const every = opts.every ?? opts.everySec;
|
|
1237
|
+
const rec = shapeWakeup(id, {
|
|
1238
|
+
every,
|
|
1239
|
+
prompt: opts.prompt,
|
|
1240
|
+
lastAt: 0,
|
|
1241
|
+
nextAt: now + parseWakeupEvery(every) * 1000,
|
|
1242
|
+
}, now);
|
|
1243
|
+
const map = readWakeups(HOME);
|
|
1244
|
+
map[id] = rec;
|
|
1245
|
+
persistWakeups(map);
|
|
1246
|
+
armWakeup(rec);
|
|
1247
|
+
wakeupLog(`cursor-backend: wakeup every ${rec.everySec}s agent=${id}`);
|
|
1248
|
+
return { ok: true, ...rec };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
async function fireAgentWakeup(agentId) {
|
|
1252
|
+
const id = String(agentId || '');
|
|
1253
|
+
const map = readWakeups(HOME);
|
|
1254
|
+
const rec = map[id];
|
|
1255
|
+
if (!rec) {
|
|
1256
|
+
wakeupTimers.delete(id);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
rec.lastAt = Date.now();
|
|
1260
|
+
rec.nextAt = rec.lastAt + rec.everySec * 1000;
|
|
1261
|
+
persistWakeups(map);
|
|
1262
|
+
armWakeup(rec);
|
|
1263
|
+
if (zooTurns.busy(id)) {
|
|
1264
|
+
wakeupLog(`cursor-backend: wakeup skip busy agent=${id}`);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const nonce = `oz-wakeup-${id}-${rec.lastAt}`;
|
|
1268
|
+
const prompt = rec.prompt || DEFAULT_WAKEUP_PROMPT;
|
|
1269
|
+
const turn = zooTurns.begin(id, nonce);
|
|
1270
|
+
const userLine = fanoutLine(id, 'user', `[wakeup]\n${prompt}`, { clientNonce: nonce, requestId: nonce });
|
|
1271
|
+
ssePush('transcript', { ...gatewayEntry(userLine), agentId: id });
|
|
1272
|
+
bumpAgent(id, { preview: '[wakeup]', notify: false });
|
|
1273
|
+
wakeupLog(`cursor-backend: wakeup start agent=${id}`);
|
|
1274
|
+
try {
|
|
1275
|
+
const z = await zooComplete(prompt, wakeupLog, id, {}, {
|
|
1276
|
+
signal: turn.signal,
|
|
1277
|
+
onProgress: (note) => {
|
|
1278
|
+
if (!zooTurns.isCurrent(id, nonce)) return;
|
|
1279
|
+
paintChatUpdate(id, note, { clientNonce: nonce, requestId: nonce });
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
if (!zooTurns.isCurrent(id, nonce)) return;
|
|
1283
|
+
const line = fanoutLine(id, 'assistant', z.text, { clientNonce: nonce, requestId: nonce });
|
|
1284
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: id });
|
|
1285
|
+
bumpAgent(id, { preview: z.text, notify: true });
|
|
1286
|
+
wakeupLog(`cursor-backend: wakeup done agent=${id} seq=${line.seq}`);
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
if (!isSupersededError(e)) wakeupLog(`cursor-backend: wakeup failed agent=${id}: ${e.message}`);
|
|
1289
|
+
} finally {
|
|
1290
|
+
zooTurns.end(id, nonce);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
export function restoreAgentWakeups(log = () => {}) {
|
|
1295
|
+
wakeupLog = log;
|
|
1296
|
+
if (wakeupsRestored) return 0;
|
|
1297
|
+
wakeupsRestored = true;
|
|
1298
|
+
const map = readWakeups(HOME);
|
|
1299
|
+
const ids = Object.keys(map);
|
|
1300
|
+
const now = Date.now();
|
|
1301
|
+
ids.forEach((id, i) => {
|
|
1302
|
+
// Do not fire during Grok Bot boot. 10 parallel zoo turns + CDP on the
|
|
1303
|
+
// spinner page left the window on a white disc. First tick is +90s.
|
|
1304
|
+
map[id].nextAt = now + 90_000 + i * 4000;
|
|
1305
|
+
});
|
|
1306
|
+
if (ids.length) persistWakeups(map);
|
|
1307
|
+
for (const id of ids) armWakeup(map[id]);
|
|
1308
|
+
if (ids.length) log(`cursor-backend: wakeups restored ${ids.length}`);
|
|
1309
|
+
return ids.length;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1188
1312
|
export function formatZooProgress({ step, maxSteps, names, command } = {}) {
|
|
1189
1313
|
const tools = Array.isArray(names) ? names.filter(Boolean).join(', ') : String(names || 'tools');
|
|
1190
1314
|
const cmd = command ? ` ${JSON.stringify(String(command).slice(0, 80))}` : '';
|
|
@@ -1195,7 +1319,7 @@ export function formatZooProgress({ step, maxSteps, names, command } = {}) {
|
|
|
1195
1319
|
* bubble is still silence. Keep it short; the model history skips ephemeral. */
|
|
1196
1320
|
export function formatZooToolLine({ name, args = {}, result } = {}) {
|
|
1197
1321
|
const detail = args.command || args.path || args.name || args.window
|
|
1198
|
-
|| args.query || args.url || args.key || args.app
|
|
1322
|
+
|| args.query || args.url || args.key || args.app || args.every
|
|
1199
1323
|
|| (args.x != null && args.y != null ? `${args.x},${args.y}` : '')
|
|
1200
1324
|
|| (args.text != null ? String(args.text).slice(0, 80) : '');
|
|
1201
1325
|
const head = detail
|
|
@@ -2003,6 +2127,34 @@ const LOCAL_TOOLS = [
|
|
|
2003
2127
|
},
|
|
2004
2128
|
},
|
|
2005
2129
|
},
|
|
2130
|
+
{
|
|
2131
|
+
type: 'function',
|
|
2132
|
+
function: {
|
|
2133
|
+
name: 'schedule_wakeup',
|
|
2134
|
+
description: 'Host timer that keeps this bot working with no human message. "never stop" / cron. Default every 5m. Min 60s. Does not spawn bots.',
|
|
2135
|
+
parameters: {
|
|
2136
|
+
type: 'object',
|
|
2137
|
+
properties: {
|
|
2138
|
+
every: { type: 'string', description: '5m, 1h, 90s. Default 5m. Floor 60s.' },
|
|
2139
|
+
prompt: { type: 'string', description: 'What to do on each tick.' },
|
|
2140
|
+
agent: { type: 'string', description: 'Name or id. Default: this bot.' },
|
|
2141
|
+
},
|
|
2142
|
+
},
|
|
2143
|
+
},
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
type: 'function',
|
|
2147
|
+
function: {
|
|
2148
|
+
name: 'cancel_wakeup',
|
|
2149
|
+
description: 'Stop the host wakeup timer for this bot (or agent).',
|
|
2150
|
+
parameters: {
|
|
2151
|
+
type: 'object',
|
|
2152
|
+
properties: {
|
|
2153
|
+
agent: { type: 'string' },
|
|
2154
|
+
},
|
|
2155
|
+
},
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2006
2158
|
];
|
|
2007
2159
|
|
|
2008
2160
|
export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
|
|
@@ -2151,8 +2303,12 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2151
2303
|
const agent = mintLocalAgent({ name, brief });
|
|
2152
2304
|
pushCreatedAgent(agent, { select: args.select !== false });
|
|
2153
2305
|
seedBriefOnCanvas(agent);
|
|
2306
|
+
const n = (cachedAgentList() || []).length;
|
|
2154
2307
|
log(`cursor-backend: create_agent tool id=${agent.id} name=${JSON.stringify(agent.name)} brief=${brief ? brief.length : 0}c`);
|
|
2155
|
-
|
|
2308
|
+
const warning = n >= 8
|
|
2309
|
+
? `Mac already has ${n} bots. Prefer schedule_wakeup over spawning. Only mint a NEW named role.`
|
|
2310
|
+
: undefined;
|
|
2311
|
+
return JSON.stringify({ ok: true, id: agent.id, name: agent.name, brief: agent.brief || '', warning });
|
|
2156
2312
|
}
|
|
2157
2313
|
if (name === 'set_brief') {
|
|
2158
2314
|
const who = findAgent(args.agent || args.id || args.name) || findAgent(ctx.agentId);
|
|
@@ -2162,10 +2318,12 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2162
2318
|
}
|
|
2163
2319
|
if (name === 'list_agents') {
|
|
2164
2320
|
const list = cachedAgentList() || [];
|
|
2321
|
+
const wakes = readWakeups(HOME);
|
|
2165
2322
|
return JSON.stringify(list.map((a) => ({
|
|
2166
2323
|
id: a.id,
|
|
2167
2324
|
name: a.name,
|
|
2168
2325
|
brief: String(a.brief || '').slice(0, 120),
|
|
2326
|
+
wakeup: wakes[a.id] ? { everySec: wakes[a.id].everySec, nextAt: wakes[a.id].nextAt } : null,
|
|
2169
2327
|
})));
|
|
2170
2328
|
}
|
|
2171
2329
|
if (name === 'message_agent') {
|
|
@@ -2177,6 +2335,17 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2177
2335
|
log,
|
|
2178
2336
|
});
|
|
2179
2337
|
}
|
|
2338
|
+
if (name === 'schedule_wakeup') {
|
|
2339
|
+
const who = findAgent(args.agent || args.id || args.name) || findAgent(ctx.agentId);
|
|
2340
|
+
if (!who) return 'ERROR no such agent';
|
|
2341
|
+
const got = scheduleAgentWakeup(who.id, { every: args.every, prompt: args.prompt });
|
|
2342
|
+
return JSON.stringify(got);
|
|
2343
|
+
}
|
|
2344
|
+
if (name === 'cancel_wakeup') {
|
|
2345
|
+
const who = findAgent(args.agent || args.id || args.name) || findAgent(ctx.agentId);
|
|
2346
|
+
if (!who) return 'ERROR no such agent';
|
|
2347
|
+
return JSON.stringify(cancelAgentWakeup(who.id));
|
|
2348
|
+
}
|
|
2180
2349
|
return `unknown tool ${name}`;
|
|
2181
2350
|
} catch (e) {
|
|
2182
2351
|
return `ERROR ${e.message}`;
|
|
@@ -2273,8 +2442,9 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2273
2442
|
return brief ? `${who} Standing brief (persisted): ${brief}` : who;
|
|
2274
2443
|
})(),
|
|
2275
2444
|
`You HAVE local tools on the user's computer via ${via}.`,
|
|
2276
|
-
'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent, set_brief, list_agents, message_agent.',
|
|
2445
|
+
'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent, set_brief, list_agents, message_agent, schedule_wakeup, cancel_wakeup.',
|
|
2277
2446
|
'create_agent mints a sidebar bot. ALWAYS pass brief so they keep the job across restart. set_brief updates it. list_agents + message_agent talk to other bots (one hop). Do not tell the human to copy-paste between canvases.',
|
|
2447
|
+
'NEVER STOP / cron / keep working between human messages: call schedule_wakeup every="5m". That is a host timer. There is no crontab. Do not spawn more bots for persistence. Do not re-read SITREP-NOW.md or STANDING-ORDERS.md every turn — do the next file or click. exec sysctl/uptime is not the job. If the tray already has named workers, answer "no" to spawning more.',
|
|
2278
2448
|
'You CAN click the Mac and fill/submit browser forms. That is required when the user asks. Do not write a markdown briefing instead of clicking. Do not tell the user to click.',
|
|
2279
2449
|
'Form loop: focus_app Brave Browser → screenshot → click the field (query or x,y) → type_text → screenshot to confirm → click Submit / key enter. screenshot.screen is click coordinate space. Prefer click query="Submit".',
|
|
2280
2450
|
'screenshot captures the display and attaches the image. For any on-screen form, dashboard, or click target, screenshot first this turn. Do not guess at UI you have not seen.',
|
|
@@ -2805,6 +2975,10 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
2805
2975
|
log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN}${visitor ? ` visitor=${visitor.shortname}` : ''} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
|
|
2806
2976
|
lastSendEchoId = String(nonce);
|
|
2807
2977
|
jsonSend(res, { accepted: true });
|
|
2978
|
+
if (!visitor && wantsWakeupCron(prompt)) {
|
|
2979
|
+
const cron = scheduleAgentWakeup(agentId, { every: '5m' });
|
|
2980
|
+
log(`cursor-backend: never-stop cron agent=${agentId} every=${cron.everySec}s`);
|
|
2981
|
+
}
|
|
2808
2982
|
const turn = zooTurns.begin(agentId, nonce);
|
|
2809
2983
|
const userLine = fanoutLine(agentId, 'user', uiText, {
|
|
2810
2984
|
clientNonce: nonce,
|
|
@@ -3013,6 +3187,7 @@ function respond(req, res, method, models) {
|
|
|
3013
3187
|
* unprivileged). Returns { server, port }.
|
|
3014
3188
|
*/
|
|
3015
3189
|
export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
3190
|
+
restoreAgentWakeups(log);
|
|
3016
3191
|
const { cert, key } = ensureCert(log);
|
|
3017
3192
|
const certPem = fs.readFileSync(cert);
|
|
3018
3193
|
const keyPem = fs.readFileSync(key);
|
package/lib/grokbotAccount.js
CHANGED
|
@@ -184,6 +184,72 @@ export function rosterForEvent(list, activity) {
|
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
export const WAKEUP_MIN_SEC = 60;
|
|
188
|
+
export const WAKEUP_MAX_SEC = 6 * 3600;
|
|
189
|
+
export const WAKEUP_DEFAULT_SEC = 5 * 60;
|
|
190
|
+
export const DEFAULT_WAKEUP_PROMPT = 'Wakeup. Continue your brief. Do the next real action (write a file, click, or message a worker). Do not only re-read sitrep. Do not spawn more bots. Do not exec sysctl/uptime.';
|
|
191
|
+
|
|
192
|
+
export function wakeupsPath(home) {
|
|
193
|
+
return path.join(home, '.openzoo', 'grokbot-wakeups.json');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** "5m" / "1h" / "90" / "30s". Floor 60s so a never-stop cron cannot storm. */
|
|
197
|
+
export function parseWakeupEvery(raw) {
|
|
198
|
+
const s = String(raw ?? '').trim().toLowerCase();
|
|
199
|
+
if (!s) return WAKEUP_DEFAULT_SEC;
|
|
200
|
+
const m = s.match(/^(\d+(?:\.\d+)?)\s*(s|sec|secs|seconds?|m|min|mins|minutes?|h|hr|hrs|hours?)?$/);
|
|
201
|
+
if (!m) return WAKEUP_DEFAULT_SEC;
|
|
202
|
+
const n = Number(m[1]);
|
|
203
|
+
if (!Number.isFinite(n) || n <= 0) return WAKEUP_DEFAULT_SEC;
|
|
204
|
+
const u = m[2] || 's';
|
|
205
|
+
let sec = n;
|
|
206
|
+
if (/^m/.test(u)) sec = n * 60;
|
|
207
|
+
else if (/^h/.test(u)) sec = n * 3600;
|
|
208
|
+
return Math.min(WAKEUP_MAX_SEC, Math.max(WAKEUP_MIN_SEC, Math.round(sec)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function wantsWakeupCron(prompt) {
|
|
212
|
+
const s = String(prompt || '');
|
|
213
|
+
if (/\bcron\b.{0,48}\bwakeups?\b/i.test(s)) return true;
|
|
214
|
+
if (/\bwakeups?\b.{0,48}\bcron\b/i.test(s)) return true;
|
|
215
|
+
if (/\bnever stop\b/i.test(s)) return true;
|
|
216
|
+
if (/\bschedule\b.{0,24}\bwakeups?\b/i.test(s)) return true;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function shapeWakeup(agentId, rec = {}, now = Date.now()) {
|
|
221
|
+
const everySec = parseWakeupEvery(rec.everySec ?? rec.every ?? rec.interval);
|
|
222
|
+
const prompt = String(rec.prompt || DEFAULT_WAKEUP_PROMPT).trim().slice(0, 2000)
|
|
223
|
+
|| DEFAULT_WAKEUP_PROMPT;
|
|
224
|
+
const lastAt = Number(rec.lastAt) || 0;
|
|
225
|
+
let nextAt = Number(rec.nextAt) || 0;
|
|
226
|
+
if (!Number.isFinite(nextAt) || nextAt <= 0) nextAt = now + everySec * 1000;
|
|
227
|
+
return {
|
|
228
|
+
agentId: String(agentId || rec.agentId || ''),
|
|
229
|
+
everySec,
|
|
230
|
+
prompt,
|
|
231
|
+
lastAt,
|
|
232
|
+
nextAt,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function readWakeups(home) {
|
|
237
|
+
const raw = readJsonFile(wakeupsPath(home));
|
|
238
|
+
const src = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
239
|
+
const out = {};
|
|
240
|
+
for (const [id, rec] of Object.entries(src)) {
|
|
241
|
+
const w = shapeWakeup(id, rec);
|
|
242
|
+
if (w.agentId) out[w.agentId] = w;
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function writeWakeups(home, map) {
|
|
248
|
+
const dir = path.dirname(wakeupsPath(home));
|
|
249
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
250
|
+
fs.writeFileSync(wakeupsPath(home), JSON.stringify(map || {}, null, 2));
|
|
251
|
+
}
|
|
252
|
+
|
|
187
253
|
export function callerKeyFromAuth(authorization) {
|
|
188
254
|
const a = String(authorization || '').trim();
|
|
189
255
|
if (!a) return '';
|
package/lib/grokcli.js
CHANGED
|
@@ -110,7 +110,7 @@ const GROK_BOT_BIN = `${APP}/Contents/MacOS/Grok Bot`;
|
|
|
110
110
|
/** pids whose command is the Grok Bot main binary (not grep itself). */
|
|
111
111
|
export function grokBotPids(run = execSync) {
|
|
112
112
|
try {
|
|
113
|
-
return run(`pgrep -f ${JSON.stringify(GROK_BOT_BIN)}`, { encoding: 'utf8' })
|
|
113
|
+
return run(`pgrep -f ${JSON.stringify(GROK_BOT_BIN)}`, { encoding: 'utf8', shell: true })
|
|
114
114
|
.trim().split('\n').map((s) => s.trim()).filter(Boolean);
|
|
115
115
|
} catch {
|
|
116
116
|
return [];
|
|
@@ -296,7 +296,7 @@ export async function runBot(argv = []) {
|
|
|
296
296
|
await new Promise((r) => setTimeout(r, 1500));
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
const launchGrokBot = () => {
|
|
300
300
|
console.error('openzoo: launching Grok Bot');
|
|
301
301
|
console.error(` CURSOR_API_BASE_URL=${url}`);
|
|
302
302
|
spawn(bin, grokBotChromiumArgs(), {
|
|
@@ -315,14 +315,14 @@ export async function runBot(argv = []) {
|
|
|
315
315
|
SAND_HOST_GATEWAY_NETWORK_TOKEN: 'openzoo',
|
|
316
316
|
},
|
|
317
317
|
}).unref();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
};
|
|
319
|
+
if (plan.spawn) launchGrokBot();
|
|
320
|
+
else console.error('openzoo: Grok Bot already hijacked — not spawning another copy');
|
|
321
321
|
|
|
322
322
|
injectSpendChipInBackground({
|
|
323
323
|
port: GROKBOT_CDP_PORT,
|
|
324
324
|
log: (m) => console.error(m),
|
|
325
|
-
delayMs: plan.spawn ?
|
|
325
|
+
delayMs: plan.spawn ? 8000 : 2000,
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
console.error('openzoo: leave this running. ctrl-c stops the backend.');
|
package/lib/ozSpendChip.js
CHANGED
|
@@ -7,9 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import net from 'node:net';
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { spendChipLabel } from './spendProof.js';
|
|
10
14
|
|
|
11
15
|
export const GROKBOT_CDP_PORT = Number(process.env.OZ_GROKBOT_CDP_PORT || 9444);
|
|
12
16
|
|
|
17
|
+
/** Session totals for a floating pill when the open canvas has no footer. */
|
|
18
|
+
export function sessionSpendLabel(home = os.homedir()) {
|
|
19
|
+
try {
|
|
20
|
+
const s = JSON.parse(fs.readFileSync(path.join(home, '.openzoo', 'session.json'), 'utf8'));
|
|
21
|
+
const spent = Number(s.spentUsd || s.spendUsd || 0);
|
|
22
|
+
const would = Number(s.directUsd || 0);
|
|
23
|
+
const saved = Number(s.savedUsd != null ? s.savedUsd : Math.max(0, would - spent));
|
|
24
|
+
if (!(spent > 0.00005)) return '';
|
|
25
|
+
return spendChipLabel({ spent, would, saved, pct: would > 0 ? (100 * saved) / would : 0 });
|
|
26
|
+
} catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
export function grokBotChromiumArgs(port = GROKBOT_CDP_PORT) {
|
|
14
32
|
return [
|
|
15
33
|
'--ignore-certificate-errors',
|
|
@@ -133,9 +151,12 @@ export function spendOnlyText(t) {
|
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
function ozEnsureSpendCss() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
let s = document.getElementById('oz-spend-css');
|
|
155
|
+
if (!s) {
|
|
156
|
+
s = document.createElement('style');
|
|
157
|
+
s.id = 'oz-spend-css';
|
|
158
|
+
(document.head || document.documentElement).appendChild(s);
|
|
159
|
+
}
|
|
139
160
|
s.textContent = [
|
|
140
161
|
'.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
|
|
141
162
|
'.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
|
|
@@ -144,8 +165,9 @@ function ozEnsureSpendCss() {
|
|
|
144
165
|
'.oz-spend>summary::-webkit-details-marker{display:none}',
|
|
145
166
|
'.oz-spend-body{white-space:pre-wrap;margin:.55rem 0 0;font-size:11px;line-height:1.45;opacity:.88;overflow-wrap:anywhere}',
|
|
146
167
|
'[data-oz-spend-hide]{display:none!important}',
|
|
168
|
+
'button[aria-label="Start voice input"],button[aria-label*="voice input" i],',
|
|
169
|
+
'button[aria-label*="steminvoer" i],button[aria-label^="Microphone"]{display:none!important}',
|
|
147
170
|
].join('');
|
|
148
|
-
(document.head || document.documentElement).appendChild(s);
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
function ozSpendHost(el) {
|
|
@@ -292,6 +314,52 @@ function ozCollapseSpend() {
|
|
|
292
314
|
ozAttachSpendChip(host, split);
|
|
293
315
|
}
|
|
294
316
|
ozHideSpendLeftovers();
|
|
317
|
+
ozEnsureFloatSpend();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function ozEnsureFloatSpend() {
|
|
321
|
+
const label = String(window.__OZ_SESSION_SPEND__ || '').trim();
|
|
322
|
+
const msgPills = document.querySelectorAll('.oz-spend:not(#oz-spend-float)').length;
|
|
323
|
+
let el = document.getElementById('oz-spend-float');
|
|
324
|
+
if (!label || msgPills) {
|
|
325
|
+
if (el) el.remove();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
ozEnsureSpendCss();
|
|
329
|
+
if (!el) {
|
|
330
|
+
el = document.createElement('details');
|
|
331
|
+
el.id = 'oz-spend-float';
|
|
332
|
+
el.className = 'oz-spend';
|
|
333
|
+
const sum = document.createElement('summary');
|
|
334
|
+
const body = document.createElement('div');
|
|
335
|
+
body.className = 'oz-spend-body';
|
|
336
|
+
body.textContent = 'session spend (this openzoo bot)';
|
|
337
|
+
el.appendChild(sum);
|
|
338
|
+
el.appendChild(body);
|
|
339
|
+
document.body.appendChild(el);
|
|
340
|
+
}
|
|
341
|
+
ozPlaceFloat(el);
|
|
342
|
+
const sum = el.querySelector('summary');
|
|
343
|
+
if (sum) sum.textContent = 'ⓘ ' + label;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function ozPlaceFloat(el) {
|
|
347
|
+
if (!el) return;
|
|
348
|
+
const ta = document.querySelector('textarea, [contenteditable="true"]');
|
|
349
|
+
const box = ta && ta.getBoundingClientRect();
|
|
350
|
+
el.style.position = 'fixed';
|
|
351
|
+
el.style.right = 'auto';
|
|
352
|
+
el.style.zIndex = '2147483646';
|
|
353
|
+
el.style.opacity = '0.95';
|
|
354
|
+
if (box && box.width > 80 && box.top > 48) {
|
|
355
|
+
el.style.left = Math.round(box.left) + 'px';
|
|
356
|
+
el.style.top = Math.round(Math.max(8, box.top - 56)) + 'px';
|
|
357
|
+
el.style.bottom = 'auto';
|
|
358
|
+
} else {
|
|
359
|
+
el.style.left = '96px';
|
|
360
|
+
el.style.bottom = '72px';
|
|
361
|
+
el.style.top = 'auto';
|
|
362
|
+
}
|
|
295
363
|
}
|
|
296
364
|
|
|
297
365
|
function ozWatchSpend() {
|
|
@@ -305,14 +373,27 @@ function ozWatchSpend() {
|
|
|
305
373
|
else document.addEventListener('DOMContentLoaded', start);
|
|
306
374
|
} catch { /* */ }
|
|
307
375
|
setInterval(run, 1500);
|
|
376
|
+
try { window.addEventListener('resize', run); } catch (e) {}
|
|
308
377
|
}
|
|
309
378
|
|
|
310
379
|
export function spendChipSource() {
|
|
311
380
|
return [
|
|
312
381
|
'(function ozSpendChip(){',
|
|
313
382
|
"'use strict';",
|
|
314
|
-
'if (window.__OZ_SPEND_CHIP__ ===
|
|
315
|
-
'
|
|
383
|
+
'if (window.__OZ_SPEND_CHIP__ === 10) {',
|
|
384
|
+
' const el = document.getElementById("oz-spend-float");',
|
|
385
|
+
' const ta = document.querySelector("textarea, [contenteditable=\\"true\\"]");',
|
|
386
|
+
' const box = ta && ta.getBoundingClientRect();',
|
|
387
|
+
' if (el && box && box.width > 80) {',
|
|
388
|
+
' el.style.position = "fixed";',
|
|
389
|
+
' el.style.left = Math.round(box.left) + "px";',
|
|
390
|
+
' el.style.top = Math.round(Math.max(8, box.top - 56)) + "px";',
|
|
391
|
+
' el.style.right = "auto";',
|
|
392
|
+
' el.style.bottom = "auto";',
|
|
393
|
+
' }',
|
|
394
|
+
' return;',
|
|
395
|
+
'}',
|
|
396
|
+
'window.__OZ_SPEND_CHIP__ = 10;',
|
|
316
397
|
chipUsd.toString(),
|
|
317
398
|
labelFromSpendBody.toString(),
|
|
318
399
|
spendLinesOnly.toString(),
|
|
@@ -327,6 +408,8 @@ export function spendChipSource() {
|
|
|
327
408
|
ozPreviousMessageCard.toString(),
|
|
328
409
|
ozAttachSpendChip.toString(),
|
|
329
410
|
ozCollapseSpend.toString(),
|
|
411
|
+
ozEnsureFloatSpend.toString(),
|
|
412
|
+
ozPlaceFloat.toString(),
|
|
330
413
|
ozWatchSpend.toString(),
|
|
331
414
|
'ozWatchSpend();',
|
|
332
415
|
'})();',
|
|
@@ -511,12 +594,24 @@ export async function listCdpPages(port, fetchImpl = fetch) {
|
|
|
511
594
|
return [];
|
|
512
595
|
}
|
|
513
596
|
|
|
514
|
-
async function injectTarget(target, source, connect) {
|
|
597
|
+
async function injectTarget(target, source, connect, { waitForUi = false } = {}) {
|
|
515
598
|
const session = await connect(target.webSocketDebuggerUrl);
|
|
516
599
|
try {
|
|
517
600
|
await session.send('Page.enable').catch(() => {});
|
|
518
601
|
await session.send('Runtime.enable').catch(() => {});
|
|
519
602
|
await session.send('Page.addScriptToEvaluateOnNewDocument', { source }).catch(() => {});
|
|
603
|
+
if (waitForUi) {
|
|
604
|
+
for (let i = 0; i < 32; i += 1) {
|
|
605
|
+
try {
|
|
606
|
+
const r = await session.send('Runtime.evaluate', {
|
|
607
|
+
expression: '!!document.querySelector(\'[class*="sand-"], textarea, [contenteditable="true"]\')',
|
|
608
|
+
returnByValue: true,
|
|
609
|
+
});
|
|
610
|
+
if (r?.result?.value) break;
|
|
611
|
+
} catch { /* still booting */ }
|
|
612
|
+
await new Promise((ok) => setTimeout(ok, 250));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
520
615
|
await session.send('Runtime.evaluate', { expression: source, awaitPromise: false });
|
|
521
616
|
} finally {
|
|
522
617
|
session.close();
|
|
@@ -531,6 +626,7 @@ export async function injectSpendChip({
|
|
|
531
626
|
connect = cdpSession,
|
|
532
627
|
tries = 40,
|
|
533
628
|
delayMs = 400,
|
|
629
|
+
waitForUi = false,
|
|
534
630
|
} = {}) {
|
|
535
631
|
let targets = [];
|
|
536
632
|
for (let i = 0; i < Math.max(1, tries); i += 1) {
|
|
@@ -542,10 +638,12 @@ export async function injectSpendChip({
|
|
|
542
638
|
log(`openzoo: spend chip CDP :${port} has no pages yet`);
|
|
543
639
|
return { ok: false, injected: 0 };
|
|
544
640
|
}
|
|
641
|
+
const label = sessionSpendLabel();
|
|
642
|
+
const src = `window.__OZ_SESSION_SPEND__=${JSON.stringify(label)};\n${source}`;
|
|
545
643
|
let injected = 0;
|
|
546
644
|
for (const t of targets) {
|
|
547
645
|
try {
|
|
548
|
-
await injectTarget(t,
|
|
646
|
+
await injectTarget(t, src, connect, { waitForUi });
|
|
549
647
|
injected += 1;
|
|
550
648
|
} catch (e) {
|
|
551
649
|
log(`openzoo: spend chip inject ${e.message}`);
|
|
@@ -555,11 +653,23 @@ export async function injectSpendChip({
|
|
|
555
653
|
return { ok: injected > 0, injected };
|
|
556
654
|
}
|
|
557
655
|
|
|
558
|
-
/**
|
|
656
|
+
/** One CDP attach, then drop the debugger. A loop here pauses the renderer
|
|
657
|
+
* so the composer accepts a keystroke and then dies. Reloads pick the chip
|
|
658
|
+
* up via Page.addScriptToEvaluateOnNewDocument from that single inject. */
|
|
559
659
|
export function injectSpendChipInBackground(opts = {}) {
|
|
560
660
|
const log = opts.log || ((m) => console.error(m));
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
661
|
+
const wait = Math.max(0, Number(opts.delayMs) || 400);
|
|
662
|
+
setTimeout(() => {
|
|
663
|
+
injectSpendChip({
|
|
664
|
+
tries: 24,
|
|
665
|
+
delayMs: 400,
|
|
666
|
+
waitForUi: true,
|
|
667
|
+
...opts,
|
|
668
|
+
log,
|
|
669
|
+
connect: (ws) => cdpSession(ws, { timeoutMs: 2500 }),
|
|
670
|
+
}).catch((e) => {
|
|
671
|
+
log(`openzoo: spend chip inject failed ${e.message}`);
|
|
672
|
+
});
|
|
673
|
+
}, wait);
|
|
564
674
|
}
|
|
565
675
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.50",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|