idea-manager 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/next.config.ts +0 -1
- package/package.json +2 -2
- package/{src/app/icon.svg → public/favicon.svg} +2 -2
- package/src/app/api/filesystem/route.ts +49 -0
- package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
- package/src/app/api/projects/[id]/items/route.ts +51 -1
- package/src/app/api/projects/[id]/scan/route.ts +73 -0
- package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
- package/src/app/api/projects/[id]/structure/route.ts +34 -3
- package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/globals.css +465 -5
- package/src/app/layout.tsx +3 -0
- package/src/app/page.tsx +260 -88
- package/src/app/projects/[id]/page.tsx +366 -183
- package/src/cli.ts +10 -10
- package/src/components/DirectoryPicker.tsx +137 -0
- package/src/components/ScanPanel.tsx +743 -0
- package/src/components/brainstorm/Editor.tsx +20 -4
- package/src/components/brainstorm/MemoPin.tsx +91 -5
- package/src/components/dashboard/SubProjectCard.tsx +76 -0
- package/src/components/dashboard/TabBar.tsx +42 -0
- package/src/components/task/ProjectTree.tsx +223 -0
- package/src/components/task/PromptEditor.tsx +107 -0
- package/src/components/task/StatusFlow.tsx +43 -0
- package/src/components/task/TaskChat.tsx +134 -0
- package/src/components/task/TaskDetail.tsx +205 -0
- package/src/components/task/TaskList.tsx +119 -0
- package/src/components/tree/CardView.tsx +206 -0
- package/src/components/tree/RefinePopover.tsx +157 -0
- package/src/components/tree/TreeNode.tsx +147 -38
- package/src/components/tree/TreeView.tsx +270 -26
- package/src/components/ui/ConfirmDialog.tsx +88 -0
- package/src/lib/ai/chat-responder.ts +4 -2
- package/src/lib/ai/cleanup.ts +87 -0
- package/src/lib/ai/client.ts +175 -58
- package/src/lib/ai/prompter.ts +19 -24
- package/src/lib/ai/refiner.ts +128 -0
- package/src/lib/ai/structurer.ts +340 -11
- package/src/lib/db/queries/context.ts +76 -0
- package/src/lib/db/queries/items.ts +133 -12
- package/src/lib/db/queries/projects.ts +12 -8
- package/src/lib/db/queries/sub-projects.ts +122 -0
- package/src/lib/db/queries/task-conversations.ts +27 -0
- package/src/lib/db/queries/task-prompts.ts +32 -0
- package/src/lib/db/queries/tasks.ts +133 -0
- package/src/lib/db/schema.ts +75 -0
- package/src/lib/mcp/server.ts +38 -39
- package/src/lib/mcp/tools.ts +47 -45
- package/src/lib/scanner.ts +573 -0
- package/src/lib/task-store.ts +97 -0
- package/src/types/index.ts +65 -0
package/src/lib/ai/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import type { IStructureWithQuestions } from '@/types';
|
|
3
3
|
|
|
4
4
|
export interface IStructuredItem {
|
|
@@ -6,10 +6,147 @@ export interface IStructuredItem {
|
|
|
6
6
|
description: string;
|
|
7
7
|
item_type: 'feature' | 'task' | 'bug' | 'idea' | 'note';
|
|
8
8
|
priority: 'high' | 'medium' | 'low';
|
|
9
|
+
status?: 'pending' | 'in_progress' | 'done';
|
|
9
10
|
children?: IStructuredItem[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
const CLI_PATH = 'claude';
|
|
14
|
+
const DEFAULT_ARGS = ['--dangerously-skip-permissions'];
|
|
15
|
+
const MODEL = 'sonnet';
|
|
16
|
+
const MAX_TURNS = 1;
|
|
17
|
+
|
|
18
|
+
export type OnTextChunk = (text: string) => void;
|
|
19
|
+
export type OnRawEvent = (event: Record<string, unknown>) => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Spawn Claude Code CLI and collect the result text.
|
|
23
|
+
* Optional onText callback receives streaming text chunks as they arrive.
|
|
24
|
+
*/
|
|
25
|
+
export function runClaude(prompt: string, onText?: OnTextChunk, onRawEvent?: OnRawEvent): Promise<string> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const args = [
|
|
28
|
+
...DEFAULT_ARGS,
|
|
29
|
+
'--model', MODEL,
|
|
30
|
+
'--output-format', 'stream-json',
|
|
31
|
+
'--max-turns', String(MAX_TURNS),
|
|
32
|
+
'-p', '-', // read prompt from stdin
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Strip Claude Code session env vars to avoid nested session detection
|
|
36
|
+
const cleanEnv = { ...process.env };
|
|
37
|
+
delete cleanEnv.CLAUDECODE;
|
|
38
|
+
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
39
|
+
delete cleanEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS;
|
|
40
|
+
for (const key of Object.keys(cleanEnv)) {
|
|
41
|
+
if (key.startsWith('CLAUDE_CODE_') || key === 'ANTHROPIC_PARENT_SESSION') {
|
|
42
|
+
delete cleanEnv[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const proc = spawn(CLI_PATH, args, {
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
env: { ...cleanEnv, FORCE_COLOR: '0' },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Write prompt to stdin and close it
|
|
53
|
+
proc.stdin?.write(prompt);
|
|
54
|
+
proc.stdin?.end();
|
|
55
|
+
|
|
56
|
+
let buffer = '';
|
|
57
|
+
let resultText = '';
|
|
58
|
+
let stderrText = '';
|
|
59
|
+
|
|
60
|
+
proc.stdout?.on('data', (chunk: Buffer) => {
|
|
61
|
+
buffer += chunk.toString();
|
|
62
|
+
const lines = buffer.split('\n');
|
|
63
|
+
buffer = lines.pop() ?? '';
|
|
64
|
+
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed) continue;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(trimmed);
|
|
70
|
+
|
|
71
|
+
// Emit raw event for live streaming to frontend
|
|
72
|
+
onRawEvent?.(parsed);
|
|
73
|
+
|
|
74
|
+
// content_block_delta — real-time streaming tokens (API-style)
|
|
75
|
+
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
|
76
|
+
resultText += parsed.delta.text;
|
|
77
|
+
onText?.(parsed.delta.text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// assistant — complete or incremental message
|
|
81
|
+
else if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
82
|
+
let fullText = '';
|
|
83
|
+
for (const block of parsed.message.content) {
|
|
84
|
+
if (block.type === 'text') {
|
|
85
|
+
fullText += block.text;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Only emit the NEW part (handles incremental assistant messages)
|
|
89
|
+
if (fullText.length > resultText.length) {
|
|
90
|
+
const newPart = fullText.slice(resultText.length);
|
|
91
|
+
onText?.(newPart);
|
|
92
|
+
}
|
|
93
|
+
resultText = fullText;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// result — final output
|
|
97
|
+
else if (parsed.type === 'result' && parsed.result) {
|
|
98
|
+
// Emit any remaining new text from result
|
|
99
|
+
if (parsed.result.length > resultText.length) {
|
|
100
|
+
const newPart = parsed.result.slice(resultText.length);
|
|
101
|
+
onText?.(newPart);
|
|
102
|
+
}
|
|
103
|
+
resultText = parsed.result;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// ignore non-JSON lines
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
proc.stderr?.on('data', (chunk: Buffer) => {
|
|
112
|
+
stderrText += chunk.toString();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
proc.on('error', (err) => {
|
|
116
|
+
reject(new Error(`Claude CLI error: ${err.message}`));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
proc.on('exit', (code, signal) => {
|
|
120
|
+
if (code !== 0 && !resultText) {
|
|
121
|
+
const detail = stderrText.slice(0, 500) || (signal ? `killed by signal ${signal}` : 'no output');
|
|
122
|
+
reject(new Error(`Claude CLI exited with code ${code}: ${detail}`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
resolve(resultText);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run Claude for free-form markdown analysis (not JSON).
|
|
132
|
+
* Used for building the hub document in multi-agent analysis.
|
|
133
|
+
*/
|
|
134
|
+
export function runAnalysis(prompt: string, onText?: OnTextChunk, onRawEvent?: OnRawEvent): Promise<string> {
|
|
135
|
+
return runClaude(prompt, onText, onRawEvent);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function extractJson(text: string, type: 'array' | 'object'): string {
|
|
139
|
+
// Strip markdown fences
|
|
140
|
+
text = text.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '').trim();
|
|
141
|
+
const pattern = type === 'array' ? /\[[\s\S]*\]/ : /\{[\s\S]*\}/;
|
|
142
|
+
const match = text.match(pattern);
|
|
143
|
+
if (!match) {
|
|
144
|
+
throw new Error(`AI did not return valid JSON ${type}`);
|
|
145
|
+
}
|
|
146
|
+
return match[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runStructure(brainstormContent: string, projectContext?: string, onText?: OnTextChunk, onRawEvent?: OnRawEvent): Promise<IStructuredItem[]> {
|
|
13
150
|
const systemPrompt = `You are a JSON-only structuring machine. You NEVER respond with text, explanations, or conversation.
|
|
14
151
|
You ALWAYS output ONLY a raw JSON array, nothing else.
|
|
15
152
|
|
|
@@ -17,46 +154,35 @@ Your job: convert ANY input text into a structured JSON array of items.
|
|
|
17
154
|
Even if the input seems like a greeting or conversation, extract the implicit intent and structure it.
|
|
18
155
|
|
|
19
156
|
Schema per item:
|
|
20
|
-
{ "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "children": [same schema] }
|
|
157
|
+
{ "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "status": "pending"|"in_progress"|"done", "children": [same schema] }
|
|
21
158
|
|
|
22
159
|
Rules:
|
|
23
160
|
- Output MUST start with [ and end with ]
|
|
24
161
|
- No markdown fences, no explanation, no text before or after the JSON
|
|
25
162
|
- Keep titles concise (under 50 chars)
|
|
26
163
|
- Group related ideas under parent items
|
|
27
|
-
- If input is vague, interpret it as best you can and create at least 1 item
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (message.type === 'result') {
|
|
41
|
-
resultText = (message as { type: string; result: string }).result || '';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Strip markdown fences if present
|
|
46
|
-
resultText = resultText.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '').trim();
|
|
47
|
-
|
|
48
|
-
// Extract JSON from the response
|
|
49
|
-
const jsonMatch = resultText.match(/\[[\s\S]*\]/);
|
|
50
|
-
if (!jsonMatch) {
|
|
51
|
-
throw new Error('AI did not return valid JSON');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return JSON.parse(jsonMatch[0]) as IStructuredItem[];
|
|
164
|
+
- If input is vague, interpret it as best you can and create at least 1 item
|
|
165
|
+
- IMPORTANT: When project source code context is provided, judge the status based on the actual code:
|
|
166
|
+
- "done": feature/task is fully implemented in the source code
|
|
167
|
+
- "in_progress": partially implemented or has TODOs
|
|
168
|
+
- "pending": not yet started or only planned
|
|
169
|
+
- If no source code context is provided, default status to "pending"`;
|
|
170
|
+
|
|
171
|
+
const ctxBlock = projectContext ? `\n\n프로젝트 문서 컨텍스트:\n${projectContext}` : '';
|
|
172
|
+
const prompt = `${systemPrompt}\n\nAnalyze this brainstorming content and structure it into a JSON tree:\n\n${brainstormContent}${ctxBlock}`;
|
|
173
|
+
|
|
174
|
+
const resultText = await runClaude(prompt, onText, onRawEvent);
|
|
175
|
+
const json = extractJson(resultText, 'array');
|
|
176
|
+
return JSON.parse(json) as IStructuredItem[];
|
|
55
177
|
}
|
|
56
178
|
|
|
57
179
|
export async function runStructureWithQuestions(
|
|
58
180
|
brainstormContent: string,
|
|
59
181
|
conversationHistory: { role: 'assistant' | 'user'; content: string }[],
|
|
182
|
+
projectContext?: string,
|
|
183
|
+
onText?: OnTextChunk,
|
|
184
|
+
onRawEvent?: OnRawEvent,
|
|
185
|
+
existingStructure?: string,
|
|
60
186
|
): Promise<IStructureWithQuestions> {
|
|
61
187
|
const systemPrompt = `You are an AI assistant that structures brainstorming content AND identifies ambiguous areas.
|
|
62
188
|
You ALWAYS output ONLY a raw JSON object (not an array), nothing else.
|
|
@@ -65,10 +191,11 @@ Your job:
|
|
|
65
191
|
1. Convert the brainstorming text into a structured JSON tree of items
|
|
66
192
|
2. Identify 0-5 areas where the brainstorming is ambiguous or could benefit from clarification
|
|
67
193
|
3. Consider the conversation history to avoid repeating questions already answered
|
|
194
|
+
4. If existing structured items are provided, UPDATE them rather than creating duplicates
|
|
68
195
|
|
|
69
196
|
Output schema:
|
|
70
197
|
{
|
|
71
|
-
"items": [{ "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "children": [same] }],
|
|
198
|
+
"items": [{ "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "status": "pending"|"in_progress"|"done", "children": [same] }],
|
|
72
199
|
"questions": [{ "anchor_text": string, "question": string }]
|
|
73
200
|
}
|
|
74
201
|
|
|
@@ -77,11 +204,20 @@ Rules:
|
|
|
77
204
|
- No markdown fences, no explanation, no text before or after the JSON
|
|
78
205
|
- Keep titles concise (under 50 chars)
|
|
79
206
|
- Group related ideas under parent items
|
|
207
|
+
- NEVER create duplicate items. If the existing structure already has an item for a concept, update it instead of adding a new one
|
|
208
|
+
- Merge similar items together. The output should be a clean, deduplicated structure
|
|
209
|
+
- Preserve the status of existing items unless the brainstorming text explicitly changes them
|
|
80
210
|
- questions[].anchor_text MUST be an exact substring from the brainstorming content (5-20 chars)
|
|
81
211
|
- questions[].question should be a helpful Korean question asking for clarification
|
|
82
212
|
- Generate 0-5 questions. Skip questions already answered in conversation history.
|
|
83
213
|
- If the brainstorming is clear enough, return an empty questions array
|
|
84
|
-
- All questions MUST be in Korean
|
|
214
|
+
- All questions MUST be in Korean
|
|
215
|
+
- If project documentation context is provided, use it to make more informed structuring decisions (e.g., matching tech stack, conventions, existing patterns)
|
|
216
|
+
- IMPORTANT: When project source code context is provided, judge the status based on the actual code:
|
|
217
|
+
- "done": feature/task is fully implemented in the source code
|
|
218
|
+
- "in_progress": partially implemented or has TODOs
|
|
219
|
+
- "pending": not yet started or only planned
|
|
220
|
+
- If no source code context is provided, default status to "pending"`;
|
|
85
221
|
|
|
86
222
|
let historyContext = '';
|
|
87
223
|
if (conversationHistory.length > 0) {
|
|
@@ -90,32 +226,13 @@ Rules:
|
|
|
90
226
|
.join('\n');
|
|
91
227
|
}
|
|
92
228
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for await (const message of query({
|
|
98
|
-
prompt: `${systemPrompt}\n\n${prompt}`,
|
|
99
|
-
options: {
|
|
100
|
-
allowedTools: [],
|
|
101
|
-
maxTurns: 1,
|
|
102
|
-
},
|
|
103
|
-
})) {
|
|
104
|
-
if (message.type === 'result') {
|
|
105
|
-
resultText = (message as { type: string; result: string }).result || '';
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Strip markdown fences if present
|
|
110
|
-
resultText = resultText.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '').trim();
|
|
111
|
-
|
|
112
|
-
// Extract JSON object from the response
|
|
113
|
-
const jsonMatch = resultText.match(/\{[\s\S]*\}/);
|
|
114
|
-
if (!jsonMatch) {
|
|
115
|
-
throw new Error('AI did not return valid JSON');
|
|
116
|
-
}
|
|
229
|
+
const ctxBlock = projectContext ? `\n\n프로젝트 문서 컨텍스트:\n${projectContext}` : '';
|
|
230
|
+
const existingBlock = existingStructure ? `\n\n현재 구조화된 항목 (중복 생성하지 말고 업데이트하세요):\n${existingStructure}` : '';
|
|
231
|
+
const prompt = `${systemPrompt}\n\n다음 브레인스토밍 내용을 분석하고 구조화하세요:\n\n${brainstormContent}${historyContext}${existingBlock}${ctxBlock}`;
|
|
117
232
|
|
|
118
|
-
const
|
|
233
|
+
const resultText = await runClaude(prompt, onText, onRawEvent);
|
|
234
|
+
const json = extractJson(resultText, 'object');
|
|
235
|
+
const parsed = JSON.parse(json);
|
|
119
236
|
|
|
120
237
|
return {
|
|
121
238
|
items: parsed.items || [],
|
package/src/lib/ai/prompter.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runClaude } from './client';
|
|
2
2
|
import { getPrompt, createPrompt } from '../db/queries/prompts';
|
|
3
3
|
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
4
4
|
import { getRecentConversations } from '../db/queries/conversations';
|
|
5
|
+
import { getProjectContextSummary } from '../db/queries/context';
|
|
5
6
|
import type { IPrompt, IItem } from '@/types';
|
|
6
7
|
|
|
7
8
|
export async function generatePrompt(
|
|
8
9
|
item: IItem,
|
|
9
|
-
projectContext?: { brainstormContent?: string; conversationHistory?: string },
|
|
10
|
+
projectContext?: { brainstormContent?: string; conversationHistory?: string; projectDocs?: string },
|
|
10
11
|
): Promise<IPrompt> {
|
|
11
|
-
// Check for existing manual prompt — don't overwrite
|
|
12
12
|
const existing = getPrompt(item.id);
|
|
13
13
|
if (existing?.prompt_type === 'manual') {
|
|
14
14
|
return existing;
|
|
@@ -21,46 +21,39 @@ Rules:
|
|
|
21
21
|
- Write in Korean
|
|
22
22
|
- Be specific and actionable
|
|
23
23
|
- Include conditions, constraints, and requirements
|
|
24
|
-
- Include relevant context from the brainstorming and
|
|
24
|
+
- Include relevant context from the brainstorming, conversation, and project documentation
|
|
25
25
|
- The prompt should be ready to paste into a coding tool
|
|
26
26
|
- Keep it concise but complete (under 500 chars)
|
|
27
27
|
- Do NOT include markdown fences or extra formatting`;
|
|
28
28
|
|
|
29
|
+
const CONTEXT_LIMIT = 30_000; // 30KB max for prompt generation context
|
|
29
30
|
let context = '';
|
|
30
31
|
if (projectContext?.brainstormContent) {
|
|
31
|
-
context += `\n\n브레인스토밍 원문:\n${projectContext.brainstormContent}`;
|
|
32
|
+
context += `\n\n브레인스토밍 원문:\n${projectContext.brainstormContent.slice(0, 3000)}`;
|
|
32
33
|
}
|
|
33
34
|
if (projectContext?.conversationHistory) {
|
|
34
|
-
context += `\n\nAI 대화 이력:\n${projectContext.conversationHistory}`;
|
|
35
|
+
context += `\n\nAI 대화 이력:\n${projectContext.conversationHistory.slice(0, 5000)}`;
|
|
36
|
+
}
|
|
37
|
+
if (projectContext?.projectDocs) {
|
|
38
|
+
const remaining = CONTEXT_LIMIT - context.length;
|
|
39
|
+
if (remaining > 1000) {
|
|
40
|
+
context += `\n\n프로젝트 문서:\n${projectContext.projectDocs.slice(0, remaining)}`;
|
|
41
|
+
}
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
const prompt =
|
|
44
|
+
const prompt = `${systemPrompt}\n\n다음 항목에 대한 실행 프롬프트를 생성하세요:
|
|
38
45
|
|
|
39
46
|
제목: ${item.title}
|
|
40
47
|
설명: ${item.description}
|
|
41
48
|
유형: ${item.item_type}
|
|
42
49
|
우선순위: ${item.priority}${context}`;
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
for await (const message of query({
|
|
47
|
-
prompt: `${systemPrompt}\n\n${prompt}`,
|
|
48
|
-
options: {
|
|
49
|
-
allowedTools: [],
|
|
50
|
-
maxTurns: 1,
|
|
51
|
-
},
|
|
52
|
-
})) {
|
|
53
|
-
if (message.type === 'result') {
|
|
54
|
-
resultText = (message as { type: string; result: string }).result || '';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
resultText = resultText.trim();
|
|
51
|
+
const resultText = await runClaude(prompt);
|
|
59
52
|
|
|
60
53
|
return createPrompt({
|
|
61
54
|
project_id: item.project_id,
|
|
62
55
|
item_id: item.id,
|
|
63
|
-
content: resultText,
|
|
56
|
+
content: resultText.trim(),
|
|
64
57
|
prompt_type: 'auto',
|
|
65
58
|
});
|
|
66
59
|
}
|
|
@@ -68,7 +61,6 @@ Rules:
|
|
|
68
61
|
export async function generatePromptForItem(
|
|
69
62
|
item: IItem,
|
|
70
63
|
): Promise<IPrompt> {
|
|
71
|
-
// Load project context
|
|
72
64
|
const brainstorm = getBrainstorm(item.project_id);
|
|
73
65
|
const conversations = getRecentConversations(item.project_id, 20);
|
|
74
66
|
|
|
@@ -76,8 +68,11 @@ export async function generatePromptForItem(
|
|
|
76
68
|
? conversations.map(c => `${c.role === 'user' ? '사용자' : 'AI'}: ${c.content}`).join('\n')
|
|
77
69
|
: undefined;
|
|
78
70
|
|
|
71
|
+
const projectDocs = getProjectContextSummary(item.project_id) || undefined;
|
|
72
|
+
|
|
79
73
|
return generatePrompt(item, {
|
|
80
74
|
brainstormContent: brainstorm?.content,
|
|
81
75
|
conversationHistory,
|
|
76
|
+
projectDocs,
|
|
82
77
|
});
|
|
83
78
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { runClaude } from './client';
|
|
2
|
+
import { updateItem, addChildItems, getItemTree, deleteItem } from '../db/queries/items';
|
|
3
|
+
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
4
|
+
import { getDb } from '../db/index';
|
|
5
|
+
import type { IItem, IItemTree } from '@/types';
|
|
6
|
+
|
|
7
|
+
interface RefineChild {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
item_type: 'feature' | 'task' | 'bug' | 'idea' | 'note';
|
|
11
|
+
priority: 'high' | 'medium' | 'low';
|
|
12
|
+
children?: RefineChild[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RefineResult {
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
children?: RefineChild[];
|
|
19
|
+
remove_children?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function refineItem(
|
|
23
|
+
item: IItem,
|
|
24
|
+
userMessage: string,
|
|
25
|
+
): Promise<{ title: string; description: string; tree: IItemTree[] }> {
|
|
26
|
+
const brainstorm = getBrainstorm(item.project_id);
|
|
27
|
+
|
|
28
|
+
// Get existing children info
|
|
29
|
+
const db = getDb();
|
|
30
|
+
const existingChildren = db.prepare(
|
|
31
|
+
'SELECT id, title, description, item_type, priority FROM items WHERE parent_id = ?'
|
|
32
|
+
).all(item.id) as Pick<IItem, 'id' | 'title' | 'description' | 'item_type' | 'priority'>[];
|
|
33
|
+
|
|
34
|
+
const childrenInfo = existingChildren.length > 0
|
|
35
|
+
? `\n\n현재 하위 항목 (${existingChildren.length}개):\n${existingChildren.map((c, i) => `${i + 1}. [${c.item_type}] ${c.title}: ${c.description}`).join('\n')}`
|
|
36
|
+
: '\n\n현재 하위 항목: 없음';
|
|
37
|
+
|
|
38
|
+
const systemPrompt = `You are a task refinement assistant. The user wants to modify a task item and potentially its sub-items.
|
|
39
|
+
You ALWAYS output ONLY a raw JSON object, nothing else.
|
|
40
|
+
|
|
41
|
+
Output schema:
|
|
42
|
+
{
|
|
43
|
+
"title": string,
|
|
44
|
+
"description": string,
|
|
45
|
+
"children": [{ "title": string, "description": string, "item_type": "feature"|"task"|"bug"|"idea"|"note", "priority": "high"|"medium"|"low", "children": [...] }],
|
|
46
|
+
"remove_children": boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Rules:
|
|
50
|
+
- Output MUST be a JSON object
|
|
51
|
+
- No markdown fences, no explanation, no text before or after the JSON
|
|
52
|
+
- Keep titles concise (under 50 chars)
|
|
53
|
+
- Write in Korean
|
|
54
|
+
- description should be detailed and actionable
|
|
55
|
+
- "children" is OPTIONAL: include it only if the user asks to add/restructure sub-items
|
|
56
|
+
- "remove_children" is OPTIONAL: set true only if the user explicitly asks to remove existing children before adding new ones
|
|
57
|
+
- If the user just wants to modify the title/description, omit "children"
|
|
58
|
+
- If the user asks to break down, detail, or expand the item, provide "children" array with sub-items`;
|
|
59
|
+
|
|
60
|
+
const context = brainstorm?.content ? `\n\n브레인스토밍 원문:\n${brainstorm.content}` : '';
|
|
61
|
+
|
|
62
|
+
const prompt = `${systemPrompt}\n\n현재 항목:
|
|
63
|
+
제목: ${item.title}
|
|
64
|
+
설명: ${item.description}
|
|
65
|
+
유형: ${item.item_type}
|
|
66
|
+
우선순위: ${item.priority}${childrenInfo}${context}
|
|
67
|
+
|
|
68
|
+
사용자 요청: ${userMessage}`;
|
|
69
|
+
|
|
70
|
+
const resultText = await runClaude(prompt);
|
|
71
|
+
|
|
72
|
+
const cleaned = resultText.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '').trim();
|
|
73
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
74
|
+
if (!jsonMatch) {
|
|
75
|
+
throw new Error('AI did not return valid JSON');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parsed = JSON.parse(jsonMatch[0]) as RefineResult;
|
|
79
|
+
|
|
80
|
+
// Update the item itself
|
|
81
|
+
updateItem(item.id, {
|
|
82
|
+
title: parsed.title,
|
|
83
|
+
description: parsed.description,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Handle children changes
|
|
87
|
+
if (parsed.children && parsed.children.length > 0) {
|
|
88
|
+
// If remove_children is set, delete existing children first
|
|
89
|
+
if (parsed.remove_children && existingChildren.length > 0) {
|
|
90
|
+
for (const child of existingChildren) {
|
|
91
|
+
deleteItem(child.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Add new children
|
|
96
|
+
addChildItems(item.project_id, item.id, parsed.children.map(c => ({
|
|
97
|
+
parent_id: item.id,
|
|
98
|
+
title: c.title,
|
|
99
|
+
description: c.description,
|
|
100
|
+
item_type: c.item_type,
|
|
101
|
+
priority: c.priority,
|
|
102
|
+
children: c.children?.map(mapChild) ?? undefined,
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Return updated tree
|
|
107
|
+
const tree = getItemTree(item.project_id);
|
|
108
|
+
|
|
109
|
+
return { title: parsed.title, description: parsed.description, tree };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function mapChild(c: RefineChild): {
|
|
113
|
+
parent_id: null;
|
|
114
|
+
title: string;
|
|
115
|
+
description: string;
|
|
116
|
+
item_type: RefineChild['item_type'];
|
|
117
|
+
priority: RefineChild['priority'];
|
|
118
|
+
children?: ReturnType<typeof mapChild>[];
|
|
119
|
+
} {
|
|
120
|
+
return {
|
|
121
|
+
parent_id: null,
|
|
122
|
+
title: c.title,
|
|
123
|
+
description: c.description,
|
|
124
|
+
item_type: c.item_type,
|
|
125
|
+
priority: c.priority,
|
|
126
|
+
children: c.children?.map(mapChild),
|
|
127
|
+
};
|
|
128
|
+
}
|