@ai-hero/sandcastle 0.8.0 → 0.9.0
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/README.md +3 -1
- package/dist/index.js +82 -14
- package/dist/index.js.map +1 -1
- package/dist/main.js +26 -11
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ npm install --save-dev @ai-hero/sandcastle
|
|
|
39
39
|
npx @ai-hero/sandcastle init
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
3. Edit `.sandcastle/.env` and fill in your default values for `
|
|
42
|
+
3. Edit `.sandcastle/.env` and fill in your default values for `CLAUDE_CODE_OAUTH_TOKEN` (run `claude setup-token` on your host to get one). To use an Anthropic API key instead, uncomment and fill in `ANTHROPIC_API_KEY`.
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
cp .sandcastle/.env.example .sandcastle/.env
|
|
@@ -828,6 +828,8 @@ Removes the Podman image.
|
|
|
828
828
|
|
|
829
829
|
After each resumable provider iteration, Sandcastle automatically captures the agent's session file from the sandbox to the host. Claude Code sessions are stored under `~/.claude/projects/<encoded-path>/<session-id>.jsonl`; Codex sessions are stored under `~/.codex/sessions/YYYY/MM/DD/rollout-*-<session-id>.jsonl`; Pi sessions are stored under `~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<session-id>.jsonl`. Any provider-specific `cwd` fields are rewritten to match the host repo root, so the provider's native resume command works.
|
|
830
830
|
|
|
831
|
+
For Claude Code, any `Agent`-tool or `Workflow`-tool subagent transcripts written under `<session-id>/subagents/agent-*.jsonl` are captured alongside the main session. Subagent capture is best-effort: a failure on an individual transcript logs a warning and lets siblings and the main session through. Main-session capture failure still fails the run (see below).
|
|
832
|
+
|
|
831
833
|
Session capture is enabled by default for `claudeCode()`, `codex()`, and `pi()` and can be opted out via `captureSessions: false`. Providers without `sessionStorage` do not attempt capture. Capture failure fails the run.
|
|
832
834
|
|
|
833
835
|
### Session resume
|
package/dist/index.js
CHANGED
|
@@ -676,7 +676,7 @@ var findMissingPromptArgKeys = (prompt, providedArgs) => {
|
|
|
676
676
|
if (seen.has(key)) continue;
|
|
677
677
|
seen.add(key);
|
|
678
678
|
if (builtInSet.has(key)) continue;
|
|
679
|
-
if (key in providedArgs) continue;
|
|
679
|
+
if (key in providedArgs && providedArgs[key] != null) continue;
|
|
680
680
|
missing.push(key);
|
|
681
681
|
}
|
|
682
682
|
return missing;
|
|
@@ -704,6 +704,14 @@ var substitutePromptArgs = (prompt, args, silentKeys) => {
|
|
|
704
704
|
})
|
|
705
705
|
);
|
|
706
706
|
}
|
|
707
|
+
const value = sanitizedArgs[key];
|
|
708
|
+
if (value == null) {
|
|
709
|
+
return yield* Effect_exports.fail(
|
|
710
|
+
new PromptError({
|
|
711
|
+
message: `Prompt argument "{{${key}}}" has value ${value === null ? "null" : "undefined"} in promptArgs`
|
|
712
|
+
})
|
|
713
|
+
);
|
|
714
|
+
}
|
|
707
715
|
}
|
|
708
716
|
for (const key of Object.keys(sanitizedArgs)) {
|
|
709
717
|
if (!referencedKeys.has(key) && !silentKeys?.has(key)) {
|
|
@@ -2403,6 +2411,21 @@ var claudeHostSessionPath = (cwd, id, projectsDir) => {
|
|
|
2403
2411
|
return join(base, encodeProjectPath(cwd), `${id}.jsonl`);
|
|
2404
2412
|
};
|
|
2405
2413
|
var claudeSandboxSessionPath = (cwd, id, projectsDir) => posix.join(projectsDir, encodeProjectPath(cwd), `${id}.jsonl`);
|
|
2414
|
+
var claudeSubagentsDirInSandbox = (cwd, id, projectsDir) => posix.join(projectsDir, encodeProjectPath(cwd), id, "subagents");
|
|
2415
|
+
var claudeSubagentsDirOnHost = (cwd, id, projectsDir) => {
|
|
2416
|
+
const base = projectsDir ?? join(process.env.HOME ?? "~", ".claude", "projects");
|
|
2417
|
+
return join(base, encodeProjectPath(cwd), id, "subagents");
|
|
2418
|
+
};
|
|
2419
|
+
var listClaudeSubagentSessionsInSandbox = async (cwd, id, handle, sandboxProjectsDir) => {
|
|
2420
|
+
const dir = claudeSubagentsDirInSandbox(cwd, id, sandboxProjectsDir);
|
|
2421
|
+
const result = await handle.exec(
|
|
2422
|
+
`find ${JSON.stringify(dir)} -type f -name ${JSON.stringify("agent-*.jsonl")} 2>/dev/null`
|
|
2423
|
+
);
|
|
2424
|
+
if (result.exitCode !== 0) return [];
|
|
2425
|
+
const stdout = result.stdout.trim();
|
|
2426
|
+
if (stdout === "") return [];
|
|
2427
|
+
return stdout.split("\n").filter((line) => line !== "");
|
|
2428
|
+
};
|
|
2406
2429
|
var findClaudeSessionOnHost = async (id, projectsDir) => {
|
|
2407
2430
|
const root = projectsDir ?? join(process.env.HOME ?? "~", ".claude", "projects");
|
|
2408
2431
|
let entries;
|
|
@@ -2424,14 +2447,18 @@ var rewriteSessionCwd = (content, fromCwd, toCwd) => {
|
|
|
2424
2447
|
if (content === "") return "";
|
|
2425
2448
|
return content.split("\n").map((line) => {
|
|
2426
2449
|
if (line === "") return line;
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
entry.cwd
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
entry.payload.cwd
|
|
2450
|
+
try {
|
|
2451
|
+
const entry = JSON.parse(line);
|
|
2452
|
+
if (typeof entry.cwd === "string" && entry.cwd === fromCwd) {
|
|
2453
|
+
entry.cwd = toCwd;
|
|
2454
|
+
}
|
|
2455
|
+
if (entry.type === "session_meta" && typeof entry.payload === "object" && entry.payload !== null && typeof entry.payload.cwd === "string" && entry.payload.cwd === fromCwd) {
|
|
2456
|
+
entry.payload.cwd = toCwd;
|
|
2457
|
+
}
|
|
2458
|
+
return JSON.stringify(entry);
|
|
2459
|
+
} catch {
|
|
2460
|
+
return line;
|
|
2433
2461
|
}
|
|
2434
|
-
return JSON.stringify(entry);
|
|
2435
2462
|
}).join("\n");
|
|
2436
2463
|
};
|
|
2437
2464
|
var transferClaudeSession = (jsonl, fromCwd, toCwd) => rewriteSessionCwd(jsonl, fromCwd, toCwd);
|
|
@@ -2705,6 +2732,19 @@ var writeSandboxFile = async (handle, sandboxPath, content, tag) => {
|
|
|
2705
2732
|
});
|
|
2706
2733
|
}
|
|
2707
2734
|
};
|
|
2735
|
+
var copyClaudeSessionFile = async ({
|
|
2736
|
+
handle,
|
|
2737
|
+
sourcePath,
|
|
2738
|
+
fromCwd,
|
|
2739
|
+
toCwd,
|
|
2740
|
+
destPath,
|
|
2741
|
+
tag
|
|
2742
|
+
}) => {
|
|
2743
|
+
const jsonl = await readSandboxFile(handle, sourcePath, tag);
|
|
2744
|
+
const rewritten = transferClaudeSession(jsonl, fromCwd, toCwd);
|
|
2745
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
2746
|
+
await writeFile(destPath, rewritten);
|
|
2747
|
+
};
|
|
2708
2748
|
var makeClaudeSessionStorage = (options) => {
|
|
2709
2749
|
const hostProjectsDir = options?.sessionStorage?.hostProjectsDir;
|
|
2710
2750
|
const sandboxProjectsDir = options?.sessionStorage?.sandboxProjectsDir ?? "/home/agent/.claude/projects";
|
|
@@ -2717,20 +2757,48 @@ var makeClaudeSessionStorage = (options) => {
|
|
|
2717
2757
|
return readFile(path2, "utf-8");
|
|
2718
2758
|
},
|
|
2719
2759
|
captureToHost: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
2720
|
-
|
|
2760
|
+
await copyClaudeSessionFile({
|
|
2761
|
+
handle,
|
|
2762
|
+
sourcePath: claudeSandboxSessionPath(
|
|
2763
|
+
sandboxCwd,
|
|
2764
|
+
sessionId,
|
|
2765
|
+
sandboxProjectsDir
|
|
2766
|
+
),
|
|
2767
|
+
fromCwd: sandboxCwd,
|
|
2768
|
+
toCwd: hostCwd,
|
|
2769
|
+
destPath: claudeHostSessionPath(hostCwd, sessionId, hostProjectsDir),
|
|
2770
|
+
tag: "claude-cap"
|
|
2771
|
+
});
|
|
2772
|
+
const subagentSandboxPaths = await listClaudeSubagentSessionsInSandbox(
|
|
2721
2773
|
sandboxCwd,
|
|
2722
2774
|
sessionId,
|
|
2775
|
+
handle,
|
|
2723
2776
|
sandboxProjectsDir
|
|
2724
2777
|
);
|
|
2725
|
-
const
|
|
2726
|
-
const rewritten = transferClaudeSession(jsonl, sandboxCwd, hostCwd);
|
|
2727
|
-
const hostPath = claudeHostSessionPath(
|
|
2778
|
+
const hostSubagentsDir = claudeSubagentsDirOnHost(
|
|
2728
2779
|
hostCwd,
|
|
2729
2780
|
sessionId,
|
|
2730
2781
|
hostProjectsDir
|
|
2731
2782
|
);
|
|
2732
|
-
|
|
2733
|
-
|
|
2783
|
+
for (const sandboxSubagentPath of subagentSandboxPaths) {
|
|
2784
|
+
try {
|
|
2785
|
+
await copyClaudeSessionFile({
|
|
2786
|
+
handle,
|
|
2787
|
+
sourcePath: sandboxSubagentPath,
|
|
2788
|
+
fromCwd: sandboxCwd,
|
|
2789
|
+
toCwd: hostCwd,
|
|
2790
|
+
destPath: join(
|
|
2791
|
+
hostSubagentsDir,
|
|
2792
|
+
posix.basename(sandboxSubagentPath)
|
|
2793
|
+
),
|
|
2794
|
+
tag: "claude-sub"
|
|
2795
|
+
});
|
|
2796
|
+
} catch (err) {
|
|
2797
|
+
console.error(
|
|
2798
|
+
`sandcastle: failed to capture Claude subagent transcript ${sandboxSubagentPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2734
2802
|
},
|
|
2735
2803
|
resumeIntoSandbox: async ({ hostCwd, sandboxCwd, sessionId, handle }) => {
|
|
2736
2804
|
const hostPath = claudeHostSessionPath(
|