openzoo 0.50.74 → 0.50.76
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/bindpath.js +84 -3
- package/lib/botlog.js +25 -2
- package/lib/cursorbackend.js +10 -4
- package/package.json +1 -1
package/lib/bindpath.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { config } from './config.js';
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
25
26
|
import { corpusHash, rememberContext, lookupContext } from './contexts.js';
|
|
26
27
|
import { withNamespace } from './namespace.js';
|
|
27
28
|
|
|
@@ -122,6 +123,74 @@ export function splitIntoParts(text, maxBytes = MAX_PART_BYTES) {
|
|
|
122
123
|
return parts;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* DELTA BIND — ship only the chunks the server does not already hold.
|
|
129
|
+
*
|
|
130
|
+
* The whole-bind path below hashes the CONCATENATED corpus and re-uploads
|
|
131
|
+
* every part on a miss, so editing one file in a repo re-ships the tree.
|
|
132
|
+
* MEASURED here: a 23MB Rust tree in 4MB parts ran 25s -> 30s -> 65s, and a
|
|
133
|
+
* deploy mid-bind threw five good parts away.
|
|
134
|
+
*
|
|
135
|
+
* Here the unit is one file (split further only when a file alone exceeds a
|
|
136
|
+
* part). Probe with sha256 per chunk, get back {missing, known}, ship the
|
|
137
|
+
* missing in batches under MAX_PART_BYTES, and when every hash resolves the
|
|
138
|
+
* corpus binds under an ordinary context_id — through the same chunked path
|
|
139
|
+
* as a whole bind, so recall cannot tell the difference. MEASURED on the
|
|
140
|
+
* sidecar: 6 files / 124,350 chars, editing one file re-ships 22,525 (18%).
|
|
141
|
+
*
|
|
142
|
+
* Any non-200 anywhere returns null and the caller falls back to the whole
|
|
143
|
+
* bind. Delta is an optimisation; it must never be the reason a bind fails.
|
|
144
|
+
*/
|
|
145
|
+
const sha256 = (t) => createHash('sha256').update(t, 'utf8').digest('hex');
|
|
146
|
+
|
|
147
|
+
async function postDelta(payload) {
|
|
148
|
+
const r = await fetch(`${config.apiBase}/v1/hrr/delta`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
151
|
+
body: JSON.stringify(payload),
|
|
152
|
+
});
|
|
153
|
+
if (r.status !== 200) return null;
|
|
154
|
+
const j = await r.json().catch(() => null);
|
|
155
|
+
return j && Array.isArray(j.missing) ? j : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function bindDelta(fileTexts, shardKey, onProgress) {
|
|
159
|
+
const chunks = [];
|
|
160
|
+
for (const t of fileTexts) for (const part of splitIntoParts(t)) chunks.push(part);
|
|
161
|
+
const hashes = chunks.map(sha256);
|
|
162
|
+
const byHash = new Map(hashes.map((h, i) => [h, chunks[i]]));
|
|
163
|
+
|
|
164
|
+
const probe = await postDelta({ chunk_hashes: hashes, shard_key: shardKey });
|
|
165
|
+
if (!probe) return null;
|
|
166
|
+
let missing = probe.missing;
|
|
167
|
+
const total = chunks.reduce((n, c) => n + Buffer.byteLength(c), 0);
|
|
168
|
+
onProgress?.({ stage: 'delta', chunks: chunks.length, missing: missing.length, bytes: total });
|
|
169
|
+
|
|
170
|
+
let last = probe;
|
|
171
|
+
let shipped = 0;
|
|
172
|
+
while (missing.length) {
|
|
173
|
+
// one fill per batch under the part ceiling; the last fill assembles
|
|
174
|
+
const batch = {};
|
|
175
|
+
let size = 0;
|
|
176
|
+
for (const h of missing) {
|
|
177
|
+
const t = byHash.get(h);
|
|
178
|
+
if (t === undefined) return null;
|
|
179
|
+
if (size && size + Buffer.byteLength(t) > MAX_PART_BYTES) break;
|
|
180
|
+
batch[h] = t; size += Buffer.byteLength(t);
|
|
181
|
+
}
|
|
182
|
+
if (!Object.keys(batch).length) return null;
|
|
183
|
+
last = await postDelta({ chunk_hashes: hashes, chunks: batch, shard_key: shardKey });
|
|
184
|
+
if (!last) return null;
|
|
185
|
+
shipped += size;
|
|
186
|
+
onProgress?.({ stage: 'delta-fill', shipped, of: total, remaining: last.missing.length });
|
|
187
|
+
if (last.missing.length >= missing.length) return null; // no progress: refused chunks
|
|
188
|
+
missing = last.missing;
|
|
189
|
+
}
|
|
190
|
+
if (!last.complete || !last.context_id) return null;
|
|
191
|
+
return { contextId: last.context_id, chunks: chunks.length, shipped, bytes: total };
|
|
192
|
+
}
|
|
193
|
+
|
|
125
194
|
async function postBind(payload) {
|
|
126
195
|
const r = await fetch(`${config.apiBase}/v1/hrr/bind`, {
|
|
127
196
|
method: 'POST',
|
|
@@ -167,9 +236,9 @@ export async function bindPath(target, { exts, onProgress, force = false } = {})
|
|
|
167
236
|
// Each file is prefixed with its path so retrieval can cite where a passage
|
|
168
237
|
// came from — a corpus of concatenated files with no provenance is much
|
|
169
238
|
// less useful to answer from. HTML is reduced to its text (see readAsText).
|
|
170
|
-
const
|
|
171
|
-
.map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${readAsText(f)}`)
|
|
172
|
-
|
|
239
|
+
const fileTexts = files
|
|
240
|
+
.map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${readAsText(f)}`);
|
|
241
|
+
const text = fileTexts.join('\n\n');
|
|
173
242
|
|
|
174
243
|
const bytes = Buffer.byteLength(text);
|
|
175
244
|
const hash = corpusHash(text);
|
|
@@ -181,6 +250,18 @@ export async function bindPath(target, { exts, onProgress, force = false } = {})
|
|
|
181
250
|
}
|
|
182
251
|
}
|
|
183
252
|
|
|
253
|
+
// Delta first: on a re-bind after an edit this ships one file, not the tree.
|
|
254
|
+
// OPENZOO_BIND_DELTA=0 forces the whole-bind path.
|
|
255
|
+
if (process.env.OPENZOO_BIND_DELTA !== '0') {
|
|
256
|
+
try {
|
|
257
|
+
const d = await bindDelta(fileTexts, resolved, onProgress);
|
|
258
|
+
if (d) {
|
|
259
|
+
rememberContext(config.apiBase, hash, d.contextId);
|
|
260
|
+
return { contextId: d.contextId, files, parts: d.chunks, bytes, reused: false, delta: true, shipped: d.shipped };
|
|
261
|
+
}
|
|
262
|
+
} catch { /* fall through to the whole bind */ }
|
|
263
|
+
}
|
|
264
|
+
|
|
184
265
|
const parts = splitIntoParts(text);
|
|
185
266
|
onProgress?.({ stage: 'start', files: files.length, parts: parts.length, bytes });
|
|
186
267
|
|
package/lib/botlog.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* Default is QUIET: only milestones and problems. `--verbose` / OPENZOO_DEBUG=1
|
|
10
10
|
* restores the firehose (it is still the right thing for debugging the wire).
|
|
11
11
|
*/
|
|
12
|
+
import fsSync from 'node:fs';
|
|
13
|
+
import osMod from 'node:os';
|
|
14
|
+
import pathMod from 'node:path';
|
|
12
15
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
16
|
|
|
14
17
|
const NOISE = [
|
|
@@ -30,6 +33,8 @@ const NOISE = [
|
|
|
30
33
|
];
|
|
31
34
|
|
|
32
35
|
const MILESTONE = [
|
|
36
|
+
/zoo POST :8402 model=/, // one line per turn: which bot asked which model
|
|
37
|
+
/<< zoo (200|4\d\d|5\d\d)/, // one line per turn: how it ended
|
|
33
38
|
/mcp ready/,
|
|
34
39
|
/mcp \S+ (mode=|FAIL|tools=\d+|re-attach|attached|appeared)/,
|
|
35
40
|
/mcp \S+ To let bots drive your real Chrome/,
|
|
@@ -49,11 +54,29 @@ export function isBotMilestone(line) {
|
|
|
49
54
|
return MILESTONE.some((re) => re.test(s));
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Terminal gets milestones (or everything with verbose); the FULL stream is
|
|
59
|
+
* always appended to `file` so quiet mode never destroys evidence.
|
|
60
|
+
* Default file: ~/.openzoo/bot.log (truncated at start of each run).
|
|
61
|
+
*/
|
|
62
|
+
export function makeBotLogger({ verbose = false, write = (m) => console.error(m), file = defaultBotLogPath(), fsMod = null } = {}) {
|
|
63
|
+
let fd = null;
|
|
64
|
+
if (file) {
|
|
65
|
+
try {
|
|
66
|
+
const fsx = fsMod || fsSync;
|
|
67
|
+
fsx.mkdirSync(pathMod.dirname(file), { recursive: true });
|
|
68
|
+
fd = fsx.openSync(file, 'w');
|
|
69
|
+
fsx.writeSync(fd, `# openzoo bot full log ${new Date().toISOString()}\n`);
|
|
70
|
+
} catch { fd = null; }
|
|
71
|
+
}
|
|
53
72
|
return (m) => {
|
|
73
|
+
if (fd != null) { try { (fsMod || fsSync).writeSync(fd, `${new Date().toISOString()} ${m}\n`); } catch { /* disk full etc. */ } }
|
|
54
74
|
if (verbose || isBotMilestone(m)) write(` backend: ${m}`);
|
|
55
75
|
};
|
|
56
76
|
}
|
|
77
|
+
export function defaultBotLogPath(home = osMod.homedir()) {
|
|
78
|
+
return pathMod.join(home, '.openzoo', 'bot.log');
|
|
79
|
+
}
|
|
57
80
|
|
|
58
81
|
/**
|
|
59
82
|
* The block a new user needs before anything else. `balances` is optional
|
|
@@ -86,7 +109,7 @@ export function payBannerLines({ solana, evm, balances = null, whop = 'https://w
|
|
|
86
109
|
lines.push(`openzoo: CHROME attached to your real browser (${chromeMode}).`);
|
|
87
110
|
}
|
|
88
111
|
lines.push('openzoo: FIRST type in any bot: "set up Grok Ship for ~/path/to/repo" — or just give it work.');
|
|
89
|
-
lines.push('openzoo: QUIET add --verbose to see every request the app makes.');
|
|
112
|
+
lines.push('openzoo: QUIET add --verbose to see every request the app makes; the full stream is always in ~/.openzoo/bot.log');
|
|
90
113
|
return lines;
|
|
91
114
|
}
|
|
92
115
|
|
package/lib/cursorbackend.js
CHANGED
|
@@ -1872,9 +1872,15 @@ const MODEL_ALIASES = {
|
|
|
1872
1872
|
flash: 'zai-org/glm-5.3-flash',
|
|
1873
1873
|
// NEVER OPENROUTER for Grok Bot: `auto` is the gateway router over
|
|
1874
1874
|
// OpenRouter's catalog, so it lands on the door-only id instead.
|
|
1875
|
-
auto
|
|
1876
|
-
|
|
1877
|
-
'openzoo/auto'
|
|
1875
|
+
// openzoo/auto is the gateway router; the gateway keeps Auto on door-served
|
|
1876
|
+
// models only, so it never lands on OpenRouter.
|
|
1877
|
+
auto: 'openzoo/auto',
|
|
1878
|
+
'openrouter/auto': 'openzoo/auto',
|
|
1879
|
+
'openzoo/auto': 'openzoo/auto',
|
|
1880
|
+
'fable-5.1': 'anthropic/claude-fable-5.1',
|
|
1881
|
+
'claude-fable-5.1': 'anthropic/claude-fable-5.1',
|
|
1882
|
+
'anthropic/fable-5.1': 'anthropic/claude-fable-5.1',
|
|
1883
|
+
'anthropic/fable-5': 'anthropic/claude-fable-5',
|
|
1878
1884
|
deepseek: 'deepseek/deepseek-v4-pro',
|
|
1879
1885
|
'deepseek-pro': 'deepseek/deepseek-v4-pro',
|
|
1880
1886
|
'deepseek-flash': 'deepseek/deepseek-v4-flash',
|
|
@@ -1888,7 +1894,7 @@ const MODEL_ALIASES = {
|
|
|
1888
1894
|
* — the bazaar (x402 upstream) row, not OpenRouter's `x-ai/grok-4.6`. The
|
|
1889
1895
|
* gateway serves the bare id off an x402 door with an on-chain cogs receipt,
|
|
1890
1896
|
* so an OpenRouter credit outage cannot take it down. */
|
|
1891
|
-
export const DEFAULT_ZOO_MODEL = '
|
|
1897
|
+
export const DEFAULT_ZOO_MODEL = 'openzoo/auto';
|
|
1892
1898
|
async function resolveModelId(raw) {
|
|
1893
1899
|
const s = String(raw || '').trim();
|
|
1894
1900
|
if (!s) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.76",
|
|
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",
|