@clanker-cleanroom/claude 0.2.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eric Siebeneich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @clanker-cleanroom/claude
2
+
3
+ Claude Code binding and Docker image for [`clanker-cleanroom`](../clanker-cleanroom).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D clanker-cleanroom @clanker-cleanroom/claude
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import "@clanker-cleanroom/claude/register";
15
+ import { Agent, buildImages } from "clanker-cleanroom";
16
+ import { DOCKER_DIR as claudeDocker, claudeAgent } from "@clanker-cleanroom/claude";
17
+
18
+ await buildImages();
19
+ await buildImages({ dir: claudeDocker });
20
+
21
+ await new Agent("claude").run({ workspace, prompt, model: "sonnet" });
22
+ // or without registration:
23
+ await claudeAgent.run({ workspace, prompt, model: "sonnet" });
24
+ ```
25
+
26
+ ## Prerequisites
27
+
28
+ Set `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`) **or** `ANTHROPIC_API_KEY` **or** provide a Linux host's `~/.claude/.credentials.json` (checked in that order). macOS keeps Claude login in the Keychain — set a token or API key on a Mac.
29
+
30
+ ## Exports
31
+
32
+ | Export | Role |
33
+ | --- | --- |
34
+ | `claudeBinding` | Command, prepare, parseResult, adaptEvents |
35
+ | `claudeAgent` | `Agent.fromBinding(claudeBinding)` |
36
+ | `resolveClaudeCredentials` / `credentialsEnv` | Host credential resolution |
37
+ | `CLAUDE_IMAGE` / credential env constants | Image tag and env names |
38
+ | `adaptClaudeEvents` | Trajectory adapter |
39
+ | `DOCKER_DIR` / `PACKAGE_ROOT` | Dockerfile folder for `buildImages({ dir })` |
40
+ | `@clanker-cleanroom/claude/register` | Side-effect: `registerBinding("claude", claudeBinding)` |
@@ -0,0 +1,32 @@
1
+ # clanker-cleanroom/claude
2
+ FROM clanker-cleanroom/base
3
+
4
+ # Never self-update or phone home from inside the container — build steps included.
5
+ ENV DISABLE_AUTOUPDATER=1
6
+ ENV DISABLE_TELEMETRY=1
7
+ ENV DISABLE_ERROR_REPORTING=1
8
+
9
+ # Official native installer drops a single self-contained binary under
10
+ # ~/.local/share/claude/versions/<version>. Relocate it to a world-readable
11
+ # path so arbitrary host UIDs (docker --user) can run claude.
12
+ RUN curl -fsSL https://claude.ai/install.sh | bash \
13
+ && VERSION_BIN="$(find /root/.local/share/claude/versions -mindepth 1 -maxdepth 1 -type f | sort -V | tail -1)" \
14
+ && test -n "$VERSION_BIN" \
15
+ && install -m 0755 "$VERSION_BIN" /usr/local/bin/claude \
16
+ && rm -rf /root/.local/share/claude /root/.local/bin/claude \
17
+ && claude --version
18
+
19
+ # Empty home for arbitrary host UIDs. ~/.claude is pre-created so a read-only
20
+ # .credentials.json bind mount does not leave the directory root-owned.
21
+ RUN mkdir -p /home/agent/.claude \
22
+ && chmod -R 0777 /home/agent
23
+
24
+ ENV HOME=/home/agent
25
+ ENV PATH="/usr/local/bin:${PATH}"
26
+
27
+ WORKDIR /workspace
28
+
29
+ # Runtime identity is set via --user <host-uid>:<host-gid>.
30
+ # Credentials arrive as env (CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY) or a
31
+ # read-only ~/.claude/.credentials.json mount.
32
+ # No ENTRYPOINT — the library passes `claude ...` as the container command.
@@ -0,0 +1,279 @@
1
+ let clanker_cleanroom = require("clanker-cleanroom");
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_fs = require("node:fs");
4
+ let node_os = require("node:os");
5
+ //#region src/constants.ts
6
+ const CLAUDE_IMAGE = "clanker-cleanroom/claude";
7
+ const CLAUDE_CONTAINER_CREDENTIALS_PATH = `${clanker_cleanroom.CONTAINER_HOME}/.claude/.credentials.json`;
8
+ /** Long-lived OAuth token from `claude setup-token` (Claude subscription). */
9
+ const CLAUDE_OAUTH_TOKEN_ENV = "CLAUDE_CODE_OAUTH_TOKEN";
10
+ /** Anthropic API key (pay-as-you-go). */
11
+ const CLAUDE_API_KEY_ENV = "ANTHROPIC_API_KEY";
12
+ /** Linux hosts persist OAuth credentials here; macOS uses the Keychain instead. */
13
+ const defaultClaudeHostCredentialsFile = (home) => `${home}/.claude/.credentials.json`;
14
+ //#endregion
15
+ //#region src/credentials.ts
16
+ /** Env OAuth token, then env API key, then a readable host credentials file. */
17
+ async function resolveClaudeCredentials(options = {}) {
18
+ const env = options.env ?? process.env;
19
+ const home = options.home ?? (0, node_os.homedir)();
20
+ const token = env[CLAUDE_OAUTH_TOKEN_ENV];
21
+ if (token !== void 0 && token !== "") return {
22
+ kind: "oauth-token",
23
+ token
24
+ };
25
+ const apiKey = env[CLAUDE_API_KEY_ENV];
26
+ if (apiKey !== void 0 && apiKey !== "") return {
27
+ kind: "api-key",
28
+ apiKey
29
+ };
30
+ const file = defaultClaudeHostCredentialsFile(home);
31
+ try {
32
+ await (0, node_fs_promises.access)(file, node_fs.constants.R_OK);
33
+ } catch {
34
+ throw new Error(`Claude Code credentials not found. Set ${CLAUDE_OAUTH_TOKEN_ENV} (run \`claude setup-token\` on the host) or ${CLAUDE_API_KEY_ENV}, or provide ${file} (Linux hosts; macOS keeps credentials in the Keychain).`);
35
+ }
36
+ return {
37
+ kind: "credentials-file",
38
+ file
39
+ };
40
+ }
41
+ /** Secret values for the docker CLI process, keyed by env names forwarded via envPassthrough. */
42
+ function credentialsEnv(credentials) {
43
+ switch (credentials.kind) {
44
+ case "oauth-token": return { [CLAUDE_OAUTH_TOKEN_ENV]: credentials.token };
45
+ case "api-key": return { [CLAUDE_API_KEY_ENV]: credentials.apiKey };
46
+ case "credentials-file": return {};
47
+ }
48
+ }
49
+ //#endregion
50
+ //#region src/trajectory.ts
51
+ function adaptClaudeEvents(rawEvents) {
52
+ const events = [];
53
+ for (const raw of rawEvents) {
54
+ const record = (0, clanker_cleanroom.asRecord)(raw);
55
+ if (record === void 0) {
56
+ events.push({
57
+ kind: "other",
58
+ type: "unknown",
59
+ raw
60
+ });
61
+ continue;
62
+ }
63
+ const type = (0, clanker_cleanroom.readString)(record.type) ?? "unknown";
64
+ switch (type) {
65
+ case "system":
66
+ events.push({
67
+ kind: "system",
68
+ sessionId: (0, clanker_cleanroom.readString)(record.session_id),
69
+ model: (0, clanker_cleanroom.readString)(record.model),
70
+ raw
71
+ });
72
+ break;
73
+ case "user":
74
+ events.push(...adaptClaudeUser(record, raw));
75
+ break;
76
+ case "assistant":
77
+ events.push(...adaptClaudeAssistant(record, raw));
78
+ break;
79
+ case "result":
80
+ events.push({
81
+ kind: "result",
82
+ text: (0, clanker_cleanroom.readString)(record.result) ?? "",
83
+ isError: record.is_error === true,
84
+ durationMs: (0, clanker_cleanroom.readTokenCount)(record.duration_ms),
85
+ costUsd: (0, clanker_cleanroom.readTokenCount)(record.total_cost_usd),
86
+ raw
87
+ });
88
+ break;
89
+ default: events.push({
90
+ kind: "other",
91
+ type,
92
+ raw
93
+ });
94
+ }
95
+ }
96
+ return events;
97
+ }
98
+ function adaptClaudeAssistant(record, raw) {
99
+ const events = [];
100
+ const text = (0, clanker_cleanroom.extractTextContent)(record.message);
101
+ if (text !== "") events.push({
102
+ kind: "assistant",
103
+ text,
104
+ raw
105
+ });
106
+ for (const block of contentBlocks(record.message)) {
107
+ if (block.type !== "tool_use") continue;
108
+ events.push({
109
+ kind: "tool",
110
+ phase: "started",
111
+ callId: (0, clanker_cleanroom.readString)(block.id) ?? "",
112
+ name: (0, clanker_cleanroom.readString)(block.name) ?? "unknown",
113
+ args: block.input ?? null,
114
+ result: null,
115
+ raw
116
+ });
117
+ }
118
+ if (events.length === 0) events.push({
119
+ kind: "assistant",
120
+ text: "",
121
+ raw
122
+ });
123
+ return events;
124
+ }
125
+ function adaptClaudeUser(record, raw) {
126
+ const events = [];
127
+ const text = (0, clanker_cleanroom.extractTextContent)(record.message);
128
+ const toolResults = [];
129
+ for (const block of contentBlocks(record.message)) {
130
+ if (block.type !== "tool_result") continue;
131
+ toolResults.push({
132
+ kind: "tool",
133
+ phase: "completed",
134
+ callId: (0, clanker_cleanroom.readString)(block.tool_use_id) ?? "",
135
+ name: "tool_result",
136
+ args: null,
137
+ result: block.content ?? null,
138
+ raw
139
+ });
140
+ }
141
+ if (text !== "" || toolResults.length === 0) events.push({
142
+ kind: "user",
143
+ text,
144
+ raw
145
+ });
146
+ events.push(...toolResults);
147
+ return events;
148
+ }
149
+ function contentBlocks(message) {
150
+ const content = (0, clanker_cleanroom.asRecord)(message)?.content;
151
+ if (!Array.isArray(content)) return [];
152
+ return content.map((block) => (0, clanker_cleanroom.asRecord)(block)).filter((block) => block !== void 0);
153
+ }
154
+ //#endregion
155
+ //#region src/binding.ts
156
+ const claudeBinding = {
157
+ image: CLAUDE_IMAGE,
158
+ displayName: "Claude",
159
+ trajectoryKind: "claude",
160
+ adaptEvents: adaptClaudeEvents,
161
+ command: ({ prompt, model }) => {
162
+ const claudeArgs = [
163
+ "claude",
164
+ "-p",
165
+ "--output-format",
166
+ "stream-json",
167
+ "--verbose",
168
+ "--dangerously-skip-permissions"
169
+ ];
170
+ if (model !== void 0 && model !== "") claudeArgs.push("--model", model);
171
+ claudeArgs.push("--", prompt);
172
+ return claudeArgs;
173
+ },
174
+ prepare: async () => {
175
+ const credentials = await resolveClaudeCredentials();
176
+ const volumes = [];
177
+ if (credentials.kind === "credentials-file") volumes.push({
178
+ host: credentials.file,
179
+ container: CLAUDE_CONTAINER_CREDENTIALS_PATH,
180
+ mode: "ro"
181
+ });
182
+ return {
183
+ volumes,
184
+ env: credentialsEnv(credentials)
185
+ };
186
+ },
187
+ parseResult: parseClaudeResult,
188
+ describeFailure: describeClaudeFailure
189
+ };
190
+ function parseClaudeResult(trajectory) {
191
+ const parsed = (0, clanker_cleanroom.parseAgentJsonOutput)(trajectory);
192
+ if (isErrorResult(parsed)) throw new Error(`Claude agent reported an error: ${describeErrorResult(parsed)}`);
193
+ const record = (0, clanker_cleanroom.asRecord)(parsed);
194
+ const usage = (0, clanker_cleanroom.asRecord)(record?.usage);
195
+ return {
196
+ durationMs: (0, clanker_cleanroom.readTokenCount)(record?.duration_ms),
197
+ costUsd: (0, clanker_cleanroom.readTokenCount)(record?.total_cost_usd),
198
+ usage: {
199
+ inputTokens: (0, clanker_cleanroom.readTokenCount)(usage?.input_tokens ?? usage?.inputTokens),
200
+ outputTokens: (0, clanker_cleanroom.readTokenCount)(usage?.output_tokens ?? usage?.outputTokens),
201
+ cacheReadTokens: (0, clanker_cleanroom.readTokenCount)(usage?.cache_read_input_tokens ?? usage?.cacheReadTokens),
202
+ cacheWriteTokens: (0, clanker_cleanroom.readTokenCount)(usage?.cache_creation_input_tokens ?? usage?.cacheWriteTokens)
203
+ }
204
+ };
205
+ }
206
+ function describeClaudeFailure(trajectory) {
207
+ try {
208
+ const parsed = (0, clanker_cleanroom.parseAgentJsonOutput)(trajectory);
209
+ return isErrorResult(parsed) ? describeErrorResult(parsed) : void 0;
210
+ } catch {
211
+ return;
212
+ }
213
+ }
214
+ function isErrorResult(value) {
215
+ return typeof value === "object" && value !== null && value.is_error === true;
216
+ }
217
+ function describeErrorResult(result) {
218
+ const message = String(result.result);
219
+ if (result.terminal_reason !== void 0 && result.terminal_reason !== "completed") return `${result.terminal_reason}: ${message}`;
220
+ if (result.subtype !== void 0 && result.subtype !== "success") return `${result.subtype}: ${message}`;
221
+ return message;
222
+ }
223
+ //#endregion
224
+ Object.defineProperty(exports, "CLAUDE_API_KEY_ENV", {
225
+ enumerable: true,
226
+ get: function() {
227
+ return CLAUDE_API_KEY_ENV;
228
+ }
229
+ });
230
+ Object.defineProperty(exports, "CLAUDE_CONTAINER_CREDENTIALS_PATH", {
231
+ enumerable: true,
232
+ get: function() {
233
+ return CLAUDE_CONTAINER_CREDENTIALS_PATH;
234
+ }
235
+ });
236
+ Object.defineProperty(exports, "CLAUDE_IMAGE", {
237
+ enumerable: true,
238
+ get: function() {
239
+ return CLAUDE_IMAGE;
240
+ }
241
+ });
242
+ Object.defineProperty(exports, "CLAUDE_OAUTH_TOKEN_ENV", {
243
+ enumerable: true,
244
+ get: function() {
245
+ return CLAUDE_OAUTH_TOKEN_ENV;
246
+ }
247
+ });
248
+ Object.defineProperty(exports, "adaptClaudeEvents", {
249
+ enumerable: true,
250
+ get: function() {
251
+ return adaptClaudeEvents;
252
+ }
253
+ });
254
+ Object.defineProperty(exports, "claudeBinding", {
255
+ enumerable: true,
256
+ get: function() {
257
+ return claudeBinding;
258
+ }
259
+ });
260
+ Object.defineProperty(exports, "credentialsEnv", {
261
+ enumerable: true,
262
+ get: function() {
263
+ return credentialsEnv;
264
+ }
265
+ });
266
+ Object.defineProperty(exports, "defaultClaudeHostCredentialsFile", {
267
+ enumerable: true,
268
+ get: function() {
269
+ return defaultClaudeHostCredentialsFile;
270
+ }
271
+ });
272
+ Object.defineProperty(exports, "resolveClaudeCredentials", {
273
+ enumerable: true,
274
+ get: function() {
275
+ return resolveClaudeCredentials;
276
+ }
277
+ });
278
+
279
+ //# sourceMappingURL=binding-C2kSQy-B.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding-C2kSQy-B.cjs","names":["CONTAINER_HOME","homedir","access","fsConstants","asRecord","readString","readTokenCount","extractTextContent","parseAgentJsonOutput","asRecord","readTokenCount"],"sources":["../src/constants.ts","../src/credentials.ts","../src/trajectory.ts","../src/binding.ts"],"sourcesContent":["import { CONTAINER_HOME } from \"clanker-cleanroom\";\n\nexport const CLAUDE_IMAGE = \"clanker-cleanroom/claude\";\nexport const CLAUDE_CONTAINER_CREDENTIALS_PATH = `${CONTAINER_HOME}/.claude/.credentials.json`;\n\n/** Long-lived OAuth token from `claude setup-token` (Claude subscription). */\nexport const CLAUDE_OAUTH_TOKEN_ENV = \"CLAUDE_CODE_OAUTH_TOKEN\";\n/** Anthropic API key (pay-as-you-go). */\nexport const CLAUDE_API_KEY_ENV = \"ANTHROPIC_API_KEY\";\n\n/** Linux hosts persist OAuth credentials here; macOS uses the Keychain instead. */\nexport const defaultClaudeHostCredentialsFile = (home: string): string =>\n `${home}/.claude/.credentials.json`;\n","import { access } from \"node:fs/promises\";\nimport { constants as fsConstants } from \"node:fs\";\nimport { homedir } from \"node:os\";\n\nimport {\n CLAUDE_API_KEY_ENV,\n CLAUDE_OAUTH_TOKEN_ENV,\n defaultClaudeHostCredentialsFile,\n} from \"./constants.js\";\n\n/**\n * How Claude Code authenticates inside the container. Env-backed kinds are forwarded\n * to `docker run` by name (never on argv); the file kind is bind-mounted read-only.\n * See `constants.ts` for which host setups produce each.\n */\nexport type ClaudeCredentials =\n | { kind: \"oauth-token\"; token: string }\n | { kind: \"api-key\"; apiKey: string }\n | { kind: \"credentials-file\"; file: string };\n\n/** Env OAuth token, then env API key, then a readable host credentials file. */\nexport async function resolveClaudeCredentials(\n options: { env?: NodeJS.ProcessEnv; home?: string } = {},\n): Promise<ClaudeCredentials> {\n const env = options.env ?? process.env;\n const home = options.home ?? homedir();\n\n const token = env[CLAUDE_OAUTH_TOKEN_ENV];\n if (token !== undefined && token !== \"\") {\n return { kind: \"oauth-token\", token };\n }\n\n const apiKey = env[CLAUDE_API_KEY_ENV];\n if (apiKey !== undefined && apiKey !== \"\") {\n return { kind: \"api-key\", apiKey };\n }\n\n const file = defaultClaudeHostCredentialsFile(home);\n try {\n await access(file, fsConstants.R_OK);\n } catch {\n throw new Error(\n `Claude Code credentials not found. Set ${CLAUDE_OAUTH_TOKEN_ENV} (run \\`claude setup-token\\` on the host) ` +\n `or ${CLAUDE_API_KEY_ENV}, or provide ${file} (Linux hosts; macOS keeps credentials in the Keychain).`,\n );\n }\n\n return { kind: \"credentials-file\", file };\n}\n\n/** Secret values for the docker CLI process, keyed by env names forwarded via envPassthrough. */\nexport function credentialsEnv(credentials: ClaudeCredentials): Record<string, string> {\n switch (credentials.kind) {\n case \"oauth-token\":\n return { [CLAUDE_OAUTH_TOKEN_ENV]: credentials.token };\n case \"api-key\":\n return { [CLAUDE_API_KEY_ENV]: credentials.apiKey };\n case \"credentials-file\":\n return {};\n }\n}\n","import {\n asRecord,\n extractTextContent,\n readString,\n readTokenCount,\n type TrajectoryEvent,\n} from \"clanker-cleanroom\";\n\nexport function adaptClaudeEvents(rawEvents: readonly unknown[]): TrajectoryEvent[] {\n const events: TrajectoryEvent[] = [];\n\n for (const raw of rawEvents) {\n const record = asRecord(raw);\n if (record === undefined) {\n events.push({ kind: \"other\", type: \"unknown\", raw });\n continue;\n }\n\n const type = readString(record.type) ?? \"unknown\";\n\n switch (type) {\n case \"system\":\n events.push({\n kind: \"system\",\n sessionId: readString(record.session_id),\n model: readString(record.model),\n raw,\n });\n break;\n case \"user\":\n events.push(...adaptClaudeUser(record, raw));\n break;\n case \"assistant\":\n events.push(...adaptClaudeAssistant(record, raw));\n break;\n case \"result\":\n events.push({\n kind: \"result\",\n text: readString(record.result) ?? \"\",\n isError: record.is_error === true,\n durationMs: readTokenCount(record.duration_ms),\n costUsd: readTokenCount(record.total_cost_usd),\n raw,\n });\n break;\n default:\n events.push({ kind: \"other\", type, raw });\n }\n }\n\n return events;\n}\n\nfunction adaptClaudeAssistant(\n record: Record<string, unknown>,\n raw: unknown,\n): TrajectoryEvent[] {\n const events: TrajectoryEvent[] = [];\n const text = extractTextContent(record.message);\n if (text !== \"\") {\n events.push({ kind: \"assistant\", text, raw });\n }\n\n for (const block of contentBlocks(record.message)) {\n if (block.type !== \"tool_use\") {\n continue;\n }\n events.push({\n kind: \"tool\",\n phase: \"started\",\n callId: readString(block.id) ?? \"\",\n name: readString(block.name) ?? \"unknown\",\n args: block.input ?? null,\n result: null,\n raw,\n });\n }\n\n if (events.length === 0) {\n events.push({ kind: \"assistant\", text: \"\", raw });\n }\n\n return events;\n}\n\nfunction adaptClaudeUser(record: Record<string, unknown>, raw: unknown): TrajectoryEvent[] {\n const events: TrajectoryEvent[] = [];\n const text = extractTextContent(record.message);\n const toolResults: TrajectoryEvent[] = [];\n\n for (const block of contentBlocks(record.message)) {\n if (block.type !== \"tool_result\") {\n continue;\n }\n toolResults.push({\n kind: \"tool\",\n phase: \"completed\",\n callId: readString(block.tool_use_id) ?? \"\",\n name: \"tool_result\",\n args: null,\n result: block.content ?? null,\n raw,\n });\n }\n\n if (text !== \"\" || toolResults.length === 0) {\n events.push({ kind: \"user\", text, raw });\n }\n events.push(...toolResults);\n return events;\n}\n\nfunction contentBlocks(message: unknown): Record<string, unknown>[] {\n const content = asRecord(message)?.content;\n if (!Array.isArray(content)) {\n return [];\n }\n return content.map((block) => asRecord(block)).filter((block) => block !== undefined);\n}\n","import {\n asRecord,\n parseAgentJsonOutput,\n readTokenCount,\n type AgentBinding,\n type AgentRunResult,\n type DockerVolumeMount,\n} from \"clanker-cleanroom\";\n\nimport { CLAUDE_CONTAINER_CREDENTIALS_PATH, CLAUDE_IMAGE } from \"./constants.js\";\nimport { credentialsEnv, resolveClaudeCredentials } from \"./credentials.js\";\nimport { adaptClaudeEvents } from \"./trajectory.js\";\n\nexport const claudeBinding: AgentBinding = {\n image: CLAUDE_IMAGE,\n displayName: \"Claude\",\n trajectoryKind: \"claude\",\n adaptEvents: adaptClaudeEvents,\n command: ({ prompt, model }) => {\n const claudeArgs = [\n \"claude\",\n \"-p\",\n \"--output-format\",\n \"stream-json\",\n \"--verbose\",\n \"--dangerously-skip-permissions\",\n ];\n if (model !== undefined && model !== \"\") {\n claudeArgs.push(\"--model\", model);\n }\n claudeArgs.push(\"--\", prompt);\n return claudeArgs;\n },\n prepare: async () => {\n const credentials = await resolveClaudeCredentials();\n const volumes: DockerVolumeMount[] = [];\n if (credentials.kind === \"credentials-file\") {\n volumes.push({\n host: credentials.file,\n container: CLAUDE_CONTAINER_CREDENTIALS_PATH,\n mode: \"ro\",\n });\n }\n return {\n volumes,\n env: credentialsEnv(credentials),\n };\n },\n parseResult: parseClaudeResult,\n describeFailure: describeClaudeFailure,\n};\n\nfunction parseClaudeResult(trajectory: string): AgentRunResult {\n const parsed = parseAgentJsonOutput(trajectory);\n if (isErrorResult(parsed)) {\n throw new Error(`Claude agent reported an error: ${describeErrorResult(parsed)}`);\n }\n\n const record = asRecord(parsed);\n const usage = asRecord(record?.usage);\n\n return {\n durationMs: readTokenCount(record?.duration_ms),\n costUsd: readTokenCount(record?.total_cost_usd),\n usage: {\n inputTokens: readTokenCount(usage?.input_tokens ?? usage?.inputTokens),\n outputTokens: readTokenCount(usage?.output_tokens ?? usage?.outputTokens),\n cacheReadTokens: readTokenCount(usage?.cache_read_input_tokens ?? usage?.cacheReadTokens),\n cacheWriteTokens: readTokenCount(\n usage?.cache_creation_input_tokens ?? usage?.cacheWriteTokens,\n ),\n },\n };\n}\n\nfunction describeClaudeFailure(trajectory: string): string | undefined {\n try {\n const parsed = parseAgentJsonOutput(trajectory);\n return isErrorResult(parsed) ? describeErrorResult(parsed) : undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ClaudeErrorResult = {\n is_error: true;\n result: string;\n subtype?: string;\n terminal_reason?: string;\n};\n\nfunction isErrorResult(value: unknown): value is ClaudeErrorResult {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { is_error?: unknown }).is_error === true\n );\n}\n\nfunction describeErrorResult(result: ClaudeErrorResult): string {\n const message = String(result.result);\n\n if (result.terminal_reason !== undefined && result.terminal_reason !== \"completed\") {\n return `${result.terminal_reason}: ${message}`;\n }\n\n if (result.subtype !== undefined && result.subtype !== \"success\") {\n return `${result.subtype}: ${message}`;\n }\n\n return message;\n}\n"],"mappings":";;;;;AAEA,MAAa,eAAe;AAC5B,MAAa,oCAAoC,GAAGA,kBAAAA,eAAe;;AAGnE,MAAa,yBAAyB;;AAEtC,MAAa,qBAAqB;;AAGlC,MAAa,oCAAoC,SAC/C,GAAG,KAAK;;;;ACSV,eAAsB,yBACpB,UAAsD,CAAC,GAC3B;CAC5B,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,MAAM,OAAO,QAAQ,SAAA,GAAQC,QAAAA,QAAAA,CAAQ;CAErC,MAAM,QAAQ,IAAI;CAClB,IAAI,UAAU,KAAA,KAAa,UAAU,IACnC,OAAO;EAAE,MAAM;EAAe;CAAM;CAGtC,MAAM,SAAS,IAAI;CACnB,IAAI,WAAW,KAAA,KAAa,WAAW,IACrC,OAAO;EAAE,MAAM;EAAW;CAAO;CAGnC,MAAM,OAAO,iCAAiC,IAAI;CAClD,IAAI;EACF,OAAA,GAAMC,iBAAAA,OAAAA,CAAO,MAAMC,QAAAA,UAAY,IAAI;CACrC,QAAQ;EACN,MAAM,IAAI,MACR,0CAA0C,uBAAuB,+CACzD,mBAAmB,eAAe,KAAK,yDACjD;CACF;CAEA,OAAO;EAAE,MAAM;EAAoB;CAAK;AAC1C;;AAGA,SAAgB,eAAe,aAAwD;CACrF,QAAQ,YAAY,MAApB;EACE,KAAK,eACH,OAAO,GAAG,yBAAyB,YAAY,MAAM;EACvD,KAAK,WACH,OAAO,GAAG,qBAAqB,YAAY,OAAO;EACpD,KAAK,oBACH,OAAO,CAAC;CACZ;AACF;;;ACpDA,SAAgB,kBAAkB,WAAkD;CAClF,MAAM,SAA4B,CAAC;CAEnC,KAAK,MAAM,OAAO,WAAW;EAC3B,MAAM,UAAA,GAASC,kBAAAA,SAAAA,CAAS,GAAG;EAC3B,IAAI,WAAW,KAAA,GAAW;GACxB,OAAO,KAAK;IAAE,MAAM;IAAS,MAAM;IAAW;GAAI,CAAC;GACnD;EACF;EAEA,MAAM,QAAA,GAAOC,kBAAAA,WAAAA,CAAW,OAAO,IAAI,KAAK;EAExC,QAAQ,MAAR;GACE,KAAK;IACH,OAAO,KAAK;KACV,MAAM;KACN,YAAA,GAAWA,kBAAAA,WAAAA,CAAW,OAAO,UAAU;KACvC,QAAA,GAAOA,kBAAAA,WAAAA,CAAW,OAAO,KAAK;KAC9B;IACF,CAAC;IACD;GACF,KAAK;IACH,OAAO,KAAK,GAAG,gBAAgB,QAAQ,GAAG,CAAC;IAC3C;GACF,KAAK;IACH,OAAO,KAAK,GAAG,qBAAqB,QAAQ,GAAG,CAAC;IAChD;GACF,KAAK;IACH,OAAO,KAAK;KACV,MAAM;KACN,OAAA,GAAMA,kBAAAA,WAAAA,CAAW,OAAO,MAAM,KAAK;KACnC,SAAS,OAAO,aAAa;KAC7B,aAAA,GAAYC,kBAAAA,eAAAA,CAAe,OAAO,WAAW;KAC7C,UAAA,GAASA,kBAAAA,eAAAA,CAAe,OAAO,cAAc;KAC7C;IACF,CAAC;IACD;GACF,SACE,OAAO,KAAK;IAAE,MAAM;IAAS;IAAM;GAAI,CAAC;EAC5C;CACF;CAEA,OAAO;AACT;AAEA,SAAS,qBACP,QACA,KACmB;CACnB,MAAM,SAA4B,CAAC;CACnC,MAAM,QAAA,GAAOC,kBAAAA,mBAAAA,CAAmB,OAAO,OAAO;CAC9C,IAAI,SAAS,IACX,OAAO,KAAK;EAAE,MAAM;EAAa;EAAM;CAAI,CAAC;CAG9C,KAAK,MAAM,SAAS,cAAc,OAAO,OAAO,GAAG;EACjD,IAAI,MAAM,SAAS,YACjB;EAEF,OAAO,KAAK;GACV,MAAM;GACN,OAAO;GACP,SAAA,GAAQF,kBAAAA,WAAAA,CAAW,MAAM,EAAE,KAAK;GAChC,OAAA,GAAMA,kBAAAA,WAAAA,CAAW,MAAM,IAAI,KAAK;GAChC,MAAM,MAAM,SAAS;GACrB,QAAQ;GACR;EACF,CAAC;CACH;CAEA,IAAI,OAAO,WAAW,GACpB,OAAO,KAAK;EAAE,MAAM;EAAa,MAAM;EAAI;CAAI,CAAC;CAGlD,OAAO;AACT;AAEA,SAAS,gBAAgB,QAAiC,KAAiC;CACzF,MAAM,SAA4B,CAAC;CACnC,MAAM,QAAA,GAAOE,kBAAAA,mBAAAA,CAAmB,OAAO,OAAO;CAC9C,MAAM,cAAiC,CAAC;CAExC,KAAK,MAAM,SAAS,cAAc,OAAO,OAAO,GAAG;EACjD,IAAI,MAAM,SAAS,eACjB;EAEF,YAAY,KAAK;GACf,MAAM;GACN,OAAO;GACP,SAAA,GAAQF,kBAAAA,WAAAA,CAAW,MAAM,WAAW,KAAK;GACzC,MAAM;GACN,MAAM;GACN,QAAQ,MAAM,WAAW;GACzB;EACF,CAAC;CACH;CAEA,IAAI,SAAS,MAAM,YAAY,WAAW,GACxC,OAAO,KAAK;EAAE,MAAM;EAAQ;EAAM;CAAI,CAAC;CAEzC,OAAO,KAAK,GAAG,WAAW;CAC1B,OAAO;AACT;AAEA,SAAS,cAAc,SAA6C;CAClE,MAAM,WAAA,GAAUD,kBAAAA,SAAAA,CAAS,OAAO,CAAC,EAAE;CACnC,IAAI,CAAC,MAAM,QAAQ,OAAO,GACxB,OAAO,CAAC;CAEV,OAAO,QAAQ,KAAK,WAAA,GAAUA,kBAAAA,SAAAA,CAAS,KAAK,CAAC,CAAC,CAAC,QAAQ,UAAU,UAAU,KAAA,CAAS;AACtF;;;ACzGA,MAAa,gBAA8B;CACzC,OAAO;CACP,aAAa;CACb,gBAAgB;CAChB,aAAa;CACb,UAAU,EAAE,QAAQ,YAAY;EAC9B,MAAM,aAAa;GACjB;GACA;GACA;GACA;GACA;GACA;EACF;EACA,IAAI,UAAU,KAAA,KAAa,UAAU,IACnC,WAAW,KAAK,WAAW,KAAK;EAElC,WAAW,KAAK,MAAM,MAAM;EAC5B,OAAO;CACT;CACA,SAAS,YAAY;EACnB,MAAM,cAAc,MAAM,yBAAyB;EACnD,MAAM,UAA+B,CAAC;EACtC,IAAI,YAAY,SAAS,oBACvB,QAAQ,KAAK;GACX,MAAM,YAAY;GAClB,WAAW;GACX,MAAM;EACR,CAAC;EAEH,OAAO;GACL;GACA,KAAK,eAAe,WAAW;EACjC;CACF;CACA,aAAa;CACb,iBAAiB;AACnB;AAEA,SAAS,kBAAkB,YAAoC;CAC7D,MAAM,UAAA,GAASI,kBAAAA,qBAAAA,CAAqB,UAAU;CAC9C,IAAI,cAAc,MAAM,GACtB,MAAM,IAAI,MAAM,mCAAmC,oBAAoB,MAAM,GAAG;CAGlF,MAAM,UAAA,GAASC,kBAAAA,SAAAA,CAAS,MAAM;CAC9B,MAAM,SAAA,GAAQA,kBAAAA,SAAAA,CAAS,QAAQ,KAAK;CAEpC,OAAO;EACL,aAAA,GAAYC,kBAAAA,eAAAA,CAAe,QAAQ,WAAW;EAC9C,UAAA,GAASA,kBAAAA,eAAAA,CAAe,QAAQ,cAAc;EAC9C,OAAO;GACL,cAAA,GAAaA,kBAAAA,eAAAA,CAAe,OAAO,gBAAgB,OAAO,WAAW;GACrE,eAAA,GAAcA,kBAAAA,eAAAA,CAAe,OAAO,iBAAiB,OAAO,YAAY;GACxE,kBAAA,GAAiBA,kBAAAA,eAAAA,CAAe,OAAO,2BAA2B,OAAO,eAAe;GACxF,mBAAA,GAAkBA,kBAAAA,eAAAA,CAChB,OAAO,+BAA+B,OAAO,gBAC/C;EACF;CACF;AACF;AAEA,SAAS,sBAAsB,YAAwC;CACrE,IAAI;EACF,MAAM,UAAA,GAASF,kBAAAA,qBAAAA,CAAqB,UAAU;EAC9C,OAAO,cAAc,MAAM,IAAI,oBAAoB,MAAM,IAAI,KAAA;CAC/D,QAAQ;EACN;CACF;AACF;AASA,SAAS,cAAc,OAA4C;CACjE,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAiC,aAAa;AAEnD;AAEA,SAAS,oBAAoB,QAAmC;CAC9D,MAAM,UAAU,OAAO,OAAO,MAAM;CAEpC,IAAI,OAAO,oBAAoB,KAAA,KAAa,OAAO,oBAAoB,aACrE,OAAO,GAAG,OAAO,gBAAgB,IAAI;CAGvC,IAAI,OAAO,YAAY,KAAA,KAAa,OAAO,YAAY,WACrD,OAAO,GAAG,OAAO,QAAQ,IAAI;CAG/B,OAAO;AACT"}
@@ -0,0 +1,226 @@
1
+ import { CONTAINER_HOME, asRecord, extractTextContent, parseAgentJsonOutput, readString, readTokenCount } from "clanker-cleanroom";
2
+ import { access } from "node:fs/promises";
3
+ import { constants } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ //#region src/constants.ts
6
+ const CLAUDE_IMAGE = "clanker-cleanroom/claude";
7
+ const CLAUDE_CONTAINER_CREDENTIALS_PATH = `${CONTAINER_HOME}/.claude/.credentials.json`;
8
+ /** Long-lived OAuth token from `claude setup-token` (Claude subscription). */
9
+ const CLAUDE_OAUTH_TOKEN_ENV = "CLAUDE_CODE_OAUTH_TOKEN";
10
+ /** Anthropic API key (pay-as-you-go). */
11
+ const CLAUDE_API_KEY_ENV = "ANTHROPIC_API_KEY";
12
+ /** Linux hosts persist OAuth credentials here; macOS uses the Keychain instead. */
13
+ const defaultClaudeHostCredentialsFile = (home) => `${home}/.claude/.credentials.json`;
14
+ //#endregion
15
+ //#region src/credentials.ts
16
+ /** Env OAuth token, then env API key, then a readable host credentials file. */
17
+ async function resolveClaudeCredentials(options = {}) {
18
+ const env = options.env ?? process.env;
19
+ const home = options.home ?? homedir();
20
+ const token = env[CLAUDE_OAUTH_TOKEN_ENV];
21
+ if (token !== void 0 && token !== "") return {
22
+ kind: "oauth-token",
23
+ token
24
+ };
25
+ const apiKey = env[CLAUDE_API_KEY_ENV];
26
+ if (apiKey !== void 0 && apiKey !== "") return {
27
+ kind: "api-key",
28
+ apiKey
29
+ };
30
+ const file = defaultClaudeHostCredentialsFile(home);
31
+ try {
32
+ await access(file, constants.R_OK);
33
+ } catch {
34
+ throw new Error(`Claude Code credentials not found. Set ${CLAUDE_OAUTH_TOKEN_ENV} (run \`claude setup-token\` on the host) or ${CLAUDE_API_KEY_ENV}, or provide ${file} (Linux hosts; macOS keeps credentials in the Keychain).`);
35
+ }
36
+ return {
37
+ kind: "credentials-file",
38
+ file
39
+ };
40
+ }
41
+ /** Secret values for the docker CLI process, keyed by env names forwarded via envPassthrough. */
42
+ function credentialsEnv(credentials) {
43
+ switch (credentials.kind) {
44
+ case "oauth-token": return { [CLAUDE_OAUTH_TOKEN_ENV]: credentials.token };
45
+ case "api-key": return { [CLAUDE_API_KEY_ENV]: credentials.apiKey };
46
+ case "credentials-file": return {};
47
+ }
48
+ }
49
+ //#endregion
50
+ //#region src/trajectory.ts
51
+ function adaptClaudeEvents(rawEvents) {
52
+ const events = [];
53
+ for (const raw of rawEvents) {
54
+ const record = asRecord(raw);
55
+ if (record === void 0) {
56
+ events.push({
57
+ kind: "other",
58
+ type: "unknown",
59
+ raw
60
+ });
61
+ continue;
62
+ }
63
+ const type = readString(record.type) ?? "unknown";
64
+ switch (type) {
65
+ case "system":
66
+ events.push({
67
+ kind: "system",
68
+ sessionId: readString(record.session_id),
69
+ model: readString(record.model),
70
+ raw
71
+ });
72
+ break;
73
+ case "user":
74
+ events.push(...adaptClaudeUser(record, raw));
75
+ break;
76
+ case "assistant":
77
+ events.push(...adaptClaudeAssistant(record, raw));
78
+ break;
79
+ case "result":
80
+ events.push({
81
+ kind: "result",
82
+ text: readString(record.result) ?? "",
83
+ isError: record.is_error === true,
84
+ durationMs: readTokenCount(record.duration_ms),
85
+ costUsd: readTokenCount(record.total_cost_usd),
86
+ raw
87
+ });
88
+ break;
89
+ default: events.push({
90
+ kind: "other",
91
+ type,
92
+ raw
93
+ });
94
+ }
95
+ }
96
+ return events;
97
+ }
98
+ function adaptClaudeAssistant(record, raw) {
99
+ const events = [];
100
+ const text = extractTextContent(record.message);
101
+ if (text !== "") events.push({
102
+ kind: "assistant",
103
+ text,
104
+ raw
105
+ });
106
+ for (const block of contentBlocks(record.message)) {
107
+ if (block.type !== "tool_use") continue;
108
+ events.push({
109
+ kind: "tool",
110
+ phase: "started",
111
+ callId: readString(block.id) ?? "",
112
+ name: readString(block.name) ?? "unknown",
113
+ args: block.input ?? null,
114
+ result: null,
115
+ raw
116
+ });
117
+ }
118
+ if (events.length === 0) events.push({
119
+ kind: "assistant",
120
+ text: "",
121
+ raw
122
+ });
123
+ return events;
124
+ }
125
+ function adaptClaudeUser(record, raw) {
126
+ const events = [];
127
+ const text = extractTextContent(record.message);
128
+ const toolResults = [];
129
+ for (const block of contentBlocks(record.message)) {
130
+ if (block.type !== "tool_result") continue;
131
+ toolResults.push({
132
+ kind: "tool",
133
+ phase: "completed",
134
+ callId: readString(block.tool_use_id) ?? "",
135
+ name: "tool_result",
136
+ args: null,
137
+ result: block.content ?? null,
138
+ raw
139
+ });
140
+ }
141
+ if (text !== "" || toolResults.length === 0) events.push({
142
+ kind: "user",
143
+ text,
144
+ raw
145
+ });
146
+ events.push(...toolResults);
147
+ return events;
148
+ }
149
+ function contentBlocks(message) {
150
+ const content = asRecord(message)?.content;
151
+ if (!Array.isArray(content)) return [];
152
+ return content.map((block) => asRecord(block)).filter((block) => block !== void 0);
153
+ }
154
+ //#endregion
155
+ //#region src/binding.ts
156
+ const claudeBinding = {
157
+ image: CLAUDE_IMAGE,
158
+ displayName: "Claude",
159
+ trajectoryKind: "claude",
160
+ adaptEvents: adaptClaudeEvents,
161
+ command: ({ prompt, model }) => {
162
+ const claudeArgs = [
163
+ "claude",
164
+ "-p",
165
+ "--output-format",
166
+ "stream-json",
167
+ "--verbose",
168
+ "--dangerously-skip-permissions"
169
+ ];
170
+ if (model !== void 0 && model !== "") claudeArgs.push("--model", model);
171
+ claudeArgs.push("--", prompt);
172
+ return claudeArgs;
173
+ },
174
+ prepare: async () => {
175
+ const credentials = await resolveClaudeCredentials();
176
+ const volumes = [];
177
+ if (credentials.kind === "credentials-file") volumes.push({
178
+ host: credentials.file,
179
+ container: CLAUDE_CONTAINER_CREDENTIALS_PATH,
180
+ mode: "ro"
181
+ });
182
+ return {
183
+ volumes,
184
+ env: credentialsEnv(credentials)
185
+ };
186
+ },
187
+ parseResult: parseClaudeResult,
188
+ describeFailure: describeClaudeFailure
189
+ };
190
+ function parseClaudeResult(trajectory) {
191
+ const parsed = parseAgentJsonOutput(trajectory);
192
+ if (isErrorResult(parsed)) throw new Error(`Claude agent reported an error: ${describeErrorResult(parsed)}`);
193
+ const record = asRecord(parsed);
194
+ const usage = asRecord(record?.usage);
195
+ return {
196
+ durationMs: readTokenCount(record?.duration_ms),
197
+ costUsd: readTokenCount(record?.total_cost_usd),
198
+ usage: {
199
+ inputTokens: readTokenCount(usage?.input_tokens ?? usage?.inputTokens),
200
+ outputTokens: readTokenCount(usage?.output_tokens ?? usage?.outputTokens),
201
+ cacheReadTokens: readTokenCount(usage?.cache_read_input_tokens ?? usage?.cacheReadTokens),
202
+ cacheWriteTokens: readTokenCount(usage?.cache_creation_input_tokens ?? usage?.cacheWriteTokens)
203
+ }
204
+ };
205
+ }
206
+ function describeClaudeFailure(trajectory) {
207
+ try {
208
+ const parsed = parseAgentJsonOutput(trajectory);
209
+ return isErrorResult(parsed) ? describeErrorResult(parsed) : void 0;
210
+ } catch {
211
+ return;
212
+ }
213
+ }
214
+ function isErrorResult(value) {
215
+ return typeof value === "object" && value !== null && value.is_error === true;
216
+ }
217
+ function describeErrorResult(result) {
218
+ const message = String(result.result);
219
+ if (result.terminal_reason !== void 0 && result.terminal_reason !== "completed") return `${result.terminal_reason}: ${message}`;
220
+ if (result.subtype !== void 0 && result.subtype !== "success") return `${result.subtype}: ${message}`;
221
+ return message;
222
+ }
223
+ //#endregion
224
+ export { CLAUDE_API_KEY_ENV as a, CLAUDE_OAUTH_TOKEN_ENV as c, resolveClaudeCredentials as i, defaultClaudeHostCredentialsFile as l, adaptClaudeEvents as n, CLAUDE_CONTAINER_CREDENTIALS_PATH as o, credentialsEnv as r, CLAUDE_IMAGE as s, claudeBinding as t };
225
+
226
+ //# sourceMappingURL=binding-DAPBfelH.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding-DAPBfelH.mjs","names":["fsConstants"],"sources":["../src/constants.ts","../src/credentials.ts","../src/trajectory.ts","../src/binding.ts"],"sourcesContent":["import { CONTAINER_HOME } from \"clanker-cleanroom\";\n\nexport const CLAUDE_IMAGE = \"clanker-cleanroom/claude\";\nexport const CLAUDE_CONTAINER_CREDENTIALS_PATH = `${CONTAINER_HOME}/.claude/.credentials.json`;\n\n/** Long-lived OAuth token from `claude setup-token` (Claude subscription). */\nexport const CLAUDE_OAUTH_TOKEN_ENV = \"CLAUDE_CODE_OAUTH_TOKEN\";\n/** Anthropic API key (pay-as-you-go). */\nexport const CLAUDE_API_KEY_ENV = \"ANTHROPIC_API_KEY\";\n\n/** Linux hosts persist OAuth credentials here; macOS uses the Keychain instead. */\nexport const defaultClaudeHostCredentialsFile = (home: string): string =>\n `${home}/.claude/.credentials.json`;\n","import { access } from \"node:fs/promises\";\nimport { constants as fsConstants } from \"node:fs\";\nimport { homedir } from \"node:os\";\n\nimport {\n CLAUDE_API_KEY_ENV,\n CLAUDE_OAUTH_TOKEN_ENV,\n defaultClaudeHostCredentialsFile,\n} from \"./constants.js\";\n\n/**\n * How Claude Code authenticates inside the container. Env-backed kinds are forwarded\n * to `docker run` by name (never on argv); the file kind is bind-mounted read-only.\n * See `constants.ts` for which host setups produce each.\n */\nexport type ClaudeCredentials =\n | { kind: \"oauth-token\"; token: string }\n | { kind: \"api-key\"; apiKey: string }\n | { kind: \"credentials-file\"; file: string };\n\n/** Env OAuth token, then env API key, then a readable host credentials file. */\nexport async function resolveClaudeCredentials(\n options: { env?: NodeJS.ProcessEnv; home?: string } = {},\n): Promise<ClaudeCredentials> {\n const env = options.env ?? process.env;\n const home = options.home ?? homedir();\n\n const token = env[CLAUDE_OAUTH_TOKEN_ENV];\n if (token !== undefined && token !== \"\") {\n return { kind: \"oauth-token\", token };\n }\n\n const apiKey = env[CLAUDE_API_KEY_ENV];\n if (apiKey !== undefined && apiKey !== \"\") {\n return { kind: \"api-key\", apiKey };\n }\n\n const file = defaultClaudeHostCredentialsFile(home);\n try {\n await access(file, fsConstants.R_OK);\n } catch {\n throw new Error(\n `Claude Code credentials not found. Set ${CLAUDE_OAUTH_TOKEN_ENV} (run \\`claude setup-token\\` on the host) ` +\n `or ${CLAUDE_API_KEY_ENV}, or provide ${file} (Linux hosts; macOS keeps credentials in the Keychain).`,\n );\n }\n\n return { kind: \"credentials-file\", file };\n}\n\n/** Secret values for the docker CLI process, keyed by env names forwarded via envPassthrough. */\nexport function credentialsEnv(credentials: ClaudeCredentials): Record<string, string> {\n switch (credentials.kind) {\n case \"oauth-token\":\n return { [CLAUDE_OAUTH_TOKEN_ENV]: credentials.token };\n case \"api-key\":\n return { [CLAUDE_API_KEY_ENV]: credentials.apiKey };\n case \"credentials-file\":\n return {};\n }\n}\n","import {\n asRecord,\n extractTextContent,\n readString,\n readTokenCount,\n type TrajectoryEvent,\n} from \"clanker-cleanroom\";\n\nexport function adaptClaudeEvents(rawEvents: readonly unknown[]): TrajectoryEvent[] {\n const events: TrajectoryEvent[] = [];\n\n for (const raw of rawEvents) {\n const record = asRecord(raw);\n if (record === undefined) {\n events.push({ kind: \"other\", type: \"unknown\", raw });\n continue;\n }\n\n const type = readString(record.type) ?? \"unknown\";\n\n switch (type) {\n case \"system\":\n events.push({\n kind: \"system\",\n sessionId: readString(record.session_id),\n model: readString(record.model),\n raw,\n });\n break;\n case \"user\":\n events.push(...adaptClaudeUser(record, raw));\n break;\n case \"assistant\":\n events.push(...adaptClaudeAssistant(record, raw));\n break;\n case \"result\":\n events.push({\n kind: \"result\",\n text: readString(record.result) ?? \"\",\n isError: record.is_error === true,\n durationMs: readTokenCount(record.duration_ms),\n costUsd: readTokenCount(record.total_cost_usd),\n raw,\n });\n break;\n default:\n events.push({ kind: \"other\", type, raw });\n }\n }\n\n return events;\n}\n\nfunction adaptClaudeAssistant(\n record: Record<string, unknown>,\n raw: unknown,\n): TrajectoryEvent[] {\n const events: TrajectoryEvent[] = [];\n const text = extractTextContent(record.message);\n if (text !== \"\") {\n events.push({ kind: \"assistant\", text, raw });\n }\n\n for (const block of contentBlocks(record.message)) {\n if (block.type !== \"tool_use\") {\n continue;\n }\n events.push({\n kind: \"tool\",\n phase: \"started\",\n callId: readString(block.id) ?? \"\",\n name: readString(block.name) ?? \"unknown\",\n args: block.input ?? null,\n result: null,\n raw,\n });\n }\n\n if (events.length === 0) {\n events.push({ kind: \"assistant\", text: \"\", raw });\n }\n\n return events;\n}\n\nfunction adaptClaudeUser(record: Record<string, unknown>, raw: unknown): TrajectoryEvent[] {\n const events: TrajectoryEvent[] = [];\n const text = extractTextContent(record.message);\n const toolResults: TrajectoryEvent[] = [];\n\n for (const block of contentBlocks(record.message)) {\n if (block.type !== \"tool_result\") {\n continue;\n }\n toolResults.push({\n kind: \"tool\",\n phase: \"completed\",\n callId: readString(block.tool_use_id) ?? \"\",\n name: \"tool_result\",\n args: null,\n result: block.content ?? null,\n raw,\n });\n }\n\n if (text !== \"\" || toolResults.length === 0) {\n events.push({ kind: \"user\", text, raw });\n }\n events.push(...toolResults);\n return events;\n}\n\nfunction contentBlocks(message: unknown): Record<string, unknown>[] {\n const content = asRecord(message)?.content;\n if (!Array.isArray(content)) {\n return [];\n }\n return content.map((block) => asRecord(block)).filter((block) => block !== undefined);\n}\n","import {\n asRecord,\n parseAgentJsonOutput,\n readTokenCount,\n type AgentBinding,\n type AgentRunResult,\n type DockerVolumeMount,\n} from \"clanker-cleanroom\";\n\nimport { CLAUDE_CONTAINER_CREDENTIALS_PATH, CLAUDE_IMAGE } from \"./constants.js\";\nimport { credentialsEnv, resolveClaudeCredentials } from \"./credentials.js\";\nimport { adaptClaudeEvents } from \"./trajectory.js\";\n\nexport const claudeBinding: AgentBinding = {\n image: CLAUDE_IMAGE,\n displayName: \"Claude\",\n trajectoryKind: \"claude\",\n adaptEvents: adaptClaudeEvents,\n command: ({ prompt, model }) => {\n const claudeArgs = [\n \"claude\",\n \"-p\",\n \"--output-format\",\n \"stream-json\",\n \"--verbose\",\n \"--dangerously-skip-permissions\",\n ];\n if (model !== undefined && model !== \"\") {\n claudeArgs.push(\"--model\", model);\n }\n claudeArgs.push(\"--\", prompt);\n return claudeArgs;\n },\n prepare: async () => {\n const credentials = await resolveClaudeCredentials();\n const volumes: DockerVolumeMount[] = [];\n if (credentials.kind === \"credentials-file\") {\n volumes.push({\n host: credentials.file,\n container: CLAUDE_CONTAINER_CREDENTIALS_PATH,\n mode: \"ro\",\n });\n }\n return {\n volumes,\n env: credentialsEnv(credentials),\n };\n },\n parseResult: parseClaudeResult,\n describeFailure: describeClaudeFailure,\n};\n\nfunction parseClaudeResult(trajectory: string): AgentRunResult {\n const parsed = parseAgentJsonOutput(trajectory);\n if (isErrorResult(parsed)) {\n throw new Error(`Claude agent reported an error: ${describeErrorResult(parsed)}`);\n }\n\n const record = asRecord(parsed);\n const usage = asRecord(record?.usage);\n\n return {\n durationMs: readTokenCount(record?.duration_ms),\n costUsd: readTokenCount(record?.total_cost_usd),\n usage: {\n inputTokens: readTokenCount(usage?.input_tokens ?? usage?.inputTokens),\n outputTokens: readTokenCount(usage?.output_tokens ?? usage?.outputTokens),\n cacheReadTokens: readTokenCount(usage?.cache_read_input_tokens ?? usage?.cacheReadTokens),\n cacheWriteTokens: readTokenCount(\n usage?.cache_creation_input_tokens ?? usage?.cacheWriteTokens,\n ),\n },\n };\n}\n\nfunction describeClaudeFailure(trajectory: string): string | undefined {\n try {\n const parsed = parseAgentJsonOutput(trajectory);\n return isErrorResult(parsed) ? describeErrorResult(parsed) : undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ClaudeErrorResult = {\n is_error: true;\n result: string;\n subtype?: string;\n terminal_reason?: string;\n};\n\nfunction isErrorResult(value: unknown): value is ClaudeErrorResult {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { is_error?: unknown }).is_error === true\n );\n}\n\nfunction describeErrorResult(result: ClaudeErrorResult): string {\n const message = String(result.result);\n\n if (result.terminal_reason !== undefined && result.terminal_reason !== \"completed\") {\n return `${result.terminal_reason}: ${message}`;\n }\n\n if (result.subtype !== undefined && result.subtype !== \"success\") {\n return `${result.subtype}: ${message}`;\n }\n\n return message;\n}\n"],"mappings":"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"}