import { Sandbox } from "@vercel/sandbox"; import { MOLTBOOK_API_URL } from "../constants"; import type { TriggerType } from "./prompts"; import { buildSystemPrompt, buildUserMessage } from "./prompts"; import type { ToolDef } from "./tool-definitions"; import { CLAWBOOK_TOOL_NAMES, CLAWBOOK_TOOLS, MOLTBOOK_TOOL_NAMES, MOLTBOOK_TOOLS, } from "./tool-definitions"; export interface AgentAction { tool: string; input: Record; } export interface AgentTurnResult { success: boolean; summary: string; actions: AgentAction[]; error?: string; } function failureResult(error: string): AgentTurnResult { return { success: false, summary: "", actions: [], error }; } export interface RunAgentOptions { clawbookApiUrl: string; sigmaMemberWif: string; moltbookApiKey?: string; anthropicAuthToken?: string; botIdentity?: { idKey: string; name: string }; customMessage?: string; } function generateInputSchema(tool: ToolDef): string { if (tool.inputFields.length === 0) return "{}"; const fields = tool.inputFields.map((f) => { const zType = f.type === "number" ? "z.number()" : "z.string()"; const optional = f.required ? "" : ".optional()"; return `${f.name}: ${zType}${optional}.describe(${JSON.stringify(f.description)})`; }); return `{ ${fields.join(", ")} }`; } // -- Codegen helpers for MCP tool response format -- /** Generate `{ content: [{ type: "text", text: }] }` */ function mcpText(textExpr: string): string { return `{ content: [{ type: "text", text: ${textExpr} }] }`; } /** Generate a catch block that returns an MCP error response */ function mcpCatchBlock(): string { return `catch (err) { return ${mcpText('JSON.stringify({ error: err.message || "Unknown error" })')}; }`; } /** Generate an MCP error return for failed API responses */ function mcpFailReturn(): string { return `return ${mcpText('JSON.stringify({ error: res.error || "Failed" })')};`; } /** Generate the success return expression based on tool config */ function mcpSuccessReturn(tool: ToolDef): string { if (tool.successText) { return `return ${mcpText(JSON.stringify(tool.successText))};`; } return `return ${mcpText("JSON.stringify(res.data)")};`; } /** Generate `(args)` or `()` depending on whether the tool has inputs */ function argsParam(tool: ToolDef): string { return tool.inputFields.length === 0 ? "()" : "(args)"; } // -- Path expression generation -- /** Build the URL path expression from tool definition, using declarative queryParams */ function generatePathExpr(tool: ToolDef): string { // Start with base path, substituting :param placeholders let basePath = tool.path; if (tool.pathParams?.length) { for (const param of tool.pathParams) { basePath = basePath.replace( `:${param}`, `" + encodeURIComponent(args.${param}) + "`, ); } } let pathExpr = `"${basePath}"`; // Append query parameters declaratively if (tool.queryParams?.length) { const segments: string[] = []; let hasGuaranteedParam = false; for (const qp of tool.queryParams) { const needsEncode = qp.field !== "limit" && qp.field !== "sort"; const hasDefault = qp.defaultValue !== undefined; const valueExpr = hasDefault ? `(args.${qp.field} || ${JSON.stringify(qp.defaultValue)})` : `args.${qp.field}`; const encodedValue = needsEncode ? `encodeURIComponent(${valueExpr})` : valueExpr; const sep = hasGuaranteedParam ? "&" : "?"; if (hasDefault) { segments.push(`"${sep}${qp.key}=" + ${encodedValue}`); hasGuaranteedParam = true; } else if (hasGuaranteedParam) { segments.push( `(args.${qp.field} ? "&${qp.key}=" + ${encodedValue} : "")`, ); } else { segments.push( `(args.${qp.field} ? "?${qp.key}=" + ${encodedValue} : "")`, ); } } pathExpr = `${pathExpr} + ${segments.join(" + ")}`; } return pathExpr; } // -- Handler code generation per network -- function generateHandlerCode(tool: ToolDef): string { if (tool.network === "moltbook") { return generateMoltbookHandlerCode(tool); } return generateClawbookHandlerCode(tool); } function generateMoltbookHandlerCode(tool: ToolDef): string { const pathExpr = generatePathExpr(tool); const args = argsParam(tool); // POST/PATCH/DELETE with body if (tool.method !== "GET" && tool.bodyFields?.length) { const bodyProps = tool.bodyFields.map((f) => `${f}: args.${f}`); return `async ${args} => { try { const res = await moltbookRequest(${pathExpr}, { method: "${tool.method}", body: JSON.stringify({ ${bodyProps.join(", ")} }), }); return ${mcpText("JSON.stringify(res)")}; } ${mcpCatchBlock()} }`; } // POST/PATCH/DELETE without body if (tool.method !== "GET") { return `async ${args} => { try { const res = await moltbookRequest(${pathExpr}, { method: "${tool.method}" }); return ${mcpText("JSON.stringify(res)")}; } ${mcpCatchBlock()} }`; } // GET return `async ${args} => { try { const res = await moltbookRequest(${pathExpr}); return ${mcpText("JSON.stringify(res)")}; } ${mcpCatchBlock()} }`; } function generateClawbookHandlerCode(tool: ToolDef): string { const pathExpr = generatePathExpr(tool); const args = argsParam(tool); if (tool.auth) { // Authenticated GET if (tool.method === "GET") { return `async ${args} => { try { const res = await request(${pathExpr}, { method: "GET", headers: signRequest(${pathExpr}, undefined), }); if (!res.success) ${mcpFailReturn()} ${mcpSuccessReturn(tool)} } ${mcpCatchBlock()} }`; } // Authenticated POST/DELETE/PATCH with signed body const bodyProps = (tool.bodyFields || []).map((f) => `${f}: args.${f}`); if (tool.name === "create_post" || tool.name === "reply_to_post") { bodyProps.push('contentType: "text/markdown"'); } return `async (args) => { try { const body = { ${bodyProps.join(", ")} }; const res = await request(${pathExpr}, { method: "${tool.method}", headers: signRequest(${pathExpr}, body), body: JSON.stringify(body), }); if (!res.success) ${mcpFailReturn()} ${mcpSuccessReturn(tool)} } ${mcpCatchBlock()} }`; } // Unauthenticated with custom transform if (tool.transformResponse) { return `async (args) => { try { const res = await request(${pathExpr}, { headers }); if (!res.success) ${mcpFailReturn()} ${tool.transformResponse} return ${mcpText("result")}; } ${mcpCatchBlock()} }`; } // Unauthenticated default return `async ${args} => { try { const res = await request(${pathExpr}, { headers }); if (!res.success) ${mcpFailReturn()} return ${mcpText("JSON.stringify(res.data)")}; } ${mcpCatchBlock()} }`; } function generateToolsArray(tools: ToolDef[]): string { const items = tools.map((tool) => { const schema = generateInputSchema(tool); const handler = generateHandlerCode(tool); return ` { name: ${JSON.stringify(tool.name)}, description: ${JSON.stringify(tool.description)}, inputSchema: ${schema}, handler: ${handler}, }`; }); return items.join(",\n"); } export function buildAgentScript(params: { systemPrompt: string; userMessage: string; clawbookApiUrl: string; sigmaMemberWif: string; moltbookApiUrl?: string; moltbookApiKey?: string; }): string { function escapeForTemplate(s: string): string { return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$"); } const systemPrompt = escapeForTemplate(params.systemPrompt); const userMessage = escapeForTemplate(params.userMessage); const apiUrl = params.clawbookApiUrl; const wif = params.sigmaMemberWif; const hasMoltbook = !!(params.moltbookApiUrl && params.moltbookApiKey); const clawbookToolsArray = generateToolsArray(CLAWBOOK_TOOLS); const clawbookAllowedJson = JSON.stringify(CLAWBOOK_TOOL_NAMES); let moltbookBlock = ""; let moltbookServerDecl = ""; let moltbookServerRef = ""; let moltbookAllowedJson = "[]"; if (hasMoltbook) { moltbookBlock = ` const MOLTBOOK_URL = "${params.moltbookApiUrl}"; const MOLTBOOK_KEY = "${params.moltbookApiKey}"; async function moltbookRequest(path, options = {}) { const res = await fetch(MOLTBOOK_URL + path, { ...options, headers: { "Content-Type": "application/json", "Authorization": "Bearer " + MOLTBOOK_KEY, ...options.headers }, }); return res.json(); } `; const moltbookToolsArray = generateToolsArray(MOLTBOOK_TOOLS); moltbookServerDecl = ` const moltbookServer = createSdkMcpServer({ name: "moltbook", version: "1.0.0", tools: [ ${moltbookToolsArray} ], }); `; moltbookServerRef = ", moltbook: moltbookServer"; moltbookAllowedJson = JSON.stringify(MOLTBOOK_TOOL_NAMES); } return ` import { query, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"; import { z } from "zod"; import { getAuthToken } from "bitcoin-auth"; const API_URL = "${apiUrl}"; const WIF = "${wif}"; async function request(path, options) { const res = await fetch(API_URL + path, options); return res.json(); } const headers = { "Content-Type": "application/json" }; function signRequest(path, bodyObj) { const bodyStr = bodyObj ? JSON.stringify(bodyObj) : undefined; const token = getAuthToken({ privateKeyWif: WIF, requestPath: path, body: bodyStr, scheme: "bsm", }); return { "Content-Type": "application/json", "X-Auth-Token": token }; } ${moltbookBlock} const clawbookServer = createSdkMcpServer({ name: "clawbook", version: "1.0.0", tools: [ ${clawbookToolsArray} ], }); ${moltbookServerDecl} const systemPrompt = \`${systemPrompt}\`; const userMessage = \`${userMessage}\`; async function main() { const actions = []; const q = query({ prompt: userMessage, options: { systemPrompt, model: "claude-sonnet-4-20250514", maxTurns: 10, permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, tools: [], mcpServers: { clawbook: clawbookServer${moltbookServerRef} }, allowedTools: [...${clawbookAllowedJson}, ...${moltbookAllowedJson}], }, }); let summary = ""; for await (const msg of q) { if (msg.type === "assistant" && msg.message?.content) { for (const block of msg.message.content) { if (block.type === "tool_use") { actions.push({ tool: block.name, input: block.input || {} }); } } } if (msg.type === "result") { if (msg.subtype === "success" && msg.result) { summary = msg.result; } } } console.log(JSON.stringify({ success: true, summary, actions })); } main().catch((err) => { const detail = err.stack || err.message || String(err); console.log(JSON.stringify({ success: false, summary: "", actions: [], error: detail })); process.exit(1); }); `; } export async function runAgentTurn( trigger: TriggerType, options: RunAgentOptions, ): Promise { const hasMoltbook = !!options.moltbookApiKey; const systemPrompt = buildSystemPrompt( options.botIdentity, hasMoltbook ? { moltbook: true } : undefined, trigger === "conversation", ); const networks = hasMoltbook ? { moltbook: true } : undefined; const userMessage = buildUserMessage( trigger, options.botIdentity, networks, options.customMessage, ); const script = buildAgentScript({ systemPrompt, userMessage, clawbookApiUrl: options.clawbookApiUrl, sigmaMemberWif: options.sigmaMemberWif, moltbookApiUrl: hasMoltbook ? MOLTBOOK_API_URL : undefined, moltbookApiKey: options.moltbookApiKey, }); let sandbox: Awaited> | undefined; try { // CLAUDE_CODE_OAUTH_TOKEN is what the CLI reads for OAuth auth. // NOT ANTHROPIC_AUTH_TOKEN — that sends a raw bearer token which gets rejected. const sandboxEnv: Record = {}; if (options.anthropicAuthToken) { sandboxEnv.CLAUDE_CODE_OAUTH_TOKEN = options.anthropicAuthToken; } sandbox = await Sandbox.create({ timeout: 5 * 60 * 1000, }); await sandbox.writeFiles([ { path: "agent.mjs", content: Buffer.from(script, "utf-8") }, ]); console.log("[agent] sandbox created, installing Claude Code CLI..."); const cliInstall = await sandbox.runCommand({ cmd: "npm", args: ["install", "-g", "@anthropic-ai/claude-code"], env: sandboxEnv, sudo: true, }); console.log(`[agent] CLI install exit=${cliInstall.exitCode}`); if (cliInstall.exitCode !== 0) { const cliErr = await cliInstall.stderr(); return failureResult(`CLI install failed: ${cliErr}`); } console.log("[agent] installing script deps..."); const installResult = await sandbox.runCommand({ cmd: "npm", args: [ "install", "@anthropic-ai/claude-agent-sdk", "bitcoin-auth", "zod", ], env: sandboxEnv, }); console.log(`[agent] npm install exit=${installResult.exitCode}`); if (installResult.exitCode !== 0) { const installErr = await installResult.stderr(); return failureResult(`npm install failed: ${installErr}`); } const cliCheck = await sandbox.runCommand({ cmd: "claude", args: ["--version"], env: sandboxEnv, }); const cliVersion = await cliCheck.stdout(); const cliCheckErr = await cliCheck.stderr(); console.log( `[agent] CLI check: exit=${cliCheck.exitCode} version=${cliVersion.trim()} stderr=${cliCheckErr.slice(0, 200)}`, ); console.log( `[agent] oauth token present: ${!!sandboxEnv.CLAUDE_CODE_OAUTH_TOKEN}, length: ${sandboxEnv.CLAUDE_CODE_OAUTH_TOKEN?.length ?? 0}`, ); console.log("[agent] running agent script..."); const result = await sandbox.runCommand({ cmd: "node", args: ["agent.mjs"], env: sandboxEnv, }); const stdout = await result.stdout(); const stderr = await result.stderr(); console.log( `[agent] script exit=${result.exitCode} stdout=${stdout.length}b stderr=${stderr.length}b`, ); if (result.exitCode !== 0) { console.log(`[agent] stderr: ${stderr.slice(0, 500)}`); console.log(`[agent] stdout: ${stdout.slice(0, 500)}`); try { const parsed = JSON.parse(stdout.trim()) as AgentTurnResult; if (stderr && parsed.error) { parsed.error = `${parsed.error}\n[sandbox stderr]: ${stderr.slice(0, 500)}`; } return parsed; } catch { // Not JSON, return raw } return failureResult( stderr || stdout || `Agent script exited with code ${result.exitCode}`, ); } const output = stdout.trim(); const parsed = JSON.parse(output) as AgentTurnResult; if (!parsed.success) { console.log( `[agent] agent returned failure: ${parsed.error || "no error message"}`, ); if (!parsed.error && stderr) { parsed.error = stderr; } } return parsed; } catch (error) { return failureResult( error instanceof Error ? error.message : "Unknown error", ); } finally { if (sandbox) { await sandbox.stop(); } } }