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.
Files changed (54) hide show
  1. package/README.md +36 -0
  2. package/bin/im.js +4 -0
  3. package/next.config.ts +8 -0
  4. package/package.json +55 -0
  5. package/postcss.config.mjs +7 -0
  6. package/public/file.svg +1 -0
  7. package/public/globe.svg +1 -0
  8. package/public/next.svg +1 -0
  9. package/public/vercel.svg +1 -0
  10. package/public/window.svg +1 -0
  11. package/src/app/api/health/route.ts +5 -0
  12. package/src/app/api/projects/[id]/brainstorm/route.ts +37 -0
  13. package/src/app/api/projects/[id]/conversations/route.ts +50 -0
  14. package/src/app/api/projects/[id]/items/[itemId]/prompt/route.ts +51 -0
  15. package/src/app/api/projects/[id]/items/[itemId]/route.ts +73 -0
  16. package/src/app/api/projects/[id]/items/route.ts +17 -0
  17. package/src/app/api/projects/[id]/memos/route.ts +18 -0
  18. package/src/app/api/projects/[id]/route.ts +39 -0
  19. package/src/app/api/projects/[id]/structure/route.ts +28 -0
  20. package/src/app/api/projects/route.ts +19 -0
  21. package/src/app/favicon.ico +0 -0
  22. package/src/app/globals.css +437 -0
  23. package/src/app/layout.tsx +42 -0
  24. package/src/app/page.tsx +175 -0
  25. package/src/app/projects/[id]/page.tsx +249 -0
  26. package/src/cli.ts +41 -0
  27. package/src/components/brainstorm/Editor.tsx +163 -0
  28. package/src/components/brainstorm/MemoPin.tsx +31 -0
  29. package/src/components/brainstorm/ResizeHandle.tsx +45 -0
  30. package/src/components/chat/ChatMessage.tsx +28 -0
  31. package/src/components/chat/ChatPanel.tsx +100 -0
  32. package/src/components/tree/ItemDetail.tsx +196 -0
  33. package/src/components/tree/LockToggle.tsx +23 -0
  34. package/src/components/tree/StatusBadge.tsx +32 -0
  35. package/src/components/tree/TreeNode.tsx +118 -0
  36. package/src/components/tree/TreeView.tsx +60 -0
  37. package/src/lib/ai/chat-responder.ts +69 -0
  38. package/src/lib/ai/client.ts +124 -0
  39. package/src/lib/ai/prompter.ts +83 -0
  40. package/src/lib/ai/structurer.ts +74 -0
  41. package/src/lib/db/index.ts +16 -0
  42. package/src/lib/db/queries/brainstorms.ts +26 -0
  43. package/src/lib/db/queries/conversations.ts +46 -0
  44. package/src/lib/db/queries/items.ts +147 -0
  45. package/src/lib/db/queries/memos.ts +66 -0
  46. package/src/lib/db/queries/projects.ts +53 -0
  47. package/src/lib/db/queries/prompts.ts +68 -0
  48. package/src/lib/db/schema.ts +78 -0
  49. package/src/lib/mcp/server.ts +117 -0
  50. package/src/lib/mcp/tools.ts +83 -0
  51. package/src/lib/utils/id.ts +5 -0
  52. package/src/lib/utils/paths.ts +16 -0
  53. package/src/types/index.ts +97 -0
  54. package/tsconfig.json +34 -0
@@ -0,0 +1,117 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { getNextTask, getProjectContext, formatItemForMcp, formatTreeForMcp } from '@/lib/mcp/tools';
5
+ import type { McpToolContext } from '@/lib/mcp/tools';
6
+
7
+ export async function startMcpServer(ctx: McpToolContext) {
8
+ const server = new McpServer({
9
+ name: 'idea-manager',
10
+ version: '1.0.0',
11
+ });
12
+
13
+ // Tool 1: list-projects
14
+ server.tool(
15
+ 'list-projects',
16
+ 'IM 프로젝트 목록 조회',
17
+ {},
18
+ async () => {
19
+ const projects = ctx.listProjects();
20
+ const text = projects.length === 0
21
+ ? '프로젝트가 없습니다.'
22
+ : projects.map(p => `[${p.id}] ${p.name} - ${p.description}`).join('\n');
23
+ return { content: [{ type: 'text', text }] };
24
+ }
25
+ );
26
+
27
+ // Tool 2: get-project-context
28
+ server.tool(
29
+ 'get-project-context',
30
+ '프로젝트 전체 구조와 상태 조회',
31
+ { projectId: z.string().describe('프로젝트 ID') },
32
+ async ({ projectId }) => {
33
+ const result = getProjectContext(ctx, projectId);
34
+ if (!result.project) {
35
+ return { content: [{ type: 'text', text: '프로젝트를 찾을 수 없습니다.' }] };
36
+ }
37
+
38
+ const lines = [
39
+ `프로젝트: ${result.project.name}`,
40
+ `설명: ${result.project.description}`,
41
+ '',
42
+ `전체: ${result.stats.total} | 대기: ${result.stats.pending} | 진행중: ${result.stats.inProgress} | 완료: ${result.stats.done} | 해제됨: ${result.stats.unlocked}`,
43
+ '',
44
+ '--- 구조 ---',
45
+ formatTreeForMcp(result.tree),
46
+ ];
47
+
48
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
49
+ }
50
+ );
51
+
52
+ // Tool 3: get-next-task
53
+ server.tool(
54
+ 'get-next-task',
55
+ '다음 실행 가능한 작업과 프롬프트 조회 (해제 + 대기 상태)',
56
+ { projectId: z.string().describe('프로젝트 ID') },
57
+ async ({ projectId }) => {
58
+ const result = getNextTask(ctx, projectId);
59
+ if (!result) {
60
+ return { content: [{ type: 'text', text: '실행 가능한 작업이 없습니다. 항목을 해제(unlock)하세요.' }] };
61
+ }
62
+
63
+ const text = formatItemForMcp(result.item, result.prompt);
64
+ return { content: [{ type: 'text', text }] };
65
+ }
66
+ );
67
+
68
+ // Tool 4: get-prompt
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에서 먼저 생성하세요.' }] };
77
+ }
78
+ return { content: [{ type: 'text', text: prompt.content }] };
79
+ }
80
+ );
81
+
82
+ // Tool 5: update-status
83
+ server.tool(
84
+ 'update-status',
85
+ '작업 상태 변경 (pending, in_progress, done)',
86
+ {
87
+ itemId: z.string().describe('항목 ID'),
88
+ status: z.enum(['pending', 'in_progress', 'done']).describe('새 상태'),
89
+ },
90
+ async ({ itemId, status }) => {
91
+ const updated = ctx.updateItem(itemId, { status });
92
+ if (!updated) {
93
+ return { content: [{ type: 'text', text: '항목을 찾을 수 없습니다.' }] };
94
+ }
95
+ return { content: [{ type: 'text', text: `${updated.title}: 상태가 '${status}'로 변경되었습니다.` }] };
96
+ }
97
+ );
98
+
99
+ // Tool 6: report-completion
100
+ server.tool(
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 });
106
+ if (!updated) {
107
+ return { content: [{ type: 'text', text: '항목을 찾을 수 없습니다.' }] };
108
+ }
109
+ return { content: [{ type: 'text', text: `✅ ${updated.title}: 완료 처리되었습니다. (자동 잠금)` }] };
110
+ }
111
+ );
112
+
113
+ // Start server with stdio transport
114
+ const transport = new StdioServerTransport();
115
+ await server.connect(transport);
116
+ console.error('IM MCP server running on stdio');
117
+ }
@@ -0,0 +1,83 @@
1
+ import type { IItemTree, IItem, IPrompt } from '@/types';
2
+
3
+ // Re-export query functions for MCP tools
4
+ // These are imported from DB queries when the MCP server initializes
5
+
6
+ export interface McpToolContext {
7
+ listProjects: () => { id: string; name: string; description: string; created_at: string; updated_at: string }[];
8
+ getProject: (id: string) => { id: string; name: string; description: string } | undefined;
9
+ getItemTree: (projectId: string) => IItemTree[];
10
+ getItems: (projectId: string) => IItem[];
11
+ getPrompt: (itemId: string) => IPrompt | undefined;
12
+ updateItem: (id: string, data: Record<string, unknown>) => IItem | undefined;
13
+ }
14
+
15
+ export function getNextTask(ctx: McpToolContext, projectId: string): {
16
+ item: IItem;
17
+ prompt?: IPrompt;
18
+ } | null {
19
+ const items = ctx.getItems(projectId);
20
+
21
+ // Find unlocked + pending items (ready to execute)
22
+ const ready = items.filter(i => !i.is_locked && i.status === 'pending');
23
+
24
+ if (ready.length === 0) return null;
25
+
26
+ // Sort by sort_order
27
+ ready.sort((a, b) => a.sort_order - b.sort_order);
28
+ const item = ready[0];
29
+ const prompt = ctx.getPrompt(item.id);
30
+
31
+ return { item, prompt };
32
+ }
33
+
34
+ export function getProjectContext(ctx: McpToolContext, projectId: string): {
35
+ project: { id: string; name: string; description: string } | undefined;
36
+ tree: IItemTree[];
37
+ stats: { total: number; pending: number; inProgress: number; done: number; unlocked: number };
38
+ } {
39
+ const project = ctx.getProject(projectId);
40
+ const tree = ctx.getItemTree(projectId);
41
+ const items = ctx.getItems(projectId);
42
+
43
+ const stats = {
44
+ total: items.length,
45
+ pending: items.filter(i => i.status === 'pending').length,
46
+ inProgress: items.filter(i => i.status === 'in_progress').length,
47
+ done: items.filter(i => i.status === 'done').length,
48
+ unlocked: items.filter(i => !i.is_locked).length,
49
+ };
50
+
51
+ return { project, tree, stats };
52
+ }
53
+
54
+ export function formatItemForMcp(item: IItem, prompt?: IPrompt): string {
55
+ const lines = [
56
+ `제목: ${item.title}`,
57
+ `설명: ${item.description}`,
58
+ `유형: ${item.item_type}`,
59
+ `우선순위: ${item.priority}`,
60
+ `상태: ${item.status}`,
61
+ `잠금: ${item.is_locked ? '잠금' : '해제'}`,
62
+ ];
63
+
64
+ if (prompt) {
65
+ lines.push('', '--- 프롬프트 ---', prompt.content);
66
+ }
67
+
68
+ return lines.join('\n');
69
+ }
70
+
71
+ export function formatTreeForMcp(tree: IItemTree[], indent = 0): string {
72
+ const lines: string[] = [];
73
+ for (const item of tree) {
74
+ const prefix = ' '.repeat(indent);
75
+ const lock = item.is_locked ? '🔐' : '🔓';
76
+ const status = item.status === 'done' ? '✅' : item.status === 'in_progress' ? '🔄' : '⏳';
77
+ lines.push(`${prefix}${lock} ${item.title} ${status}`);
78
+ if (item.children.length > 0) {
79
+ lines.push(formatTreeForMcp(item.children, indent + 1));
80
+ }
81
+ }
82
+ return lines.join('\n');
83
+ }
@@ -0,0 +1,5 @@
1
+ import { nanoid } from 'nanoid';
2
+
3
+ export function generateId(): string {
4
+ return nanoid(16);
5
+ }
@@ -0,0 +1,16 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import fs from 'fs';
4
+
5
+ const DATA_DIR = path.join(os.homedir(), '.idea-manager', 'data');
6
+
7
+ export function getDataDir(): string {
8
+ if (!fs.existsSync(DATA_DIR)) {
9
+ fs.mkdirSync(DATA_DIR, { recursive: true });
10
+ }
11
+ return DATA_DIR;
12
+ }
13
+
14
+ export function getDbPath(): string {
15
+ return path.join(getDataDir(), 'im.db');
16
+ }
@@ -0,0 +1,97 @@
1
+ export interface IProject {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ created_at: string;
6
+ updated_at: string;
7
+ }
8
+
9
+ export interface IBrainstorm {
10
+ id: string;
11
+ project_id: string;
12
+ content: string;
13
+ version: number;
14
+ created_at: string;
15
+ updated_at: string;
16
+ }
17
+
18
+ export type ItemType = 'feature' | 'task' | 'bug' | 'idea' | 'note';
19
+ export type ItemStatus = 'pending' | 'in_progress' | 'done';
20
+ export type ItemPriority = 'high' | 'medium' | 'low';
21
+
22
+ export interface IItem {
23
+ id: string;
24
+ project_id: string;
25
+ brainstorm_id: string | null;
26
+ parent_id: string | null;
27
+ title: string;
28
+ description: string;
29
+ item_type: ItemType;
30
+ priority: ItemPriority;
31
+ status: ItemStatus;
32
+ is_locked: boolean;
33
+ sort_order: number;
34
+ metadata: string | null;
35
+ created_at: string;
36
+ updated_at: string;
37
+ }
38
+
39
+ export interface IItemTree extends IItem {
40
+ children: IItemTree[];
41
+ }
42
+
43
+ export interface IStructureResult {
44
+ items: {
45
+ id?: string;
46
+ parent_id: string | null;
47
+ title: string;
48
+ description: string;
49
+ item_type: ItemType;
50
+ priority: ItemPriority;
51
+ children?: IStructureResult['items'];
52
+ }[];
53
+ }
54
+
55
+ export interface IConversation {
56
+ id: string;
57
+ project_id: string;
58
+ role: 'assistant' | 'user';
59
+ content: string;
60
+ metadata: string | null;
61
+ created_at: string;
62
+ }
63
+
64
+ export interface IMemo {
65
+ id: string;
66
+ project_id: string;
67
+ conversation_id: string | null;
68
+ anchor_text: string;
69
+ question: string;
70
+ is_resolved: boolean;
71
+ created_at: string;
72
+ updated_at: string;
73
+ }
74
+
75
+ export interface IPrompt {
76
+ id: string;
77
+ project_id: string;
78
+ item_id: string;
79
+ content: string;
80
+ prompt_type: 'auto' | 'manual';
81
+ version: number;
82
+ created_at: string;
83
+ }
84
+
85
+ export interface IStructureWithQuestions {
86
+ items: {
87
+ title: string;
88
+ description: string;
89
+ item_type: ItemType;
90
+ priority: ItemPriority;
91
+ children?: IStructureWithQuestions['items'];
92
+ }[];
93
+ questions: {
94
+ anchor_text: string;
95
+ question: string;
96
+ }[];
97
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }