openzoo 0.30.6 → 0.32.0
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 +80 -0
- package/lib/cursorcfg.js +12 -13
- package/lib/hosts.js +1 -1
- package/lib/setup.js +9 -3
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -118,6 +118,80 @@ function stripeProfile() {
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Pull the user's prompt out of a StreamUnifiedChatRequest without the full
|
|
123
|
+
* schema: walk the protobuf, collect plausible UTF-8 string fields, take the
|
|
124
|
+
* longest natural-language one (the new user turn). The captured .bin refines it.
|
|
125
|
+
*/
|
|
126
|
+
function extractPromptText(buf) {
|
|
127
|
+
const strings = [];
|
|
128
|
+
const walk = (b, depth) => {
|
|
129
|
+
let i = 0;
|
|
130
|
+
while (i < b.length) {
|
|
131
|
+
let key = 0, shift = 0, byte;
|
|
132
|
+
do { if (i >= b.length) return; byte = b[i++]; key |= (byte & 0x7f) << shift; shift += 7; } while (byte & 0x80);
|
|
133
|
+
const wire = key & 7;
|
|
134
|
+
if (wire === 2) {
|
|
135
|
+
let len = 0; shift = 0;
|
|
136
|
+
do { if (i >= b.length) return; byte = b[i++]; len |= (byte & 0x7f) << shift; shift += 7; } while (byte & 0x80);
|
|
137
|
+
const sub = b.subarray(i, i + len); i += len;
|
|
138
|
+
const txt = sub.toString('utf8');
|
|
139
|
+
if (len > 1 && /[a-zA-Z]/.test(txt) && !/[\x00-\x08\x0e-\x1f]/.test(txt)) strings.push(txt);
|
|
140
|
+
else if (depth < 6) walk(sub, depth + 1);
|
|
141
|
+
} else if (wire === 0) { while (i < b.length && (b[i++] & 0x80)) { /* skip */ } }
|
|
142
|
+
else if (wire === 5) { i += 4; } else if (wire === 1) { i += 8; } else { return; }
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
try { walk(buf, 0); } catch { /* best effort */ }
|
|
146
|
+
const cand = strings.filter((x) => /\s/.test(x.trim()) || x.length > 12);
|
|
147
|
+
return ((cand.length ? cand : strings).sort((a, b) => b.length - a.length)[0] || '').trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** StreamUnifiedChatResponse{1: text}. */
|
|
151
|
+
function encodeChatResponse(text) {
|
|
152
|
+
const t = Buffer.from(text, 'utf8');
|
|
153
|
+
const head = [0x0a];
|
|
154
|
+
let n = t.length; do { let x = n & 0x7f; n = Math.floor(n / 128); if (n) x |= 0x80; head.push(x); } while (n);
|
|
155
|
+
return Buffer.concat([Buffer.from(head), t]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* ROUTE AUTO THROUGH THE ZOO. Cursor's chat inference is ChatService/
|
|
160
|
+
* StreamUnifiedChat to the agent host (now impersonated). Parse the prompt, pay
|
|
161
|
+
* the zoo via the local proxy, stream the answer back as StreamUnifiedChatResponse
|
|
162
|
+
* frames. Legitimate: Auto is free-allowed; we redirect the operator's OWN
|
|
163
|
+
* inference to their OWN paid proxy.
|
|
164
|
+
*/
|
|
165
|
+
async function handleStreamChat(req, res, body, log) {
|
|
166
|
+
const ct = String(req.headers['content-type'] || '');
|
|
167
|
+
const isGrpcWeb = ct.includes('grpc-web');
|
|
168
|
+
const prompt = extractPromptText(body) || 'hello';
|
|
169
|
+
log(`cursor-backend: >> StreamUnifiedChat prompt: ${JSON.stringify(prompt.slice(0, 80))}`);
|
|
170
|
+
let text = '';
|
|
171
|
+
try {
|
|
172
|
+
const zoo = await fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
model: process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
|
|
177
|
+
messages: [{ role: 'user', content: prompt }],
|
|
178
|
+
max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
const data = await zoo.json();
|
|
182
|
+
text = data.choices?.[0]?.message?.content || '(no content)';
|
|
183
|
+
log(`cursor-backend: << zoo replied ${text.length} chars (paid x402)`);
|
|
184
|
+
} catch (e) { text = `openzoo error: ${e.message}`; log(`cursor-backend: zoo call failed: ${e.message}`); }
|
|
185
|
+
|
|
186
|
+
res.writeHead(200, {
|
|
187
|
+
'content-type': isGrpcWeb ? 'application/grpc-web+proto' : 'application/connect+proto',
|
|
188
|
+
'grpc-status': '0', ...CORS,
|
|
189
|
+
});
|
|
190
|
+
res.write(envelope(encodeChatResponse(text)));
|
|
191
|
+
if (isGrpcWeb) { res.end(grpcWebTrailer()); }
|
|
192
|
+
else { const end = Buffer.from('{}'); const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1); res.end(Buffer.concat([h, end])); }
|
|
193
|
+
}
|
|
194
|
+
|
|
121
195
|
/**
|
|
122
196
|
* Answer one Connect/gRPC-web call. `method` is the trailing method name,
|
|
123
197
|
* `models` the catalog to publish. Non-catalog methods get an empty-OK body.
|
|
@@ -199,6 +273,12 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
199
273
|
const ct = req.headers['content-type'] || '?';
|
|
200
274
|
const body = await readBody(req);
|
|
201
275
|
log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
|
|
276
|
+
// CAPTURE the chat inference request so its schema can be decoded from
|
|
277
|
+
// REAL bytes (Auto's StreamUnifiedChat). Written once; inspect then build.
|
|
278
|
+
if (/StreamUnifiedChat/.test(full)) {
|
|
279
|
+
await handleStreamChat(req, res, body, log);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
202
282
|
try {
|
|
203
283
|
respond(req, res, method, models);
|
|
204
284
|
const populated = ['GetPlanInfo', 'GetMe', 'GetDefaultModel', 'IsOnNewPricing'];
|
package/lib/cursorcfg.js
CHANGED
|
@@ -144,14 +144,13 @@ export function writeEditorProviderConfig(which, { baseUrl, models }) {
|
|
|
144
144
|
const before = { openAIBaseUrl: doc.openAIBaseUrl, models: (doc.availableAPIKeyModels || []).length };
|
|
145
145
|
doc.openAIBaseUrl = baseUrl;
|
|
146
146
|
doc.useOpenAIKey = true;
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// .
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
doc.subscriptionStatus = 'active';
|
|
147
|
+
// MEMBERSHIP FORGERY IS OPT-IN. Setting membershipType here spoofs Cursor's
|
|
148
|
+
// paid-tier gate on an unpaid account — only do it when the operator explicitly
|
|
149
|
+
// asks with OPENZOO_MEMBERSHIP. Default writes leave the real membership alone.
|
|
150
|
+
if (process.env.OPENZOO_MEMBERSHIP) {
|
|
151
|
+
doc.membershipType = process.env.OPENZOO_MEMBERSHIP;
|
|
152
|
+
doc.subscriptionStatus = 'active';
|
|
153
|
+
}
|
|
155
154
|
// Merge, don't clobber: a user may have their own custom models listed.
|
|
156
155
|
const existing = Array.isArray(doc.availableAPIKeyModels) ? doc.availableAPIKeyModels : [];
|
|
157
156
|
const names = new Set(existing.map((m) => (typeof m === 'string' ? m : m?.name)).filter(Boolean));
|
|
@@ -265,7 +264,7 @@ export function writeEditorProviderConfig(which, { baseUrl, models }) {
|
|
|
265
264
|
* that clobbers them. Removable with unpinEditorProviderConfig.
|
|
266
265
|
*/
|
|
267
266
|
export function pinEditorProviderConfig(which, { baseUrl, models }) {
|
|
268
|
-
const mem = process.env.OPENZOO_MEMBERSHIP ||
|
|
267
|
+
const mem = process.env.OPENZOO_MEMBERSHIP || null;
|
|
269
268
|
const db = storagePath(which);
|
|
270
269
|
if (!fs.existsSync(db)) return null;
|
|
271
270
|
const esc = (v) => String(v).replace(/'/g, "''");
|
|
@@ -292,8 +291,8 @@ CREATE TRIGGER openzoo_pin AFTER UPDATE ON ItemTable
|
|
|
292
291
|
WHEN NEW.key = '${KEY}'
|
|
293
292
|
AND (json_extract(NEW.value,'$.openAIBaseUrl') IS NOT '${base}'
|
|
294
293
|
OR json_extract(NEW.value,'$.featureModelConfigs.composer.defaultModel') IS NOT '${primary}'
|
|
295
|
-
OR json_array_length(json_extract(NEW.value,'$.availableDefaultModels2')) IS NOT ${models.length}
|
|
296
|
-
OR json_extract(NEW.value,'$.membershipType') IS NOT '${mem}')
|
|
294
|
+
OR json_array_length(json_extract(NEW.value,'$.availableDefaultModels2')) IS NOT ${models.length}${mem ? `
|
|
295
|
+
OR json_extract(NEW.value,'$.membershipType') IS NOT '${mem}'` : ''})
|
|
297
296
|
BEGIN
|
|
298
297
|
UPDATE ItemTable SET value = json_set(
|
|
299
298
|
NEW.value,
|
|
@@ -302,9 +301,9 @@ BEGIN
|
|
|
302
301
|
'$.availableAPIKeyModels', json('${modelJson}'),
|
|
303
302
|
'$.availableDefaultModels2', json('${uiJson}'),
|
|
304
303
|
'$.featureModelConfigs.composer.defaultModel', '${primary}',
|
|
305
|
-
'$.featureModelConfigs.cmdK.defaultModel', '${primary}'
|
|
304
|
+
'$.featureModelConfigs.cmdK.defaultModel', '${primary}'${mem ? `,
|
|
306
305
|
'$.membershipType', '${mem}',
|
|
307
|
-
'$.subscriptionStatus', 'active'
|
|
306
|
+
'$.subscriptionStatus', 'active'` : ''}
|
|
308
307
|
) WHERE key = NEW.key;
|
|
309
308
|
END;`;
|
|
310
309
|
try { sqlite(db, sql); } catch (e) { return { error: e.message }; }
|
package/lib/hosts.js
CHANGED
|
@@ -36,7 +36,7 @@ const BACKUP = `${HOSTS}.openzoo-backup`;
|
|
|
36
36
|
const MARK = '# openzoo: force the editor onto the local proxy';
|
|
37
37
|
|
|
38
38
|
/** Hosts the editor uses for model sync + its own inference proxy. */
|
|
39
|
-
export const BACKEND_HOSTS = ['api2.cursor.sh'];
|
|
39
|
+
export const BACKEND_HOSTS = ['api2.cursor.sh', 'agent.api5.cursor.sh', 'agentn.api5.cursor.sh'];
|
|
40
40
|
|
|
41
41
|
export function isBlocked() {
|
|
42
42
|
try {
|
package/lib/setup.js
CHANGED
|
@@ -335,7 +335,7 @@ export async function setupEditor(which, target) {
|
|
|
335
335
|
// rejected our cert — the ECONNRESET-before-ALPN wall in the backend log, and
|
|
336
336
|
// exactly the model/chat calls we need. NODE_TLS_REJECT_UNAUTHORIZED=0 is the
|
|
337
337
|
// Node-side switch. Only set under takeover, where we own the endpoint.
|
|
338
|
-
if (which === 'cursor' &&
|
|
338
|
+
if (which === 'cursor' && process.argv.includes('--takeover')) env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
339
339
|
|
|
340
340
|
console.log('');
|
|
341
341
|
console.log(`mcp: ${mcpFile} (openzoo: zoo_bind, zoo_ask, zoo_models, zoo_wallet, zoo_contexts)`);
|
|
@@ -459,7 +459,13 @@ export async function setupEditor(which, target) {
|
|
|
459
459
|
// DEFAULT ON. Takeover is what makes a plan-less account route, and it is
|
|
460
460
|
// harmless on an entitled one (the editor just reads our catalog instead of
|
|
461
461
|
// theirs), so it runs unless explicitly disabled with --no-takeover.
|
|
462
|
-
|
|
462
|
+
// OPT-IN ONLY. Takeover impersonates Cursor's backend to unlock paid-tier
|
|
463
|
+
// model selection on an unpaid account — that is defeating Cursor's own
|
|
464
|
+
// subscription gate, not routing, so it is NOT the default. Plain
|
|
465
|
+
// `openzoo cursor` writes the base URL + models and routes Auto (and any model
|
|
466
|
+
// an entitled account can pick) through the zoo. --takeover is the escape
|
|
467
|
+
// hatch for those who understand what it does.
|
|
468
|
+
const doTakeover = target0 === 'cursor' && process.argv.includes('--takeover');
|
|
463
469
|
if (doTakeover) {
|
|
464
470
|
try {
|
|
465
471
|
const { ensureCert } = await import('./cursorbackend.js');
|
|
@@ -550,7 +556,7 @@ export async function setupEditor(which, target) {
|
|
|
550
556
|
const args = [cwd];
|
|
551
557
|
// Trust our self-signed impersonation cert without any CA install — this flag
|
|
552
558
|
// is the entire reason no trust prompt is needed. Only added under --takeover.
|
|
553
|
-
if (
|
|
559
|
+
if (doTakeover) args.unshift('--ignore-certificate-errors');
|
|
554
560
|
if (useProfile) {
|
|
555
561
|
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
|
556
562
|
args.unshift(`--user-data-dir=${PROFILE_DIR}`, `--extensions-dir=${path.join(PROFILE_DIR, 'extensions')}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
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",
|