@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/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
- safeParseJSON,
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
- workdir: workDir,
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
- stderrTail: bridgeStartupStderr,
518
- stderrDone: bridgeStartupStderrDone
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
- workdir,
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 ${workdir}/.claude/skills`,
616
+ command: `mkdir -p ${shellQuote(homeDir)}/.claude/skills`,
572
617
  abortSignal
573
618
  });
574
619
  for (const skill of skills) {
575
- const path = `${workdir}/.claude/skills/${skill.name}.md`;
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