@ai-sdk/harness-claude-code 1.0.0-canary.2 → 1.0.0-canary.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/bridge/index.mjs +9 -0
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +111 -78
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/bridge/claude-skills-option.ts +11 -0
- package/src/bridge/index.ts +3 -0
- package/src/claude-code-bridge-protocol.ts +1 -0
- package/src/claude-code-harness.ts +138 -88
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// src/claude-code-harness.ts
|
|
2
2
|
import { randomBytes } from "crypto";
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
4
5
|
import { fileURLToPath } from "url";
|
|
5
6
|
import {
|
|
6
7
|
commonTool,
|
|
7
8
|
harnessV1DiagnosticFromBridgeFrame,
|
|
8
9
|
HarnessCapabilityUnsupportedError
|
|
9
10
|
} from "@ai-sdk/harness";
|
|
10
|
-
import { classifyDiskLog, SandboxChannel } from "@ai-sdk/harness/utils";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
classifyDiskLog,
|
|
13
|
+
markBridgeStarting,
|
|
14
|
+
SandboxChannel,
|
|
15
|
+
waitForBridgeReady
|
|
16
|
+
} from "@ai-sdk/harness/utils";
|
|
17
|
+
import {
|
|
13
18
|
tool
|
|
14
19
|
} from "@ai-sdk/provider-utils";
|
|
15
20
|
import { WebSocket } from "ws";
|
|
@@ -101,6 +106,7 @@ var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
|
|
|
101
106
|
var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
|
|
102
107
|
thinking: z.enum(["off", "on", "adaptive"]).optional(),
|
|
103
108
|
maxTurns: z.number().optional(),
|
|
109
|
+
skills: z.array(z.string()).optional(),
|
|
104
110
|
// Resume signal. When true, the bridge passes `{ continue: true }` to the
|
|
105
111
|
// Claude SDK so the in-workdir thread state is rehydrated. The host sets this
|
|
106
112
|
// on the first prompt after a cross-process resume.
|
|
@@ -110,7 +116,6 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
|
|
|
110
116
|
startMessageSchema,
|
|
111
117
|
...harnessV1BridgeInboundCommandSchemas
|
|
112
118
|
]);
|
|
113
|
-
var bridgeReadySchema = harnessV1BridgeReadySchema;
|
|
114
119
|
|
|
115
120
|
// src/claude-code-harness.ts
|
|
116
121
|
var CLAUDE_CODE_BUILTIN_TOOLS = {
|
|
@@ -460,7 +465,8 @@ function createClaudeCode(settings = {}) {
|
|
|
460
465
|
bridgeToken: coords.token,
|
|
461
466
|
sandboxId,
|
|
462
467
|
debug: startOpts.observability?.debug,
|
|
463
|
-
permissionMode: startOpts.permissionMode
|
|
468
|
+
permissionMode: startOpts.permissionMode,
|
|
469
|
+
skills: startOpts.skills ?? []
|
|
464
470
|
});
|
|
465
471
|
} catch {
|
|
466
472
|
}
|
|
@@ -477,12 +483,17 @@ function createClaudeCode(settings = {}) {
|
|
|
477
483
|
respawnStrategy = "replay";
|
|
478
484
|
}
|
|
479
485
|
}
|
|
486
|
+
const sandboxHomeDir = startOpts.skills && startOpts.skills.length > 0 ? await resolveSandboxHomeDir({
|
|
487
|
+
sandbox: session,
|
|
488
|
+
abortSignal: startOpts.abortSignal
|
|
489
|
+
}) : void 0;
|
|
480
490
|
const port = resolveBridgePort(sandboxSession, settings.port);
|
|
481
491
|
const token = randomBytes(32).toString("hex");
|
|
482
492
|
const env = {
|
|
483
493
|
...resolveClaudeCodeEnv(settings.auth),
|
|
484
494
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
485
495
|
BRIDGE_WS_PORT: String(port),
|
|
496
|
+
...sandboxHomeDir ? { HOME: sandboxHomeDir } : {},
|
|
486
497
|
...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
|
|
487
498
|
};
|
|
488
499
|
if (respawnStrategy === void 0) {
|
|
@@ -491,14 +502,23 @@ function createClaudeCode(settings = {}) {
|
|
|
491
502
|
abortSignal: startOpts.abortSignal
|
|
492
503
|
});
|
|
493
504
|
if (startOpts.skills && startOpts.skills.length > 0) {
|
|
505
|
+
if (!sandboxHomeDir) {
|
|
506
|
+
throw new Error("Unable to resolve sandbox HOME directory.");
|
|
507
|
+
}
|
|
494
508
|
await writeSkills({
|
|
495
509
|
sandbox: session,
|
|
496
|
-
|
|
510
|
+
homeDir: sandboxHomeDir,
|
|
497
511
|
skills: startOpts.skills,
|
|
498
512
|
abortSignal: startOpts.abortSignal
|
|
499
513
|
});
|
|
500
514
|
}
|
|
501
515
|
}
|
|
516
|
+
await markBridgeStarting({
|
|
517
|
+
sandbox: session,
|
|
518
|
+
bridgeStateDir,
|
|
519
|
+
bridgeType: "claude-code",
|
|
520
|
+
abortSignal: startOpts.abortSignal
|
|
521
|
+
});
|
|
502
522
|
const proc = await session.spawn({
|
|
503
523
|
command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${workDir} --bridge-state-dir ${bridgeStateDir}`,
|
|
504
524
|
env,
|
|
@@ -512,10 +532,25 @@ function createClaudeCode(settings = {}) {
|
|
|
512
532
|
void bridgeStartupStderrDone;
|
|
513
533
|
const { port: boundPort } = await waitForBridgeReady({
|
|
514
534
|
proc,
|
|
535
|
+
sandbox: session,
|
|
536
|
+
bridgeStateDir,
|
|
537
|
+
bridgeType: "claude-code",
|
|
515
538
|
timeoutMs,
|
|
516
539
|
abortSignal: startOpts.abortSignal,
|
|
517
|
-
|
|
518
|
-
|
|
540
|
+
createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
|
|
541
|
+
message: "claude-code bridge did not become ready in time.",
|
|
542
|
+
proc: proc2,
|
|
543
|
+
stdoutTail,
|
|
544
|
+
stderrTail: bridgeStartupStderr,
|
|
545
|
+
stderrDone: bridgeStartupStderrDone
|
|
546
|
+
}),
|
|
547
|
+
createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
|
|
548
|
+
message: "claude-code bridge exited before becoming ready.",
|
|
549
|
+
proc: proc2,
|
|
550
|
+
stdoutTail,
|
|
551
|
+
stderrTail: bridgeStartupStderr,
|
|
552
|
+
stderrDone: bridgeStartupStderrDone
|
|
553
|
+
})
|
|
519
554
|
});
|
|
520
555
|
void drainRest(proc.stdout);
|
|
521
556
|
const wsUrl = await sandboxSession.getPortUrl({
|
|
@@ -548,7 +583,8 @@ function createClaudeCode(settings = {}) {
|
|
|
548
583
|
bridgeToken: token,
|
|
549
584
|
sandboxId,
|
|
550
585
|
debug: startOpts.observability?.debug,
|
|
551
|
-
permissionMode: startOpts.permissionMode
|
|
586
|
+
permissionMode: startOpts.permissionMode,
|
|
587
|
+
skills: startOpts.skills ?? []
|
|
552
588
|
});
|
|
553
589
|
}
|
|
554
590
|
};
|
|
@@ -563,16 +599,27 @@ function resolveBridgePort(sandboxSession, override) {
|
|
|
563
599
|
}
|
|
564
600
|
async function writeSkills({
|
|
565
601
|
sandbox,
|
|
566
|
-
|
|
602
|
+
homeDir,
|
|
567
603
|
skills,
|
|
568
604
|
abortSignal
|
|
569
605
|
}) {
|
|
606
|
+
for (const skill of skills) {
|
|
607
|
+
safeClaudeSkillName(skill.name);
|
|
608
|
+
for (const file of skill.files ?? []) {
|
|
609
|
+
safeClaudeSkillFilePath({
|
|
610
|
+
skillName: skill.name,
|
|
611
|
+
filePath: file.path
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
570
615
|
await sandbox.run({
|
|
571
|
-
command: `mkdir -p ${
|
|
616
|
+
command: `mkdir -p ${shellQuote(homeDir)}/.claude/skills`,
|
|
572
617
|
abortSignal
|
|
573
618
|
});
|
|
574
619
|
for (const skill of skills) {
|
|
575
|
-
const
|
|
620
|
+
const name = safeClaudeSkillName(skill.name);
|
|
621
|
+
const skillDir = `${homeDir}/.claude/skills/${name}`;
|
|
622
|
+
const path2 = `${skillDir}/SKILL.md`;
|
|
576
623
|
const content = `---
|
|
577
624
|
name: ${skill.name}
|
|
578
625
|
description: ${skill.description}
|
|
@@ -580,9 +627,57 @@ description: ${skill.description}
|
|
|
580
627
|
|
|
581
628
|
${skill.content}
|
|
582
629
|
`;
|
|
583
|
-
await sandbox.writeTextFile({ path, content, abortSignal });
|
|
630
|
+
await sandbox.writeTextFile({ path: path2, content, abortSignal });
|
|
631
|
+
for (const file of skill.files ?? []) {
|
|
632
|
+
const filePath = safeClaudeSkillFilePath({
|
|
633
|
+
skillName: skill.name,
|
|
634
|
+
filePath: file.path
|
|
635
|
+
});
|
|
636
|
+
await sandbox.writeTextFile({
|
|
637
|
+
path: `${skillDir}/${filePath}`,
|
|
638
|
+
content: file.content,
|
|
639
|
+
abortSignal
|
|
640
|
+
});
|
|
641
|
+
}
|
|
584
642
|
}
|
|
585
643
|
}
|
|
644
|
+
async function resolveSandboxHomeDir({
|
|
645
|
+
sandbox,
|
|
646
|
+
abortSignal
|
|
647
|
+
}) {
|
|
648
|
+
const result = await sandbox.run({
|
|
649
|
+
command: 'printf "%s" "$HOME"',
|
|
650
|
+
abortSignal
|
|
651
|
+
});
|
|
652
|
+
const homeDir = result.stdout.trim();
|
|
653
|
+
if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
return homeDir;
|
|
659
|
+
}
|
|
660
|
+
function safeClaudeSkillName(name) {
|
|
661
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") {
|
|
662
|
+
throw new Error(`Invalid Claude Code skill name: ${name}`);
|
|
663
|
+
}
|
|
664
|
+
return name;
|
|
665
|
+
}
|
|
666
|
+
function safeClaudeSkillFilePath({
|
|
667
|
+
skillName,
|
|
668
|
+
filePath
|
|
669
|
+
}) {
|
|
670
|
+
const normalized = path.posix.normalize(filePath);
|
|
671
|
+
if (normalized === "." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`Invalid Claude Code skill file path for ${skillName}: ${filePath}`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return normalized;
|
|
677
|
+
}
|
|
678
|
+
function shellQuote(value) {
|
|
679
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
680
|
+
}
|
|
586
681
|
async function readBridgeAsset(name) {
|
|
587
682
|
const candidates = [
|
|
588
683
|
new URL(`./bridge/${name}`, import.meta.url),
|
|
@@ -600,71 +695,6 @@ async function readBridgeAsset(name) {
|
|
|
600
695
|
}
|
|
601
696
|
throw lastErr ?? new Error(`bridge asset not found: ${name}`);
|
|
602
697
|
}
|
|
603
|
-
async function waitForBridgeReady({
|
|
604
|
-
proc,
|
|
605
|
-
timeoutMs,
|
|
606
|
-
abortSignal,
|
|
607
|
-
stderrTail,
|
|
608
|
-
stderrDone
|
|
609
|
-
}) {
|
|
610
|
-
const reader = proc.stdout.pipeThrough(new TextDecoderStream()).getReader();
|
|
611
|
-
const decoder = lineDecoder();
|
|
612
|
-
const stdoutTail = [];
|
|
613
|
-
const deadline = Date.now() + timeoutMs;
|
|
614
|
-
try {
|
|
615
|
-
while (true) {
|
|
616
|
-
if (abortSignal?.aborted) {
|
|
617
|
-
await proc.kill();
|
|
618
|
-
throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
|
|
619
|
-
}
|
|
620
|
-
const remaining = deadline - Date.now();
|
|
621
|
-
if (remaining <= 0) {
|
|
622
|
-
await proc.kill();
|
|
623
|
-
throw await createBridgeStartupError({
|
|
624
|
-
message: "claude-code bridge did not become ready in time.",
|
|
625
|
-
proc,
|
|
626
|
-
stdoutTail,
|
|
627
|
-
stderrTail,
|
|
628
|
-
stderrDone
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
const { value, done } = await Promise.race([
|
|
632
|
-
reader.read(),
|
|
633
|
-
new Promise(
|
|
634
|
-
(resolve) => setTimeout(
|
|
635
|
-
() => resolve({ value: void 0, done: false }),
|
|
636
|
-
remaining
|
|
637
|
-
)
|
|
638
|
-
)
|
|
639
|
-
]);
|
|
640
|
-
if (done) {
|
|
641
|
-
for (const line of decoder.flush()) {
|
|
642
|
-
stdoutTail.push(line);
|
|
643
|
-
if (stdoutTail.length > 20) stdoutTail.shift();
|
|
644
|
-
}
|
|
645
|
-
throw await createBridgeStartupError({
|
|
646
|
-
message: "claude-code bridge exited before becoming ready.",
|
|
647
|
-
proc,
|
|
648
|
-
stdoutTail,
|
|
649
|
-
stderrTail,
|
|
650
|
-
stderrDone
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
if (value === void 0) continue;
|
|
654
|
-
for (const line of decoder.push(value)) {
|
|
655
|
-
stdoutTail.push(line);
|
|
656
|
-
if (stdoutTail.length > 20) stdoutTail.shift();
|
|
657
|
-
const parsed = await safeParseJSON({
|
|
658
|
-
text: line,
|
|
659
|
-
schema: bridgeReadySchema
|
|
660
|
-
});
|
|
661
|
-
if (parsed.success) return { port: parsed.value.port };
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
} finally {
|
|
665
|
-
reader.releaseLock();
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
698
|
async function createBridgeStartupError({
|
|
669
699
|
message,
|
|
670
700
|
proc,
|
|
@@ -910,7 +940,8 @@ function createSession({
|
|
|
910
940
|
bridgeToken,
|
|
911
941
|
sandboxId,
|
|
912
942
|
debug,
|
|
913
|
-
permissionMode
|
|
943
|
+
permissionMode,
|
|
944
|
+
skills
|
|
914
945
|
}) {
|
|
915
946
|
let stopped = false;
|
|
916
947
|
let stopPromise;
|
|
@@ -1054,6 +1085,7 @@ function createSession({
|
|
|
1054
1085
|
model,
|
|
1055
1086
|
maxTurns,
|
|
1056
1087
|
thinking,
|
|
1088
|
+
...skills.length > 0 ? { skills: skills.map((skill) => skill.name) } : {},
|
|
1057
1089
|
...permissionMode ? { permissionMode } : {},
|
|
1058
1090
|
...debug ? { debug } : {},
|
|
1059
1091
|
...pendingResumeFlag ? { continue: true } : {}
|
|
@@ -1087,6 +1119,7 @@ function createSession({
|
|
|
1087
1119
|
model,
|
|
1088
1120
|
maxTurns,
|
|
1089
1121
|
thinking,
|
|
1122
|
+
...skills.length > 0 ? { skills: skills.map((skill) => skill.name) } : {},
|
|
1090
1123
|
...permissionMode ? { permissionMode } : {},
|
|
1091
1124
|
...debug ? { debug } : {},
|
|
1092
1125
|
continue: true
|