mindpm 1.2.0 → 1.2.1

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 (2) hide show
  1. package/dist/index.js +422 -402
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -705,302 +705,6 @@ function registerNoteTools(server2) {
705
705
 
706
706
  // src/tools/sessions.ts
707
707
  import { z as z5 } from "zod/v4";
708
- function getActivitySince(db2, projectId, cutoffTime) {
709
- return db2.prepare(`
710
- SELECT 'task_created' as type, id, title, created_at as timestamp
711
- FROM tasks
712
- WHERE project_id = ? AND created_at > ?
713
- UNION ALL
714
- SELECT 'task_updated' as type, id, title, updated_at as timestamp
715
- FROM tasks
716
- WHERE project_id = ? AND updated_at > ? AND updated_at != created_at
717
- UNION ALL
718
- SELECT 'decision' as type, id, title, created_at as timestamp
719
- FROM decisions
720
- WHERE project_id = ? AND created_at > ?
721
- UNION ALL
722
- SELECT 'note' as type, id, substr(content, 1, 80) as title, created_at as timestamp
723
- FROM notes
724
- WHERE project_id = ? AND created_at > ?
725
- ORDER BY timestamp DESC
726
- `).all(
727
- projectId,
728
- cutoffTime,
729
- projectId,
730
- cutoffTime,
731
- projectId,
732
- cutoffTime,
733
- projectId,
734
- cutoffTime
735
- );
736
- }
737
- function registerSessionTools(server2) {
738
- server2.registerTool(
739
- "start_session",
740
- {
741
- title: "Start Session",
742
- description: "Begin a work session for a project. Returns the full project overview including last session's next_steps, active tasks, blockers, and recent decisions. Call this at the start of every conversation.",
743
- inputSchema: {
744
- project: z5.string().optional().describe("Project name or ID")
745
- }
746
- },
747
- async ({ project }) => {
748
- const resolved = resolveProjectOrDefault(project);
749
- if (!resolved) {
750
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
751
- }
752
- const db2 = getDb();
753
- const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(resolved.id);
754
- let lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
755
- const cutoffTime = lastSession?.created_at ?? "1970-01-01";
756
- const recentActivity = getActivitySince(db2, resolved.id, cutoffTime);
757
- if (recentActivity.length > 0) {
758
- const taskIds = [...new Set(
759
- recentActivity.filter((a) => a.type === "task_created" || a.type === "task_updated").map((a) => a.id)
760
- )];
761
- const decisionIds = [...new Set(
762
- recentActivity.filter((a) => a.type === "decision").map((a) => a.id)
763
- )];
764
- const syntheticId = generateId();
765
- db2.prepare(
766
- `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made) VALUES (?, ?, ?, ?, ?)`
767
- ).run(
768
- syntheticId,
769
- resolved.id,
770
- `Auto-generated: ${recentActivity.length} activities since last session`,
771
- taskIds.length > 0 ? JSON.stringify(taskIds) : null,
772
- decisionIds.length > 0 ? JSON.stringify(decisionIds) : null
773
- );
774
- lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
775
- }
776
- const activeTasks = db2.prepare(
777
- `SELECT id, title, status, priority, tags FROM tasks
778
- WHERE project_id = ? AND status NOT IN ('done', 'cancelled')
779
- ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`
780
- ).all(resolved.id);
781
- const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
782
- const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(resolved.id);
783
- const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
784
- const contextItems = db2.prepare("SELECT key, value, category FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
785
- db2.prepare("UPDATE projects SET status = status WHERE id = ?").run(resolved.id);
786
- const result = {
787
- project: projectRow,
788
- last_session: lastSession ? {
789
- summary: lastSession.summary,
790
- next_steps: lastSession.next_steps,
791
- when: lastSession.created_at
792
- } : null,
793
- recent_activity: recentActivity.slice(0, 20),
794
- task_summary: taskCounts,
795
- active_tasks: activeTasks,
796
- blocked_tasks: blockedTasks,
797
- recent_decisions: recentDecisions,
798
- context: contextItems
799
- };
800
- return {
801
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
802
- };
803
- }
804
- );
805
- server2.registerTool(
806
- "end_session",
807
- {
808
- title: "End Session",
809
- description: "End a work session with a summary of what was accomplished and what to do next. Call this when the user is done working.",
810
- inputSchema: {
811
- project: z5.string().optional().describe("Project name or ID"),
812
- summary: z5.string().describe("Summary of what was accomplished this session"),
813
- tasks_worked_on: z5.array(z5.string()).optional().describe("Task IDs that were worked on"),
814
- decisions_made: z5.array(z5.string()).optional().describe("Decision IDs that were made"),
815
- next_steps: z5.string().optional().describe("What to do next time")
816
- }
817
- },
818
- async ({ project, summary, tasks_worked_on, decisions_made, next_steps }) => {
819
- const resolved = resolveProjectOrDefault(project);
820
- if (!resolved) {
821
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
822
- }
823
- const db2 = getDb();
824
- const id = generateId();
825
- db2.prepare(
826
- `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made, next_steps) VALUES (?, ?, ?, ?, ?, ?)`
827
- ).run(
828
- id,
829
- resolved.id,
830
- summary,
831
- tasks_worked_on ? JSON.stringify(tasks_worked_on) : null,
832
- decisions_made ? JSON.stringify(decisions_made) : null,
833
- next_steps ?? null
834
- );
835
- return {
836
- content: [{
837
- type: "text",
838
- text: JSON.stringify({ session_id: id, message: `Session ended for ${resolved.name}. Summary saved.` })
839
- }]
840
- };
841
- }
842
- );
843
- }
844
-
845
- // src/tools/queries.ts
846
- import { z as z6 } from "zod/v4";
847
- function registerQueryTools(server2) {
848
- server2.registerTool(
849
- "query",
850
- {
851
- title: "Query Database",
852
- description: "Execute a read-only SQL query against the database. Only SELECT statements are allowed. Use this for custom queries not covered by other tools.",
853
- inputSchema: {
854
- sql: z6.string().describe("SQL SELECT query to execute")
855
- }
856
- },
857
- async ({ sql }) => {
858
- const trimmed = sql.trim();
859
- if (!trimmed.toUpperCase().startsWith("SELECT")) {
860
- return { content: [{ type: "text", text: "Only SELECT queries are allowed." }], isError: true };
861
- }
862
- const db2 = getDb();
863
- try {
864
- const stmt = db2.prepare(trimmed);
865
- if (!stmt.reader) {
866
- return { content: [{ type: "text", text: "Only read-only queries are allowed." }], isError: true };
867
- }
868
- const rows = stmt.all();
869
- return {
870
- content: [{ type: "text", text: JSON.stringify({ rows, count: rows.length }, null, 2) }]
871
- };
872
- } catch (e) {
873
- return { content: [{ type: "text", text: `Query error: ${e.message}` }], isError: true };
874
- }
875
- }
876
- );
877
- server2.registerTool(
878
- "get_project_summary",
879
- {
880
- title: "Get Project Summary",
881
- description: "High-level summary of a project: total tasks by status, recent activity, open blockers, and upcoming priorities.",
882
- inputSchema: {
883
- project: z6.string().optional().describe("Project name or ID")
884
- }
885
- },
886
- async ({ project }) => {
887
- const resolved = resolveProjectOrDefault(project);
888
- if (!resolved) {
889
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
890
- }
891
- const db2 = getDb();
892
- const tasksByStatus = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
893
- const blockers = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
894
- const upcomingPriorities = db2.prepare(
895
- `SELECT id, title, priority, status FROM tasks
896
- WHERE project_id = ? AND status IN ('todo', 'in_progress')
897
- ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END
898
- LIMIT 10`
899
- ).all(resolved.id);
900
- const recentActivity = db2.prepare(
901
- `SELECT 'task' as type, title, updated_at FROM tasks WHERE project_id = ? AND updated_at > datetime('now', '-7 days')
902
- UNION ALL
903
- SELECT 'decision' as type, title, created_at as updated_at FROM decisions WHERE project_id = ? AND created_at > datetime('now', '-7 days')
904
- UNION ALL
905
- SELECT 'note' as type, substr(content, 1, 50) as title, created_at as updated_at FROM notes WHERE project_id = ? AND created_at > datetime('now', '-7 days')
906
- ORDER BY updated_at DESC
907
- LIMIT 20`
908
- ).all(resolved.id, resolved.id, resolved.id);
909
- const totalNotes = db2.prepare("SELECT COUNT(*) as count FROM notes WHERE project_id = ?").get(resolved.id);
910
- const totalDecisions = db2.prepare("SELECT COUNT(*) as count FROM decisions WHERE project_id = ?").get(resolved.id);
911
- const totalSessions = db2.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(resolved.id);
912
- return {
913
- content: [{
914
- type: "text",
915
- text: JSON.stringify(
916
- {
917
- project: resolved.name,
918
- tasks_by_status: tasksByStatus,
919
- blockers,
920
- upcoming_priorities: upcomingPriorities,
921
- recent_activity: recentActivity,
922
- totals: { notes: totalNotes.count, decisions: totalDecisions.count, sessions: totalSessions.count }
923
- },
924
- null,
925
- 2
926
- )
927
- }]
928
- };
929
- }
930
- );
931
- server2.registerTool(
932
- "get_blockers",
933
- {
934
- title: "Get Blockers",
935
- description: "List all blocked tasks with what's blocking them.",
936
- inputSchema: {
937
- project: z6.string().optional().describe("Project name or ID")
938
- }
939
- },
940
- async ({ project }) => {
941
- const resolved = resolveProjectOrDefault(project);
942
- if (!resolved) {
943
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
944
- }
945
- const db2 = getDb();
946
- const blockers = db2.prepare("SELECT * FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
947
- const enriched = blockers.map((task) => {
948
- let blockingTasks = [];
949
- if (task.blocked_by) {
950
- try {
951
- const ids = JSON.parse(task.blocked_by);
952
- blockingTasks = ids.map((id) => {
953
- const blocking = db2.prepare("SELECT id, title, status FROM tasks WHERE id = ?").get(id);
954
- return blocking ?? { id, title: "Unknown task", status: "unknown" };
955
- });
956
- } catch {
957
- }
958
- }
959
- return { ...task, blocking_tasks: blockingTasks };
960
- });
961
- return {
962
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2) }]
963
- };
964
- }
965
- );
966
- server2.registerTool(
967
- "search",
968
- {
969
- title: "Search Everything",
970
- description: "Full-text search across tasks, notes, and decisions for a project.",
971
- inputSchema: {
972
- project: z6.string().optional().describe("Project name or ID"),
973
- query: z6.string().describe("Search query")
974
- }
975
- },
976
- async ({ project, query }) => {
977
- const resolved = resolveProjectOrDefault(project);
978
- if (!resolved) {
979
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
980
- }
981
- const db2 = getDb();
982
- const pattern = `%${query}%`;
983
- const tasks = db2.prepare("SELECT id, title, description, status, priority, 'task' as type FROM tasks WHERE project_id = ? AND (title LIKE ? OR description LIKE ?)").all(resolved.id, pattern, pattern);
984
- const notes = db2.prepare("SELECT id, content, category, 'note' as type FROM notes WHERE project_id = ? AND content LIKE ?").all(resolved.id, pattern);
985
- const decisions = db2.prepare("SELECT id, title, decision, reasoning, 'decision' as type FROM decisions WHERE project_id = ? AND (title LIKE ? OR decision LIKE ? OR reasoning LIKE ?)").all(resolved.id, pattern, pattern, pattern);
986
- return {
987
- content: [{
988
- type: "text",
989
- text: JSON.stringify(
990
- {
991
- project: resolved.name,
992
- query,
993
- results: { tasks, notes, decisions },
994
- total: tasks.length + notes.length + decisions.length
995
- },
996
- null,
997
- 2
998
- )
999
- }]
1000
- };
1001
- }
1002
- );
1003
- }
1004
708
 
1005
709
  // src/server/http.ts
1006
710
  import { createServer } from "http";
@@ -1008,6 +712,7 @@ import { readFile } from "fs/promises";
1008
712
  import { join, extname } from "path";
1009
713
  import { fileURLToPath } from "url";
1010
714
  import { dirname as dirname2 } from "path";
715
+ import { spawn } from "child_process";
1011
716
 
1012
717
  // src/server/routes.ts
1013
718
  var listProjects = async (_req, res) => {
@@ -1201,129 +906,444 @@ async function handleApiRequest(req, res) {
1201
906
  await route.handler(req, res, params);
1202
907
  return;
1203
908
  }
1204
- }
1205
- sendJson(res, 404, { error: "Not found" });
909
+ }
910
+ sendJson(res, 404, { error: "Not found" });
911
+ }
912
+
913
+ // src/server/http.ts
914
+ function openBrowser(url) {
915
+ const platform = process.platform;
916
+ const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
917
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
918
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
919
+ }
920
+ var __filename = fileURLToPath(import.meta.url);
921
+ var __dirname = dirname2(__filename);
922
+ function resolveStaticDir() {
923
+ const scriptDir = dirname2(process.argv[1] || __filename);
924
+ return join(scriptDir, "ui");
925
+ }
926
+ var MIME_TYPES = {
927
+ ".html": "text/html; charset=utf-8",
928
+ ".js": "application/javascript; charset=utf-8",
929
+ ".css": "text/css; charset=utf-8",
930
+ ".json": "application/json; charset=utf-8",
931
+ ".svg": "image/svg+xml",
932
+ ".png": "image/png",
933
+ ".jpg": "image/jpeg",
934
+ ".ico": "image/x-icon",
935
+ ".woff": "font/woff",
936
+ ".woff2": "font/woff2",
937
+ ".ttf": "font/ttf"
938
+ };
939
+ function parseBody(req) {
940
+ return new Promise((resolve2, reject) => {
941
+ const chunks = [];
942
+ req.on("data", (chunk) => chunks.push(chunk));
943
+ req.on("end", () => {
944
+ if (chunks.length === 0) {
945
+ resolve2({});
946
+ return;
947
+ }
948
+ try {
949
+ resolve2(JSON.parse(Buffer.concat(chunks).toString()));
950
+ } catch {
951
+ resolve2({});
952
+ }
953
+ });
954
+ req.on("error", reject);
955
+ });
956
+ }
957
+ function matchRoute(pattern, pathname) {
958
+ const patternParts = pattern.split("/").filter(Boolean);
959
+ const pathParts = pathname.split("/").filter(Boolean);
960
+ if (patternParts.length !== pathParts.length) return null;
961
+ const params = {};
962
+ for (let i = 0; i < patternParts.length; i++) {
963
+ if (patternParts[i].startsWith(":")) {
964
+ params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
965
+ } else if (patternParts[i] !== pathParts[i]) {
966
+ return null;
967
+ }
968
+ }
969
+ return params;
970
+ }
971
+ function sendJson(res, status, data) {
972
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
973
+ res.end(JSON.stringify(data));
974
+ }
975
+ async function serveStatic(req, res) {
976
+ const staticDir = resolveStaticDir();
977
+ const url = new URL(req.url || "/", "http://localhost");
978
+ let filePath = join(staticDir, url.pathname === "/" ? "index.html" : url.pathname);
979
+ try {
980
+ const content = await readFile(filePath);
981
+ const ext = extname(filePath);
982
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
983
+ res.writeHead(200, { "Content-Type": contentType });
984
+ res.end(content);
985
+ } catch {
986
+ try {
987
+ const indexPath = join(staticDir, "index.html");
988
+ const content = await readFile(indexPath);
989
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
990
+ res.end(content);
991
+ } catch {
992
+ res.writeHead(503, { "Content-Type": "text/html; charset=utf-8" });
993
+ res.end(
994
+ "<html><body><h1>mindpm UI not built</h1><p>Run <code>npm run build:ui</code> to build the Kanban UI.</p></body></html>"
995
+ );
996
+ }
997
+ }
998
+ }
999
+ var _httpPort = null;
1000
+ function getHttpPort() {
1001
+ return _httpPort;
1002
+ }
1003
+ function startHttpServer(port) {
1004
+ const server2 = createServer(async (req, res) => {
1005
+ try {
1006
+ if (req.url?.startsWith("/api/")) {
1007
+ await handleApiRequest(req, res);
1008
+ } else {
1009
+ await serveStatic(req, res);
1010
+ }
1011
+ } catch (err) {
1012
+ process.stderr.write(`[mindpm] HTTP error: ${err}
1013
+ `);
1014
+ if (!res.headersSent) {
1015
+ sendJson(res, 500, { error: "Internal server error" });
1016
+ }
1017
+ }
1018
+ });
1019
+ server2.on("error", (err) => {
1020
+ if (err.code === "EADDRINUSE") {
1021
+ process.stderr.write(
1022
+ `[mindpm] Warning: Port ${port} is in use. Kanban UI not available. MCP server continues.
1023
+ `
1024
+ );
1025
+ } else {
1026
+ process.stderr.write(`[mindpm] HTTP server error: ${err.message}
1027
+ `);
1028
+ }
1029
+ });
1030
+ server2.listen(port, () => {
1031
+ _httpPort = port;
1032
+ const url = `http://localhost:${port}`;
1033
+ process.stderr.write(`[mindpm] Kanban UI available at ${url}
1034
+ `);
1035
+ if (process.env.MINDPM_OPEN_BROWSER === "1") {
1036
+ openBrowser(url);
1037
+ }
1038
+ });
1039
+ return server2;
1040
+ }
1041
+
1042
+ // src/tools/sessions.ts
1043
+ function getActivitySince(db2, projectId, cutoffTime) {
1044
+ return db2.prepare(`
1045
+ SELECT 'task_created' as type, id, title, created_at as timestamp
1046
+ FROM tasks
1047
+ WHERE project_id = ? AND created_at > ?
1048
+ UNION ALL
1049
+ SELECT 'task_updated' as type, id, title, updated_at as timestamp
1050
+ FROM tasks
1051
+ WHERE project_id = ? AND updated_at > ? AND updated_at != created_at
1052
+ UNION ALL
1053
+ SELECT 'decision' as type, id, title, created_at as timestamp
1054
+ FROM decisions
1055
+ WHERE project_id = ? AND created_at > ?
1056
+ UNION ALL
1057
+ SELECT 'note' as type, id, substr(content, 1, 80) as title, created_at as timestamp
1058
+ FROM notes
1059
+ WHERE project_id = ? AND created_at > ?
1060
+ ORDER BY timestamp DESC
1061
+ `).all(
1062
+ projectId,
1063
+ cutoffTime,
1064
+ projectId,
1065
+ cutoffTime,
1066
+ projectId,
1067
+ cutoffTime,
1068
+ projectId,
1069
+ cutoffTime
1070
+ );
1071
+ }
1072
+ function registerSessionTools(server2) {
1073
+ server2.registerTool(
1074
+ "start_session",
1075
+ {
1076
+ title: "Start Session",
1077
+ description: "Begin a work session for a project. Returns the full project overview including last session's next_steps, active tasks, blockers, and recent decisions. Call this at the start of every conversation. IMPORTANT: Always show the kanban_url to the user as a clickable link so they can open the Kanban board.",
1078
+ inputSchema: {
1079
+ project: z5.string().optional().describe("Project name or ID")
1080
+ }
1081
+ },
1082
+ async ({ project }) => {
1083
+ const resolved = resolveProjectOrDefault(project);
1084
+ if (!resolved) {
1085
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1086
+ }
1087
+ const db2 = getDb();
1088
+ const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(resolved.id);
1089
+ let lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
1090
+ const cutoffTime = lastSession?.created_at ?? "1970-01-01";
1091
+ const recentActivity = getActivitySince(db2, resolved.id, cutoffTime);
1092
+ if (recentActivity.length > 0) {
1093
+ const taskIds = [...new Set(
1094
+ recentActivity.filter((a) => a.type === "task_created" || a.type === "task_updated").map((a) => a.id)
1095
+ )];
1096
+ const decisionIds = [...new Set(
1097
+ recentActivity.filter((a) => a.type === "decision").map((a) => a.id)
1098
+ )];
1099
+ const syntheticId = generateId();
1100
+ db2.prepare(
1101
+ `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made) VALUES (?, ?, ?, ?, ?)`
1102
+ ).run(
1103
+ syntheticId,
1104
+ resolved.id,
1105
+ `Auto-generated: ${recentActivity.length} activities since last session`,
1106
+ taskIds.length > 0 ? JSON.stringify(taskIds) : null,
1107
+ decisionIds.length > 0 ? JSON.stringify(decisionIds) : null
1108
+ );
1109
+ lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
1110
+ }
1111
+ const activeTasks = db2.prepare(
1112
+ `SELECT id, title, status, priority, tags FROM tasks
1113
+ WHERE project_id = ? AND status NOT IN ('done', 'cancelled')
1114
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`
1115
+ ).all(resolved.id);
1116
+ const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
1117
+ const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(resolved.id);
1118
+ const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
1119
+ const contextItems = db2.prepare("SELECT key, value, category FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
1120
+ db2.prepare("UPDATE projects SET status = status WHERE id = ?").run(resolved.id);
1121
+ const port = getHttpPort();
1122
+ const result = {
1123
+ kanban_url: port ? `http://localhost:${port}` : null,
1124
+ project: projectRow,
1125
+ last_session: lastSession ? {
1126
+ summary: lastSession.summary,
1127
+ next_steps: lastSession.next_steps,
1128
+ when: lastSession.created_at
1129
+ } : null,
1130
+ recent_activity: recentActivity.slice(0, 20),
1131
+ task_summary: taskCounts,
1132
+ active_tasks: activeTasks,
1133
+ blocked_tasks: blockedTasks,
1134
+ recent_decisions: recentDecisions,
1135
+ context: contextItems
1136
+ };
1137
+ return {
1138
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1139
+ };
1140
+ }
1141
+ );
1142
+ server2.registerTool(
1143
+ "end_session",
1144
+ {
1145
+ title: "End Session",
1146
+ description: "End a work session with a summary of what was accomplished and what to do next. Call this when the user is done working.",
1147
+ inputSchema: {
1148
+ project: z5.string().optional().describe("Project name or ID"),
1149
+ summary: z5.string().describe("Summary of what was accomplished this session"),
1150
+ tasks_worked_on: z5.array(z5.string()).optional().describe("Task IDs that were worked on"),
1151
+ decisions_made: z5.array(z5.string()).optional().describe("Decision IDs that were made"),
1152
+ next_steps: z5.string().optional().describe("What to do next time")
1153
+ }
1154
+ },
1155
+ async ({ project, summary, tasks_worked_on, decisions_made, next_steps }) => {
1156
+ const resolved = resolveProjectOrDefault(project);
1157
+ if (!resolved) {
1158
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1159
+ }
1160
+ const db2 = getDb();
1161
+ const id = generateId();
1162
+ db2.prepare(
1163
+ `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made, next_steps) VALUES (?, ?, ?, ?, ?, ?)`
1164
+ ).run(
1165
+ id,
1166
+ resolved.id,
1167
+ summary,
1168
+ tasks_worked_on ? JSON.stringify(tasks_worked_on) : null,
1169
+ decisions_made ? JSON.stringify(decisions_made) : null,
1170
+ next_steps ?? null
1171
+ );
1172
+ return {
1173
+ content: [{
1174
+ type: "text",
1175
+ text: JSON.stringify({ session_id: id, message: `Session ended for ${resolved.name}. Summary saved.` })
1176
+ }]
1177
+ };
1178
+ }
1179
+ );
1206
1180
  }
1207
1181
 
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;
1182
+ // src/tools/queries.ts
1183
+ import { z as z6 } from "zod/v4";
1184
+ function registerQueryTools(server2) {
1185
+ server2.registerTool(
1186
+ "query",
1187
+ {
1188
+ title: "Query Database",
1189
+ description: "Execute a read-only SQL query against the database. Only SELECT statements are allowed. Use this for custom queries not covered by other tools.",
1190
+ inputSchema: {
1191
+ sql: z6.string().describe("SQL SELECT query to execute")
1192
+ }
1193
+ },
1194
+ async ({ sql }) => {
1195
+ const trimmed = sql.trim();
1196
+ if (!trimmed.toUpperCase().startsWith("SELECT")) {
1197
+ return { content: [{ type: "text", text: "Only SELECT queries are allowed." }], isError: true };
1236
1198
  }
1199
+ const db2 = getDb();
1237
1200
  try {
1238
- resolve2(JSON.parse(Buffer.concat(chunks).toString()));
1239
- } catch {
1240
- resolve2({});
1201
+ const stmt = db2.prepare(trimmed);
1202
+ if (!stmt.reader) {
1203
+ return { content: [{ type: "text", text: "Only read-only queries are allowed." }], isError: true };
1204
+ }
1205
+ const rows = stmt.all();
1206
+ return {
1207
+ content: [{ type: "text", text: JSON.stringify({ rows, count: rows.length }, null, 2) }]
1208
+ };
1209
+ } catch (e) {
1210
+ return { content: [{ type: "text", text: `Query error: ${e.message}` }], isError: true };
1241
1211
  }
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
1212
  }
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
- );
1213
+ );
1214
+ server2.registerTool(
1215
+ "get_project_summary",
1216
+ {
1217
+ title: "Get Project Summary",
1218
+ description: "High-level summary of a project: total tasks by status, recent activity, open blockers, and upcoming priorities.",
1219
+ inputSchema: {
1220
+ project: z6.string().optional().describe("Project name or ID")
1221
+ }
1222
+ },
1223
+ async ({ project }) => {
1224
+ const resolved = resolveProjectOrDefault(project);
1225
+ if (!resolved) {
1226
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1227
+ }
1228
+ const db2 = getDb();
1229
+ const tasksByStatus = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
1230
+ const blockers = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
1231
+ const upcomingPriorities = db2.prepare(
1232
+ `SELECT id, title, priority, status FROM tasks
1233
+ WHERE project_id = ? AND status IN ('todo', 'in_progress')
1234
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END
1235
+ LIMIT 10`
1236
+ ).all(resolved.id);
1237
+ const recentActivity = db2.prepare(
1238
+ `SELECT 'task' as type, title, updated_at FROM tasks WHERE project_id = ? AND updated_at > datetime('now', '-7 days')
1239
+ UNION ALL
1240
+ SELECT 'decision' as type, title, created_at as updated_at FROM decisions WHERE project_id = ? AND created_at > datetime('now', '-7 days')
1241
+ UNION ALL
1242
+ SELECT 'note' as type, substr(content, 1, 50) as title, created_at as updated_at FROM notes WHERE project_id = ? AND created_at > datetime('now', '-7 days')
1243
+ ORDER BY updated_at DESC
1244
+ LIMIT 20`
1245
+ ).all(resolved.id, resolved.id, resolved.id);
1246
+ const totalNotes = db2.prepare("SELECT COUNT(*) as count FROM notes WHERE project_id = ?").get(resolved.id);
1247
+ const totalDecisions = db2.prepare("SELECT COUNT(*) as count FROM decisions WHERE project_id = ?").get(resolved.id);
1248
+ const totalSessions = db2.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(resolved.id);
1249
+ return {
1250
+ content: [{
1251
+ type: "text",
1252
+ text: JSON.stringify(
1253
+ {
1254
+ project: resolved.name,
1255
+ tasks_by_status: tasksByStatus,
1256
+ blockers,
1257
+ upcoming_priorities: upcomingPriorities,
1258
+ recent_activity: recentActivity,
1259
+ totals: { notes: totalNotes.count, decisions: totalDecisions.count, sessions: totalSessions.count }
1260
+ },
1261
+ null,
1262
+ 2
1263
+ )
1264
+ }]
1265
+ };
1285
1266
  }
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);
1267
+ );
1268
+ server2.registerTool(
1269
+ "get_blockers",
1270
+ {
1271
+ title: "Get Blockers",
1272
+ description: "List all blocked tasks with what's blocking them.",
1273
+ inputSchema: {
1274
+ project: z6.string().optional().describe("Project name or ID")
1295
1275
  }
1296
- } catch (err) {
1297
- process.stderr.write(`[mindpm] HTTP error: ${err}
1298
- `);
1299
- if (!res.headersSent) {
1300
- sendJson(res, 500, { error: "Internal server error" });
1276
+ },
1277
+ async ({ project }) => {
1278
+ const resolved = resolveProjectOrDefault(project);
1279
+ if (!resolved) {
1280
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1301
1281
  }
1282
+ const db2 = getDb();
1283
+ const blockers = db2.prepare("SELECT * FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
1284
+ const enriched = blockers.map((task) => {
1285
+ let blockingTasks = [];
1286
+ if (task.blocked_by) {
1287
+ try {
1288
+ const ids = JSON.parse(task.blocked_by);
1289
+ blockingTasks = ids.map((id) => {
1290
+ const blocking = db2.prepare("SELECT id, title, status FROM tasks WHERE id = ?").get(id);
1291
+ return blocking ?? { id, title: "Unknown task", status: "unknown" };
1292
+ });
1293
+ } catch {
1294
+ }
1295
+ }
1296
+ return { ...task, blocking_tasks: blockingTasks };
1297
+ });
1298
+ return {
1299
+ content: [{ type: "text", text: JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2) }]
1300
+ };
1302
1301
  }
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
- `);
1302
+ );
1303
+ server2.registerTool(
1304
+ "search",
1305
+ {
1306
+ title: "Search Everything",
1307
+ description: "Full-text search across tasks, notes, and decisions for a project.",
1308
+ inputSchema: {
1309
+ project: z6.string().optional().describe("Project name or ID"),
1310
+ query: z6.string().describe("Search query")
1311
+ }
1312
+ },
1313
+ async ({ project, query }) => {
1314
+ const resolved = resolveProjectOrDefault(project);
1315
+ if (!resolved) {
1316
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1317
+ }
1318
+ const db2 = getDb();
1319
+ const pattern = `%${query}%`;
1320
+ const tasks = db2.prepare("SELECT id, title, description, status, priority, 'task' as type FROM tasks WHERE project_id = ? AND (title LIKE ? OR description LIKE ?)").all(resolved.id, pattern, pattern);
1321
+ const notes = db2.prepare("SELECT id, content, category, 'note' as type FROM notes WHERE project_id = ? AND content LIKE ?").all(resolved.id, pattern);
1322
+ const decisions = db2.prepare("SELECT id, title, decision, reasoning, 'decision' as type FROM decisions WHERE project_id = ? AND (title LIKE ? OR decision LIKE ? OR reasoning LIKE ?)").all(resolved.id, pattern, pattern, pattern);
1323
+ return {
1324
+ content: [{
1325
+ type: "text",
1326
+ text: JSON.stringify(
1327
+ {
1328
+ project: resolved.name,
1329
+ query,
1330
+ results: { tasks, notes, decisions },
1331
+ total: tasks.length + notes.length + decisions.length
1332
+ },
1333
+ null,
1334
+ 2
1335
+ )
1336
+ }]
1337
+ };
1313
1338
  }
1314
- });
1315
- server2.listen(port, () => {
1316
- process.stderr.write(`[mindpm] Kanban UI available at http://localhost:${port}
1317
- `);
1318
- });
1319
- return server2;
1339
+ );
1320
1340
  }
1321
1341
 
1322
1342
  // src/index.ts
1323
1343
  var server = new McpServer(
1324
1344
  {
1325
1345
  name: "mindpm",
1326
- version: "1.2.0"
1346
+ version: "1.2.1"
1327
1347
  },
1328
1348
  {
1329
1349
  capabilities: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindpm",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Persistent project memory for LLMs via MCP. Never re-explain your project again.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",