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 --- package-lock.json | 362 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/main/claude/index.ts | 142 ++++++++++++++++++ src/main/claude/phases.ts | 104 +++++++++++++ src/main/db/index.ts | 31 ++++ src/main/db/projects.ts | 38 +++++ src/main/db/schema.ts | 35 +++++ src/main/db/sessions.ts | 106 ++++++++++++++ 8 files changed, 821 insertions(+), 2 deletions(-) create mode 100644 src/main/claude/index.ts create mode 100644 src/main/claude/phases.ts create mode 100644 src/main/db/index.ts create mode 100644 src/main/db/projects.ts create mode 100644 src/main/db/schema.ts create mode 100644 src/main/db/sessions.ts diff --git a/package-lock.json b/package-lock.json index 74bac1f..64d46a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.63", "better-sqlite3": "12.2.0", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "uuid": "^13.0.0" }, "devDependencies": { "@electron/rebuild": "4.0.1", @@ -19,6 +21,7 @@ "@types/node": "24.3.2", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", + "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^5.0.2", "concurrently": "^9.2.1", "electron": "38.1.0", @@ -28,6 +31,29 @@ "wait-on": "^8.0.5" } }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.2.63", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.63.tgz", + "integrity": "sha512-ZNiaQb/v6xkbrGt3dtq5J0DGY+AaOhoehUyposa3msvlAlkTHWNGR+NhbCcTE0ML1U91xhPqMAAwZIUqrlkKyQ==", + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.34.2", + "@img/sharp-darwin-x64": "^0.34.2", + "@img/sharp-linux-arm": "^0.34.2", + "@img/sharp-linux-arm64": "^0.34.2", + "@img/sharp-linux-x64": "^0.34.2", + "@img/sharp-linuxmusl-arm64": "^0.34.2", + "@img/sharp-linuxmusl-x64": "^0.34.2", + "@img/sharp-win32-arm64": "^0.34.2", + "@img/sharp-win32-x64": "^0.34.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1749,6 +1775,310 @@ "@hapi/hoek": "^11.0.2" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2624,6 +2954,13 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -7447,6 +7784,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -7706,6 +8056,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 2f7e388..a76759a 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,11 @@ } }, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.63", "better-sqlite3": "12.2.0", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "uuid": "^13.0.0" }, "devDependencies": { "@electron/rebuild": "4.0.1", @@ -53,6 +55,7 @@ "@types/node": "24.3.2", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", + "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^5.0.2", "concurrently": "^9.2.1", "electron": "38.1.0", 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"; +} diff --git a/src/main/db/index.ts b/src/main/db/index.ts new file mode 100644 index 0000000..a77cdd4 --- /dev/null +++ b/src/main/db/index.ts @@ -0,0 +1,31 @@ +import Database from "better-sqlite3"; +import { app } from "electron"; +import path from "node:path"; +import fs from "node:fs"; +import { initSchema } from "./schema"; + +let db: Database.Database | null = null; + +export function getDb(): Database.Database { + if (db) return db; + + const dbDir = app.getPath("userData"); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + + const dbPath = path.join(dbDir, "claude-flow.db"); + db = new Database(dbPath); + db.pragma("journal_mode = WAL"); + db.pragma("foreign_keys = ON"); + + initSchema(db); + return db; +} + +export function closeDb() { + if (db) { + db.close(); + db = null; + } +} diff --git a/src/main/db/projects.ts b/src/main/db/projects.ts new file mode 100644 index 0000000..88ef2f6 --- /dev/null +++ b/src/main/db/projects.ts @@ -0,0 +1,38 @@ +import { getDb } from "./index"; +import { v4 as uuid } from "uuid"; + +export interface Project { + id: string; + name: string; + path: string; + created_at: number; + updated_at: number; +} + +export function listProjects(): Project[] { + return getDb() + .prepare("SELECT * FROM projects ORDER BY updated_at DESC") + .all() as Project[]; +} + +export function getProject(id: string): Project | undefined { + return getDb() + .prepare("SELECT * FROM projects WHERE id = ?") + .get(id) as Project | undefined; +} + +export function createProject(name: string, projectPath: string): Project { + const db = getDb(); + const id = uuid(); + const now = Math.floor(Date.now() / 1000); + + db.prepare( + "INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)" + ).run(id, name, projectPath, now, now); + + return { id, name, path: projectPath, created_at: now, updated_at: now }; +} + +export function deleteProject(id: string): void { + getDb().prepare("DELETE FROM projects WHERE id = ?").run(id); +} diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts new file mode 100644 index 0000000..c2093f9 --- /dev/null +++ b/src/main/db/schema.ts @@ -0,0 +1,35 @@ +import Database from "better-sqlite3"; + +export function initSchema(db: Database.Database) { + db.exec(` + CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()) + ); + + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + name TEXT NOT NULL, + phase TEXT NOT NULL DEFAULT 'research', + claude_session_id TEXT, + permission_mode TEXT NOT NULL DEFAULT 'acceptEdits', + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()) + ); + + CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + role TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()) + ); + + CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); + CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); + `); +} diff --git a/src/main/db/sessions.ts b/src/main/db/sessions.ts new file mode 100644 index 0000000..684bb9e --- /dev/null +++ b/src/main/db/sessions.ts @@ -0,0 +1,106 @@ +import { getDb } from "./index"; +import { v4 as uuid } from "uuid"; + +export type Phase = "research" | "plan" | "implement"; +export type PermissionMode = "acceptEdits" | "bypassPermissions"; + +export interface Session { + id: string; + project_id: string; + name: string; + phase: Phase; + claude_session_id: string | null; + permission_mode: PermissionMode; + created_at: number; + updated_at: number; +} + +export interface Message { + id: string; + session_id: string; + role: "user" | "assistant"; + content: string; + created_at: number; +} + +export function listSessions(projectId: string): Session[] { + return getDb() + .prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC") + .all(projectId) as Session[]; +} + +export function getSession(id: string): Session | undefined { + return getDb() + .prepare("SELECT * FROM sessions WHERE id = ?") + .get(id) as Session | undefined; +} + +export function createSession(projectId: string, name: string): Session { + const db = getDb(); + const id = uuid(); + const now = Math.floor(Date.now() / 1000); + + db.prepare( + `INSERT INTO sessions (id, project_id, name, phase, permission_mode, created_at, updated_at) + VALUES (?, ?, ?, 'research', 'acceptEdits', ?, ?)` + ).run(id, projectId, name, now, now); + + return { + id, + project_id: projectId, + name, + phase: "research", + claude_session_id: null, + permission_mode: "acceptEdits", + created_at: now, + updated_at: now, + }; +} + +export function updateSession( + id: string, + updates: Partial> +): void { + const db = getDb(); + const sets: string[] = []; + const values: any[] = []; + + for (const [key, value] of Object.entries(updates)) { + if (value !== undefined) { + sets.push(`${key} = ?`); + values.push(value); + } + } + + if (sets.length > 0) { + sets.push("updated_at = ?"); + values.push(Math.floor(Date.now() / 1000)); + values.push(id); + db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values); + } +} + +export function deleteSession(id: string): void { + getDb().prepare("DELETE FROM sessions WHERE id = ?").run(id); +} + +// Messages +export function listMessages(sessionId: string): Message[] { + return getDb() + .prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC") + .all(sessionId) as Message[]; +} + +export function addMessage(sessionId: string, role: Message["role"], content: string): Message { + const db = getDb(); + const id = uuid(); + const now = Math.floor(Date.now() / 1000); + + db.prepare( + "INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)" + ).run(id, sessionId, role, content, now); + + db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId); + + return { id, session_id: sessionId, role, content, created_at: now }; +} -- cgit v1.2.3