openzoo 0.28.2 → 0.29.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/bin/openzoo.js +2 -1
- package/lib/cursorapi.js +101 -0
- package/lib/cursorbackend.js +148 -0
- package/lib/cursorcfg.js +9 -1
- package/lib/hosts.js +28 -0
- package/lib/setup.js +25 -0
- package/package.json +2 -1
package/bin/openzoo.js
CHANGED
|
@@ -77,7 +77,8 @@ async function main() {
|
|
|
77
77
|
break;
|
|
78
78
|
}
|
|
79
79
|
case 'unblock': {
|
|
80
|
-
const { unblockBackend, isBlocked } = await import('../lib/hosts.js');
|
|
80
|
+
const { unblockBackend, unredirect443, isBlocked } = await import('../lib/hosts.js');
|
|
81
|
+
try { unredirect443(); } catch { /* no redirect to remove */ }
|
|
81
82
|
const r = unblockBackend();
|
|
82
83
|
console.log(r.already ? 'not blocked — nothing to undo' : (isBlocked() ? 'still blocked (sudo declined?)' : 'restored: the editor can reach its own backend again'));
|
|
83
84
|
break;
|
package/lib/cursorapi.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSWER api2.cursor.sh OURSELVES — so a plan-less account can still route.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. Every config-level trick hit the same wall: the editor decides
|
|
5
|
+
* client-side whether a model may be used, and that decision comes from its own
|
|
6
|
+
* backend. On an entitled account the custom OpenAI endpoint works (measured: two
|
|
7
|
+
* Solana settlements). On a plan-less one the editor refuses before a request
|
|
8
|
+
* exists — "NOT ROUTING, 0 requests" — and nothing we write to its database can
|
|
9
|
+
* reach that. Blackholing api2.cursor.sh stops the refusal but also stops the
|
|
10
|
+
* catalog, which is why the picker collapses.
|
|
11
|
+
*
|
|
12
|
+
* So: instead of blackholing that host, SERVE it. /etc/hosts already points it at
|
|
13
|
+
* 127.0.0.1; this is the thing that answers. We return the model catalog and the
|
|
14
|
+
* entitlement fields ourselves, so the editor believes every model we publish is
|
|
15
|
+
* available, and its inference still goes to the configured base URL — us.
|
|
16
|
+
*
|
|
17
|
+
* WIRE FORMAT, read out of the editor's own bundle (not guessed):
|
|
18
|
+
* POST /aiserver.v1.<Service>/<Method>, Connect-RPC, content-type
|
|
19
|
+
* application/proto (binary) or application/json.
|
|
20
|
+
* aiserver.v1.AvailableModelsResponse
|
|
21
|
+
* 1 model_names repeated string
|
|
22
|
+
* 2 models repeated AvailableModel
|
|
23
|
+
* AvailableModelsResponse.AvailableModel
|
|
24
|
+
* 1 name string · 2 default_on bool · 5 supports_agent bool
|
|
25
|
+
* 6 degradation_status enum(0=UNSPECIFIED) · 9 supports_thinking bool
|
|
26
|
+
* 10 supports_images bool · 14 supports_max_mode bool
|
|
27
|
+
* 19 supports_non_max_mode bool · 17 client_display_name string
|
|
28
|
+
* 18 server_model_name string · 22 supports_plan_mode bool
|
|
29
|
+
*
|
|
30
|
+
* LIMITS, STATED PLAINLY: this impersonates a host the editor authenticates to,
|
|
31
|
+
* so it needs a TLS cert the editor trusts (a local CA in the system store) and
|
|
32
|
+
* it will break whenever the vendor changes their protobuf. It is opt-in.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Minimal protobuf wire writer — enough for the two message shapes above. */
|
|
36
|
+
class Buf {
|
|
37
|
+
constructor() { this.parts = []; }
|
|
38
|
+
|
|
39
|
+
tag(field, wire) { return this.varint((field << 3) | wire); }
|
|
40
|
+
|
|
41
|
+
varint(n) {
|
|
42
|
+
const out = [];
|
|
43
|
+
let v = Number(n);
|
|
44
|
+
do { let b = v & 0x7f; v >>>= 7; if (v) b |= 0x80; out.push(b); } while (v);
|
|
45
|
+
this.parts.push(Buffer.from(out));
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bool(field, v) { if (v === undefined) return this; this.tag(field, 0); return this.varint(v ? 1 : 0); }
|
|
50
|
+
|
|
51
|
+
int(field, v) { if (v === undefined) return this; this.tag(field, 0); return this.varint(v); }
|
|
52
|
+
|
|
53
|
+
str(field, v) {
|
|
54
|
+
if (v === undefined || v === null) return this;
|
|
55
|
+
const b = Buffer.from(String(v), 'utf8');
|
|
56
|
+
this.tag(field, 2); this.varint(b.length); this.parts.push(b);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
msg(field, inner) {
|
|
61
|
+
const b = inner.done();
|
|
62
|
+
this.tag(field, 2); this.varint(b.length); this.parts.push(b);
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
done() { return Buffer.concat(this.parts); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One AvailableModel, with every gate answered in the affirmative. */
|
|
70
|
+
function encodeModel(m) {
|
|
71
|
+
return new Buf()
|
|
72
|
+
.str(1, m.name)
|
|
73
|
+
.bool(2, true) // default_on
|
|
74
|
+
.bool(5, true) // supports_agent
|
|
75
|
+
.int(6, 0) // degradation_status = UNSPECIFIED
|
|
76
|
+
.bool(9, true) // supports_thinking
|
|
77
|
+
.bool(10, true) // supports_images
|
|
78
|
+
.bool(14, true) // supports_max_mode <- kills "Max Mode required"
|
|
79
|
+
.bool(19, true) // supports_non_max_mode
|
|
80
|
+
.int(15, m.contextTokenLimit ?? 200000)
|
|
81
|
+
.str(17, m.label ?? m.name) // client_display_name
|
|
82
|
+
.str(18, m.name) // server_model_name
|
|
83
|
+
.bool(21, true) // is_recommended_for_background_composer
|
|
84
|
+
.bool(22, true); // supports_plan_mode
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** aiserver.v1.AvailableModelsResponse over the models we serve. */
|
|
88
|
+
export function encodeAvailableModels(models) {
|
|
89
|
+
const b = new Buf();
|
|
90
|
+
for (const m of models) b.str(1, m.name); // model_names
|
|
91
|
+
for (const m of models) b.msg(2, encodeModel(m)); // models
|
|
92
|
+
return b.done();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Connect unary framing: 5-byte prefix (flags + big-endian length) + payload. */
|
|
96
|
+
export function connectFrame(payload) {
|
|
97
|
+
const head = Buffer.alloc(5);
|
|
98
|
+
head.writeUInt8(0, 0);
|
|
99
|
+
head.writeUInt32BE(payload.length, 1);
|
|
100
|
+
return Buffer.concat([head, payload]);
|
|
101
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impersonate api2.cursor.sh so a PLAN-LESS editor still routes to the zoo.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM THIS SOLVES. The editor decides client-side whether a model may be
|
|
5
|
+
* used, from an answer its own backend gives. On an entitled account the custom
|
|
6
|
+
* OpenAI endpoint works (measured: real Solana settlements). On a free account the
|
|
7
|
+
* editor refuses before a request exists — "NOT ROUTING, 0 requests" — and nothing
|
|
8
|
+
* we write to its database reaches that decision. The only way to change the
|
|
9
|
+
* decision is to be the thing that answers it.
|
|
10
|
+
*
|
|
11
|
+
* WHY NO CERT INSTALL. Impersonating an HTTPS host normally needs a CA the editor
|
|
12
|
+
* trusts, and no real user installs one — the correct objection that killed the
|
|
13
|
+
* mkcert approach. But WE spawn the editor binary, so we pass Chromium's
|
|
14
|
+
* `--ignore-certificate-errors`, and a plain self-signed cert is accepted with
|
|
15
|
+
* zero prompts and zero trust-store changes. Verified from the bundle that the
|
|
16
|
+
* editor does NOT pin certs (no certificatePinning / pinnedPublicKey /
|
|
17
|
+
* checkServerIdentity), so the flag is honoured.
|
|
18
|
+
*
|
|
19
|
+
* WHAT WE ANSWER. Only `AvailableModels` needs a real body — the catalog, with
|
|
20
|
+
* every gate (supports_max_mode, etc.) set true so nothing is refused. Every other
|
|
21
|
+
* startup call (GetServerConfig, CheckUsage, GetTeams, GetUserPrivacyMode, …) gets
|
|
22
|
+
* an EMPTY protobuf message, which decodes as all-defaults and is valid for ANY
|
|
23
|
+
* message type — so we do not need each method's schema, only the one that matters.
|
|
24
|
+
*
|
|
25
|
+
* WIRE FORMAT. The editor uses Connect-RPC and gRPC-web. We detect the request's
|
|
26
|
+
* content-type and answer in kind: bare message for connect unary proto, enveloped
|
|
27
|
+
* + trailers for grpc-web. Both are handled below.
|
|
28
|
+
*/
|
|
29
|
+
import https from 'node:https';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import os from 'node:os';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { execFileSync } from 'node:child_process';
|
|
34
|
+
import { encodeAvailableModels } from './cursorapi.js';
|
|
35
|
+
|
|
36
|
+
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
37
|
+
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
38
|
+
|
|
39
|
+
/** Generate (once) a self-signed cert covering the cursor backends. openssl is
|
|
40
|
+
* on every mac/linux; this is the only external tool and it is not a trust op. */
|
|
41
|
+
export function ensureCert(log = () => {}) {
|
|
42
|
+
const cert = path.join(TLS_DIR, 'cert.pem');
|
|
43
|
+
const key = path.join(TLS_DIR, 'key.pem');
|
|
44
|
+
try { fs.accessSync(cert); fs.accessSync(key); return { cert, key }; } catch { /* make it */ }
|
|
45
|
+
fs.mkdirSync(TLS_DIR, { recursive: true });
|
|
46
|
+
const san = `subjectAltName=${CURSOR_HOSTS.map((h) => `DNS:${h}`).join(',')}`;
|
|
47
|
+
execFileSync('openssl', [
|
|
48
|
+
'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
|
|
49
|
+
'-keyout', key, '-out', cert, '-days', '3650',
|
|
50
|
+
'-subj', '/CN=api2.cursor.sh', '-addext', san,
|
|
51
|
+
], { stdio: 'ignore' });
|
|
52
|
+
log(`cursor-tls: self-signed cert minted at ${TLS_DIR} (no CA, no trust prompt)`);
|
|
53
|
+
return { cert, key };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Read the whole request body. */
|
|
57
|
+
function readBody(req) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
req.on('data', (c) => chunks.push(c));
|
|
61
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
62
|
+
req.on('error', () => resolve(Buffer.alloc(0)));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** grpc-web trailers frame: an enveloped block of "grpc-status:0\r\n". */
|
|
67
|
+
function grpcWebTrailer() {
|
|
68
|
+
const t = Buffer.from('grpc-status:0\r\ngrpc-message:\r\n', 'utf8');
|
|
69
|
+
const head = Buffer.alloc(5);
|
|
70
|
+
head.writeUInt8(0x80, 0); // trailer flag
|
|
71
|
+
head.writeUInt32BE(t.length, 1);
|
|
72
|
+
return Buffer.concat([head, t]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Envelope a message for grpc-web / connect-streaming (5-byte prefix). */
|
|
76
|
+
function envelope(payload) {
|
|
77
|
+
const head = Buffer.alloc(5);
|
|
78
|
+
head.writeUInt8(0, 0);
|
|
79
|
+
head.writeUInt32BE(payload.length, 1);
|
|
80
|
+
return Buffer.concat([head, payload]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Answer one Connect/gRPC-web call. `method` is the trailing method name,
|
|
85
|
+
* `models` the catalog to publish. Non-catalog methods get an empty-OK body.
|
|
86
|
+
*/
|
|
87
|
+
function respond(req, res, method, models) {
|
|
88
|
+
const ct = String(req.headers['content-type'] || '');
|
|
89
|
+
const isGrpcWeb = ct.includes('grpc-web');
|
|
90
|
+
const isJson = ct.includes('json');
|
|
91
|
+
|
|
92
|
+
const bodyProto = method === 'AvailableModels' ? encodeAvailableModels(models) : Buffer.alloc(0);
|
|
93
|
+
|
|
94
|
+
if (isJson) {
|
|
95
|
+
// Connect unary JSON. AvailableModels as JSON; everything else an empty object.
|
|
96
|
+
const json = method === 'AvailableModels'
|
|
97
|
+
? JSON.stringify({ modelNames: models.map((m) => m.name), models: models.map((m) => ({
|
|
98
|
+
name: m.name, defaultOn: true, supportsAgent: true, supportsMaxMode: true,
|
|
99
|
+
supportsNonMaxMode: true, supportsThinking: true, supportsImages: true,
|
|
100
|
+
clientDisplayName: m.label ?? m.name, serverModelName: m.name, supportsPlanMode: true,
|
|
101
|
+
})) })
|
|
102
|
+
: '{}';
|
|
103
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
104
|
+
res.end(json);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isGrpcWeb) {
|
|
109
|
+
res.writeHead(200, {
|
|
110
|
+
'content-type': ct.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
|
|
111
|
+
'grpc-status': '0',
|
|
112
|
+
});
|
|
113
|
+
res.end(Buffer.concat([envelope(bodyProto), grpcWebTrailer()]));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Connect unary proto: bare message, status in headers.
|
|
118
|
+
res.writeHead(200, { 'content-type': 'application/proto' });
|
|
119
|
+
res.end(bodyProto);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Start the impersonation server. Binds `port` (default 8443 — a privileged
|
|
124
|
+
* 443->port redirect is installed alongside the /etc/hosts entry, so this stays
|
|
125
|
+
* unprivileged). Returns { server, port }.
|
|
126
|
+
*/
|
|
127
|
+
export function startCursorBackend({ port = 8443, models, log = () => {} } = {}) {
|
|
128
|
+
const { cert, key } = ensureCert(log);
|
|
129
|
+
const server = https.createServer(
|
|
130
|
+
{ cert: fs.readFileSync(cert), key: fs.readFileSync(key) },
|
|
131
|
+
async (req, res) => {
|
|
132
|
+
// Connect method is the last path segment: /aiserver.v1.XService/Method
|
|
133
|
+
const method = (req.url || '').split('/').filter(Boolean).pop() || '';
|
|
134
|
+
await readBody(req);
|
|
135
|
+
try {
|
|
136
|
+
respond(req, res, method, models);
|
|
137
|
+
if (method === 'AvailableModels') log(`cursor-backend: served AvailableModels (${models.length} models, all gates open)`);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
res.writeHead(200, { 'content-type': 'application/proto', 'grpc-status': '0' });
|
|
140
|
+
res.end(Buffer.alloc(0));
|
|
141
|
+
log(`cursor-backend: ${method} -> empty-ok (${e.message})`);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
server.on('tlsClientError', () => { /* editor probing; ignore */ });
|
|
146
|
+
server.listen(port, '127.0.0.1', () => log(`cursor-backend: listening on 127.0.0.1:${port} as ${CURSOR_HOSTS[0]}`));
|
|
147
|
+
return { server, port };
|
|
148
|
+
}
|
package/lib/cursorcfg.js
CHANGED
|
@@ -190,6 +190,14 @@ export function writeEditorProviderConfig(which, { baseUrl, models }) {
|
|
|
190
190
|
// in a group the dropdown does not draw — visible in Settings (which ignores
|
|
191
191
|
// the index) but "unavailable" in the composer picker. That split — right in
|
|
192
192
|
// Settings, missing from the dropdown — is the tell.
|
|
193
|
+
// BOTH VIEWS. Read out of Cursor's own bundle:
|
|
194
|
+
// routed view (the dropdown that opens on "Auto"):
|
|
195
|
+
// _d_(t) => t.visibleInRoutedModelView === true && t.defaultOn !== false
|
|
196
|
+
// named view (the Settings list):
|
|
197
|
+
// filter(v => v.namedModelSectionIndex !== undefined)
|
|
198
|
+
// We only ever set the second, so our models were present in Settings and
|
|
199
|
+
// ABSENT from the composer dropdown — exactly the split that was reported.
|
|
200
|
+
visibleInRoutedModelView: true,
|
|
193
201
|
namedModelSectionIndex: 0,
|
|
194
202
|
cloudAgentEffortModes: [],
|
|
195
203
|
modelPickerBadges: [],
|
|
@@ -265,7 +273,7 @@ export function pinEditorProviderConfig(which, { baseUrl, models }) {
|
|
|
265
273
|
isRecommendedForBackgroundComposer: false, supportsPlanMode: true,
|
|
266
274
|
supportsSandboxing: true, isUserAdded: true, inputboxShortModelName: slotFor(m).label,
|
|
267
275
|
parameterDefinitions: [], variants: [], legacySlugs: [], idAliases: [],
|
|
268
|
-
namedModelSectionIndex: 0, cloudAgentEffortModes: [], modelPickerBadges: [],
|
|
276
|
+
visibleInRoutedModelView: true, namedModelSectionIndex: 0, cloudAgentEffortModes: [], modelPickerBadges: [],
|
|
269
277
|
}))));
|
|
270
278
|
const primary = esc(models[0]);
|
|
271
279
|
const base = esc(baseUrl);
|
package/lib/hosts.js
CHANGED
|
@@ -109,3 +109,31 @@ export function unblockBackend() {
|
|
|
109
109
|
);
|
|
110
110
|
return { ok, blocked: isBlocked() };
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Redirect 127.0.0.1:443 -> a high unprivileged port, so the impersonation
|
|
115
|
+
* server (lib/cursorbackend.js) can answer api2.cursor.sh without running as
|
|
116
|
+
* root. Applied in the SAME privileged step as the hosts entry; undone by
|
|
117
|
+
* unblock. Platform-branched; on anything but macOS/Linux we print the command.
|
|
118
|
+
*/
|
|
119
|
+
export function redirect443(toPort, log = console.log) {
|
|
120
|
+
if (process.platform === 'darwin') {
|
|
121
|
+
// pf anchor scoped to loopback; -E keeps pf enabled, the anchor is ours to flush.
|
|
122
|
+
const rule = `rdr pass on lo0 inet proto tcp from any to 127.0.0.1 port 443 -> 127.0.0.1 port ${toPort}`;
|
|
123
|
+
const ok = privileged(`echo '${rule}' | pfctl -a openzoo -f - 2>/dev/null; pfctl -e 2>/dev/null; true`);
|
|
124
|
+
return { ok };
|
|
125
|
+
}
|
|
126
|
+
if (process.platform === 'linux') {
|
|
127
|
+
const ok = privileged(`iptables -t nat -C OUTPUT -p tcp -o lo --dport 443 -j REDIRECT --to-ports ${toPort} 2>/dev/null || iptables -t nat -A OUTPUT -p tcp -o lo --dport 443 -j REDIRECT --to-ports ${toPort}; true`);
|
|
128
|
+
return { ok };
|
|
129
|
+
}
|
|
130
|
+
log(` windows: run in an ADMINISTRATOR shell:`);
|
|
131
|
+
log(` netsh interface portproxy add v4tov4 listenport=443 listenaddress=127.0.0.1 connectport=${toPort} connectaddress=127.0.0.1`);
|
|
132
|
+
return { ok: false, manual: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function unredirect443() {
|
|
136
|
+
if (process.platform === 'darwin') return { ok: privileged('pfctl -a openzoo -F all 2>/dev/null; true') };
|
|
137
|
+
if (process.platform === 'linux') return { ok: privileged('iptables -t nat -D OUTPUT -p tcp -o lo --dport 443 -j REDIRECT --to-ports 8443 2>/dev/null; true') };
|
|
138
|
+
return { ok: false, manual: true };
|
|
139
|
+
}
|
package/lib/setup.js
CHANGED
|
@@ -442,6 +442,28 @@ export async function setupEditor(which, target) {
|
|
|
442
442
|
else console.log('backend: NOT blocked — the editor will keep using its own backend and\n nothing will reach the zoo. Re-run and enter your password.');
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
// 2c. TAKEOVER (--takeover): impersonate the editor's backend so a PLAN-LESS
|
|
446
|
+
// account can route. Config alone cannot help there — the editor refuses
|
|
447
|
+
// unentitled models before a request exists. So we ANSWER api2.cursor.sh
|
|
448
|
+
// ourselves: catalog with every gate open, empty-valid for the rest. No CA
|
|
449
|
+
// install — the editor is launched with --ignore-certificate-errors below,
|
|
450
|
+
// and it does not pin certs (verified), so a self-signed cert is accepted.
|
|
451
|
+
if (target0 === 'cursor' && process.argv.includes('--takeover')) {
|
|
452
|
+
try {
|
|
453
|
+
const { startCursorBackend } = await import('./cursorbackend.js');
|
|
454
|
+
const { redirect443 } = await import('./hosts.js');
|
|
455
|
+
const backModels = models.map((m) => ({ name: m, label: m }));
|
|
456
|
+
startCursorBackend({ port: 8443, models: backModels, log: (s) => console.log(s) });
|
|
457
|
+
const rr = redirect443(8443, console.log);
|
|
458
|
+
console.log(rr.ok ? 'takeover: 443 -> 8443 redirect installed (npx openzoo unblock reverts)'
|
|
459
|
+
: rr.manual ? 'takeover: apply the redirect above, then relaunch'
|
|
460
|
+
: 'takeover: could NOT install the 443 redirect — impersonation will not receive traffic');
|
|
461
|
+
console.log('takeover: launching with --ignore-certificate-errors so the self-signed cert is trusted (no CA install)');
|
|
462
|
+
} catch (e) {
|
|
463
|
+
console.log(`takeover: failed to start (${e.message}) — falling back to plain routing`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
445
467
|
|
|
446
468
|
|
|
447
469
|
// 3. LAUNCH with that env. Editor resolved platform-agnostically; Cursor
|
|
@@ -473,6 +495,9 @@ export async function setupEditor(which, target) {
|
|
|
473
495
|
}
|
|
474
496
|
const useProfile = process.env.OPENZOO_PROFILE === '1' || process.argv.includes('--profile');
|
|
475
497
|
const args = [cwd];
|
|
498
|
+
// Trust our self-signed impersonation cert without any CA install — this flag
|
|
499
|
+
// is the entire reason no trust prompt is needed. Only added under --takeover.
|
|
500
|
+
if (target0 === 'cursor' && process.argv.includes('--takeover')) args.unshift('--ignore-certificate-errors');
|
|
476
501
|
if (useProfile) {
|
|
477
502
|
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
|
478
503
|
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.29.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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
23
23
|
"@solana/spl-token": "^0.4.14",
|
|
24
24
|
"@solana/web3.js": "^1.98.4",
|
|
25
|
+
"selfsigned": "^5.5.0",
|
|
25
26
|
"viem": "^2.21.0",
|
|
26
27
|
"zod": "^3.24.0"
|
|
27
28
|
},
|