@tanstack/ai-claude-code 0.2.3 → 0.3.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/dist/esm/adapters/policy-map.js +44 -46
- package/dist/esm/adapters/policy-map.js.map +1 -1
- package/dist/esm/adapters/projection.js +146 -81
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/adapters/text.js +271 -296
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/index.js +3 -13
- package/dist/esm/messages/prompt.js +28 -33
- package/dist/esm/messages/prompt.js.map +1 -1
- package/dist/esm/model-meta.js +20 -13
- package/dist/esm/model-meta.js.map +1 -1
- package/dist/esm/stream/translate.js +361 -359
- package/dist/esm/stream/translate.js.map +1 -1
- package/package.json +8 -8
- package/src/adapters/text.ts +92 -18
- package/dist/esm/index.js.map +0 -1
|
@@ -1,51 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
//#region src/adapters/policy-map.ts
|
|
2
|
+
var WRITE_TOOLS = [
|
|
3
|
+
"Write",
|
|
4
|
+
"Edit",
|
|
5
|
+
"MultiEdit"
|
|
6
|
+
];
|
|
7
|
+
var NETWORK_TOOLS = ["WebFetch", "WebSearch"];
|
|
8
|
+
var BUILTIN_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
9
|
+
"Bash",
|
|
10
|
+
"Read",
|
|
11
|
+
"Write",
|
|
12
|
+
"Edit",
|
|
13
|
+
"MultiEdit",
|
|
14
|
+
"Glob",
|
|
15
|
+
"Grep",
|
|
16
|
+
"WebFetch",
|
|
17
|
+
"WebSearch",
|
|
18
|
+
"NotebookEdit",
|
|
19
|
+
"Task"
|
|
15
20
|
]);
|
|
16
21
|
function modeFor(decision) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
case "deny":
|
|
23
|
-
return "default";
|
|
24
|
-
}
|
|
22
|
+
switch (decision) {
|
|
23
|
+
case "allow": return "bypassPermissions";
|
|
24
|
+
case "ask": return "acceptEdits";
|
|
25
|
+
case "deny": return "default";
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
28
|
function mapPolicyToClaudeFlags(policy) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
};
|
|
44
|
-
if (policy.default !== void 0)
|
|
45
|
-
result.permissionMode = modeFor(policy.default);
|
|
46
|
-
return result;
|
|
29
|
+
const allowedTools = [];
|
|
30
|
+
const disallowedTools = [];
|
|
31
|
+
if (!policy) return {
|
|
32
|
+
allowedTools,
|
|
33
|
+
disallowedTools
|
|
34
|
+
};
|
|
35
|
+
if (policy.capabilities?.fileWrite === "deny") disallowedTools.push(...WRITE_TOOLS);
|
|
36
|
+
if (policy.capabilities?.network === "deny") disallowedTools.push(...NETWORK_TOOLS);
|
|
37
|
+
for (const pattern of policy.commands?.deny ?? []) if (BUILTIN_TOOL_NAMES.has(pattern)) disallowedTools.push(pattern);
|
|
38
|
+
for (const pattern of policy.commands?.allow ?? []) if (BUILTIN_TOOL_NAMES.has(pattern)) allowedTools.push(pattern);
|
|
39
|
+
const result = {
|
|
40
|
+
allowedTools: [...new Set(allowedTools)],
|
|
41
|
+
disallowedTools: [...new Set(disallowedTools)]
|
|
42
|
+
};
|
|
43
|
+
if (policy.default !== void 0) result.permissionMode = modeFor(policy.default);
|
|
44
|
+
return result;
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//# sourceMappingURL=policy-map.js.map
|
|
46
|
+
//#endregion
|
|
47
|
+
export { mapPolicyToClaudeFlags };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=policy-map.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-map.js","sources":["../../../src/adapters/policy-map.ts"],"sourcesContent":["/**\n * Map a portable {@link SandboxPolicy} onto Claude Code CLI permission flags.\n *\n * This is a best-effort, coarse mapping (the CLI's permission model is\n * tool-level + a permission mode, not arbitrary command globs):\n *\n * - `default` decision → `--permission-mode`:\n * `'allow'` → `bypassPermissions`, `'acceptEdits'`-ish `'ask'` → `acceptEdits`,\n * `'deny'` → `default` (in `-p` mode, prompts are auto-denied).\n * - `capabilities.fileWrite === 'deny'` → disallow `Write`,`Edit`,`MultiEdit`.\n * - `capabilities.network === 'deny'` → disallow `WebFetch`,`WebSearch`.\n * - `commands.deny` that name a bare built-in tool (e.g. `Bash`) are added to\n * `--disallowedTools`; fine-grained command-glob enforcement is left to the\n * MCP permission-prompt tool (interactive approvals).\n *\n * Returns the permission mode plus tool allow/deny additions; the adapter\n * merges these with its own config.\n */\nimport type { PolicyDecision, SandboxPolicy } from '@tanstack/ai-sandbox'\nimport type { ClaudeCodePermissionMode } from './text'\n\nexport interface ClaudePolicyFlags {\n permissionMode?: ClaudeCodePermissionMode\n allowedTools: Array<string>\n disallowedTools: Array<string>\n}\n\nconst WRITE_TOOLS = ['Write', 'Edit', 'MultiEdit']\nconst NETWORK_TOOLS = ['WebFetch', 'WebSearch']\nconst BUILTIN_TOOL_NAMES = new Set([\n 'Bash',\n 'Read',\n 'Write',\n 'Edit',\n 'MultiEdit',\n 'Glob',\n 'Grep',\n 'WebFetch',\n 'WebSearch',\n 'NotebookEdit',\n 'Task',\n])\n\nfunction modeFor(decision: PolicyDecision): ClaudeCodePermissionMode {\n switch (decision) {\n case 'allow':\n return 'bypassPermissions'\n case 'ask':\n return 'acceptEdits'\n case 'deny':\n return 'default'\n }\n}\n\nexport function mapPolicyToClaudeFlags(\n policy: SandboxPolicy | undefined,\n): ClaudePolicyFlags {\n const allowedTools: Array<string> = []\n const disallowedTools: Array<string> = []\n if (!policy) return { allowedTools, disallowedTools }\n\n if (policy.capabilities?.fileWrite === 'deny')\n disallowedTools.push(...WRITE_TOOLS)\n if (policy.capabilities?.network === 'deny')\n disallowedTools.push(...NETWORK_TOOLS)\n\n // Tool-name-level command rules map directly; everything else is left to the\n // permission-prompt tool.\n for (const pattern of policy.commands?.deny ?? []) {\n if (BUILTIN_TOOL_NAMES.has(pattern)) disallowedTools.push(pattern)\n }\n for (const pattern of policy.commands?.allow ?? []) {\n if (BUILTIN_TOOL_NAMES.has(pattern)) allowedTools.push(pattern)\n }\n\n const result: ClaudePolicyFlags = {\n allowedTools: [...new Set(allowedTools)],\n disallowedTools: [...new Set(disallowedTools)],\n }\n if (policy.default !== undefined)\n result.permissionMode = modeFor(policy.default)\n return result\n}\n"],"
|
|
1
|
+
{"version":3,"file":"policy-map.js","names":[],"sources":["../../../src/adapters/policy-map.ts"],"sourcesContent":["/**\n * Map a portable {@link SandboxPolicy} onto Claude Code CLI permission flags.\n *\n * This is a best-effort, coarse mapping (the CLI's permission model is\n * tool-level + a permission mode, not arbitrary command globs):\n *\n * - `default` decision → `--permission-mode`:\n * `'allow'` → `bypassPermissions`, `'acceptEdits'`-ish `'ask'` → `acceptEdits`,\n * `'deny'` → `default` (in `-p` mode, prompts are auto-denied).\n * - `capabilities.fileWrite === 'deny'` → disallow `Write`,`Edit`,`MultiEdit`.\n * - `capabilities.network === 'deny'` → disallow `WebFetch`,`WebSearch`.\n * - `commands.deny` that name a bare built-in tool (e.g. `Bash`) are added to\n * `--disallowedTools`; fine-grained command-glob enforcement is left to the\n * MCP permission-prompt tool (interactive approvals).\n *\n * Returns the permission mode plus tool allow/deny additions; the adapter\n * merges these with its own config.\n */\nimport type { PolicyDecision, SandboxPolicy } from '@tanstack/ai-sandbox'\nimport type { ClaudeCodePermissionMode } from './text'\n\nexport interface ClaudePolicyFlags {\n permissionMode?: ClaudeCodePermissionMode\n allowedTools: Array<string>\n disallowedTools: Array<string>\n}\n\nconst WRITE_TOOLS = ['Write', 'Edit', 'MultiEdit']\nconst NETWORK_TOOLS = ['WebFetch', 'WebSearch']\nconst BUILTIN_TOOL_NAMES = new Set([\n 'Bash',\n 'Read',\n 'Write',\n 'Edit',\n 'MultiEdit',\n 'Glob',\n 'Grep',\n 'WebFetch',\n 'WebSearch',\n 'NotebookEdit',\n 'Task',\n])\n\nfunction modeFor(decision: PolicyDecision): ClaudeCodePermissionMode {\n switch (decision) {\n case 'allow':\n return 'bypassPermissions'\n case 'ask':\n return 'acceptEdits'\n case 'deny':\n return 'default'\n }\n}\n\nexport function mapPolicyToClaudeFlags(\n policy: SandboxPolicy | undefined,\n): ClaudePolicyFlags {\n const allowedTools: Array<string> = []\n const disallowedTools: Array<string> = []\n if (!policy) return { allowedTools, disallowedTools }\n\n if (policy.capabilities?.fileWrite === 'deny')\n disallowedTools.push(...WRITE_TOOLS)\n if (policy.capabilities?.network === 'deny')\n disallowedTools.push(...NETWORK_TOOLS)\n\n // Tool-name-level command rules map directly; everything else is left to the\n // permission-prompt tool.\n for (const pattern of policy.commands?.deny ?? []) {\n if (BUILTIN_TOOL_NAMES.has(pattern)) disallowedTools.push(pattern)\n }\n for (const pattern of policy.commands?.allow ?? []) {\n if (BUILTIN_TOOL_NAMES.has(pattern)) allowedTools.push(pattern)\n }\n\n const result: ClaudePolicyFlags = {\n allowedTools: [...new Set(allowedTools)],\n disallowedTools: [...new Set(disallowedTools)],\n }\n if (policy.default !== undefined)\n result.permissionMode = modeFor(policy.default)\n return result\n}\n"],"mappings":";AA2BA,IAAM,cAAc;CAAC;CAAS;CAAQ;AAAW;AACjD,IAAM,gBAAgB,CAAC,YAAY,WAAW;AAC9C,IAAM,qCAAqB,IAAI,IAAI;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,QAAQ,UAAoD;CACnE,QAAQ,UAAR;EACE,KAAK,SACH,OAAO;EACT,KAAK,OACH,OAAO;EACT,KAAK,QACH,OAAO;CACX;AACF;AAEA,SAAgB,uBACd,QACmB;CACnB,MAAM,eAA8B,CAAC;CACrC,MAAM,kBAAiC,CAAC;CACxC,IAAI,CAAC,QAAQ,OAAO;EAAE;EAAc;CAAgB;CAEpD,IAAI,OAAO,cAAc,cAAc,QACrC,gBAAgB,KAAK,GAAG,WAAW;CACrC,IAAI,OAAO,cAAc,YAAY,QACnC,gBAAgB,KAAK,GAAG,aAAa;CAIvC,KAAK,MAAM,WAAW,OAAO,UAAU,QAAQ,CAAC,GAC9C,IAAI,mBAAmB,IAAI,OAAO,GAAG,gBAAgB,KAAK,OAAO;CAEnE,KAAK,MAAM,WAAW,OAAO,UAAU,SAAS,CAAC,GAC/C,IAAI,mBAAmB,IAAI,OAAO,GAAG,aAAa,KAAK,OAAO;CAGhE,MAAM,SAA4B;EAChC,cAAc,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC;EACvC,iBAAiB,CAAC,GAAG,IAAI,IAAI,eAAe,CAAC;CAC/C;CACA,IAAI,OAAO,YAAY,KAAA,GACrB,OAAO,iBAAiB,QAAQ,OAAO,OAAO;CAChD,OAAO;AACT"}
|
|
@@ -1,101 +1,166 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isSecretRef, resolveGitSkillDir } from "@tanstack/ai-sandbox";
|
|
2
|
+
//#region src/adapters/projection.ts
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code workspace projector — the reference implementation the other
|
|
5
|
+
* harness projectors (codex, opencode) mirror.
|
|
6
|
+
*
|
|
7
|
+
* `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a
|
|
8
|
+
* secret resolver, and a one-time marker path) via a capability. Each harness
|
|
9
|
+
* adapter reads it in its `chatStream` setup and projects those inputs into the
|
|
10
|
+
* CLI's native format. For Claude Code that means:
|
|
11
|
+
*
|
|
12
|
+
* - MCP servers → a project-scoped `.mcp.json` at the workspace root.
|
|
13
|
+
* - gitSkill repos → linked under `.claude/skills/<basename>`.
|
|
14
|
+
* - agentSkill → no reliable claude primitive pulls a public skill by
|
|
15
|
+
* bare name, so we warn and skip rather than invent one.
|
|
16
|
+
* - plugins → `claude plugin install <name>` (best-effort).
|
|
17
|
+
*
|
|
18
|
+
* The secret-bearing `.mcp.json` is (re)written on EVERY call, re-resolving
|
|
19
|
+
* secrets each time, so claude always reads current values and a snapshot can
|
|
20
|
+
* never serve a stale or rotated secret. Only the safe, idempotent, non-secret
|
|
21
|
+
* operations (gitSkill links, plugin installs, agentSkill handling) are guarded
|
|
22
|
+
* by a one-time marker file under the workspace.
|
|
23
|
+
*
|
|
24
|
+
* External-convention caveat: the `.mcp.json` location/shape, the skills dir,
|
|
25
|
+
* and the plugin-install command are verified against the installed `claude`
|
|
26
|
+
* CLI. Where claude has no clean primitive (agentSkill by bare name) we no-op
|
|
27
|
+
* with a warning instead of fabricating a command.
|
|
28
|
+
*/
|
|
29
|
+
/** POSIX single-quote escape for embedding a value in a shell command. */
|
|
2
30
|
function shellQuote(value) {
|
|
3
|
-
|
|
31
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4
32
|
}
|
|
33
|
+
/** Last path segment of a `gitSkill` clone dir, used as the skills-dir name. */
|
|
5
34
|
function basenameOf(path) {
|
|
6
|
-
|
|
7
|
-
|
|
35
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
36
|
+
return segments[segments.length - 1] ?? path;
|
|
8
37
|
}
|
|
38
|
+
/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */
|
|
9
39
|
function isBearerMarker(value) {
|
|
10
|
-
|
|
40
|
+
return typeof value === "object" && value !== null && isSecretRef(value.__bearerRef);
|
|
11
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a single MCP header value: a `SecretRef` resolves to its plaintext, a
|
|
44
|
+
* `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string is
|
|
45
|
+
* passed through unchanged.
|
|
46
|
+
*/
|
|
12
47
|
function resolveHeaderValue(value, resolveSecret) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
48
|
+
if (isSecretRef(value)) return resolveSecret(value);
|
|
49
|
+
if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`;
|
|
50
|
+
return value;
|
|
16
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Build claude's project-scoped MCP config from the `{ kind: 'mcp' }` skills,
|
|
54
|
+
* resolving every header value (SecretRef / bearer / string). Returns
|
|
55
|
+
* `undefined` when there are no MCP skills so the caller can skip the write.
|
|
56
|
+
*/
|
|
17
57
|
function buildMcpConfig(skills, resolveSecret) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
const mcpServers = {};
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const skill of skills) {
|
|
61
|
+
if (skill.kind !== "mcp") continue;
|
|
62
|
+
count += 1;
|
|
63
|
+
const headers = {};
|
|
64
|
+
const rawHeaders = skill.config.headers ?? {};
|
|
65
|
+
for (const [name, value] of Object.entries(rawHeaders)) headers[name] = resolveHeaderValue(value, resolveSecret);
|
|
66
|
+
const rawUrl = skill.config["url"];
|
|
67
|
+
const url = typeof rawUrl === "string" ? rawUrl : "";
|
|
68
|
+
mcpServers[skill.name] = {
|
|
69
|
+
type: "http",
|
|
70
|
+
url,
|
|
71
|
+
headers
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return count > 0 ? { mcpServers } : void 0;
|
|
33
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Write the project-scoped `.mcp.json`, re-resolving every secret. This runs on
|
|
78
|
+
* EVERY projection call (never gated by the marker) so claude always reads the
|
|
79
|
+
* current secret values and a snapshot can never serve a stale or rotated one.
|
|
80
|
+
* With `snapshot:'after-run'` the file may still be captured in the image, so
|
|
81
|
+
* secret-bearing MCP material is best used with the default `after-setup`
|
|
82
|
+
* strategy. When there are no MCP skills the write is skipped.
|
|
83
|
+
*/
|
|
34
84
|
async function projectMcpServers(handle, projection) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
85
|
+
const config = buildMcpConfig(projection.skills, projection.resolveSecret);
|
|
86
|
+
if (config === void 0) return;
|
|
87
|
+
const target = `${projection.root}/.mcp.json`;
|
|
88
|
+
await handle.fs.write(target, JSON.stringify(config, null, 2));
|
|
39
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Ensure each cloned `gitSkill` repo is available under claude's project skills
|
|
92
|
+
* dir (`<root>/.claude/skills/<basename>`) via a symlink, falling back to a
|
|
93
|
+
* recursive copy on platforms without `ln -s`.
|
|
94
|
+
*/
|
|
40
95
|
async function projectGitSkills(handle, projection) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`[claude-code] failed to link gitSkill "${skill.repo}" into ${target}: ${copied.stderr.trim()}`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
96
|
+
const skillsDir = `${projection.root}/.claude/skills`;
|
|
97
|
+
let madeDir = false;
|
|
98
|
+
for (const skill of projection.skills) {
|
|
99
|
+
if (skill.kind !== "git") continue;
|
|
100
|
+
if (!madeDir) {
|
|
101
|
+
await handle.fs.mkdir(skillsDir);
|
|
102
|
+
madeDir = true;
|
|
103
|
+
}
|
|
104
|
+
const source = skill.into ?? resolveGitSkillDir(projection.root, skill);
|
|
105
|
+
const target = `${skillsDir}/${basenameOf(source)}`;
|
|
106
|
+
const lnCmd = `ln -s ${shellQuote(source)} ${shellQuote(target)}`;
|
|
107
|
+
if ((await handle.process.exec(lnCmd, { cwd: projection.root })).exitCode !== 0) {
|
|
108
|
+
const cpCmd = `cp -r ${shellQuote(source)} ${shellQuote(target)}`;
|
|
109
|
+
const copied = await handle.process.exec(cpCmd, { cwd: projection.root });
|
|
110
|
+
if (copied.exitCode !== 0) console.warn(`[claude-code] failed to link gitSkill "${skill.repo}" into ${target}: ${copied.stderr.trim()}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
63
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* `agentSkill` references a public skill by bare name. Claude Code has no
|
|
116
|
+
* primitive to fetch a skill from a bare name (skills resolve from local
|
|
117
|
+
* `.claude/skills/` dirs and plugin marketplaces), so we warn and skip rather
|
|
118
|
+
* than fabricate a command.
|
|
119
|
+
*/
|
|
64
120
|
function projectAgentSkills(projection) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
}
|
|
121
|
+
for (const skill of projection.skills) {
|
|
122
|
+
if (skill.kind !== "agent-skill") continue;
|
|
123
|
+
console.warn(`[claude-code] agentSkill "${skill.name}" cannot be projected: Claude Code has no command to install a public skill by bare name. Provide it as a gitSkill or a plugin instead. Skipping.`);
|
|
124
|
+
}
|
|
71
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Install each declared plugin via `claude plugin install <name>`. Plugin
|
|
128
|
+
* installs are best-effort: a failure (no marketplace, network, …) warns but
|
|
129
|
+
* never throws, so a missing plugin can't break the run.
|
|
130
|
+
*/
|
|
72
131
|
async function projectPlugins(handle, projection) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
-
console.warn(
|
|
85
|
-
`[claude-code] failed to install plugin "${name}": ${message}`
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
132
|
+
for (const name of projection.plugins) {
|
|
133
|
+
const cmd = `claude plugin install ${shellQuote(name)}`;
|
|
134
|
+
try {
|
|
135
|
+
const result = await handle.process.exec(cmd, { cwd: projection.root });
|
|
136
|
+
if (result.exitCode !== 0) console.warn(`[claude-code] "claude plugin install ${name}" exited ${result.exitCode}: ${result.stderr.trim()}`);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
console.warn(`[claude-code] failed to install plugin "${name}": ${message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
89
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Project a `WorkspaceProjection` into the Claude Code sandbox. Safe to call on
|
|
145
|
+
* every `chatStream`. The secret-bearing `.mcp.json` is (re)written on every
|
|
146
|
+
* call, re-resolving secrets, so claude always reads current values and a
|
|
147
|
+
* snapshot can never serve a stale or rotated secret. The safe, idempotent,
|
|
148
|
+
* non-secret operations (gitSkill links, plugin installs, agentSkill handling)
|
|
149
|
+
* are guarded by a one-time marker so they run only on the first call after
|
|
150
|
+
* create/restore.
|
|
151
|
+
*
|
|
152
|
+
* @param handle - The sandbox handle (`fs` + `process`).
|
|
153
|
+
* @param projection - The portable workspace inputs from `withSandbox`.
|
|
154
|
+
*/
|
|
90
155
|
async function projectClaudeWorkspace(handle, projection) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
156
|
+
await projectMcpServers(handle, projection);
|
|
157
|
+
if (await handle.fs.exists(projection.markerPath)) return;
|
|
158
|
+
await projectGitSkills(handle, projection);
|
|
159
|
+
projectAgentSkills(projection);
|
|
160
|
+
await projectPlugins(handle, projection);
|
|
161
|
+
await handle.fs.write(projection.markerPath, "");
|
|
97
162
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//# sourceMappingURL=projection.js.map
|
|
163
|
+
//#endregion
|
|
164
|
+
export { projectClaudeWorkspace };
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.js","sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * Claude Code workspace projector — the reference implementation the other\n * harness projectors (codex, opencode) mirror.\n *\n * `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a\n * secret resolver, and a one-time marker path) via a capability. Each harness\n * adapter reads it in its `chatStream` setup and projects those inputs into the\n * CLI's native format. For Claude Code that means:\n *\n * - MCP servers → a project-scoped `.mcp.json` at the workspace root.\n * - gitSkill repos → linked under `.claude/skills/<basename>`.\n * - agentSkill → no reliable claude primitive pulls a public skill by\n * bare name, so we warn and skip rather than invent one.\n * - plugins → `claude plugin install <name>` (best-effort).\n *\n * The secret-bearing `.mcp.json` is (re)written on EVERY call, re-resolving\n * secrets each time, so claude always reads current values and a snapshot can\n * never serve a stale or rotated secret. Only the safe, idempotent, non-secret\n * operations (gitSkill links, plugin installs, agentSkill handling) are guarded\n * by a one-time marker file under the workspace.\n *\n * External-convention caveat: the `.mcp.json` location/shape, the skills dir,\n * and the plugin-install command are verified against the installed `claude`\n * CLI. Where claude has no clean primitive (agentSkill by bare name) we no-op\n * with a warning instead of fabricating a command.\n */\nimport { isSecretRef, resolveGitSkillDir } from '@tanstack/ai-sandbox'\nimport type {\n BearerRef,\n SandboxHandle,\n SecretRef,\n WorkspaceProjection,\n WorkspaceSkill,\n} from '@tanstack/ai-sandbox'\n\n/** POSIX single-quote escape for embedding a value in a shell command. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\n/** Last path segment of a `gitSkill` clone dir, used as the skills-dir name. */\nfunction basenameOf(path: string): string {\n const segments = path.split('/').filter((segment) => segment !== '')\n return segments[segments.length - 1] ?? path\n}\n\n/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */\nfunction isBearerMarker(value: unknown): value is BearerRef {\n return (\n typeof value === 'object' &&\n value !== null &&\n isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)\n )\n}\n\n/**\n * Resolve a single MCP header value: a `SecretRef` resolves to its plaintext, a\n * `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string is\n * passed through unchanged.\n */\nfunction resolveHeaderValue(\n value: string | SecretRef | BearerRef,\n resolveSecret: (ref: SecretRef) => string,\n): string {\n if (isSecretRef(value)) return resolveSecret(value)\n if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`\n return value\n}\n\n/** A claude-format HTTP MCP server entry. */\ninterface ClaudeMcpServer {\n type: 'http'\n url: string\n headers: Record<string, string>\n}\n\n/**\n * Build claude's project-scoped MCP config from the `{ kind: 'mcp' }` skills,\n * resolving every header value (SecretRef / bearer / string). Returns\n * `undefined` when there are no MCP skills so the caller can skip the write.\n */\nfunction buildMcpConfig(\n skills: Array<WorkspaceSkill>,\n resolveSecret: (ref: SecretRef) => string,\n): { mcpServers: Record<string, ClaudeMcpServer> } | undefined {\n const mcpServers: Record<string, ClaudeMcpServer> = {}\n let count = 0\n for (const skill of skills) {\n if (skill.kind !== 'mcp') continue\n count += 1\n const headers: Record<string, string> = {}\n const rawHeaders = skill.config.headers ?? {}\n for (const [name, value] of Object.entries(rawHeaders)) {\n headers[name] = resolveHeaderValue(value, resolveSecret)\n }\n const rawUrl = skill.config['url']\n const url = typeof rawUrl === 'string' ? rawUrl : ''\n mcpServers[skill.name] = { type: 'http', url, headers }\n }\n return count > 0 ? { mcpServers } : undefined\n}\n\n/**\n * Write the project-scoped `.mcp.json`, re-resolving every secret. This runs on\n * EVERY projection call (never gated by the marker) so claude always reads the\n * current secret values and a snapshot can never serve a stale or rotated one.\n * With `snapshot:'after-run'` the file may still be captured in the image, so\n * secret-bearing MCP material is best used with the default `after-setup`\n * strategy. When there are no MCP skills the write is skipped.\n */\nasync function projectMcpServers(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const config = buildMcpConfig(projection.skills, projection.resolveSecret)\n if (config === undefined) return\n const target = `${projection.root}/.mcp.json`\n await handle.fs.write(target, JSON.stringify(config, null, 2))\n}\n\n/**\n * Ensure each cloned `gitSkill` repo is available under claude's project skills\n * dir (`<root>/.claude/skills/<basename>`) via a symlink, falling back to a\n * recursive copy on platforms without `ln -s`.\n */\nasync function projectGitSkills(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const skillsDir = `${projection.root}/.claude/skills`\n let madeDir = false\n for (const skill of projection.skills) {\n if (skill.kind !== 'git') continue\n if (!madeDir) {\n await handle.fs.mkdir(skillsDir)\n madeDir = true\n }\n const source = skill.into ?? resolveGitSkillDir(projection.root, skill)\n const target = `${skillsDir}/${basenameOf(source)}`\n const lnCmd = `ln -s ${shellQuote(source)} ${shellQuote(target)}`\n const result = await handle.process.exec(lnCmd, { cwd: projection.root })\n if (result.exitCode !== 0) {\n const cpCmd = `cp -r ${shellQuote(source)} ${shellQuote(target)}`\n const copied = await handle.process.exec(cpCmd, { cwd: projection.root })\n if (copied.exitCode !== 0) {\n console.warn(\n `[claude-code] failed to link gitSkill \"${skill.repo}\" into ${target}: ${copied.stderr.trim()}`,\n )\n }\n }\n }\n}\n\n/**\n * `agentSkill` references a public skill by bare name. Claude Code has no\n * primitive to fetch a skill from a bare name (skills resolve from local\n * `.claude/skills/` dirs and plugin marketplaces), so we warn and skip rather\n * than fabricate a command.\n */\nfunction projectAgentSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'agent-skill') continue\n console.warn(\n `[claude-code] agentSkill \"${skill.name}\" cannot be projected: Claude Code has ` +\n 'no command to install a public skill by bare name. Provide it as a gitSkill ' +\n 'or a plugin instead. Skipping.',\n )\n }\n}\n\n/**\n * Install each declared plugin via `claude plugin install <name>`. Plugin\n * installs are best-effort: a failure (no marketplace, network, …) warns but\n * never throws, so a missing plugin can't break the run.\n */\nasync function projectPlugins(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n for (const name of projection.plugins) {\n const cmd = `claude plugin install ${shellQuote(name)}`\n try {\n const result = await handle.process.exec(cmd, { cwd: projection.root })\n if (result.exitCode !== 0) {\n console.warn(\n `[claude-code] \"claude plugin install ${name}\" exited ${result.exitCode}: ${result.stderr.trim()}`,\n )\n }\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error)\n console.warn(\n `[claude-code] failed to install plugin \"${name}\": ${message}`,\n )\n }\n }\n}\n\n/**\n * Project a `WorkspaceProjection` into the Claude Code sandbox. Safe to call on\n * every `chatStream`. The secret-bearing `.mcp.json` is (re)written on every\n * call, re-resolving secrets, so claude always reads current values and a\n * snapshot can never serve a stale or rotated secret. The safe, idempotent,\n * non-secret operations (gitSkill links, plugin installs, agentSkill handling)\n * are guarded by a one-time marker so they run only on the first call after\n * create/restore.\n *\n * @param handle - The sandbox handle (`fs` + `process`).\n * @param projection - The portable workspace inputs from `withSandbox`.\n */\nexport async function projectClaudeWorkspace(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n // Always re-resolve and rewrite the secret-bearing MCP config so rotated\n // secrets re-apply and snapshots can't serve stale values.\n await projectMcpServers(handle, projection)\n\n // Gate only the safe, idempotent, non-secret operations on the marker.\n if (await handle.fs.exists(projection.markerPath)) return\n\n await projectGitSkills(handle, projection)\n projectAgentSkills(projection)\n await projectPlugins(handle, projection)\n\n await handle.fs.write(projection.markerPath, '')\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"projection.js","names":[],"sources":["../../../src/adapters/projection.ts"],"sourcesContent":["/**\n * Claude Code workspace projector — the reference implementation the other\n * harness projectors (codex, opencode) mirror.\n *\n * `withSandbox` surfaces a portable `WorkspaceProjection` (skills, plugins, a\n * secret resolver, and a one-time marker path) via a capability. Each harness\n * adapter reads it in its `chatStream` setup and projects those inputs into the\n * CLI's native format. For Claude Code that means:\n *\n * - MCP servers → a project-scoped `.mcp.json` at the workspace root.\n * - gitSkill repos → linked under `.claude/skills/<basename>`.\n * - agentSkill → no reliable claude primitive pulls a public skill by\n * bare name, so we warn and skip rather than invent one.\n * - plugins → `claude plugin install <name>` (best-effort).\n *\n * The secret-bearing `.mcp.json` is (re)written on EVERY call, re-resolving\n * secrets each time, so claude always reads current values and a snapshot can\n * never serve a stale or rotated secret. Only the safe, idempotent, non-secret\n * operations (gitSkill links, plugin installs, agentSkill handling) are guarded\n * by a one-time marker file under the workspace.\n *\n * External-convention caveat: the `.mcp.json` location/shape, the skills dir,\n * and the plugin-install command are verified against the installed `claude`\n * CLI. Where claude has no clean primitive (agentSkill by bare name) we no-op\n * with a warning instead of fabricating a command.\n */\nimport { isSecretRef, resolveGitSkillDir } from '@tanstack/ai-sandbox'\nimport type {\n BearerRef,\n SandboxHandle,\n SecretRef,\n WorkspaceProjection,\n WorkspaceSkill,\n} from '@tanstack/ai-sandbox'\n\n/** POSIX single-quote escape for embedding a value in a shell command. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\n/** Last path segment of a `gitSkill` clone dir, used as the skills-dir name. */\nfunction basenameOf(path: string): string {\n const segments = path.split('/').filter((segment) => segment !== '')\n return segments[segments.length - 1] ?? path\n}\n\n/** True when `value` is a `bearer(ref)` marker created by `@tanstack/ai-sandbox`. */\nfunction isBearerMarker(value: unknown): value is BearerRef {\n return (\n typeof value === 'object' &&\n value !== null &&\n isSecretRef((value as { __bearerRef?: unknown }).__bearerRef)\n )\n}\n\n/**\n * Resolve a single MCP header value: a `SecretRef` resolves to its plaintext, a\n * `bearer(ref)` marker resolves to `Bearer <plaintext>`, and a plain string is\n * passed through unchanged.\n */\nfunction resolveHeaderValue(\n value: string | SecretRef | BearerRef,\n resolveSecret: (ref: SecretRef) => string,\n): string {\n if (isSecretRef(value)) return resolveSecret(value)\n if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`\n return value\n}\n\n/** A claude-format HTTP MCP server entry. */\ninterface ClaudeMcpServer {\n type: 'http'\n url: string\n headers: Record<string, string>\n}\n\n/**\n * Build claude's project-scoped MCP config from the `{ kind: 'mcp' }` skills,\n * resolving every header value (SecretRef / bearer / string). Returns\n * `undefined` when there are no MCP skills so the caller can skip the write.\n */\nfunction buildMcpConfig(\n skills: Array<WorkspaceSkill>,\n resolveSecret: (ref: SecretRef) => string,\n): { mcpServers: Record<string, ClaudeMcpServer> } | undefined {\n const mcpServers: Record<string, ClaudeMcpServer> = {}\n let count = 0\n for (const skill of skills) {\n if (skill.kind !== 'mcp') continue\n count += 1\n const headers: Record<string, string> = {}\n const rawHeaders = skill.config.headers ?? {}\n for (const [name, value] of Object.entries(rawHeaders)) {\n headers[name] = resolveHeaderValue(value, resolveSecret)\n }\n const rawUrl = skill.config['url']\n const url = typeof rawUrl === 'string' ? rawUrl : ''\n mcpServers[skill.name] = { type: 'http', url, headers }\n }\n return count > 0 ? { mcpServers } : undefined\n}\n\n/**\n * Write the project-scoped `.mcp.json`, re-resolving every secret. This runs on\n * EVERY projection call (never gated by the marker) so claude always reads the\n * current secret values and a snapshot can never serve a stale or rotated one.\n * With `snapshot:'after-run'` the file may still be captured in the image, so\n * secret-bearing MCP material is best used with the default `after-setup`\n * strategy. When there are no MCP skills the write is skipped.\n */\nasync function projectMcpServers(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const config = buildMcpConfig(projection.skills, projection.resolveSecret)\n if (config === undefined) return\n const target = `${projection.root}/.mcp.json`\n await handle.fs.write(target, JSON.stringify(config, null, 2))\n}\n\n/**\n * Ensure each cloned `gitSkill` repo is available under claude's project skills\n * dir (`<root>/.claude/skills/<basename>`) via a symlink, falling back to a\n * recursive copy on platforms without `ln -s`.\n */\nasync function projectGitSkills(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n const skillsDir = `${projection.root}/.claude/skills`\n let madeDir = false\n for (const skill of projection.skills) {\n if (skill.kind !== 'git') continue\n if (!madeDir) {\n await handle.fs.mkdir(skillsDir)\n madeDir = true\n }\n const source = skill.into ?? resolveGitSkillDir(projection.root, skill)\n const target = `${skillsDir}/${basenameOf(source)}`\n const lnCmd = `ln -s ${shellQuote(source)} ${shellQuote(target)}`\n const result = await handle.process.exec(lnCmd, { cwd: projection.root })\n if (result.exitCode !== 0) {\n const cpCmd = `cp -r ${shellQuote(source)} ${shellQuote(target)}`\n const copied = await handle.process.exec(cpCmd, { cwd: projection.root })\n if (copied.exitCode !== 0) {\n console.warn(\n `[claude-code] failed to link gitSkill \"${skill.repo}\" into ${target}: ${copied.stderr.trim()}`,\n )\n }\n }\n }\n}\n\n/**\n * `agentSkill` references a public skill by bare name. Claude Code has no\n * primitive to fetch a skill from a bare name (skills resolve from local\n * `.claude/skills/` dirs and plugin marketplaces), so we warn and skip rather\n * than fabricate a command.\n */\nfunction projectAgentSkills(projection: WorkspaceProjection): void {\n for (const skill of projection.skills) {\n if (skill.kind !== 'agent-skill') continue\n console.warn(\n `[claude-code] agentSkill \"${skill.name}\" cannot be projected: Claude Code has ` +\n 'no command to install a public skill by bare name. Provide it as a gitSkill ' +\n 'or a plugin instead. Skipping.',\n )\n }\n}\n\n/**\n * Install each declared plugin via `claude plugin install <name>`. Plugin\n * installs are best-effort: a failure (no marketplace, network, …) warns but\n * never throws, so a missing plugin can't break the run.\n */\nasync function projectPlugins(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n for (const name of projection.plugins) {\n const cmd = `claude plugin install ${shellQuote(name)}`\n try {\n const result = await handle.process.exec(cmd, { cwd: projection.root })\n if (result.exitCode !== 0) {\n console.warn(\n `[claude-code] \"claude plugin install ${name}\" exited ${result.exitCode}: ${result.stderr.trim()}`,\n )\n }\n } catch (error: unknown) {\n const message = error instanceof Error ? error.message : String(error)\n console.warn(\n `[claude-code] failed to install plugin \"${name}\": ${message}`,\n )\n }\n }\n}\n\n/**\n * Project a `WorkspaceProjection` into the Claude Code sandbox. Safe to call on\n * every `chatStream`. The secret-bearing `.mcp.json` is (re)written on every\n * call, re-resolving secrets, so claude always reads current values and a\n * snapshot can never serve a stale or rotated secret. The safe, idempotent,\n * non-secret operations (gitSkill links, plugin installs, agentSkill handling)\n * are guarded by a one-time marker so they run only on the first call after\n * create/restore.\n *\n * @param handle - The sandbox handle (`fs` + `process`).\n * @param projection - The portable workspace inputs from `withSandbox`.\n */\nexport async function projectClaudeWorkspace(\n handle: SandboxHandle,\n projection: WorkspaceProjection,\n): Promise<void> {\n // Always re-resolve and rewrite the secret-bearing MCP config so rotated\n // secrets re-apply and snapshots can't serve stale values.\n await projectMcpServers(handle, projection)\n\n // Gate only the safe, idempotent, non-secret operations on the marker.\n if (await handle.fs.exists(projection.markerPath)) return\n\n await projectGitSkills(handle, projection)\n projectAgentSkills(projection)\n await projectPlugins(handle, projection)\n\n await handle.fs.write(projection.markerPath, '')\n}\n"],"mappings":"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"}
|