openzoo 0.50.20 → 0.50.21
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 +329 -59
- package/lib/grokcli.js +6 -0
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -32,7 +32,9 @@ import fs from 'node:fs';
|
|
|
32
32
|
import os from 'node:os';
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
import zlib from 'node:zlib';
|
|
35
|
-
import { execFileSync } from 'node:child_process';
|
|
35
|
+
import { execFileSync, execFile } from 'node:child_process';
|
|
36
|
+
import { promisify } from 'node:util';
|
|
37
|
+
import tls from 'node:tls';
|
|
36
38
|
import { randomUUID } from 'node:crypto';
|
|
37
39
|
import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
|
|
38
40
|
|
|
@@ -640,8 +642,14 @@ function handleLocalExecFrames(frames, log) {
|
|
|
640
642
|
} else if (f.kind === 'file-error' || f.kind === 'messages-error') {
|
|
641
643
|
w.reject(new Error(f.error || 'local-exec error'));
|
|
642
644
|
localExecWaiters.delete(f.requestId);
|
|
643
|
-
} else if (f.kind === 'client' || f.kind === 'control') {
|
|
644
|
-
w.resolve({
|
|
645
|
+
} else if (f.kind === 'client' || f.kind === 'control' || f.kind === 'result' || f.kind === 'exec-result') {
|
|
646
|
+
w.resolve({
|
|
647
|
+
kind: f.kind,
|
|
648
|
+
message: f.message || f.text || f.stdout || JSON.stringify(f),
|
|
649
|
+
stdout: f.stdout,
|
|
650
|
+
stderr: f.stderr,
|
|
651
|
+
exitCode: f.exitCode,
|
|
652
|
+
});
|
|
645
653
|
localExecWaiters.delete(f.requestId);
|
|
646
654
|
}
|
|
647
655
|
}
|
|
@@ -670,12 +678,14 @@ async function handleLocalExecHttp(req, res, path0, body, log) {
|
|
|
670
678
|
...CORS,
|
|
671
679
|
});
|
|
672
680
|
localExecSse.add(res);
|
|
673
|
-
|
|
681
|
+
// Daemon parser (asar CNt) ignores unknown welcome; a comment ping is enough
|
|
682
|
+
// to keep the stream alive until it POSTs hello to /responses.
|
|
683
|
+
res.write(': openzoo local-exec\n\n');
|
|
674
684
|
const iv = setInterval(() => {
|
|
675
685
|
try { res.write(': ping\n\n'); } catch { clearInterval(iv); localExecSse.delete(res); }
|
|
676
|
-
},
|
|
677
|
-
req.on('close', () => { clearInterval(iv); localExecSse.delete(res); });
|
|
678
|
-
log(
|
|
686
|
+
}, 10000);
|
|
687
|
+
req.on('close', () => { clearInterval(iv); localExecSse.delete(res); log('cursor-backend: local-exec sse closed'); });
|
|
688
|
+
log(`cursor-backend: -> local-exec /requests sse n=${localExecSse.size}`);
|
|
679
689
|
return true;
|
|
680
690
|
}
|
|
681
691
|
if (/\/responses$/.test(path0)) {
|
|
@@ -683,7 +693,7 @@ async function handleLocalExecHttp(req, res, path0, body, log) {
|
|
|
683
693
|
try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
|
|
684
694
|
handleLocalExecFrames(parsed.frames, log);
|
|
685
695
|
jsonSend(res, { ok: true });
|
|
686
|
-
log(`cursor-backend: -> local-exec /responses n=${(parsed.frames || []).length}`);
|
|
696
|
+
log(`cursor-backend: -> local-exec /responses n=${(parsed.frames || []).length} kinds=${(parsed.frames || []).map((f) => f?.kind).join(',')}`);
|
|
687
697
|
return true;
|
|
688
698
|
}
|
|
689
699
|
jsonSend(res, { ok: true });
|
|
@@ -832,7 +842,19 @@ async function zooSpendOverlay(data) {
|
|
|
832
842
|
return lines.join('\n');
|
|
833
843
|
}
|
|
834
844
|
|
|
835
|
-
const
|
|
845
|
+
const MODELS_PATH = path.join(os.homedir(), '.openzoo', 'grokbot-models.json');
|
|
846
|
+
function loadAgentModels() {
|
|
847
|
+
try {
|
|
848
|
+
return new Map(Object.entries(JSON.parse(fs.readFileSync(MODELS_PATH, 'utf8'))));
|
|
849
|
+
} catch { return new Map(); }
|
|
850
|
+
}
|
|
851
|
+
function saveAgentModels() {
|
|
852
|
+
try {
|
|
853
|
+
fs.mkdirSync(path.dirname(MODELS_PATH), { recursive: true });
|
|
854
|
+
fs.writeFileSync(MODELS_PATH, JSON.stringify(Object.fromEntries(agentModels)));
|
|
855
|
+
} catch { /* */ }
|
|
856
|
+
}
|
|
857
|
+
const agentModels = loadAgentModels();
|
|
836
858
|
const MODEL_ALIASES = {
|
|
837
859
|
fable: 'anthropic/claude-fable-5',
|
|
838
860
|
'fable-5': 'anthropic/claude-fable-5',
|
|
@@ -852,65 +874,300 @@ function resolveModelId(raw) {
|
|
|
852
874
|
if (s.includes('/')) return s;
|
|
853
875
|
return null;
|
|
854
876
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
log(`cursor-backend: zoo POST :8402 ${JSON.stringify((prompt || '').slice(0, 60))}`);
|
|
858
|
-
const model = agentModels.get(agentId)
|
|
877
|
+
function currentModel(agentId) {
|
|
878
|
+
return agentModels.get(agentId)
|
|
859
879
|
|| process.env.OPENZOO_DEFAULT_MODEL
|
|
860
|
-
|| '
|
|
861
|
-
|
|
862
|
-
|
|
880
|
+
|| 'x-ai/grok-4.6';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const execFileAsync = promisify(execFile);
|
|
884
|
+
const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|heic|svg)$/i;
|
|
885
|
+
const IMAGE_MAGIC = [
|
|
886
|
+
[Buffer.from([0x89, 0x50, 0x4e, 0x47]), 'image/png'],
|
|
887
|
+
[Buffer.from([0xff, 0xd8, 0xff]), 'image/jpeg'],
|
|
888
|
+
[Buffer.from('GIF8'), 'image/gif'],
|
|
889
|
+
[Buffer.from('RIFF'), 'image/webp'],
|
|
890
|
+
];
|
|
891
|
+
function mimeFromBytes(buf, p = '') {
|
|
892
|
+
if (IMAGE_EXT.test(p)) {
|
|
893
|
+
const ext = path.extname(p).toLowerCase();
|
|
894
|
+
return { 'png': 'image/png', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.bmp': 'image/bmp', '.heic': 'image/heic', '.svg': 'image/svg+xml' }[ext] || 'image/png';
|
|
895
|
+
}
|
|
896
|
+
for (const [magic, mime] of IMAGE_MAGIC) {
|
|
897
|
+
if (buf.length >= magic.length && buf.subarray(0, magic.length).equals(magic)) return mime;
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
function dataUrlsFromRichText(rt) {
|
|
902
|
+
const s = String(rt || '');
|
|
903
|
+
const out = [];
|
|
904
|
+
const re = /data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/]+=*/g;
|
|
905
|
+
let m;
|
|
906
|
+
while ((m = re.exec(s))) out.push(m[0]);
|
|
907
|
+
return out;
|
|
908
|
+
}
|
|
909
|
+
function attachmentList(parsed, prompt) {
|
|
910
|
+
const out = [];
|
|
911
|
+
const seen = new Set();
|
|
912
|
+
const add = (raw, name) => {
|
|
913
|
+
if (!raw || seen.has(raw)) return;
|
|
914
|
+
seen.add(raw);
|
|
915
|
+
out.push({ raw, name });
|
|
916
|
+
};
|
|
917
|
+
const paths = parsed?.attachmentPaths;
|
|
918
|
+
const names = parsed?.attachmentNames;
|
|
919
|
+
if (Array.isArray(paths)) {
|
|
920
|
+
for (let i = 0; i < paths.length; i++) add(String(paths[i]), names?.[i]);
|
|
921
|
+
}
|
|
922
|
+
for (const p of extractLocalPaths(prompt)) {
|
|
923
|
+
if (/[*?]/.test(p)) continue;
|
|
924
|
+
add(p);
|
|
925
|
+
}
|
|
926
|
+
const rt = String(parsed?.richText || '');
|
|
927
|
+
const srcRe = /(?:src|path|filePath|url)"?\s*[:=]\s*"((?:file:\/\/|\/(?:var|tmp|private|Users)|~\/)[^"]+\.(?:png|jpe?g|gif|webp|bmp|heic))"/gi;
|
|
928
|
+
let sm;
|
|
929
|
+
while ((sm = srcRe.exec(rt))) {
|
|
930
|
+
add(sm[1].replace(/^file:\/\//, ''));
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async function readLocalBytes(abs, log) {
|
|
936
|
+
if (localExecSse.size > 0) {
|
|
937
|
+
log(`cursor-backend: local-exec download ${abs}`);
|
|
938
|
+
const got = await localExecAsk({ kind: 'download', path: abs });
|
|
939
|
+
return got.bytes || Buffer.from(got.text || '', 'utf8');
|
|
940
|
+
}
|
|
941
|
+
return fs.readFileSync(abs);
|
|
942
|
+
}
|
|
943
|
+
async function writeLocalBytes(abs, bytes, log) {
|
|
944
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(String(bytes ?? ''), 'utf8');
|
|
945
|
+
if (localExecSse.size > 0) {
|
|
946
|
+
log(`cursor-backend: local-exec upload ${abs} ${buf.length}b`);
|
|
947
|
+
await localExecAsk({
|
|
948
|
+
kind: 'upload',
|
|
949
|
+
path: abs,
|
|
950
|
+
bytesBase64: buf.toString('base64'),
|
|
951
|
+
});
|
|
952
|
+
return `wrote ${abs} (${buf.length} bytes) via local-exec`;
|
|
953
|
+
}
|
|
954
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
955
|
+
fs.writeFileSync(abs, buf);
|
|
956
|
+
return `wrote ${abs} (${buf.length} bytes)`;
|
|
957
|
+
}
|
|
958
|
+
async function execLocal(command, cwd, log) {
|
|
959
|
+
const dir = cwd ? expandUserPath(cwd) : os.homedir();
|
|
960
|
+
if (localExecSse.size > 0) {
|
|
961
|
+
log(`cursor-backend: local-exec exec ${JSON.stringify(command).slice(0, 80)}`);
|
|
962
|
+
const got = await localExecAsk({
|
|
963
|
+
kind: 'exec',
|
|
964
|
+
serverMessage: { command, cwd: dir, workingDirectory: dir },
|
|
965
|
+
}, 60000);
|
|
966
|
+
const out = got.stdout || got.message || '';
|
|
967
|
+
const err = got.stderr ? `\nstderr:\n${got.stderr}` : '';
|
|
968
|
+
return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
|
|
969
|
+
}
|
|
970
|
+
const { stdout, stderr } = await execFileAsync('/bin/zsh', ['-lc', command], {
|
|
971
|
+
cwd: dir,
|
|
972
|
+
timeout: 30000,
|
|
973
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
974
|
+
env: process.env,
|
|
975
|
+
});
|
|
976
|
+
return `${stdout || ''}${stderr ? `\nstderr:\n${stderr}` : ''}`.trim() || '(no output)';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const LOCAL_TOOLS = [
|
|
980
|
+
{
|
|
981
|
+
type: 'function',
|
|
982
|
+
function: {
|
|
983
|
+
name: 'read_file',
|
|
984
|
+
description: 'Read a file on the user\'s Mac. Use this for any local path.',
|
|
985
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
type: 'function',
|
|
990
|
+
function: {
|
|
991
|
+
name: 'write_file',
|
|
992
|
+
description: 'Write a file on the user\'s Mac. Put HTML/games/code on disk — do not dump huge files in chat.',
|
|
993
|
+
parameters: {
|
|
994
|
+
type: 'object',
|
|
995
|
+
properties: { path: { type: 'string' }, content: { type: 'string' } },
|
|
996
|
+
required: ['path', 'content'],
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
type: 'function',
|
|
1002
|
+
function: {
|
|
1003
|
+
name: 'exec',
|
|
1004
|
+
description: 'Run a shell command on the user\'s Mac (zsh -lc). Use for npm, curl, ls, git, installing MCP, etc.',
|
|
1005
|
+
parameters: {
|
|
1006
|
+
type: 'object',
|
|
1007
|
+
properties: { command: { type: 'string' }, cwd: { type: 'string' } },
|
|
1008
|
+
required: ['command'],
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
type: 'function',
|
|
1014
|
+
function: {
|
|
1015
|
+
name: 'list_dir',
|
|
1016
|
+
description: 'List a directory on the user\'s Mac.',
|
|
1017
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
];
|
|
1021
|
+
|
|
1022
|
+
async function runLocalTool(name, args, log) {
|
|
1023
|
+
try {
|
|
1024
|
+
if (name === 'read_file') {
|
|
1025
|
+
const buf = await readLocalBytes(expandUserPath(args.path), log);
|
|
1026
|
+
if (buf.length > 180000) return `file ${args.path} is ${buf.length} bytes; first 180000:\n${buf.subarray(0, 180000).toString('utf8')}`;
|
|
1027
|
+
return buf.toString('utf8');
|
|
1028
|
+
}
|
|
1029
|
+
if (name === 'write_file') {
|
|
1030
|
+
return await writeLocalBytes(expandUserPath(args.path), args.content ?? '', log);
|
|
1031
|
+
}
|
|
1032
|
+
if (name === 'list_dir') {
|
|
1033
|
+
const abs = expandUserPath(args.path || os.homedir());
|
|
1034
|
+
if (localExecSse.size > 0) {
|
|
1035
|
+
const got = await localExecAsk({ kind: 'exec', serverMessage: { command: `ls -la ${JSON.stringify(abs)}`, cwd: os.homedir() } });
|
|
1036
|
+
return got.stdout || got.message || '';
|
|
1037
|
+
}
|
|
1038
|
+
return fs.readdirSync(abs, { withFileTypes: true })
|
|
1039
|
+
.map((e) => `${e.isDirectory() ? 'd' : '-'} ${e.name}`)
|
|
1040
|
+
.join('\n');
|
|
1041
|
+
}
|
|
1042
|
+
if (name === 'exec') {
|
|
1043
|
+
return await execLocal(String(args.command || ''), args.cwd, log);
|
|
1044
|
+
}
|
|
1045
|
+
return `unknown tool ${name}`;
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
return `ERROR ${e.message}`;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function zooTextFromMessage(msg, data) {
|
|
1052
|
+
let c = msg?.content;
|
|
1053
|
+
if (Array.isArray(c)) {
|
|
1054
|
+
c = c.map((p) => (typeof p === 'string' ? p : (p?.text || p?.content || ''))).join('');
|
|
1055
|
+
}
|
|
1056
|
+
if (typeof c === 'string' && c.trim()) return c;
|
|
1057
|
+
if (typeof data?.error?.message === 'string' && data.error.message) return data.error.message;
|
|
1058
|
+
return '';
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
1062
|
+
const model = currentModel(agentId);
|
|
1063
|
+
const helper = localExecSse.size > 0;
|
|
1064
|
+
log(`cursor-backend: zoo POST :8402 model=${model} helper=${helper ? localExecSse.size : 0} ${JSON.stringify((prompt || '').slice(0, 60))}`);
|
|
1065
|
+
|
|
1066
|
+
const images = [];
|
|
1067
|
+
const textFiles = [];
|
|
1068
|
+
for (const { raw, name } of attachmentList(parsed, prompt)) {
|
|
863
1069
|
const abs = expandUserPath(raw);
|
|
864
1070
|
try {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1071
|
+
const buf = await readLocalBytes(abs, log);
|
|
1072
|
+
const mime = mimeFromBytes(buf, name || raw);
|
|
1073
|
+
if (mime) {
|
|
1074
|
+
images.push({ path: raw, mime, dataUrl: `data:${mime};base64,${buf.toString('base64')}` });
|
|
1075
|
+
log(`cursor-backend: attached image ${raw} ${mime} ${buf.length}b`);
|
|
869
1076
|
} else {
|
|
870
|
-
|
|
871
|
-
attachments.push({ path: raw, abs, text });
|
|
1077
|
+
textFiles.push({ path: raw, abs, text: buf.toString('utf8') });
|
|
872
1078
|
}
|
|
873
1079
|
} catch (e) {
|
|
874
|
-
|
|
1080
|
+
textFiles.push({ path: raw, abs, error: e.message });
|
|
875
1081
|
}
|
|
876
1082
|
}
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
1083
|
+
for (const url of dataUrlsFromRichText(parsed.richText)) {
|
|
1084
|
+
images.push({ path: '(richText)', mime: 'image', dataUrl: url });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const via = helper ? 'Grok Bot Helper local-exec SSE' : 'this Mac (hijack process; Helper SSE not connected yet)';
|
|
1088
|
+
const messages = [
|
|
1089
|
+
{
|
|
1090
|
+
role: 'system',
|
|
1091
|
+
content: [
|
|
1092
|
+
`You are ${model} served through openzoo inside Grok Bot.`,
|
|
1093
|
+
`You HAVE local tools on the user's computer via ${via}.`,
|
|
1094
|
+
'Tools: read_file, write_file, exec, list_dir. USE THEM. Write files to disk instead of pasting giant HTML into chat.',
|
|
1095
|
+
'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
|
|
1096
|
+
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
1097
|
+
'A spend footer is appended after your reply by the host — ignore it.',
|
|
1098
|
+
].join(' '),
|
|
1099
|
+
},
|
|
1100
|
+
];
|
|
1101
|
+
if (textFiles.length) {
|
|
1102
|
+
const bits = textFiles.map((a) => (
|
|
880
1103
|
a.error
|
|
881
1104
|
? `FILE ${a.path} ERROR: ${a.error}`
|
|
882
1105
|
: `FILE ${a.path} (${a.abs})\n${String(a.text).slice(0, 180000)}`
|
|
883
1106
|
));
|
|
884
|
-
messages.push({
|
|
885
|
-
role: 'system',
|
|
886
|
-
content: 'Local files below were fetched via Grok Bot local-exec (the user\'s computer). You CAN review them. Do not claim you lack filesystem access.',
|
|
887
|
-
});
|
|
888
1107
|
messages.push({ role: 'user', content: bits.join('\n\n') });
|
|
889
1108
|
}
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1109
|
+
|
|
1110
|
+
const userContent = [];
|
|
1111
|
+
for (const img of images) {
|
|
1112
|
+
userContent.push({ type: 'image_url', image_url: { url: img.dataUrl } });
|
|
1113
|
+
}
|
|
1114
|
+
userContent.push({ type: 'text', text: prompt || (images.length ? '(see attached image)' : 'hello') });
|
|
1115
|
+
messages.push({
|
|
1116
|
+
role: 'user',
|
|
1117
|
+
content: userContent.length === 1 && userContent[0].type === 'text'
|
|
1118
|
+
? userContent[0].text
|
|
1119
|
+
: userContent,
|
|
901
1120
|
});
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1121
|
+
|
|
1122
|
+
const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 4096);
|
|
1123
|
+
let lastData = {};
|
|
1124
|
+
let text = '';
|
|
1125
|
+
for (let step = 0; step < 8; step++) {
|
|
1126
|
+
const payload = {
|
|
1127
|
+
model,
|
|
1128
|
+
messages,
|
|
1129
|
+
tools: LOCAL_TOOLS,
|
|
1130
|
+
tool_choice: 'auto',
|
|
1131
|
+
max_tokens: maxTok,
|
|
1132
|
+
};
|
|
1133
|
+
const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
1134
|
+
method: 'POST',
|
|
1135
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
|
|
1136
|
+
body: JSON.stringify(payload),
|
|
1137
|
+
signal: AbortSignal.timeout(120000),
|
|
1138
|
+
});
|
|
1139
|
+
let r = await post();
|
|
1140
|
+
if (r.status === 402) {
|
|
1141
|
+
log('cursor-backend: x402 402 — dwell/retry');
|
|
1142
|
+
await new Promise((ok) => setTimeout(ok, 2500));
|
|
1143
|
+
r = await post();
|
|
1144
|
+
}
|
|
1145
|
+
const data = await r.json();
|
|
1146
|
+
lastData = data;
|
|
1147
|
+
const msg = data.choices?.[0]?.message || {};
|
|
1148
|
+
const calls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
1149
|
+
if (calls.length) {
|
|
1150
|
+
log(`cursor-backend: zoo tools step=${step} n=${calls.length} ${calls.map((c) => c.function?.name || c.name).join(',')}`);
|
|
1151
|
+
messages.push(msg);
|
|
1152
|
+
for (const c of calls) {
|
|
1153
|
+
let args = {};
|
|
1154
|
+
try { args = JSON.parse(c.function?.arguments || c.arguments || '{}'); } catch { args = {}; }
|
|
1155
|
+
const name = c.function?.name || c.name || '';
|
|
1156
|
+
const result = await runLocalTool(name, args, log);
|
|
1157
|
+
messages.push({
|
|
1158
|
+
role: 'tool',
|
|
1159
|
+
tool_call_id: c.id,
|
|
1160
|
+
content: String(result).slice(0, 120000),
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
text = zooTextFromMessage(msg, data) || (step ? '(tool loop ended with empty content)' : '(empty zoo reply)');
|
|
1166
|
+
log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${data.choices?.[0]?.finish_reason || '?'}`);
|
|
1167
|
+
break;
|
|
908
1168
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
try { text += await zooSpendOverlay(data); } catch { /* overlay must never eat the reply */ }
|
|
912
|
-
log(`cursor-backend: << zoo ${r.status} ${text.length}c`);
|
|
913
|
-
return { text, data };
|
|
1169
|
+
try { text += await zooSpendOverlay(lastData); } catch { /* overlay must never eat the reply */ }
|
|
1170
|
+
return { text, data: lastData };
|
|
914
1171
|
}
|
|
915
1172
|
|
|
916
1173
|
async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
@@ -918,7 +1175,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
918
1175
|
const waiting = (full || '').split('?')[0];
|
|
919
1176
|
const podPath = waiting === '/health' || waiting === '/healthz' || waiting === '/events'
|
|
920
1177
|
|| waiting.startsWith('/api/') || waiting.startsWith('/webauthn/')
|
|
921
|
-
|| waiting.startsWith('/cookie-origin-approval/')
|
|
1178
|
+
|| waiting.startsWith('/cookie-origin-approval/');
|
|
1179
|
+
if (waiting.startsWith('/local-exec/')) return handleLocalExecHttp(req, res, waiting, body, log);
|
|
922
1180
|
if (realPod?.agent && podPath) return proxyPodHttp(req, res, full, body, log);
|
|
923
1181
|
if (!podPath) return false;
|
|
924
1182
|
if (waiting === '/health' || waiting === '/healthz') {
|
|
@@ -984,7 +1242,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
984
1242
|
const prompt = promptFromSendBody(parsed);
|
|
985
1243
|
const agentId = String(parsed.agentId || parsed.id || 'openzoo');
|
|
986
1244
|
const nonce = parsed.clientNonce || `oz-${Date.now()}`;
|
|
987
|
-
|
|
1245
|
+
const attN = Array.isArray(parsed.attachmentPaths) ? parsed.attachmentPaths.length : 0;
|
|
1246
|
+
log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
|
|
988
1247
|
lastSendEchoId = String(nonce);
|
|
989
1248
|
const userLine = fanoutLine(agentId, 'user', prompt, {
|
|
990
1249
|
clientNonce: nonce,
|
|
@@ -1000,7 +1259,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1000
1259
|
if (modelCmd) {
|
|
1001
1260
|
const want = modelCmd[1];
|
|
1002
1261
|
if (!want) {
|
|
1003
|
-
const cur =
|
|
1262
|
+
const cur = currentModel(agentId);
|
|
1004
1263
|
text = `current model: ${cur}\nset with /model fable | opus | sonnet | grok | provider/id`;
|
|
1005
1264
|
} else {
|
|
1006
1265
|
const id = resolveModelId(want) || (want.includes('/') ? want : null);
|
|
@@ -1008,12 +1267,13 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1008
1267
|
text = `unknown model "${want}". try /model fable | opus | sonnet | grok or a full id like anthropic/claude-fable-5`;
|
|
1009
1268
|
} else {
|
|
1010
1269
|
agentModels.set(agentId, id);
|
|
1270
|
+
saveAgentModels();
|
|
1011
1271
|
text = `model set to ${id}`;
|
|
1012
1272
|
}
|
|
1013
1273
|
}
|
|
1014
1274
|
try { text += await zooSpendOverlay({}); } catch { /* */ }
|
|
1015
1275
|
} else {
|
|
1016
|
-
const z = await zooComplete(prompt, log, agentId);
|
|
1276
|
+
const z = await zooComplete(prompt, log, agentId, parsed);
|
|
1017
1277
|
text = z.text;
|
|
1018
1278
|
}
|
|
1019
1279
|
} catch (e) {
|
|
@@ -1155,6 +1415,9 @@ function respond(req, res, method, models) {
|
|
|
1155
1415
|
*/
|
|
1156
1416
|
export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
1157
1417
|
const { cert, key } = ensureCert(log);
|
|
1418
|
+
const certPem = fs.readFileSync(cert);
|
|
1419
|
+
const keyPem = fs.readFileSync(key);
|
|
1420
|
+
const secureContext = tls.createSecureContext({ cert: certPem, key: keyPem });
|
|
1158
1421
|
let conns = 0;
|
|
1159
1422
|
// SERVE BOTH h2 AND h1. An earlier build forced h1-only after concluding the
|
|
1160
1423
|
// editor's h2 connections reset — but that log was the STALE 8443 backend the
|
|
@@ -1163,13 +1426,20 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
1163
1426
|
// (ERR_SSL_NO_APPLICATION_PROTOCOL when we advertise just h1), so we must
|
|
1164
1427
|
// negotiate both. allowHTTP1 keeps the h1 fetch() calls (stripe/updates)
|
|
1165
1428
|
// working; ALPN h2 first satisfies the Connect gRPC client.
|
|
1429
|
+
// SNICallback MUST pass the SecureContext — cb(null) with no ctx is why the
|
|
1430
|
+
// Helper daemon's Node fetch never completed GET /local-exec/requests (TLS
|
|
1431
|
+
// ECONNRESET, no request log). Chromium --ignore-certificate-errors hid this
|
|
1432
|
+
// for the UI process.
|
|
1166
1433
|
const server = http2.createSecureServer(
|
|
1167
1434
|
{
|
|
1168
|
-
cert:
|
|
1169
|
-
key:
|
|
1435
|
+
cert: certPem,
|
|
1436
|
+
key: keyPem,
|
|
1170
1437
|
allowHTTP1: true,
|
|
1171
1438
|
ALPNProtocols: ['h2', 'http/1.1'],
|
|
1172
|
-
SNICallback: (servername, cb) => {
|
|
1439
|
+
SNICallback: (servername, cb) => {
|
|
1440
|
+
log(`cursor-tls: <- ClientHello SNI=${servername || '?'}`);
|
|
1441
|
+
cb(null, secureContext);
|
|
1442
|
+
},
|
|
1173
1443
|
},
|
|
1174
1444
|
async (req, res) => {
|
|
1175
1445
|
conns += 1;
|
package/lib/grokcli.js
CHANGED
|
@@ -190,6 +190,12 @@ export async function runBot(argv = []) {
|
|
|
190
190
|
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
191
191
|
CURSOR_API_BASE_URL: url,
|
|
192
192
|
SAND_BACKEND_URL: url,
|
|
193
|
+
// Helper daemon (local-exec-daemon/main.cjs) uses Node fetch, not Chromium.
|
|
194
|
+
// Without these it dials the cached cursorvm 1337 URL / dies on our
|
|
195
|
+
// self-signed cert and GET /local-exec/requests never arrives.
|
|
196
|
+
SAND_HOST_GATEWAY_URL: url,
|
|
197
|
+
SAND_HOST_GATEWAY_TOKEN: 'openzoo',
|
|
198
|
+
SAND_HOST_GATEWAY_NETWORK_TOKEN: 'openzoo',
|
|
193
199
|
},
|
|
194
200
|
}).unref();
|
|
195
201
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.21",
|
|
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",
|