@ai-sdk/harness-claude-code 1.0.137 → 1.0.139

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/index.js CHANGED
@@ -1,2014 +1,1742 @@
1
- // src/claude-code-harness.ts
2
- import { posix } from "path";
3
- import {
4
- commonTool,
5
- HARNESS_V1_BUILTIN_TOOLS,
6
- harnessV1DiagnosticFromBridgeFrame,
7
- HarnessCapabilityUnsupportedError,
8
- harnessStateDirectoryPath,
9
- harnessSessionDataDirectoryPath
10
- } from "@ai-sdk/harness";
11
- import {
12
- applyCredentialForwarding,
13
- classifyDiskLog,
14
- resolveSandboxCredentialEnvironment,
15
- createBridgeToken,
16
- experimental_createBridgeUserMessageSubmitter,
17
- createBridgeErrorHandler,
18
- createBridgeStartupError,
19
- drainBridgeProcessStream,
20
- forwardBridgeProcessStream,
21
- getRestrictedSandboxSession,
22
- markBridgeStarting,
23
- resolveSandboxHomeDir,
24
- SandboxChannel,
25
- shellQuote,
26
- sleep,
27
- warnCredentialBrokeringUnavailable,
28
- waitForBridgeReady,
29
- withBridgeToken,
30
- writeSkills
31
- } from "@ai-sdk/harness/utils";
32
- import {
33
- safeParseJSON as safeParseJSON2,
34
- tool
35
- } from "@ai-sdk/provider-utils";
1
+ import { dirname, join, posix, resolve } from "node:path";
2
+ import { HARNESS_V1_BUILTIN_TOOLS, HarnessCapabilityUnsupportedError, commonTool, harnessSessionDataDirectoryPath, harnessStateDirectoryPath, harnessV1BridgeInboundCommandSchemas, harnessV1BridgeOutboundMessageSchema, harnessV1BridgeStartBaseSchema, harnessV1DiagnosticFromBridgeFrame } from "@ai-sdk/harness";
3
+ import { SandboxChannel, applyCredentialForwarding, classifyDiskLog, createBridgeErrorHandler, createBridgeStartupError, createBridgeToken, createCredentialRequestTransformation, createReadBridgeAsset, drainBridgeProcessStream, experimental_createBridgeUserMessageSubmitter, forwardBridgeProcessStream, getAiGatewayAuthFromEnv, getRestrictedSandboxSession, isAccessTokenExpiringSoon, isHarnessAuthenticationEnvironment, isMacOS, markBridgeStarting, readMacOSKeychainPassword, refreshOAuthAccessToken, resolveSandboxCredentialEnvironment, resolveSandboxHomeDir, shellQuote, shouldResolveNativeSubscription, sleep, waitForBridgeReady, warnCredentialBrokeringUnavailable, withBridgeToken, writeSkills } from "@ai-sdk/harness/utils";
4
+ import { isRecord, safeParseJSON, tool } from "@ai-sdk/provider-utils";
36
5
  import { WebSocket } from "ws";
37
- import { z as z2 } from "zod/v4";
38
-
39
- // src/claude-code-bootstrap.ts
40
- import { createReadBridgeAsset } from "@ai-sdk/harness/utils";
41
- var readBridgeAsset = createReadBridgeAsset({
42
- "package.json": new URL("./bridge/package.json", import.meta.url),
43
- "pnpm-lock.yaml": new URL("./bridge/pnpm-lock.yaml", import.meta.url),
44
- "pnpm-workspace.yaml": new URL(
45
- "./bridge/pnpm-workspace.yaml",
46
- import.meta.url
47
- ),
48
- "index.mjs": new URL("./bridge/index.mjs", import.meta.url)
6
+ import { z } from "zod/v4";
7
+ import { execFileSync } from "node:child_process";
8
+ import { readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
11
+ //#region src/claude-code-bootstrap.ts
12
+ const readBridgeAsset = createReadBridgeAsset({
13
+ "package.json": new URL("./bridge/package.json", import.meta.url),
14
+ "pnpm-lock.yaml": new URL("./bridge/pnpm-lock.yaml", import.meta.url),
15
+ "pnpm-workspace.yaml": new URL("./bridge/pnpm-workspace.yaml", import.meta.url),
16
+ "index.mjs": new URL("./bridge/index.mjs", import.meta.url)
49
17
  });
50
- var CLAUDE_CODE_BOOTSTRAP_DIR = ".harness-bootstrap/claude-code";
51
- var cachedBootstrap;
18
+ const CLAUDE_CODE_BOOTSTRAP_DIR = ".harness-bootstrap/claude-code";
19
+ let cachedBootstrap;
52
20
  async function getClaudeCodeBootstrap() {
53
- if (cachedBootstrap != null) return cachedBootstrap;
54
- const [pkg, lock, workspace, bridge] = await Promise.all([
55
- readBridgeAsset("package.json"),
56
- readBridgeAsset("pnpm-lock.yaml"),
57
- readBridgeAsset("pnpm-workspace.yaml"),
58
- readBridgeAsset("index.mjs")
59
- ]);
60
- cachedBootstrap = {
61
- harnessId: "claude-code",
62
- bootstrapDir: CLAUDE_CODE_BOOTSTRAP_DIR,
63
- files: [
64
- { path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/package.json`, content: pkg },
65
- { path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
66
- {
67
- path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/pnpm-workspace.yaml`,
68
- content: workspace
69
- },
70
- { path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge }
71
- ],
72
- commands: [
73
- {
74
- command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
75
- },
76
- {
77
- command: "./node_modules/.bin/claude --version"
78
- }
79
- ]
80
- };
81
- return cachedBootstrap;
21
+ if (cachedBootstrap != null) return cachedBootstrap;
22
+ const [pkg, lock, workspace, bridge] = await Promise.all([
23
+ readBridgeAsset("package.json"),
24
+ readBridgeAsset("pnpm-lock.yaml"),
25
+ readBridgeAsset("pnpm-workspace.yaml"),
26
+ readBridgeAsset("index.mjs")
27
+ ]);
28
+ cachedBootstrap = {
29
+ harnessId: "claude-code",
30
+ bootstrapDir: CLAUDE_CODE_BOOTSTRAP_DIR,
31
+ files: [
32
+ {
33
+ path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/package.json`,
34
+ content: pkg
35
+ },
36
+ {
37
+ path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/pnpm-lock.yaml`,
38
+ content: lock
39
+ },
40
+ {
41
+ path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/pnpm-workspace.yaml`,
42
+ content: workspace
43
+ },
44
+ {
45
+ path: `${CLAUDE_CODE_BOOTSTRAP_DIR}/bridge.mjs`,
46
+ content: bridge
47
+ }
48
+ ],
49
+ commands: [{ command: "pnpm install --frozen-lockfile --store-dir .pnpm-store" }, { command: "./node_modules/.bin/claude --version" }]
50
+ };
51
+ return cachedBootstrap;
82
52
  }
83
-
84
- // src/claude-code-auth.ts
85
- import { execFileSync } from "child_process";
86
- import { readFileSync } from "fs";
87
- import { homedir } from "os";
88
- import { join } from "path";
89
- import {
90
- createCredentialRequestTransformation,
91
- getAiGatewayAuthFromEnv,
92
- isHarnessAuthenticationEnvironment
93
- } from "@ai-sdk/harness/utils";
94
- var CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
95
- "AI_GATEWAY_API_KEY",
96
- "ANTHROPIC_API_KEY",
97
- "ANTHROPIC_AUTH_TOKEN",
98
- "CLAUDE_CODE_OAUTH_TOKEN"
53
+ //#endregion
54
+ //#region src/claude-code-auth.ts
55
+ const CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
56
+ "AI_GATEWAY_API_KEY",
57
+ "ANTHROPIC_API_KEY",
58
+ "ANTHROPIC_AUTH_TOKEN",
59
+ "CLAUDE_CODE_OAUTH_TOKEN"
99
60
  ];
100
- function createClaudeCodeRequestTransformations({
101
- env: environment,
102
- sandboxEnv: sandboxEnvironment,
103
- auth: authenticationMode
104
- }) {
105
- const matchUrl = authenticationMode === "ai-gateway" ? environment.ANTHROPIC_BASE_URL : environment.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
106
- const transformations = [];
107
- if (environment.ANTHROPIC_API_KEY != null && sandboxEnvironment.ANTHROPIC_API_KEY != null) {
108
- transformations.push(
109
- createCredentialRequestTransformation({
110
- matchUrl,
111
- matchHeaders: {
112
- "x-api-key": sandboxEnvironment.ANTHROPIC_API_KEY
113
- },
114
- transformHeaders: {
115
- "x-api-key": environment.ANTHROPIC_API_KEY
116
- }
117
- })
118
- );
119
- }
120
- if (environment.ANTHROPIC_AUTH_TOKEN != null && sandboxEnvironment.ANTHROPIC_AUTH_TOKEN != null) {
121
- transformations.push(
122
- createCredentialRequestTransformation({
123
- matchUrl,
124
- matchHeaders: {
125
- Authorization: `Bearer ${sandboxEnvironment.ANTHROPIC_AUTH_TOKEN}`
126
- },
127
- transformHeaders: {
128
- Authorization: `Bearer ${environment.ANTHROPIC_AUTH_TOKEN}`
129
- }
130
- })
131
- );
132
- }
133
- return transformations;
61
+ function createClaudeCodeRequestTransformations({ env: environment, sandboxEnv: sandboxEnvironment, auth: authenticationMode }) {
62
+ const matchUrl = authenticationMode === "ai-gateway" ? environment.ANTHROPIC_BASE_URL : environment.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
63
+ const transformations = [];
64
+ if (environment.ANTHROPIC_API_KEY != null && sandboxEnvironment.ANTHROPIC_API_KEY != null) transformations.push(createCredentialRequestTransformation({
65
+ matchUrl,
66
+ matchHeaders: { "x-api-key": sandboxEnvironment.ANTHROPIC_API_KEY },
67
+ transformHeaders: { "x-api-key": environment.ANTHROPIC_API_KEY }
68
+ }));
69
+ if (environment.ANTHROPIC_AUTH_TOKEN != null && sandboxEnvironment.ANTHROPIC_AUTH_TOKEN != null) transformations.push(createCredentialRequestTransformation({
70
+ matchUrl,
71
+ matchHeaders: { Authorization: `Bearer ${sandboxEnvironment.ANTHROPIC_AUTH_TOKEN}` },
72
+ transformHeaders: { Authorization: `Bearer ${environment.ANTHROPIC_AUTH_TOKEN}` }
73
+ }));
74
+ return transformations;
134
75
  }
135
76
  function resolveClaudeCodeEnv(auth, processEnv = process.env, options = {}) {
136
- const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
137
- const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
138
- const readApiKey = suppliedEnvironment || auth === "direct" ? () => void 0 : options.readApiKeyHelper ?? readApiKeyHelper;
139
- if (auth === "direct") {
140
- return pickAnthropic({
141
- processEnv: authenticationEnvironment,
142
- readApiKey
143
- });
144
- }
145
- const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
146
- env: authenticationEnvironment
147
- });
148
- if (auth === "ai-gateway" || gatewayAuthFromEnv.apiKey) {
149
- return pickGateway({ gatewayAuthFromEnv });
150
- }
151
- return pickAnthropic({
152
- processEnv: authenticationEnvironment,
153
- readApiKey
154
- });
77
+ const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
78
+ const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
79
+ const readApiKey = suppliedEnvironment || auth === "direct" ? () => void 0 : options.readApiKeyHelper ?? readApiKeyHelper;
80
+ if (auth === "direct") return pickAnthropic({
81
+ processEnv: authenticationEnvironment,
82
+ readApiKey
83
+ });
84
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({ env: authenticationEnvironment });
85
+ if (auth === "ai-gateway" || gatewayAuthFromEnv.apiKey) return pickGateway({ gatewayAuthFromEnv });
86
+ return pickAnthropic({
87
+ processEnv: authenticationEnvironment,
88
+ readApiKey
89
+ });
155
90
  }
156
91
  function resolveClaudeCodeAuthenticationMode(auth, processEnv = process.env) {
157
- if (isHarnessAuthenticationEnvironment(auth)) {
158
- return getAiGatewayAuthFromEnv({ env: auth }).apiKey ? "ai-gateway" : "direct";
159
- }
160
- if (auth === "direct") {
161
- return "direct";
162
- }
163
- if (auth === "ai-gateway") {
164
- return "ai-gateway";
165
- }
166
- return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey ? "ai-gateway" : "direct";
92
+ if (isHarnessAuthenticationEnvironment(auth)) return getAiGatewayAuthFromEnv({ env: auth }).apiKey ? "ai-gateway" : "direct";
93
+ if (auth === "direct") return "direct";
94
+ if (auth === "ai-gateway") return "ai-gateway";
95
+ return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey ? "ai-gateway" : "direct";
167
96
  }
168
- function pickAnthropic({
169
- processEnv,
170
- readApiKey
171
- }) {
172
- const env = {};
173
- const helperKey = readApiKey();
174
- const apiKey = processEnv.ANTHROPIC_API_KEY ?? helperKey;
175
- const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ?? helperKey;
176
- const oauthToken = processEnv.CLAUDE_CODE_OAUTH_TOKEN;
177
- if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
178
- if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
179
- if (oauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
180
- const baseUrl = processEnv.ANTHROPIC_BASE_URL;
181
- if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
182
- return env;
97
+ function pickAnthropic({ processEnv, readApiKey }) {
98
+ const env = {};
99
+ const helperKey = readApiKey();
100
+ const apiKey = processEnv.ANTHROPIC_API_KEY ?? helperKey;
101
+ const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ?? helperKey;
102
+ const oauthToken = processEnv.CLAUDE_CODE_OAUTH_TOKEN;
103
+ if (apiKey) env.ANTHROPIC_API_KEY = apiKey;
104
+ if (authToken) env.ANTHROPIC_AUTH_TOKEN = authToken;
105
+ if (oauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
106
+ const baseUrl = processEnv.ANTHROPIC_BASE_URL;
107
+ if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl;
108
+ return env;
183
109
  }
110
+ /**
111
+ * Read the `apiKeyHelper` setting from `~/.claude/settings.json` and run
112
+ * it. The `claude` CLI uses this hook to fetch credentials from password
113
+ * managers and similar tools; mirroring it here lets users with that
114
+ * setup run the harness without having to set `ANTHROPIC_API_KEY`
115
+ * explicitly.
116
+ */
184
117
  function readApiKeyHelper() {
185
- const home = homedir();
186
- if (!home) return void 0;
187
- let raw;
188
- try {
189
- raw = readFileSync(join(home, ".claude", "settings.json"), "utf8");
190
- } catch {
191
- return void 0;
192
- }
193
- let settings;
194
- try {
195
- settings = JSON.parse(raw);
196
- } catch {
197
- return void 0;
198
- }
199
- const command = settings.apiKeyHelper;
200
- if (typeof command !== "string" || command.length === 0) return void 0;
201
- try {
202
- const output = execFileSync("sh", ["-c", command], {
203
- encoding: "utf8",
204
- stdio: ["ignore", "pipe", "ignore"]
205
- });
206
- const trimmed = output.trim();
207
- return trimmed.length > 0 ? trimmed : void 0;
208
- } catch {
209
- return void 0;
210
- }
118
+ const home = homedir();
119
+ if (!home) return void 0;
120
+ let raw;
121
+ try {
122
+ raw = readFileSync(join(home, ".claude", "settings.json"), "utf8");
123
+ } catch {
124
+ return;
125
+ }
126
+ let settings;
127
+ try {
128
+ settings = JSON.parse(raw);
129
+ } catch {
130
+ return;
131
+ }
132
+ const command = settings.apiKeyHelper;
133
+ if (typeof command !== "string" || command.length === 0) return void 0;
134
+ try {
135
+ const trimmed = execFileSync("sh", ["-c", command], {
136
+ encoding: "utf8",
137
+ stdio: [
138
+ "ignore",
139
+ "pipe",
140
+ "ignore"
141
+ ]
142
+ }).trim();
143
+ return trimmed.length > 0 ? trimmed : void 0;
144
+ } catch {
145
+ return;
146
+ }
211
147
  }
212
- function pickGateway({
213
- gatewayAuthFromEnv
214
- }) {
215
- const apiKey = gatewayAuthFromEnv.apiKey;
216
- const baseUrl = gatewayAuthFromEnv.baseUrl;
217
- const env = {};
218
- if (apiKey) {
219
- env.AI_GATEWAY_API_KEY = apiKey;
220
- env.ANTHROPIC_API_KEY = apiKey;
221
- }
222
- env.AI_GATEWAY_BASE_URL = baseUrl;
223
- env.ANTHROPIC_BASE_URL = baseUrl;
224
- return env;
148
+ function pickGateway({ gatewayAuthFromEnv }) {
149
+ const apiKey = gatewayAuthFromEnv.apiKey;
150
+ const baseUrl = gatewayAuthFromEnv.baseUrl;
151
+ const env = {};
152
+ if (apiKey) {
153
+ env.AI_GATEWAY_API_KEY = apiKey;
154
+ env.ANTHROPIC_API_KEY = apiKey;
155
+ }
156
+ env.AI_GATEWAY_BASE_URL = baseUrl;
157
+ env.ANTHROPIC_BASE_URL = baseUrl;
158
+ return env;
225
159
  }
226
-
227
- // src/claude-code-subscription.ts
228
- import { execFileSync as execFileSync2 } from "child_process";
229
- import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
230
- import { homedir as homedir2 } from "os";
231
- import { dirname, join as join2, resolve } from "path";
232
- import {
233
- createCredentialRequestTransformation as createCredentialRequestTransformation2,
234
- isAccessTokenExpiringSoon,
235
- isHarnessAuthenticationEnvironment as isHarnessAuthenticationEnvironment2,
236
- isMacOS,
237
- readMacOSKeychainPassword,
238
- refreshOAuthAccessToken,
239
- shouldResolveNativeSubscription
240
- } from "@ai-sdk/harness/utils";
241
- import { isRecord, safeParseJSON } from "@ai-sdk/provider-utils";
242
- var CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
243
- var CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
244
- function createClaudeCodeSubscriptionRequestTransformations({
245
- env: environment,
246
- sandboxEnv: sandboxEnvironment,
247
- auth: authenticationMode
248
- }) {
249
- if (environment.CLAUDE_CODE_OAUTH_TOKEN == null || sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN == null) {
250
- return [];
251
- }
252
- return [
253
- createCredentialRequestTransformation2({
254
- matchUrl: authenticationMode === "ai-gateway" ? environment.ANTHROPIC_BASE_URL : environment.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
255
- matchHeaders: {
256
- Authorization: `Bearer ${sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN}`
257
- },
258
- transformHeaders: {
259
- Authorization: `Bearer ${environment.CLAUDE_CODE_OAUTH_TOKEN}`
260
- }
261
- })
262
- ];
160
+ //#endregion
161
+ //#region src/claude-code-subscription.ts
162
+ const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
163
+ const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
164
+ function createClaudeCodeSubscriptionRequestTransformations({ env: environment, sandboxEnv: sandboxEnvironment, auth: authenticationMode }) {
165
+ if (environment.CLAUDE_CODE_OAUTH_TOKEN == null || sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN == null) return [];
166
+ return [createCredentialRequestTransformation({
167
+ matchUrl: authenticationMode === "ai-gateway" ? environment.ANTHROPIC_BASE_URL : environment.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
168
+ matchHeaders: { Authorization: `Bearer ${sandboxEnvironment.CLAUDE_CODE_OAUTH_TOKEN}` },
169
+ transformHeaders: { Authorization: `Bearer ${environment.CLAUDE_CODE_OAUTH_TOKEN}` }
170
+ })];
263
171
  }
264
- async function resolveClaudeCodeAuthentication({
265
- auth,
266
- processEnv = process.env,
267
- options = {},
268
- readSubscription = readClaudeCodeSubscription
269
- }) {
270
- const environmentWithoutHelper = resolveClaudeCodeEnv(auth, processEnv, {
271
- readApiKeyHelper: () => void 0
272
- });
273
- if (isHarnessAuthenticationEnvironment2(auth)) {
274
- return environmentWithoutHelper;
275
- }
276
- if (!shouldResolveNativeSubscription({
277
- auth,
278
- env: environmentWithoutHelper,
279
- hasDirectCredential: environmentWithoutHelper.ANTHROPIC_API_KEY != null || environmentWithoutHelper.ANTHROPIC_AUTH_TOKEN != null || environmentWithoutHelper.CLAUDE_CODE_OAUTH_TOKEN != null
280
- })) {
281
- return environmentWithoutHelper;
282
- }
283
- return await readSubscription() ?? resolveClaudeCodeEnv(auth, processEnv, options);
172
+ async function resolveClaudeCodeAuthentication({ auth, processEnv = process.env, options = {}, readSubscription = readClaudeCodeSubscription }) {
173
+ const environmentWithoutHelper = resolveClaudeCodeEnv(auth, processEnv, { readApiKeyHelper: () => void 0 });
174
+ if (isHarnessAuthenticationEnvironment(auth)) return environmentWithoutHelper;
175
+ if (!shouldResolveNativeSubscription({
176
+ auth,
177
+ env: environmentWithoutHelper,
178
+ hasDirectCredential: environmentWithoutHelper.ANTHROPIC_API_KEY != null || environmentWithoutHelper.ANTHROPIC_AUTH_TOKEN != null || environmentWithoutHelper.CLAUDE_CODE_OAUTH_TOKEN != null
179
+ })) return environmentWithoutHelper;
180
+ return await readSubscription() ?? resolveClaudeCodeEnv(auth, processEnv, options);
284
181
  }
285
- async function readClaudeCodeSubscription({
286
- env = process.env,
287
- homeDirectory = homedir2(),
288
- platform = process.platform,
289
- fetch
290
- } = {}) {
291
- const configDirectory = resolve(
292
- env.CLAUDE_CONFIG_DIR ?? join2(homeDirectory, ".claude")
293
- );
294
- const credentialPath = join2(configDirectory, ".credentials.json");
295
- const stored = await readClaudeCredentialStore({
296
- credentialPath,
297
- homeDirectory,
298
- configDirectory,
299
- platform
300
- });
301
- if (stored == null) return void 0;
302
- const oauth = getClaudeOAuthCredential({ value: stored.value });
303
- if (oauth == null) return void 0;
304
- const { accessToken, refreshToken, expiresAt } = oauth;
305
- let resolvedAccessToken = accessToken;
306
- if (isAccessTokenExpiringSoon({ expiresAt })) {
307
- const refreshed = await refreshOAuthAccessToken({
308
- tokenUrl: CLAUDE_TOKEN_URL,
309
- clientId: CLAUDE_CLIENT_ID,
310
- refreshToken,
311
- requestFormat: "json",
312
- ...fetch == null ? {} : { fetch }
313
- });
314
- resolvedAccessToken = refreshed.accessToken;
315
- await stored.write({
316
- ...stored.value,
317
- claudeAiOauth: {
318
- ...oauth,
319
- accessToken: refreshed.accessToken,
320
- refreshToken: refreshed.refreshToken ?? refreshToken,
321
- expiresAt: refreshed.expiresAt
322
- }
323
- });
324
- }
325
- return {
326
- CLAUDE_CODE_OAUTH_TOKEN: resolvedAccessToken,
327
- ANTHROPIC_BASE_URL: "https://api.anthropic.com"
328
- };
182
+ async function readClaudeCodeSubscription({ env = process.env, homeDirectory = homedir(), platform = process.platform, fetch } = {}) {
183
+ const configDirectory = resolve(env.CLAUDE_CONFIG_DIR ?? join(homeDirectory, ".claude"));
184
+ const stored = await readClaudeCredentialStore({
185
+ credentialPath: join(configDirectory, ".credentials.json"),
186
+ homeDirectory,
187
+ configDirectory,
188
+ platform
189
+ });
190
+ if (stored == null) return void 0;
191
+ const oauth = getClaudeOAuthCredential({ value: stored.value });
192
+ if (oauth == null) return void 0;
193
+ const { accessToken, refreshToken, expiresAt } = oauth;
194
+ let resolvedAccessToken = accessToken;
195
+ if (isAccessTokenExpiringSoon({ expiresAt })) {
196
+ const refreshed = await refreshOAuthAccessToken({
197
+ tokenUrl: CLAUDE_TOKEN_URL,
198
+ clientId: CLAUDE_CLIENT_ID,
199
+ refreshToken,
200
+ requestFormat: "json",
201
+ ...fetch == null ? {} : { fetch }
202
+ });
203
+ resolvedAccessToken = refreshed.accessToken;
204
+ await stored.write({
205
+ ...stored.value,
206
+ claudeAiOauth: {
207
+ ...oauth,
208
+ accessToken: refreshed.accessToken,
209
+ refreshToken: refreshed.refreshToken ?? refreshToken,
210
+ expiresAt: refreshed.expiresAt
211
+ }
212
+ });
213
+ }
214
+ return {
215
+ CLAUDE_CODE_OAUTH_TOKEN: resolvedAccessToken,
216
+ ANTHROPIC_BASE_URL: "https://api.anthropic.com"
217
+ };
329
218
  }
330
- async function readClaudeCredentialStore({
331
- credentialPath,
332
- homeDirectory,
333
- configDirectory,
334
- platform
335
- }) {
336
- const text = await readFile(credentialPath, "utf8").catch(() => void 0);
337
- if (text != null) {
338
- const value2 = await parseClaudeCredentialFile(text);
339
- if (value2 != null && getClaudeOAuthCredential({ value: value2 }) != null) {
340
- return {
341
- value: value2,
342
- write: (updated) => writeClaudeCredentialFile({ credentialPath, value: updated })
343
- };
344
- }
345
- }
346
- if (!isMacOS(platform)) return void 0;
347
- if (configDirectory !== resolve(join2(homeDirectory, ".claude"))) {
348
- return void 0;
349
- }
350
- const service = "Claude Code-credentials";
351
- const keychainText = await readMacOSKeychainPassword({
352
- service,
353
- account: process.env.USER ?? ""
354
- });
355
- if (keychainText == null) return void 0;
356
- const value = await parseClaudeCredentialFile(keychainText);
357
- if (value == null) return void 0;
358
- return {
359
- value,
360
- write: async (updated) => {
361
- execFileSync2(
362
- "/usr/bin/security",
363
- [
364
- "add-generic-password",
365
- "-U",
366
- "-s",
367
- service,
368
- "-a",
369
- process.env.USER ?? "",
370
- "-w",
371
- JSON.stringify(updated)
372
- ],
373
- { stdio: "ignore" }
374
- );
375
- }
376
- };
219
+ async function readClaudeCredentialStore({ credentialPath, homeDirectory, configDirectory, platform }) {
220
+ const text = await readFile(credentialPath, "utf8").catch(() => void 0);
221
+ if (text != null) {
222
+ const value = await parseClaudeCredentialFile(text);
223
+ if (value != null && getClaudeOAuthCredential({ value }) != null) return {
224
+ value,
225
+ write: (updated) => writeClaudeCredentialFile({
226
+ credentialPath,
227
+ value: updated
228
+ })
229
+ };
230
+ }
231
+ if (!isMacOS(platform)) return void 0;
232
+ if (configDirectory !== resolve(join(homeDirectory, ".claude"))) return;
233
+ const service = "Claude Code-credentials";
234
+ const keychainText = await readMacOSKeychainPassword({
235
+ service,
236
+ account: process.env.USER ?? ""
237
+ });
238
+ if (keychainText == null) return void 0;
239
+ const value = await parseClaudeCredentialFile(keychainText);
240
+ if (value == null) return void 0;
241
+ return {
242
+ value,
243
+ write: async (updated) => {
244
+ execFileSync("/usr/bin/security", [
245
+ "add-generic-password",
246
+ "-U",
247
+ "-s",
248
+ service,
249
+ "-a",
250
+ process.env.USER ?? "",
251
+ "-w",
252
+ JSON.stringify(updated)
253
+ ], { stdio: "ignore" });
254
+ }
255
+ };
377
256
  }
378
- function getClaudeOAuthCredential({
379
- value
380
- }) {
381
- const oauth = value.claudeAiOauth;
382
- if (!isRecord(oauth) || typeof oauth.accessToken !== "string" || typeof oauth.refreshToken !== "string" || typeof oauth.expiresAt !== "number") {
383
- return void 0;
384
- }
385
- return oauth;
257
+ function getClaudeOAuthCredential({ value }) {
258
+ const oauth = value.claudeAiOauth;
259
+ if (!isRecord(oauth) || typeof oauth.accessToken !== "string" || typeof oauth.refreshToken !== "string" || typeof oauth.expiresAt !== "number") return;
260
+ return oauth;
386
261
  }
387
262
  async function parseClaudeCredentialFile(text) {
388
- const parsed = await safeParseJSON({ text });
389
- return parsed.success && isRecord(parsed.value) ? parsed.value : void 0;
263
+ const parsed = await safeParseJSON({ text });
264
+ return parsed.success && isRecord(parsed.value) ? parsed.value : void 0;
390
265
  }
391
- async function writeClaudeCredentialFile({
392
- credentialPath,
393
- value
394
- }) {
395
- await mkdir(dirname(credentialPath), { recursive: true });
396
- const temporaryPath = `${credentialPath}.${process.pid}.tmp`;
397
- await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
398
- `, {
399
- mode: 384
400
- });
401
- await chmod(temporaryPath, 384);
402
- await rename(temporaryPath, credentialPath);
266
+ async function writeClaudeCredentialFile({ credentialPath, value }) {
267
+ await mkdir(dirname(credentialPath), { recursive: true });
268
+ const temporaryPath = `${credentialPath}.${process.pid}.tmp`;
269
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
270
+ await chmod(temporaryPath, 384);
271
+ await rename(temporaryPath, credentialPath);
403
272
  }
404
-
405
- // src/claude-code-bridge-protocol.ts
406
- import {
407
- harnessV1BridgeInboundCommandSchemas,
408
- harnessV1BridgeOutboundMessageSchema,
409
- harnessV1BridgeReadySchema,
410
- harnessV1BridgeStartBaseSchema
411
- } from "@ai-sdk/harness";
412
- import { z } from "zod/v4";
413
- var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
414
- var thinkingDisplaySchema = z.enum(["summarized", "omitted"]).optional();
415
- var thinkingSchema = z.discriminatedUnion("type", [
416
- z.object({
417
- type: z.literal("adaptive"),
418
- display: thinkingDisplaySchema
419
- }),
420
- z.object({
421
- type: z.literal("enabled"),
422
- display: thinkingDisplaySchema
423
- }),
424
- z.object({ type: z.literal("disabled") })
273
+ //#endregion
274
+ //#region src/claude-code-bridge-protocol.ts
275
+ const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
276
+ const thinkingDisplaySchema = z.enum(["summarized", "omitted"]).optional();
277
+ const thinkingSchema = z.discriminatedUnion("type", [
278
+ z.object({
279
+ type: z.literal("adaptive"),
280
+ display: thinkingDisplaySchema
281
+ }),
282
+ z.object({
283
+ type: z.literal("enabled"),
284
+ display: thinkingDisplaySchema
285
+ }),
286
+ z.object({ type: z.literal("disabled") })
425
287
  ]);
426
- var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
427
- instructions: z.string().optional(),
428
- thinking: thinkingSchema,
429
- effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
430
- maxTurns: z.number().optional(),
431
- env: z.record(z.string(), z.string()).optional(),
432
- skills: z.array(z.string()).optional(),
433
- mcpServers: z.record(z.string(), z.unknown()).optional(),
434
- // Resume signal. When true, the bridge passes `{ continue: true }` to the
435
- // Claude SDK so the in-workdir thread state is rehydrated. The host sets this
436
- // on the first prompt after a cross-process resume.
437
- continue: z.boolean().optional(),
438
- // Exact conversation to resume, as the Claude SDK's `resume` option.
439
- //
440
- // Preferred over `continue` whenever the host knows which conversation it
441
- // means: `continue` resolves to the most recent thread in the workdir, which
442
- // is the wrong one as soon as anything else has run there since — including
443
- // a resume that itself started a new thread. Mutually exclusive with
444
- // `continue` in the SDK, so the bridge sends one or the other.
445
- resumeSessionId: z.string().optional()
288
+ const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
289
+ instructions: z.string().optional(),
290
+ thinking: thinkingSchema,
291
+ effort: z.enum([
292
+ "low",
293
+ "medium",
294
+ "high",
295
+ "xhigh",
296
+ "max"
297
+ ]).optional(),
298
+ maxTurns: z.number().optional(),
299
+ agentProgressSummaries: z.boolean().optional(),
300
+ forwardSubagentText: z.boolean().optional(),
301
+ env: z.record(z.string(), z.string()).optional(),
302
+ skills: z.array(z.string()).optional(),
303
+ mcpServers: z.record(z.string(), z.unknown()).optional(),
304
+ continue: z.boolean().optional(),
305
+ resumeSessionId: z.string().optional()
446
306
  });
447
- var inboundMessageSchema = z.discriminatedUnion("type", [
448
- startMessageSchema,
449
- ...harnessV1BridgeInboundCommandSchemas
450
- ]);
451
-
452
- // src/version.ts
453
- var VERSION = true ? "1.0.137" : "0.0.0-test";
454
-
455
- // src/claude-code-harness.ts
456
- var CLAUDE_CODE_CLIENT_APP = `ai-sdk-harness-claude-code/${VERSION}`;
457
- var listMcpResourcesInputSchema = z2.object({
458
- server: z2.string().optional()
307
+ z.discriminatedUnion("type", [startMessageSchema, ...harnessV1BridgeInboundCommandSchemas]);
308
+ //#endregion
309
+ //#region src/version.ts
310
+ const VERSION = "1.0.139";
311
+ //#endregion
312
+ //#region src/claude-code-harness.ts
313
+ /**
314
+ * Value to use in User-Agent and `x-client-app` headers.
315
+ */
316
+ const CLAUDE_CODE_CLIENT_APP = `ai-sdk-harness-claude-code/${VERSION}`;
317
+ const listMcpResourcesInputSchema = z.object({ server: z.string().optional() });
318
+ const readMcpResourceInputSchema = z.object({
319
+ server: z.string(),
320
+ uri: z.string()
459
321
  });
460
- var readMcpResourceInputSchema = z2.object({
461
- server: z2.string(),
462
- uri: z2.string()
463
- });
464
- var structuredTeamMessageSchema = z2.discriminatedUnion("type", [
465
- z2.object({
466
- type: z2.literal("shutdown_request"),
467
- reason: z2.string().optional()
468
- }),
469
- z2.object({
470
- type: z2.literal("shutdown_response"),
471
- request_id: z2.string(),
472
- approve: z2.boolean(),
473
- reason: z2.string().optional()
474
- }),
475
- z2.object({
476
- type: z2.literal("plan_approval_response"),
477
- request_id: z2.string(),
478
- approve: z2.boolean(),
479
- feedback: z2.string().optional()
480
- })
322
+ const structuredTeamMessageSchema = z.discriminatedUnion("type", [
323
+ z.object({
324
+ type: z.literal("shutdown_request"),
325
+ reason: z.string().optional()
326
+ }),
327
+ z.object({
328
+ type: z.literal("shutdown_response"),
329
+ request_id: z.string(),
330
+ approve: z.boolean(),
331
+ reason: z.string().optional()
332
+ }),
333
+ z.object({
334
+ type: z.literal("plan_approval_response"),
335
+ request_id: z.string(),
336
+ approve: z.boolean(),
337
+ feedback: z.string().optional()
338
+ })
481
339
  ]);
482
- var CLAUDE_CODE_BUILTIN_TOOLS = {
483
- read: commonTool("read", {
484
- nativeName: "Read",
485
- toolUseKind: "readonly",
486
- description: "Read file contents (text, image, PDF, notebook)",
487
- inputSchema: z2.object({
488
- file_path: z2.string(),
489
- offset: z2.number().optional(),
490
- limit: z2.number().optional(),
491
- pages: z2.string().optional()
492
- })
493
- }),
494
- write: commonTool("write", {
495
- nativeName: "Write",
496
- toolUseKind: "edit",
497
- description: "Overwrite or create a file at an absolute path",
498
- inputSchema: z2.object({
499
- file_path: z2.string(),
500
- content: z2.string()
501
- })
502
- }),
503
- edit: commonTool("edit", {
504
- nativeName: "Edit",
505
- toolUseKind: "edit",
506
- description: "Edit a file by exact string replacement",
507
- inputSchema: z2.object({
508
- file_path: z2.string(),
509
- old_string: z2.string(),
510
- new_string: z2.string(),
511
- replace_all: z2.boolean().optional()
512
- })
513
- }),
514
- bash: commonTool("bash", {
515
- nativeName: "Bash",
516
- toolUseKind: "bash",
517
- description: "Execute a shell command, optionally in background",
518
- inputSchema: z2.object({
519
- command: z2.string(),
520
- timeout: z2.number().optional(),
521
- description: z2.string().optional(),
522
- run_in_background: z2.boolean().optional(),
523
- dangerouslyDisableSandbox: z2.boolean().optional()
524
- })
525
- }),
526
- glob: commonTool("glob", {
527
- nativeName: "Glob",
528
- toolUseKind: "readonly",
529
- description: "Fast file-pattern search using glob syntax",
530
- inputSchema: z2.object({
531
- pattern: z2.string(),
532
- path: z2.string().optional()
533
- })
534
- }),
535
- grep: commonTool("grep", {
536
- nativeName: "Grep",
537
- toolUseKind: "readonly",
538
- description: "Regex search over file contents via ripgrep",
539
- inputSchema: z2.object({
540
- pattern: z2.string(),
541
- path: z2.string().optional(),
542
- glob: z2.string().optional(),
543
- output_mode: z2.enum(["content", "files_with_matches", "count"]).optional(),
544
- "-B": z2.number().optional(),
545
- "-A": z2.number().optional(),
546
- "-C": z2.number().optional(),
547
- context: z2.number().optional(),
548
- "-n": z2.boolean().optional(),
549
- "-i": z2.boolean().optional(),
550
- "-o": z2.boolean().optional(),
551
- type: z2.string().optional(),
552
- head_limit: z2.number().optional(),
553
- offset: z2.number().optional(),
554
- multiline: z2.boolean().optional()
555
- })
556
- }),
557
- webSearch: commonTool("webSearch", {
558
- nativeName: "WebSearch",
559
- toolUseKind: "readonly",
560
- description: "Issue web search queries with optional domain filters",
561
- inputSchema: z2.object({
562
- query: z2.string(),
563
- allowed_domains: z2.array(z2.string()).optional(),
564
- blocked_domains: z2.array(z2.string()).optional()
565
- })
566
- }),
567
- WebFetch: tool({
568
- description: "Fetch a URL and run a prompt against its content",
569
- inputSchema: z2.object({
570
- url: z2.string(),
571
- prompt: z2.string()
572
- })
573
- }),
574
- NotebookEdit: tool({
575
- description: "Edit, insert, or delete a Jupyter notebook cell",
576
- inputSchema: z2.object({
577
- notebook_path: z2.string(),
578
- new_source: z2.string(),
579
- cell_id: z2.string().optional(),
580
- cell_type: z2.enum(["code", "markdown"]).optional(),
581
- edit_mode: z2.enum(["replace", "insert", "delete"]).optional()
582
- })
583
- }),
584
- TodoWrite: tool({
585
- description: "Replace the session todo list",
586
- inputSchema: z2.object({
587
- todos: z2.array(
588
- z2.object({
589
- content: z2.string(),
590
- status: z2.enum(["pending", "in_progress", "completed"]),
591
- activeForm: z2.string()
592
- })
593
- )
594
- })
595
- }),
596
- Agent: tool({
597
- description: "Spawn a subagent with a task",
598
- inputSchema: z2.object({
599
- description: z2.string(),
600
- prompt: z2.string(),
601
- subagent_type: z2.string().optional(),
602
- model: z2.enum(["sonnet", "opus", "haiku"]).optional(),
603
- run_in_background: z2.boolean().optional(),
604
- name: z2.string().optional(),
605
- team_name: z2.string().optional(),
606
- mode: z2.enum([
607
- "acceptEdits",
608
- "auto",
609
- "bypassPermissions",
610
- "default",
611
- "dontAsk",
612
- "plan"
613
- ]).optional(),
614
- isolation: z2.literal("worktree").optional()
615
- })
616
- }),
617
- TaskCreate: tool({
618
- description: "Create a task in the session-local task list",
619
- inputSchema: z2.object({
620
- subject: z2.string(),
621
- description: z2.string(),
622
- activeForm: z2.string().optional(),
623
- metadata: z2.record(z2.string(), z2.unknown()).optional()
624
- })
625
- }),
626
- TaskGet: tool({
627
- description: "Retrieve a task by id",
628
- inputSchema: z2.object({ taskId: z2.string() })
629
- }),
630
- TaskUpdate: tool({
631
- description: "Update fields of an existing task",
632
- inputSchema: z2.object({
633
- taskId: z2.string(),
634
- subject: z2.string().optional(),
635
- description: z2.string().optional(),
636
- activeForm: z2.string().optional(),
637
- status: z2.union([
638
- z2.enum(["pending", "in_progress", "completed"]),
639
- z2.literal("deleted")
640
- ]).optional(),
641
- addBlocks: z2.array(z2.string()).optional(),
642
- addBlockedBy: z2.array(z2.string()).optional(),
643
- owner: z2.string().optional(),
644
- metadata: z2.record(z2.string(), z2.unknown()).optional()
645
- })
646
- }),
647
- TaskList: tool({
648
- description: "Return all tasks in the session-local task list",
649
- inputSchema: z2.object({})
650
- }),
651
- TaskStop: tool({
652
- description: "Stop a running background task by id",
653
- inputSchema: z2.object({
654
- task_id: z2.string().optional(),
655
- shell_id: z2.string().optional()
656
- })
657
- }),
658
- TaskOutput: tool({
659
- description: "Poll for output from a background task",
660
- inputSchema: z2.object({
661
- task_id: z2.string(),
662
- block: z2.boolean(),
663
- timeout: z2.number()
664
- })
665
- }),
666
- Monitor: {
667
- ...tool({
668
- description: "Run and monitor a shell command or WebSocket",
669
- inputSchema: z2.object({
670
- description: z2.string().optional(),
671
- timeout_ms: z2.number().optional(),
672
- persistent: z2.boolean().optional(),
673
- command: z2.string().optional(),
674
- ws: z2.object({
675
- url: z2.string(),
676
- protocols: z2.array(z2.string()).optional()
677
- }).optional()
678
- })
679
- }),
680
- toolUseKind: "bash"
681
- },
682
- ListMcpResources: {
683
- ...tool({
684
- description: "List resources available from MCP servers",
685
- inputSchema: listMcpResourcesInputSchema
686
- }),
687
- toolUseKind: "readonly"
688
- },
689
- ListMcpResourcesTool: {
690
- ...tool({
691
- description: "List resources available from MCP servers",
692
- inputSchema: listMcpResourcesInputSchema
693
- }),
694
- toolUseKind: "readonly"
695
- },
696
- ReadMcpResource: {
697
- ...tool({
698
- description: "Read a specific MCP resource by URI",
699
- inputSchema: readMcpResourceInputSchema
700
- }),
701
- toolUseKind: "readonly"
702
- },
703
- ReadMcpResourceTool: {
704
- ...tool({
705
- description: "Read a specific MCP resource by URI",
706
- inputSchema: readMcpResourceInputSchema
707
- }),
708
- toolUseKind: "readonly"
709
- },
710
- ReadMcpResourceDirTool: {
711
- ...tool({
712
- description: "List direct children of an MCP directory resource",
713
- inputSchema: readMcpResourceInputSchema
714
- }),
715
- toolUseKind: "readonly"
716
- },
717
- RefreshMcpTools: {
718
- ...tool({
719
- description: "Refresh tools from one or all connected MCP servers",
720
- inputSchema: z2.object({ server: z2.string().optional() })
721
- }),
722
- toolUseKind: "readonly"
723
- },
724
- ExitPlanMode: tool({
725
- description: "Exit plan mode with optional permission approvals",
726
- inputSchema: z2.looseObject({
727
- allowedPrompts: z2.array(
728
- z2.object({
729
- tool: z2.literal("Bash"),
730
- prompt: z2.string()
731
- })
732
- ).optional()
733
- })
734
- }),
735
- EnterPlanMode: {
736
- ...tool({
737
- description: "Enter plan mode",
738
- inputSchema: z2.object({})
739
- }),
740
- toolUseKind: "readonly"
741
- },
742
- EnterWorktree: tool({
743
- description: "Create or enter an isolated git worktree",
744
- inputSchema: z2.object({
745
- name: z2.string().optional(),
746
- path: z2.string().optional()
747
- })
748
- }),
749
- ExitWorktree: tool({
750
- description: "Exit the current worktree session",
751
- inputSchema: z2.object({
752
- action: z2.enum(["keep", "remove"]),
753
- discard_changes: z2.boolean().optional()
754
- })
755
- }),
756
- askUserQuestions: {
757
- ...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
758
- nativeName: "AskUserQuestion",
759
- toolUseKind: "readonly"
760
- },
761
- Skill: {
762
- ...tool({
763
- description: "Activate a skill by name",
764
- inputSchema: z2.object({
765
- skill: z2.string(),
766
- args: z2.string().optional()
767
- })
768
- }),
769
- toolUseKind: "readonly"
770
- },
771
- ToolSearch: tool({
772
- description: "Search deferred MCP / catalog tools so the model can load them on demand",
773
- inputSchema: z2.object({
774
- query: z2.string(),
775
- max_results: z2.number().optional()
776
- })
777
- }),
778
- Artifact: {
779
- ...tool({
780
- description: "Publish or list claude.ai artifacts",
781
- inputSchema: z2.object({
782
- action: z2.enum(["publish", "list"]).optional(),
783
- file_path: z2.string().optional(),
784
- favicon: z2.string().optional(),
785
- limit: z2.number().optional(),
786
- scope: z2.enum(["mine", "shared", "all"]).optional(),
787
- title: z2.string().optional(),
788
- description: z2.string().optional(),
789
- label: z2.string().optional(),
790
- url: z2.string().optional(),
791
- force: z2.boolean().optional()
792
- })
793
- }),
794
- toolUseKind: "edit"
795
- },
796
- CronCreate: {
797
- ...tool({
798
- description: "Schedule a recurring or one-shot prompt",
799
- inputSchema: z2.object({
800
- cron: z2.string(),
801
- prompt: z2.string(),
802
- recurring: z2.boolean().optional(),
803
- durable: z2.boolean().optional()
804
- })
805
- }),
806
- toolUseKind: "edit"
807
- },
808
- CronDelete: {
809
- ...tool({
810
- description: "Delete a scheduled prompt by id",
811
- inputSchema: z2.object({ id: z2.string() })
812
- }),
813
- toolUseKind: "edit"
814
- },
815
- CronList: {
816
- ...tool({
817
- description: "List scheduled prompts for the current session",
818
- inputSchema: z2.object({})
819
- }),
820
- toolUseKind: "readonly"
821
- },
822
- DesignSync: {
823
- ...tool({
824
- description: "Read or update claude.ai Design projects",
825
- inputSchema: z2.object({
826
- method: z2.enum([
827
- "list_projects",
828
- "get_project",
829
- "list_files",
830
- "get_file",
831
- "finalize_plan",
832
- "write_files",
833
- "delete_files",
834
- "register_assets",
835
- "unregister_assets",
836
- "create_project",
837
- "report_validate"
838
- ]),
839
- projectId: z2.string().optional(),
840
- path: z2.string().optional(),
841
- writes: z2.array(z2.string()).max(256).optional(),
842
- deletes: z2.array(z2.string()).max(256).optional(),
843
- planId: z2.string().optional(),
844
- files: z2.array(
845
- z2.object({
846
- path: z2.string(),
847
- localPath: z2.string().optional(),
848
- data: z2.string().optional(),
849
- encoding: z2.literal("base64").optional(),
850
- mimeType: z2.string().optional()
851
- })
852
- ).max(256).optional(),
853
- paths: z2.array(z2.string()).max(256).optional(),
854
- name: z2.string().optional(),
855
- assets: z2.array(
856
- z2.object({
857
- name: z2.string(),
858
- path: z2.string(),
859
- subtitle: z2.string().optional(),
860
- viewport: z2.object({
861
- width: z2.number(),
862
- height: z2.number().optional()
863
- }).optional(),
864
- group: z2.string().optional()
865
- })
866
- ).max(256).optional(),
867
- localDir: z2.string().optional(),
868
- counts: z2.object({
869
- total: z2.number(),
870
- bad: z2.number(),
871
- thin: z2.number(),
872
- variantsIdentical: z2.number(),
873
- iterations: z2.number()
874
- }).optional()
875
- })
876
- }),
877
- toolUseKind: "edit"
878
- },
879
- LSP: {
880
- ...tool({
881
- description: "Query a language server for code intelligence",
882
- inputSchema: z2.object({
883
- operation: z2.enum([
884
- "goToDefinition",
885
- "findReferences",
886
- "hover",
887
- "documentSymbol",
888
- "workspaceSymbol",
889
- "goToImplementation",
890
- "prepareCallHierarchy",
891
- "incomingCalls",
892
- "outgoingCalls"
893
- ]),
894
- filePath: z2.string(),
895
- line: z2.number().int().positive(),
896
- character: z2.number().int().positive(),
897
- query: z2.string().optional()
898
- })
899
- }),
900
- toolUseKind: "readonly"
901
- },
902
- PowerShell: {
903
- ...tool({
904
- description: "Execute a PowerShell command, optionally in background",
905
- inputSchema: z2.object({
906
- command: z2.string(),
907
- timeout: z2.number().optional(),
908
- description: z2.string().optional(),
909
- run_in_background: z2.boolean().optional(),
910
- dangerouslyDisableSandbox: z2.boolean().optional()
911
- })
912
- }),
913
- toolUseKind: "bash"
914
- },
915
- PushNotification: {
916
- ...tool({
917
- description: "Send a notification for proactive or scheduled work",
918
- inputSchema: z2.object({
919
- message: z2.string(),
920
- status: z2.literal("proactive")
921
- })
922
- }),
923
- toolUseKind: "edit"
924
- },
925
- RemoteTrigger: {
926
- ...tool({
927
- description: "List, manage, or run a claude.ai Routine trigger",
928
- inputSchema: z2.object({
929
- action: z2.enum(["list", "get", "create", "update", "run"]),
930
- trigger_id: z2.string().optional(),
931
- body: z2.record(z2.string(), z2.unknown()).optional()
932
- })
933
- }),
934
- toolUseKind: "edit"
935
- },
936
- ReportFindings: {
937
- ...tool({
938
- description: "Return verified code-review findings",
939
- inputSchema: z2.object({
940
- level: z2.enum(["low", "medium", "high", "xhigh", "max"]).optional(),
941
- findings: z2.array(
942
- z2.object({
943
- file: z2.string(),
944
- line: z2.number().optional(),
945
- summary: z2.string(),
946
- short_summary: z2.string().optional(),
947
- failure_scenario: z2.string(),
948
- category: z2.string().optional(),
949
- verdict: z2.enum(["CONFIRMED", "PLAUSIBLE"]).optional(),
950
- outcome: z2.enum(["fixed", "skipped", "no_change_needed"]).optional()
951
- })
952
- ).max(32)
953
- })
954
- }),
955
- toolUseKind: "readonly"
956
- },
957
- ScheduleWakeup: {
958
- ...tool({
959
- description: "Schedule or stop the next iteration of a dynamic loop",
960
- inputSchema: z2.object({
961
- delaySeconds: z2.number().optional(),
962
- reason: z2.string().optional(),
963
- prompt: z2.string().optional(),
964
- stop: z2.boolean().optional()
965
- })
966
- }),
967
- toolUseKind: "edit"
968
- },
969
- SendMessage: {
970
- ...tool({
971
- description: "Send a plain-text or protocol message to another agent",
972
- inputSchema: z2.object({
973
- to: z2.string(),
974
- summary: z2.string().max(200).optional(),
975
- message: z2.union([z2.string(), structuredTeamMessageSchema])
976
- })
977
- }),
978
- toolUseKind: "edit"
979
- },
980
- SendUserFile: {
981
- ...tool({
982
- description: "Send one or more files to the user",
983
- inputSchema: z2.object({
984
- files: z2.union([z2.string(), z2.array(z2.string()).min(1)]),
985
- caption: z2.string().optional(),
986
- status: z2.enum(["normal", "proactive"]),
987
- display: z2.enum(["render", "attach"]).optional()
988
- })
989
- }),
990
- toolUseKind: "readonly"
991
- },
992
- ShareOnboardingGuide: {
993
- ...tool({
994
- description: "Create, update, inspect, or delete an onboarding guide",
995
- inputSchema: z2.object({
996
- mode: z2.enum(["check", "update", "create", "delete"]).optional(),
997
- short_code: z2.string().regex(/^[A-Za-z0-9_-]{1,64}$/).optional()
998
- })
999
- }),
1000
- toolUseKind: "edit"
1001
- },
1002
- WaitForMcpServers: {
1003
- ...tool({
1004
- description: "Wait for MCP servers that are still connecting",
1005
- inputSchema: z2.object({
1006
- servers: z2.array(z2.string()).optional()
1007
- })
1008
- }),
1009
- toolUseKind: "readonly"
1010
- },
1011
- Workflow: {
1012
- ...tool({
1013
- description: "Run or resume a dynamic multi-agent workflow",
1014
- inputSchema: z2.object({
1015
- script: z2.string().optional(),
1016
- name: z2.string().optional(),
1017
- description: z2.string().optional(),
1018
- title: z2.string().optional(),
1019
- args: z2.record(z2.string(), z2.unknown()).optional(),
1020
- scriptPath: z2.string().optional(),
1021
- resumeFromRunId: z2.string().optional()
1022
- })
1023
- }),
1024
- toolUseKind: "edit"
1025
- }
340
+ const CLAUDE_CODE_BUILTIN_TOOLS = {
341
+ read: commonTool("read", {
342
+ nativeName: "Read",
343
+ toolUseKind: "readonly",
344
+ description: "Read file contents (text, image, PDF, notebook)",
345
+ inputSchema: z.object({
346
+ file_path: z.string(),
347
+ offset: z.number().optional(),
348
+ limit: z.number().optional(),
349
+ pages: z.string().optional()
350
+ })
351
+ }),
352
+ write: commonTool("write", {
353
+ nativeName: "Write",
354
+ toolUseKind: "edit",
355
+ description: "Overwrite or create a file at an absolute path",
356
+ inputSchema: z.object({
357
+ file_path: z.string(),
358
+ content: z.string()
359
+ })
360
+ }),
361
+ edit: commonTool("edit", {
362
+ nativeName: "Edit",
363
+ toolUseKind: "edit",
364
+ description: "Edit a file by exact string replacement",
365
+ inputSchema: z.object({
366
+ file_path: z.string(),
367
+ old_string: z.string(),
368
+ new_string: z.string(),
369
+ replace_all: z.boolean().optional()
370
+ })
371
+ }),
372
+ bash: commonTool("bash", {
373
+ nativeName: "Bash",
374
+ toolUseKind: "bash",
375
+ description: "Execute a shell command, optionally in background",
376
+ inputSchema: z.object({
377
+ command: z.string(),
378
+ timeout: z.number().optional(),
379
+ description: z.string().optional(),
380
+ run_in_background: z.boolean().optional(),
381
+ dangerouslyDisableSandbox: z.boolean().optional()
382
+ })
383
+ }),
384
+ glob: commonTool("glob", {
385
+ nativeName: "Glob",
386
+ toolUseKind: "readonly",
387
+ description: "Fast file-pattern search using glob syntax",
388
+ inputSchema: z.object({
389
+ pattern: z.string(),
390
+ path: z.string().optional()
391
+ })
392
+ }),
393
+ grep: commonTool("grep", {
394
+ nativeName: "Grep",
395
+ toolUseKind: "readonly",
396
+ description: "Regex search over file contents via ripgrep",
397
+ inputSchema: z.object({
398
+ pattern: z.string(),
399
+ path: z.string().optional(),
400
+ glob: z.string().optional(),
401
+ output_mode: z.enum([
402
+ "content",
403
+ "files_with_matches",
404
+ "count"
405
+ ]).optional(),
406
+ "-B": z.number().optional(),
407
+ "-A": z.number().optional(),
408
+ "-C": z.number().optional(),
409
+ context: z.number().optional(),
410
+ "-n": z.boolean().optional(),
411
+ "-i": z.boolean().optional(),
412
+ "-o": z.boolean().optional(),
413
+ type: z.string().optional(),
414
+ head_limit: z.number().optional(),
415
+ offset: z.number().optional(),
416
+ multiline: z.boolean().optional()
417
+ })
418
+ }),
419
+ webSearch: commonTool("webSearch", {
420
+ nativeName: "WebSearch",
421
+ toolUseKind: "readonly",
422
+ description: "Issue web search queries with optional domain filters",
423
+ inputSchema: z.object({
424
+ query: z.string(),
425
+ allowed_domains: z.array(z.string()).optional(),
426
+ blocked_domains: z.array(z.string()).optional()
427
+ })
428
+ }),
429
+ WebFetch: tool({
430
+ description: "Fetch a URL and run a prompt against its content",
431
+ inputSchema: z.object({
432
+ url: z.string(),
433
+ prompt: z.string()
434
+ })
435
+ }),
436
+ NotebookEdit: tool({
437
+ description: "Edit, insert, or delete a Jupyter notebook cell",
438
+ inputSchema: z.object({
439
+ notebook_path: z.string(),
440
+ new_source: z.string(),
441
+ cell_id: z.string().optional(),
442
+ cell_type: z.enum(["code", "markdown"]).optional(),
443
+ edit_mode: z.enum([
444
+ "replace",
445
+ "insert",
446
+ "delete"
447
+ ]).optional()
448
+ })
449
+ }),
450
+ TodoWrite: tool({
451
+ description: "Replace the session todo list",
452
+ inputSchema: z.object({ todos: z.array(z.object({
453
+ content: z.string(),
454
+ status: z.enum([
455
+ "pending",
456
+ "in_progress",
457
+ "completed"
458
+ ]),
459
+ activeForm: z.string()
460
+ })) })
461
+ }),
462
+ Agent: tool({
463
+ description: "Spawn a subagent with a task",
464
+ inputSchema: z.object({
465
+ description: z.string(),
466
+ prompt: z.string(),
467
+ subagent_type: z.string().optional(),
468
+ model: z.enum([
469
+ "sonnet",
470
+ "opus",
471
+ "haiku"
472
+ ]).optional(),
473
+ run_in_background: z.boolean().optional(),
474
+ name: z.string().optional(),
475
+ team_name: z.string().optional(),
476
+ mode: z.enum([
477
+ "acceptEdits",
478
+ "auto",
479
+ "bypassPermissions",
480
+ "default",
481
+ "dontAsk",
482
+ "plan"
483
+ ]).optional(),
484
+ isolation: z.literal("worktree").optional()
485
+ })
486
+ }),
487
+ TaskCreate: tool({
488
+ description: "Create a task in the session-local task list",
489
+ inputSchema: z.object({
490
+ subject: z.string(),
491
+ description: z.string(),
492
+ activeForm: z.string().optional(),
493
+ metadata: z.record(z.string(), z.unknown()).optional()
494
+ })
495
+ }),
496
+ TaskGet: tool({
497
+ description: "Retrieve a task by id",
498
+ inputSchema: z.object({ taskId: z.string() })
499
+ }),
500
+ TaskUpdate: tool({
501
+ description: "Update fields of an existing task",
502
+ inputSchema: z.object({
503
+ taskId: z.string(),
504
+ subject: z.string().optional(),
505
+ description: z.string().optional(),
506
+ activeForm: z.string().optional(),
507
+ status: z.union([z.enum([
508
+ "pending",
509
+ "in_progress",
510
+ "completed"
511
+ ]), z.literal("deleted")]).optional(),
512
+ addBlocks: z.array(z.string()).optional(),
513
+ addBlockedBy: z.array(z.string()).optional(),
514
+ owner: z.string().optional(),
515
+ metadata: z.record(z.string(), z.unknown()).optional()
516
+ })
517
+ }),
518
+ TaskList: tool({
519
+ description: "Return all tasks in the session-local task list",
520
+ inputSchema: z.object({})
521
+ }),
522
+ TaskStop: tool({
523
+ description: "Stop a running background task by id",
524
+ inputSchema: z.object({
525
+ task_id: z.string().optional(),
526
+ shell_id: z.string().optional()
527
+ })
528
+ }),
529
+ TaskOutput: tool({
530
+ description: "Poll for output from a background task",
531
+ inputSchema: z.object({
532
+ task_id: z.string(),
533
+ block: z.boolean(),
534
+ timeout: z.number()
535
+ })
536
+ }),
537
+ Monitor: {
538
+ ...tool({
539
+ description: "Run and monitor a shell command or WebSocket",
540
+ inputSchema: z.object({
541
+ description: z.string().optional(),
542
+ timeout_ms: z.number().optional(),
543
+ persistent: z.boolean().optional(),
544
+ command: z.string().optional(),
545
+ ws: z.object({
546
+ url: z.string(),
547
+ protocols: z.array(z.string()).optional()
548
+ }).optional()
549
+ })
550
+ }),
551
+ toolUseKind: "bash"
552
+ },
553
+ ListMcpResources: {
554
+ ...tool({
555
+ description: "List resources available from MCP servers",
556
+ inputSchema: listMcpResourcesInputSchema
557
+ }),
558
+ toolUseKind: "readonly"
559
+ },
560
+ ListMcpResourcesTool: {
561
+ ...tool({
562
+ description: "List resources available from MCP servers",
563
+ inputSchema: listMcpResourcesInputSchema
564
+ }),
565
+ toolUseKind: "readonly"
566
+ },
567
+ ReadMcpResource: {
568
+ ...tool({
569
+ description: "Read a specific MCP resource by URI",
570
+ inputSchema: readMcpResourceInputSchema
571
+ }),
572
+ toolUseKind: "readonly"
573
+ },
574
+ ReadMcpResourceTool: {
575
+ ...tool({
576
+ description: "Read a specific MCP resource by URI",
577
+ inputSchema: readMcpResourceInputSchema
578
+ }),
579
+ toolUseKind: "readonly"
580
+ },
581
+ ReadMcpResourceDirTool: {
582
+ ...tool({
583
+ description: "List direct children of an MCP directory resource",
584
+ inputSchema: readMcpResourceInputSchema
585
+ }),
586
+ toolUseKind: "readonly"
587
+ },
588
+ RefreshMcpTools: {
589
+ ...tool({
590
+ description: "Refresh tools from one or all connected MCP servers",
591
+ inputSchema: z.object({ server: z.string().optional() })
592
+ }),
593
+ toolUseKind: "readonly"
594
+ },
595
+ ExitPlanMode: tool({
596
+ description: "Exit plan mode with optional permission approvals",
597
+ inputSchema: z.looseObject({ allowedPrompts: z.array(z.object({
598
+ tool: z.literal("Bash"),
599
+ prompt: z.string()
600
+ })).optional() })
601
+ }),
602
+ EnterPlanMode: {
603
+ ...tool({
604
+ description: "Enter plan mode",
605
+ inputSchema: z.object({})
606
+ }),
607
+ toolUseKind: "readonly"
608
+ },
609
+ EnterWorktree: tool({
610
+ description: "Create or enter an isolated git worktree",
611
+ inputSchema: z.object({
612
+ name: z.string().optional(),
613
+ path: z.string().optional()
614
+ })
615
+ }),
616
+ ExitWorktree: tool({
617
+ description: "Exit the current worktree session",
618
+ inputSchema: z.object({
619
+ action: z.enum(["keep", "remove"]),
620
+ discard_changes: z.boolean().optional()
621
+ })
622
+ }),
623
+ askUserQuestions: {
624
+ ...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
625
+ nativeName: "AskUserQuestion",
626
+ toolUseKind: "readonly"
627
+ },
628
+ Skill: {
629
+ ...tool({
630
+ description: "Activate a skill by name",
631
+ inputSchema: z.object({
632
+ skill: z.string(),
633
+ args: z.string().optional()
634
+ })
635
+ }),
636
+ toolUseKind: "readonly"
637
+ },
638
+ ToolSearch: tool({
639
+ description: "Search deferred MCP / catalog tools so the model can load them on demand",
640
+ inputSchema: z.object({
641
+ query: z.string(),
642
+ max_results: z.number().optional()
643
+ })
644
+ }),
645
+ Artifact: {
646
+ ...tool({
647
+ description: "Publish or list claude.ai artifacts",
648
+ inputSchema: z.object({
649
+ action: z.enum(["publish", "list"]).optional(),
650
+ file_path: z.string().optional(),
651
+ favicon: z.string().optional(),
652
+ limit: z.number().optional(),
653
+ scope: z.enum([
654
+ "mine",
655
+ "shared",
656
+ "all"
657
+ ]).optional(),
658
+ title: z.string().optional(),
659
+ description: z.string().optional(),
660
+ label: z.string().optional(),
661
+ url: z.string().optional(),
662
+ force: z.boolean().optional()
663
+ })
664
+ }),
665
+ toolUseKind: "edit"
666
+ },
667
+ CronCreate: {
668
+ ...tool({
669
+ description: "Schedule a recurring or one-shot prompt",
670
+ inputSchema: z.object({
671
+ cron: z.string(),
672
+ prompt: z.string(),
673
+ recurring: z.boolean().optional(),
674
+ durable: z.boolean().optional()
675
+ })
676
+ }),
677
+ toolUseKind: "edit"
678
+ },
679
+ CronDelete: {
680
+ ...tool({
681
+ description: "Delete a scheduled prompt by id",
682
+ inputSchema: z.object({ id: z.string() })
683
+ }),
684
+ toolUseKind: "edit"
685
+ },
686
+ CronList: {
687
+ ...tool({
688
+ description: "List scheduled prompts for the current session",
689
+ inputSchema: z.object({})
690
+ }),
691
+ toolUseKind: "readonly"
692
+ },
693
+ DesignSync: {
694
+ ...tool({
695
+ description: "Read or update claude.ai Design projects",
696
+ inputSchema: z.object({
697
+ method: z.enum([
698
+ "list_projects",
699
+ "get_project",
700
+ "list_files",
701
+ "get_file",
702
+ "finalize_plan",
703
+ "write_files",
704
+ "delete_files",
705
+ "register_assets",
706
+ "unregister_assets",
707
+ "create_project",
708
+ "report_validate"
709
+ ]),
710
+ projectId: z.string().optional(),
711
+ path: z.string().optional(),
712
+ writes: z.array(z.string()).max(256).optional(),
713
+ deletes: z.array(z.string()).max(256).optional(),
714
+ planId: z.string().optional(),
715
+ files: z.array(z.object({
716
+ path: z.string(),
717
+ localPath: z.string().optional(),
718
+ data: z.string().optional(),
719
+ encoding: z.literal("base64").optional(),
720
+ mimeType: z.string().optional()
721
+ })).max(256).optional(),
722
+ paths: z.array(z.string()).max(256).optional(),
723
+ name: z.string().optional(),
724
+ assets: z.array(z.object({
725
+ name: z.string(),
726
+ path: z.string(),
727
+ subtitle: z.string().optional(),
728
+ viewport: z.object({
729
+ width: z.number(),
730
+ height: z.number().optional()
731
+ }).optional(),
732
+ group: z.string().optional()
733
+ })).max(256).optional(),
734
+ localDir: z.string().optional(),
735
+ counts: z.object({
736
+ total: z.number(),
737
+ bad: z.number(),
738
+ thin: z.number(),
739
+ variantsIdentical: z.number(),
740
+ iterations: z.number()
741
+ }).optional()
742
+ })
743
+ }),
744
+ toolUseKind: "edit"
745
+ },
746
+ LSP: {
747
+ ...tool({
748
+ description: "Query a language server for code intelligence",
749
+ inputSchema: z.object({
750
+ operation: z.enum([
751
+ "goToDefinition",
752
+ "findReferences",
753
+ "hover",
754
+ "documentSymbol",
755
+ "workspaceSymbol",
756
+ "goToImplementation",
757
+ "prepareCallHierarchy",
758
+ "incomingCalls",
759
+ "outgoingCalls"
760
+ ]),
761
+ filePath: z.string(),
762
+ line: z.number().int().positive(),
763
+ character: z.number().int().positive(),
764
+ query: z.string().optional()
765
+ })
766
+ }),
767
+ toolUseKind: "readonly"
768
+ },
769
+ PowerShell: {
770
+ ...tool({
771
+ description: "Execute a PowerShell command, optionally in background",
772
+ inputSchema: z.object({
773
+ command: z.string(),
774
+ timeout: z.number().optional(),
775
+ description: z.string().optional(),
776
+ run_in_background: z.boolean().optional(),
777
+ dangerouslyDisableSandbox: z.boolean().optional()
778
+ })
779
+ }),
780
+ toolUseKind: "bash"
781
+ },
782
+ PushNotification: {
783
+ ...tool({
784
+ description: "Send a notification for proactive or scheduled work",
785
+ inputSchema: z.object({
786
+ message: z.string(),
787
+ status: z.literal("proactive")
788
+ })
789
+ }),
790
+ toolUseKind: "edit"
791
+ },
792
+ RemoteTrigger: {
793
+ ...tool({
794
+ description: "List, manage, or run a claude.ai Routine trigger",
795
+ inputSchema: z.object({
796
+ action: z.enum([
797
+ "list",
798
+ "get",
799
+ "create",
800
+ "update",
801
+ "run"
802
+ ]),
803
+ trigger_id: z.string().optional(),
804
+ body: z.record(z.string(), z.unknown()).optional()
805
+ })
806
+ }),
807
+ toolUseKind: "edit"
808
+ },
809
+ ReportFindings: {
810
+ ...tool({
811
+ description: "Return verified code-review findings",
812
+ inputSchema: z.object({
813
+ level: z.enum([
814
+ "low",
815
+ "medium",
816
+ "high",
817
+ "xhigh",
818
+ "max"
819
+ ]).optional(),
820
+ findings: z.array(z.object({
821
+ file: z.string(),
822
+ line: z.number().optional(),
823
+ summary: z.string(),
824
+ short_summary: z.string().optional(),
825
+ failure_scenario: z.string(),
826
+ category: z.string().optional(),
827
+ verdict: z.enum(["CONFIRMED", "PLAUSIBLE"]).optional(),
828
+ outcome: z.enum([
829
+ "fixed",
830
+ "skipped",
831
+ "no_change_needed"
832
+ ]).optional()
833
+ })).max(32)
834
+ })
835
+ }),
836
+ toolUseKind: "readonly"
837
+ },
838
+ ScheduleWakeup: {
839
+ ...tool({
840
+ description: "Schedule or stop the next iteration of a dynamic loop",
841
+ inputSchema: z.object({
842
+ delaySeconds: z.number().optional(),
843
+ reason: z.string().optional(),
844
+ prompt: z.string().optional(),
845
+ stop: z.boolean().optional()
846
+ })
847
+ }),
848
+ toolUseKind: "edit"
849
+ },
850
+ SendMessage: {
851
+ ...tool({
852
+ description: "Send a plain-text or protocol message to another agent",
853
+ inputSchema: z.object({
854
+ to: z.string(),
855
+ summary: z.string().max(200).optional(),
856
+ message: z.union([z.string(), structuredTeamMessageSchema])
857
+ })
858
+ }),
859
+ toolUseKind: "edit"
860
+ },
861
+ SendUserFile: {
862
+ ...tool({
863
+ description: "Send one or more files to the user",
864
+ inputSchema: z.object({
865
+ files: z.union([z.string(), z.array(z.string()).min(1)]),
866
+ caption: z.string().optional(),
867
+ status: z.enum(["normal", "proactive"]),
868
+ display: z.enum(["render", "attach"]).optional()
869
+ })
870
+ }),
871
+ toolUseKind: "readonly"
872
+ },
873
+ ShareOnboardingGuide: {
874
+ ...tool({
875
+ description: "Create, update, inspect, or delete an onboarding guide",
876
+ inputSchema: z.object({
877
+ mode: z.enum([
878
+ "check",
879
+ "update",
880
+ "create",
881
+ "delete"
882
+ ]).optional(),
883
+ short_code: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/).optional()
884
+ })
885
+ }),
886
+ toolUseKind: "edit"
887
+ },
888
+ WaitForMcpServers: {
889
+ ...tool({
890
+ description: "Wait for MCP servers that are still connecting",
891
+ inputSchema: z.object({ servers: z.array(z.string()).optional() })
892
+ }),
893
+ toolUseKind: "readonly"
894
+ },
895
+ Workflow: {
896
+ ...tool({
897
+ description: "Run or resume a dynamic multi-agent workflow",
898
+ inputSchema: z.object({
899
+ script: z.string().optional(),
900
+ name: z.string().optional(),
901
+ description: z.string().optional(),
902
+ title: z.string().optional(),
903
+ args: z.record(z.string(), z.unknown()).optional(),
904
+ scriptPath: z.string().optional(),
905
+ resumeFromRunId: z.string().optional()
906
+ })
907
+ }),
908
+ toolUseKind: "edit"
909
+ }
1026
910
  };
1027
- var claudeCodeBridgeCoordsSchema = z2.object({
1028
- port: z2.number(),
1029
- token: z2.string(),
1030
- lastSeenEventId: z2.number(),
1031
- sandboxId: z2.string().optional()
911
+ /**
912
+ * Live bridge coordinates returned by `doDetach()` and `doSuspendTurn()`. A
913
+ * future process uses them to reopen a socket to the still-running bridge
914
+ * (`attach`) instead of re-spawning it. Absent on a `doStop()` payload.
915
+ */
916
+ const claudeCodeBridgeCoordsSchema = z.object({
917
+ port: z.number(),
918
+ token: z.string(),
919
+ lastSeenEventId: z.number(),
920
+ sandboxId: z.string().optional()
1032
921
  });
1033
- var claudeCodeResumeStateSchema = z2.looseObject({
1034
- bridge: claudeCodeBridgeCoordsSchema.optional(),
1035
- sandboxCredentialEnvironment: z2.record(z2.string(), z2.string()).optional(),
1036
- /**
1037
- * The exact Claude conversation to rehydrate on resume. Written by the
1038
- * adapter on `doStop()`/`doDetach()`/`doSuspendTurn()` from the session id
1039
- * the bridge observed, so a resume names the conversation instead of
1040
- * relying on the SDK's `continue` flag — "most recent thread in this
1041
- * workdir" — which silently picks the wrong one once a second thread
1042
- * exists there. Hosts that captured the id themselves (it is surfaced as
1043
- * `harnessMetadata['claude-code'].sessionId` on `finish` parts) may also
1044
- * set it explicitly.
1045
- */
1046
- claudeSessionId: z2.string().optional()
922
+ /**
923
+ * Schema for the adapter-specific portion of lifecycle state `data`.
924
+ *
925
+ * A `doStop()` payload is structurally empty (`{}`): the framework derives the
926
+ * sandbox via `provider.resumeSession({ sessionId })`, and the Claude SDK's
927
+ * `{ continue: true }` flag rehydrates the thread from the workdir. A
928
+ * `doDetach()` payload additionally carries `bridge` coordinates for
929
+ * cross-process `attach`. A loose object keeps both shapes valid.
930
+ */
931
+ const claudeCodeResumeStateSchema = z.looseObject({
932
+ bridge: claudeCodeBridgeCoordsSchema.optional(),
933
+ sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
934
+ /**
935
+ * The exact Claude conversation to rehydrate on resume. Written by the
936
+ * adapter on `doStop()`/`doDetach()`/`doSuspendTurn()` from the session id
937
+ * the bridge observed, so a resume names the conversation instead of
938
+ * relying on the SDK's `continue` flag — "most recent thread in this
939
+ * workdir" — which silently picks the wrong one once a second thread
940
+ * exists there. Hosts that captured the id themselves (it is surfaced as
941
+ * `harnessMetadata['claude-code'].sessionId` on `finish` parts) may also
942
+ * set it explicitly.
943
+ */
944
+ claudeSessionId: z.string().optional()
1047
945
  });
1048
946
  function createClaudeCode(settings = {}) {
1049
- if (settings.mcpServers != null && Object.prototype.hasOwnProperty.call(settings.mcpServers, "harness-tools")) {
1050
- throw new Error(
1051
- 'Claude Code MCP server name "harness-tools" is reserved for HarnessAgent tools.'
1052
- );
1053
- }
1054
- const thinking = settings.thinking ?? {
1055
- type: "adaptive",
1056
- display: "summarized"
1057
- };
1058
- return {
1059
- specificationVersion: "harness-v1",
1060
- harnessId: "claude-code",
1061
- builtinTools: CLAUDE_CODE_BUILTIN_TOOLS,
1062
- supportsBuiltinToolApprovals: true,
1063
- supportsBuiltinToolFiltering: true,
1064
- lifecycleStateSchema: claudeCodeResumeStateSchema,
1065
- getBootstrap: getClaudeCodeBootstrap,
1066
- doStart: async (startOpts) => {
1067
- const sandboxSession = startOpts.sandboxSession;
1068
- const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
1069
- const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
1070
- validateBasicSandboxSettings({
1071
- sandboxSession,
1072
- port: settings.port,
1073
- portEndpoint: settings.portEndpoint
1074
- });
1075
- if (settings.mintBridgeToken != null && sandboxId == null) {
1076
- throw new HarnessCapabilityUnsupportedError({
1077
- harnessId: "claude-code",
1078
- message: "The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
1079
- });
1080
- }
1081
- const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
1082
- const isResume = lifecycleState != null;
1083
- const isContinue = startOpts.continueFrom != null;
1084
- const resumeData = isResume ? lifecycleState?.data : void 0;
1085
- const coords = resumeData?.bridge;
1086
- const authenticationMode = resolveClaudeCodeAuthenticationMode(
1087
- settings.auth
1088
- );
1089
- const resolvedAuthEnvironment = await resolveClaudeCodeAuthentication({
1090
- auth: settings.auth
1091
- });
1092
- const claudeEnvironment = {
1093
- ...resolvedAuthEnvironment,
1094
- /*
1095
- * The Claude Agent SDK does not expose arbitrary model-request
1096
- * headers. It reads this environment variable and sends the value as
1097
- * `x-client-app`, while also appending `client-app/<value>` to
1098
- * `User-Agent`, so this is the attribution path for AI Gateway.
1099
- */
1100
- ...resolvedAuthEnvironment.AI_GATEWAY_BASE_URL ? { CLAUDE_AGENT_SDK_CLIENT_APP: CLAUDE_CODE_CLIENT_APP } : {},
1101
- ...settings.env,
1102
- ...startOpts.headers != null ? {
1103
- ANTHROPIC_CUSTOM_HEADERS: Object.entries(startOpts.headers).map(([name, value]) => `${name}: ${value}`).join("\n")
1104
- } : {}
1105
- };
1106
- let sandboxClaudeEnvironment = claudeEnvironment;
1107
- let sandboxCredentialEnvironment;
1108
- if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
1109
- sandboxCredentialEnvironment = await resolveSandboxCredentialEnvironment({
1110
- environment: claudeEnvironment,
1111
- credentialEnvironmentVariables: CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
1112
- credentialForwarding: settings.credentialForwarding,
1113
- previousSandboxCredentialEnvironment: resumeData?.sandboxCredentialEnvironment
1114
- });
1115
- sandboxClaudeEnvironment = {
1116
- ...claudeEnvironment,
1117
- ...sandboxCredentialEnvironment
1118
- };
1119
- const transformationSources = {
1120
- env: claudeEnvironment,
1121
- sandboxEnv: sandboxClaudeEnvironment,
1122
- auth: authenticationMode
1123
- };
1124
- const requestTransformations = [
1125
- ...createClaudeCodeRequestTransformations(transformationSources),
1126
- ...createClaudeCodeSubscriptionRequestTransformations(
1127
- transformationSources
1128
- )
1129
- ];
1130
- if (requestTransformations.length > 0) {
1131
- await sandboxSession.addRequestTransformations(
1132
- requestTransformations
1133
- );
1134
- }
1135
- } else {
1136
- sandboxClaudeEnvironment = await applyCredentialForwarding({
1137
- environment: sandboxClaudeEnvironment,
1138
- credentialEnvironmentVariables: CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
1139
- credentialForwarding: settings.credentialForwarding
1140
- });
1141
- warnCredentialBrokeringUnavailable({
1142
- environment: claudeEnvironment,
1143
- forwardedEnvironment: sandboxClaudeEnvironment,
1144
- credentialEnvironmentVariables: CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES
1145
- });
1146
- }
1147
- const sandboxHomeDir = await resolveSandboxHomeDir({
1148
- sandbox: toolSafeSandboxSession,
1149
- abortSignal: startOpts.abortSignal
1150
- });
1151
- const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
1152
- const bootstrapDir = posix.resolve(stateDir, CLAUDE_CODE_BOOTSTRAP_DIR);
1153
- const resumeSessionId = resumeData?.claudeSessionId;
1154
- const workDir = startOpts.sessionWorkDir;
1155
- const sessionDataDir = harnessSessionDataDirectoryPath({
1156
- stateDirectory: stateDir,
1157
- sessionId: startOpts.sessionId
1158
- });
1159
- const bridgeStateDir = `${sessionDataDir}/bridge`;
1160
- const timeoutMs = settings.startupTimeoutMs ?? 12e4;
1161
- const report = startOpts.observability?.report;
1162
- const onDiagnostic = report ? (frame) => report(
1163
- harnessV1DiagnosticFromBridgeFrame(frame, {
1164
- sessionId: startOpts.sessionId,
1165
- timestamp: Date.now()
1166
- })
1167
- ) : void 0;
1168
- const onBridgeError = createBridgeErrorHandler({
1169
- harnessId: "claude-code",
1170
- sessionId: startOpts.sessionId
1171
- });
1172
- let supportsUserMessageResponses = false;
1173
- const buildConnect = (endpoint2) => async ({
1174
- abortSignal
1175
- }) => {
1176
- return openBridgeWebSocket({
1177
- endpoint: endpoint2,
1178
- timeoutMs,
1179
- abortSignal,
1180
- onHello: (supportsResponses) => {
1181
- supportsUserMessageResponses = supportsResponses;
1182
- }
1183
- });
1184
- };
1185
- if (coords) {
1186
- try {
1187
- const endpoint2 = await resolveBridgeEndpoint({
1188
- sandboxSession,
1189
- override: settings.portEndpoint,
1190
- port: coords.port
1191
- });
1192
- const attachEndpoint = withBridgeToken({
1193
- endpoint: endpoint2,
1194
- token: coords.token
1195
- });
1196
- const attachChannel = new SandboxChannel({
1197
- connect: buildConnect(attachEndpoint),
1198
- outboundSchema: outboundMessageSchema,
1199
- initialLastSeenEventId: coords.lastSeenEventId,
1200
- onDiagnostic,
1201
- onBridgeError,
1202
- reconnect: settings.reconnect
1203
- });
1204
- const finishListenerAttachment2 = isContinue ? attachChannel.beginListenerAttachment() : void 0;
1205
- try {
1206
- await attachChannel.open(isContinue ? { resume: true } : void 0);
1207
- } catch (error) {
1208
- finishListenerAttachment2?.();
1209
- throw error;
1210
- }
1211
- return createSession({
1212
- sessionId: startOpts.sessionId,
1213
- channel: attachChannel,
1214
- finishListenerAttachment: finishListenerAttachment2,
1215
- ...resumeSessionId ? { resumeSessionId } : {},
1216
- // The live bridge was spawned by another process; this one owns no
1217
- // process handle. The session lifecycle method decides whether the
1218
- // sandbox is left running, stopped, or destroyed.
1219
- proc: void 0,
1220
- maxTurns: settings.maxTurns,
1221
- env: sandboxClaudeEnvironment,
1222
- thinking,
1223
- effort: settings.effort,
1224
- isResume: true,
1225
- continueOnFirstPrompt: false,
1226
- rerunContinue: false,
1227
- bridgePort: coords.port,
1228
- bridgeToken: coords.token,
1229
- sandboxId,
1230
- sandboxCredentialEnvironment,
1231
- debug: startOpts.observability?.debug,
1232
- permissionMode: startOpts.permissionMode,
1233
- builtinToolFiltering: startOpts.builtinToolFiltering,
1234
- sandbox: toolSafeSandboxSession,
1235
- sandboxHomeDir,
1236
- mcpServers: settings.mcpServers,
1237
- supportsUserMessageResponses: () => supportsUserMessageResponses
1238
- });
1239
- } catch {
1240
- }
1241
- }
1242
- let respawnStrategy = isResume ? "rerun" : void 0;
1243
- if (coords && isContinue) {
1244
- const logRaw = await Promise.resolve(
1245
- toolSafeSandboxSession.readTextFile({
1246
- path: `${bridgeStateDir}/event-log.ndjson`,
1247
- abortSignal: startOpts.abortSignal
1248
- })
1249
- ).catch(() => null);
1250
- if (await classifyDiskLog(logRaw) === "replay") {
1251
- respawnStrategy = "replay";
1252
- }
1253
- }
1254
- const port = resolveBridgePort({
1255
- sandboxSession,
1256
- override: settings.port
1257
- });
1258
- const token = settings.mintBridgeToken == null ? createBridgeToken() : settings.mintBridgeToken(sandboxId);
1259
- const env = {
1260
- BRIDGE_CHANNEL_TOKEN: token,
1261
- BRIDGE_WS_PORT: String(port),
1262
- ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
1263
- };
1264
- if (respawnStrategy === void 0) {
1265
- await toolSafeSandboxSession.run({
1266
- command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1267
- abortSignal: startOpts.abortSignal
1268
- });
1269
- }
1270
- await markBridgeStarting({
1271
- sandbox: toolSafeSandboxSession,
1272
- bridgeStateDir,
1273
- bridgeType: "claude-code",
1274
- abortSignal: startOpts.abortSignal
1275
- });
1276
- const proc = await toolSafeSandboxSession.spawn({
1277
- command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)}`,
1278
- env,
1279
- abortSignal: startOpts.abortSignal
1280
- });
1281
- const bridgeStartupStderr = [];
1282
- const bridgeStartupStderrDone = forwardBridgeProcessStream({
1283
- stream: proc.stderr,
1284
- streamName: "stderr",
1285
- source: "claude-code",
1286
- collectTail: bridgeStartupStderr
1287
- });
1288
- void bridgeStartupStderrDone;
1289
- const { port: boundPort } = await waitForBridgeReady({
1290
- proc,
1291
- sandbox: toolSafeSandboxSession,
1292
- bridgeStateDir,
1293
- bridgeType: "claude-code",
1294
- timeoutMs,
1295
- abortSignal: startOpts.abortSignal,
1296
- createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
1297
- message: "claude-code bridge did not become ready in time.",
1298
- proc: proc2,
1299
- stdoutTail,
1300
- stderrTail: bridgeStartupStderr,
1301
- stderrDone: bridgeStartupStderrDone
1302
- }),
1303
- createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
1304
- message: "claude-code bridge exited before becoming ready.",
1305
- proc: proc2,
1306
- stdoutTail,
1307
- stderrTail: bridgeStartupStderr,
1308
- stderrDone: bridgeStartupStderrDone
1309
- })
1310
- });
1311
- void drainBridgeProcessStream(proc.stdout);
1312
- const endpoint = await resolveBridgeEndpoint({
1313
- sandboxSession,
1314
- override: settings.portEndpoint,
1315
- port: boundPort
1316
- });
1317
- const bridgeEndpoint = withBridgeToken({ endpoint, token });
1318
- const channel = new SandboxChannel({
1319
- connect: buildConnect(bridgeEndpoint),
1320
- outboundSchema: outboundMessageSchema,
1321
- onDiagnostic,
1322
- onBridgeError,
1323
- reconnect: settings.reconnect,
1324
- // In replay mode the respawned bridge reloaded the finished turn from
1325
- // disk; seed the cursor and resume so it streams the tail (incl.
1326
- // `finish`) rather than starting empty.
1327
- ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
1328
- });
1329
- const finishListenerAttachment = respawnStrategy === "replay" ? channel.beginListenerAttachment() : void 0;
1330
- try {
1331
- await channel.open(
1332
- respawnStrategy === "replay" ? { resume: true } : void 0
1333
- );
1334
- } catch (error) {
1335
- finishListenerAttachment?.();
1336
- throw error;
1337
- }
1338
- return createSession({
1339
- sessionId: startOpts.sessionId,
1340
- channel,
1341
- finishListenerAttachment,
1342
- proc,
1343
- maxTurns: settings.maxTurns,
1344
- env: sandboxClaudeEnvironment,
1345
- thinking,
1346
- effort: settings.effort,
1347
- isResume: respawnStrategy !== void 0,
1348
- continueOnFirstPrompt: respawnStrategy !== void 0,
1349
- ...resumeSessionId ? { resumeSessionId } : {},
1350
- rerunContinue: respawnStrategy === "rerun",
1351
- bridgePort: boundPort,
1352
- bridgeToken: token,
1353
- sandboxId,
1354
- sandboxCredentialEnvironment,
1355
- debug: startOpts.observability?.debug,
1356
- permissionMode: startOpts.permissionMode,
1357
- builtinToolFiltering: startOpts.builtinToolFiltering,
1358
- sandbox: toolSafeSandboxSession,
1359
- sandboxHomeDir,
1360
- mcpServers: settings.mcpServers,
1361
- supportsUserMessageResponses: () => supportsUserMessageResponses
1362
- });
1363
- }
1364
- };
947
+ if (settings.mcpServers != null && Object.prototype.hasOwnProperty.call(settings.mcpServers, "harness-tools")) throw new Error("Claude Code MCP server name \"harness-tools\" is reserved for HarnessAgent tools.");
948
+ const thinking = settings.thinking ?? {
949
+ type: "adaptive",
950
+ display: "summarized"
951
+ };
952
+ return {
953
+ specificationVersion: "harness-v1",
954
+ harnessId: "claude-code",
955
+ builtinTools: CLAUDE_CODE_BUILTIN_TOOLS,
956
+ supportsBuiltinToolApprovals: true,
957
+ supportsBuiltinToolFiltering: true,
958
+ lifecycleStateSchema: claudeCodeResumeStateSchema,
959
+ getBootstrap: getClaudeCodeBootstrap,
960
+ doStart: async (startOpts) => {
961
+ const sandboxSession = startOpts.sandboxSession;
962
+ const toolSafeSandboxSession = getRestrictedSandboxSession(sandboxSession);
963
+ const sandboxId = "id" in sandboxSession ? sandboxSession.id : void 0;
964
+ validateBasicSandboxSettings({
965
+ sandboxSession,
966
+ port: settings.port,
967
+ portEndpoint: settings.portEndpoint
968
+ });
969
+ if (settings.mintBridgeToken != null && sandboxId == null) throw new HarnessCapabilityUnsupportedError({
970
+ harnessId: "claude-code",
971
+ message: "The Claude Code harness cannot use `mintBridgeToken` with a sandbox session that does not expose an id."
972
+ });
973
+ const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
974
+ const isResume = lifecycleState != null;
975
+ const isContinue = startOpts.continueFrom != null;
976
+ const resumeData = isResume ? lifecycleState?.data : void 0;
977
+ const coords = resumeData?.bridge;
978
+ const authenticationMode = resolveClaudeCodeAuthenticationMode(settings.auth);
979
+ const resolvedAuthEnvironment = await resolveClaudeCodeAuthentication({ auth: settings.auth });
980
+ const claudeEnvironment = {
981
+ ...resolvedAuthEnvironment,
982
+ ...resolvedAuthEnvironment.AI_GATEWAY_BASE_URL ? { CLAUDE_AGENT_SDK_CLIENT_APP: CLAUDE_CODE_CLIENT_APP } : {},
983
+ ...settings.env,
984
+ ...startOpts.headers != null ? { ANTHROPIC_CUSTOM_HEADERS: Object.entries(startOpts.headers).map(([name, value]) => `${name}: ${value}`).join("\n") } : {}
985
+ };
986
+ let sandboxClaudeEnvironment = claudeEnvironment;
987
+ let sandboxCredentialEnvironment;
988
+ if ("addRequestTransformations" in sandboxSession && sandboxSession.addRequestTransformations != null) {
989
+ sandboxCredentialEnvironment = await resolveSandboxCredentialEnvironment({
990
+ environment: claudeEnvironment,
991
+ credentialEnvironmentVariables: CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
992
+ credentialForwarding: settings.credentialForwarding,
993
+ previousSandboxCredentialEnvironment: resumeData?.sandboxCredentialEnvironment
994
+ });
995
+ sandboxClaudeEnvironment = {
996
+ ...claudeEnvironment,
997
+ ...sandboxCredentialEnvironment
998
+ };
999
+ const transformationSources = {
1000
+ env: claudeEnvironment,
1001
+ sandboxEnv: sandboxClaudeEnvironment,
1002
+ auth: authenticationMode
1003
+ };
1004
+ const requestTransformations = [...createClaudeCodeRequestTransformations(transformationSources), ...createClaudeCodeSubscriptionRequestTransformations(transformationSources)];
1005
+ if (requestTransformations.length > 0) await sandboxSession.addRequestTransformations(requestTransformations);
1006
+ } else {
1007
+ sandboxClaudeEnvironment = await applyCredentialForwarding({
1008
+ environment: sandboxClaudeEnvironment,
1009
+ credentialEnvironmentVariables: CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
1010
+ credentialForwarding: settings.credentialForwarding
1011
+ });
1012
+ warnCredentialBrokeringUnavailable({
1013
+ environment: claudeEnvironment,
1014
+ forwardedEnvironment: sandboxClaudeEnvironment,
1015
+ credentialEnvironmentVariables: CLAUDE_CODE_CREDENTIAL_ENVIRONMENT_VARIABLES
1016
+ });
1017
+ }
1018
+ const sandboxHomeDir = await resolveSandboxHomeDir({
1019
+ sandbox: toolSafeSandboxSession,
1020
+ abortSignal: startOpts.abortSignal
1021
+ });
1022
+ const stateDir = harnessStateDirectoryPath({ sandboxHomeDir });
1023
+ const bootstrapDir = posix.resolve(stateDir, CLAUDE_CODE_BOOTSTRAP_DIR);
1024
+ const resumeSessionId = resumeData?.claudeSessionId;
1025
+ const workDir = startOpts.sessionWorkDir;
1026
+ const bridgeStateDir = `${harnessSessionDataDirectoryPath({
1027
+ stateDirectory: stateDir,
1028
+ sessionId: startOpts.sessionId
1029
+ })}/bridge`;
1030
+ const timeoutMs = settings.startupTimeoutMs ?? 12e4;
1031
+ const report = startOpts.observability?.report;
1032
+ const onDiagnostic = report ? (frame) => report(harnessV1DiagnosticFromBridgeFrame(frame, {
1033
+ sessionId: startOpts.sessionId,
1034
+ timestamp: Date.now()
1035
+ })) : void 0;
1036
+ const onBridgeError = createBridgeErrorHandler({
1037
+ harnessId: "claude-code",
1038
+ sessionId: startOpts.sessionId
1039
+ });
1040
+ let supportsUserMessageResponses = false;
1041
+ const buildConnect = (endpoint) => async ({ abortSignal }) => {
1042
+ return openBridgeWebSocket({
1043
+ endpoint,
1044
+ timeoutMs,
1045
+ abortSignal,
1046
+ onHello: (supportsResponses) => {
1047
+ supportsUserMessageResponses = supportsResponses;
1048
+ }
1049
+ });
1050
+ };
1051
+ if (coords) try {
1052
+ const endpoint = await resolveBridgeEndpoint({
1053
+ sandboxSession,
1054
+ override: settings.portEndpoint,
1055
+ port: coords.port
1056
+ });
1057
+ const attachEndpoint = withBridgeToken({
1058
+ endpoint,
1059
+ token: coords.token
1060
+ });
1061
+ const attachChannel = new SandboxChannel({
1062
+ connect: buildConnect(attachEndpoint),
1063
+ outboundSchema: outboundMessageSchema,
1064
+ initialLastSeenEventId: coords.lastSeenEventId,
1065
+ onDiagnostic,
1066
+ onBridgeError,
1067
+ reconnect: settings.reconnect
1068
+ });
1069
+ const finishListenerAttachment = isContinue ? attachChannel.beginListenerAttachment() : void 0;
1070
+ try {
1071
+ await attachChannel.open(isContinue ? { resume: true } : void 0);
1072
+ } catch (error) {
1073
+ finishListenerAttachment?.();
1074
+ throw error;
1075
+ }
1076
+ return createSession({
1077
+ sessionId: startOpts.sessionId,
1078
+ channel: attachChannel,
1079
+ finishListenerAttachment,
1080
+ ...resumeSessionId ? { resumeSessionId } : {},
1081
+ proc: void 0,
1082
+ maxTurns: settings.maxTurns,
1083
+ agentProgressSummaries: settings.agentProgressSummaries,
1084
+ forwardSubagentText: settings.forwardSubagentText,
1085
+ env: sandboxClaudeEnvironment,
1086
+ thinking,
1087
+ effort: settings.effort,
1088
+ isResume: true,
1089
+ continueOnFirstPrompt: false,
1090
+ rerunContinue: false,
1091
+ bridgePort: coords.port,
1092
+ bridgeToken: coords.token,
1093
+ sandboxId,
1094
+ sandboxCredentialEnvironment,
1095
+ debug: startOpts.observability?.debug,
1096
+ permissionMode: startOpts.permissionMode,
1097
+ builtinToolFiltering: startOpts.builtinToolFiltering,
1098
+ sandbox: toolSafeSandboxSession,
1099
+ sandboxHomeDir,
1100
+ mcpServers: settings.mcpServers,
1101
+ supportsUserMessageResponses: () => supportsUserMessageResponses
1102
+ });
1103
+ } catch {}
1104
+ let respawnStrategy = isResume ? "rerun" : void 0;
1105
+ if (coords && isContinue) {
1106
+ const logRaw = await Promise.resolve(toolSafeSandboxSession.readTextFile({
1107
+ path: `${bridgeStateDir}/event-log.ndjson`,
1108
+ abortSignal: startOpts.abortSignal
1109
+ })).catch(() => null);
1110
+ if (await classifyDiskLog(logRaw) === "replay") respawnStrategy = "replay";
1111
+ }
1112
+ const port = resolveBridgePort({
1113
+ sandboxSession,
1114
+ override: settings.port
1115
+ });
1116
+ const token = settings.mintBridgeToken == null ? createBridgeToken() : settings.mintBridgeToken(sandboxId);
1117
+ const env = {
1118
+ BRIDGE_CHANNEL_TOKEN: token,
1119
+ BRIDGE_WS_PORT: String(port),
1120
+ ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
1121
+ };
1122
+ if (respawnStrategy === void 0) await toolSafeSandboxSession.run({
1123
+ command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
1124
+ abortSignal: startOpts.abortSignal
1125
+ });
1126
+ await markBridgeStarting({
1127
+ sandbox: toolSafeSandboxSession,
1128
+ bridgeStateDir,
1129
+ bridgeType: "claude-code",
1130
+ abortSignal: startOpts.abortSignal
1131
+ });
1132
+ const proc = await toolSafeSandboxSession.spawn({
1133
+ command: `node ${shellQuote(`${bootstrapDir}/bridge.mjs`)} --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)}`,
1134
+ env,
1135
+ abortSignal: startOpts.abortSignal
1136
+ });
1137
+ const bridgeStartupStderr = [];
1138
+ const bridgeStartupStderrDone = forwardBridgeProcessStream({
1139
+ stream: proc.stderr,
1140
+ streamName: "stderr",
1141
+ source: "claude-code",
1142
+ collectTail: bridgeStartupStderr
1143
+ });
1144
+ const { port: boundPort } = await waitForBridgeReady({
1145
+ proc,
1146
+ sandbox: toolSafeSandboxSession,
1147
+ bridgeStateDir,
1148
+ bridgeType: "claude-code",
1149
+ timeoutMs,
1150
+ abortSignal: startOpts.abortSignal,
1151
+ createTimeoutError: ({ proc, stdoutTail }) => createBridgeStartupError({
1152
+ message: "claude-code bridge did not become ready in time.",
1153
+ proc,
1154
+ stdoutTail,
1155
+ stderrTail: bridgeStartupStderr,
1156
+ stderrDone: bridgeStartupStderrDone
1157
+ }),
1158
+ createExitError: ({ proc, stdoutTail }) => createBridgeStartupError({
1159
+ message: "claude-code bridge exited before becoming ready.",
1160
+ proc,
1161
+ stdoutTail,
1162
+ stderrTail: bridgeStartupStderr,
1163
+ stderrDone: bridgeStartupStderrDone
1164
+ })
1165
+ });
1166
+ drainBridgeProcessStream(proc.stdout);
1167
+ const endpoint = await resolveBridgeEndpoint({
1168
+ sandboxSession,
1169
+ override: settings.portEndpoint,
1170
+ port: boundPort
1171
+ });
1172
+ const bridgeEndpoint = withBridgeToken({
1173
+ endpoint,
1174
+ token
1175
+ });
1176
+ const channel = new SandboxChannel({
1177
+ connect: buildConnect(bridgeEndpoint),
1178
+ outboundSchema: outboundMessageSchema,
1179
+ onDiagnostic,
1180
+ onBridgeError,
1181
+ reconnect: settings.reconnect,
1182
+ ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
1183
+ });
1184
+ const finishListenerAttachment = respawnStrategy === "replay" ? channel.beginListenerAttachment() : void 0;
1185
+ try {
1186
+ await channel.open(respawnStrategy === "replay" ? { resume: true } : void 0);
1187
+ } catch (error) {
1188
+ finishListenerAttachment?.();
1189
+ throw error;
1190
+ }
1191
+ return createSession({
1192
+ sessionId: startOpts.sessionId,
1193
+ channel,
1194
+ finishListenerAttachment,
1195
+ proc,
1196
+ maxTurns: settings.maxTurns,
1197
+ agentProgressSummaries: settings.agentProgressSummaries,
1198
+ forwardSubagentText: settings.forwardSubagentText,
1199
+ env: sandboxClaudeEnvironment,
1200
+ thinking,
1201
+ effort: settings.effort,
1202
+ isResume: respawnStrategy !== void 0,
1203
+ continueOnFirstPrompt: respawnStrategy !== void 0,
1204
+ ...resumeSessionId ? { resumeSessionId } : {},
1205
+ rerunContinue: respawnStrategy === "rerun",
1206
+ bridgePort: boundPort,
1207
+ bridgeToken: token,
1208
+ sandboxId,
1209
+ sandboxCredentialEnvironment,
1210
+ debug: startOpts.observability?.debug,
1211
+ permissionMode: startOpts.permissionMode,
1212
+ builtinToolFiltering: startOpts.builtinToolFiltering,
1213
+ sandbox: toolSafeSandboxSession,
1214
+ sandboxHomeDir,
1215
+ mcpServers: settings.mcpServers,
1216
+ supportsUserMessageResponses: () => supportsUserMessageResponses
1217
+ });
1218
+ }
1219
+ };
1365
1220
  }
1366
- function resolveBridgePort({
1367
- sandboxSession,
1368
- override
1369
- }) {
1370
- if (override !== void 0) return override;
1371
- if ("ports" in sandboxSession && sandboxSession.ports.length > 0) {
1372
- return sandboxSession.ports[0];
1373
- }
1374
- throw new HarnessCapabilityUnsupportedError({
1375
- harnessId: "claude-code",
1376
- message: "The claude-code harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createClaudeCode({ port })`."
1377
- });
1221
+ function resolveBridgePort({ sandboxSession, override }) {
1222
+ if (override !== void 0) return override;
1223
+ if ("ports" in sandboxSession && sandboxSession.ports.length > 0) return sandboxSession.ports[0];
1224
+ throw new HarnessCapabilityUnsupportedError({
1225
+ harnessId: "claude-code",
1226
+ message: "The claude-code harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createClaudeCode({ port })`."
1227
+ });
1378
1228
  }
1379
- function validateBasicSandboxSettings({
1380
- sandboxSession,
1381
- port,
1382
- portEndpoint
1383
- }) {
1384
- if ("getPortEndpoint" in sandboxSession) return;
1385
- if (port == null) {
1386
- throw new HarnessCapabilityUnsupportedError({
1387
- harnessId: "claude-code",
1388
- message: "The Claude Code harness requires an explicit `port` when using a basic sandbox session."
1389
- });
1390
- }
1391
- if (portEndpoint == null) {
1392
- throw new HarnessCapabilityUnsupportedError({
1393
- harnessId: "claude-code",
1394
- message: "The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session."
1395
- });
1396
- }
1229
+ function validateBasicSandboxSettings({ sandboxSession, port, portEndpoint }) {
1230
+ if ("getPortEndpoint" in sandboxSession) return;
1231
+ if (port == null) throw new HarnessCapabilityUnsupportedError({
1232
+ harnessId: "claude-code",
1233
+ message: "The Claude Code harness requires an explicit `port` when using a basic sandbox session."
1234
+ });
1235
+ if (portEndpoint == null) throw new HarnessCapabilityUnsupportedError({
1236
+ harnessId: "claude-code",
1237
+ message: "The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session."
1238
+ });
1397
1239
  }
1398
- async function resolveBridgeEndpoint({
1399
- sandboxSession,
1400
- override,
1401
- port
1402
- }) {
1403
- if (override != null) return override;
1404
- if ("getPortEndpoint" in sandboxSession) {
1405
- return sandboxSession.getPortEndpoint({ port, protocol: "ws" });
1406
- }
1407
- throw new HarnessCapabilityUnsupportedError({
1408
- harnessId: "claude-code",
1409
- message: "The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session."
1410
- });
1240
+ async function resolveBridgeEndpoint({ sandboxSession, override, port }) {
1241
+ if (override != null) return override;
1242
+ if ("getPortEndpoint" in sandboxSession) return sandboxSession.getPortEndpoint({
1243
+ port,
1244
+ protocol: "ws"
1245
+ });
1246
+ throw new HarnessCapabilityUnsupportedError({
1247
+ harnessId: "claude-code",
1248
+ message: "The Claude Code harness requires an explicit `portEndpoint` when using a basic sandbox session."
1249
+ });
1411
1250
  }
1412
- function openWebSocketAndWaitForBridgeHello({
1413
- endpoint,
1414
- openTimeoutMs,
1415
- getHelloTimeoutMs,
1416
- abortSignal,
1417
- onHello
1418
- }) {
1419
- return new Promise((resolve2, reject) => {
1420
- const abortReason = () => abortSignal.reason ?? new Error("WebSocket connection aborted");
1421
- if (abortSignal.aborted) {
1422
- reject(abortReason());
1423
- return;
1424
- }
1425
- const ws = new WebSocket(endpoint.url, {
1426
- headers: endpoint.headers == null ? void 0 : { ...endpoint.headers }
1427
- });
1428
- let opened = false;
1429
- let sawBridgeHello = false;
1430
- let settled = false;
1431
- let openTimer;
1432
- let helloTimer;
1433
- let onAbort;
1434
- const cleanup = ({
1435
- keepTerminationListeners = false
1436
- } = {}) => {
1437
- if (openTimer) clearTimeout(openTimer);
1438
- if (helloTimer) clearTimeout(helloTimer);
1439
- ws.off("open", onOpen);
1440
- ws.off("message", onMessage);
1441
- if (onAbort != null) {
1442
- abortSignal.removeEventListener("abort", onAbort);
1443
- }
1444
- if (!keepTerminationListeners) {
1445
- ws.off("close", onClose);
1446
- ws.off("error", onError);
1447
- }
1448
- };
1449
- const settle = (err) => {
1450
- if (settled) return;
1451
- settled = true;
1452
- if (err) {
1453
- cleanup({ keepTerminationListeners: true });
1454
- try {
1455
- ws.terminate();
1456
- } catch {
1457
- cleanup();
1458
- }
1459
- reject(err);
1460
- } else {
1461
- cleanup();
1462
- resolve2(ws);
1463
- }
1464
- };
1465
- const tryResolve = () => {
1466
- if (opened && sawBridgeHello) settle();
1467
- };
1468
- const startHelloTimer = () => {
1469
- if (helloTimer) return;
1470
- const helloTimeoutMs = getHelloTimeoutMs();
1471
- helloTimer = setTimeout(
1472
- () => settle(
1473
- new Error(
1474
- `claude-code bridge did not send bridge-hello within ${helloTimeoutMs}ms`
1475
- )
1476
- ),
1477
- helloTimeoutMs
1478
- );
1479
- helloTimer.unref?.();
1480
- };
1481
- const onOpen = () => {
1482
- opened = true;
1483
- if (openTimer) {
1484
- clearTimeout(openTimer);
1485
- openTimer = void 0;
1486
- }
1487
- startHelloTimer();
1488
- tryResolve();
1489
- };
1490
- const onMessage = (raw) => {
1491
- void (async () => {
1492
- const parsed = await safeParseJSON2({
1493
- text: webSocketMessageToString(raw)
1494
- });
1495
- if (!parsed.success || settled) return;
1496
- const value = parsed.value;
1497
- if (typeof value === "object" && value !== null && !Array.isArray(value) && value.type === "bridge-hello") {
1498
- const capabilities = value.capabilities;
1499
- onHello(capabilities?.experimental_userMessageResponses === true);
1500
- sawBridgeHello = true;
1501
- tryResolve();
1502
- }
1503
- })();
1504
- };
1505
- const onClose = () => {
1506
- settle(
1507
- new Error("claude-code bridge closed before sending bridge-hello")
1508
- );
1509
- cleanup();
1510
- };
1511
- const onError = (err) => settle(err);
1512
- onAbort = () => settle(abortReason());
1513
- openTimer = setTimeout(
1514
- () => settle(new Error(`WebSocket open timed out after ${openTimeoutMs}ms`)),
1515
- openTimeoutMs
1516
- );
1517
- openTimer.unref?.();
1518
- ws.on("open", onOpen);
1519
- ws.on("message", onMessage);
1520
- ws.on("close", onClose);
1521
- ws.on("error", onError);
1522
- abortSignal.addEventListener("abort", onAbort, { once: true });
1523
- });
1251
+ /**
1252
+ * Materialise skill files into
1253
+ * `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
1254
+ * auto-discovers skills from that directory on startup, so the files have to
1255
+ * be in place before the bridge is spawned without mutating the session
1256
+ * workdir. Each file uses the YAML-frontmatter shape the CLI expects.
1257
+ */
1258
+ /**
1259
+ * Wait for the bridge's `bridge-hello` message to arrive on the freshly
1260
+ * opened WebSocket before any other host-side code touches it.
1261
+ *
1262
+ * Some sandbox runtimes (Vercel in particular) complete the WS upgrade
1263
+ * with the host long before the connection is actually forwarded to the
1264
+ * sandbox-side bridge. Anything sent in that gap is silently dropped —
1265
+ * including the `start` message we send next. The bridge emits
1266
+ * `bridge-hello` the instant it accepts the connection, so receiving it
1267
+ * is the only reliable evidence that the end-to-end link is live.
1268
+ */
1269
+ function openWebSocketAndWaitForBridgeHello({ endpoint, openTimeoutMs, getHelloTimeoutMs, abortSignal, onHello }) {
1270
+ return new Promise((resolve, reject) => {
1271
+ const abortReason = () => abortSignal.reason ?? /* @__PURE__ */ new Error("WebSocket connection aborted");
1272
+ if (abortSignal.aborted) {
1273
+ reject(abortReason());
1274
+ return;
1275
+ }
1276
+ const ws = new WebSocket(endpoint.url, { headers: endpoint.headers == null ? void 0 : { ...endpoint.headers } });
1277
+ let opened = false;
1278
+ let sawBridgeHello = false;
1279
+ let settled = false;
1280
+ let openTimer;
1281
+ let helloTimer;
1282
+ let onAbort;
1283
+ const cleanup = ({ keepTerminationListeners = false } = {}) => {
1284
+ if (openTimer) clearTimeout(openTimer);
1285
+ if (helloTimer) clearTimeout(helloTimer);
1286
+ ws.off("open", onOpen);
1287
+ ws.off("message", onMessage);
1288
+ if (onAbort != null) abortSignal.removeEventListener("abort", onAbort);
1289
+ if (!keepTerminationListeners) {
1290
+ ws.off("close", onClose);
1291
+ ws.off("error", onError);
1292
+ }
1293
+ };
1294
+ const settle = (err) => {
1295
+ if (settled) return;
1296
+ settled = true;
1297
+ if (err) {
1298
+ cleanup({ keepTerminationListeners: true });
1299
+ try {
1300
+ ws.terminate();
1301
+ } catch {
1302
+ cleanup();
1303
+ }
1304
+ reject(err);
1305
+ } else {
1306
+ cleanup();
1307
+ resolve(ws);
1308
+ }
1309
+ };
1310
+ const tryResolve = () => {
1311
+ if (opened && sawBridgeHello) settle();
1312
+ };
1313
+ const startHelloTimer = () => {
1314
+ if (helloTimer) return;
1315
+ const helloTimeoutMs = getHelloTimeoutMs();
1316
+ helloTimer = setTimeout(() => settle(/* @__PURE__ */ new Error(`claude-code bridge did not send bridge-hello within ${helloTimeoutMs}ms`)), helloTimeoutMs);
1317
+ helloTimer.unref?.();
1318
+ };
1319
+ const onOpen = () => {
1320
+ opened = true;
1321
+ if (openTimer) {
1322
+ clearTimeout(openTimer);
1323
+ openTimer = void 0;
1324
+ }
1325
+ startHelloTimer();
1326
+ tryResolve();
1327
+ };
1328
+ const onMessage = (raw) => {
1329
+ (async () => {
1330
+ const parsed = await safeParseJSON({ text: webSocketMessageToString(raw) });
1331
+ if (!parsed.success || settled) return;
1332
+ const value = parsed.value;
1333
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && value.type === "bridge-hello") {
1334
+ const capabilities = value.capabilities;
1335
+ onHello(capabilities?.experimental_userMessageResponses === true);
1336
+ sawBridgeHello = true;
1337
+ tryResolve();
1338
+ }
1339
+ })();
1340
+ };
1341
+ const onClose = () => {
1342
+ settle(/* @__PURE__ */ new Error("claude-code bridge closed before sending bridge-hello"));
1343
+ cleanup();
1344
+ };
1345
+ const onError = (err) => settle(err);
1346
+ onAbort = () => settle(abortReason());
1347
+ openTimer = setTimeout(() => settle(/* @__PURE__ */ new Error(`WebSocket open timed out after ${openTimeoutMs}ms`)), openTimeoutMs);
1348
+ openTimer.unref?.();
1349
+ ws.on("open", onOpen);
1350
+ ws.on("message", onMessage);
1351
+ ws.on("close", onClose);
1352
+ ws.on("error", onError);
1353
+ abortSignal.addEventListener("abort", onAbort, { once: true });
1354
+ });
1524
1355
  }
1525
- async function openBridgeWebSocket({
1526
- endpoint,
1527
- timeoutMs,
1528
- abortSignal,
1529
- onHello
1530
- }) {
1531
- const deadline = Date.now() + timeoutMs;
1532
- let attempt = 0;
1533
- let lastError;
1534
- while (!abortSignal.aborted && Date.now() < deadline) {
1535
- attempt++;
1536
- try {
1537
- const remaining = Math.max(1, deadline - Date.now());
1538
- return await openWebSocketAndWaitForBridgeHello({
1539
- endpoint,
1540
- openTimeoutMs: Math.min(1e4, remaining),
1541
- getHelloTimeoutMs: () => Math.min(5e3, Math.max(1, deadline - Date.now())),
1542
- abortSignal,
1543
- onHello
1544
- });
1545
- } catch (err) {
1546
- if (abortSignal.aborted) {
1547
- throw abortSignal.reason ?? new Error("WebSocket connection aborted");
1548
- }
1549
- lastError = err;
1550
- const remaining = deadline - Date.now();
1551
- if (remaining <= 0) break;
1552
- await sleep({
1553
- ms: Math.min(250 * attempt, 1e3, remaining),
1554
- abortSignal
1555
- });
1556
- }
1557
- }
1558
- if (abortSignal.aborted) {
1559
- throw abortSignal.reason ?? new Error("WebSocket connection aborted");
1560
- }
1561
- throw new Error(
1562
- `claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`
1563
- );
1356
+ async function openBridgeWebSocket({ endpoint, timeoutMs, abortSignal, onHello }) {
1357
+ const deadline = Date.now() + timeoutMs;
1358
+ let attempt = 0;
1359
+ let lastError;
1360
+ while (!abortSignal.aborted && Date.now() < deadline) {
1361
+ attempt++;
1362
+ try {
1363
+ const remaining = Math.max(1, deadline - Date.now());
1364
+ return await openWebSocketAndWaitForBridgeHello({
1365
+ endpoint,
1366
+ openTimeoutMs: Math.min(1e4, remaining),
1367
+ getHelloTimeoutMs: () => Math.min(5e3, Math.max(1, deadline - Date.now())),
1368
+ abortSignal,
1369
+ onHello
1370
+ });
1371
+ } catch (err) {
1372
+ if (abortSignal.aborted) throw abortSignal.reason ?? /* @__PURE__ */ new Error("WebSocket connection aborted");
1373
+ lastError = err;
1374
+ const remaining = deadline - Date.now();
1375
+ if (remaining <= 0) break;
1376
+ await sleep({
1377
+ ms: Math.min(250 * attempt, 1e3, remaining),
1378
+ abortSignal
1379
+ });
1380
+ }
1381
+ }
1382
+ if (abortSignal.aborted) throw abortSignal.reason ?? /* @__PURE__ */ new Error("WebSocket connection aborted");
1383
+ throw new Error(`claude-code bridge did not complete WebSocket handshake within ${timeoutMs}ms after ${attempt} attempt(s). Last error: ${formatUnknownError(lastError)}`);
1564
1384
  }
1565
1385
  function webSocketMessageToString(raw) {
1566
- if (typeof raw === "string") return raw;
1567
- if (Buffer.isBuffer(raw)) return raw.toString("utf8");
1568
- if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
1569
- if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
1570
- if (ArrayBuffer.isView(raw)) {
1571
- return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString(
1572
- "utf8"
1573
- );
1574
- }
1575
- return String(raw);
1386
+ if (typeof raw === "string") return raw;
1387
+ if (Buffer.isBuffer(raw)) return raw.toString("utf8");
1388
+ if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8");
1389
+ if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString("utf8");
1390
+ if (ArrayBuffer.isView(raw)) return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString("utf8");
1391
+ return String(raw);
1576
1392
  }
1577
1393
  function formatUnknownError(error) {
1578
- if (error instanceof Error) return error.message;
1579
- return String(error);
1394
+ if (error instanceof Error) return error.message;
1395
+ return String(error);
1580
1396
  }
1581
- function createSession({
1582
- sessionId,
1583
- channel,
1584
- finishListenerAttachment,
1585
- proc,
1586
- maxTurns,
1587
- env,
1588
- thinking,
1589
- effort,
1590
- isResume,
1591
- continueOnFirstPrompt,
1592
- resumeSessionId,
1593
- rerunContinue,
1594
- bridgePort,
1595
- bridgeToken,
1596
- sandboxId,
1597
- sandboxCredentialEnvironment,
1598
- debug,
1599
- permissionMode,
1600
- builtinToolFiltering,
1601
- sandbox,
1602
- sandboxHomeDir,
1603
- mcpServers,
1604
- supportsUserMessageResponses
1605
- }) {
1606
- let stopped = false;
1607
- let stopPromise;
1608
- let pendingResumeFlag = continueOnFirstPrompt;
1609
- let lastClaudeSessionId = resumeSessionId;
1610
- const wireTurn = (turnOpts) => {
1611
- let pendingResolve;
1612
- let pendingReject;
1613
- const done = new Promise((resolve2, reject) => {
1614
- pendingResolve = resolve2;
1615
- pendingReject = reject;
1616
- });
1617
- const userMessageSubmitter = supportsUserMessageResponses() ? experimental_createBridgeUserMessageSubmitter({
1618
- send: (message) => channel.send(message),
1619
- onResponse: (listener) => channel.on("user-message-response", listener),
1620
- onReconnect: (listener) => channel.onReconnect(listener)
1621
- }) : void 0;
1622
- const unsubs = [];
1623
- const forward = (event) => {
1624
- try {
1625
- turnOpts.emit(event);
1626
- } catch {
1627
- }
1628
- };
1629
- let isSettled = false;
1630
- const settleSuccess = () => {
1631
- if (isSettled) return;
1632
- isSettled = true;
1633
- userMessageSubmitter?.close();
1634
- for (const u of unsubs) u();
1635
- pendingResolve();
1636
- };
1637
- const settleError = (err) => {
1638
- if (isSettled) return;
1639
- isSettled = true;
1640
- userMessageSubmitter?.close(err);
1641
- for (const u of unsubs) u();
1642
- pendingReject(err);
1643
- };
1644
- const eventTypes = [
1645
- "stream-start",
1646
- "text-start",
1647
- "text-delta",
1648
- "text-end",
1649
- "reasoning-start",
1650
- "reasoning-delta",
1651
- "reasoning-end",
1652
- "tool-input-start",
1653
- "tool-input-delta",
1654
- "tool-input-end",
1655
- "tool-call",
1656
- "tool-approval-request",
1657
- "tool-result",
1658
- "finish-step",
1659
- "compaction",
1660
- "raw"
1661
- ];
1662
- for (const type of eventTypes) {
1663
- unsubs.push(
1664
- channel.on(type, (msg) => {
1665
- forward(msg);
1666
- })
1667
- );
1668
- }
1669
- unsubs.push(
1670
- channel.on("finish", (msg) => {
1671
- const metadata = msg.harnessMetadata?.["claude-code"]?.sessionId;
1672
- if (typeof metadata === "string" && metadata.length > 0) {
1673
- lastClaudeSessionId = metadata;
1674
- }
1675
- forward(msg);
1676
- settleSuccess();
1677
- })
1678
- );
1679
- unsubs.push(
1680
- channel.on("error", (msg) => {
1681
- forward(msg);
1682
- settleError(msg.error);
1683
- })
1684
- );
1685
- finishListenerAttachment?.();
1686
- finishListenerAttachment = void 0;
1687
- const onClose = (_code, reason) => {
1688
- if (isSettled) return;
1689
- if (reason === "suspended") {
1690
- settleSuccess();
1691
- return;
1692
- }
1693
- settleError(
1694
- new Error("claude-code bridge closed before the turn finished.")
1695
- );
1696
- };
1697
- channel.onClose(onClose);
1698
- const onAbort = () => {
1699
- if (isSettled) return;
1700
- try {
1701
- channel.send({ type: "abort" });
1702
- } catch {
1703
- }
1704
- settleError(
1705
- turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError")
1706
- );
1707
- };
1708
- if (turnOpts.abortSignal) {
1709
- if (turnOpts.abortSignal.aborted) {
1710
- onAbort();
1711
- } else {
1712
- turnOpts.abortSignal.addEventListener("abort", onAbort, {
1713
- once: true
1714
- });
1715
- }
1716
- }
1717
- return {
1718
- submitToolResult: async (input) => {
1719
- channel.send({
1720
- type: "tool-result",
1721
- toolCallId: input.toolCallId,
1722
- output: input.output,
1723
- isError: input.isError,
1724
- ...input.toolResult !== void 0 ? { toolResult: input.toolResult } : {}
1725
- });
1726
- },
1727
- submitToolApproval: async (input) => {
1728
- channel.send({
1729
- type: "tool-approval-response",
1730
- approvalId: input.approvalId,
1731
- approved: input.approved,
1732
- reason: input.reason
1733
- });
1734
- },
1735
- ...userMessageSubmitter == null ? {} : {
1736
- submitUserMessage: async (text) => {
1737
- await userMessageSubmitter.submit(text);
1738
- }
1739
- },
1740
- done
1741
- };
1742
- };
1743
- const prepareTurn = async (turnOpts) => {
1744
- if (turnOpts.responseFormat?.type === "json" && turnOpts.responseFormat.schema == null) {
1745
- throw new HarnessCapabilityUnsupportedError({
1746
- message: "Harness 'claude-code' requires a JSON schema for structured output.",
1747
- harnessId: "claude-code"
1748
- });
1749
- }
1750
- await writeSkills({
1751
- sandbox,
1752
- homePath: sandboxHomeDir,
1753
- skillsDir: ".claude/skills",
1754
- skills: turnOpts.skills,
1755
- abortSignal: turnOpts.abortSignal,
1756
- invalidSkillNameMessage: ({ name }) => `Invalid Claude Code skill name: ${name}`,
1757
- invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid Claude Code skill file path for ${skillName}: ${filePath}`,
1758
- trailingNewline: true
1759
- });
1760
- return wireTurn({
1761
- emit: turnOpts.emit,
1762
- abortSignal: turnOpts.abortSignal
1763
- });
1764
- };
1765
- return {
1766
- sessionId,
1767
- isResume,
1768
- doPromptTurn: async (promptOpts) => {
1769
- const control = await prepareTurn(promptOpts);
1770
- if (promptOpts.abortSignal?.aborted) {
1771
- return control;
1772
- }
1773
- const startMessage = {
1774
- type: "start",
1775
- prompt: extractUserText(promptOpts.prompt),
1776
- tools: (promptOpts.tools ?? []).map((t) => ({
1777
- name: t.name,
1778
- description: t.description,
1779
- inputSchema: t.inputSchema
1780
- })),
1781
- ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
1782
- ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
1783
- model: promptOpts.model,
1784
- maxTurns,
1785
- ...env !== void 0 ? { env } : {},
1786
- thinking,
1787
- ...effort !== void 0 ? { effort } : {},
1788
- ...promptOpts.skills.length > 0 ? { skills: promptOpts.skills.map((skill) => skill.name) } : {},
1789
- ...mcpServers == null ? {} : { mcpServers },
1790
- ...permissionMode ? { permissionMode } : {},
1791
- ...builtinToolFiltering ? { builtinToolFiltering } : {},
1792
- ...debug ? { debug } : {},
1793
- ...lastClaudeSessionId ? { resumeSessionId: lastClaudeSessionId } : pendingResumeFlag ? { continue: true } : {}
1794
- };
1795
- pendingResumeFlag = false;
1796
- channel.send(startMessage);
1797
- return control;
1798
- },
1799
- doContinueTurn: async (continueOpts) => {
1800
- const control = await prepareTurn(continueOpts);
1801
- if (rerunContinue && !continueOpts.abortSignal?.aborted) {
1802
- pendingResumeFlag = false;
1803
- channel.send({
1804
- type: "start",
1805
- /*
1806
- * A continuation nudge rather than an empty prompt: `continue: true`
1807
- * rehydrates the prior thread, and this is the new user turn that
1808
- * drives it forward. It must be non-empty — an empty text block is
1809
- * rejected by the Anthropic API once the SDK stamps it with
1810
- * `cache_control`.
1811
- */
1812
- prompt: "Continue.",
1813
- tools: (continueOpts.tools ?? []).map((t) => ({
1814
- name: t.name,
1815
- description: t.description,
1816
- inputSchema: t.inputSchema
1817
- })),
1818
- ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
1819
- ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
1820
- model: continueOpts.model,
1821
- maxTurns,
1822
- ...env !== void 0 ? { env } : {},
1823
- thinking,
1824
- ...effort !== void 0 ? { effort } : {},
1825
- ...continueOpts.skills.length > 0 ? { skills: continueOpts.skills.map((skill) => skill.name) } : {},
1826
- ...mcpServers == null ? {} : { mcpServers },
1827
- ...permissionMode ? { permissionMode } : {},
1828
- ...builtinToolFiltering ? { builtinToolFiltering } : {},
1829
- ...debug ? { debug } : {},
1830
- ...lastClaudeSessionId ? { resumeSessionId: lastClaudeSessionId } : { continue: true }
1831
- });
1832
- }
1833
- return control;
1834
- },
1835
- doCompact: async (customInstructions) => {
1836
- const text = customInstructions && customInstructions.trim() ? `/compact ${customInstructions.trim()}` : "/compact";
1837
- channel.send({ type: "user-message", text });
1838
- },
1839
- doDetach: async () => {
1840
- if (stopped) {
1841
- throw new Error(
1842
- `claude-code session ${sessionId} is already stopped; cannot detach.`
1843
- );
1844
- }
1845
- stopped = true;
1846
- const lastSeenEventId = await channel.suspend();
1847
- const payload = {
1848
- type: "resume-session",
1849
- harnessId: "claude-code",
1850
- specificationVersion: "harness-v1",
1851
- data: {
1852
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1853
- bridge: {
1854
- port: bridgePort,
1855
- token: bridgeToken,
1856
- lastSeenEventId,
1857
- ...sandboxId == null ? {} : { sandboxId }
1858
- },
1859
- ...lastClaudeSessionId ? { claudeSessionId: lastClaudeSessionId } : {}
1860
- }
1861
- };
1862
- return payload;
1863
- },
1864
- doDestroy: async () => {
1865
- if (stopped) return stopPromise;
1866
- stopped = true;
1867
- stopPromise = (async () => {
1868
- channel.beginClose();
1869
- try {
1870
- if (!channel.isClosed()) {
1871
- channel.send({ type: "destroy" });
1872
- }
1873
- } catch {
1874
- }
1875
- let stopTimer;
1876
- try {
1877
- if (proc) {
1878
- await Promise.race([
1879
- proc.wait(),
1880
- new Promise((resolve2) => {
1881
- stopTimer = setTimeout(resolve2, 5e3);
1882
- stopTimer.unref?.();
1883
- })
1884
- ]);
1885
- }
1886
- } finally {
1887
- if (stopTimer) clearTimeout(stopTimer);
1888
- try {
1889
- await proc?.kill();
1890
- } catch {
1891
- }
1892
- channel.close();
1893
- }
1894
- })();
1895
- return stopPromise;
1896
- },
1897
- doStop: async () => {
1898
- if (stopped) {
1899
- throw new Error(
1900
- `claude-code session ${sessionId} is already stopped; cannot stop.`
1901
- );
1902
- }
1903
- stopped = true;
1904
- channel.beginClose();
1905
- const data = channel.isClosed() ? {} : await new Promise((resolve2, reject) => {
1906
- const timer = setTimeout(() => {
1907
- unsub();
1908
- reject(
1909
- new Error(
1910
- `claude-code session ${sessionId} did not reply to stop within 5s.`
1911
- )
1912
- );
1913
- }, 5e3);
1914
- timer.unref?.();
1915
- const unsub = channel.on("bridge-stop", (msg) => {
1916
- clearTimeout(timer);
1917
- unsub();
1918
- resolve2(msg.data);
1919
- });
1920
- try {
1921
- channel.send({ type: "stop" });
1922
- } catch (err) {
1923
- clearTimeout(timer);
1924
- unsub();
1925
- reject(err);
1926
- }
1927
- });
1928
- let stopTimer;
1929
- try {
1930
- if (proc) {
1931
- await Promise.race([
1932
- proc.wait(),
1933
- new Promise((resolve2) => {
1934
- stopTimer = setTimeout(resolve2, 5e3);
1935
- stopTimer.unref?.();
1936
- })
1937
- ]);
1938
- }
1939
- } finally {
1940
- if (stopTimer) clearTimeout(stopTimer);
1941
- try {
1942
- await proc?.kill();
1943
- } catch {
1944
- }
1945
- channel.close();
1946
- }
1947
- const lifecycleData = data != null && typeof data === "object" && !Array.isArray(data) ? { ...data } : {};
1948
- const payload = {
1949
- type: "resume-session",
1950
- harnessId: "claude-code",
1951
- specificationVersion: "harness-v1",
1952
- // The bridge's stop reply carries the session id it observed; the
1953
- // adapter's own record backfills it when the reply predates the
1954
- // field or the channel was already closed.
1955
- data: {
1956
- ...lastClaudeSessionId ? { claudeSessionId: lastClaudeSessionId } : {},
1957
- ...lifecycleData,
1958
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment }
1959
- }
1960
- };
1961
- return payload;
1962
- },
1963
- doSuspendTurn: async () => {
1964
- if (stopped) {
1965
- throw new Error(
1966
- `claude-code session ${sessionId} is stopped; cannot suspend.`
1967
- );
1968
- }
1969
- stopped = true;
1970
- const lastSeenEventId = await channel.suspend();
1971
- const payload = {
1972
- type: "continue-turn",
1973
- harnessId: "claude-code",
1974
- specificationVersion: "harness-v1",
1975
- data: {
1976
- ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1977
- bridge: {
1978
- port: bridgePort,
1979
- token: bridgeToken,
1980
- lastSeenEventId,
1981
- ...sandboxId == null ? {} : { sandboxId }
1982
- },
1983
- ...lastClaudeSessionId ? { claudeSessionId: lastClaudeSessionId } : {}
1984
- }
1985
- };
1986
- return payload;
1987
- }
1988
- };
1397
+ function createSession({ sessionId, channel, finishListenerAttachment, proc, maxTurns, agentProgressSummaries, forwardSubagentText, env, thinking, effort, isResume, continueOnFirstPrompt, resumeSessionId, rerunContinue, bridgePort, bridgeToken, sandboxId, sandboxCredentialEnvironment, debug, permissionMode, builtinToolFiltering, sandbox, sandboxHomeDir, mcpServers, supportsUserMessageResponses }) {
1398
+ let stopped = false;
1399
+ let stopPromise;
1400
+ let pendingResumeFlag = continueOnFirstPrompt;
1401
+ let lastClaudeSessionId = resumeSessionId;
1402
+ const wireTurn = (turnOpts) => {
1403
+ let pendingResolve;
1404
+ let pendingReject;
1405
+ const done = new Promise((resolve, reject) => {
1406
+ pendingResolve = resolve;
1407
+ pendingReject = reject;
1408
+ });
1409
+ const userMessageSubmitter = supportsUserMessageResponses() ? experimental_createBridgeUserMessageSubmitter({
1410
+ send: (message) => channel.send(message),
1411
+ onResponse: (listener) => channel.on("user-message-response", listener),
1412
+ onReconnect: (listener) => channel.onReconnect(listener)
1413
+ }) : void 0;
1414
+ const unsubs = [];
1415
+ const forward = (event) => {
1416
+ try {
1417
+ turnOpts.emit(event);
1418
+ } catch {}
1419
+ };
1420
+ let isSettled = false;
1421
+ const settleSuccess = () => {
1422
+ if (isSettled) return;
1423
+ isSettled = true;
1424
+ userMessageSubmitter?.close();
1425
+ for (const u of unsubs) u();
1426
+ pendingResolve();
1427
+ };
1428
+ const settleError = (err) => {
1429
+ if (isSettled) return;
1430
+ isSettled = true;
1431
+ userMessageSubmitter?.close(err);
1432
+ for (const u of unsubs) u();
1433
+ pendingReject(err);
1434
+ };
1435
+ for (const type of [
1436
+ "stream-start",
1437
+ "text-start",
1438
+ "text-delta",
1439
+ "text-end",
1440
+ "reasoning-start",
1441
+ "reasoning-delta",
1442
+ "reasoning-end",
1443
+ "tool-input-start",
1444
+ "tool-input-delta",
1445
+ "tool-input-end",
1446
+ "tool-call",
1447
+ "tool-approval-request",
1448
+ "tool-result",
1449
+ "finish-step",
1450
+ "compaction",
1451
+ "raw"
1452
+ ]) unsubs.push(channel.on(type, (msg) => {
1453
+ forward(msg);
1454
+ }));
1455
+ unsubs.push(channel.on("finish", (msg) => {
1456
+ const metadata = msg.harnessMetadata?.["claude-code"]?.sessionId;
1457
+ if (typeof metadata === "string" && metadata.length > 0) lastClaudeSessionId = metadata;
1458
+ forward(msg);
1459
+ settleSuccess();
1460
+ }));
1461
+ unsubs.push(channel.on("error", (msg) => {
1462
+ forward(msg);
1463
+ settleError(msg.error);
1464
+ }));
1465
+ finishListenerAttachment?.();
1466
+ finishListenerAttachment = void 0;
1467
+ const onClose = (_code, reason) => {
1468
+ if (isSettled) return;
1469
+ if (reason === "suspended") {
1470
+ settleSuccess();
1471
+ return;
1472
+ }
1473
+ settleError(/* @__PURE__ */ new Error("claude-code bridge closed before the turn finished."));
1474
+ };
1475
+ channel.onClose(onClose);
1476
+ const onAbort = () => {
1477
+ if (isSettled) return;
1478
+ try {
1479
+ channel.send({ type: "abort" });
1480
+ } catch {}
1481
+ settleError(turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError"));
1482
+ };
1483
+ if (turnOpts.abortSignal) {
1484
+ if (turnOpts.abortSignal.aborted) onAbort();
1485
+ else turnOpts.abortSignal.addEventListener("abort", onAbort, { once: true });
1486
+ }
1487
+ return {
1488
+ submitToolResult: async (input) => {
1489
+ channel.send({
1490
+ type: "tool-result",
1491
+ toolCallId: input.toolCallId,
1492
+ output: input.output,
1493
+ isError: input.isError,
1494
+ ...input.toolResult !== void 0 ? { toolResult: input.toolResult } : {}
1495
+ });
1496
+ },
1497
+ submitToolApproval: async (input) => {
1498
+ channel.send({
1499
+ type: "tool-approval-response",
1500
+ approvalId: input.approvalId,
1501
+ approved: input.approved,
1502
+ reason: input.reason
1503
+ });
1504
+ },
1505
+ ...userMessageSubmitter == null ? {} : { submitUserMessage: async (text) => {
1506
+ await userMessageSubmitter.submit(text);
1507
+ } },
1508
+ done
1509
+ };
1510
+ };
1511
+ const prepareTurn = async (turnOpts) => {
1512
+ if (turnOpts.responseFormat?.type === "json" && turnOpts.responseFormat.schema == null) throw new HarnessCapabilityUnsupportedError({
1513
+ message: "Harness 'claude-code' requires a JSON schema for structured output.",
1514
+ harnessId: "claude-code"
1515
+ });
1516
+ await writeSkills({
1517
+ sandbox,
1518
+ homePath: sandboxHomeDir,
1519
+ skillsDir: ".claude/skills",
1520
+ skills: turnOpts.skills,
1521
+ abortSignal: turnOpts.abortSignal,
1522
+ invalidSkillNameMessage: ({ name }) => `Invalid Claude Code skill name: ${name}`,
1523
+ invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid Claude Code skill file path for ${skillName}: ${filePath}`,
1524
+ trailingNewline: true
1525
+ });
1526
+ return wireTurn({
1527
+ emit: turnOpts.emit,
1528
+ abortSignal: turnOpts.abortSignal
1529
+ });
1530
+ };
1531
+ return {
1532
+ sessionId,
1533
+ isResume,
1534
+ doPromptTurn: async (promptOpts) => {
1535
+ const control = await prepareTurn(promptOpts);
1536
+ if (promptOpts.abortSignal?.aborted) return control;
1537
+ const startMessage = {
1538
+ type: "start",
1539
+ prompt: extractUserText(promptOpts.prompt),
1540
+ tools: (promptOpts.tools ?? []).map((t) => ({
1541
+ name: t.name,
1542
+ description: t.description,
1543
+ inputSchema: t.inputSchema
1544
+ })),
1545
+ ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
1546
+ ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
1547
+ model: promptOpts.model,
1548
+ maxTurns,
1549
+ ...agentProgressSummaries !== void 0 ? { agentProgressSummaries } : {},
1550
+ ...forwardSubagentText !== void 0 ? { forwardSubagentText } : {},
1551
+ ...env !== void 0 ? { env } : {},
1552
+ thinking,
1553
+ ...effort !== void 0 ? { effort } : {},
1554
+ ...promptOpts.skills.length > 0 ? { skills: promptOpts.skills.map((skill) => skill.name) } : {},
1555
+ ...mcpServers == null ? {} : { mcpServers },
1556
+ ...permissionMode ? { permissionMode } : {},
1557
+ ...builtinToolFiltering ? { builtinToolFiltering } : {},
1558
+ ...debug ? { debug } : {},
1559
+ ...lastClaudeSessionId ? { resumeSessionId: lastClaudeSessionId } : pendingResumeFlag ? { continue: true } : {}
1560
+ };
1561
+ pendingResumeFlag = false;
1562
+ channel.send(startMessage);
1563
+ return control;
1564
+ },
1565
+ doContinueTurn: async (continueOpts) => {
1566
+ const control = await prepareTurn(continueOpts);
1567
+ if (rerunContinue && !continueOpts.abortSignal?.aborted) {
1568
+ pendingResumeFlag = false;
1569
+ channel.send({
1570
+ type: "start",
1571
+ prompt: "Continue.",
1572
+ tools: (continueOpts.tools ?? []).map((t) => ({
1573
+ name: t.name,
1574
+ description: t.description,
1575
+ inputSchema: t.inputSchema
1576
+ })),
1577
+ ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
1578
+ ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
1579
+ model: continueOpts.model,
1580
+ maxTurns,
1581
+ ...agentProgressSummaries !== void 0 ? { agentProgressSummaries } : {},
1582
+ ...forwardSubagentText !== void 0 ? { forwardSubagentText } : {},
1583
+ ...env !== void 0 ? { env } : {},
1584
+ thinking,
1585
+ ...effort !== void 0 ? { effort } : {},
1586
+ ...continueOpts.skills.length > 0 ? { skills: continueOpts.skills.map((skill) => skill.name) } : {},
1587
+ ...mcpServers == null ? {} : { mcpServers },
1588
+ ...permissionMode ? { permissionMode } : {},
1589
+ ...builtinToolFiltering ? { builtinToolFiltering } : {},
1590
+ ...debug ? { debug } : {},
1591
+ ...lastClaudeSessionId ? { resumeSessionId: lastClaudeSessionId } : { continue: true }
1592
+ });
1593
+ }
1594
+ return control;
1595
+ },
1596
+ doCompact: async (customInstructions) => {
1597
+ const text = customInstructions && customInstructions.trim() ? `/compact ${customInstructions.trim()}` : "/compact";
1598
+ channel.send({
1599
+ type: "user-message",
1600
+ text
1601
+ });
1602
+ },
1603
+ doDetach: async () => {
1604
+ if (stopped) throw new Error(`claude-code session ${sessionId} is already stopped; cannot detach.`);
1605
+ stopped = true;
1606
+ const lastSeenEventId = await channel.suspend();
1607
+ return {
1608
+ type: "resume-session",
1609
+ harnessId: "claude-code",
1610
+ specificationVersion: "harness-v1",
1611
+ data: {
1612
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1613
+ bridge: {
1614
+ port: bridgePort,
1615
+ token: bridgeToken,
1616
+ lastSeenEventId,
1617
+ ...sandboxId == null ? {} : { sandboxId }
1618
+ },
1619
+ ...lastClaudeSessionId ? { claudeSessionId: lastClaudeSessionId } : {}
1620
+ }
1621
+ };
1622
+ },
1623
+ doDestroy: async () => {
1624
+ if (stopped) return stopPromise;
1625
+ stopped = true;
1626
+ stopPromise = (async () => {
1627
+ channel.beginClose();
1628
+ try {
1629
+ if (!channel.isClosed()) channel.send({ type: "destroy" });
1630
+ } catch {}
1631
+ let stopTimer;
1632
+ try {
1633
+ if (proc) await Promise.race([proc.wait(), new Promise((resolve) => {
1634
+ stopTimer = setTimeout(resolve, 5e3);
1635
+ stopTimer.unref?.();
1636
+ })]);
1637
+ } finally {
1638
+ if (stopTimer) clearTimeout(stopTimer);
1639
+ try {
1640
+ await proc?.kill();
1641
+ } catch {}
1642
+ channel.close();
1643
+ }
1644
+ })();
1645
+ return stopPromise;
1646
+ },
1647
+ doStop: async () => {
1648
+ if (stopped) throw new Error(`claude-code session ${sessionId} is already stopped; cannot stop.`);
1649
+ stopped = true;
1650
+ channel.beginClose();
1651
+ const data = channel.isClosed() ? {} : await new Promise((resolve, reject) => {
1652
+ const timer = setTimeout(() => {
1653
+ unsub();
1654
+ reject(/* @__PURE__ */ new Error(`claude-code session ${sessionId} did not reply to stop within 5s.`));
1655
+ }, 5e3);
1656
+ timer.unref?.();
1657
+ const unsub = channel.on("bridge-stop", (msg) => {
1658
+ clearTimeout(timer);
1659
+ unsub();
1660
+ resolve(msg.data);
1661
+ });
1662
+ try {
1663
+ channel.send({ type: "stop" });
1664
+ } catch (err) {
1665
+ clearTimeout(timer);
1666
+ unsub();
1667
+ reject(err);
1668
+ }
1669
+ });
1670
+ let stopTimer;
1671
+ try {
1672
+ if (proc) await Promise.race([proc.wait(), new Promise((resolve) => {
1673
+ stopTimer = setTimeout(resolve, 5e3);
1674
+ stopTimer.unref?.();
1675
+ })]);
1676
+ } finally {
1677
+ if (stopTimer) clearTimeout(stopTimer);
1678
+ try {
1679
+ await proc?.kill();
1680
+ } catch {}
1681
+ channel.close();
1682
+ }
1683
+ const lifecycleData = data != null && typeof data === "object" && !Array.isArray(data) ? { ...data } : {};
1684
+ return {
1685
+ type: "resume-session",
1686
+ harnessId: "claude-code",
1687
+ specificationVersion: "harness-v1",
1688
+ data: {
1689
+ ...lastClaudeSessionId ? { claudeSessionId: lastClaudeSessionId } : {},
1690
+ ...lifecycleData,
1691
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment }
1692
+ }
1693
+ };
1694
+ },
1695
+ doSuspendTurn: async () => {
1696
+ if (stopped) throw new Error(`claude-code session ${sessionId} is stopped; cannot suspend.`);
1697
+ stopped = true;
1698
+ const lastSeenEventId = await channel.suspend();
1699
+ return {
1700
+ type: "continue-turn",
1701
+ harnessId: "claude-code",
1702
+ specificationVersion: "harness-v1",
1703
+ data: {
1704
+ ...sandboxCredentialEnvironment == null ? {} : { sandboxCredentialEnvironment },
1705
+ bridge: {
1706
+ port: bridgePort,
1707
+ token: bridgeToken,
1708
+ lastSeenEventId,
1709
+ ...sandboxId == null ? {} : { sandboxId }
1710
+ },
1711
+ ...lastClaudeSessionId ? { claudeSessionId: lastClaudeSessionId } : {}
1712
+ }
1713
+ };
1714
+ }
1715
+ };
1989
1716
  }
1990
1717
  function extractUserText(prompt) {
1991
- if (typeof prompt === "string") return prompt;
1992
- const { content } = prompt;
1993
- if (typeof content === "string") return content;
1994
- const parts = [];
1995
- for (const part of content) {
1996
- if (part.type !== "text") {
1997
- throw new HarnessCapabilityUnsupportedError({
1998
- harnessId: "claude-code",
1999
- message: `The claude-code harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
2000
- });
2001
- }
2002
- parts.push(part.text);
2003
- }
2004
- return parts.join("\n\n");
1718
+ if (typeof prompt === "string") return prompt;
1719
+ const { content } = prompt;
1720
+ if (typeof content === "string") return content;
1721
+ const parts = [];
1722
+ for (const part of content) {
1723
+ if (part.type !== "text") throw new HarnessCapabilityUnsupportedError({
1724
+ harnessId: "claude-code",
1725
+ message: `The claude-code harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
1726
+ });
1727
+ parts.push(part.text);
1728
+ }
1729
+ return parts.join("\n\n");
2005
1730
  }
1731
+ //#endregion
1732
+ //#region src/index.ts
1733
+ /**
1734
+ * Default `claude-code` harness instance with no overrides — suitable for the
1735
+ * common case where the underlying `claude` CLI's defaults are fine.
1736
+ * Equivalent to `createClaudeCode()`.
1737
+ */
1738
+ const claudeCode = createClaudeCode();
1739
+ //#endregion
1740
+ export { VERSION, claudeCode, createClaudeCode };
2006
1741
 
2007
- // src/index.ts
2008
- var claudeCode = createClaudeCode();
2009
- export {
2010
- VERSION,
2011
- claudeCode,
2012
- createClaudeCode
2013
- };
2014
1742
  //# sourceMappingURL=index.js.map