@ai-sdk/harness-claude-code 0.0.0 → 1.0.0-beta.10

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.
@@ -0,0 +1,1023 @@
1
+ // ../harness/dist/bridge/index.js
2
+ import { appendFile, mkdir, writeFile } from "fs/promises";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { env as procEnv, pid, stdout } from "process";
5
+ import { WebSocketServer } from "ws";
6
+ var DEBUG_LEVEL_WEIGHT = {
7
+ error: 0,
8
+ warn: 1,
9
+ info: 2,
10
+ debug: 3,
11
+ trace: 4
12
+ };
13
+ function subsystemMatches(filters, subsystem) {
14
+ if (!filters || filters.length === 0) return true;
15
+ return filters.some(
16
+ (filter) => subsystem === filter || subsystem.startsWith(`${filter}.`)
17
+ );
18
+ }
19
+ function formatBridgeError(err) {
20
+ if (err instanceof Error) {
21
+ return { name: err.name, message: err.message, stack: err.stack };
22
+ }
23
+ return { message: String(err) };
24
+ }
25
+ function parseEnvList(value) {
26
+ if (!value) return void 0;
27
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
28
+ return items.length > 0 ? items : void 0;
29
+ }
30
+ var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
31
+ var WS_OPEN = 1;
32
+ async function runBridge(options) {
33
+ const { bridgeType, bridgeStateDir: bridgeStateDir2, onStart, onDetach } = options;
34
+ const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
35
+ const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
36
+ const bridgeMetaPath = `${bridgeStateDir2}/bridge-meta.json`;
37
+ const startConfigPath = `${bridgeStateDir2}/start-config.json`;
38
+ const rerunStartConfigPath = `${bridgeStateDir2}/rerun-start-config.json`;
39
+ const eventLogPath = `${bridgeStateDir2}/event-log.ndjson`;
40
+ try {
41
+ await mkdir(bridgeStateDir2, { recursive: true });
42
+ } catch {
43
+ }
44
+ let currentBoundPort = 0;
45
+ let currentTurnState = "init";
46
+ let activeSocket;
47
+ let isFirstTurn = true;
48
+ let turnAbort;
49
+ let currentUserMessages;
50
+ let debugConfig;
51
+ let consoleCaptureInstalled = false;
52
+ const envDebugEnabled = ENV_TRUTHY.has(
53
+ (procEnv.HARNESS_DEBUG ?? "").toLowerCase()
54
+ );
55
+ let seqCounter = 0;
56
+ let eventLog = [];
57
+ let diskBuffer = "";
58
+ let flushPromise = null;
59
+ const flushEventsToDisk = async () => {
60
+ while (diskBuffer.length > 0) {
61
+ const buf = diskBuffer;
62
+ diskBuffer = "";
63
+ await appendFile(eventLogPath, buf).catch(() => {
64
+ });
65
+ }
66
+ };
67
+ const scheduleEventFlush = () => {
68
+ if (flushPromise) return;
69
+ flushPromise = new Promise((resolve) => {
70
+ setImmediate(() => {
71
+ void flushEventsToDisk().finally(resolve);
72
+ });
73
+ }).finally(() => {
74
+ flushPromise = null;
75
+ if (diskBuffer.length > 0) {
76
+ scheduleEventFlush();
77
+ }
78
+ });
79
+ };
80
+ const flushPendingEventsToDisk = async () => {
81
+ if (diskBuffer.length > 0 && !flushPromise) {
82
+ scheduleEventFlush();
83
+ }
84
+ let inFlight = flushPromise;
85
+ while (inFlight) {
86
+ await inFlight;
87
+ inFlight = flushPromise;
88
+ }
89
+ };
90
+ const replayFromDisk = procEnv.BRIDGE_REPLAY_FROM_DISK === "1";
91
+ if (replayFromDisk && existsSync(eventLogPath)) {
92
+ try {
93
+ const lines = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
94
+ eventLog = lines.map((line) => ({
95
+ seq: JSON.parse(line).seq,
96
+ line
97
+ }));
98
+ seqCounter = eventLog.at(-1)?.seq ?? 0;
99
+ } catch {
100
+ eventLog = [];
101
+ seqCounter = 0;
102
+ }
103
+ }
104
+ const pendingToolResults = /* @__PURE__ */ new Map();
105
+ const pendingToolApprovals = /* @__PURE__ */ new Map();
106
+ const writeBridgeMeta = async (state) => {
107
+ try {
108
+ await writeFile(
109
+ bridgeMetaPath,
110
+ JSON.stringify({
111
+ type: bridgeType,
112
+ port: currentBoundPort,
113
+ state,
114
+ pid
115
+ })
116
+ );
117
+ } catch {
118
+ }
119
+ };
120
+ const writeStartConfig = async (start) => {
121
+ try {
122
+ const serialized = JSON.stringify(start);
123
+ await writeFile(startConfigPath, serialized);
124
+ if (!existsSync(rerunStartConfigPath)) {
125
+ await writeFile(rerunStartConfigPath, serialized);
126
+ }
127
+ } catch {
128
+ }
129
+ };
130
+ const sendControl = (msg) => {
131
+ if (activeSocket?.readyState === WS_OPEN) {
132
+ try {
133
+ activeSocket.send(JSON.stringify(msg));
134
+ } catch {
135
+ }
136
+ }
137
+ };
138
+ const emit = (event) => {
139
+ const seq = ++seqCounter;
140
+ const line = JSON.stringify({ ...event, seq });
141
+ eventLog.push({ seq, line });
142
+ diskBuffer += `${line}
143
+ `;
144
+ scheduleEventFlush();
145
+ if (activeSocket?.readyState === WS_OPEN) {
146
+ try {
147
+ activeSocket.send(line);
148
+ } catch {
149
+ }
150
+ }
151
+ };
152
+ const replay = (ws, afterSeq) => {
153
+ for (const entry of eventLog) {
154
+ if (entry.seq > afterSeq && ws.readyState === WS_OPEN) {
155
+ ws.send(entry.line);
156
+ }
157
+ }
158
+ };
159
+ const shouldEmitDebugEvent = (level, subsystem) => {
160
+ if (!debugConfig?.enabled) return false;
161
+ const threshold = debugConfig.level ?? "debug";
162
+ if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
163
+ return subsystemMatches(debugConfig.subsystems, subsystem);
164
+ };
165
+ const rawStdoutWrite = process.stdout.write.bind(process.stdout);
166
+ const rawStderrWrite = process.stderr.write.bind(process.stderr);
167
+ const installConsoleCapture = () => {
168
+ if (consoleCaptureInstalled) return;
169
+ consoleCaptureInstalled = true;
170
+ const buffers = {
171
+ stdout: "",
172
+ stderr: ""
173
+ };
174
+ const patch = (stream, raw) => (chunk, encoding, cb) => {
175
+ if (debugConfig?.enabled) {
176
+ try {
177
+ const enc = typeof encoding === "string" ? encoding : "utf8";
178
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(
179
+ enc
180
+ );
181
+ const combined = buffers[stream] + text.replace(/\r\n/g, "\n");
182
+ const parts = combined.split("\n");
183
+ buffers[stream] = parts.pop() ?? "";
184
+ for (const line of parts) {
185
+ const trimmed = line.replace(/\s+$/, "");
186
+ if (trimmed) {
187
+ emit({
188
+ type: "sandbox-log",
189
+ source: bridgeType,
190
+ stream,
191
+ line: trimmed
192
+ });
193
+ }
194
+ }
195
+ } catch {
196
+ }
197
+ }
198
+ return raw(
199
+ chunk,
200
+ encoding,
201
+ cb
202
+ );
203
+ };
204
+ process.stdout.write = patch(
205
+ "stdout",
206
+ rawStdoutWrite
207
+ );
208
+ process.stderr.write = patch(
209
+ "stderr",
210
+ rawStderrWrite
211
+ );
212
+ };
213
+ const handleInbound = async (msg, ws) => {
214
+ switch (msg.type) {
215
+ case "start": {
216
+ const firstTurn = isFirstTurn;
217
+ isFirstTurn = false;
218
+ eventLog = [];
219
+ diskBuffer = "";
220
+ void writeFile(eventLogPath, "").catch(() => {
221
+ });
222
+ turnAbort = new AbortController();
223
+ currentTurnState = "running";
224
+ void writeStartConfig(msg);
225
+ void writeBridgeMeta("running");
226
+ const startDebug = msg.debug;
227
+ debugConfig = {
228
+ enabled: startDebug?.enabled ?? envDebugEnabled,
229
+ level: startDebug?.level ?? procEnv.HARNESS_DEBUG_LEVEL,
230
+ subsystems: startDebug?.subsystems ?? parseEnvList(procEnv.HARNESS_DEBUG_SUBSYSTEMS)
231
+ };
232
+ if (debugConfig.enabled) {
233
+ installConsoleCapture();
234
+ }
235
+ const turn = {
236
+ emit,
237
+ requestToolResult: (toolCallId) => new Promise((resolve) => {
238
+ pendingToolResults.set(toolCallId, resolve);
239
+ }),
240
+ requestToolApproval: (approvalId) => new Promise((resolve) => {
241
+ pendingToolApprovals.set(approvalId, resolve);
242
+ }),
243
+ pendingUserMessages: [],
244
+ abortSignal: turnAbort.signal,
245
+ firstTurn,
246
+ bridgeLog: (input) => {
247
+ const level = input.level ?? "debug";
248
+ if (!shouldEmitDebugEvent(level, input.subsystem)) return;
249
+ emit({
250
+ type: "debug-event",
251
+ level,
252
+ subsystem: input.subsystem,
253
+ message: input.message,
254
+ ...input.attrs ? { attrs: input.attrs } : {},
255
+ ...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
256
+ });
257
+ }
258
+ };
259
+ currentUserMessages = turn.pendingUserMessages;
260
+ try {
261
+ await onStart(msg, turn);
262
+ } catch (err) {
263
+ emit({ type: "error", error: serialiseError(err) });
264
+ } finally {
265
+ currentTurnState = "waiting";
266
+ void writeBridgeMeta("waiting");
267
+ }
268
+ return;
269
+ }
270
+ case "tool-result": {
271
+ const resolver = pendingToolResults.get(msg.toolCallId);
272
+ if (resolver) {
273
+ pendingToolResults.delete(msg.toolCallId);
274
+ resolver({ output: msg.output, isError: msg.isError });
275
+ }
276
+ return;
277
+ }
278
+ case "tool-approval-response": {
279
+ const resolver = pendingToolApprovals.get(msg.approvalId);
280
+ if (resolver) {
281
+ pendingToolApprovals.delete(msg.approvalId);
282
+ resolver({ approved: msg.approved, reason: msg.reason });
283
+ }
284
+ return;
285
+ }
286
+ case "user-message":
287
+ currentUserMessages?.push(msg.text);
288
+ return;
289
+ case "abort":
290
+ turnAbort?.abort();
291
+ return;
292
+ case "resume":
293
+ replay(ws, msg.lastSeenEventId);
294
+ return;
295
+ case "shutdown":
296
+ currentTurnState = "done";
297
+ void writeBridgeMeta("done");
298
+ drainThenExit(ws, 1e3, "shutdown");
299
+ return;
300
+ case "detach": {
301
+ currentTurnState = "done";
302
+ void writeBridgeMeta("done");
303
+ const data = await onDetach?.() ?? {};
304
+ sendControl({ type: "bridge-detach", data });
305
+ drainThenExit(ws, 1e3, "detach");
306
+ return;
307
+ }
308
+ }
309
+ };
310
+ void writeBridgeMeta("init");
311
+ const wss = new WebSocketServer({ port: bridgeWsPort, host: "0.0.0.0" });
312
+ const exit = () => {
313
+ if (options.onExit) {
314
+ options.onExit();
315
+ return;
316
+ }
317
+ wss.close(() => process.exit(0));
318
+ setTimeout(() => process.exit(0), 1e3).unref();
319
+ };
320
+ const drainThenExit = (ws, code, reason) => {
321
+ const start = Date.now();
322
+ const tick = () => {
323
+ const drained = ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN;
324
+ if (drained || Date.now() - start >= 5e3) {
325
+ void flushPendingEventsToDisk().finally(() => {
326
+ try {
327
+ ws.close(code, reason);
328
+ } finally {
329
+ exit();
330
+ }
331
+ });
332
+ return;
333
+ }
334
+ setTimeout(tick, 10).unref();
335
+ };
336
+ tick();
337
+ };
338
+ wss.on("listening", () => {
339
+ const addr = wss.address();
340
+ currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
341
+ currentTurnState = "waiting";
342
+ void writeBridgeMeta("waiting");
343
+ stdout.write(
344
+ JSON.stringify({
345
+ type: "bridge-ready",
346
+ port: currentBoundPort
347
+ }) + "\n"
348
+ );
349
+ options.onListening?.(currentBoundPort);
350
+ });
351
+ wss.on("connection", (ws, req) => {
352
+ const url = new URL(req.url ?? "/", "http://localhost");
353
+ if (url.searchParams.get("agent_bridge_token") !== expectedToken) {
354
+ ws.close(1008, "unauthorized");
355
+ return;
356
+ }
357
+ activeSocket = ws;
358
+ sendControl({
359
+ type: "bridge-hello",
360
+ state: currentTurnState,
361
+ lastSeq: seqCounter
362
+ });
363
+ ws.on("message", (raw) => {
364
+ let parsed;
365
+ try {
366
+ const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
367
+ parsed = JSON.parse(text);
368
+ } catch (err) {
369
+ sendControl({
370
+ type: "error",
371
+ error: `protocol parse error: ${err.message}`
372
+ });
373
+ return;
374
+ }
375
+ void handleInbound(parsed, ws);
376
+ });
377
+ ws.on("close", () => {
378
+ if (activeSocket === ws) {
379
+ activeSocket = void 0;
380
+ }
381
+ });
382
+ ws.on("error", () => {
383
+ });
384
+ });
385
+ process.on("uncaughtException", (err) => {
386
+ emit({ type: "error", error: serialiseError(err) });
387
+ });
388
+ process.on("unhandledRejection", (err) => {
389
+ emit({ type: "error", error: serialiseError(err) });
390
+ });
391
+ await new Promise((resolve, reject) => {
392
+ if (wss.address() != null) {
393
+ resolve();
394
+ return;
395
+ }
396
+ wss.once("listening", resolve);
397
+ wss.once("error", reject);
398
+ });
399
+ return {
400
+ port: currentBoundPort,
401
+ close: () => new Promise((resolve) => {
402
+ wss.close(() => resolve());
403
+ })
404
+ };
405
+ }
406
+ function serialiseError(err) {
407
+ if (err instanceof Error) {
408
+ return { name: err.name, message: err.message, stack: err.stack };
409
+ }
410
+ return err;
411
+ }
412
+
413
+ // src/bridge/compaction-latch.ts
414
+ function createCompactionLatch(emit) {
415
+ let boundary;
416
+ let summary;
417
+ const tryEmit = () => {
418
+ if (!boundary || summary === void 0) return;
419
+ emit({
420
+ type: "compaction",
421
+ trigger: boundary.trigger,
422
+ summary,
423
+ ...boundary.tokensBefore !== void 0 ? { tokensBefore: boundary.tokensBefore } : {},
424
+ ...boundary.tokensAfter !== void 0 ? { tokensAfter: boundary.tokensAfter } : {}
425
+ });
426
+ boundary = void 0;
427
+ summary = void 0;
428
+ };
429
+ return {
430
+ onBoundary(next) {
431
+ boundary = next;
432
+ tryEmit();
433
+ },
434
+ onSummary(next) {
435
+ summary = next;
436
+ tryEmit();
437
+ }
438
+ };
439
+ }
440
+
441
+ // src/bridge/index.ts
442
+ import { randomUUID } from "crypto";
443
+ import { argv, stdout as stdout2 } from "process";
444
+ import * as claudeAgentSdk from "@anthropic-ai/claude-agent-sdk";
445
+ import * as mcpServerModule from "@modelcontextprotocol/sdk/server/mcp.js";
446
+ import { z } from "zod";
447
+
448
+ // src/bridge/claude-skills-option.ts
449
+ function toClaudeSkillsOption(skills) {
450
+ return skills && skills.length > 0 ? "all" : void 0;
451
+ }
452
+
453
+ // src/bridge/index.ts
454
+ var NATIVE_TO_COMMON = {
455
+ Read: "read",
456
+ Write: "write",
457
+ Edit: "edit",
458
+ Bash: "bash",
459
+ Glob: "glob",
460
+ Grep: "grep",
461
+ WebSearch: "webSearch"
462
+ };
463
+ var NATIVE_TOOL_KINDS = {
464
+ Read: "readonly",
465
+ Glob: "readonly",
466
+ Grep: "readonly",
467
+ WebSearch: "readonly",
468
+ WebFetch: "readonly",
469
+ TaskGet: "readonly",
470
+ TaskList: "readonly",
471
+ TaskOutput: "readonly",
472
+ ListMcpResources: "readonly",
473
+ ReadMcpResource: "readonly",
474
+ Write: "edit",
475
+ Edit: "edit",
476
+ NotebookEdit: "edit",
477
+ TodoWrite: "edit",
478
+ TaskCreate: "edit",
479
+ TaskUpdate: "edit",
480
+ TaskStop: "edit",
481
+ EnterWorktree: "edit",
482
+ ExitWorktree: "edit",
483
+ ExitPlanMode: "edit",
484
+ Skill: "edit",
485
+ AskUserQuestion: "readonly",
486
+ Bash: "bash"
487
+ };
488
+ function toCommonName(nativeName) {
489
+ return NATIVE_TO_COMMON[nativeName] ?? nativeName;
490
+ }
491
+ function toThinkingConfig(thinking) {
492
+ switch (thinking) {
493
+ case "adaptive":
494
+ return { type: "adaptive", display: "summarized" };
495
+ case "on":
496
+ return { type: "enabled", display: "summarized" };
497
+ case "off":
498
+ return { type: "disabled" };
499
+ default:
500
+ return void 0;
501
+ }
502
+ }
503
+ var args = parseArgs(argv.slice(2));
504
+ var workdir = args.workdir;
505
+ var bridgeStateDir = args.bridgeStateDir;
506
+ if (!workdir) {
507
+ emitFatal("Missing --workdir argument.");
508
+ }
509
+ if (!bridgeStateDir) {
510
+ emitFatal("Missing --bridge-state-dir argument.");
511
+ }
512
+ var claudeSdk = claudeAgentSdk;
513
+ var mcpModule = mcpServerModule;
514
+ await runBridge({
515
+ bridgeType: "claude-code",
516
+ bridgeStateDir,
517
+ onStart: runTurn,
518
+ // Claude Code's session state lives in the workdir on the sandbox filesystem
519
+ // (captured by the sandbox snapshot on stop); the resume payload is empty.
520
+ onDetach: () => ({})
521
+ });
522
+ function createPermissionOptions(input) {
523
+ const permissionMode = input.start.permissionMode ?? "allow-all";
524
+ if (permissionMode === "allow-all") {
525
+ return {
526
+ permissionMode: "bypassPermissions",
527
+ allowDangerouslySkipPermissions: true
528
+ };
529
+ }
530
+ return {
531
+ permissionMode: permissionMode === "allow-edits" ? "acceptEdits" : "default",
532
+ allowDangerouslySkipPermissions: false,
533
+ settings: createPermissionSettings({ permissionMode }),
534
+ canUseTool: async (toolName, toolInput, options) => {
535
+ if (toolName.startsWith("mcp__harness-tools__")) {
536
+ return { behavior: "allow", updatedInput: toolInput };
537
+ }
538
+ if (!nativeToolRequiresApproval({
539
+ nativeName: toolName,
540
+ permissionMode
541
+ })) {
542
+ return { behavior: "allow", updatedInput: toolInput };
543
+ }
544
+ const approvalId = options.toolUseID;
545
+ input.approvalRequestedToolUseIds.add(approvalId);
546
+ input.nativeToolCallNames.set(approvalId, toolName);
547
+ input.emit({
548
+ type: "tool-call",
549
+ toolCallId: approvalId,
550
+ toolName: toCommonName(toolName),
551
+ nativeName: toolName,
552
+ input: JSON.stringify(toolInput ?? {}),
553
+ providerExecuted: true
554
+ });
555
+ input.emit({
556
+ type: "tool-approval-request",
557
+ approvalId,
558
+ toolCallId: approvalId
559
+ });
560
+ const decision = await input.turn.requestToolApproval(approvalId);
561
+ return decision.approved ? { behavior: "allow", updatedInput: toolInput, toolUseID: approvalId } : {
562
+ behavior: "deny",
563
+ message: decision.reason ?? "Denied",
564
+ toolUseID: approvalId
565
+ };
566
+ }
567
+ };
568
+ }
569
+ function createPermissionSettings(input) {
570
+ const askRules = Object.entries(NATIVE_TOOL_KINDS).filter(
571
+ ([, kind]) => input.permissionMode === "allow-reads" ? kind === "edit" || kind === "bash" : input.permissionMode === "allow-edits" ? kind === "bash" : false
572
+ ).map(([nativeName]) => `${nativeName}(*)`);
573
+ if (askRules.length === 0) return void 0;
574
+ return {
575
+ permissions: { ask: askRules },
576
+ sandbox: { autoAllowBashIfSandboxed: false }
577
+ };
578
+ }
579
+ function nativeToolRequiresApproval(input) {
580
+ if (input.permissionMode === "allow-all") return false;
581
+ const kind = NATIVE_TOOL_KINDS[input.nativeName] ?? "edit";
582
+ if (input.permissionMode === "allow-edits") return kind === "bash";
583
+ return kind === "edit" || kind === "bash";
584
+ }
585
+ async function runTurn(start, turn) {
586
+ const emit = (msg) => turn.emit(msg);
587
+ const abortCtl = new AbortController();
588
+ if (turn.abortSignal.aborted) {
589
+ abortCtl.abort();
590
+ } else {
591
+ turn.abortSignal.addEventListener("abort", () => abortCtl.abort(), {
592
+ once: true
593
+ });
594
+ }
595
+ const nativeToolCallNames = /* @__PURE__ */ new Map();
596
+ const approvalRequestedToolUseIds = /* @__PURE__ */ new Set();
597
+ const mcpToolUseIds = /* @__PURE__ */ new Set();
598
+ const mcpServers = {};
599
+ if (start.tools && start.tools.length > 0) {
600
+ const server = new mcpModule.McpServer({
601
+ name: "harness-tools",
602
+ version: "1.0.0"
603
+ });
604
+ for (const tool of start.tools) {
605
+ const shape = jsonSchemaToZodShape(tool.inputSchema, z);
606
+ server.tool(
607
+ tool.name,
608
+ tool.description ?? "",
609
+ shape,
610
+ async (input) => {
611
+ const toolCallId = randomUUID();
612
+ emit({
613
+ type: "tool-call",
614
+ toolCallId,
615
+ toolName: tool.name,
616
+ input: JSON.stringify(input),
617
+ providerExecuted: false
618
+ });
619
+ const { output, isError } = await turn.requestToolResult(toolCallId);
620
+ emit({
621
+ type: "tool-result",
622
+ toolCallId,
623
+ toolName: tool.name,
624
+ result: output ?? null,
625
+ isError: !!isError
626
+ });
627
+ return {
628
+ content: [{ type: "text", text: JSON.stringify(output ?? null) }],
629
+ isError
630
+ };
631
+ }
632
+ );
633
+ }
634
+ mcpServers["harness-tools"] = {
635
+ type: "sdk",
636
+ name: "harness-tools",
637
+ instance: server
638
+ };
639
+ }
640
+ const compaction = createCompactionLatch((event) => emit(event));
641
+ const queryInput = createQueryInput({
642
+ initialUserMessage: start.prompt,
643
+ pendingUserMessages: turn.pendingUserMessages,
644
+ abortSignal: abortCtl.signal
645
+ });
646
+ const skillsOption = toClaudeSkillsOption(start.skills);
647
+ const permissionOptions = createPermissionOptions({
648
+ start,
649
+ turn,
650
+ emit,
651
+ nativeToolCallNames,
652
+ approvalRequestedToolUseIds
653
+ });
654
+ const q = claudeSdk.query({
655
+ prompt: queryInput.input,
656
+ options: {
657
+ ...start.model ? { model: start.model } : {},
658
+ ...start.maxTurns !== void 0 ? { maxTurns: start.maxTurns } : {},
659
+ ...skillsOption ? { skills: skillsOption } : {},
660
+ ...toThinkingConfig(start.thinking) ? { thinking: toThinkingConfig(start.thinking) } : {},
661
+ includePartialMessages: true,
662
+ // The `PostCompact` hook carries the compaction summary, which the
663
+ // `compact_boundary` system message does not. Latch it for the unified
664
+ // `compaction` event; return an empty output so compaction proceeds.
665
+ hooks: {
666
+ PostCompact: [
667
+ {
668
+ hooks: [
669
+ async (input) => {
670
+ if (typeof input?.compact_summary === "string") {
671
+ compaction.onSummary(input.compact_summary);
672
+ }
673
+ return {};
674
+ }
675
+ ]
676
+ }
677
+ ]
678
+ },
679
+ // Continuation rule: the host can force-continue (resume after a
680
+ // cross-process detach) by setting `start.continue: true`; otherwise
681
+ // we continue every subsequent turn after the first one in this
682
+ // bridge process.
683
+ ...start.continue === true || !turn.firstTurn ? { continue: true } : {},
684
+ ...permissionOptions,
685
+ mcpServers,
686
+ cwd: workdir,
687
+ abortSignal: abortCtl.signal
688
+ }
689
+ });
690
+ let stepUsage;
691
+ let totalCostUsd;
692
+ let observedTerminalError;
693
+ let emittedTerminalError = false;
694
+ let emittedTerminalFinish = false;
695
+ let streamStarted = false;
696
+ const partialBlocks = /* @__PURE__ */ new Map();
697
+ const emitTerminalError = (message) => {
698
+ const normalized = message?.trim();
699
+ if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
700
+ observedTerminalError = normalized;
701
+ emittedTerminalError = true;
702
+ emit({ type: "error", error: normalized });
703
+ queryInput.close();
704
+ abortCtl.abort();
705
+ };
706
+ try {
707
+ for await (const msg of q) {
708
+ if (abortCtl.signal.aborted) break;
709
+ if (typeof msg.error === "string" && msg.error.trim()) {
710
+ observedTerminalError = msg.error.trim();
711
+ }
712
+ const type = msg.type;
713
+ if (!streamStarted) {
714
+ const initModel = type === "system" && msg.subtype === "init" && typeof msg.model === "string" ? msg.model : void 0;
715
+ emit({
716
+ type: "stream-start",
717
+ ...initModel ? { modelId: initModel } : {}
718
+ });
719
+ streamStarted = true;
720
+ }
721
+ if (type === "auth_status" && typeof msg.error === "string" && msg.error.trim()) {
722
+ emitTerminalError(msg.error);
723
+ continue;
724
+ }
725
+ if (type === "system" && msg.subtype === "api_retry" && typeof msg.error_status === "number" && [401, 403, 404].includes(msg.error_status)) {
726
+ emitTerminalError(
727
+ `HTTP ${msg.error_status}: ${msg.error ?? "provider request failed"}`
728
+ );
729
+ continue;
730
+ }
731
+ if (type === "system" && msg.subtype === "task_updated" && msg.patch?.status === "failed" && typeof msg.patch.error === "string") {
732
+ emitTerminalError(msg.patch.error);
733
+ continue;
734
+ }
735
+ if (type === "system" && msg.subtype === "compact_boundary") {
736
+ const meta = msg.compact_metadata;
737
+ if (meta) {
738
+ compaction.onBoundary({
739
+ trigger: meta.trigger,
740
+ ...typeof meta.pre_tokens === "number" ? { tokensBefore: meta.pre_tokens } : {},
741
+ ...typeof meta.post_tokens === "number" ? { tokensAfter: meta.post_tokens } : {}
742
+ });
743
+ }
744
+ continue;
745
+ }
746
+ if (type === "stream_event") {
747
+ handleStreamEvent(msg.event, partialBlocks, emit);
748
+ continue;
749
+ }
750
+ if (type === "assistant" && msg.message?.content) {
751
+ for (const block of msg.message.content) {
752
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
753
+ const mcpPrefix = "mcp__harness-tools__";
754
+ if (block.name.startsWith(mcpPrefix)) {
755
+ mcpToolUseIds.add(block.id);
756
+ continue;
757
+ }
758
+ nativeToolCallNames.set(block.id, block.name);
759
+ if (approvalRequestedToolUseIds.has(block.id)) {
760
+ continue;
761
+ }
762
+ emit({
763
+ type: "tool-call",
764
+ toolCallId: block.id,
765
+ toolName: toCommonName(block.name),
766
+ nativeName: block.name,
767
+ input: JSON.stringify(block.input ?? {}),
768
+ providerExecuted: true
769
+ });
770
+ }
771
+ }
772
+ continue;
773
+ }
774
+ if (type === "user" && msg.message?.content) {
775
+ for (const block of msg.message.content) {
776
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
777
+ if (mcpToolUseIds.has(block.tool_use_id)) {
778
+ mcpToolUseIds.delete(block.tool_use_id);
779
+ continue;
780
+ }
781
+ approvalRequestedToolUseIds.delete(block.tool_use_id);
782
+ const nativeName = nativeToolCallNames.get(block.tool_use_id) ?? "unknown";
783
+ nativeToolCallNames.delete(block.tool_use_id);
784
+ const toolName = toCommonName(nativeName);
785
+ const isError = !!block.is_error;
786
+ const content = stringifyContent(block.content);
787
+ const result = toolName === "bash" ? { exitCode: isError ? 1 : 0, stdout: content } : content;
788
+ emit({
789
+ type: "tool-result",
790
+ toolCallId: block.tool_use_id,
791
+ toolName,
792
+ result,
793
+ isError
794
+ });
795
+ }
796
+ }
797
+ continue;
798
+ }
799
+ if (type === "result") {
800
+ if (msg.subtype === "success") {
801
+ const emptyResult = !msg.result?.trim?.();
802
+ if (emptyResult && observedTerminalError) {
803
+ emitTerminalError(observedTerminalError);
804
+ continue;
805
+ }
806
+ const usage = msg.usage ?? msg.message?.usage;
807
+ const harnessUsage = mapUsage(usage);
808
+ if (harnessUsage) stepUsage = harnessUsage;
809
+ if (typeof msg.total_cost_usd === "number") {
810
+ totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
811
+ }
812
+ const metadata = typeof msg.total_cost_usd === "number" ? { "claude-code": { costUsd: msg.total_cost_usd } } : void 0;
813
+ emit({
814
+ type: "finish-step",
815
+ finishReason: { unified: "stop", raw: "stop" },
816
+ usage: harnessUsage ?? defaultUsage(),
817
+ ...metadata ? { harnessMetadata: metadata } : {}
818
+ });
819
+ queryInput.close();
820
+ break;
821
+ } else {
822
+ emitTerminalError(
823
+ (Array.isArray(msg.errors) ? msg.errors.join("\n") : void 0) || observedTerminalError || msg.result || "Unknown error"
824
+ );
825
+ }
826
+ continue;
827
+ }
828
+ }
829
+ } catch (err) {
830
+ if (!(abortCtl.signal.aborted && emittedTerminalError)) {
831
+ emit({ type: "error", error: serialiseError2(err) });
832
+ }
833
+ return;
834
+ } finally {
835
+ queryInput.close();
836
+ }
837
+ if (emittedTerminalError) return;
838
+ emittedTerminalFinish = true;
839
+ void emittedTerminalFinish;
840
+ emit({
841
+ type: "finish",
842
+ finishReason: { unified: "stop", raw: "stop" },
843
+ totalUsage: stepUsage ?? defaultUsage(),
844
+ ...totalCostUsd !== void 0 ? { harnessMetadata: { "claude-code": { costUsd: totalCostUsd } } } : {}
845
+ });
846
+ }
847
+ function handleStreamEvent(event, partialBlocks, send) {
848
+ if (!event || typeof event.index !== "number") return;
849
+ const index = event.index;
850
+ if (event.type === "content_block_start") {
851
+ const blockType = event.content_block?.type;
852
+ if (blockType === "text") {
853
+ const id = randomUUID();
854
+ partialBlocks.set(index, { id, kind: "text" });
855
+ send({ type: "text-start", id });
856
+ } else if (blockType === "thinking") {
857
+ const id = randomUUID();
858
+ partialBlocks.set(index, { id, kind: "thinking" });
859
+ send({ type: "reasoning-start", id });
860
+ }
861
+ return;
862
+ }
863
+ if (event.type === "content_block_delta") {
864
+ const block = partialBlocks.get(index);
865
+ if (!block) return;
866
+ if (block.kind === "text" && event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
867
+ send({ type: "text-delta", id: block.id, delta: event.delta.text });
868
+ } else if (block.kind === "thinking" && event.delta?.type === "thinking_delta" && typeof event.delta.thinking === "string") {
869
+ send({
870
+ type: "reasoning-delta",
871
+ id: block.id,
872
+ delta: event.delta.thinking
873
+ });
874
+ }
875
+ return;
876
+ }
877
+ if (event.type === "content_block_stop") {
878
+ const block = partialBlocks.get(index);
879
+ if (!block) return;
880
+ partialBlocks.delete(index);
881
+ if (block.kind === "text") {
882
+ send({ type: "text-end", id: block.id });
883
+ } else {
884
+ send({ type: "reasoning-end", id: block.id });
885
+ }
886
+ }
887
+ }
888
+ function createQueryInput({
889
+ initialUserMessage,
890
+ pendingUserMessages,
891
+ abortSignal
892
+ }) {
893
+ let closed = false;
894
+ const close = () => {
895
+ closed = true;
896
+ };
897
+ if (abortSignal.aborted) {
898
+ close();
899
+ } else {
900
+ abortSignal.addEventListener("abort", close, { once: true });
901
+ }
902
+ const toUserMessage = (text) => ({
903
+ type: "user",
904
+ message: {
905
+ role: "user",
906
+ content: [{ type: "text", text }]
907
+ }
908
+ });
909
+ return {
910
+ close,
911
+ input: {
912
+ [Symbol.asyncIterator]() {
913
+ let sentInitial = false;
914
+ return {
915
+ async next() {
916
+ while (!closed && !abortSignal.aborted) {
917
+ if (!sentInitial) {
918
+ sentInitial = true;
919
+ return {
920
+ value: toUserMessage(initialUserMessage),
921
+ done: false
922
+ };
923
+ }
924
+ if (pendingUserMessages.length > 0) {
925
+ return {
926
+ value: toUserMessage(pendingUserMessages.shift()),
927
+ done: false
928
+ };
929
+ }
930
+ await new Promise((resolve) => setTimeout(resolve, 50));
931
+ }
932
+ return { value: void 0, done: true };
933
+ }
934
+ };
935
+ }
936
+ }
937
+ };
938
+ }
939
+ function stringifyContent(content) {
940
+ if (typeof content === "string") return content;
941
+ if (Array.isArray(content)) {
942
+ return content.map(
943
+ (entry) => entry && typeof entry === "object" && "text" in entry ? String(entry.text ?? "") : JSON.stringify(entry)
944
+ ).join("");
945
+ }
946
+ return JSON.stringify(content);
947
+ }
948
+ function mapUsage(usage) {
949
+ if (!usage || typeof usage !== "object") return void 0;
950
+ const u = usage;
951
+ return {
952
+ inputTokens: {
953
+ total: (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0),
954
+ noCache: u.input_tokens ?? 0,
955
+ cacheRead: u.cache_read_input_tokens ?? 0,
956
+ cacheWrite: u.cache_creation_input_tokens ?? 0
957
+ },
958
+ outputTokens: {
959
+ total: u.output_tokens ?? 0,
960
+ text: u.output_tokens ?? 0
961
+ }
962
+ };
963
+ }
964
+ function defaultUsage() {
965
+ return {
966
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
967
+ outputTokens: { total: 0, text: 0 }
968
+ };
969
+ }
970
+ function jsonSchemaToZodShape(schema, z2) {
971
+ if (!schema || typeof schema !== "object") return {};
972
+ const s = schema;
973
+ const shape = {};
974
+ const required = new Set(s.required ?? []);
975
+ for (const [key, val] of Object.entries(s.properties ?? {})) {
976
+ let z_;
977
+ switch (val.type) {
978
+ case "string":
979
+ z_ = z2.string();
980
+ break;
981
+ case "number":
982
+ case "integer":
983
+ z_ = z2.number();
984
+ break;
985
+ case "boolean":
986
+ z_ = z2.boolean();
987
+ break;
988
+ case "array":
989
+ z_ = z2.array(z2.any());
990
+ break;
991
+ default:
992
+ z_ = z2.any();
993
+ }
994
+ if (val.description)
995
+ z_ = z_.describe(
996
+ val.description
997
+ );
998
+ shape[key] = required.has(key) ? z_ : z_.optional();
999
+ }
1000
+ return shape;
1001
+ }
1002
+ function parseArgs(args2) {
1003
+ const out = {};
1004
+ for (let i = 0; i < args2.length; i++) {
1005
+ if (args2[i] === "--workdir" && i + 1 < args2.length) {
1006
+ out.workdir = args2[++i];
1007
+ } else if (args2[i] === "--bridge-state-dir" && i + 1 < args2.length) {
1008
+ out.bridgeStateDir = args2[++i];
1009
+ }
1010
+ }
1011
+ return out;
1012
+ }
1013
+ function serialiseError2(err) {
1014
+ if (err instanceof Error) {
1015
+ return { name: err.name, message: err.message, stack: err.stack };
1016
+ }
1017
+ return err;
1018
+ }
1019
+ function emitFatal(message) {
1020
+ stdout2.write(JSON.stringify({ type: "bridge-fatal", message }) + "\n");
1021
+ process.exit(1);
1022
+ }
1023
+ //# sourceMappingURL=index.mjs.map