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.
- package/lib/autobind.js +109 -0
- package/lib/launch.js +19 -0
- package/lib/proxy.js +14 -0
- package/package.json +1 -1
package/lib/autobind.js
ADDED
|
@@ -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.
|
|
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",
|