aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/claude/index.ts
blob: 8bdcccdc34325535ab59931732d6d308cc637394 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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<string, Query>();

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<void> {
  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,
      systemPrompt: 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<void> {
  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";