aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/claude/index.ts142
-rw-r--r--src/main/claude/phases.ts104
-rw-r--r--src/main/db/index.ts31
-rw-r--r--src/main/db/projects.ts38
-rw-r--r--src/main/db/schema.ts35
-rw-r--r--src/main/db/sessions.ts106
6 files changed, 456 insertions, 0 deletions
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 @@
1import { query, type SDKMessage, type Query } from "@anthropic-ai/claude-agent-sdk";
2import type { Session } from "../db/sessions";
3import { getPhaseConfig, getNextPhase, getArtifactFilename } from "./phases";
4import type { Phase, UserPermissionMode } from "./phases";
5import { getProject } from "../db/projects";
6import { updateSession } from "../db/sessions";
7import fs from "node:fs";
8import path from "node:path";
9
10// Track active queries by session ID
11const activeQueries = new Map<string, Query>();
12
13function ensureArtifactDir(projectPath: string): void {
14 const dir = path.join(projectPath, ".claude-flow");
15 if (!fs.existsSync(dir)) {
16 fs.mkdirSync(dir, { recursive: true });
17 }
18}
19
20export interface SendMessageOptions {
21 session: Session;
22 message: string;
23 onMessage: (msg: SDKMessage) => void;
24}
25
26export async function sendMessage({
27 session,
28 message,
29 onMessage,
30}: SendMessageOptions): Promise<void> {
31 const project = getProject(session.project_id);
32 if (!project) throw new Error("Project not found");
33
34 ensureArtifactDir(project.path);
35
36 const phaseConfig = getPhaseConfig(
37 session.phase as Phase,
38 session.permission_mode as UserPermissionMode
39 );
40
41 const q = query({
42 prompt: message,
43 options: {
44 cwd: project.path,
45 resume: session.claude_session_id ?? undefined,
46 tools: phaseConfig.tools,
47 permissionMode: phaseConfig.permissionMode,
48 // Add system prompt via extraArgs since there's no direct option
49 extraArgs: {
50 "system-prompt": phaseConfig.systemPrompt,
51 },
52 },
53 });
54
55 activeQueries.set(session.id, q);
56
57 try {
58 for await (const msg of q) {
59 // Capture session ID from init message
60 if (msg.type === "system" && msg.subtype === "init") {
61 if (!session.claude_session_id) {
62 updateSession(session.id, { claude_session_id: msg.session_id });
63 }
64 }
65 onMessage(msg);
66 }
67 } finally {
68 activeQueries.delete(session.id);
69 }
70}
71
72export function interruptSession(sessionId: string): void {
73 const q = activeQueries.get(sessionId);
74 if (q) {
75 q.close();
76 activeQueries.delete(sessionId);
77 }
78}
79
80/**
81 * Trigger a review: Claude reads the document and addresses user annotations
82 */
83export async function triggerReview(
84 session: Session,
85 onMessage: (msg: SDKMessage) => void
86): Promise<void> {
87 const docName = getArtifactFilename(session.phase as Phase);
88 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.`;
89
90 await sendMessage({ session, message, onMessage });
91}
92
93/**
94 * Advance to the next phase
95 */
96export function advancePhase(session: Session): Phase | null {
97 const nextPhase = getNextPhase(session.phase as Phase);
98 if (nextPhase) {
99 updateSession(session.id, { phase: nextPhase });
100 }
101 return nextPhase;
102}
103
104/**
105 * Read an artifact file from the project's .claude-flow directory
106 */
107export function readArtifact(
108 projectPath: string,
109 filename: string
110): string | null {
111 const filePath = path.join(projectPath, ".claude-flow", filename);
112 if (fs.existsSync(filePath)) {
113 return fs.readFileSync(filePath, "utf-8");
114 }
115 return null;
116}
117
118/**
119 * Write an artifact file to the project's .claude-flow directory
120 */
121export function writeArtifact(
122 projectPath: string,
123 filename: string,
124 content: string
125): void {
126 const dir = path.join(projectPath, ".claude-flow");
127 if (!fs.existsSync(dir)) {
128 fs.mkdirSync(dir, { recursive: true });
129 }
130 fs.writeFileSync(path.join(dir, filename), content, "utf-8");
131}
132
133/**
134 * Get the initial message for a phase
135 */
136export function getPhaseInitialMessage(phase: Phase): string {
137 return getPhaseConfig(phase).initialMessage;
138}
139
140// Re-export types
141export type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
142export 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 @@
1import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
2
3export type Phase = "research" | "plan" | "implement";
4export type UserPermissionMode = "acceptEdits" | "bypassPermissions";
5
6export interface PhaseConfig {
7 systemPrompt: string;
8 tools: string[];
9 permissionMode: PermissionMode;
10 initialMessage: string;
11}
12
13export const phaseConfigs: Record<Phase, PhaseConfig> = {
14 research: {
15 permissionMode: "plan",
16 tools: ["Read", "Glob", "Grep", "Bash", "Write"],
17 initialMessage:
18 "What areas of the codebase should I research? What are you trying to build?",
19 systemPrompt: `You are in RESEARCH mode.
20
21Your job is to deeply understand the codebase before any changes are made.
22
23When the user tells you what to research:
241. Read files thoroughly — understand all intricacies
252. Write your findings to .claude-flow/research.md
263. Format it as clear, readable markdown
27
28Rules:
29- DO NOT make any code changes
30- DO NOT modify any files except .claude-flow/research.md
31- Be thorough — surface-level reading is not acceptable
32
33When the user clicks "Review", read .claude-flow/research.md for their annotations and update accordingly.
34When the user clicks "Submit", they're ready to move to planning.`,
35 },
36
37 plan: {
38 permissionMode: "plan",
39 tools: ["Read", "Glob", "Grep", "Write"],
40 initialMessage:
41 "I'll create a detailed implementation plan based on my research. Give me a moment...",
42 systemPrompt: `You are in PLANNING mode.
43
44Based on the research in .claude-flow/research.md, write a detailed implementation plan.
45
46Write the plan to .claude-flow/plan.md with:
47- Detailed explanation of the approach
48- Specific code snippets showing proposed changes
49- File paths that will be modified
50- Trade-offs and considerations
51- A granular TODO list with checkboxes
52
53Rules:
54- DO NOT implement anything
55- DO NOT modify any source files
56- Only write to .claude-flow/plan.md
57
58The plan should be detailed enough that implementation becomes mechanical.
59
60When the user clicks "Review", read .claude-flow/plan.md for their annotations and update accordingly.
61When the user clicks "Submit", begin implementation.`,
62 },
63
64 implement: {
65 permissionMode: "acceptEdits",
66 tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
67 initialMessage:
68 "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.",
69 systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved.
70
71Read .claude-flow/plan.md and execute it:
72- Follow the plan exactly
73- Mark tasks complete (- [x]) as you finish them
74- Run typecheck/lint continuously if available
75- Do not add unnecessary comments
76- Do not stop until all tasks are complete
77
78If you encounter issues not covered by the plan, stop and ask.`,
79 },
80};
81
82export function getPhaseConfig(
83 phase: Phase,
84 userPermissionMode?: UserPermissionMode
85): PhaseConfig {
86 const config = { ...phaseConfigs[phase] };
87 if (phase === "implement" && userPermissionMode) {
88 config.permissionMode = userPermissionMode;
89 }
90 return config;
91}
92
93export function getNextPhase(phase: Phase): Phase | null {
94 const transitions: Record<Phase, Phase | null> = {
95 research: "plan",
96 plan: "implement",
97 implement: null,
98 };
99 return transitions[phase];
100}
101
102export function getArtifactFilename(phase: Phase): string {
103 return phase === "research" ? "research.md" : "plan.md";
104}
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 @@
1import Database from "better-sqlite3";
2import { app } from "electron";
3import path from "node:path";
4import fs from "node:fs";
5import { initSchema } from "./schema";
6
7let db: Database.Database | null = null;
8
9export function getDb(): Database.Database {
10 if (db) return db;
11
12 const dbDir = app.getPath("userData");
13 if (!fs.existsSync(dbDir)) {
14 fs.mkdirSync(dbDir, { recursive: true });
15 }
16
17 const dbPath = path.join(dbDir, "claude-flow.db");
18 db = new Database(dbPath);
19 db.pragma("journal_mode = WAL");
20 db.pragma("foreign_keys = ON");
21
22 initSchema(db);
23 return db;
24}
25
26export function closeDb() {
27 if (db) {
28 db.close();
29 db = null;
30 }
31}
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 @@
1import { getDb } from "./index";
2import { v4 as uuid } from "uuid";
3
4export interface Project {
5 id: string;
6 name: string;
7 path: string;
8 created_at: number;
9 updated_at: number;
10}
11
12export function listProjects(): Project[] {
13 return getDb()
14 .prepare("SELECT * FROM projects ORDER BY updated_at DESC")
15 .all() as Project[];
16}
17
18export function getProject(id: string): Project | undefined {
19 return getDb()
20 .prepare("SELECT * FROM projects WHERE id = ?")
21 .get(id) as Project | undefined;
22}
23
24export function createProject(name: string, projectPath: string): Project {
25 const db = getDb();
26 const id = uuid();
27 const now = Math.floor(Date.now() / 1000);
28
29 db.prepare(
30 "INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
31 ).run(id, name, projectPath, now, now);
32
33 return { id, name, path: projectPath, created_at: now, updated_at: now };
34}
35
36export function deleteProject(id: string): void {
37 getDb().prepare("DELETE FROM projects WHERE id = ?").run(id);
38}
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 @@
1import Database from "better-sqlite3";
2
3export function initSchema(db: Database.Database) {
4 db.exec(`
5 CREATE TABLE IF NOT EXISTS projects (
6 id TEXT PRIMARY KEY,
7 name TEXT NOT NULL,
8 path TEXT NOT NULL,
9 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
10 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
11 );
12
13 CREATE TABLE IF NOT EXISTS sessions (
14 id TEXT PRIMARY KEY,
15 project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
16 name TEXT NOT NULL,
17 phase TEXT NOT NULL DEFAULT 'research',
18 claude_session_id TEXT,
19 permission_mode TEXT NOT NULL DEFAULT 'acceptEdits',
20 created_at INTEGER NOT NULL DEFAULT (unixepoch()),
21 updated_at INTEGER NOT NULL DEFAULT (unixepoch())
22 );
23
24 CREATE TABLE IF NOT EXISTS messages (
25 id TEXT PRIMARY KEY,
26 session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
27 role TEXT NOT NULL,
28 content TEXT NOT NULL,
29 created_at INTEGER NOT NULL DEFAULT (unixepoch())
30 );
31
32 CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
33 CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
34 `);
35}
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 @@
1import { getDb } from "./index";
2import { v4 as uuid } from "uuid";
3
4export type Phase = "research" | "plan" | "implement";
5export type PermissionMode = "acceptEdits" | "bypassPermissions";
6
7export interface Session {
8 id: string;
9 project_id: string;
10 name: string;
11 phase: Phase;
12 claude_session_id: string | null;
13 permission_mode: PermissionMode;
14 created_at: number;
15 updated_at: number;
16}
17
18export interface Message {
19 id: string;
20 session_id: string;
21 role: "user" | "assistant";
22 content: string;
23 created_at: number;
24}
25
26export function listSessions(projectId: string): Session[] {
27 return getDb()
28 .prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC")
29 .all(projectId) as Session[];
30}
31
32export function getSession(id: string): Session | undefined {
33 return getDb()
34 .prepare("SELECT * FROM sessions WHERE id = ?")
35 .get(id) as Session | undefined;
36}
37
38export function createSession(projectId: string, name: string): Session {
39 const db = getDb();
40 const id = uuid();
41 const now = Math.floor(Date.now() / 1000);
42
43 db.prepare(
44 `INSERT INTO sessions (id, project_id, name, phase, permission_mode, created_at, updated_at)
45 VALUES (?, ?, ?, 'research', 'acceptEdits', ?, ?)`
46 ).run(id, projectId, name, now, now);
47
48 return {
49 id,
50 project_id: projectId,
51 name,
52 phase: "research",
53 claude_session_id: null,
54 permission_mode: "acceptEdits",
55 created_at: now,
56 updated_at: now,
57 };
58}
59
60export function updateSession(
61 id: string,
62 updates: Partial<Pick<Session, "name" | "phase" | "claude_session_id" | "permission_mode">>
63): void {
64 const db = getDb();
65 const sets: string[] = [];
66 const values: any[] = [];
67
68 for (const [key, value] of Object.entries(updates)) {
69 if (value !== undefined) {
70 sets.push(`${key} = ?`);
71 values.push(value);
72 }
73 }
74
75 if (sets.length > 0) {
76 sets.push("updated_at = ?");
77 values.push(Math.floor(Date.now() / 1000));
78 values.push(id);
79 db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values);
80 }
81}
82
83export function deleteSession(id: string): void {
84 getDb().prepare("DELETE FROM sessions WHERE id = ?").run(id);
85}
86
87// Messages
88export function listMessages(sessionId: string): Message[] {
89 return getDb()
90 .prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC")
91 .all(sessionId) as Message[];
92}
93
94export function addMessage(sessionId: string, role: Message["role"], content: string): Message {
95 const db = getDb();
96 const id = uuid();
97 const now = Math.floor(Date.now() / 1000);
98
99 db.prepare(
100 "INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)"
101 ).run(id, sessionId, role, content, now);
102
103 db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId);
104
105 return { id, session_id: sessionId, role, content, created_at: now };
106}