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.
Files changed (60) hide show
  1. package/next.config.ts +0 -1
  2. package/package.json +2 -2
  3. package/{src/app/icon.svg → public/favicon.svg} +2 -2
  4. package/src/app/api/filesystem/route.ts +49 -0
  5. package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
  6. package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
  7. package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
  8. package/src/app/api/projects/[id]/items/route.ts +51 -1
  9. package/src/app/api/projects/[id]/scan/route.ts +73 -0
  10. package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
  11. package/src/app/api/projects/[id]/structure/route.ts +34 -3
  12. package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
  13. package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
  14. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
  15. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
  16. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
  17. package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
  18. package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
  19. package/src/app/api/projects/route.ts +1 -1
  20. package/src/app/globals.css +465 -5
  21. package/src/app/layout.tsx +3 -0
  22. package/src/app/page.tsx +260 -88
  23. package/src/app/projects/[id]/page.tsx +366 -183
  24. package/src/cli.ts +10 -10
  25. package/src/components/DirectoryPicker.tsx +137 -0
  26. package/src/components/ScanPanel.tsx +743 -0
  27. package/src/components/brainstorm/Editor.tsx +20 -4
  28. package/src/components/brainstorm/MemoPin.tsx +91 -5
  29. package/src/components/dashboard/SubProjectCard.tsx +76 -0
  30. package/src/components/dashboard/TabBar.tsx +42 -0
  31. package/src/components/task/ProjectTree.tsx +223 -0
  32. package/src/components/task/PromptEditor.tsx +107 -0
  33. package/src/components/task/StatusFlow.tsx +43 -0
  34. package/src/components/task/TaskChat.tsx +134 -0
  35. package/src/components/task/TaskDetail.tsx +205 -0
  36. package/src/components/task/TaskList.tsx +119 -0
  37. package/src/components/tree/CardView.tsx +206 -0
  38. package/src/components/tree/RefinePopover.tsx +157 -0
  39. package/src/components/tree/TreeNode.tsx +147 -38
  40. package/src/components/tree/TreeView.tsx +270 -26
  41. package/src/components/ui/ConfirmDialog.tsx +88 -0
  42. package/src/lib/ai/chat-responder.ts +4 -2
  43. package/src/lib/ai/cleanup.ts +87 -0
  44. package/src/lib/ai/client.ts +175 -58
  45. package/src/lib/ai/prompter.ts +19 -24
  46. package/src/lib/ai/refiner.ts +128 -0
  47. package/src/lib/ai/structurer.ts +340 -11
  48. package/src/lib/db/queries/context.ts +76 -0
  49. package/src/lib/db/queries/items.ts +133 -12
  50. package/src/lib/db/queries/projects.ts +12 -8
  51. package/src/lib/db/queries/sub-projects.ts +122 -0
  52. package/src/lib/db/queries/task-conversations.ts +27 -0
  53. package/src/lib/db/queries/task-prompts.ts +32 -0
  54. package/src/lib/db/queries/tasks.ts +133 -0
  55. package/src/lib/db/schema.ts +75 -0
  56. package/src/lib/mcp/server.ts +38 -39
  57. package/src/lib/mcp/tools.ts +47 -45
  58. package/src/lib/scanner.ts +573 -0
  59. package/src/lib/task-store.ts +97 -0
  60. package/src/types/index.ts +65 -0
@@ -12,14 +12,14 @@ export function getProject(id: string): IProject | undefined {
12
12
  return db.prepare('SELECT * FROM projects WHERE id = ?').get(id) as IProject | undefined;
13
13
  }
14
14
 
15
- export function createProject(name: string, description: string = ''): IProject {
15
+ export function createProject(name: string, description: string = '', projectPath?: string): IProject {
16
16
  const db = getDb();
17
17
  const id = generateId();
18
18
  const now = new Date().toISOString();
19
19
 
20
20
  db.prepare(
21
- 'INSERT INTO projects (id, name, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
22
- ).run(id, name, description, now, now);
21
+ 'INSERT INTO projects (id, name, description, project_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
22
+ ).run(id, name, description, projectPath ?? null, now, now);
23
23
 
24
24
  // Also create a default brainstorm
25
25
  const brainstormId = generateId();
@@ -30,18 +30,22 @@ export function createProject(name: string, description: string = ''): IProject
30
30
  return getProject(id)!;
31
31
  }
32
32
 
33
- export function updateProject(id: string, data: { name?: string; description?: string }): IProject | undefined {
33
+ export function updateProject(id: string, data: { name?: string; description?: string; project_path?: string | null }): IProject | undefined {
34
34
  const db = getDb();
35
35
  const project = getProject(id);
36
36
  if (!project) return undefined;
37
37
 
38
- const name = data.name ?? project.name;
39
- const description = data.description ?? project.description;
40
38
  const now = new Date().toISOString();
41
39
 
42
40
  db.prepare(
43
- 'UPDATE projects SET name = ?, description = ?, updated_at = ? WHERE id = ?'
44
- ).run(name, description, now, id);
41
+ 'UPDATE projects SET name = ?, description = ?, project_path = ?, updated_at = ? WHERE id = ?'
42
+ ).run(
43
+ data.name ?? project.name,
44
+ data.description ?? project.description,
45
+ data.project_path !== undefined ? data.project_path : project.project_path,
46
+ now,
47
+ id,
48
+ );
45
49
 
46
50
  return getProject(id);
47
51
  }
@@ -0,0 +1,122 @@
1
+ import { getDb } from '../index';
2
+ import { generateId } from '../../utils/id';
3
+ import type { ISubProject, ISubProjectWithStats, TaskStatus } from '@/types';
4
+
5
+ export function getSubProjects(projectId: string): ISubProject[] {
6
+ const db = getDb();
7
+ return db.prepare(
8
+ 'SELECT * FROM sub_projects WHERE project_id = ? ORDER BY sort_order ASC'
9
+ ).all(projectId) as ISubProject[];
10
+ }
11
+
12
+ export function getSubProject(id: string): ISubProject | undefined {
13
+ const db = getDb();
14
+ return db.prepare('SELECT * FROM sub_projects WHERE id = ?').get(id) as ISubProject | undefined;
15
+ }
16
+
17
+ export function getSubProjectsWithStats(projectId: string): ISubProjectWithStats[] {
18
+ const db = getDb();
19
+ const subProjects = db.prepare(
20
+ 'SELECT * FROM sub_projects WHERE project_id = ? ORDER BY sort_order ASC'
21
+ ).all(projectId) as ISubProject[];
22
+
23
+ return subProjects.map(sp => {
24
+ const stats = db.prepare(`
25
+ SELECT
26
+ COUNT(*) as task_count,
27
+ SUM(CASE WHEN status IN ('submitted','testing') THEN 1 ELSE 0 END) as active_count,
28
+ SUM(CASE WHEN status IN ('idea','writing') THEN 1 ELSE 0 END) as pending_count,
29
+ SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done_count,
30
+ SUM(CASE WHEN status = 'problem' THEN 1 ELSE 0 END) as problem_count,
31
+ MAX(updated_at) as last_activity
32
+ FROM tasks WHERE sub_project_id = ?
33
+ `).get(sp.id) as {
34
+ task_count: number;
35
+ active_count: number;
36
+ pending_count: number;
37
+ done_count: number;
38
+ problem_count: number;
39
+ last_activity: string | null;
40
+ };
41
+
42
+ const previewTasks = db.prepare(
43
+ `SELECT title, status FROM tasks WHERE sub_project_id = ?
44
+ ORDER BY CASE status
45
+ WHEN 'submitted' THEN 0 WHEN 'testing' THEN 1 WHEN 'writing' THEN 2
46
+ WHEN 'idea' THEN 3 WHEN 'problem' THEN 4 WHEN 'done' THEN 5
47
+ END, sort_order ASC LIMIT 5`
48
+ ).all(sp.id) as { title: string; status: TaskStatus }[];
49
+
50
+ return {
51
+ ...sp,
52
+ task_count: stats.task_count ?? 0,
53
+ active_count: stats.active_count ?? 0,
54
+ pending_count: stats.pending_count ?? 0,
55
+ done_count: stats.done_count ?? 0,
56
+ problem_count: stats.problem_count ?? 0,
57
+ last_activity: stats.last_activity,
58
+ preview_tasks: previewTasks,
59
+ };
60
+ });
61
+ }
62
+
63
+ export function createSubProject(data: {
64
+ project_id: string;
65
+ name: string;
66
+ description?: string;
67
+ folder_path?: string;
68
+ }): ISubProject {
69
+ const db = getDb();
70
+ const id = generateId();
71
+ const now = new Date().toISOString();
72
+
73
+ const maxOrder = db.prepare(
74
+ 'SELECT MAX(sort_order) as max_order FROM sub_projects WHERE project_id = ?'
75
+ ).get(data.project_id) as { max_order: number | null };
76
+ const sortOrder = (maxOrder?.max_order ?? -1) + 1;
77
+
78
+ db.prepare(
79
+ `INSERT INTO sub_projects (id, project_id, name, description, folder_path, sort_order, created_at, updated_at)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
81
+ ).run(id, data.project_id, data.name, data.description ?? '', data.folder_path ?? null, sortOrder, now, now);
82
+
83
+ return getSubProject(id)!;
84
+ }
85
+
86
+ export function updateSubProject(id: string, data: {
87
+ name?: string;
88
+ description?: string;
89
+ folder_path?: string | null;
90
+ }): ISubProject | undefined {
91
+ const db = getDb();
92
+ const sp = getSubProject(id);
93
+ if (!sp) return undefined;
94
+
95
+ const now = new Date().toISOString();
96
+ db.prepare(
97
+ 'UPDATE sub_projects SET name = ?, description = ?, folder_path = ?, updated_at = ? WHERE id = ?'
98
+ ).run(
99
+ data.name ?? sp.name,
100
+ data.description ?? sp.description,
101
+ data.folder_path !== undefined ? data.folder_path : sp.folder_path,
102
+ now,
103
+ id,
104
+ );
105
+
106
+ return getSubProject(id);
107
+ }
108
+
109
+ export function deleteSubProject(id: string): boolean {
110
+ const db = getDb();
111
+ const result = db.prepare('DELETE FROM sub_projects WHERE id = ?').run(id);
112
+ return result.changes > 0;
113
+ }
114
+
115
+ export function reorderSubProjects(projectId: string, orderedIds: string[]): void {
116
+ const db = getDb();
117
+ const stmt = db.prepare('UPDATE sub_projects SET sort_order = ? WHERE id = ? AND project_id = ?');
118
+ const tx = db.transaction(() => {
119
+ orderedIds.forEach((id, idx) => stmt.run(idx, id, projectId));
120
+ });
121
+ tx();
122
+ }
@@ -0,0 +1,27 @@
1
+ import { getDb } from '../index';
2
+ import { generateId } from '../../utils/id';
3
+ import type { ITaskConversation } from '@/types';
4
+
5
+ export function getTaskConversations(taskId: string): ITaskConversation[] {
6
+ const db = getDb();
7
+ return db.prepare(
8
+ 'SELECT * FROM task_conversations WHERE task_id = ? ORDER BY created_at ASC'
9
+ ).all(taskId) as ITaskConversation[];
10
+ }
11
+
12
+ export function addTaskConversation(taskId: string, role: 'assistant' | 'user', content: string): ITaskConversation {
13
+ const db = getDb();
14
+ const id = generateId();
15
+ const now = new Date().toISOString();
16
+
17
+ db.prepare(
18
+ 'INSERT INTO task_conversations (id, task_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)'
19
+ ).run(id, taskId, role, content, now);
20
+
21
+ return db.prepare('SELECT * FROM task_conversations WHERE id = ?').get(id) as ITaskConversation;
22
+ }
23
+
24
+ export function deleteTaskConversations(taskId: string): void {
25
+ const db = getDb();
26
+ db.prepare('DELETE FROM task_conversations WHERE task_id = ?').run(taskId);
27
+ }
@@ -0,0 +1,32 @@
1
+ import { getDb } from '../index';
2
+ import { generateId } from '../../utils/id';
3
+ import type { ITaskPrompt } from '@/types';
4
+
5
+ export function getTaskPrompt(taskId: string): ITaskPrompt | undefined {
6
+ const db = getDb();
7
+ return db.prepare('SELECT * FROM task_prompts WHERE task_id = ?').get(taskId) as ITaskPrompt | undefined;
8
+ }
9
+
10
+ export function upsertTaskPrompt(taskId: string, content: string, promptType: 'manual' | 'ai_assisted' = 'manual'): ITaskPrompt {
11
+ const db = getDb();
12
+ const existing = getTaskPrompt(taskId);
13
+ const now = new Date().toISOString();
14
+
15
+ if (existing) {
16
+ db.prepare(
17
+ 'UPDATE task_prompts SET content = ?, prompt_type = ?, updated_at = ? WHERE task_id = ?'
18
+ ).run(content, promptType, now, taskId);
19
+ } else {
20
+ const id = generateId();
21
+ db.prepare(
22
+ 'INSERT INTO task_prompts (id, task_id, content, prompt_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
23
+ ).run(id, taskId, content, promptType, now, now);
24
+ }
25
+
26
+ return getTaskPrompt(taskId)!;
27
+ }
28
+
29
+ export function deleteTaskPrompt(taskId: string): void {
30
+ const db = getDb();
31
+ db.prepare('DELETE FROM task_prompts WHERE task_id = ?').run(taskId);
32
+ }
@@ -0,0 +1,133 @@
1
+ import { getDb } from '../index';
2
+ import { generateId } from '../../utils/id';
3
+ import type { ITask, TaskStatus, ItemPriority } from '@/types';
4
+
5
+ interface TaskRow {
6
+ id: string;
7
+ project_id: string;
8
+ sub_project_id: string;
9
+ title: string;
10
+ description: string;
11
+ status: TaskStatus;
12
+ priority: ItemPriority;
13
+ is_today: number;
14
+ sort_order: number;
15
+ created_at: string;
16
+ updated_at: string;
17
+ }
18
+
19
+ function rowToTask(row: TaskRow): ITask {
20
+ return { ...row, is_today: row.is_today === 1 };
21
+ }
22
+
23
+ export function getTasks(subProjectId: string): ITask[] {
24
+ const db = getDb();
25
+ const rows = db.prepare(
26
+ 'SELECT * FROM tasks WHERE sub_project_id = ? ORDER BY sort_order ASC'
27
+ ).all(subProjectId) as TaskRow[];
28
+ return rows.map(rowToTask);
29
+ }
30
+
31
+ export function getTask(id: string): ITask | undefined {
32
+ const db = getDb();
33
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id) as TaskRow | undefined;
34
+ return row ? rowToTask(row) : undefined;
35
+ }
36
+
37
+ export function getTasksByProject(projectId: string): ITask[] {
38
+ const db = getDb();
39
+ const rows = db.prepare(
40
+ 'SELECT * FROM tasks WHERE project_id = ? ORDER BY sort_order ASC'
41
+ ).all(projectId) as TaskRow[];
42
+ return rows.map(rowToTask);
43
+ }
44
+
45
+ export function getTodayTasks(projectId: string): ITask[] {
46
+ const db = getDb();
47
+ const rows = db.prepare(
48
+ 'SELECT * FROM tasks WHERE project_id = ? AND is_today = 1 ORDER BY sort_order ASC'
49
+ ).all(projectId) as TaskRow[];
50
+ return rows.map(rowToTask);
51
+ }
52
+
53
+ export function getActiveTasks(projectId: string): ITask[] {
54
+ const db = getDb();
55
+ const rows = db.prepare(
56
+ "SELECT * FROM tasks WHERE project_id = ? AND status IN ('submitted','testing') ORDER BY sort_order ASC"
57
+ ).all(projectId) as TaskRow[];
58
+ return rows.map(rowToTask);
59
+ }
60
+
61
+ export function createTask(data: {
62
+ project_id: string;
63
+ sub_project_id: string;
64
+ title: string;
65
+ description?: string;
66
+ status?: TaskStatus;
67
+ priority?: ItemPriority;
68
+ }): ITask {
69
+ const db = getDb();
70
+ const id = generateId();
71
+ const now = new Date().toISOString();
72
+
73
+ const maxOrder = db.prepare(
74
+ 'SELECT MAX(sort_order) as max_order FROM tasks WHERE sub_project_id = ?'
75
+ ).get(data.sub_project_id) as { max_order: number | null };
76
+ const sortOrder = (maxOrder?.max_order ?? -1) + 1;
77
+
78
+ db.prepare(
79
+ `INSERT INTO tasks (id, project_id, sub_project_id, title, description, status, priority, is_today, sort_order, created_at, updated_at)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`
81
+ ).run(id, data.project_id, data.sub_project_id, data.title, data.description ?? '', data.status ?? 'idea', data.priority ?? 'medium', sortOrder, now, now);
82
+
83
+ return getTask(id)!;
84
+ }
85
+
86
+ export function updateTask(id: string, data: {
87
+ title?: string;
88
+ description?: string;
89
+ status?: TaskStatus;
90
+ priority?: ItemPriority;
91
+ is_today?: boolean;
92
+ sort_order?: number;
93
+ sub_project_id?: string;
94
+ }): ITask | undefined {
95
+ const db = getDb();
96
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id) as TaskRow | undefined;
97
+ if (!row) return undefined;
98
+
99
+ const now = new Date().toISOString();
100
+ db.prepare(`
101
+ UPDATE tasks SET
102
+ title = ?, description = ?, status = ?, priority = ?,
103
+ is_today = ?, sort_order = ?, sub_project_id = ?, updated_at = ?
104
+ WHERE id = ?
105
+ `).run(
106
+ data.title ?? row.title,
107
+ data.description ?? row.description,
108
+ data.status ?? row.status,
109
+ data.priority ?? row.priority,
110
+ data.is_today !== undefined ? (data.is_today ? 1 : 0) : row.is_today,
111
+ data.sort_order ?? row.sort_order,
112
+ data.sub_project_id ?? row.sub_project_id,
113
+ now,
114
+ id,
115
+ );
116
+
117
+ return getTask(id);
118
+ }
119
+
120
+ export function deleteTask(id: string): boolean {
121
+ const db = getDb();
122
+ const result = db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
123
+ return result.changes > 0;
124
+ }
125
+
126
+ export function reorderTasks(subProjectId: string, orderedIds: string[]): void {
127
+ const db = getDb();
128
+ const stmt = db.prepare('UPDATE tasks SET sort_order = ? WHERE id = ? AND sub_project_id = ?');
129
+ const tx = db.transaction(() => {
130
+ orderedIds.forEach((id, idx) => stmt.run(idx, id, subProjectId));
131
+ });
132
+ tx();
133
+ }
@@ -6,6 +6,7 @@ export function initSchema(db: Database.Database): void {
6
6
  id TEXT PRIMARY KEY,
7
7
  name TEXT NOT NULL,
8
8
  description TEXT NOT NULL DEFAULT '',
9
+ project_path TEXT,
9
10
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
10
11
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
11
12
  );
@@ -31,6 +32,7 @@ export function initSchema(db: Database.Database): void {
31
32
  priority TEXT NOT NULL DEFAULT 'medium',
32
33
  status TEXT NOT NULL DEFAULT 'pending',
33
34
  is_locked INTEGER NOT NULL DEFAULT 1,
35
+ is_pinned INTEGER NOT NULL DEFAULT 1,
34
36
  sort_order INTEGER NOT NULL DEFAULT 0,
35
37
  metadata TEXT,
36
38
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
@@ -74,5 +76,78 @@ export function initSchema(db: Database.Database): void {
74
76
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
75
77
  FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
76
78
  );
79
+
80
+ CREATE TABLE IF NOT EXISTS project_context (
81
+ id TEXT PRIMARY KEY,
82
+ project_id TEXT NOT NULL,
83
+ file_path TEXT NOT NULL,
84
+ content TEXT NOT NULL,
85
+ scanned_at TEXT NOT NULL DEFAULT (datetime('now')),
86
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
87
+ );
88
+ `);
89
+
90
+ // Migrations for existing DBs
91
+ const itemCols = db.prepare("PRAGMA table_info(items)").all() as { name: string }[];
92
+ if (!itemCols.some(c => c.name === 'is_pinned')) {
93
+ db.exec("ALTER TABLE items ADD COLUMN is_pinned INTEGER NOT NULL DEFAULT 1");
94
+ }
95
+
96
+ const projCols = db.prepare("PRAGMA table_info(projects)").all() as { name: string }[];
97
+ if (!projCols.some(c => c.name === 'project_path')) {
98
+ db.exec("ALTER TABLE projects ADD COLUMN project_path TEXT");
99
+ }
100
+
101
+ // v2 tables
102
+ db.exec(`
103
+ CREATE TABLE IF NOT EXISTS sub_projects (
104
+ id TEXT PRIMARY KEY,
105
+ project_id TEXT NOT NULL,
106
+ name TEXT NOT NULL,
107
+ description TEXT NOT NULL DEFAULT '',
108
+ folder_path TEXT,
109
+ sort_order INTEGER NOT NULL DEFAULT 0,
110
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
111
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
112
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
113
+ );
114
+
115
+ CREATE TABLE IF NOT EXISTS tasks (
116
+ id TEXT PRIMARY KEY,
117
+ project_id TEXT NOT NULL,
118
+ sub_project_id TEXT NOT NULL,
119
+ title TEXT NOT NULL,
120
+ description TEXT NOT NULL DEFAULT '',
121
+ status TEXT NOT NULL DEFAULT 'idea'
122
+ CHECK(status IN ('idea','writing','submitted','testing','done','problem')),
123
+ priority TEXT NOT NULL DEFAULT 'medium'
124
+ CHECK(priority IN ('high','medium','low')),
125
+ is_today INTEGER NOT NULL DEFAULT 0,
126
+ sort_order INTEGER NOT NULL DEFAULT 0,
127
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
128
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
129
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
130
+ FOREIGN KEY (sub_project_id) REFERENCES sub_projects(id) ON DELETE CASCADE
131
+ );
132
+
133
+ CREATE TABLE IF NOT EXISTS task_prompts (
134
+ id TEXT PRIMARY KEY,
135
+ task_id TEXT NOT NULL UNIQUE,
136
+ content TEXT NOT NULL DEFAULT '',
137
+ prompt_type TEXT NOT NULL DEFAULT 'manual'
138
+ CHECK(prompt_type IN ('manual','ai_assisted')),
139
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
140
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
141
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
142
+ );
143
+
144
+ CREATE TABLE IF NOT EXISTS task_conversations (
145
+ id TEXT PRIMARY KEY,
146
+ task_id TEXT NOT NULL,
147
+ role TEXT NOT NULL CHECK(role IN ('assistant','user')),
148
+ content TEXT NOT NULL,
149
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
150
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
151
+ );
77
152
  `);
78
153
  }
@@ -1,24 +1,24 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { z } from 'zod';
4
- import { getNextTask, getProjectContext, formatItemForMcp, formatTreeForMcp } from '@/lib/mcp/tools';
4
+ import { getNextTask, getProjectContext, formatTaskForMcp, formatProjectForMcp } from '@/lib/mcp/tools';
5
5
  import type { McpToolContext } from '@/lib/mcp/tools';
6
6
 
7
7
  export async function startMcpServer(ctx: McpToolContext) {
8
8
  const server = new McpServer({
9
9
  name: 'idea-manager',
10
- version: '1.0.0',
10
+ version: '2.0.0',
11
11
  });
12
12
 
13
13
  // Tool 1: list-projects
14
14
  server.tool(
15
15
  'list-projects',
16
- 'IM 프로젝트 목록 조회',
16
+ 'List all IM projects',
17
17
  {},
18
18
  async () => {
19
19
  const projects = ctx.listProjects();
20
20
  const text = projects.length === 0
21
- ? '프로젝트가 없습니다.'
21
+ ? 'No projects.'
22
22
  : projects.map(p => `[${p.id}] ${p.name} - ${p.description}`).join('\n');
23
23
  return { content: [{ type: 'text', text }] };
24
24
  }
@@ -27,22 +27,22 @@ export async function startMcpServer(ctx: McpToolContext) {
27
27
  // Tool 2: get-project-context
28
28
  server.tool(
29
29
  'get-project-context',
30
- '프로젝트 전체 구조와 상태 조회',
31
- { projectId: z.string().describe('프로젝트 ID') },
30
+ 'Get project structure: sub-projects, tasks, and stats',
31
+ { projectId: z.string().describe('Project ID') },
32
32
  async ({ projectId }) => {
33
33
  const result = getProjectContext(ctx, projectId);
34
34
  if (!result.project) {
35
- return { content: [{ type: 'text', text: '프로젝트를 찾을 수 없습니다.' }] };
35
+ return { content: [{ type: 'text', text: 'Project not found.' }] };
36
36
  }
37
37
 
38
38
  const lines = [
39
- `프로젝트: ${result.project.name}`,
40
- `설명: ${result.project.description}`,
39
+ `Project: ${result.project.name}`,
40
+ `Description: ${result.project.description}`,
41
41
  '',
42
- `전체: ${result.stats.total} | 대기: ${result.stats.pending} | 진행중: ${result.stats.inProgress} | 완료: ${result.stats.done} | 해제됨: ${result.stats.unlocked}`,
42
+ `Total: ${result.stats.total} | Submitted: ${result.stats.submitted} | Testing: ${result.stats.testing} | Done: ${result.stats.done} | Problem: ${result.stats.problem}`,
43
43
  '',
44
- '--- 구조 ---',
45
- formatTreeForMcp(result.tree),
44
+ '--- Structure ---',
45
+ formatProjectForMcp(result.subProjects, result.tasks),
46
46
  ];
47
47
 
48
48
  return { content: [{ type: 'text', text: lines.join('\n') }] };
@@ -52,28 +52,28 @@ export async function startMcpServer(ctx: McpToolContext) {
52
52
  // Tool 3: get-next-task
53
53
  server.tool(
54
54
  'get-next-task',
55
- '다음 실행 가능한 작업과 프롬프트 조회 (해제 + 대기 상태)',
56
- { projectId: z.string().describe('프로젝트 ID') },
55
+ 'Get next submitted task with its prompt',
56
+ { projectId: z.string().describe('Project ID') },
57
57
  async ({ projectId }) => {
58
58
  const result = getNextTask(ctx, projectId);
59
59
  if (!result) {
60
- return { content: [{ type: 'text', text: '실행 가능한 작업이 없습니다. 항목을 해제(unlock)하세요.' }] };
60
+ return { content: [{ type: 'text', text: 'No submitted tasks available.' }] };
61
61
  }
62
62
 
63
- const text = formatItemForMcp(result.item, result.prompt);
63
+ const text = formatTaskForMcp(result.task, result.prompt);
64
64
  return { content: [{ type: 'text', text }] };
65
65
  }
66
66
  );
67
67
 
68
- // Tool 4: get-prompt
68
+ // Tool 4: get-task-prompt
69
69
  server.tool(
70
- 'get-prompt',
71
- '특정 항목의 프롬프트 조회',
72
- { itemId: z.string().describe('항목 ID') },
73
- async ({ itemId }) => {
74
- const prompt = ctx.getPrompt(itemId);
75
- if (!prompt) {
76
- return { content: [{ type: 'text', text: ' 항목에 프롬프트가 없습니다. 웹 UI에서 먼저 생성하세요.' }] };
70
+ 'get-task-prompt',
71
+ 'Get prompt for a specific task',
72
+ { taskId: z.string().describe('Task ID') },
73
+ async ({ taskId }) => {
74
+ const prompt = ctx.getTaskPrompt(taskId);
75
+ if (!prompt?.content) {
76
+ return { content: [{ type: 'text', text: 'No prompt for this task.' }] };
77
77
  }
78
78
  return { content: [{ type: 'text', text: prompt.content }] };
79
79
  }
@@ -82,36 +82,35 @@ export async function startMcpServer(ctx: McpToolContext) {
82
82
  // Tool 5: update-status
83
83
  server.tool(
84
84
  'update-status',
85
- '작업 상태 변경 (pending, in_progress, done)',
85
+ 'Update task status (idea, writing, submitted, testing, done, problem)',
86
86
  {
87
- itemId: z.string().describe('항목 ID'),
88
- status: z.enum(['pending', 'in_progress', 'done']).describe(' 상태'),
87
+ taskId: z.string().describe('Task ID'),
88
+ status: z.enum(['idea', 'writing', 'submitted', 'testing', 'done', 'problem']).describe('New status'),
89
89
  },
90
- async ({ itemId, status }) => {
91
- const updated = ctx.updateItem(itemId, { status });
90
+ async ({ taskId, status }) => {
91
+ const updated = ctx.updateTask(taskId, { status });
92
92
  if (!updated) {
93
- return { content: [{ type: 'text', text: '항목을 찾을 수 없습니다.' }] };
93
+ return { content: [{ type: 'text', text: 'Task not found.' }] };
94
94
  }
95
- return { content: [{ type: 'text', text: `${updated.title}: 상태가 '${status}' 변경되었습니다.` }] };
95
+ return { content: [{ type: 'text', text: `${updated.title}: status changed to '${status}'.` }] };
96
96
  }
97
97
  );
98
98
 
99
99
  // Tool 6: report-completion
100
100
  server.tool(
101
101
  'report-completion',
102
- '작업 완료 보고 (상태를 done으로 변경 + 자동 잠금)',
103
- { itemId: z.string().describe('항목 ID') },
104
- async ({ itemId }) => {
105
- const updated = ctx.updateItem(itemId, { status: 'done', is_locked: true });
102
+ 'Report task completion (sets status to done)',
103
+ { taskId: z.string().describe('Task ID') },
104
+ async ({ taskId }) => {
105
+ const updated = ctx.updateTask(taskId, { status: 'done' });
106
106
  if (!updated) {
107
- return { content: [{ type: 'text', text: '항목을 찾을 수 없습니다.' }] };
107
+ return { content: [{ type: 'text', text: 'Task not found.' }] };
108
108
  }
109
- return { content: [{ type: 'text', text: `✅ ${updated.title}: 완료 처리되었습니다. (자동 잠금)` }] };
109
+ return { content: [{ type: 'text', text: `${updated.title}: completed.` }] };
110
110
  }
111
111
  );
112
112
 
113
- // Start server with stdio transport
114
113
  const transport = new StdioServerTransport();
115
114
  await server.connect(transport);
116
- console.error('IM MCP server running on stdio');
115
+ console.error('IM MCP server v2 running on stdio');
117
116
  }