@ai-sdk/harness-claude-code 1.0.0-canary.2 → 1.0.0-canary.3
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 +14 -0
- package/dist/bridge/index.mjs +9 -0
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +82 -8
- 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 +106 -9
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import {
|
|
5
6
|
commonTool,
|
|
@@ -506,6 +507,7 @@ export function createClaudeCode(
|
|
|
506
507
|
sandboxId,
|
|
507
508
|
debug: startOpts.observability?.debug,
|
|
508
509
|
permissionMode: startOpts.permissionMode,
|
|
510
|
+
skills: startOpts.skills ?? [],
|
|
509
511
|
});
|
|
510
512
|
} catch {
|
|
511
513
|
// Bridge no longer reachable — recover by respawning below.
|
|
@@ -536,12 +538,20 @@ export function createClaudeCode(
|
|
|
536
538
|
}
|
|
537
539
|
}
|
|
538
540
|
|
|
541
|
+
const sandboxHomeDir =
|
|
542
|
+
startOpts.skills && startOpts.skills.length > 0
|
|
543
|
+
? await resolveSandboxHomeDir({
|
|
544
|
+
sandbox: session,
|
|
545
|
+
abortSignal: startOpts.abortSignal,
|
|
546
|
+
})
|
|
547
|
+
: undefined;
|
|
539
548
|
const port = resolveBridgePort(sandboxSession, settings.port);
|
|
540
549
|
const token = randomBytes(32).toString('hex');
|
|
541
550
|
const env = {
|
|
542
551
|
...resolveClaudeCodeEnv(settings.auth),
|
|
543
552
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
544
553
|
BRIDGE_WS_PORT: String(port),
|
|
554
|
+
...(sandboxHomeDir ? { HOME: sandboxHomeDir } : {}),
|
|
545
555
|
...(respawnStrategy === 'replay'
|
|
546
556
|
? { BRIDGE_REPLAY_FROM_DISK: '1' }
|
|
547
557
|
: {}),
|
|
@@ -560,9 +570,12 @@ export function createClaudeCode(
|
|
|
560
570
|
});
|
|
561
571
|
|
|
562
572
|
if (startOpts.skills && startOpts.skills.length > 0) {
|
|
573
|
+
if (!sandboxHomeDir) {
|
|
574
|
+
throw new Error('Unable to resolve sandbox HOME directory.');
|
|
575
|
+
}
|
|
563
576
|
await writeSkills({
|
|
564
577
|
sandbox: session,
|
|
565
|
-
|
|
578
|
+
homeDir: sandboxHomeDir,
|
|
566
579
|
skills: startOpts.skills,
|
|
567
580
|
abortSignal: startOpts.abortSignal,
|
|
568
581
|
});
|
|
@@ -632,6 +645,7 @@ export function createClaudeCode(
|
|
|
632
645
|
sandboxId,
|
|
633
646
|
debug: startOpts.observability?.debug,
|
|
634
647
|
permissionMode: startOpts.permissionMode,
|
|
648
|
+
skills: startOpts.skills ?? [],
|
|
635
649
|
});
|
|
636
650
|
},
|
|
637
651
|
};
|
|
@@ -652,33 +666,108 @@ function resolveBridgePort(
|
|
|
652
666
|
}
|
|
653
667
|
|
|
654
668
|
/**
|
|
655
|
-
* Materialise skill files into
|
|
656
|
-
* `claude` CLI
|
|
657
|
-
*
|
|
658
|
-
* the
|
|
669
|
+
* Materialise skill files into
|
|
670
|
+
* `$HOME/.claude/skills/<name>/SKILL.md`. The `claude` CLI
|
|
671
|
+
* auto-discovers skills from that directory on startup, so the files have to
|
|
672
|
+
* be in place before the bridge is spawned without mutating the session
|
|
673
|
+
* workdir. Each file uses the YAML-frontmatter shape the CLI expects.
|
|
659
674
|
*/
|
|
660
675
|
async function writeSkills({
|
|
661
676
|
sandbox,
|
|
662
|
-
|
|
677
|
+
homeDir,
|
|
663
678
|
skills,
|
|
664
679
|
abortSignal,
|
|
665
680
|
}: {
|
|
666
681
|
sandbox: Experimental_SandboxSession;
|
|
667
|
-
|
|
682
|
+
homeDir: string;
|
|
668
683
|
skills: ReadonlyArray<HarnessV1Skill>;
|
|
669
684
|
abortSignal?: AbortSignal;
|
|
670
685
|
}): Promise<void> {
|
|
686
|
+
for (const skill of skills) {
|
|
687
|
+
safeClaudeSkillName(skill.name);
|
|
688
|
+
for (const file of skill.files ?? []) {
|
|
689
|
+
safeClaudeSkillFilePath({
|
|
690
|
+
skillName: skill.name,
|
|
691
|
+
filePath: file.path,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
671
696
|
await sandbox.run({
|
|
672
|
-
command: `mkdir -p ${
|
|
697
|
+
command: `mkdir -p ${shellQuote(homeDir)}/.claude/skills`,
|
|
673
698
|
abortSignal,
|
|
674
699
|
});
|
|
675
700
|
for (const skill of skills) {
|
|
676
|
-
const
|
|
701
|
+
const name = safeClaudeSkillName(skill.name);
|
|
702
|
+
const skillDir = `${homeDir}/.claude/skills/${name}`;
|
|
703
|
+
const path = `${skillDir}/SKILL.md`;
|
|
677
704
|
const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}\n`;
|
|
678
705
|
await sandbox.writeTextFile({ path, content, abortSignal });
|
|
706
|
+
for (const file of skill.files ?? []) {
|
|
707
|
+
const filePath = safeClaudeSkillFilePath({
|
|
708
|
+
skillName: skill.name,
|
|
709
|
+
filePath: file.path,
|
|
710
|
+
});
|
|
711
|
+
await sandbox.writeTextFile({
|
|
712
|
+
path: `${skillDir}/${filePath}`,
|
|
713
|
+
content: file.content,
|
|
714
|
+
abortSignal,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
679
717
|
}
|
|
680
718
|
}
|
|
681
719
|
|
|
720
|
+
async function resolveSandboxHomeDir({
|
|
721
|
+
sandbox,
|
|
722
|
+
abortSignal,
|
|
723
|
+
}: {
|
|
724
|
+
sandbox: Experimental_SandboxSession;
|
|
725
|
+
abortSignal?: AbortSignal;
|
|
726
|
+
}): Promise<string> {
|
|
727
|
+
const result = await sandbox.run({
|
|
728
|
+
command: 'printf "%s" "$HOME"',
|
|
729
|
+
abortSignal,
|
|
730
|
+
});
|
|
731
|
+
const homeDir = result.stdout.trim();
|
|
732
|
+
if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
return homeDir;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function safeClaudeSkillName(name: string): string {
|
|
741
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === '.' || name === '..') {
|
|
742
|
+
throw new Error(`Invalid Claude Code skill name: ${name}`);
|
|
743
|
+
}
|
|
744
|
+
return name;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function safeClaudeSkillFilePath({
|
|
748
|
+
skillName,
|
|
749
|
+
filePath,
|
|
750
|
+
}: {
|
|
751
|
+
skillName: string;
|
|
752
|
+
filePath: string;
|
|
753
|
+
}): string {
|
|
754
|
+
const normalized = path.posix.normalize(filePath);
|
|
755
|
+
if (
|
|
756
|
+
normalized === '.' ||
|
|
757
|
+
normalized.startsWith('../') ||
|
|
758
|
+
path.posix.isAbsolute(normalized)
|
|
759
|
+
) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Invalid Claude Code skill file path for ${skillName}: ${filePath}`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return normalized;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function shellQuote(value: string): string {
|
|
768
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
769
|
+
}
|
|
770
|
+
|
|
682
771
|
async function readBridgeAsset(name: string): Promise<string> {
|
|
683
772
|
const candidates = [
|
|
684
773
|
new URL(`./bridge/${name}`, import.meta.url),
|
|
@@ -1063,6 +1152,7 @@ function createSession({
|
|
|
1063
1152
|
sandboxId,
|
|
1064
1153
|
debug,
|
|
1065
1154
|
permissionMode,
|
|
1155
|
+
skills,
|
|
1066
1156
|
}: {
|
|
1067
1157
|
sessionId: string;
|
|
1068
1158
|
channel: ClaudeCodeChannel;
|
|
@@ -1079,6 +1169,7 @@ function createSession({
|
|
|
1079
1169
|
sandboxId: string;
|
|
1080
1170
|
debug: HarnessV1DebugConfig | undefined;
|
|
1081
1171
|
permissionMode: HarnessV1PermissionMode | undefined;
|
|
1172
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
1082
1173
|
}): HarnessV1Session {
|
|
1083
1174
|
let stopped = false;
|
|
1084
1175
|
let stopPromise: Promise<void> | undefined;
|
|
@@ -1259,6 +1350,9 @@ function createSession({
|
|
|
1259
1350
|
model,
|
|
1260
1351
|
maxTurns,
|
|
1261
1352
|
thinking,
|
|
1353
|
+
...(skills.length > 0
|
|
1354
|
+
? { skills: skills.map(skill => skill.name) }
|
|
1355
|
+
: {}),
|
|
1262
1356
|
...(permissionMode ? { permissionMode } : {}),
|
|
1263
1357
|
...(debug ? { debug } : {}),
|
|
1264
1358
|
...(pendingResumeFlag ? { continue: true } : {}),
|
|
@@ -1307,6 +1401,9 @@ function createSession({
|
|
|
1307
1401
|
model,
|
|
1308
1402
|
maxTurns,
|
|
1309
1403
|
thinking,
|
|
1404
|
+
...(skills.length > 0
|
|
1405
|
+
? { skills: skills.map(skill => skill.name) }
|
|
1406
|
+
: {}),
|
|
1310
1407
|
...(permissionMode ? { permissionMode } : {}),
|
|
1311
1408
|
...(debug ? { debug } : {}),
|
|
1312
1409
|
continue: true,
|