@bman654/clodex 2.1.2 → 2.1.4
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/dist/claude-wrapper.js +9 -0
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +13 -5
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +2 -0
- package/package.json +1 -1
package/dist/claude-wrapper.js
CHANGED
|
@@ -23,6 +23,14 @@ function isExecutableFile(path) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
function execIntoClaude(file, args, env) {
|
|
27
|
+
if (isWindows || typeof process.execve !== "function") return;
|
|
28
|
+
if (!isExecutableFile(file)) return;
|
|
29
|
+
try {
|
|
30
|
+
process.execve(file, [file, ...args], env);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
26
34
|
async function main() {
|
|
27
35
|
const argv = process.argv.slice(2);
|
|
28
36
|
const checkOnly = argv[0] === "--check";
|
|
@@ -53,6 +61,7 @@ async function main() {
|
|
|
53
61
|
process.exit(1);
|
|
54
62
|
}
|
|
55
63
|
const env = computeWrapperEnv(process.env, state);
|
|
64
|
+
execIntoClaude(claudePath, claudeArgs, env);
|
|
56
65
|
const child = spawn(claudePath, claudeArgs, {
|
|
57
66
|
stdio: "inherit",
|
|
58
67
|
env,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/claude-wrapper.ts"],"sourcesContent":["// src/claude-wrapper.ts — the `clodex-claude` bin.\n//\n// A tiny, fast exec-style wrapper around the Claude Code binary that injects\n// bridge env for a running standalone `clodex server` (discovered via\n// ~/.clodex/server-runtime.json). Two invocation shapes:\n//\n// 1. CLAUDE_CODE_PROCESS_WRAPPER contract: Claude Code invokes\n// `clodex-claude <claude-binary-path> <args...>` for every process it\n// spawns (agents view sessions, background agents). First arg is the\n// claude binary to exec.\n// 2. Direct terminal use: `clodex-claude [args...]` — the claude binary is\n// discovered the same way `clodex claude` discovers it\n// (CLODEX_CLAUDE_PATH override, config override, PATH, fallbacks).\n//\n// With a live proxy-mode server: HTTPS_PROXY/HTTP_PROXY + NODE_EXTRA_CA_CERTS\n// point at it and ANTHROPIC_BASE_URL is removed (claude keeps its own\n// Anthropic auth — this is the recommended mode). With a live endpoint-mode\n// server: ANTHROPIC_BASE_URL points at the gateway. With no live server the\n// env is passed through untouched, so claude always launches.\n//\n// This file must stay a thin shell over pure helpers (wrapper-env.ts,\n// server-runtime.ts) with minimal imports — it runs for every spawned agent.\n\nimport { spawn } from 'node:child_process';\nimport { accessSync, constants as fsConstants, statSync } from 'node:fs';\nimport { constants as osConstants } from 'node:os';\nimport { findClaudeBinary } from './launch.js';\nimport { waitForTcpListenerCandidate } from './listener-ready.js';\nimport {\n orderWrapperServerCandidates,\n readLiveServerRuntimeStates,\n type ServerRuntimeState,\n} from './server-runtime.js';\nimport { computeWrapperEnv, wrapperRequiresServer } from './wrapper-env.js';\n\nconst isWindows = process.platform === 'win32';\nconst WRAPPER_SERVER_READY_TIMEOUT_MS = 500;\n\nfunction isExecutableFile(path: string): boolean {\n try {\n if (!statSync(path).isFile()) return false;\n if (!isWindows) accessSync(path, fsConstants.X_OK);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const checkOnly = argv[0] === '--check';\n\n let claudePath: string | null = null;\n let claudeArgs: string[] = [];\n if (checkOnly) {\n // Readiness checks validate discovery and TCP state without launching Claude.\n } else if (argv[0] && isExecutableFile(argv[0])) {\n // CLAUDE_CODE_PROCESS_WRAPPER shape: first arg is the claude binary path.\n claudePath = argv[0];\n claudeArgs = argv.slice(1);\n } else {\n claudePath = findClaudeBinary();\n claudeArgs = argv;\n }\n\n if (!checkOnly && !claudePath) {\n process.stderr.write('clodex-claude: could not find the claude binary (set CLODEX_CLAUDE_PATH)\\n');\n process.exit(127);\n }\n\n // Selection policy (see orderWrapperServerCandidates): proxy-mode servers\n // are preferred over endpoint-mode ones — bridging keeps Claude Code's own\n // Anthropic auth — with newest startedAt breaking ties within a mode. A fast\n // probe round covers every candidate so an unreachable preferred record\n // cannot delay a reachable fallback. Timed-out probes retry under one shared\n // deadline; definitive connection errors fail immediately.\n const candidates = orderWrapperServerCandidates(readLiveServerRuntimeStates());\n const state: ServerRuntimeState | null = await waitForTcpListenerCandidate(\n '127.0.0.1',\n candidates,\n WRAPPER_SERVER_READY_TIMEOUT_MS,\n { retryFailure: result => result === 'timeout' },\n );\n if (checkOnly) process.exit(state ? 0 : 1);\n if (!state && wrapperRequiresServer(process.env)) {\n process.stderr.write('clodex-claude: no live clodex server is available\\n');\n process.exit(1);\n }\n const env = computeWrapperEnv(process.env, state);\n\n const child = spawn(claudePath!, claudeArgs, {\n stdio: 'inherit',\n env,\n shell: isWindows,\n });\n\n const forward = (signal: NodeJS.Signals) => child.kill(signal);\n process.once('SIGINT', () => forward('SIGINT'));\n process.once('SIGTERM', () => forward('SIGTERM'));\n\n child.on('error', err => {\n process.stderr.write(`clodex-claude: failed to launch ${claudePath}: ${err.message}\\n`);\n process.exit(127);\n });\n child.on('exit', (code, signal) => {\n if (signal) {\n const signum = osConstants.signals[signal as keyof typeof osConstants.signals];\n process.exit(signum ? 128 + signum : 1);\n }\n process.exit(code ?? 0);\n });\n}\n\nvoid main();\n"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"sources":["../src/claude-wrapper.ts"],"sourcesContent":["// src/claude-wrapper.ts — the `clodex-claude` bin.\n//\n// A tiny, fast exec-style wrapper around the Claude Code binary that injects\n// bridge env for a running standalone `clodex server` (discovered via\n// ~/.clodex/server-runtime.json). Two invocation shapes:\n//\n// 1. CLAUDE_CODE_PROCESS_WRAPPER contract: Claude Code invokes\n// `clodex-claude <claude-binary-path> <args...>` for every process it\n// spawns (agents view sessions, background agents). First arg is the\n// claude binary to exec.\n// 2. Direct terminal use: `clodex-claude [args...]` — the claude binary is\n// discovered the same way `clodex claude` discovers it\n// (CLODEX_CLAUDE_PATH override, config override, PATH, fallbacks).\n//\n// With a live proxy-mode server: HTTPS_PROXY/HTTP_PROXY + NODE_EXTRA_CA_CERTS\n// point at it and ANTHROPIC_BASE_URL is removed (claude keeps its own\n// Anthropic auth — this is the recommended mode). With a live endpoint-mode\n// server: ANTHROPIC_BASE_URL points at the gateway. With no live server the\n// env is passed through untouched, so claude always launches.\n//\n// The wrapper REPLACES its own process image with claude (execve) rather than\n// parenting it — see execIntoClaude below for why that distinction matters.\n//\n// This file must stay a thin shell over pure helpers (wrapper-env.ts,\n// server-runtime.ts) with minimal imports — it runs for every spawned agent.\n\nimport { spawn } from 'node:child_process';\nimport { accessSync, constants as fsConstants, statSync } from 'node:fs';\nimport { constants as osConstants } from 'node:os';\nimport { findClaudeBinary } from './launch.js';\nimport { waitForTcpListenerCandidate } from './listener-ready.js';\nimport {\n orderWrapperServerCandidates,\n readLiveServerRuntimeStates,\n type ServerRuntimeState,\n} from './server-runtime.js';\nimport { computeWrapperEnv, wrapperRequiresServer } from './wrapper-env.js';\n\nconst isWindows = process.platform === 'win32';\nconst WRAPPER_SERVER_READY_TIMEOUT_MS = 500;\n\nfunction isExecutableFile(path: string): boolean {\n try {\n if (!statSync(path).isFile()) return false;\n if (!isWindows) accessSync(path, fsConstants.X_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Replace this process with claude instead of parenting it. Returns when exec\n * is unavailable or declined, leaving the caller to spawn a child instead.\n *\n * Claude Code starts each background pty host with `detached: true` so the\n * process it spawns leads its own process group, then delivers terminal\n * resizes to that group with `process.kill(-process.pid, 'SIGWINCH')`. A\n * wrapper that spawns claude as a child takes the group-leader role for\n * itself: claude's pid no longer matches its group id, the signal fails with\n * ESRCH inside Claude Code's silent `catch {}`, and every background session\n * stays frozen at the startup size it was given on the command line (200x50)\n * however the terminal is later resized. Interactive sessions hid the bug\n * because there the kernel delivers SIGWINCH through the controlling terminal.\n *\n * Replacing the process image keeps the pid, process group, and inherited fds\n * exactly as Claude Code handed them out, so it cannot tell this wrapper apart\n * from launching claude directly. That also makes the signal forwarding and\n * exit-code mapping below unnecessary on this path.\n *\n * `process.execve` is POSIX-only and landed in Node 22.15. Windows and the\n * older 22.x releases still permitted by `engines.node` fall back to spawning,\n * which behaves correctly apart from background pty resizes.\n *\n * A failed exec cannot fall back to spawning: on syscall failure `execve`\n * aborts with a native crash dump (exit 134) rather than throwing. So re-check\n * the binary immediately before the call — `main` has awaited up to 500ms of\n * server probing since it first looked, and Claude Code replaces its own\n * binary when it self-updates — and let a vanished or unreadable file take the\n * spawn path, which still reports it as a one-line error and exit 127. Only\n * argument and platform validation, which run before the syscall, are\n * catchable; claude has not been launched when they throw.\n */\nfunction execIntoClaude(file: string, args: string[], env: NodeJS.ProcessEnv): void {\n if (isWindows || typeof process.execve !== 'function') return;\n if (!isExecutableFile(file)) return;\n\n try {\n process.execve(file, [file, ...args], env);\n } catch {\n // Rejected before the syscall — leave claude to the spawn path below.\n }\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const checkOnly = argv[0] === '--check';\n\n let claudePath: string | null = null;\n let claudeArgs: string[] = [];\n if (checkOnly) {\n // Readiness checks validate discovery and TCP state without launching Claude.\n } else if (argv[0] && isExecutableFile(argv[0])) {\n // CLAUDE_CODE_PROCESS_WRAPPER shape: first arg is the claude binary path.\n claudePath = argv[0];\n claudeArgs = argv.slice(1);\n } else {\n claudePath = findClaudeBinary();\n claudeArgs = argv;\n }\n\n if (!checkOnly && !claudePath) {\n process.stderr.write('clodex-claude: could not find the claude binary (set CLODEX_CLAUDE_PATH)\\n');\n process.exit(127);\n }\n\n // Selection policy (see orderWrapperServerCandidates): proxy-mode servers\n // are preferred over endpoint-mode ones — bridging keeps Claude Code's own\n // Anthropic auth — with newest startedAt breaking ties within a mode. A fast\n // probe round covers every candidate so an unreachable preferred record\n // cannot delay a reachable fallback. Timed-out probes retry under one shared\n // deadline; definitive connection errors fail immediately.\n const candidates = orderWrapperServerCandidates(readLiveServerRuntimeStates());\n const state: ServerRuntimeState | null = await waitForTcpListenerCandidate(\n '127.0.0.1',\n candidates,\n WRAPPER_SERVER_READY_TIMEOUT_MS,\n { retryFailure: result => result === 'timeout' },\n );\n if (checkOnly) process.exit(state ? 0 : 1);\n if (!state && wrapperRequiresServer(process.env)) {\n process.stderr.write('clodex-claude: no live clodex server is available\\n');\n process.exit(1);\n }\n const env = computeWrapperEnv(process.env, state);\n\n execIntoClaude(claudePath!, claudeArgs, env);\n\n // Only reached when exec is unavailable or failed.\n const child = spawn(claudePath!, claudeArgs, {\n stdio: 'inherit',\n env,\n shell: isWindows,\n });\n\n const forward = (signal: NodeJS.Signals) => child.kill(signal);\n process.once('SIGINT', () => forward('SIGINT'));\n process.once('SIGTERM', () => forward('SIGTERM'));\n\n child.on('error', err => {\n process.stderr.write(`clodex-claude: failed to launch ${claudePath}: ${err.message}\\n`);\n process.exit(127);\n });\n child.on('exit', (code, signal) => {\n if (signal) {\n const signum = osConstants.signals[signal as keyof typeof osConstants.signals];\n process.exit(signum ? 128 + signum : 1);\n }\n process.exit(code ?? 0);\n });\n}\n\nvoid main();\n"],"mappings":";;;;;;;;;;;AA0BA,SAAS,aAAa;AACtB,SAAS,YAAY,aAAa,aAAa,gBAAgB;AAC/D,SAAS,aAAa,mBAAmB;AAUzC,IAAM,YAAY,QAAQ,aAAa;AACvC,IAAM,kCAAkC;AAExC,SAAS,iBAAiB,MAAuB;AAC/C,MAAI;AACF,QAAI,CAAC,SAAS,IAAI,EAAE,OAAO,EAAG,QAAO;AACrC,QAAI,CAAC,UAAW,YAAW,MAAM,YAAY,IAAI;AACjD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAkCA,SAAS,eAAe,MAAc,MAAgB,KAA8B;AAClF,MAAI,aAAa,OAAO,QAAQ,WAAW,WAAY;AACvD,MAAI,CAAC,iBAAiB,IAAI,EAAG;AAE7B,MAAI;AACF,YAAQ,OAAO,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG,GAAG;AAAA,EAC3C,QAAQ;AAAA,EAER;AACF;AAEA,eAAe,OAAsB;AACnC,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,QAAM,YAAY,KAAK,CAAC,MAAM;AAE9B,MAAI,aAA4B;AAChC,MAAI,aAAuB,CAAC;AAC5B,MAAI,WAAW;AAAA,EAEf,WAAW,KAAK,CAAC,KAAK,iBAAiB,KAAK,CAAC,CAAC,GAAG;AAE/C,iBAAa,KAAK,CAAC;AACnB,iBAAa,KAAK,MAAM,CAAC;AAAA,EAC3B,OAAO;AACL,iBAAa,iBAAiB;AAC9B,iBAAa;AAAA,EACf;AAEA,MAAI,CAAC,aAAa,CAAC,YAAY;AAC7B,YAAQ,OAAO,MAAM,4EAA4E;AACjG,YAAQ,KAAK,GAAG;AAAA,EAClB;AAQA,QAAM,aAAa,6BAA6B,4BAA4B,CAAC;AAC7E,QAAM,QAAmC,MAAM;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA,EAAE,cAAc,YAAU,WAAW,UAAU;AAAA,EACjD;AACA,MAAI,UAAW,SAAQ,KAAK,QAAQ,IAAI,CAAC;AACzC,MAAI,CAAC,SAAS,sBAAsB,QAAQ,GAAG,GAAG;AAChD,YAAQ,OAAO,MAAM,qDAAqD;AAC1E,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,MAAM,kBAAkB,QAAQ,KAAK,KAAK;AAEhD,iBAAe,YAAa,YAAY,GAAG;AAG3C,QAAM,QAAQ,MAAM,YAAa,YAAY;AAAA,IAC3C,OAAO;AAAA,IACP;AAAA,IACA,OAAO;AAAA,EACT,CAAC;AAED,QAAM,UAAU,CAAC,WAA2B,MAAM,KAAK,MAAM;AAC7D,UAAQ,KAAK,UAAU,MAAM,QAAQ,QAAQ,CAAC;AAC9C,UAAQ,KAAK,WAAW,MAAM,QAAQ,SAAS,CAAC;AAEhD,QAAM,GAAG,SAAS,SAAO;AACvB,YAAQ,OAAO,MAAM,mCAAmC,UAAU,KAAK,IAAI,OAAO;AAAA,CAAI;AACtF,YAAQ,KAAK,GAAG;AAAA,EAClB,CAAC;AACD,QAAM,GAAG,QAAQ,CAAC,MAAM,WAAW;AACjC,QAAI,QAAQ;AACV,YAAM,SAAS,YAAY,QAAQ,MAA0C;AAC7E,cAAQ,KAAK,SAAS,MAAM,SAAS,CAAC;AAAA,IACxC;AACA,YAAQ,KAAK,QAAQ,CAAC;AAAA,EACxB,CAAC;AACH;AAEA,KAAK,KAAK;","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -216,7 +216,7 @@ import { join } from "path";
|
|
|
216
216
|
// package.json
|
|
217
217
|
var package_default = {
|
|
218
218
|
name: "@bman654/clodex",
|
|
219
|
-
version: "2.1.
|
|
219
|
+
version: "2.1.4",
|
|
220
220
|
publishConfig: {
|
|
221
221
|
access: "public"
|
|
222
222
|
},
|
|
@@ -9739,7 +9739,7 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
|
|
|
9739
9739
|
let openToolId = null;
|
|
9740
9740
|
let finishReason = "end_turn";
|
|
9741
9741
|
let usage = {
|
|
9742
|
-
input_tokens: 0,
|
|
9742
|
+
input_tokens: observer?.initialInputTokens ?? 0,
|
|
9743
9743
|
output_tokens: 0,
|
|
9744
9744
|
cache_creation_input_tokens: 0,
|
|
9745
9745
|
cache_read_input_tokens: 0
|
|
@@ -9758,7 +9758,7 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
|
|
|
9758
9758
|
stop_reason: null,
|
|
9759
9759
|
stop_sequence: null,
|
|
9760
9760
|
usage: {
|
|
9761
|
-
input_tokens:
|
|
9761
|
+
input_tokens: 0,
|
|
9762
9762
|
output_tokens: 0,
|
|
9763
9763
|
cache_creation_input_tokens: 0,
|
|
9764
9764
|
cache_read_input_tokens: 0
|
|
@@ -9895,7 +9895,9 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
|
|
|
9895
9895
|
}
|
|
9896
9896
|
case "finish":
|
|
9897
9897
|
if (part.totalUsage) {
|
|
9898
|
-
|
|
9898
|
+
const finalUsage = toAnthropicUsage(part.totalUsage);
|
|
9899
|
+
const hasFinalInputUsage = finalUsage.input_tokens + finalUsage.cache_creation_input_tokens + finalUsage.cache_read_input_tokens > 0;
|
|
9900
|
+
usage = hasFinalInputUsage ? finalUsage : { ...usage, output_tokens: finalUsage.output_tokens };
|
|
9899
9901
|
}
|
|
9900
9902
|
if (part.finishReason === "tool-calls") finishReason = "tool_use";
|
|
9901
9903
|
else if (part.finishReason === "length") finishReason = "max_tokens";
|
|
@@ -10125,6 +10127,7 @@ function anthropicPromptTooLongMessage(body, contextWindow) {
|
|
|
10125
10127
|
// src/proxy.ts
|
|
10126
10128
|
var STREAM_KEEPALIVE_INTERVAL_MS = 2e4;
|
|
10127
10129
|
var STREAM_KEEPALIVE_PING = 'event: ping\ndata: {"type":"ping"}\n\n';
|
|
10130
|
+
var INTERNAL_ADAPTER_KEEPALIVE_TIMEOUT_MS = 6e4;
|
|
10128
10131
|
function createTranslationLifecycle(logPath, requestId, claudeSessionId, modelId, provider) {
|
|
10129
10132
|
if (!logPath || !requestId) return void 0;
|
|
10130
10133
|
const startedAt = Date.now();
|
|
@@ -10656,6 +10659,7 @@ data: ${JSON.stringify({
|
|
|
10656
10659
|
}
|
|
10657
10660
|
anthropicError(res, 404, `Unknown endpoint: ${req.method} ${req.url}`);
|
|
10658
10661
|
});
|
|
10662
|
+
server.keepAliveTimeout = INTERNAL_ADAPTER_KEEPALIVE_TIMEOUT_MS;
|
|
10659
10663
|
let address;
|
|
10660
10664
|
try {
|
|
10661
10665
|
address = await listenTcpServer(server, 0, "127.0.0.1");
|
|
@@ -12152,7 +12156,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
|
|
|
12152
12156
|
upstream.end(rawBody);
|
|
12153
12157
|
});
|
|
12154
12158
|
}
|
|
12155
|
-
function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http.request, lifecycle, isLocalShutdown = () => false) {
|
|
12159
|
+
function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http.request, adapterAgent, lifecycle, isLocalShutdown = () => false) {
|
|
12156
12160
|
return new Promise((resolve3) => {
|
|
12157
12161
|
const startedAt = Date.now();
|
|
12158
12162
|
let lastActivityAt = startedAt;
|
|
@@ -12260,6 +12264,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http.requ
|
|
|
12260
12264
|
port: adapter.port,
|
|
12261
12265
|
method: "POST",
|
|
12262
12266
|
path: req.url,
|
|
12267
|
+
agent: adapterAgent,
|
|
12263
12268
|
headers: {
|
|
12264
12269
|
"Content-Type": "application/json",
|
|
12265
12270
|
"Content-Length": String(rawBody.length),
|
|
@@ -12397,6 +12402,7 @@ async function startHttpProxy(options) {
|
|
|
12397
12402
|
options.modelAliases
|
|
12398
12403
|
);
|
|
12399
12404
|
}
|
|
12405
|
+
const adapterAgent = adapter ? new http.Agent({ keepAlive: true }) : void 0;
|
|
12400
12406
|
let shuttingDown = false;
|
|
12401
12407
|
const mitmServer = https.createServer({
|
|
12402
12408
|
key: certificates.serverKey,
|
|
@@ -12490,6 +12496,7 @@ async function startHttpProxy(options) {
|
|
|
12490
12496
|
adapterBody,
|
|
12491
12497
|
adapter,
|
|
12492
12498
|
options.adapterRequest,
|
|
12499
|
+
adapterAgent,
|
|
12493
12500
|
messagesEndpoint === "messages" && options.inferenceLogPath ? {
|
|
12494
12501
|
logPath: options.inferenceLogPath,
|
|
12495
12502
|
requestId,
|
|
@@ -12597,6 +12604,7 @@ async function startHttpProxy(options) {
|
|
|
12597
12604
|
options.host ?? "127.0.0.1"
|
|
12598
12605
|
);
|
|
12599
12606
|
} catch (err) {
|
|
12607
|
+
adapterAgent?.destroy();
|
|
12600
12608
|
adapter?.close();
|
|
12601
12609
|
throw err;
|
|
12602
12610
|
}
|