openzoo 0.50.4 → 0.50.6

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * AUTO-BIND THE WORKING DIRECTORY.
3
+ *
4
+ * WHY THIS EXISTS, measured: a Claude Code session through the proxy reported
5
+ * `spilled 0/12 calls` and `0.67x vs direct` — i.e. it cost MORE than buying
6
+ * the same calls direct. That is not a pricing bug on its own. leCore only
7
+ * earns its markup when it removes context from the upstream request, and it
8
+ * can only remove context that was BOUND. Nothing was ever bound, so every call
9
+ * paid a multiple for a service that did no work.
10
+ *
11
+ * `bindPath()` and `collectFiles()` already existed. Nothing called them.
12
+ * `openzoo bind ./x` was a thing you had to know about and remember, which
13
+ * means in practice it never happened and the headline number stayed below 1.
14
+ *
15
+ * SO: bind the cwd on launch, in the background, and attach the resulting
16
+ * context to every forwarded call.
17
+ *
18
+ * NEVER $HOME. Binding a home directory means walking ~, dotfiles, caches,
19
+ * SSH keys and browser profiles into a corpus that leaves the machine. The
20
+ * home check is not a performance guard, it is the safety one — same for /, and
21
+ * for anywhere that is not actually a project.
22
+ *
23
+ * NON-BLOCKING BY CONSTRUCTION. The agent must start instantly; a bind of a
24
+ * large repo takes seconds to minutes. So this returns immediately, the bind
25
+ * runs in the background, and calls made before it lands simply go unbound —
26
+ * exactly what happens today. Nothing waits, nothing fails closed.
27
+ */
28
+ import { existsSync, statSync } from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+
32
+ /** Set once the background bind lands. Read by the proxy on every forward. */
33
+ let contextId = null;
34
+ let state = 'idle'; // idle | binding | ready | skipped | failed
35
+ let detail = '';
36
+
37
+ export function autoContext() { return contextId; }
38
+ export function autoBindState() { return { state, contextId, detail }; }
39
+
40
+ /**
41
+ * Directories that must never be walked into a corpus.
42
+ *
43
+ * $HOME is the important one — see the module comment. The rest are the places
44
+ * where "bind the cwd" would mean "upload the machine".
45
+ */
46
+ function refuseReason(dir) {
47
+ const home = os.homedir();
48
+ const resolved = path.resolve(dir);
49
+ if (resolved === path.resolve(home)) return 'cwd is $HOME';
50
+ if (resolved === path.parse(resolved).root) return 'cwd is the filesystem root';
51
+ if (resolved === '/tmp' || resolved === os.tmpdir()) return 'cwd is a temp dir';
52
+ // A project has SOMETHING that marks it. Without one of these, "the current
53
+ // directory" is just wherever the shell happened to be, and binding it is a
54
+ // surprise rather than a feature.
55
+ const marks = ['.git', 'package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml',
56
+ 'requirements.txt', 'Gemfile', 'pom.xml', 'build.gradle', 'CMakeLists.txt',
57
+ 'composer.json', 'mix.exs', 'Makefile', 'CLAUDE.md', 'AGENTS.md'];
58
+ if (!marks.some((m) => existsSync(path.join(resolved, m)))) {
59
+ return 'no project marker (.git, package.json, …)';
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Kick off a background bind of `dir`. Returns immediately.
66
+ *
67
+ * `log` is the caller's own writer so this never owns stdout — during a Claude
68
+ * Code session stdout belongs to the TUI, and a stray line corrupts it.
69
+ */
70
+ export function startAutoBind(dir, { log = () => {} } = {}) {
71
+ if (process.env.OPENZOO_NO_AUTOBIND === '1') {
72
+ state = 'skipped'; detail = 'OPENZOO_NO_AUTOBIND=1';
73
+ return;
74
+ }
75
+ let target;
76
+ try {
77
+ target = path.resolve(dir || process.cwd());
78
+ if (!statSync(target).isDirectory()) { state = 'skipped'; detail = 'not a directory'; return; }
79
+ } catch { state = 'skipped'; detail = 'unreadable cwd'; return; }
80
+
81
+ const refuse = refuseReason(target);
82
+ if (refuse) { state = 'skipped'; detail = refuse; return; }
83
+
84
+ state = 'binding';
85
+ detail = target;
86
+
87
+ // Deliberately NOT awaited. See the module comment.
88
+ (async () => {
89
+ try {
90
+ const { bindPath } = await import('./bindpath.js');
91
+ const res = await bindPath(target, {});
92
+ const id = res?.contextId || res?.context_id || null;
93
+ if (id) {
94
+ contextId = id;
95
+ state = 'ready';
96
+ log(`auto-bound ${path.basename(target)} -> ${id}`);
97
+ } else {
98
+ state = 'failed';
99
+ detail = 'bind returned no context id';
100
+ }
101
+ } catch (err) {
102
+ // A failed bind must never take the session with it. Unbound calls are
103
+ // the status quo, not an outage.
104
+ state = 'failed';
105
+ detail = err?.message || String(err);
106
+ log(`auto-bind failed: ${detail}`);
107
+ }
108
+ })();
109
+ }
package/lib/launch.js CHANGED
@@ -402,6 +402,25 @@ export async function launchClaude(argv) {
402
402
  };
403
403
  } catch { /* HUD is best-effort */ }
404
404
 
405
+ // BIND THE WORKING DIRECTORY, IN THE BACKGROUND, BEFORE THE TUI STARTS.
406
+ //
407
+ // Measured on a live session: `spilled 0/12 calls`, `0.67x vs direct` — the
408
+ // proxy cost MORE than buying the same calls direct, because leCore can only
409
+ // remove context that was bound and nothing ever was. `openzoo bind` existed
410
+ // but you had to know about it, so in practice it never ran.
411
+ //
412
+ // Returns immediately; the bind runs in the background and calls made before
413
+ // it lands go unbound, which is exactly today's behaviour. Refuses $HOME and
414
+ // any directory with no project marker — see autobind.js for why that is a
415
+ // safety check and not a performance one. OPENZOO_NO_AUTOBIND=1 opts out.
416
+ try {
417
+ const { startAutoBind, autoBindState } = await import('./autobind.js');
418
+ startAutoBind(process.cwd(), { log: (m) => console.error(`openzoo: ${m}`) });
419
+ const st = autoBindState();
420
+ if (st.state === 'binding') console.error(`openzoo: binding ${st.detail} in the background…`);
421
+ else if (st.state === 'skipped') console.error(`openzoo: not auto-binding (${st.detail})`);
422
+ } catch { /* best-effort: never block the agent on a bind */ }
423
+
405
424
  // SKIP CLAUDE CODE'S FIRST-RUN WIZARD. openzoo needs no account and no key,
406
425
  // so the one thing standing between `openzoo claude` and a prompt is Claude
407
426
  // Code's own onboarding: theme picker, then the trust-this-folder dialog,
package/lib/proxy.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
11
11
  import { tokenBalance } from './x402.js';
12
12
  import { evmTokenBalance } from './evm.js';
13
+ import { autoContext } from './autobind.js';
13
14
  import { modelsListForRequest, isHarnessAliasId } from './models.js';
14
15
  import { withNamespace } from './namespace.js';
15
16
  import { loadSessionSpend, saveSessionSpend } from './session.js';
@@ -62,6 +63,19 @@ function upstreamHeaders(req) {
62
63
  for (const [k, v] of Object.entries(req.headers)) {
63
64
  if (!HOP_BY_HOP.has(k.toLowerCase())) out[k] = v;
64
65
  }
66
+ // ATTACH THE AUTO-BOUND CWD, if there is one and the caller did not name a
67
+ // context itself. Claude Code has no idea x402 or leCore exist and will never
68
+ // send this header, so without an injection here a background bind is dead
69
+ // weight — bound, paid for, never referenced. That is the state that produced
70
+ // `spilled 0/12 calls` and a sub-1.0 savings multiple.
71
+ //
72
+ // An explicit x-hrr-context ALWAYS wins: a caller naming a corpus is stating
73
+ // intent, and silently retargeting it at the cwd would answer from the wrong
74
+ // corpus — the exact failure REPLAY_KEY_HEADERS exists to keep apart.
75
+ if (!Object.keys(out).some((k) => k.toLowerCase() === 'x-hrr-context')) {
76
+ const ctx = autoContext();
77
+ if (ctx) out['x-hrr-context'] = ctx;
78
+ }
65
79
  // EVERY forwarded request carries this wallet's context namespace — binds
66
80
  // and the chats that reference them must land in the same tenant.
67
81
  return withNamespace(out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.4",
3
+ "version": "0.50.6",
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",