UNPKG

@posthog/agent

Version:

TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog

467 lines (464 loc) 16.2 kB
import { Logger } from '../../utils/logger.js'; import { toolNames, replaceAndCalculateLocation, SYSTEM_REMINDER } from './mcp-server.js'; function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ debug: false, prefix: "[ClaudeTools]" })) { const name = toolUse.name; const input = toolUse.input; switch (name) { case "Task": return { title: input?.description ? input.description : "Task", kind: "think", content: input?.prompt ? [ { type: "content", content: { type: "text", text: input.prompt }, }, ] : [], }; case "NotebookRead": return { title: input?.notebook_path ? `Read Notebook ${input.notebook_path}` : "Read Notebook", kind: "read", content: [], locations: input?.notebook_path ? [{ path: input.notebook_path }] : [], }; case "NotebookEdit": return { title: input?.notebook_path ? `Edit Notebook ${input.notebook_path}` : "Edit Notebook", kind: "edit", content: input?.new_source ? [ { type: "content", content: { type: "text", text: input.new_source }, }, ] : [], locations: input?.notebook_path ? [{ path: input.notebook_path }] : [], }; case "Bash": case toolNames.bash: return { title: input?.command ? `\`${input.command.replaceAll("`", "\\`")}\`` : "Terminal", kind: "execute", content: input?.description ? [ { type: "content", content: { type: "text", text: input.description }, }, ] : [], }; case "BashOutput": case toolNames.bashOutput: return { title: "Tail Logs", kind: "execute", content: [], }; case "KillShell": case toolNames.killShell: return { title: "Kill Process", kind: "execute", content: [], }; case toolNames.read: { let limit = ""; if (input.limit) { limit = " (" + ((input.offset ?? 0) + 1) + " - " + ((input.offset ?? 0) + input.limit) + ")"; } else if (input.offset) { limit = ` (from line ${input.offset + 1})`; } return { title: `Read ${input.file_path ?? "File"}${limit}`, kind: "read", locations: input.file_path ? [ { path: input.file_path, line: input.offset ?? 0, }, ] : [], content: [], }; } case "Read": return { title: "Read File", kind: "read", content: [], locations: [{ path: input.file_path, line: input.offset ?? 0 }], }; case "LS": return { title: `List the ${input?.path ? `\`${input.path}\`` : "current"} directory's contents`, kind: "search", content: [], locations: [], }; case toolNames.edit: case "Edit": { const path = input?.file_path ?? input?.file_path; let oldText = input.old_string ?? null; let newText = input.new_string ?? ""; let affectedLines = []; if (path && oldText) { try { const oldContent = cachedFileContent[path] || ""; const newContent = replaceAndCalculateLocation(oldContent, [ { oldText, newText, replaceAll: false, }, ]); oldText = oldContent; newText = newContent.newContent; affectedLines = newContent.lineNumbers; } catch (e) { logger.error("Failed to edit file", e); } } return { title: path ? `Edit \`${path}\`` : "Edit", kind: "edit", content: input && path ? [ { type: "diff", path, oldText, newText, }, ] : [], locations: path ? affectedLines.length > 0 ? affectedLines.map((line) => ({ line, path })) : [{ path }] : [], }; } case toolNames.write: { let content = []; if (input?.file_path) { content = [ { type: "diff", path: input.file_path, oldText: null, newText: input.content, }, ]; } else if (input?.content) { content = [ { type: "content", content: { type: "text", text: input.content }, }, ]; } return { title: input?.file_path ? `Write ${input.file_path}` : "Write", kind: "edit", content, locations: input?.file_path ? [{ path: input.file_path }] : [], }; } case "Write": return { title: input?.file_path ? `Write ${input.file_path}` : "Write", kind: "edit", content: input?.file_path ? [ { type: "diff", path: input.file_path, oldText: null, newText: input.content, }, ] : [], locations: input?.file_path ? [{ path: input.file_path }] : [], }; case "Glob": { let label = "Find"; if (input.path) { label += ` \`${input.path}\``; } if (input.pattern) { label += ` \`${input.pattern}\``; } return { title: label, kind: "search", content: [], locations: input.path ? [{ path: input.path }] : [], }; } case "Grep": { let label = "grep"; if (input["-i"]) { label += " -i"; } if (input["-n"]) { label += " -n"; } if (input["-A"] !== undefined) { label += ` -A ${input["-A"]}`; } if (input["-B"] !== undefined) { label += ` -B ${input["-B"]}`; } if (input["-C"] !== undefined) { label += ` -C ${input["-C"]}`; } if (input.output_mode) { switch (input.output_mode) { case "FilesWithMatches": label += " -l"; break; case "Count": label += " -c"; break; } } if (input.head_limit !== undefined) { label += ` | head -${input.head_limit}`; } if (input.glob) { label += ` --include="${input.glob}"`; } if (input.type) { label += ` --type=${input.type}`; } if (input.multiline) { label += " -P"; } label += ` "${input.pattern}"`; if (input.path) { label += ` ${input.path}`; } return { title: label, kind: "search", content: [], }; } case "WebFetch": return { title: input?.url ? `Fetch ${input.url}` : "Fetch", kind: "fetch", content: input?.prompt ? [ { type: "content", content: { type: "text", text: input.prompt }, }, ] : [], }; case "WebSearch": { let label = `"${input.query}"`; if (input.allowed_domains && input.allowed_domains.length > 0) { label += ` (allowed: ${input.allowed_domains.join(", ")})`; } if (input.blocked_domains && input.blocked_domains.length > 0) { label += ` (blocked: ${input.blocked_domains.join(", ")})`; } return { title: label, kind: "fetch", content: [], }; } case "TodoWrite": return { title: Array.isArray(input?.todos) ? `Update TODOs: ${input.todos.map((todo) => todo.content).join(", ")}` : "Update TODOs", kind: "think", content: [], }; case "ExitPlanMode": return { title: "Ready to code?", kind: "switch_mode", content: input?.plan ? [{ type: "content", content: { type: "text", text: input.plan } }] : [], }; case "Other": { let output; try { output = JSON.stringify(input, null, 2); } catch { output = typeof input === "string" ? input : "{}"; } return { title: name || "Unknown Tool", kind: "other", content: [ { type: "content", content: { type: "text", text: `\`\`\`json\n${output}\`\`\``, }, }, ], }; } default: return { title: name || "Unknown Tool", kind: "other", content: [], }; } } function toolUpdateFromToolResult(toolResult, toolUse) { switch (toolUse?.name) { case "Read": case toolNames.read: if (Array.isArray(toolResult.content) && toolResult.content.length > 0) { return { content: toolResult.content.map((content) => ({ type: "content", content: content.type === "text" ? { type: "text", text: markdownEscape(content.text.replace(SYSTEM_REMINDER, "")), } : content, })), }; } else if (typeof toolResult.content === "string" && toolResult.content.length > 0) { return { content: [ { type: "content", content: { type: "text", text: markdownEscape(toolResult.content.replace(SYSTEM_REMINDER, "")), }, }, ], }; } return {}; case toolNames.bash: case "edit": case "Edit": case toolNames.edit: case toolNames.write: case "Write": { if ("is_error" in toolResult && toolResult.is_error && toolResult.content && toolResult.content.length > 0) { // Only return errors return toAcpContentUpdate(toolResult.content, true); } return {}; } case "ExitPlanMode": { return { title: "Exited Plan Mode" }; } default: { return toAcpContentUpdate(toolResult.content, "is_error" in toolResult ? toolResult.is_error : false); } } } function toAcpContentUpdate(content, isError = false) { if (Array.isArray(content) && content.length > 0) { return { content: content.map((content) => ({ type: "content", content: isError && content.type === "text" ? { ...content, text: `\`\`\`\n${content.text}\n\`\`\``, } : content, })), }; } else if (typeof content === "string" && content.length > 0) { return { content: [ { type: "content", content: { type: "text", text: isError ? `\`\`\`\n${content}\n\`\`\`` : content, }, }, ], }; } return {}; } function planEntries(input) { return input.todos.map((input) => ({ content: input.content, status: input.status, priority: "medium", })); } function markdownEscape(text) { let escapedText = "```"; for (const [m] of text.matchAll(/^```+/gm)) { while (m.length >= escapedText.length) { escapedText += "`"; } } return `${escapedText}\n${text}${text.endsWith("\n") ? "" : "\n"}${escapedText}`; } /* A global variable to store callbacks that should be executed when receiving hooks from Claude Code */ const toolUseCallbacks = {}; /* Setup callbacks that will be called when receiving hooks from Claude Code */ const registerHookCallback = (toolUseID, { onPostToolUseHook, }) => { toolUseCallbacks[toolUseID] = { onPostToolUseHook, }; }; /* A callback for Claude Code that is called when receiving a PostToolUse hook */ const createPostToolUseHook = (logger = new Logger({ prefix: "[createPostToolUseHook]" })) => async (input, toolUseID) => { if (input.hook_event_name === "PostToolUse" && toolUseID) { const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook; if (onPostToolUseHook) { await onPostToolUseHook(toolUseID, input.tool_input, input.tool_response); delete toolUseCallbacks[toolUseID]; // Cleanup after execution } else { logger.error(`No onPostToolUseHook found for tool use ID: ${toolUseID}`); delete toolUseCallbacks[toolUseID]; } } return { continue: true }; }; export { createPostToolUseHook, markdownEscape, planEntries, registerHookCallback, toolInfoFromToolUse, toolUpdateFromToolResult }; //# sourceMappingURL=tools.js.map