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
|
@@ -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(
|
|
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
|
+
}
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -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
|
}
|
package/src/lib/mcp/server.ts
CHANGED
|
@@ -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,
|
|
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: '
|
|
10
|
+
version: '2.0.0',
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
// Tool 1: list-projects
|
|
14
14
|
server.tool(
|
|
15
15
|
'list-projects',
|
|
16
|
-
'
|
|
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('
|
|
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
|
-
|
|
40
|
-
|
|
39
|
+
`Project: ${result.project.name}`,
|
|
40
|
+
`Description: ${result.project.description}`,
|
|
41
41
|
'',
|
|
42
|
-
|
|
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
|
-
|
|
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('
|
|
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: '
|
|
60
|
+
return { content: [{ type: 'text', text: 'No submitted tasks available.' }] };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const text =
|
|
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
|
-
{
|
|
73
|
-
async ({
|
|
74
|
-
const prompt = ctx.
|
|
75
|
-
if (!prompt) {
|
|
76
|
-
return { content: [{ type: 'text', text: '
|
|
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
|
-
'
|
|
85
|
+
'Update task status (idea, writing, submitted, testing, done, problem)',
|
|
86
86
|
{
|
|
87
|
-
|
|
88
|
-
status: z.enum(['
|
|
87
|
+
taskId: z.string().describe('Task ID'),
|
|
88
|
+
status: z.enum(['idea', 'writing', 'submitted', 'testing', 'done', 'problem']).describe('New status'),
|
|
89
89
|
},
|
|
90
|
-
async ({
|
|
91
|
-
const updated = ctx.
|
|
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}:
|
|
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
|
-
'
|
|
103
|
-
{
|
|
104
|
-
async ({
|
|
105
|
-
const updated = ctx.
|
|
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:
|
|
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
|
}
|