import { query, type SDKMessage, type Query, type PermissionResult } from "@anthropic-ai/claude-agent-sdk"; import type { Session } from "../db/sessions"; import { getPhaseConfig, getNextPhase, getArtifactFilename } from "./phases"; import type { Phase, UserPermissionMode } from "./phases"; import { getProject } from "../db/projects"; import { updateSession } from "../db/sessions"; import { getSetting } from "../db/settings"; import { autoCommitTurn } from "../git"; import fs from "node:fs"; import path from "node:path"; // Track active queries by session ID const activeQueries = new Map(); // Snapshot of plan.md content per session, updated after each implement turn. // Used to detect newly-completed tasks for commit messages. const planSnapshots = new Map(); function ensureDir(dirPath: string): void { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } // Artifacts live inside the project directory so the SDK's Write tool can reach them function getSessionDir(projectPath: string, sessionId: string): string { return path.join(projectPath, ".claude-flow", "sessions", sessionId); } export interface SendMessageOptions { session: Session; message: string; onMessage: (msg: SDKMessage) => void; } export async function sendMessage({ session, message, onMessage, }: SendMessageOptions): Promise { const project = getProject(session.project_id); if (!project) throw new Error("Project not found"); // Ensure session artifact directory exists inside the project const sessionDir = getSessionDir(project.path, session.id); ensureDir(sessionDir); // Load any custom system prompt for this phase (null → use default) const customSystemPrompt = getSetting(`systemPrompt.${session.phase}`) ?? undefined; // Load global model override (empty string or null → let SDK use its default) const configuredModel = getSetting("model") || undefined; // Load MCP servers config (JSON string → object, or undefined if not set) const mcpServersJson = getSetting("mcpServers"); const mcpServers = mcpServersJson ? JSON.parse(mcpServersJson) : undefined; const phaseConfig = getPhaseConfig( session.phase as Phase, sessionDir, session.permission_mode as UserPermissionMode, customSystemPrompt ); const q = query({ prompt: message, options: { cwd: project.path, model: configuredModel, mcpServers, resume: session.claude_session_id ?? undefined, tools: phaseConfig.tools, permissionMode: phaseConfig.permissionMode, // Required companion flag when bypassPermissions is active allowDangerouslySkipPermissions: phaseConfig.permissionMode === "bypassPermissions", systemPrompt: phaseConfig.systemPrompt, // Allow Claude to inspect git state during implementation without prompts. // git add/commit intentionally omitted — the app handles those. ...(session.phase === "implement" && { allowedTools: ["Bash(git status*)", "Bash(git log*)", "Bash(git diff*)"], }), // Auto-allow MCP tools in research phase (where external data is most useful) ...(session.phase === "research" && mcpServers && { canUseTool: async (toolName: string): Promise => { // Auto-approve all MCP tools (they start with "mcp__") if (toolName.startsWith("mcp__")) { return { behavior: "allow" }; } // For non-MCP tools, let the default permission flow handle it // Return deny with interrupt:false to fall through to default behavior // Actually, we need to return allow for tools that acceptEdits would allow // For now, just allow all tools in research phase since it's read-heavy return { behavior: "allow" }; }, }), }, }); activeQueries.set(session.id, q); try { for await (const msg of q) { // Capture session ID from init message if (msg.type === "system" && msg.subtype === "init") { if (!session.claude_session_id) { updateSession(session.id, { claude_session_id: msg.session_id }); } } onMessage(msg); } } finally { activeQueries.delete(session.id); } // Auto-commit after a successful implement-phase turn. // This runs only if for-await completed without throwing (successful turn). // Interrupted / errored turns skip the commit. if (session.phase === "implement") { const previousPlan = planSnapshots.get(session.id) ?? ""; const currentPlan = autoCommitTurn( project.path, session.git_branch, previousPlan, sessionDir ); planSnapshots.set(session.id, currentPlan); } } export function interruptSession(sessionId: string): void { const q = activeQueries.get(sessionId); if (q) { q.close(); activeQueries.delete(sessionId); } } /** * Trigger a review: Claude reads the document and addresses user annotations */ export async function triggerReview( session: Session, onMessage: (msg: SDKMessage) => void ): Promise { const artifactPath = getArtifactPath(session); const message = `I've updated ${artifactPath} with annotations. Read the file, find all my inline notes (marked with // REVIEW:, // NOTE:, TODO:, or similar), address each one, and update the document accordingly. Do not implement anything yet.`; await sendMessage({ session, message, onMessage }); } /** * Advance to the next phase */ export function advancePhase(session: Session): Phase | null { const nextPhase = getNextPhase(session.phase as Phase); if (nextPhase) { updateSession(session.id, { phase: nextPhase }); } return nextPhase; } /** * Get the artifact path for a session and phase (inside the project directory) */ export function getArtifactPath(session: Session): string { const project = getProject(session.project_id); if (!project) throw new Error("Project not found"); const filename = getArtifactFilename(session.phase as Phase); return path.join(getSessionDir(project.path, session.id), filename); } /** * Read an artifact file for a session */ export function readSessionArtifact( projectId: string, sessionId: string, filename: string ): string | null { const project = getProject(projectId); if (!project) return null; const filePath = path.join(getSessionDir(project.path, sessionId), filename); if (fs.existsSync(filePath)) { return fs.readFileSync(filePath, "utf-8"); } return null; } /** * Write an artifact file for a session */ export function writeSessionArtifact( projectId: string, sessionId: string, filename: string, content: string ): void { const project = getProject(projectId); if (!project) throw new Error("Project not found"); const dir = getSessionDir(project.path, sessionId); ensureDir(dir); fs.writeFileSync(path.join(dir, filename), content, "utf-8"); } /** * Read CLAUDE.md from project root */ export function readClaudeMd(projectPath: string): string | null { const filePath = path.join(projectPath, "CLAUDE.md"); if (fs.existsSync(filePath)) { return fs.readFileSync(filePath, "utf-8"); } return null; } /** * Write CLAUDE.md to project root */ export function writeClaudeMd(projectPath: string, content: string): void { const filePath = path.join(projectPath, "CLAUDE.md"); fs.writeFileSync(filePath, content, "utf-8"); } /** * Clear session artifacts */ export function clearSessionArtifacts(projectId: string, sessionId: string): void { const project = getProject(projectId); if (!project) return; const dir = getSessionDir(project.path, sessionId); if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } // Re-export types export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; export type { Phase, UserPermissionMode } from "./phases";