openzoo 0.50.98 → 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/lib/cursorbackend.js +91 -28
- package/package.json +1 -1
- package/lib/cursorbackend.js.bak-hiddenbots +0 -4072
package/lib/cursorbackend.js
CHANGED
|
@@ -1088,28 +1088,30 @@ function handleLocalExecFrames(frames, log) {
|
|
|
1088
1088
|
} else if (f.kind === 'file-error' || f.kind === 'messages-error') {
|
|
1089
1089
|
w.reject(new Error(f.error || 'local-exec error'));
|
|
1090
1090
|
localExecWaiters.delete(id);
|
|
1091
|
-
} else if (f.kind === 'output' || f.kind === 'stdout' || f.kind === 'stderr') {
|
|
1092
|
-
const norm = normalizeExecFrame(f);
|
|
1093
|
-
const chunk = norm.stdout || norm.message
|
|
1094
|
-
|| (typeof f.data === 'string' ? f.data : '')
|
|
1095
|
-
|| (typeof f.chunk === 'string' ? f.chunk : '');
|
|
1096
|
-
if (f.kind === 'stderr') w.stderr += toolResultText(chunk);
|
|
1097
|
-
else w.chunks += toolResultText(chunk);
|
|
1098
1091
|
} else if (shellStreamOf(f)) {
|
|
1099
1092
|
// The real Grok Bot lane: stdout/stderr arrive as many `kind:"client"`
|
|
1100
1093
|
// frames and the turn is only over on `exit`. Resolving on the first one
|
|
1101
1094
|
// (what the old client/control branch did) truncated every command to its
|
|
1102
1095
|
// first chunk.
|
|
1103
1096
|
const ss = shellStreamOf(f);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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) {
|
|
1107
1104
|
w.resolve({
|
|
1108
1105
|
kind: 'exit',
|
|
1109
1106
|
message: w.chunks || w.stderr,
|
|
1110
1107
|
stdout: w.chunks,
|
|
1111
1108
|
stderr: w.stderr,
|
|
1112
|
-
exitCode: Number(
|
|
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),
|
|
1113
1115
|
});
|
|
1114
1116
|
localExecWaiters.delete(id);
|
|
1115
1117
|
}
|
|
@@ -1118,8 +1120,11 @@ function handleLocalExecFrames(frames, log) {
|
|
|
1118
1120
|
// any other, so the generic branch below used to resolve the call with an
|
|
1119
1121
|
// empty body the moment the daemon first breathed.
|
|
1120
1122
|
continue;
|
|
1121
|
-
} else if (f.kind === 'client' || f.kind === 'control'
|
|
1122
|
-
|
|
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.
|
|
1123
1128
|
const norm = normalizeExecFrame(f);
|
|
1124
1129
|
const stdout = w.chunks + (norm.stdout || '');
|
|
1125
1130
|
const stderr = w.stderr + (norm.stderr || '');
|
|
@@ -1190,9 +1195,25 @@ function extractLocalPaths(text) {
|
|
|
1190
1195
|
}
|
|
1191
1196
|
return [...new Set(out)];
|
|
1192
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
|
+
*/
|
|
1193
1212
|
function expandUserPath(p) {
|
|
1194
|
-
|
|
1195
|
-
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;
|
|
1196
1217
|
}
|
|
1197
1218
|
|
|
1198
1219
|
/** Per-agent transcript. sendPrompt is async: accept immediately, zooComplete
|
|
@@ -1446,10 +1467,23 @@ export function restoreAgentWakeups(log = () => {}) {
|
|
|
1446
1467
|
return ids.length;
|
|
1447
1468
|
}
|
|
1448
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
|
+
|
|
1449
1483
|
export function formatZooProgress({ step, maxSteps, names, command } = {}) {
|
|
1450
1484
|
const tools = Array.isArray(names) ? names.filter(Boolean).join(', ') : String(names || 'tools');
|
|
1451
1485
|
const cmd = command ? ` ${JSON.stringify(String(command).slice(0, 80))}` : '';
|
|
1452
|
-
return `Working on your
|
|
1486
|
+
return `Working on your ${hostNoun()} (step ${Number(step) + 1}/${maxSteps || '?'}): ${tools}${cmd}`;
|
|
1453
1487
|
}
|
|
1454
1488
|
|
|
1455
1489
|
/** One canvas line per tool. Asar ingest is append-only — mutating a working
|
|
@@ -2275,11 +2309,27 @@ export function localExecFrame(command, cwd, timeoutMs = 120_000) {
|
|
|
2275
2309
|
/** `agent.v1.ShellStream` off an ExecClientMessage: `{stdout:{data}}`,
|
|
2276
2310
|
* `{stderr:{data}}`, `{exit:{code, cwd}}`. `data` is a proto string (T=9), not
|
|
2277
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
|
+
|
|
2278
2323
|
export function shellStreamOf(frame) {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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 : '';
|
|
2283
2333
|
}
|
|
2284
2334
|
|
|
2285
2335
|
/** exec used to hardcode `/bin/zsh -lc`: on Linux that is usually ENOENT and on
|
|
@@ -2296,6 +2346,20 @@ export function execShell(command, platform = process.platform) {
|
|
|
2296
2346
|
return { file: '/bin/sh', args: ['-c', command] };
|
|
2297
2347
|
}
|
|
2298
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
|
+
|
|
2299
2363
|
async function execLocal(command, cwd, log) {
|
|
2300
2364
|
let dir = cwd ? expandUserPath(cwd) : os.homedir();
|
|
2301
2365
|
try {
|
|
@@ -2309,7 +2373,8 @@ async function execLocal(command, cwd, log) {
|
|
|
2309
2373
|
// Daemon-side budget stays under our own wait so a slow command reports the
|
|
2310
2374
|
// shell's error, not our generic "is Grok Bot Helper connected?".
|
|
2311
2375
|
const got = await localExecAsk(localExecFrame(command, dir, 55_000), 60000);
|
|
2312
|
-
|
|
2376
|
+
let out = toolResultText(got.stdout || got.message || '');
|
|
2377
|
+
if (!out && got.outputPath) out = await localExecSpooledOutput(got.outputPath, log);
|
|
2313
2378
|
const errText = toolResultText(got.stderr);
|
|
2314
2379
|
const err = errText ? `\nstderr:\n${errText}` : '';
|
|
2315
2380
|
return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
|
|
@@ -2848,12 +2913,10 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2848
2913
|
const abs = expandUserPath(args.path || os.homedir());
|
|
2849
2914
|
try {
|
|
2850
2915
|
if (localExecSse.size > 0) {
|
|
2851
|
-
|
|
2852
|
-
//
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
const text = toolResultText(got.stdout || got.stderr || got.message).trim();
|
|
2856
|
-
if (text) return text;
|
|
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;
|
|
2857
2920
|
}
|
|
2858
2921
|
const rows = fs.readdirSync(abs, { withFileTypes: true })
|
|
2859
2922
|
.map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`);
|
|
@@ -3160,7 +3223,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3160
3223
|
if (mediaKind) return await mediaTurn({ model, kind: mediaKind, prompt: spoken, log, onProgress: opts.onProgress, signal: opts.signal });
|
|
3161
3224
|
}
|
|
3162
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))}`);
|
|
3163
|
-
if (typeof opts.onProgress === 'function') opts.onProgress(
|
|
3226
|
+
if (typeof opts.onProgress === 'function') opts.onProgress(`Working on your ${hostNoun()}…`);
|
|
3164
3227
|
|
|
3165
3228
|
const images = [];
|
|
3166
3229
|
const textFiles = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.99",
|
|
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",
|