mindpm 1.1.2 → 1.2.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/LICENSE +201 -201
- package/README.md +168 -168
- package/dist/index.js +324 -1
- package/dist/ui/assets/index-CLK3PAFj.js +2 -0
- package/dist/ui/assets/index-D3uZo-NO.css +1 -0
- package/dist/ui/index.html +13 -0
- package/package.json +51 -49
package/README.md
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
# mindpm
|
|
2
|
-
|
|
3
|
-
**Persistent project memory for LLMs.** Never re-explain your project again.
|
|
4
|
-
|
|
5
|
-
mindpm is an MCP (Model Context Protocol) server that gives LLMs a SQLite-backed brain for your projects. It tracks tasks, decisions, architecture notes, and session context — so every new conversation picks up exactly where you left off.
|
|
6
|
-
|
|
7
|
-
## The Problem
|
|
8
|
-
|
|
9
|
-
Every new LLM chat starts from zero:
|
|
10
|
-
|
|
11
|
-
- *"Let me remind you about my project..."*
|
|
12
|
-
- *"Last time we decided to use Redis for..."*
|
|
13
|
-
- *"Where did we leave off?"*
|
|
14
|
-
|
|
15
|
-
## The Solution
|
|
16
|
-
|
|
17
|
-
mindpm persists your project state in a local SQLite database. The LLM reads and writes to it via MCP tools. No chat history needed. No memory features needed.
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
You: "What should I work on next?"
|
|
21
|
-
LLM: [queries mindpm] "Last session you finished the auth refactor.
|
|
22
|
-
You have 3 high-priority tasks: rate limiting, API docs, and
|
|
23
|
-
the webhook retry bug. Rate limiting is unblocked — start there."
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## What It Tracks
|
|
27
|
-
|
|
28
|
-
- **Tasks** — status, priority, blockers, sub-tasks
|
|
29
|
-
- **Decisions** — what was decided, why, what alternatives were rejected
|
|
30
|
-
- **Notes** — architecture, bugs, ideas, research
|
|
31
|
-
- **Context** — key-value pairs (tech stack, conventions, config)
|
|
32
|
-
- **Sessions** — what was done, what's next
|
|
33
|
-
|
|
34
|
-
## Setup
|
|
35
|
-
|
|
36
|
-
### Install
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm install -g mindpm
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Or run from source:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
git clone https://github.com/umitkavala/mindpm.git
|
|
46
|
-
cd mindpm
|
|
47
|
-
npm install
|
|
48
|
-
npm run build
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Configure with Claude Code
|
|
52
|
-
|
|
53
|
-
Add to your MCP config (`~/.claude/claude_desktop_config.json` or similar):
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"mcpServers": {
|
|
58
|
-
"mindpm": {
|
|
59
|
-
"command": "mindpm",
|
|
60
|
-
"env": {
|
|
61
|
-
"MINDPM_DB_PATH": "~/.mindpm/memory.db"
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
If running from source, use the built file directly:
|
|
69
|
-
|
|
70
|
-
```json
|
|
71
|
-
{
|
|
72
|
-
"mcpServers": {
|
|
73
|
-
"mindpm": {
|
|
74
|
-
"command": "node",
|
|
75
|
-
"args": ["/path/to/mindpm/dist/index.js"],
|
|
76
|
-
"env": {
|
|
77
|
-
"MINDPM_DB_PATH": "~/.mindpm/memory.db"
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Start Using
|
|
85
|
-
|
|
86
|
-
That's it. The LLM now has access to mindpm tools. Just start talking about your projects.
|
|
87
|
-
|
|
88
|
-
## MCP Tools
|
|
89
|
-
|
|
90
|
-
### Projects
|
|
91
|
-
| Tool | Description |
|
|
92
|
-
|------|-------------|
|
|
93
|
-
| `create_project` | Create a new project |
|
|
94
|
-
| `list_projects` | List all projects |
|
|
95
|
-
| `get_project_status` | Full project overview |
|
|
96
|
-
|
|
97
|
-
### Tasks
|
|
98
|
-
| Tool | Description |
|
|
99
|
-
|------|-------------|
|
|
100
|
-
| `create_task` | Add a task |
|
|
101
|
-
| `update_task` | Update status, priority, etc. |
|
|
102
|
-
| `list_tasks` | List with filters |
|
|
103
|
-
| `get_task` | Full task detail with sub-tasks and notes |
|
|
104
|
-
| `get_next_tasks` | Smart: highest priority, unblocked |
|
|
105
|
-
|
|
106
|
-
### Decisions
|
|
107
|
-
| Tool | Description |
|
|
108
|
-
|------|-------------|
|
|
109
|
-
| `log_decision` | Record a decision with reasoning |
|
|
110
|
-
| `list_decisions` | Browse decision history |
|
|
111
|
-
|
|
112
|
-
### Notes & Context
|
|
113
|
-
| Tool | Description |
|
|
114
|
-
|------|-------------|
|
|
115
|
-
| `add_note` | Add a note (architecture, bug, idea, etc.) |
|
|
116
|
-
| `search_notes` | Full-text search |
|
|
117
|
-
| `set_context` | Store key-value context |
|
|
118
|
-
| `get_context` | Retrieve context |
|
|
119
|
-
|
|
120
|
-
### Sessions
|
|
121
|
-
| Tool | Description |
|
|
122
|
-
|------|-------------|
|
|
123
|
-
| `start_session` | Get full project context + last session's next steps |
|
|
124
|
-
| `end_session` | Record summary + what to do next time |
|
|
125
|
-
|
|
126
|
-
### Query
|
|
127
|
-
| Tool | Description |
|
|
128
|
-
|------|-------------|
|
|
129
|
-
| `query` | Read-only SQL against the database |
|
|
130
|
-
| `get_project_summary` | Tasks by status, blockers, recent activity |
|
|
131
|
-
| `get_blockers` | All blocked tasks with what's blocking them |
|
|
132
|
-
| `search` | Full-text search across everything |
|
|
133
|
-
|
|
134
|
-
## How It Works
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
┌─────────────┐ MCP ┌─────────┐ SQLite ┌──────────┐
|
|
138
|
-
│ Claude Code │ ◄──────────► │ mindpm │ ◄────────────► │ memory.db│
|
|
139
|
-
│ / Desktop │ tools │ server │ read/write │ │
|
|
140
|
-
└─────────────┘ └─────────┘ └──────────┘
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
1. You start a conversation and mention your project
|
|
144
|
-
2. The LLM calls `start_session` → gets full context
|
|
145
|
-
3. During the conversation, it creates tasks, logs decisions, adds notes
|
|
146
|
-
4. When you're done, it calls `end_session` → saves what's next
|
|
147
|
-
5. Next conversation: instant context, zero re-explanation
|
|
148
|
-
|
|
149
|
-
## Storage
|
|
150
|
-
|
|
151
|
-
Default: `~/.mindpm/memory.db`
|
|
152
|
-
|
|
153
|
-
Override with `MINDPM_DB_PATH` or `PROJECT_MEMORY_DB_PATH` environment variable.
|
|
154
|
-
|
|
155
|
-
Database and tables are created automatically on first run.
|
|
156
|
-
|
|
157
|
-
## Development
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
npm install
|
|
161
|
-
npm run build # Build with tsup
|
|
162
|
-
npm run typecheck # Type-check without emitting
|
|
163
|
-
npm run dev # Build in watch mode
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## License
|
|
167
|
-
|
|
168
|
-
MIT
|
|
1
|
+
# mindpm
|
|
2
|
+
|
|
3
|
+
**Persistent project memory for LLMs.** Never re-explain your project again.
|
|
4
|
+
|
|
5
|
+
mindpm is an MCP (Model Context Protocol) server that gives LLMs a SQLite-backed brain for your projects. It tracks tasks, decisions, architecture notes, and session context — so every new conversation picks up exactly where you left off.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Every new LLM chat starts from zero:
|
|
10
|
+
|
|
11
|
+
- *"Let me remind you about my project..."*
|
|
12
|
+
- *"Last time we decided to use Redis for..."*
|
|
13
|
+
- *"Where did we leave off?"*
|
|
14
|
+
|
|
15
|
+
## The Solution
|
|
16
|
+
|
|
17
|
+
mindpm persists your project state in a local SQLite database. The LLM reads and writes to it via MCP tools. No chat history needed. No memory features needed.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
You: "What should I work on next?"
|
|
21
|
+
LLM: [queries mindpm] "Last session you finished the auth refactor.
|
|
22
|
+
You have 3 high-priority tasks: rate limiting, API docs, and
|
|
23
|
+
the webhook retry bug. Rate limiting is unblocked — start there."
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What It Tracks
|
|
27
|
+
|
|
28
|
+
- **Tasks** — status, priority, blockers, sub-tasks
|
|
29
|
+
- **Decisions** — what was decided, why, what alternatives were rejected
|
|
30
|
+
- **Notes** — architecture, bugs, ideas, research
|
|
31
|
+
- **Context** — key-value pairs (tech stack, conventions, config)
|
|
32
|
+
- **Sessions** — what was done, what's next
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
### Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g mindpm
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or run from source:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/umitkavala/mindpm.git
|
|
46
|
+
cd mindpm
|
|
47
|
+
npm install
|
|
48
|
+
npm run build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Configure with Claude Code
|
|
52
|
+
|
|
53
|
+
Add to your MCP config (`~/.claude/claude_desktop_config.json` or similar):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"mindpm": {
|
|
59
|
+
"command": "mindpm",
|
|
60
|
+
"env": {
|
|
61
|
+
"MINDPM_DB_PATH": "~/.mindpm/memory.db"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If running from source, use the built file directly:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"mindpm": {
|
|
74
|
+
"command": "node",
|
|
75
|
+
"args": ["/path/to/mindpm/dist/index.js"],
|
|
76
|
+
"env": {
|
|
77
|
+
"MINDPM_DB_PATH": "~/.mindpm/memory.db"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Start Using
|
|
85
|
+
|
|
86
|
+
That's it. The LLM now has access to mindpm tools. Just start talking about your projects.
|
|
87
|
+
|
|
88
|
+
## MCP Tools
|
|
89
|
+
|
|
90
|
+
### Projects
|
|
91
|
+
| Tool | Description |
|
|
92
|
+
|------|-------------|
|
|
93
|
+
| `create_project` | Create a new project |
|
|
94
|
+
| `list_projects` | List all projects |
|
|
95
|
+
| `get_project_status` | Full project overview |
|
|
96
|
+
|
|
97
|
+
### Tasks
|
|
98
|
+
| Tool | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `create_task` | Add a task |
|
|
101
|
+
| `update_task` | Update status, priority, etc. |
|
|
102
|
+
| `list_tasks` | List with filters |
|
|
103
|
+
| `get_task` | Full task detail with sub-tasks and notes |
|
|
104
|
+
| `get_next_tasks` | Smart: highest priority, unblocked |
|
|
105
|
+
|
|
106
|
+
### Decisions
|
|
107
|
+
| Tool | Description |
|
|
108
|
+
|------|-------------|
|
|
109
|
+
| `log_decision` | Record a decision with reasoning |
|
|
110
|
+
| `list_decisions` | Browse decision history |
|
|
111
|
+
|
|
112
|
+
### Notes & Context
|
|
113
|
+
| Tool | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `add_note` | Add a note (architecture, bug, idea, etc.) |
|
|
116
|
+
| `search_notes` | Full-text search |
|
|
117
|
+
| `set_context` | Store key-value context |
|
|
118
|
+
| `get_context` | Retrieve context |
|
|
119
|
+
|
|
120
|
+
### Sessions
|
|
121
|
+
| Tool | Description |
|
|
122
|
+
|------|-------------|
|
|
123
|
+
| `start_session` | Get full project context + last session's next steps |
|
|
124
|
+
| `end_session` | Record summary + what to do next time |
|
|
125
|
+
|
|
126
|
+
### Query
|
|
127
|
+
| Tool | Description |
|
|
128
|
+
|------|-------------|
|
|
129
|
+
| `query` | Read-only SQL against the database |
|
|
130
|
+
| `get_project_summary` | Tasks by status, blockers, recent activity |
|
|
131
|
+
| `get_blockers` | All blocked tasks with what's blocking them |
|
|
132
|
+
| `search` | Full-text search across everything |
|
|
133
|
+
|
|
134
|
+
## How It Works
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
┌─────────────┐ MCP ┌─────────┐ SQLite ┌──────────┐
|
|
138
|
+
│ Claude Code │ ◄──────────► │ mindpm │ ◄────────────► │ memory.db│
|
|
139
|
+
│ / Desktop │ tools │ server │ read/write │ │
|
|
140
|
+
└─────────────┘ └─────────┘ └──────────┘
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
1. You start a conversation and mention your project
|
|
144
|
+
2. The LLM calls `start_session` → gets full context
|
|
145
|
+
3. During the conversation, it creates tasks, logs decisions, adds notes
|
|
146
|
+
4. When you're done, it calls `end_session` → saves what's next
|
|
147
|
+
5. Next conversation: instant context, zero re-explanation
|
|
148
|
+
|
|
149
|
+
## Storage
|
|
150
|
+
|
|
151
|
+
Default: `~/.mindpm/memory.db`
|
|
152
|
+
|
|
153
|
+
Override with `MINDPM_DB_PATH` or `PROJECT_MEMORY_DB_PATH` environment variable.
|
|
154
|
+
|
|
155
|
+
Database and tables are created automatically on first run.
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm install
|
|
161
|
+
npm run build # Build with tsup
|
|
162
|
+
npm run typecheck # Type-check without emitting
|
|
163
|
+
npm run dev # Build in watch mode
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -1002,11 +1002,328 @@ function registerQueryTools(server2) {
|
|
|
1002
1002
|
);
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
1005
|
+
// src/server/http.ts
|
|
1006
|
+
import { createServer } from "http";
|
|
1007
|
+
import { readFile } from "fs/promises";
|
|
1008
|
+
import { join, extname } from "path";
|
|
1009
|
+
import { fileURLToPath } from "url";
|
|
1010
|
+
import { dirname as dirname2 } from "path";
|
|
1011
|
+
|
|
1012
|
+
// src/server/routes.ts
|
|
1013
|
+
var listProjects = async (_req, res) => {
|
|
1014
|
+
const db2 = getDb();
|
|
1015
|
+
const url = new URL(_req.url || "/", "http://localhost");
|
|
1016
|
+
const status = url.searchParams.get("status");
|
|
1017
|
+
let rows;
|
|
1018
|
+
if (status) {
|
|
1019
|
+
rows = db2.prepare("SELECT * FROM projects WHERE status = ? ORDER BY updated_at DESC").all(status);
|
|
1020
|
+
} else {
|
|
1021
|
+
rows = db2.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all();
|
|
1022
|
+
}
|
|
1023
|
+
sendJson(res, 200, rows);
|
|
1024
|
+
};
|
|
1025
|
+
var getProject = async (_req, res, params) => {
|
|
1026
|
+
const db2 = getDb();
|
|
1027
|
+
const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
|
|
1028
|
+
if (!project) {
|
|
1029
|
+
sendJson(res, 404, { error: "Project not found" });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(params.id);
|
|
1033
|
+
sendJson(res, 200, { ...project, task_counts: taskCounts });
|
|
1034
|
+
};
|
|
1035
|
+
var updateProject = async (req, res, params) => {
|
|
1036
|
+
const db2 = getDb();
|
|
1037
|
+
const body = await parseBody(req);
|
|
1038
|
+
const existing = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
|
|
1039
|
+
if (!existing) {
|
|
1040
|
+
sendJson(res, 404, { error: "Project not found" });
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const updates = [];
|
|
1044
|
+
const sqlParams = [];
|
|
1045
|
+
if (body.name !== void 0) {
|
|
1046
|
+
updates.push("name = ?");
|
|
1047
|
+
sqlParams.push(body.name);
|
|
1048
|
+
}
|
|
1049
|
+
if (body.description !== void 0) {
|
|
1050
|
+
updates.push("description = ?");
|
|
1051
|
+
sqlParams.push(body.description);
|
|
1052
|
+
}
|
|
1053
|
+
if (body.status !== void 0) {
|
|
1054
|
+
updates.push("status = ?");
|
|
1055
|
+
sqlParams.push(body.status);
|
|
1056
|
+
}
|
|
1057
|
+
if (updates.length === 0) {
|
|
1058
|
+
sendJson(res, 400, { error: "No updates provided" });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
sqlParams.push(params.id);
|
|
1062
|
+
try {
|
|
1063
|
+
db2.prepare(`UPDATE projects SET ${updates.join(", ")} WHERE id = ?`).run(...sqlParams);
|
|
1064
|
+
} catch (e) {
|
|
1065
|
+
if (e.message?.includes("UNIQUE constraint failed")) {
|
|
1066
|
+
sendJson(res, 409, { error: "A project with that name already exists" });
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
throw e;
|
|
1070
|
+
}
|
|
1071
|
+
const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
|
|
1072
|
+
sendJson(res, 200, updated);
|
|
1073
|
+
};
|
|
1074
|
+
var listTasks = async (req, res, params) => {
|
|
1075
|
+
const db2 = getDb();
|
|
1076
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
1077
|
+
const includeDone = url.searchParams.get("include_done") === "true";
|
|
1078
|
+
let sql = "SELECT * FROM tasks WHERE project_id = ?";
|
|
1079
|
+
if (!includeDone) {
|
|
1080
|
+
sql += " AND status NOT IN ('done', 'cancelled')";
|
|
1081
|
+
}
|
|
1082
|
+
sql += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at DESC";
|
|
1083
|
+
const rows = db2.prepare(sql).all(params.pid);
|
|
1084
|
+
sendJson(res, 200, rows);
|
|
1085
|
+
};
|
|
1086
|
+
var createTask = async (req, res, params) => {
|
|
1087
|
+
const db2 = getDb();
|
|
1088
|
+
const body = await parseBody(req);
|
|
1089
|
+
if (!body.title || typeof body.title !== "string") {
|
|
1090
|
+
sendJson(res, 400, { error: "title is required" });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(params.pid);
|
|
1094
|
+
if (!project) {
|
|
1095
|
+
sendJson(res, 404, { error: "Project not found" });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const id = generateId();
|
|
1099
|
+
const priority = body.priority || "medium";
|
|
1100
|
+
const tags = Array.isArray(body.tags) ? JSON.stringify(body.tags) : null;
|
|
1101
|
+
db2.prepare(
|
|
1102
|
+
"INSERT INTO tasks (id, project_id, title, description, priority, tags, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
1103
|
+
).run(
|
|
1104
|
+
id,
|
|
1105
|
+
params.pid,
|
|
1106
|
+
body.title,
|
|
1107
|
+
body.description ?? null,
|
|
1108
|
+
priority,
|
|
1109
|
+
tags,
|
|
1110
|
+
body.parent_task_id ?? null
|
|
1111
|
+
);
|
|
1112
|
+
const task = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
1113
|
+
sendJson(res, 201, task);
|
|
1114
|
+
};
|
|
1115
|
+
var updateTask = async (req, res, params) => {
|
|
1116
|
+
const db2 = getDb();
|
|
1117
|
+
const body = await parseBody(req);
|
|
1118
|
+
const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
|
|
1119
|
+
if (!existing) {
|
|
1120
|
+
sendJson(res, 404, { error: "Task not found" });
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const updates = [];
|
|
1124
|
+
const sqlParams = [];
|
|
1125
|
+
if (body.title !== void 0) {
|
|
1126
|
+
updates.push("title = ?");
|
|
1127
|
+
sqlParams.push(body.title);
|
|
1128
|
+
}
|
|
1129
|
+
if (body.description !== void 0) {
|
|
1130
|
+
updates.push("description = ?");
|
|
1131
|
+
sqlParams.push(body.description);
|
|
1132
|
+
}
|
|
1133
|
+
if (body.status !== void 0) {
|
|
1134
|
+
updates.push("status = ?");
|
|
1135
|
+
sqlParams.push(body.status);
|
|
1136
|
+
if (body.status === "done") {
|
|
1137
|
+
updates.push("completed_at = CURRENT_TIMESTAMP");
|
|
1138
|
+
} else {
|
|
1139
|
+
updates.push("completed_at = NULL");
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (body.priority !== void 0) {
|
|
1143
|
+
updates.push("priority = ?");
|
|
1144
|
+
sqlParams.push(body.priority);
|
|
1145
|
+
}
|
|
1146
|
+
if (body.tags !== void 0) {
|
|
1147
|
+
updates.push("tags = ?");
|
|
1148
|
+
sqlParams.push(Array.isArray(body.tags) ? JSON.stringify(body.tags) : null);
|
|
1149
|
+
}
|
|
1150
|
+
if (body.blocked_by !== void 0) {
|
|
1151
|
+
updates.push("blocked_by = ?");
|
|
1152
|
+
sqlParams.push(Array.isArray(body.blocked_by) ? JSON.stringify(body.blocked_by) : null);
|
|
1153
|
+
if (Array.isArray(body.blocked_by) && body.blocked_by.length > 0 && body.status === void 0) {
|
|
1154
|
+
updates.push("status = 'blocked'");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (updates.length === 0) {
|
|
1158
|
+
sendJson(res, 400, { error: "No updates provided" });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
sqlParams.push(params.id);
|
|
1162
|
+
db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...sqlParams);
|
|
1163
|
+
const updated = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
|
|
1164
|
+
sendJson(res, 200, updated);
|
|
1165
|
+
};
|
|
1166
|
+
var deleteTask = async (_req, res, params) => {
|
|
1167
|
+
const db2 = getDb();
|
|
1168
|
+
const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
|
|
1169
|
+
if (!existing) {
|
|
1170
|
+
sendJson(res, 404, { error: "Task not found" });
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const deleteTransaction = db2.transaction((taskId) => {
|
|
1174
|
+
const subtasks = db2.prepare("SELECT id FROM tasks WHERE parent_task_id = ?").all(taskId);
|
|
1175
|
+
for (const sub of subtasks) {
|
|
1176
|
+
db2.prepare("DELETE FROM notes WHERE task_id = ?").run(sub.id);
|
|
1177
|
+
db2.prepare("DELETE FROM tasks WHERE id = ?").run(sub.id);
|
|
1178
|
+
}
|
|
1179
|
+
db2.prepare("DELETE FROM notes WHERE task_id = ?").run(taskId);
|
|
1180
|
+
db2.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
|
|
1181
|
+
});
|
|
1182
|
+
deleteTransaction(params.id);
|
|
1183
|
+
sendJson(res, 200, { message: "Task deleted" });
|
|
1184
|
+
};
|
|
1185
|
+
var routes = [
|
|
1186
|
+
{ method: "GET", pattern: "/api/projects", handler: listProjects },
|
|
1187
|
+
{ method: "GET", pattern: "/api/projects/:id", handler: getProject },
|
|
1188
|
+
{ method: "PATCH", pattern: "/api/projects/:id", handler: updateProject },
|
|
1189
|
+
{ method: "GET", pattern: "/api/projects/:pid/tasks", handler: listTasks },
|
|
1190
|
+
{ method: "POST", pattern: "/api/projects/:pid/tasks", handler: createTask },
|
|
1191
|
+
{ method: "PATCH", pattern: "/api/tasks/:id", handler: updateTask },
|
|
1192
|
+
{ method: "DELETE", pattern: "/api/tasks/:id", handler: deleteTask }
|
|
1193
|
+
];
|
|
1194
|
+
async function handleApiRequest(req, res) {
|
|
1195
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
1196
|
+
const method = req.method || "GET";
|
|
1197
|
+
for (const route of routes) {
|
|
1198
|
+
if (route.method !== method) continue;
|
|
1199
|
+
const params = matchRoute(route.pattern, url.pathname);
|
|
1200
|
+
if (params) {
|
|
1201
|
+
await route.handler(req, res, params);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
sendJson(res, 404, { error: "Not found" });
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/server/http.ts
|
|
1209
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1210
|
+
var __dirname = dirname2(__filename);
|
|
1211
|
+
function resolveStaticDir() {
|
|
1212
|
+
const scriptDir = dirname2(process.argv[1] || __filename);
|
|
1213
|
+
return join(scriptDir, "ui");
|
|
1214
|
+
}
|
|
1215
|
+
var MIME_TYPES = {
|
|
1216
|
+
".html": "text/html; charset=utf-8",
|
|
1217
|
+
".js": "application/javascript; charset=utf-8",
|
|
1218
|
+
".css": "text/css; charset=utf-8",
|
|
1219
|
+
".json": "application/json; charset=utf-8",
|
|
1220
|
+
".svg": "image/svg+xml",
|
|
1221
|
+
".png": "image/png",
|
|
1222
|
+
".jpg": "image/jpeg",
|
|
1223
|
+
".ico": "image/x-icon",
|
|
1224
|
+
".woff": "font/woff",
|
|
1225
|
+
".woff2": "font/woff2",
|
|
1226
|
+
".ttf": "font/ttf"
|
|
1227
|
+
};
|
|
1228
|
+
function parseBody(req) {
|
|
1229
|
+
return new Promise((resolve2, reject) => {
|
|
1230
|
+
const chunks = [];
|
|
1231
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1232
|
+
req.on("end", () => {
|
|
1233
|
+
if (chunks.length === 0) {
|
|
1234
|
+
resolve2({});
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
resolve2(JSON.parse(Buffer.concat(chunks).toString()));
|
|
1239
|
+
} catch {
|
|
1240
|
+
resolve2({});
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
req.on("error", reject);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
function matchRoute(pattern, pathname) {
|
|
1247
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
1248
|
+
const pathParts = pathname.split("/").filter(Boolean);
|
|
1249
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
1250
|
+
const params = {};
|
|
1251
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
1252
|
+
if (patternParts[i].startsWith(":")) {
|
|
1253
|
+
params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
|
|
1254
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return params;
|
|
1259
|
+
}
|
|
1260
|
+
function sendJson(res, status, data) {
|
|
1261
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
1262
|
+
res.end(JSON.stringify(data));
|
|
1263
|
+
}
|
|
1264
|
+
async function serveStatic(req, res) {
|
|
1265
|
+
const staticDir = resolveStaticDir();
|
|
1266
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
1267
|
+
let filePath = join(staticDir, url.pathname === "/" ? "index.html" : url.pathname);
|
|
1268
|
+
try {
|
|
1269
|
+
const content = await readFile(filePath);
|
|
1270
|
+
const ext = extname(filePath);
|
|
1271
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
1272
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1273
|
+
res.end(content);
|
|
1274
|
+
} catch {
|
|
1275
|
+
try {
|
|
1276
|
+
const indexPath = join(staticDir, "index.html");
|
|
1277
|
+
const content = await readFile(indexPath);
|
|
1278
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1279
|
+
res.end(content);
|
|
1280
|
+
} catch {
|
|
1281
|
+
res.writeHead(503, { "Content-Type": "text/html; charset=utf-8" });
|
|
1282
|
+
res.end(
|
|
1283
|
+
"<html><body><h1>mindpm UI not built</h1><p>Run <code>npm run build:ui</code> to build the Kanban UI.</p></body></html>"
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function startHttpServer(port) {
|
|
1289
|
+
const server2 = createServer(async (req, res) => {
|
|
1290
|
+
try {
|
|
1291
|
+
if (req.url?.startsWith("/api/")) {
|
|
1292
|
+
await handleApiRequest(req, res);
|
|
1293
|
+
} else {
|
|
1294
|
+
await serveStatic(req, res);
|
|
1295
|
+
}
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
process.stderr.write(`[mindpm] HTTP error: ${err}
|
|
1298
|
+
`);
|
|
1299
|
+
if (!res.headersSent) {
|
|
1300
|
+
sendJson(res, 500, { error: "Internal server error" });
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
server2.on("error", (err) => {
|
|
1305
|
+
if (err.code === "EADDRINUSE") {
|
|
1306
|
+
process.stderr.write(
|
|
1307
|
+
`[mindpm] Warning: Port ${port} is in use. Kanban UI not available. MCP server continues.
|
|
1308
|
+
`
|
|
1309
|
+
);
|
|
1310
|
+
} else {
|
|
1311
|
+
process.stderr.write(`[mindpm] HTTP server error: ${err.message}
|
|
1312
|
+
`);
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
server2.listen(port, () => {
|
|
1316
|
+
process.stderr.write(`[mindpm] Kanban UI available at http://localhost:${port}
|
|
1317
|
+
`);
|
|
1318
|
+
});
|
|
1319
|
+
return server2;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1005
1322
|
// src/index.ts
|
|
1006
1323
|
var server = new McpServer(
|
|
1007
1324
|
{
|
|
1008
1325
|
name: "mindpm",
|
|
1009
|
-
version: "1.
|
|
1326
|
+
version: "1.2.0"
|
|
1010
1327
|
},
|
|
1011
1328
|
{
|
|
1012
1329
|
capabilities: {
|
|
@@ -1020,21 +1337,27 @@ registerDecisionTools(server);
|
|
|
1020
1337
|
registerNoteTools(server);
|
|
1021
1338
|
registerSessionTools(server);
|
|
1022
1339
|
registerQueryTools(server);
|
|
1340
|
+
var httpServer;
|
|
1023
1341
|
async function main() {
|
|
1024
1342
|
ensureDbDirectory();
|
|
1343
|
+
const port = parseInt(process.env.MINDPM_PORT || "3131", 10);
|
|
1344
|
+
httpServer = startHttpServer(port);
|
|
1025
1345
|
const transport = new StdioServerTransport();
|
|
1026
1346
|
await server.connect(transport);
|
|
1027
1347
|
}
|
|
1028
1348
|
main().catch((error) => {
|
|
1029
1349
|
console.error("Fatal error:", error);
|
|
1350
|
+
httpServer?.close();
|
|
1030
1351
|
closeDb();
|
|
1031
1352
|
process.exit(1);
|
|
1032
1353
|
});
|
|
1033
1354
|
process.on("SIGINT", () => {
|
|
1355
|
+
httpServer?.close();
|
|
1034
1356
|
closeDb();
|
|
1035
1357
|
process.exit(0);
|
|
1036
1358
|
});
|
|
1037
1359
|
process.on("SIGTERM", () => {
|
|
1360
|
+
httpServer?.close();
|
|
1038
1361
|
closeDb();
|
|
1039
1362
|
process.exit(0);
|
|
1040
1363
|
});
|