From 332e5cec2992fefb302251962a3ceca38437a110 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sat, 28 Feb 2026 07:26:43 -0800 Subject: Phase 2: Claude integration layer - Add @anthropic-ai/claude-agent-sdk dependency - Implement src/main/claude/phases.ts with phase configs (research/plan/implement) - Implement src/main/claude/index.ts with SDK wrapper - query() integration with session management - Session resume support - Artifact read/write utilities - Phase advancement logic --- src/main/claude/index.ts | 142 ++++++++++++++++++++++++++++++++++++++++++++++ src/main/claude/phases.ts | 104 +++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 src/main/claude/index.ts create mode 100644 src/main/claude/phases.ts (limited to 'src/main/claude') diff --git a/src/main/claude/index.ts b/src/main/claude/index.ts new file mode 100644 index 0000000..34a914e --- /dev/null +++ b/src/main/claude/index.ts @@ -0,0 +1,142 @@ +import { query, type SDKMessage, type Query } 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 fs from "node:fs"; +import path from "node:path"; + +// Track active queries by session ID +const activeQueries = new Map(); + +function ensureArtifactDir(projectPath: string): void { + const dir = path.join(projectPath, ".claude-flow"); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +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"); + + ensureArtifactDir(project.path); + + const phaseConfig = getPhaseConfig( + session.phase as Phase, + session.permission_mode as UserPermissionMode + ); + + const q = query({ + prompt: message, + options: { + cwd: project.path, + resume: session.claude_session_id ?? undefined, + tools: phaseConfig.tools, + permissionMode: phaseConfig.permissionMode, + // Add system prompt via extraArgs since there's no direct option + extraArgs: { + "system-prompt": phaseConfig.systemPrompt, + }, + }, + }); + + 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); + } +} + +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 docName = getArtifactFilename(session.phase as Phase); + const message = `I've updated .claude-flow/${docName} 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; +} + +/** + * Read an artifact file from the project's .claude-flow directory + */ +export function readArtifact( + projectPath: string, + filename: string +): string | null { + const filePath = path.join(projectPath, ".claude-flow", filename); + if (fs.existsSync(filePath)) { + return fs.readFileSync(filePath, "utf-8"); + } + return null; +} + +/** + * Write an artifact file to the project's .claude-flow directory + */ +export function writeArtifact( + projectPath: string, + filename: string, + content: string +): void { + const dir = path.join(projectPath, ".claude-flow"); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(path.join(dir, filename), content, "utf-8"); +} + +/** + * Get the initial message for a phase + */ +export function getPhaseInitialMessage(phase: Phase): string { + return getPhaseConfig(phase).initialMessage; +} + +// Re-export types +export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk"; +export type { Phase, UserPermissionMode } from "./phases"; diff --git a/src/main/claude/phases.ts b/src/main/claude/phases.ts new file mode 100644 index 0000000..d503f3a --- /dev/null +++ b/src/main/claude/phases.ts @@ -0,0 +1,104 @@ +import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk"; + +export type Phase = "research" | "plan" | "implement"; +export type UserPermissionMode = "acceptEdits" | "bypassPermissions"; + +export interface PhaseConfig { + systemPrompt: string; + tools: string[]; + permissionMode: PermissionMode; + initialMessage: string; +} + +export const phaseConfigs: Record = { + research: { + permissionMode: "plan", + tools: ["Read", "Glob", "Grep", "Bash", "Write"], + initialMessage: + "What areas of the codebase should I research? What are you trying to build?", + systemPrompt: `You are in RESEARCH mode. + +Your job is to deeply understand the codebase before any changes are made. + +When the user tells you what to research: +1. Read files thoroughly — understand all intricacies +2. Write your findings to .claude-flow/research.md +3. Format it as clear, readable markdown + +Rules: +- DO NOT make any code changes +- DO NOT modify any files except .claude-flow/research.md +- Be thorough — surface-level reading is not acceptable + +When the user clicks "Review", read .claude-flow/research.md for their annotations and update accordingly. +When the user clicks "Submit", they're ready to move to planning.`, + }, + + plan: { + permissionMode: "plan", + tools: ["Read", "Glob", "Grep", "Write"], + initialMessage: + "I'll create a detailed implementation plan based on my research. Give me a moment...", + systemPrompt: `You are in PLANNING mode. + +Based on the research in .claude-flow/research.md, write a detailed implementation plan. + +Write the plan to .claude-flow/plan.md with: +- Detailed explanation of the approach +- Specific code snippets showing proposed changes +- File paths that will be modified +- Trade-offs and considerations +- A granular TODO list with checkboxes + +Rules: +- DO NOT implement anything +- DO NOT modify any source files +- Only write to .claude-flow/plan.md + +The plan should be detailed enough that implementation becomes mechanical. + +When the user clicks "Review", read .claude-flow/plan.md for their annotations and update accordingly. +When the user clicks "Submit", begin implementation.`, + }, + + implement: { + permissionMode: "acceptEdits", + tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], + initialMessage: + "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.", + systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved. + +Read .claude-flow/plan.md and execute it: +- Follow the plan exactly +- Mark tasks complete (- [x]) as you finish them +- Run typecheck/lint continuously if available +- Do not add unnecessary comments +- Do not stop until all tasks are complete + +If you encounter issues not covered by the plan, stop and ask.`, + }, +}; + +export function getPhaseConfig( + phase: Phase, + userPermissionMode?: UserPermissionMode +): PhaseConfig { + const config = { ...phaseConfigs[phase] }; + if (phase === "implement" && userPermissionMode) { + config.permissionMode = userPermissionMode; + } + return config; +} + +export function getNextPhase(phase: Phase): Phase | null { + const transitions: Record = { + research: "plan", + plan: "implement", + implement: null, + }; + return transitions[phase]; +} + +export function getArtifactFilename(phase: Phase): string { + return phase === "research" ? "research.md" : "plan.md"; +} -- cgit v1.2.3