@bman654/clodex 2.13.0 → 2.14.0
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/README.md +6 -2
- package/dist/{chunk-ZM2KYLPH.js → chunk-3NCDMS7N.js} +468 -2
- package/dist/chunk-3NCDMS7N.js.map +1 -0
- package/dist/claude-wrapper.js +276 -7
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +555 -488
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +35 -0
- package/docs/windows-setup.md +203 -168
- package/launcher/clodex-claude-launcher.cs +311 -0
- package/package.json +2 -1
- package/dist/chunk-ZM2KYLPH.js.map +0 -1
package/dist/claude-wrapper.js
CHANGED
|
@@ -2,28 +2,286 @@
|
|
|
2
2
|
import {
|
|
3
3
|
OAUTH_ACCOUNT_ENV,
|
|
4
4
|
computeWrapperEnv,
|
|
5
|
+
emitParentNotice,
|
|
5
6
|
findClaudeBinary,
|
|
7
|
+
installParentNoticeSink,
|
|
6
8
|
orderWrapperServerCandidates,
|
|
7
9
|
readLiveServerRuntimeStates,
|
|
10
|
+
readWrapperPatchManifest,
|
|
11
|
+
sha256File,
|
|
8
12
|
waitForTcpListenerCandidate,
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
wrapperInvocationIsChat,
|
|
14
|
+
wrapperIsTopLevelVsCodeHost,
|
|
15
|
+
wrapperRequiresServer,
|
|
16
|
+
wrapperSpawnShell,
|
|
17
|
+
wrapperSubstitutionEligible,
|
|
18
|
+
writeParentNoticeLinesSync
|
|
19
|
+
} from "./chunk-3NCDMS7N.js";
|
|
11
20
|
|
|
12
21
|
// src/claude-wrapper.ts
|
|
13
22
|
import { spawn } from "child_process";
|
|
14
|
-
import { accessSync, constants as
|
|
23
|
+
import { accessSync as accessSync2, constants as fsConstants2, statSync as statSync2 } from "fs";
|
|
15
24
|
import { constants as osConstants } from "os";
|
|
25
|
+
import { isAbsolute } from "path";
|
|
26
|
+
|
|
27
|
+
// src/wrapper-target.ts
|
|
28
|
+
import { accessSync, constants as fsConstants, statSync } from "fs";
|
|
29
|
+
var WINDOWS_EXECUTABLE = /\.exe$/i;
|
|
30
|
+
function requireWrapperExecutable(path, platform = process.platform) {
|
|
31
|
+
if (platform === "win32") {
|
|
32
|
+
if (!WINDOWS_EXECUTABLE.test(path)) throw new Error("not a Windows executable");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
accessSync(path, fsConstants.X_OK);
|
|
36
|
+
}
|
|
37
|
+
var defaultWrapperTargetFileOps = {
|
|
38
|
+
stat: (path) => statSync(path, { bigint: true }),
|
|
39
|
+
requireExecutable: (path) => requireWrapperExecutable(path),
|
|
40
|
+
sha256: sha256File
|
|
41
|
+
};
|
|
42
|
+
function identityOf(path, fileOps) {
|
|
43
|
+
const stat = fileOps.stat(path);
|
|
44
|
+
if (!stat.isFile()) throw new Error("not a file");
|
|
45
|
+
fileOps.requireExecutable(path);
|
|
46
|
+
return {
|
|
47
|
+
dev: stat.dev,
|
|
48
|
+
ino: stat.ino,
|
|
49
|
+
size: stat.size,
|
|
50
|
+
mode: stat.mode,
|
|
51
|
+
mtimeNs: stat.mtimeNs,
|
|
52
|
+
ctimeNs: stat.ctimeNs
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function sameFile(left, right) {
|
|
56
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
57
|
+
}
|
|
58
|
+
function sameIdentity(left, right) {
|
|
59
|
+
return sameFile(left, right) && left.size === right.size && left.mode === right.mode && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
60
|
+
}
|
|
61
|
+
function fallbackNotice(handedInPath, candidatePath, manifestPath, claudeVersion, detail) {
|
|
62
|
+
const version = claudeVersion ? ` for patched Claude ${claudeVersion}` : "";
|
|
63
|
+
return `clodex-claude: running ${JSON.stringify(handedInPath)} because ${detail}${version}; the patched install ${JSON.stringify(candidatePath)} was not selected, so clodex model-picker entries may be absent; manifest=${JSON.stringify(manifestPath)}; align the VS Code and Claude Code builds, then run \`clodex patch\` again.`;
|
|
64
|
+
}
|
|
65
|
+
function fallback(handedInPath, candidatePath, manifestPath, claudeVersion, reason, detail) {
|
|
66
|
+
return {
|
|
67
|
+
path: handedInPath,
|
|
68
|
+
reason,
|
|
69
|
+
notice: fallbackNotice(handedInPath, candidatePath, manifestPath, claudeVersion, detail)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function prepareWrapperTarget(handedInPath, options = {}) {
|
|
73
|
+
const manifestRead = readWrapperPatchManifest(options.manifestPath);
|
|
74
|
+
if (manifestRead.status === "missing") {
|
|
75
|
+
return {
|
|
76
|
+
kind: "decided",
|
|
77
|
+
decision: { path: handedInPath, reason: "manifest-missing" }
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (manifestRead.status === "invalid") {
|
|
81
|
+
const candidatePath2 = manifestRead.binaryPath;
|
|
82
|
+
if (!candidatePath2 || candidatePath2 === handedInPath || !manifestRead.claudeVersion) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "decided",
|
|
85
|
+
decision: { path: handedInPath, reason: "manifest-invalid" }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
kind: "decided",
|
|
90
|
+
decision: fallback(
|
|
91
|
+
handedInPath,
|
|
92
|
+
candidatePath2,
|
|
93
|
+
manifestRead.path,
|
|
94
|
+
manifestRead.claudeVersion,
|
|
95
|
+
"manifest-invalid",
|
|
96
|
+
"the patch manifest does not contain every fingerprint required for safe substitution"
|
|
97
|
+
)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const { manifest } = manifestRead;
|
|
101
|
+
const candidatePath = manifest.binaryPath;
|
|
102
|
+
const fileOps = options.fileOps ?? defaultWrapperTargetFileOps;
|
|
103
|
+
let handedInIdentity;
|
|
104
|
+
let candidateIdentity;
|
|
105
|
+
try {
|
|
106
|
+
handedInIdentity = identityOf(handedInPath, fileOps);
|
|
107
|
+
} catch {
|
|
108
|
+
return {
|
|
109
|
+
kind: "decided",
|
|
110
|
+
decision: fallback(
|
|
111
|
+
handedInPath,
|
|
112
|
+
candidatePath,
|
|
113
|
+
manifestRead.path,
|
|
114
|
+
manifest.claudeVersion,
|
|
115
|
+
"handed-in-inspection-failed",
|
|
116
|
+
"VS Code's handed-in executable could not be inspected"
|
|
117
|
+
)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
candidateIdentity = identityOf(candidatePath, fileOps);
|
|
122
|
+
} catch {
|
|
123
|
+
return {
|
|
124
|
+
kind: "decided",
|
|
125
|
+
decision: fallback(
|
|
126
|
+
handedInPath,
|
|
127
|
+
candidatePath,
|
|
128
|
+
manifestRead.path,
|
|
129
|
+
manifest.claudeVersion,
|
|
130
|
+
"patched-install-unavailable",
|
|
131
|
+
"the recorded patched executable is missing, unreadable, or not executable"
|
|
132
|
+
)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (candidatePath === handedInPath || sameFile(handedInIdentity, candidateIdentity)) {
|
|
136
|
+
return {
|
|
137
|
+
kind: "decided",
|
|
138
|
+
decision: { path: handedInPath, reason: "manifest-target-is-handed-in" }
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (candidateIdentity.size !== BigInt(manifest.patchedSize)) {
|
|
142
|
+
return {
|
|
143
|
+
kind: "decided",
|
|
144
|
+
decision: fallback(
|
|
145
|
+
handedInPath,
|
|
146
|
+
candidatePath,
|
|
147
|
+
manifestRead.path,
|
|
148
|
+
manifest.claudeVersion,
|
|
149
|
+
"patched-install-size-changed",
|
|
150
|
+
"the recorded patched executable has changed size since it was patched"
|
|
151
|
+
)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
let handedInSha256;
|
|
155
|
+
try {
|
|
156
|
+
handedInSha256 = fileOps.sha256(handedInPath);
|
|
157
|
+
const afterHash = identityOf(handedInPath, fileOps);
|
|
158
|
+
if (!sameIdentity(handedInIdentity, afterHash)) {
|
|
159
|
+
return {
|
|
160
|
+
kind: "decided",
|
|
161
|
+
decision: fallback(
|
|
162
|
+
handedInPath,
|
|
163
|
+
candidatePath,
|
|
164
|
+
manifestRead.path,
|
|
165
|
+
manifest.claudeVersion,
|
|
166
|
+
"input-changed-before-handoff",
|
|
167
|
+
"VS Code's handed-in executable changed while its identity was being checked"
|
|
168
|
+
)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
handedInIdentity = afterHash;
|
|
172
|
+
} catch {
|
|
173
|
+
return {
|
|
174
|
+
kind: "decided",
|
|
175
|
+
decision: fallback(
|
|
176
|
+
handedInPath,
|
|
177
|
+
candidatePath,
|
|
178
|
+
manifestRead.path,
|
|
179
|
+
manifest.claudeVersion,
|
|
180
|
+
"handed-in-inspection-failed",
|
|
181
|
+
"VS Code's handed-in executable could not be hashed"
|
|
182
|
+
)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (handedInSha256 === manifest.patchedSha256) {
|
|
186
|
+
return {
|
|
187
|
+
kind: "decided",
|
|
188
|
+
decision: { path: handedInPath, reason: "handed-in-already-patched" }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (handedInSha256 !== manifest.pristineSha256) {
|
|
192
|
+
return {
|
|
193
|
+
kind: "decided",
|
|
194
|
+
decision: fallback(
|
|
195
|
+
handedInPath,
|
|
196
|
+
candidatePath,
|
|
197
|
+
manifestRead.path,
|
|
198
|
+
manifest.claudeVersion,
|
|
199
|
+
"handed-in-not-recorded-pristine",
|
|
200
|
+
"VS Code's handed-in bytes do not match the pristine source recorded by clodex"
|
|
201
|
+
)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
kind: "candidate",
|
|
206
|
+
handedInPath,
|
|
207
|
+
candidatePath,
|
|
208
|
+
manifestPath: manifestRead.path,
|
|
209
|
+
manifest,
|
|
210
|
+
handedInIdentity,
|
|
211
|
+
candidateIdentity,
|
|
212
|
+
fileOps
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function finalizeWrapperTarget(prepared) {
|
|
216
|
+
if (prepared.kind === "decided") return prepared.decision;
|
|
217
|
+
const {
|
|
218
|
+
handedInPath,
|
|
219
|
+
candidatePath,
|
|
220
|
+
manifestPath,
|
|
221
|
+
manifest,
|
|
222
|
+
handedInIdentity,
|
|
223
|
+
candidateIdentity,
|
|
224
|
+
fileOps
|
|
225
|
+
} = prepared;
|
|
226
|
+
const changed = () => fallback(
|
|
227
|
+
handedInPath,
|
|
228
|
+
candidatePath,
|
|
229
|
+
manifestPath,
|
|
230
|
+
manifest.claudeVersion,
|
|
231
|
+
"input-changed-before-handoff",
|
|
232
|
+
"one of the executables changed between inspection and launch"
|
|
233
|
+
);
|
|
234
|
+
try {
|
|
235
|
+
if (!sameIdentity(handedInIdentity, identityOf(handedInPath, fileOps))) return changed();
|
|
236
|
+
if (!sameIdentity(candidateIdentity, identityOf(candidatePath, fileOps))) return changed();
|
|
237
|
+
const candidateSha256 = fileOps.sha256(candidatePath);
|
|
238
|
+
if (!sameIdentity(candidateIdentity, identityOf(candidatePath, fileOps))) return changed();
|
|
239
|
+
if (!sameIdentity(handedInIdentity, identityOf(handedInPath, fileOps))) return changed();
|
|
240
|
+
if (candidateSha256 !== manifest.patchedSha256) {
|
|
241
|
+
return fallback(
|
|
242
|
+
handedInPath,
|
|
243
|
+
candidatePath,
|
|
244
|
+
manifestPath,
|
|
245
|
+
manifest.claudeVersion,
|
|
246
|
+
"patched-install-verification-failed",
|
|
247
|
+
"the recorded patched executable no longer has the SHA-256 clodex published"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
return fallback(
|
|
252
|
+
handedInPath,
|
|
253
|
+
candidatePath,
|
|
254
|
+
manifestPath,
|
|
255
|
+
manifest.claudeVersion,
|
|
256
|
+
"patched-install-verification-failed",
|
|
257
|
+
"one of the executables could not be verified at launch"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return { path: candidatePath, reason: "verified-patched-install" };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/claude-wrapper.ts
|
|
16
264
|
var isWindows = process.platform === "win32";
|
|
17
265
|
var WRAPPER_SERVER_READY_TIMEOUT_MS = 500;
|
|
18
266
|
function isExecutableFile(path) {
|
|
19
267
|
try {
|
|
20
|
-
if (!
|
|
21
|
-
if (!isWindows)
|
|
268
|
+
if (!statSync2(path).isFile()) return false;
|
|
269
|
+
if (!isWindows) accessSync2(path, fsConstants2.X_OK);
|
|
22
270
|
return true;
|
|
23
271
|
} catch {
|
|
24
272
|
return false;
|
|
25
273
|
}
|
|
26
274
|
}
|
|
275
|
+
function emitPreExecNotice(message) {
|
|
276
|
+
const lines = [];
|
|
277
|
+
const release = installParentNoticeSink((line) => lines.push(line));
|
|
278
|
+
try {
|
|
279
|
+
emitParentNotice(message);
|
|
280
|
+
} finally {
|
|
281
|
+
release();
|
|
282
|
+
}
|
|
283
|
+
writeParentNoticeLinesSync(lines);
|
|
284
|
+
}
|
|
27
285
|
function activeProviderCredentialOverride(env) {
|
|
28
286
|
const variablePattern = isWindows ? /^CLODEX_KEY_[A-Z0-9_]+$/i : /^CLODEX_KEY_[A-Z0-9_]+$/;
|
|
29
287
|
return Object.keys(env).sort().find((name) => variablePattern.test(name) && Boolean(env[name]?.trim())) ?? null;
|
|
@@ -41,9 +299,13 @@ async function main() {
|
|
|
41
299
|
const checkOnly = argv[0] === "--check";
|
|
42
300
|
let claudePath = null;
|
|
43
301
|
let claudeArgs = [];
|
|
302
|
+
let handedInClaudePath = null;
|
|
303
|
+
const firstArgIsExecutable = Boolean(argv[0] && isExecutableFile(argv[0]));
|
|
304
|
+
const vscodeExplicitPath = wrapperIsTopLevelVsCodeHost(process.env) && Boolean(argv[0] && isAbsolute(argv[0]));
|
|
44
305
|
if (checkOnly) {
|
|
45
|
-
} else if (argv[0] &&
|
|
306
|
+
} else if (argv[0] && (firstArgIsExecutable || vscodeExplicitPath)) {
|
|
46
307
|
claudePath = argv[0];
|
|
308
|
+
handedInClaudePath = firstArgIsExecutable ? argv[0] : null;
|
|
47
309
|
claudeArgs = argv.slice(1);
|
|
48
310
|
} else {
|
|
49
311
|
claudePath = findClaudeBinary();
|
|
@@ -79,11 +341,18 @@ async function main() {
|
|
|
79
341
|
);
|
|
80
342
|
}
|
|
81
343
|
const env = computeWrapperEnv(process.env, state);
|
|
344
|
+
if (handedInClaudePath && wrapperSubstitutionEligible(process.env, state)) {
|
|
345
|
+
const target = finalizeWrapperTarget(prepareWrapperTarget(handedInClaudePath));
|
|
346
|
+
claudePath = target.path;
|
|
347
|
+
if (target.notice && wrapperInvocationIsChat(claudeArgs)) {
|
|
348
|
+
emitPreExecNotice(target.notice);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
82
351
|
execIntoClaude(claudePath, claudeArgs, env);
|
|
83
352
|
const child = spawn(claudePath, claudeArgs, {
|
|
84
353
|
stdio: "inherit",
|
|
85
354
|
env,
|
|
86
|
-
shell:
|
|
355
|
+
shell: wrapperSpawnShell(process.platform, claudePath)
|
|
87
356
|
});
|
|
88
357
|
const forward = (signal) => child.kill(signal);
|
|
89
358
|
process.once("SIGINT", () => forward("SIGINT"));
|
|
@@ -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// 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 { OAUTH_ACCOUNT_ENV } from './oauth-account-selection.js';\nimport { findClaudeBinary } from './claude-binary.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\nfunction activeProviderCredentialOverride(env: NodeJS.ProcessEnv): string | null {\n const variablePattern = isWindows\n ? /^CLODEX_KEY_[A-Z0-9_]+$/i\n : /^CLODEX_KEY_[A-Z0-9_]+$/;\n return Object.keys(env)\n .sort()\n .find(name => variablePattern.test(name) && Boolean(env[name]?.trim()))\n ?? null;\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 if (state && process.env[OAUTH_ACCOUNT_ENV]?.trim()) {\n process.stderr.write(\n `clodex-claude: warning: ${OAUTH_ACCOUNT_ENV} is ignored by an already-running standalone server; `\n + 'restart that server with the override and then launch without the variable, '\n + 'or use clodex claude --proxy\\n',\n );\n }\n const providerCredentialOverride = state\n ? activeProviderCredentialOverride(process.env)\n : null;\n if (state && providerCredentialOverride) {\n process.stderr.write(\n `clodex-claude: warning: ${providerCredentialOverride} is ignored by an already-running standalone server; `\n + 'unset the process-only key to use the configured credential/catalog, or save that credential as a '\n + 'provider or account, refresh its models, and restart the server\\n',\n );\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":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/claude-wrapper.ts","../src/wrapper-target.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 { isAbsolute } from 'node:path';\nimport { OAUTH_ACCOUNT_ENV } from './oauth-account-selection.js';\nimport { findClaudeBinary } from './claude-binary.js';\nimport { waitForTcpListenerCandidate } from './listener-ready.js';\nimport {\n emitParentNotice,\n installParentNoticeSink,\n writeParentNoticeLinesSync,\n} from './parent-notice.js';\nimport {\n orderWrapperServerCandidates,\n readLiveServerRuntimeStates,\n type ServerRuntimeState,\n} from './server-runtime.js';\nimport {\n computeWrapperEnv,\n wrapperInvocationIsChat,\n wrapperIsTopLevelVsCodeHost,\n wrapperRequiresServer,\n wrapperSpawnShell,\n wrapperSubstitutionEligible,\n} from './wrapper-env.js';\nimport { finalizeWrapperTarget, prepareWrapperTarget } from './wrapper-target.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/** Emit one normalized notice synchronously before exec takes ownership of stderr. */\nfunction emitPreExecNotice(message: string): void {\n const lines: string[] = [];\n const release = installParentNoticeSink(line => lines.push(line));\n try {\n emitParentNotice(message);\n } finally {\n release();\n }\n writeParentNoticeLinesSync(lines);\n}\n\nfunction activeProviderCredentialOverride(env: NodeJS.ProcessEnv): string | null {\n const variablePattern = isWindows\n ? /^CLODEX_KEY_[A-Z0-9_]+$/i\n : /^CLODEX_KEY_[A-Z0-9_]+$/;\n return Object.keys(env)\n .sort()\n .find(name => variablePattern.test(name) && Boolean(env[name]?.trim()))\n ?? null;\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 let handedInClaudePath: string | null = null;\n const firstArgIsExecutable = Boolean(argv[0] && isExecutableFile(argv[0]));\n const vscodeExplicitPath = wrapperIsTopLevelVsCodeHost(process.env)\n && Boolean(argv[0] && isAbsolute(argv[0]));\n if (checkOnly) {\n // Readiness checks validate discovery and TCP state without launching Claude.\n } else if (argv[0] && (firstArgIsExecutable || vscodeExplicitPath)) {\n // CLAUDE_CODE_PROCESS_WRAPPER shape: first arg is the claude binary path.\n // VS Code's path stays authoritative even if an update removed it before\n // this stat; silently discovering another install would run the wrong file.\n claudePath = argv[0];\n handedInClaudePath = firstArgIsExecutable ? argv[0] : null;\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 if (state && process.env[OAUTH_ACCOUNT_ENV]?.trim()) {\n process.stderr.write(\n `clodex-claude: warning: ${OAUTH_ACCOUNT_ENV} is ignored by an already-running standalone server; `\n + 'restart that server with the override and then launch without the variable, '\n + 'or use clodex claude --proxy\\n',\n );\n }\n const providerCredentialOverride = state\n ? activeProviderCredentialOverride(process.env)\n : null;\n if (state && providerCredentialOverride) {\n process.stderr.write(\n `clodex-claude: warning: ${providerCredentialOverride} is ignored by an already-running standalone server; `\n + 'unset the process-only key to use the configured credential/catalog, or save that credential as a '\n + 'provider or account, refresh its models, and restart the server\\n',\n );\n }\n const env = computeWrapperEnv(process.env, state);\n\n // The VS Code extension hands its bundled executable to the configured\n // process wrapper. In proxy mode only, replace a known-pristine copy with the\n // exact patched output recorded for those bytes. Every refusal keeps the\n // explicit path authoritative; discovery must never pick a different binary.\n // On Windows this runs under the native launcher and reaches the spawn\n // fallback below, which starts a `.exe` directly — the selector accepts\n // nothing else there.\n if (\n handedInClaudePath\n && wrapperSubstitutionEligible(process.env, state)\n ) {\n const target = finalizeWrapperTarget(prepareWrapperTarget(handedInClaudePath));\n claudePath = target.path;\n if (target.notice && wrapperInvocationIsChat(claudeArgs)) {\n emitPreExecNotice(target.notice);\n }\n }\n\n execIntoClaude(claudePath!, claudeArgs, env);\n\n // Only reached when exec is unavailable or failed. A shell is used only for\n // Windows .cmd/.bat launchers, which Node cannot spawn directly; a native\n // .exe is spawned directly so its arguments never cross cmd.exe.\n const child = spawn(claudePath!, claudeArgs, {\n stdio: 'inherit',\n env,\n shell: wrapperSpawnShell(process.platform, claudePath!),\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","import { accessSync, constants as fsConstants, statSync, type BigIntStats } from 'node:fs';\nimport { readWrapperPatchManifest, type WrapperPatchManifest } from './patch-manifest.js';\nimport { sha256File } from './patch-backup.js';\n\nexport type WrapperTargetReason =\n | 'verified-patched-install'\n | 'manifest-missing'\n | 'manifest-invalid'\n | 'manifest-target-is-handed-in'\n | 'handed-in-already-patched'\n | 'handed-in-not-recorded-pristine'\n | 'handed-in-inspection-failed'\n | 'patched-install-unavailable'\n | 'patched-install-size-changed'\n | 'patched-install-verification-failed'\n | 'input-changed-before-handoff';\n\nexport interface WrapperTargetDecision {\n path: string;\n reason: WrapperTargetReason;\n notice?: string;\n}\n\n/**\n * Everything `stat` reports that a rewrite of the file can disturb, held as bigints so nothing is\n * rounded. NTFS matters here: Node reports `ino` as the 64-bit file reference number (a 16-bit\n * sequence number above a 48-bit record index), which a double cannot hold exactly once the\n * sequence number reaches 32, and `ctime` as the NTFS ChangeTime — it moves on every write and on\n * `SetFileTime`, so an in-place rewrite that restores `mtime` still shows here. Times are\n * compared at nanosecond resolution (100 ns on NTFS) rather than the rounded `*Ms` doubles.\n */\ninterface FileIdentity {\n dev: bigint;\n ino: bigint;\n size: bigint;\n mode: bigint;\n mtimeNs: bigint;\n ctimeNs: bigint;\n}\n\nexport interface WrapperTargetFileOps {\n stat(path: string): BigIntStats;\n requireExecutable(path: string): void;\n sha256(path: string): string;\n}\n\nconst WINDOWS_EXECUTABLE = /\\.exe$/i;\n\n/**\n * What \"executable\" means before either file may be run. POSIX asks the kernel. Windows has no\n * execute bit — libuv's `access(X_OK)` passes for any existing file — so the rule there is the one\n * the spawn path applies: a native `.exe`, which the wrapper spawns directly. The extension's\n * bundled `claude.exe` and every patch target clodex records on Windows (npm's `bin\\claude.exe`,\n * the native installer's `claude.exe`) satisfy it; a `.cmd`/`.bat` launcher, which would be run\n * through `cmd.exe` and could name any program, is refused rather than hashed.\n */\nexport function requireWrapperExecutable(\n path: string,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (platform === 'win32') {\n if (!WINDOWS_EXECUTABLE.test(path)) throw new Error('not a Windows executable');\n return;\n }\n accessSync(path, fsConstants.X_OK);\n}\n\nexport const defaultWrapperTargetFileOps: WrapperTargetFileOps = {\n stat: path => statSync(path, { bigint: true }),\n requireExecutable: path => requireWrapperExecutable(path),\n sha256: sha256File,\n};\n\nexport type PreparedWrapperTarget =\n | { kind: 'decided'; decision: WrapperTargetDecision }\n | {\n kind: 'candidate';\n handedInPath: string;\n candidatePath: string;\n manifestPath: string;\n manifest: WrapperPatchManifest;\n handedInIdentity: FileIdentity;\n candidateIdentity: FileIdentity;\n fileOps: WrapperTargetFileOps;\n };\n\nfunction identityOf(path: string, fileOps: WrapperTargetFileOps): FileIdentity {\n const stat = fileOps.stat(path);\n if (!stat.isFile()) throw new Error('not a file');\n fileOps.requireExecutable(path);\n return {\n dev: stat.dev,\n ino: stat.ino,\n size: stat.size,\n mode: stat.mode,\n mtimeNs: stat.mtimeNs,\n ctimeNs: stat.ctimeNs,\n };\n}\n\nfunction sameFile(left: FileIdentity, right: FileIdentity): boolean {\n return left.dev === right.dev && left.ino === right.ino;\n}\n\nfunction sameIdentity(left: FileIdentity, right: FileIdentity): boolean {\n return sameFile(left, right)\n && left.size === right.size\n && left.mode === right.mode\n && left.mtimeNs === right.mtimeNs\n && left.ctimeNs === right.ctimeNs;\n}\n\nfunction fallbackNotice(\n handedInPath: string,\n candidatePath: string,\n manifestPath: string,\n claudeVersion: string | undefined,\n detail: string,\n): string {\n const version = claudeVersion ? ` for patched Claude ${claudeVersion}` : '';\n return `clodex-claude: running ${JSON.stringify(handedInPath)} because ${detail}${version}; `\n + `the patched install ${JSON.stringify(candidatePath)} was not selected, so clodex model-picker `\n + `entries may be absent; manifest=${JSON.stringify(manifestPath)}; align the VS Code and Claude `\n + 'Code builds, then run `clodex patch` again.';\n}\n\nfunction fallback(\n handedInPath: string,\n candidatePath: string,\n manifestPath: string,\n claudeVersion: string | undefined,\n reason: WrapperTargetReason,\n detail: string,\n): WrapperTargetDecision {\n return {\n path: handedInPath,\n reason,\n notice: fallbackNotice(handedInPath, candidatePath, manifestPath, claudeVersion, detail),\n };\n}\n\n/**\n * Inspect the handed-in binary and the one strict patch manifest that may replace\n * it. This stage hashes only the handed-in file. The patched file's full hash is\n * deliberately deferred to `finalizeWrapperTarget`, immediately before exec.\n */\nexport function prepareWrapperTarget(\n handedInPath: string,\n options: {\n manifestPath?: string;\n fileOps?: WrapperTargetFileOps;\n } = {},\n): PreparedWrapperTarget {\n const manifestRead = readWrapperPatchManifest(options.manifestPath);\n if (manifestRead.status === 'missing') {\n return {\n kind: 'decided',\n decision: { path: handedInPath, reason: 'manifest-missing' },\n };\n }\n if (manifestRead.status === 'invalid') {\n const candidatePath = manifestRead.binaryPath;\n if (!candidatePath || candidatePath === handedInPath || !manifestRead.claudeVersion) {\n return {\n kind: 'decided',\n decision: { path: handedInPath, reason: 'manifest-invalid' },\n };\n }\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifestRead.claudeVersion,\n 'manifest-invalid',\n 'the patch manifest does not contain every fingerprint required for safe substitution',\n ),\n };\n }\n\n const { manifest } = manifestRead;\n const candidatePath = manifest.binaryPath;\n const fileOps = options.fileOps ?? defaultWrapperTargetFileOps;\n let handedInIdentity: FileIdentity;\n let candidateIdentity: FileIdentity;\n try {\n handedInIdentity = identityOf(handedInPath, fileOps);\n } catch {\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifest.claudeVersion,\n 'handed-in-inspection-failed',\n 'VS Code\\'s handed-in executable could not be inspected',\n ),\n };\n }\n try {\n candidateIdentity = identityOf(candidatePath, fileOps);\n } catch {\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifest.claudeVersion,\n 'patched-install-unavailable',\n 'the recorded patched executable is missing, unreadable, or not executable',\n ),\n };\n }\n\n if (candidatePath === handedInPath || sameFile(handedInIdentity, candidateIdentity)) {\n return {\n kind: 'decided',\n decision: { path: handedInPath, reason: 'manifest-target-is-handed-in' },\n };\n }\n if (candidateIdentity.size !== BigInt(manifest.patchedSize)) {\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifest.claudeVersion,\n 'patched-install-size-changed',\n 'the recorded patched executable has changed size since it was patched',\n ),\n };\n }\n\n let handedInSha256: string;\n try {\n handedInSha256 = fileOps.sha256(handedInPath);\n const afterHash = identityOf(handedInPath, fileOps);\n if (!sameIdentity(handedInIdentity, afterHash)) {\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifest.claudeVersion,\n 'input-changed-before-handoff',\n 'VS Code\\'s handed-in executable changed while its identity was being checked',\n ),\n };\n }\n handedInIdentity = afterHash;\n } catch {\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifest.claudeVersion,\n 'handed-in-inspection-failed',\n 'VS Code\\'s handed-in executable could not be hashed',\n ),\n };\n }\n\n if (handedInSha256 === manifest.patchedSha256) {\n return {\n kind: 'decided',\n decision: { path: handedInPath, reason: 'handed-in-already-patched' },\n };\n }\n if (handedInSha256 !== manifest.pristineSha256) {\n return {\n kind: 'decided',\n decision: fallback(\n handedInPath,\n candidatePath,\n manifestRead.path,\n manifest.claudeVersion,\n 'handed-in-not-recorded-pristine',\n 'VS Code\\'s handed-in bytes do not match the pristine source recorded by clodex',\n ),\n };\n }\n\n return {\n kind: 'candidate',\n handedInPath,\n candidatePath,\n manifestPath: manifestRead.path,\n manifest,\n handedInIdentity,\n candidateIdentity,\n fileOps,\n };\n}\n\n/**\n * Verify the selected executable at the handoff. Both inspected path identities\n * are rechecked around the patched file's full hash so an updater race declines\n * substitution. The remaining hash-to-exec path race is inherent to execve.\n */\nexport function finalizeWrapperTarget(\n prepared: PreparedWrapperTarget,\n): WrapperTargetDecision {\n if (prepared.kind === 'decided') return prepared.decision;\n\n const {\n handedInPath,\n candidatePath,\n manifestPath,\n manifest,\n handedInIdentity,\n candidateIdentity,\n fileOps,\n } = prepared;\n const changed = () => fallback(\n handedInPath,\n candidatePath,\n manifestPath,\n manifest.claudeVersion,\n 'input-changed-before-handoff',\n 'one of the executables changed between inspection and launch',\n );\n\n try {\n if (!sameIdentity(handedInIdentity, identityOf(handedInPath, fileOps))) return changed();\n if (!sameIdentity(candidateIdentity, identityOf(candidatePath, fileOps))) return changed();\n\n const candidateSha256 = fileOps.sha256(candidatePath);\n if (!sameIdentity(candidateIdentity, identityOf(candidatePath, fileOps))) return changed();\n if (!sameIdentity(handedInIdentity, identityOf(handedInPath, fileOps))) return changed();\n if (candidateSha256 !== manifest.patchedSha256) {\n return fallback(\n handedInPath,\n candidatePath,\n manifestPath,\n manifest.claudeVersion,\n 'patched-install-verification-failed',\n 'the recorded patched executable no longer has the SHA-256 clodex published',\n );\n }\n } catch {\n return fallback(\n handedInPath,\n candidatePath,\n manifestPath,\n manifest.claudeVersion,\n 'patched-install-verification-failed',\n 'one of the executables could not be verified at launch',\n );\n }\n\n return { path: candidatePath, reason: 'verified-patched-install' };\n}\n"],"mappings":"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","names":["accessSync","fsConstants","statSync","candidatePath","statSync","accessSync","fsConstants"]}
|