idea-manager 0.1.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/README.md +36 -0
- package/bin/im.js +4 -0
- package/next.config.ts +8 -0
- package/package.json +55 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/health/route.ts +5 -0
- package/src/app/api/projects/[id]/brainstorm/route.ts +37 -0
- package/src/app/api/projects/[id]/conversations/route.ts +50 -0
- package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +51 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +73 -0
- package/src/app/api/projects/[id]/items/route.ts +17 -0
- package/src/app/api/projects/[id]/memos/route.ts +18 -0
- package/src/app/api/projects/[id]/route.ts +39 -0
- package/src/app/api/projects/[id]/structure/route.ts +28 -0
- package/src/app/api/projects/route.ts +19 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +437 -0
- package/src/app/layout.tsx +42 -0
- package/src/app/page.tsx +175 -0
- package/src/app/projects/[id]/page.tsx +249 -0
- package/src/cli.ts +41 -0
- package/src/components/brainstorm/Editor.tsx +163 -0
- package/src/components/brainstorm/MemoPin.tsx +31 -0
- package/src/components/brainstorm/ResizeHandle.tsx +45 -0
- package/src/components/chat/ChatMessage.tsx +28 -0
- package/src/components/chat/ChatPanel.tsx +100 -0
- package/src/components/tree/ItemDetail.tsx +196 -0
- package/src/components/tree/LockToggle.tsx +23 -0
- package/src/components/tree/StatusBadge.tsx +32 -0
- package/src/components/tree/TreeNode.tsx +118 -0
- package/src/components/tree/TreeView.tsx +60 -0
- package/src/lib/ai/chat-responder.ts +69 -0
- package/src/lib/ai/client.ts +124 -0
- package/src/lib/ai/prompter.ts +83 -0
- package/src/lib/ai/structurer.ts +74 -0
- package/src/lib/db/index.ts +16 -0
- package/src/lib/db/queries/brainstorms.ts +26 -0
- package/src/lib/db/queries/conversations.ts +46 -0
- package/src/lib/db/queries/items.ts +147 -0
- package/src/lib/db/queries/memos.ts +66 -0
- package/src/lib/db/queries/projects.ts +53 -0
- package/src/lib/db/queries/prompts.ts +68 -0
- package/src/lib/db/schema.ts +78 -0
- package/src/lib/mcp/server.ts +117 -0
- package/src/lib/mcp/tools.ts +83 -0
- package/src/lib/utils/id.ts +5 -0
- package/src/lib/utils/paths.ts +16 -0
- package/src/types/index.ts +97 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { getPrompt, createPrompt } from '../db/queries/prompts';
|
|
3
|
+
import { getBrainstorm } from '../db/queries/brainstorms';
|
|
4
|
+
import { getRecentConversations } from '../db/queries/conversations';
|
|
5
|
+
import type { IPrompt, IItem } from '@/types';
|
|
6
|
+
|
|
7
|
+
export async function generatePrompt(
|
|
8
|
+
item: IItem,
|
|
9
|
+
projectContext?: { brainstormContent?: string; conversationHistory?: string },
|
|
10
|
+
): Promise<IPrompt> {
|
|
11
|
+
// Check for existing manual prompt — don't overwrite
|
|
12
|
+
const existing = getPrompt(item.id);
|
|
13
|
+
if (existing?.prompt_type === 'manual') {
|
|
14
|
+
return existing;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const systemPrompt = `You are a prompt engineering expert. Generate a clear, actionable prompt for a coding assistant (like Cursor or Claude Code) to implement the given task.
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Output ONLY the prompt text, nothing else
|
|
21
|
+
- Write in Korean
|
|
22
|
+
- Be specific and actionable
|
|
23
|
+
- Include conditions, constraints, and requirements
|
|
24
|
+
- Include relevant context from the brainstorming and conversation
|
|
25
|
+
- The prompt should be ready to paste into a coding tool
|
|
26
|
+
- Keep it concise but complete (under 500 chars)
|
|
27
|
+
- Do NOT include markdown fences or extra formatting`;
|
|
28
|
+
|
|
29
|
+
let context = '';
|
|
30
|
+
if (projectContext?.brainstormContent) {
|
|
31
|
+
context += `\n\n브레인스토밍 원문:\n${projectContext.brainstormContent}`;
|
|
32
|
+
}
|
|
33
|
+
if (projectContext?.conversationHistory) {
|
|
34
|
+
context += `\n\nAI 대화 이력:\n${projectContext.conversationHistory}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const prompt = `다음 항목에 대한 실행 프롬프트를 생성하세요:
|
|
38
|
+
|
|
39
|
+
제목: ${item.title}
|
|
40
|
+
설명: ${item.description}
|
|
41
|
+
유형: ${item.item_type}
|
|
42
|
+
우선순위: ${item.priority}${context}`;
|
|
43
|
+
|
|
44
|
+
let resultText = '';
|
|
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();
|
|
59
|
+
|
|
60
|
+
return createPrompt({
|
|
61
|
+
project_id: item.project_id,
|
|
62
|
+
item_id: item.id,
|
|
63
|
+
content: resultText,
|
|
64
|
+
prompt_type: 'auto',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function generatePromptForItem(
|
|
69
|
+
item: IItem,
|
|
70
|
+
): Promise<IPrompt> {
|
|
71
|
+
// Load project context
|
|
72
|
+
const brainstorm = getBrainstorm(item.project_id);
|
|
73
|
+
const conversations = getRecentConversations(item.project_id, 20);
|
|
74
|
+
|
|
75
|
+
const conversationHistory = conversations.length > 0
|
|
76
|
+
? conversations.map(c => `${c.role === 'user' ? '사용자' : 'AI'}: ${c.content}`).join('\n')
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
return generatePrompt(item, {
|
|
80
|
+
brainstormContent: brainstorm?.content,
|
|
81
|
+
conversationHistory,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { runStructure, runStructureWithQuestions, type IStructuredItem } from './client';
|
|
2
|
+
import { replaceItems } from '../db/queries/items';
|
|
3
|
+
import { getRecentConversations, addMessage } from '../db/queries/conversations';
|
|
4
|
+
import { resolveMemos, createMemosFromQuestions } from '../db/queries/memos';
|
|
5
|
+
import type { IItemTree, IMemo, IConversation } from '@/types';
|
|
6
|
+
|
|
7
|
+
export async function structureBrainstorm(
|
|
8
|
+
projectId: string,
|
|
9
|
+
brainstormId: string,
|
|
10
|
+
content: string,
|
|
11
|
+
): Promise<IItemTree[]> {
|
|
12
|
+
if (!content.trim()) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const structured = await runStructure(content);
|
|
17
|
+
|
|
18
|
+
const dbItems = mapToDbFormat(structured);
|
|
19
|
+
|
|
20
|
+
return replaceItems(projectId, brainstormId, dbItems);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function structureWithChat(
|
|
24
|
+
projectId: string,
|
|
25
|
+
brainstormId: string,
|
|
26
|
+
content: string,
|
|
27
|
+
): Promise<{ items: IItemTree[]; memos: IMemo[]; message: IConversation | null }> {
|
|
28
|
+
if (!content.trim()) {
|
|
29
|
+
return { items: [], memos: [], message: null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load recent conversation history
|
|
33
|
+
const history = getRecentConversations(projectId, 20);
|
|
34
|
+
const historyForAi = history.map(h => ({
|
|
35
|
+
role: h.role,
|
|
36
|
+
content: h.content,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
// AI call with questions
|
|
40
|
+
const result = await runStructureWithQuestions(content, historyForAi);
|
|
41
|
+
|
|
42
|
+
// Replace items in DB
|
|
43
|
+
const dbItems = mapToDbFormat(result.items as IStructuredItem[]);
|
|
44
|
+
const tree = replaceItems(projectId, brainstormId, dbItems);
|
|
45
|
+
|
|
46
|
+
// Resolve old memos
|
|
47
|
+
resolveMemos(projectId);
|
|
48
|
+
|
|
49
|
+
// Build AI message from questions
|
|
50
|
+
let aiMessage: IConversation | null = null;
|
|
51
|
+
let memos: IMemo[] = [];
|
|
52
|
+
|
|
53
|
+
if (result.questions.length > 0) {
|
|
54
|
+
const messageContent = result.questions
|
|
55
|
+
.map((q, i) => `${i + 1}. ${q.question}`)
|
|
56
|
+
.join('\n');
|
|
57
|
+
|
|
58
|
+
aiMessage = addMessage(projectId, 'assistant', messageContent);
|
|
59
|
+
memos = createMemosFromQuestions(projectId, aiMessage.id, result.questions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { items: tree, memos, message: aiMessage };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mapToDbFormat(items: IStructuredItem[]): Parameters<typeof replaceItems>[2] {
|
|
66
|
+
return items.map((item) => ({
|
|
67
|
+
parent_id: null,
|
|
68
|
+
title: item.title,
|
|
69
|
+
description: item.description,
|
|
70
|
+
item_type: item.item_type,
|
|
71
|
+
priority: item.priority,
|
|
72
|
+
children: item.children ? mapToDbFormat(item.children) : undefined,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { getDbPath } from '../utils/paths';
|
|
3
|
+
import { initSchema } from './schema';
|
|
4
|
+
|
|
5
|
+
let db: Database.Database | null = null;
|
|
6
|
+
|
|
7
|
+
export function getDb(): Database.Database {
|
|
8
|
+
if (!db) {
|
|
9
|
+
const dbPath = getDbPath();
|
|
10
|
+
db = new Database(dbPath);
|
|
11
|
+
db.pragma('journal_mode = WAL');
|
|
12
|
+
db.pragma('foreign_keys = ON');
|
|
13
|
+
initSchema(db);
|
|
14
|
+
}
|
|
15
|
+
return db;
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getDb } from '../index';
|
|
2
|
+
import type { IBrainstorm } from '@/types';
|
|
3
|
+
|
|
4
|
+
export function getBrainstorm(projectId: string): IBrainstorm | undefined {
|
|
5
|
+
const db = getDb();
|
|
6
|
+
return db.prepare(
|
|
7
|
+
'SELECT * FROM brainstorms WHERE project_id = ? ORDER BY version DESC LIMIT 1'
|
|
8
|
+
).get(projectId) as IBrainstorm | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function updateBrainstorm(projectId: string, content: string): IBrainstorm | undefined {
|
|
12
|
+
const db = getDb();
|
|
13
|
+
const now = new Date().toISOString();
|
|
14
|
+
|
|
15
|
+
const existing = getBrainstorm(projectId);
|
|
16
|
+
if (!existing) return undefined;
|
|
17
|
+
|
|
18
|
+
db.prepare(
|
|
19
|
+
'UPDATE brainstorms SET content = ?, version = version + 1, updated_at = ? WHERE id = ?'
|
|
20
|
+
).run(content, now, existing.id);
|
|
21
|
+
|
|
22
|
+
// Also update project's updated_at
|
|
23
|
+
db.prepare('UPDATE projects SET updated_at = ? WHERE id = ?').run(now, projectId);
|
|
24
|
+
|
|
25
|
+
return getBrainstorm(projectId);
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getDb } from '../index';
|
|
2
|
+
import { generateId } from '../../utils/id';
|
|
3
|
+
import type { IConversation } from '@/types';
|
|
4
|
+
|
|
5
|
+
export function getConversations(projectId: string, limit = 20): IConversation[] {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
return db.prepare(
|
|
8
|
+
`SELECT * FROM conversations WHERE project_id = ?
|
|
9
|
+
ORDER BY created_at ASC LIMIT ?`
|
|
10
|
+
).all(projectId, limit) as IConversation[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getRecentConversations(projectId: string, limit = 20): IConversation[] {
|
|
14
|
+
const db = getDb();
|
|
15
|
+
// Get the last N messages ordered chronologically
|
|
16
|
+
const rows = db.prepare(
|
|
17
|
+
`SELECT * FROM (
|
|
18
|
+
SELECT * FROM conversations WHERE project_id = ?
|
|
19
|
+
ORDER BY created_at DESC LIMIT ?
|
|
20
|
+
) sub ORDER BY created_at ASC`
|
|
21
|
+
).all(projectId, limit) as IConversation[];
|
|
22
|
+
return rows;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function addMessage(
|
|
26
|
+
projectId: string,
|
|
27
|
+
role: 'assistant' | 'user',
|
|
28
|
+
content: string,
|
|
29
|
+
metadata?: string,
|
|
30
|
+
): IConversation {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
const id = generateId();
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
|
|
35
|
+
db.prepare(
|
|
36
|
+
`INSERT INTO conversations (id, project_id, role, content, metadata, created_at)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
38
|
+
).run(id, projectId, role, content, metadata ?? null, now);
|
|
39
|
+
|
|
40
|
+
return db.prepare('SELECT * FROM conversations WHERE id = ?').get(id) as IConversation;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function deleteConversations(projectId: string): void {
|
|
44
|
+
const db = getDb();
|
|
45
|
+
db.prepare('DELETE FROM conversations WHERE project_id = ?').run(projectId);
|
|
46
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getDb } from '../index';
|
|
2
|
+
import { generateId } from '../../utils/id';
|
|
3
|
+
import type { IItem, IItemTree, ItemType, ItemPriority, ItemStatus } from '@/types';
|
|
4
|
+
|
|
5
|
+
export function getItems(projectId: string): IItem[] {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
return db.prepare(
|
|
8
|
+
'SELECT * FROM items WHERE project_id = ? ORDER BY sort_order ASC'
|
|
9
|
+
).all(projectId) as IItem[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getItemTree(projectId: string): IItemTree[] {
|
|
13
|
+
const items = getItems(projectId);
|
|
14
|
+
return buildTree(items);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildTree(items: IItem[]): IItemTree[] {
|
|
18
|
+
const map = new Map<string, IItemTree>();
|
|
19
|
+
const roots: IItemTree[] = [];
|
|
20
|
+
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
map.set(item.id, { ...item, children: [] });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
const node = map.get(item.id)!;
|
|
27
|
+
if (item.parent_id && map.has(item.parent_id)) {
|
|
28
|
+
map.get(item.parent_id)!.children.push(node);
|
|
29
|
+
} else {
|
|
30
|
+
roots.push(node);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return roots;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createItem(data: {
|
|
38
|
+
project_id: string;
|
|
39
|
+
brainstorm_id?: string;
|
|
40
|
+
parent_id?: string;
|
|
41
|
+
title: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
item_type?: ItemType;
|
|
44
|
+
priority?: ItemPriority;
|
|
45
|
+
sort_order?: number;
|
|
46
|
+
}): IItem {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const id = generateId();
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
|
|
51
|
+
db.prepare(`
|
|
52
|
+
INSERT INTO items (id, project_id, brainstorm_id, parent_id, title, description,
|
|
53
|
+
item_type, priority, status, is_locked, sort_order, created_at, updated_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 1, ?, ?, ?)
|
|
55
|
+
`).run(
|
|
56
|
+
id,
|
|
57
|
+
data.project_id,
|
|
58
|
+
data.brainstorm_id ?? null,
|
|
59
|
+
data.parent_id ?? null,
|
|
60
|
+
data.title,
|
|
61
|
+
data.description ?? '',
|
|
62
|
+
data.item_type ?? 'feature',
|
|
63
|
+
data.priority ?? 'medium',
|
|
64
|
+
data.sort_order ?? 0,
|
|
65
|
+
now,
|
|
66
|
+
now,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return db.prepare('SELECT * FROM items WHERE id = ?').get(id) as IItem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function updateItem(id: string, data: {
|
|
73
|
+
title?: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
status?: ItemStatus;
|
|
76
|
+
is_locked?: boolean;
|
|
77
|
+
priority?: ItemPriority;
|
|
78
|
+
sort_order?: number;
|
|
79
|
+
}): IItem | undefined {
|
|
80
|
+
const db = getDb();
|
|
81
|
+
const item = db.prepare('SELECT * FROM items WHERE id = ?').get(id) as IItem | undefined;
|
|
82
|
+
if (!item) return undefined;
|
|
83
|
+
|
|
84
|
+
const now = new Date().toISOString();
|
|
85
|
+
db.prepare(`
|
|
86
|
+
UPDATE items SET
|
|
87
|
+
title = ?, description = ?, status = ?, is_locked = ?,
|
|
88
|
+
priority = ?, sort_order = ?, updated_at = ?
|
|
89
|
+
WHERE id = ?
|
|
90
|
+
`).run(
|
|
91
|
+
data.title ?? item.title,
|
|
92
|
+
data.description ?? item.description,
|
|
93
|
+
data.status ?? item.status,
|
|
94
|
+
data.is_locked !== undefined ? (data.is_locked ? 1 : 0) : (item.is_locked ? 1 : 0),
|
|
95
|
+
data.priority ?? item.priority,
|
|
96
|
+
data.sort_order ?? item.sort_order,
|
|
97
|
+
now,
|
|
98
|
+
id,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return db.prepare('SELECT * FROM items WHERE id = ?').get(id) as IItem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteItemsByProject(projectId: string): void {
|
|
105
|
+
const db = getDb();
|
|
106
|
+
db.prepare('DELETE FROM items WHERE project_id = ?').run(projectId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function replaceItems(projectId: string, brainstormId: string, newItems: {
|
|
110
|
+
parent_id: string | null;
|
|
111
|
+
title: string;
|
|
112
|
+
description: string;
|
|
113
|
+
item_type: ItemType;
|
|
114
|
+
priority: ItemPriority;
|
|
115
|
+
children?: typeof newItems;
|
|
116
|
+
}[]): IItemTree[] {
|
|
117
|
+
const db = getDb();
|
|
118
|
+
|
|
119
|
+
const insertItems = db.transaction(() => {
|
|
120
|
+
// Delete existing items for this project
|
|
121
|
+
db.prepare('DELETE FROM items WHERE project_id = ?').run(projectId);
|
|
122
|
+
|
|
123
|
+
// Insert new items recursively
|
|
124
|
+
let sortOrder = 0;
|
|
125
|
+
const insertRecursive = (items: typeof newItems, parentId: string | null) => {
|
|
126
|
+
for (const item of items) {
|
|
127
|
+
const id = generateId();
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
db.prepare(`
|
|
130
|
+
INSERT INTO items (id, project_id, brainstorm_id, parent_id, title, description,
|
|
131
|
+
item_type, priority, status, is_locked, sort_order, created_at, updated_at)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 1, ?, ?, ?)
|
|
133
|
+
`).run(id, projectId, brainstormId, parentId, item.title, item.description,
|
|
134
|
+
item.item_type, item.priority, sortOrder++, now, now);
|
|
135
|
+
|
|
136
|
+
if (item.children?.length) {
|
|
137
|
+
insertRecursive(item.children, id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
insertRecursive(newItems, null);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
insertItems();
|
|
146
|
+
return getItemTree(projectId);
|
|
147
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getDb } from '../index';
|
|
2
|
+
import { generateId } from '../../utils/id';
|
|
3
|
+
import type { IMemo } from '@/types';
|
|
4
|
+
|
|
5
|
+
export function getMemos(projectId: string, onlyUnresolved = false): IMemo[] {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
const whereClause = onlyUnresolved
|
|
8
|
+
? 'WHERE project_id = ? AND is_resolved = 0'
|
|
9
|
+
: 'WHERE project_id = ?';
|
|
10
|
+
|
|
11
|
+
return db.prepare(
|
|
12
|
+
`SELECT * FROM memos ${whereClause} ORDER BY created_at ASC`
|
|
13
|
+
).all(projectId) as IMemo[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createMemo(data: {
|
|
17
|
+
project_id: string;
|
|
18
|
+
conversation_id?: string;
|
|
19
|
+
anchor_text: string;
|
|
20
|
+
question: string;
|
|
21
|
+
}): IMemo {
|
|
22
|
+
const db = getDb();
|
|
23
|
+
const id = generateId();
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
|
|
26
|
+
db.prepare(
|
|
27
|
+
`INSERT INTO memos (id, project_id, conversation_id, anchor_text, question, is_resolved, created_at, updated_at)
|
|
28
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?)`
|
|
29
|
+
).run(id, data.project_id, data.conversation_id ?? null, data.anchor_text, data.question, now, now);
|
|
30
|
+
|
|
31
|
+
return db.prepare('SELECT * FROM memos WHERE id = ?').get(id) as IMemo;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveMemos(projectId: string): void {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
db.prepare(
|
|
38
|
+
`UPDATE memos SET is_resolved = 1, updated_at = ? WHERE project_id = ? AND is_resolved = 0`
|
|
39
|
+
).run(now, projectId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createMemosFromQuestions(
|
|
43
|
+
projectId: string,
|
|
44
|
+
conversationId: string,
|
|
45
|
+
questions: { anchor_text: string; question: string }[],
|
|
46
|
+
): IMemo[] {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const memos: IMemo[] = [];
|
|
49
|
+
|
|
50
|
+
const insertMemo = db.prepare(
|
|
51
|
+
`INSERT INTO memos (id, project_id, conversation_id, anchor_text, question, is_resolved, created_at, updated_at)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, ?)`
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const insertAll = db.transaction(() => {
|
|
56
|
+
for (const q of questions) {
|
|
57
|
+
const id = generateId();
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
insertMemo.run(id, projectId, conversationId, q.anchor_text, q.question, now, now);
|
|
60
|
+
memos.push(db.prepare('SELECT * FROM memos WHERE id = ?').get(id) as IMemo);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
insertAll();
|
|
65
|
+
return memos;
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getDb } from '../index';
|
|
2
|
+
import { generateId } from '../../utils/id';
|
|
3
|
+
import type { IProject } from '@/types';
|
|
4
|
+
|
|
5
|
+
export function listProjects(): IProject[] {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
return db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as IProject[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getProject(id: string): IProject | undefined {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
return db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as IProject | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createProject(name: string, description: string = ''): IProject {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
const id = generateId();
|
|
18
|
+
const now = new Date().toISOString();
|
|
19
|
+
|
|
20
|
+
db.prepare(
|
|
21
|
+
'INSERT INTO projects (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
|
22
|
+
).run(id, name, description, now, now);
|
|
23
|
+
|
|
24
|
+
// Also create a default brainstorm
|
|
25
|
+
const brainstormId = generateId();
|
|
26
|
+
db.prepare(
|
|
27
|
+
'INSERT INTO brainstorms (id, project_id, content, version, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)'
|
|
28
|
+
).run(brainstormId, id, '', now, now);
|
|
29
|
+
|
|
30
|
+
return getProject(id)!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function updateProject(id: string, data: { name?: string; description?: string }): IProject | undefined {
|
|
34
|
+
const db = getDb();
|
|
35
|
+
const project = getProject(id);
|
|
36
|
+
if (!project) return undefined;
|
|
37
|
+
|
|
38
|
+
const name = data.name ?? project.name;
|
|
39
|
+
const description = data.description ?? project.description;
|
|
40
|
+
const now = new Date().toISOString();
|
|
41
|
+
|
|
42
|
+
db.prepare(
|
|
43
|
+
'UPDATE projects SET name = ?, description = ?, updated_at = ? WHERE id = ?'
|
|
44
|
+
).run(name, description, now, id);
|
|
45
|
+
|
|
46
|
+
return getProject(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function deleteProject(id: string): boolean {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const result = db.prepare('DELETE FROM projects WHERE id = ?').run(id);
|
|
52
|
+
return result.changes > 0;
|
|
53
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getDb } from '../index';
|
|
2
|
+
import { generateId } from '../../utils/id';
|
|
3
|
+
import type { IPrompt } from '@/types';
|
|
4
|
+
|
|
5
|
+
export function getPrompt(itemId: string): IPrompt | undefined {
|
|
6
|
+
const db = getDb();
|
|
7
|
+
return db.prepare(
|
|
8
|
+
'SELECT * FROM prompts WHERE item_id = ? ORDER BY version DESC LIMIT 1'
|
|
9
|
+
).get(itemId) as IPrompt | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getPromptsByProject(projectId: string): IPrompt[] {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
// Get latest version of each item's prompt
|
|
15
|
+
return db.prepare(
|
|
16
|
+
`SELECT p.* FROM prompts p
|
|
17
|
+
INNER JOIN (
|
|
18
|
+
SELECT item_id, MAX(version) as max_version
|
|
19
|
+
FROM prompts WHERE project_id = ?
|
|
20
|
+
GROUP BY item_id
|
|
21
|
+
) latest ON p.item_id = latest.item_id AND p.version = latest.max_version
|
|
22
|
+
WHERE p.project_id = ?`
|
|
23
|
+
).all(projectId, projectId) as IPrompt[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createPrompt(data: {
|
|
27
|
+
project_id: string;
|
|
28
|
+
item_id: string;
|
|
29
|
+
content: string;
|
|
30
|
+
prompt_type?: 'auto' | 'manual';
|
|
31
|
+
}): IPrompt {
|
|
32
|
+
const db = getDb();
|
|
33
|
+
const id = generateId();
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
|
|
36
|
+
// Get next version number
|
|
37
|
+
const existing = db.prepare(
|
|
38
|
+
'SELECT MAX(version) as max_ver FROM prompts WHERE item_id = ?'
|
|
39
|
+
).get(data.item_id) as { max_ver: number | null } | undefined;
|
|
40
|
+
const version = (existing?.max_ver ?? 0) + 1;
|
|
41
|
+
|
|
42
|
+
db.prepare(
|
|
43
|
+
`INSERT INTO prompts (id, project_id, item_id, content, prompt_type, version, created_at)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
45
|
+
).run(id, data.project_id, data.item_id, data.content, data.prompt_type ?? 'auto', version, now);
|
|
46
|
+
|
|
47
|
+
return db.prepare('SELECT * FROM prompts WHERE id = ?').get(id) as IPrompt;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function updatePromptContent(itemId: string, content: string): IPrompt {
|
|
51
|
+
// Create a new manual version
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const existing = getPrompt(itemId);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
throw new Error('No prompt found for this item');
|
|
56
|
+
}
|
|
57
|
+
return createPrompt({
|
|
58
|
+
project_id: existing.project_id,
|
|
59
|
+
item_id: itemId,
|
|
60
|
+
content,
|
|
61
|
+
prompt_type: 'manual',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function deletePromptsByItem(itemId: string): void {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
db.prepare('DELETE FROM prompts WHERE item_id = ?').run(itemId);
|
|
68
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
export function initSchema(db: Database.Database): void {
|
|
4
|
+
db.exec(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
description TEXT NOT NULL DEFAULT '',
|
|
9
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS brainstorms (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
project_id TEXT NOT NULL,
|
|
16
|
+
content TEXT NOT NULL DEFAULT '',
|
|
17
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
18
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
19
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
20
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
project_id TEXT NOT NULL,
|
|
26
|
+
brainstorm_id TEXT,
|
|
27
|
+
parent_id TEXT,
|
|
28
|
+
title TEXT NOT NULL,
|
|
29
|
+
description TEXT NOT NULL DEFAULT '',
|
|
30
|
+
item_type TEXT NOT NULL DEFAULT 'feature',
|
|
31
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
32
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
33
|
+
is_locked INTEGER NOT NULL DEFAULT 1,
|
|
34
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
metadata TEXT,
|
|
36
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
38
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
39
|
+
FOREIGN KEY (brainstorm_id) REFERENCES brainstorms(id),
|
|
40
|
+
FOREIGN KEY (parent_id) REFERENCES items(id)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
44
|
+
id TEXT PRIMARY KEY,
|
|
45
|
+
project_id TEXT NOT NULL,
|
|
46
|
+
role TEXT NOT NULL CHECK(role IN ('assistant', 'user')),
|
|
47
|
+
content TEXT NOT NULL,
|
|
48
|
+
metadata TEXT,
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
50
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS memos (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
project_id TEXT NOT NULL,
|
|
56
|
+
conversation_id TEXT,
|
|
57
|
+
anchor_text TEXT NOT NULL,
|
|
58
|
+
question TEXT NOT NULL,
|
|
59
|
+
is_resolved INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
61
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
62
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
63
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS prompts (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
project_id TEXT NOT NULL,
|
|
69
|
+
item_id TEXT NOT NULL,
|
|
70
|
+
content TEXT NOT NULL,
|
|
71
|
+
prompt_type TEXT NOT NULL DEFAULT 'auto',
|
|
72
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
73
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
74
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
75
|
+
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
|
|
76
|
+
);
|
|
77
|
+
`);
|
|
78
|
+
}
|