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.
@@ -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({ kind: f.kind, message: f.message });
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
- res.write(`data: ${JSON.stringify({ kind: 'welcome', providerId: 'openzoo' })}\n\n`);
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
- }, 15000);
677
- req.on('close', () => { clearInterval(iv); localExecSse.delete(res); });
678
- log('cursor-backend: -> local-exec /requests sse');
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 agentModels = new Map();
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
- async function zooComplete(prompt, log, agentId) {
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
- || 'anthropic/claude-opus-5';
861
- const attachments = [];
862
- for (const raw of extractLocalPaths(prompt)) {
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
- if (localExecSse.size > 0) {
866
- log(`cursor-backend: local-exec download ${abs}`);
867
- const got = await localExecAsk({ kind: 'download', path: abs });
868
- attachments.push({ path: raw, abs, text: got.text || '' });
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
- const text = fs.readFileSync(abs, 'utf8');
871
- attachments.push({ path: raw, abs, text });
1077
+ textFiles.push({ path: raw, abs, text: buf.toString('utf8') });
872
1078
  }
873
1079
  } catch (e) {
874
- attachments.push({ path: raw, abs, error: e.message });
1080
+ textFiles.push({ path: raw, abs, error: e.message });
875
1081
  }
876
1082
  }
877
- const messages = [];
878
- if (attachments.length) {
879
- const bits = attachments.map((a) => (
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
- messages.push({ role: 'user', content: prompt || 'hello' });
891
- const payload = {
892
- model,
893
- messages,
894
- max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
895
- };
896
- const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
897
- method: 'POST',
898
- headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
899
- body: JSON.stringify(payload),
900
- signal: AbortSignal.timeout(120000),
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
- let r = await post();
903
- // x402 dwell: proxy usually pays internally; a leftover 402 is retryable.
904
- if (r.status === 402) {
905
- log('cursor-backend: x402 402 dwell/retry');
906
- await new Promise((ok) => setTimeout(ok, 2500));
907
- r = await post();
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
- const data = await r.json();
910
- let text = data.choices?.[0]?.message?.content || data.error?.message || '(empty zoo reply)';
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/') || waiting.startsWith('/local-exec/');
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
- log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
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 = agentModels.get(agentId) || process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5';
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: fs.readFileSync(cert),
1169
- key: fs.readFileSync(key),
1435
+ cert: certPem,
1436
+ key: keyPem,
1170
1437
  allowHTTP1: true,
1171
1438
  ALPNProtocols: ['h2', 'http/1.1'],
1172
- SNICallback: (servername, cb) => { log(`cursor-tls: <- ClientHello SNI=${servername}`); cb(null); },
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.20",
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",