openzoo 0.11.0 → 0.11.2

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 CHANGED
@@ -38,6 +38,35 @@ const DEFAULT_EXTS = [
38
38
 
39
39
  const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'dist', 'build', '.next', '__pycache__', '.venv']);
40
40
 
41
+ /** Refuse rather than grind. A directory of exports can be hundreds of MB;
42
+ * binding that takes many minutes and mostly buys markup. Raise deliberately. */
43
+ export const MAX_TOTAL_BYTES = Number(process.env.OPENZOO_BIND_MAX_BYTES || 32 * 1024 * 1024);
44
+
45
+ /**
46
+ * HTML → readable text. Chat/forum exports are ~90% markup: binding the raw
47
+ * pages wastes the corpus on tags and makes retrieval rank div soup against
48
+ * the question. Deliberately crude (no parser dependency) but it keeps the
49
+ * text nodes in document order, which is all retrieval needs.
50
+ */
51
+ export function stripHtml(html) {
52
+ return html
53
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
54
+ .replace(/<!--[\s\S]*?-->/g, ' ')
55
+ .replace(/<br\s*\/?>/gi, '\n')
56
+ .replace(/<\/(p|div|li|tr|h[1-6])>/gi, '\n')
57
+ .replace(/<[^>]+>/g, ' ')
58
+ .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')
59
+ .replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
60
+ .replace(/[ \t]+/g, ' ')
61
+ .replace(/\n\s*\n\s*\n+/g, '\n\n')
62
+ .trim();
63
+ }
64
+
65
+ const readAsText = (file) => {
66
+ const raw = fs.readFileSync(file, 'utf8');
67
+ return /\.html?$/i.test(file) ? stripHtml(raw) : raw;
68
+ };
69
+
41
70
  /** Every bindable file under `root` (or just `root` if it is a file). */
42
71
  export function collectFiles(root, { exts = DEFAULT_EXTS, maxFiles = 5000 } = {}) {
43
72
  const st = fs.statSync(root);
@@ -109,11 +138,27 @@ export async function bindPath(target, { exts, onProgress, force = false } = {})
109
138
  const files = collectFiles(resolved, exts ? { exts } : {});
110
139
  if (!files.length) throw new Error(`nothing bindable under ${resolved} (looked for text-like files)`);
111
140
 
141
+ // Size the job BEFORE reading anything. A 2GB export folder is 100MB of
142
+ // text-like files, and silently grinding through it for minutes reads as a
143
+ // hang — an actionable refusal is better than a spinner.
144
+ const onDisk = files.reduce((n, f) => n + fs.statSync(f).size, 0);
145
+ if (onDisk > MAX_TOTAL_BYTES) {
146
+ const biggest = files
147
+ .map((f) => ({ f, size: fs.statSync(f).size }))
148
+ .sort((a, b) => b.size - a.size).slice(0, 3)
149
+ .map(({ f, size }) => `${path.basename(f)} (${(size / 1048576).toFixed(1)}MB)`);
150
+ throw new Error(
151
+ `${(onDisk / 1048576).toFixed(0)}MB across ${files.length} files exceeds the ${(MAX_TOTAL_BYTES / 1048576).toFixed(0)}MB bind limit. `
152
+ + `Largest: ${biggest.join(', ')}. Point at one file, narrow with --ext/ext, `
153
+ + `or raise OPENZOO_BIND_MAX_BYTES if you really want all of it.`,
154
+ );
155
+ }
156
+
112
157
  // Each file is prefixed with its path so retrieval can cite where a passage
113
158
  // came from — a corpus of concatenated files with no provenance is much
114
- // less useful to answer from.
159
+ // less useful to answer from. HTML is reduced to its text (see readAsText).
115
160
  const text = files
116
- .map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${fs.readFileSync(f, 'utf8')}`)
161
+ .map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${readAsText(f)}`)
117
162
  .join('\n\n');
118
163
 
119
164
  const bytes = Buffer.byteLength(text);
package/lib/brief.js CHANGED
@@ -33,7 +33,8 @@ export const briefFor = (selfUrl) => [
33
33
  ' Keep any single request under ~8MB. Bigger bodies are dropped by the network hop before they reach the proxy (an opaque 413 or a dead connection). This is a REQUEST size limit, not a context limit — the bound context can be far larger, which is what parts are for.',
34
34
  ' Paths: your base_url already ends in /v1, so post to {base_url}/hrr/bind — NOT {base_url}/v1/hrr/bind (that double /v1 404s; the proxy repairs it, but do not rely on that).',
35
35
  '',
36
- '3. PAYMENT IS HANDLED. Every call is paid per-request from the operator\'s wallet via x402 (Solana / Base / Robinhood Chain, whichever is funded). There is no API key to find, no account, no rate limit to negotiate. Never search the operator\'s machine for credentials or endpoints — GET / on this proxy returns everything discoverable about it.',
36
+ '3. PAYMENT IS HANDLED. Every call is paid per-request from the operator\'s wallet via x402 (Solana / Base / Robinhood Chain, whichever is funded). There is no account and no rate limit to negotiate, and you never handle money. Never search the operator\'s machine for credentials — GET / on this proxy describes it.',
37
+ ' AUTH, precisely: /hrr/bind and GET /models need NO key, so a script you write can call them directly. Paid endpoints (/chat/completions) need the bearer key your client is already configured with — you cannot read that key, so DO NOT write a standalone script that calls a paid endpoint. Bind from a script if you like, then ask through this conversation, which is already authenticated.',
37
38
  '',
38
39
  '4. MODEL IDS ARE FORGIVING. Ask for any model id you like; unknown ids are matched to the nearest served model, and /v1/models lists what is real (each alias row carries `served_by`).',
39
40
  ].join('\n');
package/lib/proxy.js CHANGED
@@ -320,8 +320,16 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
320
320
  }, null, 2));
321
321
  return;
322
322
  }
323
+ // Binding COSTS NOTHING — no 402, no wallet, no settlement. Gating it
324
+ // behind the key only stopped agents from using the one endpoint that
325
+ // makes a big corpus workable: observed in the wild, an agent wrote a
326
+ // correct multi-part bind script, got 401 on the final append, and
327
+ // fell back to stuffing the corpus inline. The money paths below stay
328
+ // gated; the worst a stranger can do here is spend our sidecar's disk.
323
329
  const freeRead = req.method === 'GET' && (p === '/v1/models' || p.startsWith('/v1/models/'));
324
- if (!freeRead) {
330
+ const freeBind = req.method === 'POST' && p === '/v1/hrr/bind';
331
+ if (freeBind) log(`public url: unauthenticated bind allowed (free endpoint) from ${req.socket.remoteAddress}`);
332
+ if (!freeRead && !freeBind) {
325
333
  log(`public url: 401 ${req.method} ${req.url}`);
326
334
  jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at the operator\'s proxy startup — GET / for discovery, GET /v1/models is open');
327
335
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",