idea-manager 0.4.0 → 0.5.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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/app/projects/[id]/page.tsx +0 -4
  3. package/src/components/brainstorm/Editor.tsx +1 -84
  4. package/src/lib/ai/client.ts +0 -123
  5. package/src/lib/db/schema.ts +0 -70
  6. package/src/types/index.ts +0 -90
  7. package/src/app/api/projects/[id]/cleanup/route.ts +0 -32
  8. package/src/app/api/projects/[id]/conversations/route.ts +0 -50
  9. package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +0 -51
  10. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +0 -36
  11. package/src/app/api/projects/[id]/items/[itemId]/route.ts +0 -95
  12. package/src/app/api/projects/[id]/items/route.ts +0 -67
  13. package/src/app/api/projects/[id]/memos/route.ts +0 -18
  14. package/src/app/api/projects/[id]/scan/route.ts +0 -73
  15. package/src/app/api/projects/[id]/scan/stream/route.ts +0 -112
  16. package/src/app/api/projects/[id]/structure/route.ts +0 -59
  17. package/src/app/api/projects/[id]/structure/stream/route.ts +0 -157
  18. package/src/components/ScanPanel.tsx +0 -743
  19. package/src/components/brainstorm/MemoPin.tsx +0 -117
  20. package/src/components/tree/CardView.tsx +0 -206
  21. package/src/components/tree/ItemDetail.tsx +0 -196
  22. package/src/components/tree/LockToggle.tsx +0 -23
  23. package/src/components/tree/RefinePopover.tsx +0 -157
  24. package/src/components/tree/StatusBadge.tsx +0 -32
  25. package/src/components/tree/TreeNode.tsx +0 -227
  26. package/src/components/tree/TreeView.tsx +0 -304
  27. package/src/lib/ai/chat-responder.ts +0 -71
  28. package/src/lib/ai/cleanup.ts +0 -87
  29. package/src/lib/ai/prompter.ts +0 -78
  30. package/src/lib/ai/refiner.ts +0 -128
  31. package/src/lib/ai/structurer.ts +0 -403
  32. package/src/lib/db/queries/context.ts +0 -76
  33. package/src/lib/db/queries/conversations.ts +0 -46
  34. package/src/lib/db/queries/items.ts +0 -268
  35. package/src/lib/db/queries/memos.ts +0 -66
  36. package/src/lib/db/queries/prompts.ts +0 -68
  37. package/src/lib/scanner.ts +0 -573
  38. package/src/lib/task-store.ts +0 -97
@@ -1,95 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getDb } from '@/lib/db/index';
3
- import { updateItem, deleteItem } from '@/lib/db/queries/items';
4
- import type { IItem, ItemStatus } from '@/types';
5
-
6
- export async function GET(
7
- _request: NextRequest,
8
- { params }: { params: Promise<{ id: string; itemId: string }> },
9
- ) {
10
- const { itemId } = await params;
11
- const db = getDb();
12
- const item = db.prepare('SELECT * FROM items WHERE id = ?').get(itemId) as IItem | undefined;
13
-
14
- if (!item) {
15
- return NextResponse.json({ error: 'Item not found' }, { status: 404 });
16
- }
17
-
18
- return NextResponse.json(item);
19
- }
20
-
21
- export async function PUT(
22
- request: NextRequest,
23
- { params }: { params: Promise<{ id: string; itemId: string }> },
24
- ) {
25
- const { id: projectId, itemId } = await params;
26
- const body = await request.json();
27
- const db = getDb();
28
-
29
- const item = db.prepare('SELECT * FROM items WHERE id = ? AND project_id = ?')
30
- .get(itemId, projectId) as IItem | undefined;
31
-
32
- if (!item) {
33
- return NextResponse.json({ error: 'Item not found' }, { status: 404 });
34
- }
35
-
36
- const updates: Parameters<typeof updateItem>[1] = {};
37
-
38
- if (body.status !== undefined) {
39
- updates.status = body.status as ItemStatus;
40
- }
41
-
42
- if (body.is_locked !== undefined) {
43
- updates.is_locked = Boolean(body.is_locked);
44
-
45
- // If locking parent, also lock all children recursively
46
- if (body.is_locked) {
47
- lockChildrenRecursive(db, itemId, true);
48
- }
49
- }
50
-
51
- if (body.is_pinned !== undefined) {
52
- updates.is_pinned = Boolean(body.is_pinned);
53
- }
54
-
55
- if (body.title !== undefined) updates.title = body.title;
56
- if (body.description !== undefined) updates.description = body.description;
57
- if (body.priority !== undefined) updates.priority = body.priority;
58
-
59
- // Auto-lock on completion
60
- if (body.status === 'done') {
61
- updates.is_locked = true;
62
- }
63
-
64
- const updated = updateItem(itemId, updates);
65
- return NextResponse.json(updated);
66
- }
67
-
68
- export async function DELETE(
69
- _request: NextRequest,
70
- { params }: { params: Promise<{ id: string; itemId: string }> },
71
- ) {
72
- const { id: projectId, itemId } = await params;
73
- const db = getDb();
74
-
75
- const item = db.prepare('SELECT * FROM items WHERE id = ? AND project_id = ?')
76
- .get(itemId, projectId) as IItem | undefined;
77
-
78
- if (!item) {
79
- return NextResponse.json({ error: 'Item not found' }, { status: 404 });
80
- }
81
-
82
- deleteItem(itemId);
83
- return NextResponse.json({ success: true });
84
- }
85
-
86
- function lockChildrenRecursive(db: ReturnType<typeof getDb>, parentId: string, locked: boolean) {
87
- const now = new Date().toISOString();
88
- const children = db.prepare('SELECT id FROM items WHERE parent_id = ?').all(parentId) as { id: string }[];
89
-
90
- for (const child of children) {
91
- db.prepare('UPDATE items SET is_locked = ?, updated_at = ? WHERE id = ?')
92
- .run(locked ? 1 : 0, now, child.id);
93
- lockChildrenRecursive(db, child.id, locked);
94
- }
95
- }
@@ -1,67 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getProject } from '@/lib/db/queries/projects';
3
- import { getItemTree, deleteItem, deleteItemsByProject, bulkUpdateStatus } from '@/lib/db/queries/items';
4
-
5
- export async function GET(
6
- _request: NextRequest,
7
- { params }: { params: Promise<{ id: string }> },
8
- ) {
9
- const { id } = await params;
10
- const project = getProject(id);
11
- if (!project) {
12
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
13
- }
14
-
15
- const tree = getItemTree(id);
16
- return NextResponse.json(tree);
17
- }
18
-
19
- // Bulk delete: DELETE /api/projects/[id]/items
20
- // body: { itemIds: string[] } or { all: true }
21
- export async function DELETE(
22
- request: NextRequest,
23
- { params }: { params: Promise<{ id: string }> },
24
- ) {
25
- const { id } = await params;
26
- const project = getProject(id);
27
- if (!project) {
28
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
29
- }
30
-
31
- const body = await request.json();
32
-
33
- if (body.all) {
34
- deleteItemsByProject(id);
35
- } else if (Array.isArray(body.itemIds)) {
36
- for (const itemId of body.itemIds) {
37
- deleteItem(itemId);
38
- }
39
- } else {
40
- return NextResponse.json({ error: 'itemIds or all required' }, { status: 400 });
41
- }
42
-
43
- const tree = getItemTree(id);
44
- return NextResponse.json(tree);
45
- }
46
-
47
- // Bulk update status: PATCH /api/projects/[id]/items
48
- // body: { status: 'done' | 'pending' | 'in_progress' }
49
- export async function PATCH(
50
- request: NextRequest,
51
- { params }: { params: Promise<{ id: string }> },
52
- ) {
53
- const { id } = await params;
54
- const project = getProject(id);
55
- if (!project) {
56
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
57
- }
58
-
59
- const body = await request.json();
60
- if (!body.status) {
61
- return NextResponse.json({ error: 'status required' }, { status: 400 });
62
- }
63
-
64
- bulkUpdateStatus(id, body.status);
65
- const tree = getItemTree(id);
66
- return NextResponse.json(tree);
67
- }
@@ -1,18 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getProject } from '@/lib/db/queries/projects';
3
- import { getMemos } from '@/lib/db/queries/memos';
4
-
5
- export async function GET(
6
- request: NextRequest,
7
- { params }: { params: Promise<{ id: string }> },
8
- ) {
9
- const { id } = await params;
10
- const project = getProject(id);
11
- if (!project) {
12
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
13
- }
14
-
15
- const unresolvedOnly = request.nextUrl.searchParams.get('unresolved') === 'true';
16
- const memos = getMemos(id, unresolvedOnly);
17
- return NextResponse.json(memos);
18
- }
@@ -1,73 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getProject } from '@/lib/db/queries/projects';
3
- import { getProjectContexts, replaceProjectContexts } from '@/lib/db/queries/context';
4
- import { scanProjectDirectory } from '@/lib/scanner';
5
- import { getFileCategory } from '@/lib/scanner';
6
-
7
- export async function GET(
8
- _request: NextRequest,
9
- { params }: { params: Promise<{ id: string }> },
10
- ) {
11
- const { id } = await params;
12
- const project = getProject(id);
13
- if (!project) {
14
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
15
- }
16
-
17
- const contexts = getProjectContexts(id);
18
- if (contexts.length === 0) {
19
- return NextResponse.json({ exists: false, files: [], total: 0, totalSize: 0, scannedAt: null });
20
- }
21
-
22
- const files = contexts.map(c => ({
23
- file_path: c.file_path,
24
- size: c.content.length,
25
- category: getFileCategory(c.file_path),
26
- folder: getFolder(c.file_path),
27
- }));
28
-
29
- return NextResponse.json({
30
- exists: true,
31
- files,
32
- total: contexts.length,
33
- totalSize: contexts.reduce((s, c) => s + c.content.length, 0),
34
- scannedAt: contexts[0]?.scanned_at || null,
35
- });
36
- }
37
-
38
- function getFolder(filePath: string): string {
39
- const parts = filePath.split('/');
40
- if (parts.length <= 1 || filePath.startsWith('__')) return '(root)';
41
- return parts[0];
42
- }
43
-
44
- export async function POST(
45
- _request: NextRequest,
46
- { params }: { params: Promise<{ id: string }> },
47
- ) {
48
- const { id } = await params;
49
- const project = getProject(id);
50
- if (!project) {
51
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
52
- }
53
-
54
- if (!project.project_path) {
55
- return NextResponse.json({ error: '프로젝트 경로가 설정되지 않았습니다' }, { status: 400 });
56
- }
57
-
58
- try {
59
- const scanned = scanProjectDirectory(project.project_path);
60
- const contexts = replaceProjectContexts(id, scanned);
61
-
62
- return NextResponse.json({
63
- files: contexts.map(c => ({
64
- file_path: c.file_path,
65
- size: c.content.length,
66
- })),
67
- total: contexts.length,
68
- });
69
- } catch (error) {
70
- const msg = error instanceof Error ? error.message : 'Scan failed';
71
- return NextResponse.json({ error: msg }, { status: 500 });
72
- }
73
- }
@@ -1,112 +0,0 @@
1
- import { NextRequest } from 'next/server';
2
- import { getProject } from '@/lib/db/queries/projects';
3
- import { replaceProjectContexts } from '@/lib/db/queries/context';
4
- import { scanProjectDirectoryStream } from '@/lib/scanner';
5
- import { runAnalysis } from '@/lib/ai/client';
6
-
7
- export async function GET(
8
- _request: NextRequest,
9
- { params }: { params: Promise<{ id: string }> },
10
- ) {
11
- const { id } = await params;
12
- const project = getProject(id);
13
- if (!project) {
14
- return new Response('Project not found', { status: 404 });
15
- }
16
-
17
- if (!project.project_path) {
18
- return new Response('No project path', { status: 400 });
19
- }
20
-
21
- const projectPath = project.project_path;
22
- const projectId = id;
23
-
24
- const stream = new ReadableStream({
25
- async start(controller) {
26
- const encoder = new TextEncoder();
27
-
28
- const send = (event: string, data: unknown) => {
29
- try {
30
- controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
31
- } catch { /* closed */ }
32
- };
33
- let directoryTree = '';
34
- let readmeContent = '';
35
- let packageJsonContent = '';
36
-
37
- try {
38
- const generator = scanProjectDirectoryStream(projectPath);
39
-
40
- for (const event of generator) {
41
- if (event.type === 'scanning_dir') {
42
- send('scanning', { dir: event.dir });
43
- } else if (event.type === 'file_found') {
44
- send('file', {
45
- file_path: event.file!.file_path,
46
- size: event.file!.size,
47
- category: event.file!.category,
48
- folder: event.file!.folder,
49
- summarized: event.file!.summarized,
50
- });
51
- } else if (event.type === 'done') {
52
- replaceProjectContexts(projectId, event.results!);
53
- directoryTree = event.results?.find(r => r.file_path === '__directory_tree.txt')?.content || '';
54
- readmeContent = event.results?.find(r => r.file_path.match(/^README\.md$/i))?.content || '';
55
- packageJsonContent = event.results?.find(r => r.file_path === 'package.json')?.content || '';
56
- send('scan_complete', {
57
- total: event.total,
58
- totalSize: event.totalSize,
59
- });
60
- }
61
- }
62
- } catch (error) {
63
- const msg = error instanceof Error ? error.message : 'Scan failed';
64
- send('error', { error: msg });
65
- controller.close();
66
- return;
67
- }
68
-
69
- // Phase 2: Auto-analyze project with AI
70
- send('analyzing', { message: '프로젝트를 분석하고 있습니다...' });
71
-
72
- const analysisPrompt = `아래 프로젝트의 디렉토리 구조와 핵심 파일을 보고, 이 프로젝트에 대해 간결하게 설명해주세요.
73
-
74
- 다음 형식으로 작성해주세요 (마크다운 없이 평문):
75
- 1. 프로젝트 목적 (1줄)
76
- 2. 기술 스택 (1줄)
77
- 3. 주요 서브 프로젝트/모듈 구성 (2-3줄)
78
- 4. 현재 개발 상태 추정 (1줄)
79
-
80
- 간결하고 핵심만 담아주세요. 한국어로 작성하세요.
81
-
82
- === 디렉토리 구조 ===
83
- ${directoryTree.slice(0, 5000)}
84
-
85
- ${readmeContent ? `=== README.md ===\n${readmeContent.slice(0, 3000)}` : ''}
86
-
87
- ${packageJsonContent ? `=== package.json ===\n${packageJsonContent.slice(0, 2000)}` : ''}`;
88
-
89
- try {
90
- const analysisResult = await runAnalysis(analysisPrompt, (text) => {
91
- send('analysis_text', { text });
92
- });
93
- send('analysis_done', { description: analysisResult });
94
- } catch (err) {
95
- const msg = err instanceof Error ? err.message : 'Analysis failed';
96
- console.error('[scan] Auto-analysis failed:', msg);
97
- send('analysis_done', { description: '', error: msg });
98
- }
99
-
100
- controller.close();
101
- },
102
- });
103
-
104
- return new Response(stream, {
105
- headers: {
106
- 'Content-Type': 'text/event-stream',
107
- 'Cache-Control': 'no-cache',
108
- 'Connection': 'keep-alive',
109
- 'X-Accel-Buffering': 'no',
110
- },
111
- });
112
- }
@@ -1,59 +0,0 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { getProject } from '@/lib/db/queries/projects';
3
- import { getBrainstorm } from '@/lib/db/queries/brainstorms';
4
- import { getProjectContextSummary } from '@/lib/db/queries/context';
5
- import { structureWithChat } from '@/lib/ai/structurer';
6
- import { getTask } from '@/lib/task-store';
7
-
8
- export async function GET(
9
- _request: NextRequest,
10
- { params }: { params: Promise<{ id: string }> },
11
- ) {
12
- const { id } = await params;
13
- const task = getTask(id);
14
- if (!task) {
15
- return NextResponse.json({ active: false });
16
- }
17
- return NextResponse.json({
18
- active: task.status === 'running',
19
- status: task.status,
20
- startedAt: task.startedAt,
21
- eventCount: task.events.length,
22
- });
23
- }
24
-
25
- export async function POST(
26
- _request: NextRequest,
27
- { params }: { params: Promise<{ id: string }> },
28
- ) {
29
- const { id } = await params;
30
- const project = getProject(id);
31
- if (!project) {
32
- return NextResponse.json({ error: 'Project not found' }, { status: 404 });
33
- }
34
-
35
- const brainstorm = getBrainstorm(id);
36
- if (!brainstorm) {
37
- return NextResponse.json({ error: 'Project not initialized' }, { status: 400 });
38
- }
39
-
40
- const hasContent = brainstorm.content.trim();
41
- const hasContext = !!getProjectContextSummary(id);
42
-
43
- if (!hasContent && !hasContext) {
44
- return NextResponse.json({ error: '브레인스토밍 내용이나 프로젝트 스캔 결과가 필요합니다' }, { status: 400 });
45
- }
46
-
47
- // If brainstorm is empty but project context exists, use a placeholder prompt
48
- const content = hasContent
49
- ? brainstorm.content
50
- : '프로젝트 스캔 결과를 분석하여 현재 프로젝트의 구조, 진행 상황, TODO 항목을 파악해주세요.';
51
-
52
- try {
53
- const result = await structureWithChat(id, brainstorm.id, content);
54
- return NextResponse.json(result);
55
- } catch (error) {
56
- const message = error instanceof Error ? error.message : 'AI structuring failed';
57
- return NextResponse.json({ error: message }, { status: 500 });
58
- }
59
- }
@@ -1,157 +0,0 @@
1
- import { NextRequest } from 'next/server';
2
- import { getProject } from '@/lib/db/queries/projects';
3
- import { getBrainstorm } from '@/lib/db/queries/brainstorms';
4
- import { getProjectContextSummary } from '@/lib/db/queries/context';
5
- import { structureWithChatDirect } from '@/lib/ai/structurer';
6
- import {
7
- getTask, startTask, addTaskEvent, finishTask, failTask,
8
- addTaskListener, cleanupTasks,
9
- } from '@/lib/task-store';
10
-
11
- export async function GET(
12
- request: NextRequest,
13
- { params }: { params: Promise<{ id: string }> },
14
- ) {
15
- const { id } = await params;
16
- const project = getProject(id);
17
- if (!project) {
18
- return new Response('Project not found', { status: 404 });
19
- }
20
-
21
- cleanupTasks();
22
-
23
- const existingTask = getTask(id);
24
-
25
- // If there's an active task, attach to it (reconnect scenario)
26
- if (existingTask && existingTask.status === 'running') {
27
- return createReconnectStream(id, existingTask);
28
- }
29
-
30
- // If recently finished task exists, replay final result
31
- if (existingTask && existingTask.status === 'done' && existingTask.result) {
32
- return createReplayStream(existingTask);
33
- }
34
-
35
- // Start new task
36
- const brainstorm = getBrainstorm(id);
37
- if (!brainstorm) {
38
- return new Response('Project not initialized', { status: 400 });
39
- }
40
-
41
- const hasContent = brainstorm.content.trim();
42
- const hasContext = !!getProjectContextSummary(id);
43
-
44
- if (!hasContent && !hasContext) {
45
- return new Response('No content to structure', { status: 400 });
46
- }
47
-
48
- // User-provided project description from scan panel
49
- const userDescription = request.nextUrl.searchParams.get('desc') || '';
50
-
51
- let content = hasContent
52
- ? brainstorm.content
53
- : '프로젝트 스캔 결과를 분석하여 현재 프로젝트의 구조, 진행 상황, TODO 항목을 파악해주세요.';
54
-
55
- if (userDescription) {
56
- content = `[사용자가 제공한 프로젝트 설명]\n${userDescription}\n\n${content}`;
57
- }
58
-
59
- const brainstormId = brainstorm.id;
60
-
61
- // Start background task
62
- startTask(id);
63
-
64
- const send = async (event: string, data: unknown) => {
65
- addTaskEvent(id, event, data);
66
- if (event === 'done') {
67
- finishTask(id, data);
68
- }
69
- };
70
-
71
- // Run structuring in background (detached from stream)
72
- (async () => {
73
- try {
74
- await structureWithChatDirect(id, brainstormId, content, send);
75
- } catch (error) {
76
- const msg = error instanceof Error ? error.message : 'Structure failed';
77
- addTaskEvent(id, 'error', { error: msg });
78
- failTask(id, msg);
79
- }
80
- })();
81
-
82
- // Stream events to this client
83
- return createReconnectStream(id, getTask(id)!);
84
- }
85
-
86
- function createReconnectStream(projectId: string, task: ReturnType<typeof getTask>) {
87
- const encoder = new TextEncoder();
88
- let unsubscribe: (() => void) | null = null;
89
-
90
- const stream = new ReadableStream({
91
- start(controller) {
92
- const send = (event: string, data: unknown) => {
93
- try {
94
- controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
95
- } catch {
96
- // controller may be closed
97
- }
98
- };
99
-
100
- // Replay past events
101
- if (task) {
102
- for (const ev of task.events) {
103
- send(ev.event, ev.data);
104
- }
105
-
106
- // If already finished, close immediately
107
- if (task.status !== 'running') {
108
- controller.close();
109
- return;
110
- }
111
- }
112
-
113
- // Listen for new events
114
- unsubscribe = addTaskListener(projectId, (event, data) => {
115
- send(event, data);
116
- if (event === 'done' || event === 'error') {
117
- try { controller.close(); } catch { /* already closed */ }
118
- unsubscribe?.();
119
- }
120
- });
121
- },
122
- cancel() {
123
- unsubscribe?.();
124
- },
125
- });
126
-
127
- return new Response(stream, {
128
- headers: {
129
- 'Content-Type': 'text/event-stream',
130
- 'Cache-Control': 'no-cache',
131
- 'Connection': 'keep-alive',
132
- 'X-Accel-Buffering': 'no',
133
- },
134
- });
135
- }
136
-
137
- function createReplayStream(task: NonNullable<ReturnType<typeof getTask>>) {
138
- const encoder = new TextEncoder();
139
- const stream = new ReadableStream({
140
- start(controller) {
141
- for (const ev of task.events) {
142
- try {
143
- controller.enqueue(encoder.encode(`event: ${ev.event}\ndata: ${JSON.stringify(ev.data)}\n\n`));
144
- } catch { break; }
145
- }
146
- controller.close();
147
- },
148
- });
149
-
150
- return new Response(stream, {
151
- headers: {
152
- 'Content-Type': 'text/event-stream',
153
- 'Cache-Control': 'no-cache',
154
- 'Connection': 'keep-alive',
155
- },
156
- });
157
- }