@tanstack/ai-claude-code 0.1.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 +18 -0
- package/dist/esm/adapters/policy-map.d.ts +8 -0
- package/dist/esm/adapters/policy-map.js +51 -0
- package/dist/esm/adapters/policy-map.js.map +1 -0
- package/dist/esm/adapters/projection.d.ts +14 -0
- package/dist/esm/adapters/projection.js +101 -0
- package/dist/esm/adapters/projection.js.map +1 -0
- package/dist/esm/adapters/text.d.ts +75 -0
- package/dist/esm/adapters/text.js +306 -0
- package/dist/esm/adapters/text.js.map +1 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/messages/prompt.d.ts +17 -0
- package/dist/esm/messages/prompt.js +37 -0
- package/dist/esm/messages/prompt.js.map +1 -0
- package/dist/esm/model-meta.d.ts +10 -0
- package/dist/esm/model-meta.js +14 -0
- package/dist/esm/model-meta.js.map +1 -0
- package/dist/esm/provider-options.d.ts +30 -0
- package/dist/esm/stream/sdk-types.d.ts +124 -0
- package/dist/esm/stream/translate.d.ts +44 -0
- package/dist/esm/stream/translate.js +368 -0
- package/dist/esm/stream/translate.js.map +1 -0
- package/package.json +62 -0
- package/src/adapters/policy-map.ts +83 -0
- package/src/adapters/projection.ts +226 -0
- package/src/adapters/text.ts +539 -0
- package/src/index.ts +21 -0
- package/src/messages/prompt.ts +68 -0
- package/src/model-meta.ts +21 -0
- package/src/provider-options.ts +30 -0
- package/src/stream/sdk-types.ts +135 -0
- package/src/stream/translate.ts +483 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @tanstack/ai-claude-code
|
|
2
|
+
|
|
3
|
+
Claude Code harness adapter for [TanStack AI](https://tanstack.com/ai) — run [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (via `@anthropic-ai/claude-agent-sdk`) as a chat backend with local tool execution, stateful coding sessions, and TanStack tool bridging.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { chat } from '@tanstack/ai'
|
|
7
|
+
import { claudeCodeText } from '@tanstack/ai-claude-code'
|
|
8
|
+
|
|
9
|
+
const stream = chat({
|
|
10
|
+
adapter: claudeCodeText('claude-opus-4-8', {
|
|
11
|
+
cwd: '/path/to/project',
|
|
12
|
+
permissionMode: 'acceptEdits',
|
|
13
|
+
}),
|
|
14
|
+
messages: [{ role: 'user', content: 'Fix the failing test.' }],
|
|
15
|
+
})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Server-only (Node). See the [Claude Code adapter docs](https://tanstack.com/ai/latest/docs/adapters/claude-code) for sessions, tool bridging, permissions, and limitations.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SandboxPolicy } from '@tanstack/ai-sandbox';
|
|
2
|
+
import { ClaudeCodePermissionMode } from './text.js';
|
|
3
|
+
export interface ClaudePolicyFlags {
|
|
4
|
+
permissionMode?: ClaudeCodePermissionMode;
|
|
5
|
+
allowedTools: Array<string>;
|
|
6
|
+
disallowedTools: Array<string>;
|
|
7
|
+
}
|
|
8
|
+
export declare function mapPolicyToClaudeFlags(policy: SandboxPolicy | undefined): ClaudePolicyFlags;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const WRITE_TOOLS = ["Write", "Edit", "MultiEdit"];
|
|
2
|
+
const NETWORK_TOOLS = ["WebFetch", "WebSearch"];
|
|
3
|
+
const BUILTIN_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
4
|
+
"Bash",
|
|
5
|
+
"Read",
|
|
6
|
+
"Write",
|
|
7
|
+
"Edit",
|
|
8
|
+
"MultiEdit",
|
|
9
|
+
"Glob",
|
|
10
|
+
"Grep",
|
|
11
|
+
"WebFetch",
|
|
12
|
+
"WebSearch",
|
|
13
|
+
"NotebookEdit",
|
|
14
|
+
"Task"
|
|
15
|
+
]);
|
|
16
|
+
function modeFor(decision) {
|
|
17
|
+
switch (decision) {
|
|
18
|
+
case "allow":
|
|
19
|
+
return "bypassPermissions";
|
|
20
|
+
case "ask":
|
|
21
|
+
return "acceptEdits";
|
|
22
|
+
case "deny":
|
|
23
|
+
return "default";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function mapPolicyToClaudeFlags(policy) {
|
|
27
|
+
const allowedTools = [];
|
|
28
|
+
const disallowedTools = [];
|
|
29
|
+
if (!policy) return { allowedTools, disallowedTools };
|
|
30
|
+
if (policy.capabilities?.fileWrite === "deny")
|
|
31
|
+
disallowedTools.push(...WRITE_TOOLS);
|
|
32
|
+
if (policy.capabilities?.network === "deny")
|
|
33
|
+
disallowedTools.push(...NETWORK_TOOLS);
|
|
34
|
+
for (const pattern of policy.commands?.deny ?? []) {
|
|
35
|
+
if (BUILTIN_TOOL_NAMES.has(pattern)) disallowedTools.push(pattern);
|
|
36
|
+
}
|
|
37
|
+
for (const pattern of policy.commands?.allow ?? []) {
|
|
38
|
+
if (BUILTIN_TOOL_NAMES.has(pattern)) allowedTools.push(pattern);
|
|
39
|
+
}
|
|
40
|
+
const result = {
|
|
41
|
+
allowedTools: [...new Set(allowedTools)],
|
|
42
|
+
disallowedTools: [...new Set(disallowedTools)]
|
|
43
|
+
};
|
|
44
|
+
if (policy.default !== void 0)
|
|
45
|
+
result.permissionMode = modeFor(policy.default);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
mapPolicyToClaudeFlags
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=policy-map.js.map
|
|
@@ -0,0 +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"],"names":[],"mappings":"AA2BA,MAAM,cAAc,CAAC,SAAS,QAAQ,WAAW;AACjD,MAAM,gBAAgB,CAAC,YAAY,WAAW;AAC9C,MAAM,yCAAyB,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,QAAQ,UAAoD;AACnE,UAAQ,UAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EAAA;AAEb;AAEO,SAAS,uBACd,QACmB;AACnB,QAAM,eAA8B,CAAA;AACpC,QAAM,kBAAiC,CAAA;AACvC,MAAI,CAAC,OAAQ,QAAO,EAAE,cAAc,gBAAA;AAEpC,MAAI,OAAO,cAAc,cAAc;AACrC,oBAAgB,KAAK,GAAG,WAAW;AACrC,MAAI,OAAO,cAAc,YAAY;AACnC,oBAAgB,KAAK,GAAG,aAAa;AAIvC,aAAW,WAAW,OAAO,UAAU,QAAQ,CAAA,GAAI;AACjD,QAAI,mBAAmB,IAAI,OAAO,EAAG,iBAAgB,KAAK,OAAO;AAAA,EACnE;AACA,aAAW,WAAW,OAAO,UAAU,SAAS,CAAA,GAAI;AAClD,QAAI,mBAAmB,IAAI,OAAO,EAAG,cAAa,KAAK,OAAO;AAAA,EAChE;AAEA,QAAM,SAA4B;AAAA,IAChC,cAAc,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC;AAAA,IACvC,iBAAiB,CAAC,GAAG,IAAI,IAAI,eAAe,CAAC;AAAA,EAAA;AAE/C,MAAI,OAAO,YAAY;AACrB,WAAO,iBAAiB,QAAQ,OAAO,OAAO;AAChD,SAAO;AACT;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SandboxHandle, WorkspaceProjection } from '@tanstack/ai-sandbox';
|
|
2
|
+
/**
|
|
3
|
+
* Project a `WorkspaceProjection` into the Claude Code sandbox. Safe to call on
|
|
4
|
+
* every `chatStream`. The secret-bearing `.mcp.json` is (re)written on every
|
|
5
|
+
* call, re-resolving secrets, so claude always reads current values and a
|
|
6
|
+
* snapshot can never serve a stale or rotated secret. The safe, idempotent,
|
|
7
|
+
* non-secret operations (gitSkill links, plugin installs, agentSkill handling)
|
|
8
|
+
* are guarded by a one-time marker so they run only on the first call after
|
|
9
|
+
* create/restore.
|
|
10
|
+
*
|
|
11
|
+
* @param handle - The sandbox handle (`fs` + `process`).
|
|
12
|
+
* @param projection - The portable workspace inputs from `withSandbox`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function projectClaudeWorkspace(handle: SandboxHandle, projection: WorkspaceProjection): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { resolveGitSkillDir, isSecretRef } from "@tanstack/ai-sandbox";
|
|
2
|
+
function shellQuote(value) {
|
|
3
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4
|
+
}
|
|
5
|
+
function basenameOf(path) {
|
|
6
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
7
|
+
return segments[segments.length - 1] ?? path;
|
|
8
|
+
}
|
|
9
|
+
function isBearerMarker(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && isSecretRef(value.__bearerRef);
|
|
11
|
+
}
|
|
12
|
+
function resolveHeaderValue(value, resolveSecret) {
|
|
13
|
+
if (isSecretRef(value)) return resolveSecret(value);
|
|
14
|
+
if (isBearerMarker(value)) return `Bearer ${resolveSecret(value.__bearerRef)}`;
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function buildMcpConfig(skills, resolveSecret) {
|
|
18
|
+
const mcpServers = {};
|
|
19
|
+
let count = 0;
|
|
20
|
+
for (const skill of skills) {
|
|
21
|
+
if (skill.kind !== "mcp") continue;
|
|
22
|
+
count += 1;
|
|
23
|
+
const headers = {};
|
|
24
|
+
const rawHeaders = skill.config.headers ?? {};
|
|
25
|
+
for (const [name, value] of Object.entries(rawHeaders)) {
|
|
26
|
+
headers[name] = resolveHeaderValue(value, resolveSecret);
|
|
27
|
+
}
|
|
28
|
+
const rawUrl = skill.config["url"];
|
|
29
|
+
const url = typeof rawUrl === "string" ? rawUrl : "";
|
|
30
|
+
mcpServers[skill.name] = { type: "http", url, headers };
|
|
31
|
+
}
|
|
32
|
+
return count > 0 ? { mcpServers } : void 0;
|
|
33
|
+
}
|
|
34
|
+
async function projectMcpServers(handle, projection) {
|
|
35
|
+
const config = buildMcpConfig(projection.skills, projection.resolveSecret);
|
|
36
|
+
if (config === void 0) return;
|
|
37
|
+
const target = `${projection.root}/.mcp.json`;
|
|
38
|
+
await handle.fs.write(target, JSON.stringify(config, null, 2));
|
|
39
|
+
}
|
|
40
|
+
async function projectGitSkills(handle, projection) {
|
|
41
|
+
const skillsDir = `${projection.root}/.claude/skills`;
|
|
42
|
+
let madeDir = false;
|
|
43
|
+
for (const skill of projection.skills) {
|
|
44
|
+
if (skill.kind !== "git") continue;
|
|
45
|
+
if (!madeDir) {
|
|
46
|
+
await handle.fs.mkdir(skillsDir);
|
|
47
|
+
madeDir = true;
|
|
48
|
+
}
|
|
49
|
+
const source = skill.into ?? resolveGitSkillDir(projection.root, skill);
|
|
50
|
+
const target = `${skillsDir}/${basenameOf(source)}`;
|
|
51
|
+
const lnCmd = `ln -s ${shellQuote(source)} ${shellQuote(target)}`;
|
|
52
|
+
const result = await handle.process.exec(lnCmd, { cwd: projection.root });
|
|
53
|
+
if (result.exitCode !== 0) {
|
|
54
|
+
const cpCmd = `cp -r ${shellQuote(source)} ${shellQuote(target)}`;
|
|
55
|
+
const copied = await handle.process.exec(cpCmd, { cwd: projection.root });
|
|
56
|
+
if (copied.exitCode !== 0) {
|
|
57
|
+
console.warn(
|
|
58
|
+
`[claude-code] failed to link gitSkill "${skill.repo}" into ${target}: ${copied.stderr.trim()}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function projectAgentSkills(projection) {
|
|
65
|
+
for (const skill of projection.skills) {
|
|
66
|
+
if (skill.kind !== "agent-skill") continue;
|
|
67
|
+
console.warn(
|
|
68
|
+
`[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.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function projectPlugins(handle, projection) {
|
|
73
|
+
for (const name of projection.plugins) {
|
|
74
|
+
const cmd = `claude plugin install ${shellQuote(name)}`;
|
|
75
|
+
try {
|
|
76
|
+
const result = await handle.process.exec(cmd, { cwd: projection.root });
|
|
77
|
+
if (result.exitCode !== 0) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[claude-code] "claude plugin install ${name}" exited ${result.exitCode}: ${result.stderr.trim()}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
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
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function projectClaudeWorkspace(handle, projection) {
|
|
91
|
+
await projectMcpServers(handle, projection);
|
|
92
|
+
if (await handle.fs.exists(projection.markerPath)) return;
|
|
93
|
+
await projectGitSkills(handle, projection);
|
|
94
|
+
projectAgentSkills(projection);
|
|
95
|
+
await projectPlugins(handle, projection);
|
|
96
|
+
await handle.fs.write(projection.markerPath, "");
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
projectClaudeWorkspace
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=projection.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BaseTextAdapter, StructuredOutputOptions, StructuredOutputResult } from '@tanstack/ai/adapters';
|
|
2
|
+
import { SandboxHandle } from '@tanstack/ai-sandbox';
|
|
3
|
+
import { DefaultMessageMetadataByModality, Modality, StreamChunk, TextOptions } from '@tanstack/ai';
|
|
4
|
+
import { ClaudeCodeModel } from '../model-meta.js';
|
|
5
|
+
import { ClaudeCodeTextProviderOptions } from '../provider-options.js';
|
|
6
|
+
export type ClaudeCodePermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
|
|
7
|
+
export interface ClaudeCodeTextConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Working directory inside the sandbox where `claude` runs. Defaults to
|
|
10
|
+
* `/workspace` (the conventional sandbox workspace root).
|
|
11
|
+
*/
|
|
12
|
+
cwd?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Claude Code permission mode passed via `--permission-mode`. Defaults to
|
|
15
|
+
* `'bypassPermissions'` — a sandbox is isolated, so the agent is allowed to
|
|
16
|
+
* edit files and run commands without prompting. Tighten via `defineSandboxPolicy`
|
|
17
|
+
* / this option for less autonomy.
|
|
18
|
+
*/
|
|
19
|
+
permissionMode?: ClaudeCodePermissionMode;
|
|
20
|
+
/** Built-in tools the harness may use (`--allowedTools`). */
|
|
21
|
+
allowedTools?: Array<string>;
|
|
22
|
+
/** Built-in tools removed from the harness (`--disallowedTools`). */
|
|
23
|
+
disallowedTools?: Array<string>;
|
|
24
|
+
/** Extra directories the agent may access (`--add-dir`). */
|
|
25
|
+
addDirs?: Array<string>;
|
|
26
|
+
/** Maximum harness-internal turns (`--max-turns`). */
|
|
27
|
+
maxTurns?: number;
|
|
28
|
+
/**
|
|
29
|
+
* How `systemPrompts` from `chat()` are applied:
|
|
30
|
+
* - `'append'` (default): `--append-system-prompt` on top of the preset.
|
|
31
|
+
* - `'replace'`: `--system-prompt` as the entire system prompt.
|
|
32
|
+
*/
|
|
33
|
+
systemPromptMode?: 'append' | 'replace';
|
|
34
|
+
/** Path/name of the claude executable inside the sandbox. Defaults to `claude`. */
|
|
35
|
+
claudeExecutable?: string;
|
|
36
|
+
/** Emit token-level deltas via `--include-partial-messages` (default true). */
|
|
37
|
+
streamPartials?: boolean;
|
|
38
|
+
/** Extra environment variables for the claude process inside the sandbox. */
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
/** Emit a `file.changed` CUSTOM event with the git diff after the run (default true). */
|
|
41
|
+
emitDiff?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare class ClaudeCodeTextAdapter<TModel extends ClaudeCodeModel> extends BaseTextAdapter<TModel, ClaudeCodeTextProviderOptions, ReadonlyArray<Modality> & readonly ['text'], DefaultMessageMetadataByModality, ReadonlyArray<string>, unknown, never> {
|
|
44
|
+
readonly name: "claude-code";
|
|
45
|
+
readonly requires: readonly [import('@tanstack/ai').Capability<SandboxHandle, "sandbox">];
|
|
46
|
+
private readonly adapterConfig;
|
|
47
|
+
constructor(config: ClaudeCodeTextConfig, model: TModel);
|
|
48
|
+
private sandboxFrom;
|
|
49
|
+
private workdir;
|
|
50
|
+
/** Build the `claude` command line (prompt goes via stdin, not argv). */
|
|
51
|
+
private buildCommand;
|
|
52
|
+
/**
|
|
53
|
+
* Build the permission-prompt resolver the host MCP bridge exposes to claude
|
|
54
|
+
* (`--permission-prompt-tool`). Maps claude's permission request onto the
|
|
55
|
+
* sandbox policy + client approvals; on an `ask` action with no decision yet,
|
|
56
|
+
* records an approval-requested event and denies (the client re-runs to grant).
|
|
57
|
+
*/
|
|
58
|
+
private buildPermissionResolver;
|
|
59
|
+
chatStream(options: TextOptions<ClaudeCodeTextProviderOptions>): AsyncIterable<StreamChunk>;
|
|
60
|
+
structuredOutput(_options: StructuredOutputOptions<ClaudeCodeTextProviderOptions>): Promise<StructuredOutputResult<unknown>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a Claude Code harness adapter that runs **inside a sandbox**.
|
|
64
|
+
*
|
|
65
|
+
* Unlike HTTP provider adapters, this is a *harness* adapter: it spawns the
|
|
66
|
+
* `claude` CLI inside the sandbox provided by `withSandbox(...)` (the adapter
|
|
67
|
+
* declares `requires: [SandboxCapability]`), streams its `stream-json` stdout
|
|
68
|
+
* back as AG-UI events, and lets Claude Code run its own agent loop and native
|
|
69
|
+
* tools (Bash, file edits, search, …) against the sandbox workspace. The
|
|
70
|
+
* sandbox image must provide the `claude` executable and `ANTHROPIC_API_KEY`
|
|
71
|
+
* in its environment (e.g. via `workspace.secrets`). The session id is
|
|
72
|
+
* surfaced via a CUSTOM `claude-code.session-id` event so follow-up calls can
|
|
73
|
+
* resume through `modelOptions.sessionId`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function claudeCodeText<TModel extends ClaudeCodeModel>(model: TModel, config?: ClaudeCodeTextConfig): ClaudeCodeTextAdapter<TModel>;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { normalizeSystemPrompts, EventType } from "@tanstack/ai";
|
|
2
|
+
import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
3
|
+
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
4
|
+
import { SandboxCapability, getSandbox, approvalId, resolveApproval, buildApprovalRequestedEvent, createBridgeEventChannel, getWorkspaceProjection, getSandboxPolicy, getToolBridgeProvisioner, nodeHttpBridgeProvisioner, spawnNdjson, mergeChunkStreams } from "@tanstack/ai-sandbox";
|
|
5
|
+
import { buildPrompt } from "../messages/prompt.js";
|
|
6
|
+
import { translateSdkStream } from "../stream/translate.js";
|
|
7
|
+
import { mapPolicyToClaudeFlags } from "./policy-map.js";
|
|
8
|
+
import { projectClaudeWorkspace } from "./projection.js";
|
|
9
|
+
const DEFAULT_WORKDIR = "/workspace";
|
|
10
|
+
function q(value) {
|
|
11
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
12
|
+
}
|
|
13
|
+
function bridgeToMcpConfig(bridge) {
|
|
14
|
+
return JSON.stringify({
|
|
15
|
+
mcpServers: {
|
|
16
|
+
[bridge.name]: {
|
|
17
|
+
type: "http",
|
|
18
|
+
url: bridge.url,
|
|
19
|
+
headers: { Authorization: `Bearer ${bridge.token}` }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
class ClaudeCodeTextAdapter extends BaseTextAdapter {
|
|
25
|
+
name = "claude-code";
|
|
26
|
+
// Harness adapter: requires a sandbox to run the agent CLI inside.
|
|
27
|
+
requires = [SandboxCapability];
|
|
28
|
+
adapterConfig;
|
|
29
|
+
constructor(config, model) {
|
|
30
|
+
super({}, model);
|
|
31
|
+
this.adapterConfig = config;
|
|
32
|
+
}
|
|
33
|
+
sandboxFrom(options) {
|
|
34
|
+
const ctx = options.capabilities;
|
|
35
|
+
if (!ctx) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Adapter "claude-code" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware (e.g. with the local-process or docker provider).'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return getSandbox(ctx);
|
|
41
|
+
}
|
|
42
|
+
workdir(options) {
|
|
43
|
+
return options.modelOptions?.cwd ?? this.adapterConfig.cwd ?? DEFAULT_WORKDIR;
|
|
44
|
+
}
|
|
45
|
+
/** Build the `claude` command line (prompt goes via stdin, not argv). */
|
|
46
|
+
buildCommand(options, resume, policyFlags, mcpConfigPath, permissionPromptTool) {
|
|
47
|
+
const config = this.adapterConfig;
|
|
48
|
+
const modelOptions = options.modelOptions;
|
|
49
|
+
const exe = config.claudeExecutable ?? "claude";
|
|
50
|
+
const args = [
|
|
51
|
+
"-p",
|
|
52
|
+
"--output-format",
|
|
53
|
+
"stream-json",
|
|
54
|
+
"--verbose",
|
|
55
|
+
"--model",
|
|
56
|
+
q(this.model)
|
|
57
|
+
];
|
|
58
|
+
if (config.streamPartials !== false) args.push("--include-partial-messages");
|
|
59
|
+
if (resume !== void 0) args.push("--resume", q(resume));
|
|
60
|
+
const permissionMode = modelOptions?.permissionMode ?? config.permissionMode ?? policyFlags.permissionMode ?? "bypassPermissions";
|
|
61
|
+
args.push("--permission-mode", q(permissionMode));
|
|
62
|
+
const maxTurns = modelOptions?.maxTurns ?? config.maxTurns;
|
|
63
|
+
if (maxTurns !== void 0) args.push("--max-turns", String(maxTurns));
|
|
64
|
+
for (const dir of config.addDirs ?? []) args.push("--add-dir", q(dir));
|
|
65
|
+
const allowedTools = [
|
|
66
|
+
...modelOptions?.allowedTools ?? config.allowedTools ?? [],
|
|
67
|
+
...policyFlags.allowedTools
|
|
68
|
+
];
|
|
69
|
+
if (allowedTools.length > 0) {
|
|
70
|
+
args.push("--allowedTools", q([...new Set(allowedTools)].join(",")));
|
|
71
|
+
}
|
|
72
|
+
const disallowedTools = [
|
|
73
|
+
...modelOptions?.disallowedTools ?? config.disallowedTools ?? [],
|
|
74
|
+
...policyFlags.disallowedTools
|
|
75
|
+
];
|
|
76
|
+
if (disallowedTools.length > 0) {
|
|
77
|
+
args.push("--disallowedTools", q([...new Set(disallowedTools)].join(",")));
|
|
78
|
+
}
|
|
79
|
+
const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((prompt) => prompt.content).filter((content) => content.trim() !== "");
|
|
80
|
+
if (systemPrompts.length > 0) {
|
|
81
|
+
const joined = systemPrompts.join("\n\n");
|
|
82
|
+
const flag = config.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt";
|
|
83
|
+
args.push(flag, q(joined));
|
|
84
|
+
}
|
|
85
|
+
if (mcpConfigPath !== void 0) args.push("--mcp-config", q(mcpConfigPath));
|
|
86
|
+
if (permissionPromptTool !== void 0) {
|
|
87
|
+
args.push("--permission-prompt-tool", q(permissionPromptTool));
|
|
88
|
+
}
|
|
89
|
+
return `${exe} ${args.join(" ")}`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the permission-prompt resolver the host MCP bridge exposes to claude
|
|
93
|
+
* (`--permission-prompt-tool`). Maps claude's permission request onto the
|
|
94
|
+
* sandbox policy + client approvals; on an `ask` action with no decision yet,
|
|
95
|
+
* records an approval-requested event and denies (the client re-runs to grant).
|
|
96
|
+
*/
|
|
97
|
+
buildPermissionResolver(policy, approvals, scripts, sink, threadId, runId) {
|
|
98
|
+
const writeTools = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
99
|
+
const networkTools = /* @__PURE__ */ new Set(["WebFetch", "WebSearch"]);
|
|
100
|
+
return (request) => {
|
|
101
|
+
const toolName = request.tool_name ?? "tool";
|
|
102
|
+
const cmdInput = request.input;
|
|
103
|
+
const command = toolName === "Bash" && cmdInput !== null && typeof cmdInput === "object" && "command" in cmdInput && typeof cmdInput.command === "string" ? cmdInput.command : void 0;
|
|
104
|
+
const capability = writeTools.has(toolName) ? "fileWrite" : networkTools.has(toolName) ? "network" : void 0;
|
|
105
|
+
const id = approvalId({
|
|
106
|
+
provider: "claude-code",
|
|
107
|
+
kind: command !== void 0 ? "command" : capability ?? "tool",
|
|
108
|
+
target: command ?? toolName
|
|
109
|
+
});
|
|
110
|
+
const outcome = resolveApproval({
|
|
111
|
+
policy,
|
|
112
|
+
approvals,
|
|
113
|
+
id,
|
|
114
|
+
scripts,
|
|
115
|
+
...command !== void 0 ? { command } : {},
|
|
116
|
+
...capability !== void 0 ? { capability } : {}
|
|
117
|
+
});
|
|
118
|
+
if (outcome.needsApproval) {
|
|
119
|
+
sink.push(
|
|
120
|
+
buildApprovalRequestedEvent({
|
|
121
|
+
approvalId: id,
|
|
122
|
+
title: `Approve ${toolName}${command !== void 0 ? `: ${command}` : ""}`,
|
|
123
|
+
threadId,
|
|
124
|
+
runId,
|
|
125
|
+
detail: { provider: "claude-code", toolName }
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
behavior: "deny",
|
|
130
|
+
message: "Awaiting client approval. Approve in the UI and re-run to continue."
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return outcome.decision === "allow" ? { behavior: "allow" } : { behavior: "deny", message: "Denied by sandbox policy." };
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async *chatStream(options) {
|
|
137
|
+
const { logger } = options;
|
|
138
|
+
let bridge;
|
|
139
|
+
let channel;
|
|
140
|
+
const approvalRequests = [];
|
|
141
|
+
let cleanupSandbox;
|
|
142
|
+
const tempFiles = [];
|
|
143
|
+
try {
|
|
144
|
+
const sandbox = this.sandboxFrom(options);
|
|
145
|
+
cleanupSandbox = sandbox;
|
|
146
|
+
const cwd = this.workdir(options);
|
|
147
|
+
const runId = options.runId ?? this.generateId();
|
|
148
|
+
const threadId = options.threadId ?? this.generateId();
|
|
149
|
+
channel = createBridgeEventChannel({ model: this.model, threadId, runId });
|
|
150
|
+
const projection = options.capabilities ? getWorkspaceProjection(options.capabilities, { optional: true }) : void 0;
|
|
151
|
+
if (projection) await projectClaudeWorkspace(sandbox, projection);
|
|
152
|
+
const policy = options.capabilities ? getSandboxPolicy(options.capabilities, { optional: true }) : void 0;
|
|
153
|
+
const permission = policy !== void 0 ? {
|
|
154
|
+
toolName: "approval_prompt",
|
|
155
|
+
resolve: this.buildPermissionResolver(
|
|
156
|
+
policy,
|
|
157
|
+
options.approvals,
|
|
158
|
+
projection?.scripts,
|
|
159
|
+
approvalRequests,
|
|
160
|
+
threadId,
|
|
161
|
+
runId
|
|
162
|
+
)
|
|
163
|
+
} : void 0;
|
|
164
|
+
const hasTools = options.tools !== void 0 && options.tools.length > 0;
|
|
165
|
+
if (hasTools || permission !== void 0) {
|
|
166
|
+
const provisioner = (options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner;
|
|
167
|
+
bridge = await provisioner.provision(options.tools ?? [], {
|
|
168
|
+
provider: sandbox.provider,
|
|
169
|
+
context: options.context,
|
|
170
|
+
emitCustomEvent: channel.emitCustomEvent,
|
|
171
|
+
...permission !== void 0 ? { permission } : {},
|
|
172
|
+
...options.abortController?.signal ? { signal: options.abortController.signal } : {}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const { prompt, resume } = buildPrompt(
|
|
176
|
+
options.messages,
|
|
177
|
+
options.modelOptions?.sessionId
|
|
178
|
+
);
|
|
179
|
+
let mcpConfigArg;
|
|
180
|
+
if (bridge) {
|
|
181
|
+
const mcpConfigFile = `.tanstack-mcp-bridge-${runId}.json`;
|
|
182
|
+
const mcpConfigPath = `${cwd}/${mcpConfigFile}`;
|
|
183
|
+
await sandbox.fs.write(mcpConfigPath, bridgeToMcpConfig(bridge));
|
|
184
|
+
tempFiles.push(mcpConfigPath);
|
|
185
|
+
mcpConfigArg = mcpConfigFile;
|
|
186
|
+
}
|
|
187
|
+
const command = this.buildCommand(
|
|
188
|
+
options,
|
|
189
|
+
resume,
|
|
190
|
+
mapPolicyToClaudeFlags(policy),
|
|
191
|
+
mcpConfigArg,
|
|
192
|
+
bridge && permission ? `mcp__${bridge.name}__${permission.toolName}` : void 0
|
|
193
|
+
);
|
|
194
|
+
let runCommand = command;
|
|
195
|
+
let stdinInput = prompt;
|
|
196
|
+
if (sandbox.capabilities.writableStdin === false) {
|
|
197
|
+
const promptPath = `/tmp/tanstack-claude-prompt-${runId}`;
|
|
198
|
+
await sandbox.fs.write(promptPath, prompt);
|
|
199
|
+
tempFiles.push(promptPath);
|
|
200
|
+
runCommand = `${command} < ${q(promptPath)}`;
|
|
201
|
+
stdinInput = void 0;
|
|
202
|
+
}
|
|
203
|
+
logger.request(
|
|
204
|
+
`activity=chat provider=claude-code model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${resume ?? "none"}`,
|
|
205
|
+
{ provider: "claude-code", model: this.model }
|
|
206
|
+
);
|
|
207
|
+
const rawEvents = spawnNdjson(sandbox, runCommand, {
|
|
208
|
+
cwd,
|
|
209
|
+
...stdinInput !== void 0 ? { input: stdinInput } : {},
|
|
210
|
+
// claude maps `bypassPermissions` to `--dangerously-skip-permissions`,
|
|
211
|
+
// which it refuses to run as root. Sandbox containers routinely run as
|
|
212
|
+
// root (Docker / Cloudflare), so set `IS_SANDBOX=1` — claude's
|
|
213
|
+
// documented escape hatch for skip-permissions in an isolated
|
|
214
|
+
// environment — merged over the sandbox env (a caller-provided value
|
|
215
|
+
// wins). Safe to set unconditionally; it is a no-op for stricter modes.
|
|
216
|
+
env: { IS_SANDBOX: "1", ...this.adapterConfig.env },
|
|
217
|
+
...options.abortController?.signal ? { signal: options.abortController.signal } : options.request?.signal ? { signal: options.request.signal } : {},
|
|
218
|
+
onNonJsonLine: (line) => logger.provider(`provider=claude-code non-json line: ${line}`, {
|
|
219
|
+
chunk: line
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
async function* asMessages() {
|
|
223
|
+
for await (const event of rawEvents) yield event;
|
|
224
|
+
}
|
|
225
|
+
yield* mergeChunkStreams(
|
|
226
|
+
translateSdkStream(asMessages(), {
|
|
227
|
+
model: this.model,
|
|
228
|
+
runId,
|
|
229
|
+
threadId,
|
|
230
|
+
...options.parentRunId !== void 0 && {
|
|
231
|
+
parentRunId: options.parentRunId
|
|
232
|
+
},
|
|
233
|
+
genId: () => this.generateId(),
|
|
234
|
+
onSdkMessage: (message) => logger.provider(`provider=claude-code type=${message.type}`, {
|
|
235
|
+
chunk: message
|
|
236
|
+
})
|
|
237
|
+
}),
|
|
238
|
+
channel.stream
|
|
239
|
+
);
|
|
240
|
+
if (this.adapterConfig.emitDiff !== false) {
|
|
241
|
+
try {
|
|
242
|
+
const diff = await sandbox.process.exec(`git -C ${q(cwd)} diff`, {
|
|
243
|
+
cwd
|
|
244
|
+
});
|
|
245
|
+
if (diff.exitCode === 0 && diff.stdout.trim() !== "") {
|
|
246
|
+
yield {
|
|
247
|
+
type: EventType.CUSTOM,
|
|
248
|
+
name: "file.changed",
|
|
249
|
+
value: { path: ".", diff: diff.stdout },
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
threadId,
|
|
252
|
+
runId
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const event of approvalRequests) yield event;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const err = error;
|
|
261
|
+
const rawEvent = toRunErrorRawEvent(error);
|
|
262
|
+
logger.errors("claude-code.chatStream fatal", {
|
|
263
|
+
error,
|
|
264
|
+
source: "claude-code.chatStream"
|
|
265
|
+
});
|
|
266
|
+
yield {
|
|
267
|
+
type: EventType.RUN_ERROR,
|
|
268
|
+
model: options.model,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
message: err.message || "Unknown error occurred",
|
|
271
|
+
...err.code !== void 0 && { code: err.code },
|
|
272
|
+
...rawEvent !== void 0 && { rawEvent },
|
|
273
|
+
error: {
|
|
274
|
+
message: err.message || "Unknown error occurred",
|
|
275
|
+
...err.code !== void 0 && { code: err.code }
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
} finally {
|
|
279
|
+
channel?.close();
|
|
280
|
+
if (bridge) await bridge.close();
|
|
281
|
+
if (cleanupSandbox) {
|
|
282
|
+
for (const path of tempFiles) {
|
|
283
|
+
try {
|
|
284
|
+
await cleanupSandbox.fs.remove(path);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
structuredOutput(_options) {
|
|
292
|
+
return Promise.reject(
|
|
293
|
+
new Error(
|
|
294
|
+
"Structured output is not yet supported by the in-sandbox Claude Code adapter. Use a model adapter (e.g. anthropic) for structured output, or omit outputSchema."
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function claudeCodeText(model, config = {}) {
|
|
300
|
+
return new ClaudeCodeTextAdapter(config, model);
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
ClaudeCodeTextAdapter,
|
|
304
|
+
claudeCodeText
|
|
305
|
+
};
|
|
306
|
+
//# sourceMappingURL=text.js.map
|