@tanstack/ai-claude-code 0.2.3 → 0.3.1
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,306 +1,281 @@
|
|
|
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
1
|
import { buildPrompt } from "../messages/prompt.js";
|
|
6
2
|
import { translateSdkStream } from "../stream/translate.js";
|
|
7
3
|
import { mapPolicyToClaudeFlags } from "./policy-map.js";
|
|
8
4
|
import { projectClaudeWorkspace } from "./projection.js";
|
|
9
|
-
|
|
5
|
+
import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
|
|
6
|
+
import { toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
|
|
7
|
+
import { BaseTextAdapter } from "@tanstack/ai/adapters";
|
|
8
|
+
import { SandboxCapability, alignedIfAttaching, approvalId, buildApprovalRequestedEvent, createBridgeEventChannel, createRunScopedIdGen, encodeRunId, getSandbox, getSandboxDurability, getSandboxPolicy, getToolBridgeProvisioner, getWorkspaceProjection, journalOptionsFor, mergeChunkStreams, nodeHttpBridgeProvisioner, resolveApproval, resolveDurableRunId, resolveDurableThreadId, spawnNdjson } from "@tanstack/ai-sandbox";
|
|
9
|
+
//#region src/adapters/text.ts
|
|
10
|
+
var DEFAULT_WORKDIR = "/workspace";
|
|
11
|
+
/** POSIX single-quote escape for embedding values in the `claude …` command. */
|
|
10
12
|
function q(value) {
|
|
11
|
-
|
|
13
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
12
14
|
}
|
|
15
|
+
/** Format a host tool-bridge as claude's `--mcp-config` JSON. */
|
|
13
16
|
function bridgeToMcpConfig(bridge) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
}
|
|
17
|
+
return JSON.stringify({ mcpServers: { [bridge.name]: {
|
|
18
|
+
type: "http",
|
|
19
|
+
url: bridge.url,
|
|
20
|
+
headers: { Authorization: `Bearer ${bridge.token}` }
|
|
21
|
+
} } });
|
|
298
22
|
}
|
|
23
|
+
var ClaudeCodeTextAdapter = class extends BaseTextAdapter {
|
|
24
|
+
name = "claude-code";
|
|
25
|
+
requires = [SandboxCapability];
|
|
26
|
+
adapterConfig;
|
|
27
|
+
constructor(config, model) {
|
|
28
|
+
super({}, model);
|
|
29
|
+
this.adapterConfig = config;
|
|
30
|
+
}
|
|
31
|
+
sandboxFrom(options) {
|
|
32
|
+
const ctx = options.capabilities;
|
|
33
|
+
if (!ctx) throw new Error("Adapter \"claude-code\" requires a sandbox. Add withSandbox(defineSandbox({ ... })) to chat() middleware (e.g. with the local-process or docker provider).");
|
|
34
|
+
return getSandbox(ctx);
|
|
35
|
+
}
|
|
36
|
+
workdir(options) {
|
|
37
|
+
return options.modelOptions?.cwd ?? this.adapterConfig.cwd ?? DEFAULT_WORKDIR;
|
|
38
|
+
}
|
|
39
|
+
/** Build the `claude` command line (prompt goes via stdin, not argv). */
|
|
40
|
+
buildCommand(options, resume, policyFlags, mcpConfigPath, permissionPromptTool) {
|
|
41
|
+
const config = this.adapterConfig;
|
|
42
|
+
const modelOptions = options.modelOptions;
|
|
43
|
+
const exe = config.claudeExecutable ?? "claude";
|
|
44
|
+
const args = [
|
|
45
|
+
"-p",
|
|
46
|
+
"--output-format",
|
|
47
|
+
"stream-json",
|
|
48
|
+
"--verbose",
|
|
49
|
+
"--model",
|
|
50
|
+
q(this.model)
|
|
51
|
+
];
|
|
52
|
+
if (config.streamPartials !== false) args.push("--include-partial-messages");
|
|
53
|
+
if (resume !== void 0) args.push("--resume", q(resume));
|
|
54
|
+
const permissionMode = modelOptions?.permissionMode ?? config.permissionMode ?? policyFlags.permissionMode ?? "bypassPermissions";
|
|
55
|
+
args.push("--permission-mode", q(permissionMode));
|
|
56
|
+
const maxTurns = modelOptions?.maxTurns ?? config.maxTurns;
|
|
57
|
+
if (maxTurns !== void 0) args.push("--max-turns", String(maxTurns));
|
|
58
|
+
for (const dir of config.addDirs ?? []) args.push("--add-dir", q(dir));
|
|
59
|
+
const allowedTools = [...modelOptions?.allowedTools ?? config.allowedTools ?? [], ...policyFlags.allowedTools];
|
|
60
|
+
if (allowedTools.length > 0) args.push("--allowedTools", q([...new Set(allowedTools)].join(",")));
|
|
61
|
+
const disallowedTools = [...modelOptions?.disallowedTools ?? config.disallowedTools ?? [], ...policyFlags.disallowedTools];
|
|
62
|
+
if (disallowedTools.length > 0) args.push("--disallowedTools", q([...new Set(disallowedTools)].join(",")));
|
|
63
|
+
const systemPrompts = normalizeSystemPrompts(options.systemPrompts).map((prompt) => prompt.content).filter((content) => content.trim() !== "");
|
|
64
|
+
if (systemPrompts.length > 0) {
|
|
65
|
+
const joined = systemPrompts.join("\n\n");
|
|
66
|
+
const flag = config.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt";
|
|
67
|
+
args.push(flag, q(joined));
|
|
68
|
+
}
|
|
69
|
+
if (mcpConfigPath !== void 0) args.push("--mcp-config", q(mcpConfigPath));
|
|
70
|
+
if (permissionPromptTool !== void 0) args.push("--permission-prompt-tool", q(permissionPromptTool));
|
|
71
|
+
return `${exe} ${args.join(" ")}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build the permission-prompt resolver the host MCP bridge exposes to claude
|
|
75
|
+
* (`--permission-prompt-tool`). Maps claude's permission request onto the
|
|
76
|
+
* sandbox policy + client approvals; on an `ask` action with no decision yet,
|
|
77
|
+
* records an approval-requested event and denies (the client re-runs to grant).
|
|
78
|
+
*/
|
|
79
|
+
buildPermissionResolver(policy, approvals, scripts, sink, threadId, runId) {
|
|
80
|
+
const writeTools = /* @__PURE__ */ new Set([
|
|
81
|
+
"Write",
|
|
82
|
+
"Edit",
|
|
83
|
+
"MultiEdit",
|
|
84
|
+
"NotebookEdit"
|
|
85
|
+
]);
|
|
86
|
+
const networkTools = /* @__PURE__ */ new Set(["WebFetch", "WebSearch"]);
|
|
87
|
+
return (request) => {
|
|
88
|
+
const toolName = request.tool_name ?? "tool";
|
|
89
|
+
const cmdInput = request.input;
|
|
90
|
+
const command = toolName === "Bash" && cmdInput !== null && typeof cmdInput === "object" && "command" in cmdInput && typeof cmdInput.command === "string" ? cmdInput.command : void 0;
|
|
91
|
+
const capability = writeTools.has(toolName) ? "fileWrite" : networkTools.has(toolName) ? "network" : void 0;
|
|
92
|
+
const id = approvalId({
|
|
93
|
+
provider: "claude-code",
|
|
94
|
+
kind: command !== void 0 ? "command" : capability ?? "tool",
|
|
95
|
+
target: command ?? toolName
|
|
96
|
+
});
|
|
97
|
+
const outcome = resolveApproval({
|
|
98
|
+
policy,
|
|
99
|
+
approvals,
|
|
100
|
+
id,
|
|
101
|
+
scripts,
|
|
102
|
+
...command !== void 0 ? { command } : {},
|
|
103
|
+
...capability !== void 0 ? { capability } : {}
|
|
104
|
+
});
|
|
105
|
+
if (outcome.needsApproval) {
|
|
106
|
+
sink.push(buildApprovalRequestedEvent({
|
|
107
|
+
approvalId: id,
|
|
108
|
+
title: `Approve ${toolName}${command !== void 0 ? `: ${command}` : ""}`,
|
|
109
|
+
threadId,
|
|
110
|
+
runId,
|
|
111
|
+
detail: {
|
|
112
|
+
provider: "claude-code",
|
|
113
|
+
toolName
|
|
114
|
+
}
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
behavior: "deny",
|
|
118
|
+
message: "Awaiting client approval. Approve in the UI and re-run to continue."
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return outcome.decision === "allow" ? { behavior: "allow" } : {
|
|
122
|
+
behavior: "deny",
|
|
123
|
+
message: "Denied by sandbox policy."
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async *chatStream(options) {
|
|
128
|
+
const { logger } = options;
|
|
129
|
+
let bridge;
|
|
130
|
+
let channel;
|
|
131
|
+
const approvalRequests = [];
|
|
132
|
+
let cleanupSandbox;
|
|
133
|
+
const tempFiles = [];
|
|
134
|
+
try {
|
|
135
|
+
const sandbox = this.sandboxFrom(options);
|
|
136
|
+
cleanupSandbox = sandbox;
|
|
137
|
+
const cwd = this.workdir(options);
|
|
138
|
+
const durability = options.capabilities ? getSandboxDurability(options.capabilities, { optional: true }) : void 0;
|
|
139
|
+
const runId = resolveDurableRunId(options.runId, {
|
|
140
|
+
durable: durability !== void 0,
|
|
141
|
+
adapter: "claude-code",
|
|
142
|
+
fallback: () => this.generateId()
|
|
143
|
+
});
|
|
144
|
+
const threadId = resolveDurableThreadId(options.threadId, {
|
|
145
|
+
durable: durability !== void 0,
|
|
146
|
+
attaching: durability?.attach === true,
|
|
147
|
+
adapter: "claude-code",
|
|
148
|
+
fallback: () => this.generateId()
|
|
149
|
+
});
|
|
150
|
+
channel = createBridgeEventChannel({
|
|
151
|
+
model: this.model,
|
|
152
|
+
threadId,
|
|
153
|
+
runId
|
|
154
|
+
});
|
|
155
|
+
const projection = options.capabilities ? getWorkspaceProjection(options.capabilities, { optional: true }) : void 0;
|
|
156
|
+
if (projection) await projectClaudeWorkspace(sandbox, projection);
|
|
157
|
+
const policy = options.capabilities ? getSandboxPolicy(options.capabilities, { optional: true }) : void 0;
|
|
158
|
+
const permission = policy !== void 0 ? {
|
|
159
|
+
toolName: "approval_prompt",
|
|
160
|
+
resolve: this.buildPermissionResolver(policy, options.approvals, projection?.scripts, approvalRequests, threadId, runId)
|
|
161
|
+
} : void 0;
|
|
162
|
+
if (options.tools !== void 0 && options.tools.length > 0 || permission !== void 0) bridge = await ((options.capabilities ? getToolBridgeProvisioner(options.capabilities, { optional: true }) : void 0) ?? nodeHttpBridgeProvisioner).provision(options.tools ?? [], {
|
|
163
|
+
provider: sandbox.provider,
|
|
164
|
+
context: options.context,
|
|
165
|
+
emitCustomEvent: channel.emitCustomEvent,
|
|
166
|
+
...permission !== void 0 ? { permission } : {},
|
|
167
|
+
...options.abortController?.signal ? { signal: options.abortController.signal } : {}
|
|
168
|
+
});
|
|
169
|
+
const { prompt, resume } = buildPrompt(options.messages, options.modelOptions?.sessionId);
|
|
170
|
+
const runIdSegment = encodeRunId(runId);
|
|
171
|
+
let mcpConfigArg;
|
|
172
|
+
if (bridge) {
|
|
173
|
+
const mcpConfigFile = `.tanstack-mcp-bridge-${runIdSegment}.json`;
|
|
174
|
+
const mcpConfigPath = `${cwd}/${mcpConfigFile}`;
|
|
175
|
+
await sandbox.fs.write(mcpConfigPath, bridgeToMcpConfig(bridge));
|
|
176
|
+
tempFiles.push(mcpConfigPath);
|
|
177
|
+
mcpConfigArg = mcpConfigFile;
|
|
178
|
+
}
|
|
179
|
+
const command = this.buildCommand(options, resume, mapPolicyToClaudeFlags(policy), mcpConfigArg, bridge && permission ? `mcp__${bridge.name}__${permission.toolName}` : void 0);
|
|
180
|
+
let runCommand = command;
|
|
181
|
+
let stdinInput = prompt;
|
|
182
|
+
if (sandbox.capabilities.writableStdin === false) {
|
|
183
|
+
const promptPath = `/tmp/tanstack-claude-prompt-${runIdSegment}`;
|
|
184
|
+
await sandbox.fs.write(promptPath, prompt);
|
|
185
|
+
tempFiles.push(promptPath);
|
|
186
|
+
runCommand = `${command} < ${q(promptPath)}`;
|
|
187
|
+
stdinInput = void 0;
|
|
188
|
+
}
|
|
189
|
+
logger.request(`activity=chat provider=claude-code model=${this.model} sandbox=${sandbox.provider} messages=${options.messages.length} resume=${resume ?? "none"}`, {
|
|
190
|
+
provider: "claude-code",
|
|
191
|
+
model: this.model
|
|
192
|
+
});
|
|
193
|
+
const journalOptions = journalOptionsFor(durability, runId);
|
|
194
|
+
const rawEvents = spawnNdjson(sandbox, runCommand, {
|
|
195
|
+
cwd,
|
|
196
|
+
...stdinInput !== void 0 ? { input: stdinInput } : {},
|
|
197
|
+
env: {
|
|
198
|
+
IS_SANDBOX: "1",
|
|
199
|
+
...this.adapterConfig.env
|
|
200
|
+
},
|
|
201
|
+
...options.abortController?.signal ? { signal: options.abortController.signal } : options.request?.signal ? { signal: options.request.signal } : {},
|
|
202
|
+
onNonJsonLine: (line) => logger.provider(`provider=claude-code non-json line: ${line}`, { chunk: line }),
|
|
203
|
+
...journalOptions === void 0 ? {} : { journal: journalOptions }
|
|
204
|
+
});
|
|
205
|
+
async function* asMessages() {
|
|
206
|
+
for await (const event of rawEvents) yield event;
|
|
207
|
+
}
|
|
208
|
+
yield* alignedIfAttaching(mergeChunkStreams(translateSdkStream(asMessages(), {
|
|
209
|
+
model: this.model,
|
|
210
|
+
runId,
|
|
211
|
+
threadId,
|
|
212
|
+
...options.parentRunId !== void 0 && { parentRunId: options.parentRunId },
|
|
213
|
+
genId: createRunScopedIdGen(runId),
|
|
214
|
+
onSdkMessage: (message) => logger.provider(`provider=claude-code type=${message.type}`, { chunk: message })
|
|
215
|
+
}), channel.stream), durability, logger);
|
|
216
|
+
if (this.adapterConfig.emitDiff !== false) try {
|
|
217
|
+
const diff = await sandbox.process.exec(`git -C ${q(cwd)} diff`, { cwd });
|
|
218
|
+
if (diff.exitCode === 0 && diff.stdout.trim() !== "") yield {
|
|
219
|
+
type: EventType.CUSTOM,
|
|
220
|
+
name: "file.changed",
|
|
221
|
+
value: {
|
|
222
|
+
path: ".",
|
|
223
|
+
diff: diff.stdout
|
|
224
|
+
},
|
|
225
|
+
timestamp: Date.now(),
|
|
226
|
+
threadId,
|
|
227
|
+
runId
|
|
228
|
+
};
|
|
229
|
+
} catch {}
|
|
230
|
+
for (const event of approvalRequests) yield event;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const err = error;
|
|
233
|
+
const rawEvent = toRunErrorRawEvent(error);
|
|
234
|
+
logger.errors("claude-code.chatStream fatal", {
|
|
235
|
+
error,
|
|
236
|
+
source: "claude-code.chatStream"
|
|
237
|
+
});
|
|
238
|
+
yield {
|
|
239
|
+
type: EventType.RUN_ERROR,
|
|
240
|
+
model: options.model,
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
message: err.message || "Unknown error occurred",
|
|
243
|
+
...err.code !== void 0 && { code: err.code },
|
|
244
|
+
...rawEvent !== void 0 && { rawEvent },
|
|
245
|
+
error: {
|
|
246
|
+
message: err.message || "Unknown error occurred",
|
|
247
|
+
...err.code !== void 0 && { code: err.code }
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
} finally {
|
|
251
|
+
channel?.close();
|
|
252
|
+
if (bridge) await bridge.close();
|
|
253
|
+
if (cleanupSandbox) for (const path of tempFiles) try {
|
|
254
|
+
await cleanupSandbox.fs.remove(path);
|
|
255
|
+
} catch {}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
structuredOutput(_options) {
|
|
259
|
+
return Promise.reject(/* @__PURE__ */ new Error("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."));
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Creates a Claude Code harness adapter that runs **inside a sandbox**.
|
|
264
|
+
*
|
|
265
|
+
* Unlike HTTP provider adapters, this is a *harness* adapter: it spawns the
|
|
266
|
+
* `claude` CLI inside the sandbox provided by `withSandbox(...)` (the adapter
|
|
267
|
+
* declares `requires: [SandboxCapability]`), streams its `stream-json` stdout
|
|
268
|
+
* back as AG-UI events, and lets Claude Code run its own agent loop and native
|
|
269
|
+
* tools (Bash, file edits, search, …) against the sandbox workspace. The
|
|
270
|
+
* sandbox image must provide the `claude` executable and `ANTHROPIC_API_KEY`
|
|
271
|
+
* in its environment (e.g. via `workspace.secrets`). The session id is
|
|
272
|
+
* surfaced via a CUSTOM `claude-code.session-id` event so follow-up calls can
|
|
273
|
+
* resume through `modelOptions.sessionId`.
|
|
274
|
+
*/
|
|
299
275
|
function claudeCodeText(model, config = {}) {
|
|
300
|
-
|
|
276
|
+
return new ClaudeCodeTextAdapter(config, model);
|
|
301
277
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
//# sourceMappingURL=text.js.map
|
|
278
|
+
//#endregion
|
|
279
|
+
export { ClaudeCodeTextAdapter, claudeCodeText };
|
|
280
|
+
|
|
281
|
+
//# sourceMappingURL=text.js.map
|