mindpm 1.2.25 → 1.2.27

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/dist/index.js CHANGED
@@ -343,949 +343,1083 @@ function recordTaskHistory(taskId, event, oldValue, newValue) {
343
343
  ).run(generateId(), taskId, event, oldValue, newValue);
344
344
  }
345
345
 
346
- // src/tools/projects.ts
347
- function registerProjectTools(server2) {
348
- server2.registerTool(
349
- "create_project",
350
- {
351
- title: "Create Project",
352
- description: "Create a new project to track. Use this when starting a new project or when a user mentions a project that doesn't exist yet.",
353
- inputSchema: {
354
- name: z.string().describe("Project name (unique)"),
355
- description: z.string().optional().describe("What this project is about"),
356
- tech_stack: z.array(z.string()).optional().describe('Technologies used, e.g. ["FastAPI", "React", "PostgreSQL"]'),
357
- repo_path: z.string().optional().describe("Path to the project repository")
358
- }
359
- },
360
- async ({ name, description, tech_stack, repo_path }) => {
361
- const db2 = getDb();
362
- const id = generateId();
363
- let slug = generateSlug(name);
364
- const existing = db2.prepare("SELECT slug FROM projects WHERE slug LIKE ?").all(`${slug}%`);
365
- const usedSlugs = new Set(existing.map((r) => r.slug));
366
- let candidate = slug;
367
- let n = 2;
368
- while (usedSlugs.has(candidate)) candidate = slug + n++;
369
- slug = candidate;
370
- try {
371
- db2.prepare(
372
- `INSERT INTO projects (id, name, slug, description, tech_stack, repo_path) VALUES (?, ?, ?, ?, ?, ?)`
373
- ).run(id, name, slug, description ?? null, tech_stack ? JSON.stringify(tech_stack) : null, repo_path ?? null);
374
- } catch (e) {
375
- if (e.message?.includes("UNIQUE constraint failed")) {
376
- return { content: [{ type: "text", text: `Project "${name}" already exists.` }], isError: true };
377
- }
378
- throw e;
379
- }
380
- return {
381
- content: [{ type: "text", text: JSON.stringify({ project_id: id, message: `Project created: "${name}"` }) }]
382
- };
346
+ // src/server/http.ts
347
+ import { createServer } from "http";
348
+ import { readFile } from "fs/promises";
349
+ import { join, extname } from "path";
350
+ import { fileURLToPath } from "url";
351
+ import { dirname as dirname2 } from "path";
352
+ import { spawn } from "child_process";
353
+
354
+ // src/server/routes.ts
355
+ var listProjects = async (_req, res) => {
356
+ const db2 = getDb();
357
+ const url = new URL(_req.url || "/", "http://localhost");
358
+ const status = url.searchParams.get("status");
359
+ const sql = `
360
+ SELECT p.*,
361
+ (SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND status NOT IN ('done','cancelled')) AS active_task_count,
362
+ (SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND status = 'done') AS done_task_count
363
+ FROM projects p
364
+ ${status ? "WHERE p.status = ?" : ""}
365
+ ORDER BY p.updated_at DESC
366
+ `;
367
+ const rows = status ? db2.prepare(sql).all(status) : db2.prepare(sql).all();
368
+ sendJson(res, 200, rows);
369
+ };
370
+ var getProject = async (_req, res, params) => {
371
+ const db2 = getDb();
372
+ const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
373
+ if (!project) {
374
+ sendJson(res, 404, { error: "Project not found" });
375
+ return;
376
+ }
377
+ const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(params.id);
378
+ sendJson(res, 200, { ...project, task_counts: taskCounts });
379
+ };
380
+ var updateProject = async (req, res, params) => {
381
+ const db2 = getDb();
382
+ const body = await parseBody(req);
383
+ const existing = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
384
+ if (!existing) {
385
+ sendJson(res, 404, { error: "Project not found" });
386
+ return;
387
+ }
388
+ const updates = [];
389
+ const sqlParams = [];
390
+ if (body.name !== void 0) {
391
+ updates.push("name = ?");
392
+ sqlParams.push(body.name);
393
+ }
394
+ if (body.description !== void 0) {
395
+ updates.push("description = ?");
396
+ sqlParams.push(body.description);
397
+ }
398
+ if (body.status !== void 0) {
399
+ updates.push("status = ?");
400
+ sqlParams.push(body.status);
401
+ }
402
+ if (updates.length === 0) {
403
+ sendJson(res, 400, { error: "No updates provided" });
404
+ return;
405
+ }
406
+ sqlParams.push(params.id);
407
+ try {
408
+ db2.prepare(`UPDATE projects SET ${updates.join(", ")} WHERE id = ?`).run(...sqlParams);
409
+ } catch (e) {
410
+ if (e.message?.includes("UNIQUE constraint failed")) {
411
+ sendJson(res, 409, { error: "A project with that name already exists" });
412
+ return;
383
413
  }
414
+ throw e;
415
+ }
416
+ const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
417
+ sendJson(res, 200, updated);
418
+ };
419
+ var listTasks = async (req, res, params) => {
420
+ const db2 = getDb();
421
+ const url = new URL(req.url || "/", "http://localhost");
422
+ const includeDone = url.searchParams.get("include_done") === "true";
423
+ let sql = "SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.project_id = ?";
424
+ if (!includeDone) {
425
+ sql += " AND t.status NOT IN ('done', 'cancelled')";
426
+ }
427
+ sql += " ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, t.created_at DESC";
428
+ const rows = db2.prepare(sql).all(params.pid);
429
+ sendJson(res, 200, rows);
430
+ };
431
+ var createTask = async (req, res, params) => {
432
+ const db2 = getDb();
433
+ const body = await parseBody(req);
434
+ if (!body.title || typeof body.title !== "string") {
435
+ sendJson(res, 400, { error: "title is required" });
436
+ return;
437
+ }
438
+ const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(params.pid);
439
+ if (!project) {
440
+ sendJson(res, 404, { error: "Project not found" });
441
+ return;
442
+ }
443
+ const id = generateId();
444
+ const priority = body.priority || "medium";
445
+ const tags = Array.isArray(body.tags) ? JSON.stringify(body.tags) : null;
446
+ const seqRow = db2.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM tasks WHERE project_id = ?").get(params.pid);
447
+ const seq = seqRow.next_seq;
448
+ db2.prepare(
449
+ "INSERT INTO tasks (id, project_id, seq, title, description, priority, tags, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
450
+ ).run(
451
+ id,
452
+ params.pid,
453
+ seq,
454
+ body.title,
455
+ body.description ?? null,
456
+ priority,
457
+ tags,
458
+ body.parent_task_id ?? null
384
459
  );
385
- server2.registerTool(
386
- "list_projects",
387
- {
388
- title: "List Projects",
389
- description: "List all tracked projects. Filter by status to see active, paused, completed, or archived projects.",
390
- inputSchema: {
391
- status: z.enum(["active", "paused", "completed", "archived"]).optional().describe("Filter by project status")
392
- }
393
- },
394
- async ({ status }) => {
395
- const db2 = getDb();
396
- let rows;
397
- if (status) {
398
- rows = db2.prepare("SELECT * FROM projects WHERE status = ? ORDER BY updated_at DESC").all(status);
399
- } else {
400
- rows = db2.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all();
401
- }
402
- return {
403
- content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
404
- };
460
+ const task = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.id = ?").get(id);
461
+ recordTaskHistory(id, "created", null, JSON.stringify({ status: priority === "medium" ? "todo" : priority, priority }));
462
+ sendJson(res, 201, task);
463
+ };
464
+ var updateTask = async (req, res, params) => {
465
+ const db2 = getDb();
466
+ const body = await parseBody(req);
467
+ const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
468
+ if (!existing) {
469
+ sendJson(res, 404, { error: "Task not found" });
470
+ return;
471
+ }
472
+ const updates = [];
473
+ const sqlParams = [];
474
+ if (body.title !== void 0) {
475
+ updates.push("title = ?");
476
+ sqlParams.push(body.title);
477
+ }
478
+ if (body.description !== void 0) {
479
+ updates.push("description = ?");
480
+ sqlParams.push(body.description);
481
+ }
482
+ if (body.status !== void 0) {
483
+ updates.push("status = ?");
484
+ sqlParams.push(body.status);
485
+ if (body.status === "done") {
486
+ updates.push("completed_at = CURRENT_TIMESTAMP");
487
+ } else {
488
+ updates.push("completed_at = NULL");
405
489
  }
406
- );
407
- server2.registerTool(
408
- "get_project_status",
409
- {
410
- title: "Get Project Status",
411
- description: "Get a full overview of a project: active tasks, recent decisions, blockers, and last session summary. Great for getting up to speed.",
412
- inputSchema: {
413
- project: z.string().describe("Project name or ID")
414
- }
415
- },
416
- async ({ project }) => {
417
- const db2 = getDb();
418
- const projectId = resolveProjectId(project);
419
- if (!projectId) {
420
- return { content: [{ type: "text", text: `Project "${project}" not found.` }], isError: true };
421
- }
422
- const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
423
- const activeTasks = db2.prepare("SELECT id, title, status, priority, tags FROM tasks WHERE project_id = ? AND status NOT IN ('done', 'cancelled') ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END").all(projectId);
424
- const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(projectId);
425
- const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(projectId);
426
- const lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(projectId);
427
- const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(projectId);
428
- const result = {
429
- project: projectRow,
430
- task_summary: taskCounts,
431
- active_tasks: activeTasks,
432
- blocked_tasks: blockedTasks,
433
- recent_decisions: recentDecisions,
434
- last_session: lastSession
435
- };
436
- return {
437
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
438
- };
490
+ }
491
+ if (body.priority !== void 0) {
492
+ updates.push("priority = ?");
493
+ sqlParams.push(body.priority);
494
+ }
495
+ if (body.tags !== void 0) {
496
+ updates.push("tags = ?");
497
+ sqlParams.push(Array.isArray(body.tags) ? JSON.stringify(body.tags) : null);
498
+ }
499
+ if (body.blocked_by !== void 0) {
500
+ updates.push("blocked_by = ?");
501
+ sqlParams.push(Array.isArray(body.blocked_by) ? JSON.stringify(body.blocked_by) : null);
502
+ if (Array.isArray(body.blocked_by) && body.blocked_by.length > 0 && body.status === void 0) {
503
+ updates.push("status = 'blocked'");
439
504
  }
440
- );
441
- }
442
-
443
- // src/tools/tasks.ts
444
- import { z as z2 } from "zod/v4";
445
- function registerTaskTools(server2) {
446
- server2.registerTool(
447
- "create_task",
448
- {
449
- title: "Create Task",
450
- description: "Create a new task in a project. Proactively use this when the user mentions something that needs to be done, a bug to fix, or a feature to build.",
451
- inputSchema: {
452
- project: z2.string().optional().describe("Project name or ID (defaults to most recent active project)"),
453
- title: z2.string().describe("Short task title"),
454
- description: z2.string().optional().describe("Detailed description of the task"),
455
- priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority (default: medium)"),
456
- tags: z2.array(z2.string()).optional().describe('Tags like "backend", "auth", "bug"'),
457
- parent_task_id: z2.string().optional().describe("Parent task ID for sub-tasks")
458
- }
459
- },
460
- async ({ project, title, description, priority, tags, parent_task_id }) => {
461
- const resolved = resolveProjectOrDefault(project);
462
- if (!resolved) {
463
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
464
- }
465
- const db2 = getDb();
466
- const id = generateId();
467
- const seqRow = db2.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM tasks WHERE project_id = ?").get(resolved.id);
468
- const seq = seqRow.next_seq;
469
- db2.prepare(
470
- `INSERT INTO tasks (id, project_id, seq, title, description, priority, tags, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
471
- ).run(
472
- id,
473
- resolved.id,
474
- seq,
475
- title,
476
- description ?? null,
477
- priority ?? "medium",
478
- tags ? JSON.stringify(tags) : null,
479
- parent_task_id ?? null
480
- );
481
- const proj = db2.prepare("SELECT slug FROM projects WHERE id = ?").get(resolved.id);
482
- const short_id = proj?.slug ? `${proj.slug}-${seq}` : null;
483
- return {
484
- content: [{
485
- type: "text",
486
- text: JSON.stringify({
487
- task_id: id,
488
- short_id,
489
- message: `Task created: "${title}" in ${resolved.name} (priority: ${priority ?? "medium"})`
490
- })
491
- }]
492
- };
505
+ }
506
+ if (updates.length === 0) {
507
+ sendJson(res, 400, { error: "No updates provided" });
508
+ return;
509
+ }
510
+ sqlParams.push(params.id);
511
+ db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...sqlParams);
512
+ if (body.status !== void 0 && body.status !== existing.status) {
513
+ recordTaskHistory(params.id, "status_changed", existing.status, body.status);
514
+ }
515
+ if (body.priority !== void 0 && body.priority !== existing.priority) {
516
+ recordTaskHistory(params.id, "priority_changed", existing.priority, body.priority);
517
+ }
518
+ if (body.title !== void 0 && body.title !== existing.title) {
519
+ recordTaskHistory(params.id, "title_changed", existing.title, body.title);
520
+ }
521
+ const updated = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.id = ?").get(params.id);
522
+ sendJson(res, 200, updated);
523
+ };
524
+ var deleteTask = async (_req, res, params) => {
525
+ const db2 = getDb();
526
+ const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
527
+ if (!existing) {
528
+ sendJson(res, 404, { error: "Task not found" });
529
+ return;
530
+ }
531
+ const deleteTransaction = db2.transaction((taskId) => {
532
+ const subtasks = db2.prepare("SELECT id FROM tasks WHERE parent_task_id = ?").all(taskId);
533
+ for (const sub of subtasks) {
534
+ db2.prepare("DELETE FROM task_history WHERE task_id = ?").run(sub.id);
535
+ db2.prepare("DELETE FROM notes WHERE task_id = ?").run(sub.id);
536
+ db2.prepare("DELETE FROM tasks WHERE id = ?").run(sub.id);
493
537
  }
494
- );
495
- server2.registerTool(
496
- "update_task",
497
- {
498
- title: "Update Task",
499
- description: "Update any field of a task. Proactively use this when a task status changes, priorities shift, or new information comes in.",
500
- inputSchema: {
501
- task_id: z2.string().describe("Task ID to update"),
502
- title: z2.string().optional().describe("New title"),
503
- description: z2.string().optional().describe("New description"),
504
- status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("New status"),
505
- priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
506
- tags: z2.array(z2.string()).optional().describe("New tags (replaces existing)"),
507
- blocked_by: z2.array(z2.string()).optional().describe("Task IDs that block this task")
508
- }
509
- },
510
- async ({ task_id, title, description, status, priority, tags, blocked_by }) => {
511
- const db2 = getDb();
512
- const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
513
- if (!existing) {
514
- return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
515
- }
516
- const updates = [];
517
- const params = [];
518
- if (title !== void 0) {
519
- updates.push("title = ?");
520
- params.push(title);
521
- }
522
- if (description !== void 0) {
523
- updates.push("description = ?");
524
- params.push(description);
525
- }
526
- if (status !== void 0) {
527
- updates.push("status = ?");
528
- params.push(status);
529
- if (status === "done") {
530
- updates.push("completed_at = CURRENT_TIMESTAMP");
531
- }
532
- }
533
- if (priority !== void 0) {
534
- updates.push("priority = ?");
535
- params.push(priority);
536
- }
537
- if (tags !== void 0) {
538
- updates.push("tags = ?");
539
- params.push(JSON.stringify(tags));
540
- }
541
- if (blocked_by !== void 0) {
542
- updates.push("blocked_by = ?");
543
- params.push(JSON.stringify(blocked_by));
544
- if (blocked_by.length > 0 && status === void 0) {
545
- updates.push("status = 'blocked'");
546
- }
547
- }
548
- if (updates.length === 0) {
549
- return { content: [{ type: "text", text: "No updates provided." }], isError: true };
550
- }
551
- params.push(task_id);
552
- db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...params);
553
- return {
554
- content: [{ type: "text", text: JSON.stringify({ task_id, message: `Task "${existing.title}" updated.` }) }]
555
- };
538
+ db2.prepare("DELETE FROM task_history WHERE task_id = ?").run(taskId);
539
+ db2.prepare("DELETE FROM notes WHERE task_id = ?").run(taskId);
540
+ db2.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
541
+ });
542
+ deleteTransaction(params.id);
543
+ sendJson(res, 200, { message: "Task deleted" });
544
+ };
545
+ var getTaskHistory = async (_req, res, params) => {
546
+ const db2 = getDb();
547
+ const rows = db2.prepare(
548
+ "SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at ASC"
549
+ ).all(params.id);
550
+ sendJson(res, 200, rows);
551
+ };
552
+ var createSession = async (req, res, params) => {
553
+ const db2 = getDb();
554
+ const body = await parseBody(req);
555
+ if (!body.summary || typeof body.summary !== "string") {
556
+ sendJson(res, 400, { error: "summary is required" });
557
+ return;
558
+ }
559
+ const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(params.pid);
560
+ if (!project) {
561
+ sendJson(res, 404, { error: "Project not found" });
562
+ return;
563
+ }
564
+ const id = generateId();
565
+ db2.prepare(
566
+ "INSERT INTO sessions (id, project_id, summary, next_steps) VALUES (?, ?, ?, ?)"
567
+ ).run(id, params.pid, body.summary, body.next_steps ?? null);
568
+ const session = db2.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
569
+ sendJson(res, 201, session);
570
+ };
571
+ var listDecisions = async (_req, res, params) => {
572
+ const db2 = getDb();
573
+ const rows = db2.prepare("SELECT * FROM decisions WHERE project_id = ? ORDER BY created_at DESC").all(params.pid);
574
+ sendJson(res, 200, rows);
575
+ };
576
+ var routes = [
577
+ { method: "GET", pattern: "/api/projects", handler: listProjects },
578
+ { method: "GET", pattern: "/api/projects/:id", handler: getProject },
579
+ { method: "PATCH", pattern: "/api/projects/:id", handler: updateProject },
580
+ { method: "POST", pattern: "/api/projects/:pid/sessions", handler: createSession },
581
+ { method: "GET", pattern: "/api/projects/:pid/decisions", handler: listDecisions },
582
+ { method: "GET", pattern: "/api/projects/:pid/tasks", handler: listTasks },
583
+ { method: "POST", pattern: "/api/projects/:pid/tasks", handler: createTask },
584
+ { method: "PATCH", pattern: "/api/tasks/:id", handler: updateTask },
585
+ { method: "DELETE", pattern: "/api/tasks/:id", handler: deleteTask },
586
+ { method: "GET", pattern: "/api/tasks/:id/history", handler: getTaskHistory }
587
+ ];
588
+ async function handleApiRequest(req, res) {
589
+ const url = new URL(req.url || "/", "http://localhost");
590
+ const method = req.method || "GET";
591
+ for (const route of routes) {
592
+ if (route.method !== method) continue;
593
+ const params = matchRoute(route.pattern, url.pathname);
594
+ if (params) {
595
+ await route.handler(req, res, params);
596
+ return;
556
597
  }
557
- );
558
- server2.registerTool(
559
- "list_tasks",
560
- {
561
- title: "List Tasks",
562
- description: "List tasks with filters. Defaults to showing non-completed tasks for the most recent active project.",
563
- inputSchema: {
564
- project: z2.string().optional().describe("Project name or ID"),
565
- status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("Filter by status"),
566
- priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority"),
567
- tag: z2.string().optional().describe("Filter by tag"),
568
- include_done: z2.boolean().optional().describe("Include completed tasks (default: false)")
569
- }
570
- },
571
- async ({ project, status, priority, tag, include_done }) => {
572
- const resolved = resolveProjectOrDefault(project);
573
- if (!resolved) {
574
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
575
- }
576
- const db2 = getDb();
577
- const conditions = ["project_id = @projectId"];
578
- const params = { projectId: resolved.id };
579
- if (status) {
580
- conditions.push("status = @status");
581
- params.status = status;
582
- } else if (!include_done) {
583
- conditions.push("status NOT IN ('done', 'cancelled')");
584
- }
585
- if (priority) {
586
- conditions.push("priority = @priority");
587
- params.priority = priority;
598
+ }
599
+ sendJson(res, 404, { error: "Not found" });
600
+ }
601
+
602
+ // src/server/http.ts
603
+ function openBrowser(url) {
604
+ const platform = process.platform;
605
+ const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
606
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
607
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
608
+ }
609
+ var __filename = fileURLToPath(import.meta.url);
610
+ var __dirname = dirname2(__filename);
611
+ function resolveStaticDir() {
612
+ return join(__dirname, "ui");
613
+ }
614
+ var MIME_TYPES = {
615
+ ".html": "text/html; charset=utf-8",
616
+ ".js": "application/javascript; charset=utf-8",
617
+ ".css": "text/css; charset=utf-8",
618
+ ".json": "application/json; charset=utf-8",
619
+ ".svg": "image/svg+xml",
620
+ ".png": "image/png",
621
+ ".jpg": "image/jpeg",
622
+ ".ico": "image/x-icon",
623
+ ".woff": "font/woff",
624
+ ".woff2": "font/woff2",
625
+ ".ttf": "font/ttf"
626
+ };
627
+ function parseBody(req) {
628
+ return new Promise((resolve2, reject) => {
629
+ const chunks = [];
630
+ req.on("data", (chunk) => chunks.push(chunk));
631
+ req.on("end", () => {
632
+ if (chunks.length === 0) {
633
+ resolve2({});
634
+ return;
588
635
  }
589
- if (tag) {
590
- conditions.push("tags LIKE '%' || @tag || '%'");
591
- params.tag = `"${tag}"`;
636
+ try {
637
+ resolve2(JSON.parse(Buffer.concat(chunks).toString()));
638
+ } catch {
639
+ resolve2({});
592
640
  }
593
- const sql = `SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE ${conditions.join(" AND ")} ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, t.created_at DESC`;
594
- const rows = db2.prepare(sql).all(params);
595
- return {
596
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, tasks: rows }, null, 2) }]
597
- };
641
+ });
642
+ req.on("error", reject);
643
+ });
644
+ }
645
+ function matchRoute(pattern, pathname) {
646
+ const patternParts = pattern.split("/").filter(Boolean);
647
+ const pathParts = pathname.split("/").filter(Boolean);
648
+ if (patternParts.length !== pathParts.length) return null;
649
+ const params = {};
650
+ for (let i = 0; i < patternParts.length; i++) {
651
+ if (patternParts[i].startsWith(":")) {
652
+ params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
653
+ } else if (patternParts[i] !== pathParts[i]) {
654
+ return null;
598
655
  }
599
- );
600
- server2.registerTool(
601
- "get_task",
602
- {
603
- title: "Get Task",
604
- description: "Get full detail for a specific task including sub-tasks and related notes.",
605
- inputSchema: {
606
- task_id: z2.string().describe("Task ID")
607
- }
608
- },
609
- async ({ task_id }) => {
610
- const db2 = getDb();
611
- const task = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.id = ?").get(task_id);
612
- if (!task) {
613
- return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
614
- }
615
- const subtasks = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.parent_task_id = ?").all(task_id);
616
- const notes = db2.prepare("SELECT * FROM notes WHERE task_id = ? ORDER BY created_at DESC").all(task_id);
617
- return {
618
- content: [{ type: "text", text: JSON.stringify({ task, subtasks, notes }, null, 2) }]
619
- };
656
+ }
657
+ return params;
658
+ }
659
+ function sendJson(res, status, data) {
660
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
661
+ res.end(JSON.stringify(data));
662
+ }
663
+ async function serveStatic(req, res) {
664
+ const staticDir = resolveStaticDir();
665
+ const url = new URL(req.url || "/", "http://localhost");
666
+ let filePath = join(staticDir, url.pathname === "/" ? "index.html" : url.pathname);
667
+ try {
668
+ const content = await readFile(filePath);
669
+ const ext = extname(filePath);
670
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
671
+ res.writeHead(200, { "Content-Type": contentType });
672
+ res.end(content);
673
+ } catch {
674
+ try {
675
+ const indexPath = join(staticDir, "index.html");
676
+ const content = await readFile(indexPath);
677
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
678
+ res.end(content);
679
+ } catch {
680
+ res.writeHead(503, { "Content-Type": "text/html; charset=utf-8" });
681
+ res.end(
682
+ "<html><body><h1>mindpm UI not built</h1><p>Run <code>npm run build:ui</code> to build the Kanban UI.</p></body></html>"
683
+ );
620
684
  }
621
- );
622
- server2.registerTool(
623
- "get_next_tasks",
624
- {
625
- title: "Get Next Tasks",
626
- description: "Smart query: what should be worked on next? Returns highest priority non-blocked tasks for a project.",
627
- inputSchema: {
628
- project: z2.string().optional().describe("Project name or ID"),
629
- limit: z2.number().optional().describe("Max number of tasks to return (default: 5)")
685
+ }
686
+ }
687
+ var _httpPort = null;
688
+ function getHttpPort() {
689
+ return _httpPort;
690
+ }
691
+ function startHttpServer(port) {
692
+ _httpPort = port;
693
+ const server2 = createServer(async (req, res) => {
694
+ try {
695
+ if (req.url?.startsWith("/api/")) {
696
+ process.stderr.write(`[mindpm] API request: ${req.method} ${req.url}
697
+ `);
698
+ await handleApiRequest(req, res);
699
+ } else {
700
+ await serveStatic(req, res);
630
701
  }
631
- },
632
- async ({ project, limit }) => {
633
- const resolved = resolveProjectOrDefault(project);
634
- if (!resolved) {
635
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
702
+ } catch (err) {
703
+ process.stderr.write(`[mindpm] HTTP error on ${req.method} ${req.url}: ${err}
704
+ `);
705
+ if (!res.headersSent) {
706
+ sendJson(res, 500, { error: "Internal server error", detail: String(err) });
636
707
  }
637
- const db2 = getDb();
638
- const rows = db2.prepare(
639
- `SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id
640
- WHERE t.project_id = ? AND t.status IN ('todo', 'in_progress')
641
- ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
642
- t.created_at ASC
643
- LIMIT ?`
644
- ).all(resolved.id, limit ?? 5);
645
- return {
646
- content: [{
647
- type: "text",
648
- text: JSON.stringify({ project: resolved.name, next_tasks: rows }, null, 2)
649
- }]
650
- };
651
708
  }
709
+ });
710
+ server2.on("error", (err) => {
711
+ if (err.code === "EADDRINUSE") {
712
+ process.stderr.write(
713
+ `[mindpm] Port ${port} already in use. Kanban UI served by existing process at http://localhost:${port}
714
+ `
715
+ );
716
+ } else {
717
+ _httpPort = null;
718
+ process.stderr.write(`[mindpm] HTTP server error: ${err.message}
719
+ `);
720
+ }
721
+ });
722
+ server2.listen(port, () => {
723
+ const url = `http://localhost:${port}`;
724
+ process.stderr.write(`[mindpm] Kanban UI available at ${url}
725
+ `);
726
+ if (process.env.MINDPM_OPEN_BROWSER === "1") {
727
+ openBrowser(url);
728
+ }
729
+ });
730
+ return server2;
731
+ }
732
+
733
+ // src/tools/auto-session.ts
734
+ var autoStartedProjects = /* @__PURE__ */ new Set();
735
+ function markSessionStarted(projectId) {
736
+ autoStartedProjects.add(projectId);
737
+ }
738
+ function getActivitySince(db2, projectId, cutoffTime) {
739
+ return db2.prepare(`
740
+ SELECT 'task_created' as type, id, title, created_at as timestamp
741
+ FROM tasks WHERE project_id = ? AND created_at > ?
742
+ UNION ALL
743
+ SELECT 'task_updated' as type, id, title, updated_at as timestamp
744
+ FROM tasks WHERE project_id = ? AND updated_at > ? AND updated_at != created_at
745
+ UNION ALL
746
+ SELECT 'decision' as type, id, title, created_at as timestamp
747
+ FROM decisions WHERE project_id = ? AND created_at > ?
748
+ UNION ALL
749
+ SELECT 'note' as type, id, substr(content, 1, 80) as title, created_at as timestamp
750
+ FROM notes WHERE project_id = ? AND created_at > ?
751
+ ORDER BY timestamp DESC
752
+ `).all(
753
+ projectId,
754
+ cutoffTime,
755
+ projectId,
756
+ cutoffTime,
757
+ projectId,
758
+ cutoffTime,
759
+ projectId,
760
+ cutoffTime
652
761
  );
653
762
  }
763
+ function buildSessionText(projectId) {
764
+ const db2 = getDb();
765
+ const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
766
+ let lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(projectId);
767
+ const cutoffTime = lastSession?.created_at ?? "1970-01-01";
768
+ const recentActivity = getActivitySince(db2, projectId, cutoffTime);
769
+ if (recentActivity.length > 0) {
770
+ const taskIds = [...new Set(
771
+ recentActivity.filter((a) => a.type === "task_created" || a.type === "task_updated").map((a) => a.id)
772
+ )];
773
+ const decisionIds = [...new Set(
774
+ recentActivity.filter((a) => a.type === "decision").map((a) => a.id)
775
+ )];
776
+ const syntheticId = generateId();
777
+ db2.prepare(
778
+ `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made) VALUES (?, ?, ?, ?, ?)`
779
+ ).run(
780
+ syntheticId,
781
+ projectId,
782
+ `Auto-generated: ${recentActivity.length} activities since last session`,
783
+ taskIds.length > 0 ? JSON.stringify(taskIds) : null,
784
+ decisionIds.length > 0 ? JSON.stringify(decisionIds) : null
785
+ );
786
+ lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(projectId);
787
+ }
788
+ const activeTasks = db2.prepare(
789
+ `SELECT id, title, status, priority, tags FROM tasks
790
+ WHERE project_id = ? AND status NOT IN ('done', 'cancelled')
791
+ ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`
792
+ ).all(projectId);
793
+ const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(projectId);
794
+ const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(projectId);
795
+ const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(projectId);
796
+ const contextItems = db2.prepare("SELECT key, value, category FROM context WHERE project_id = ? ORDER BY category, key").all(projectId);
797
+ db2.prepare("UPDATE projects SET status = status WHERE id = ?").run(projectId);
798
+ const port = getHttpPort();
799
+ const kanbanUrl = port ? `http://localhost:${port}?project=${projectId}` : null;
800
+ const result = {
801
+ kanban_url: kanbanUrl,
802
+ project: projectRow,
803
+ last_session: lastSession ? {
804
+ summary: lastSession.summary,
805
+ next_steps: lastSession.next_steps,
806
+ when: lastSession.created_at
807
+ } : null,
808
+ recent_activity: recentActivity.slice(0, 20),
809
+ task_summary: taskCounts,
810
+ active_tasks: activeTasks,
811
+ blocked_tasks: blockedTasks,
812
+ recent_decisions: recentDecisions,
813
+ context: contextItems
814
+ };
815
+ const kanbanLine = kanbanUrl ? `Kanban board: ${kanbanUrl}` : "Kanban board: unavailable (HTTP server not running)";
816
+ return `${kanbanLine}
817
+
818
+ ${JSON.stringify(result, null, 2)}`;
819
+ }
820
+ function maybeAutoSession(projectId) {
821
+ if (autoStartedProjects.has(projectId)) return null;
822
+ autoStartedProjects.add(projectId);
823
+ return buildSessionText(projectId);
824
+ }
654
825
 
655
- // src/tools/decisions.ts
656
- import { z as z3 } from "zod/v4";
657
- function registerDecisionTools(server2) {
826
+ // src/tools/projects.ts
827
+ function registerProjectTools(server2) {
658
828
  server2.registerTool(
659
- "log_decision",
829
+ "create_project",
660
830
  {
661
- title: "Log Decision",
662
- description: "Record a decision with reasoning and alternatives considered. Proactively use this when the user makes a technical decision, chooses between options, or settles a debate.",
831
+ title: "Create Project",
832
+ description: "Create a new project to track. Use this when starting a new project or when a user mentions a project that doesn't exist yet.",
663
833
  inputSchema: {
664
- project: z3.string().optional().describe("Project name or ID"),
665
- task_id: z3.string().optional().describe("Task ID to associate this decision with (omit for project-level)"),
666
- title: z3.string().describe("Short title for the decision"),
667
- decision: z3.string().describe("What was decided"),
668
- reasoning: z3.string().optional().describe("Why this was decided"),
669
- alternatives: z3.array(z3.string()).optional().describe("Rejected alternatives"),
670
- tags: z3.array(z3.string()).optional().describe('Tags like "architecture", "database", "api"')
834
+ name: z.string().describe("Project name (unique)"),
835
+ description: z.string().optional().describe("What this project is about"),
836
+ tech_stack: z.array(z.string()).optional().describe('Technologies used, e.g. ["FastAPI", "React", "PostgreSQL"]'),
837
+ repo_path: z.string().optional().describe("Path to the project repository")
671
838
  }
672
839
  },
673
- async ({ project, task_id, title, decision, reasoning, alternatives, tags }) => {
674
- const resolved = resolveProjectOrDefault(project);
675
- if (!resolved) {
676
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
677
- }
840
+ async ({ name, description, tech_stack, repo_path }) => {
678
841
  const db2 = getDb();
679
842
  const id = generateId();
680
- db2.prepare(
681
- `INSERT INTO decisions (id, project_id, task_id, title, decision, reasoning, alternatives, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
682
- ).run(
683
- id,
684
- resolved.id,
685
- task_id ?? null,
686
- title,
687
- decision,
688
- reasoning ?? null,
689
- alternatives ? JSON.stringify(alternatives) : null,
690
- tags ? JSON.stringify(tags) : null
691
- );
692
- const scope = task_id ? `task ${task_id} in ${resolved.name}` : resolved.name;
843
+ let slug = generateSlug(name);
844
+ const existing = db2.prepare("SELECT slug FROM projects WHERE slug LIKE ?").all(`${slug}%`);
845
+ const usedSlugs = new Set(existing.map((r) => r.slug));
846
+ let candidate = slug;
847
+ let n = 2;
848
+ while (usedSlugs.has(candidate)) candidate = slug + n++;
849
+ slug = candidate;
850
+ try {
851
+ db2.prepare(
852
+ `INSERT INTO projects (id, name, slug, description, tech_stack, repo_path) VALUES (?, ?, ?, ?, ?, ?)`
853
+ ).run(id, name, slug, description ?? null, tech_stack ? JSON.stringify(tech_stack) : null, repo_path ?? null);
854
+ } catch (e) {
855
+ if (e.message?.includes("UNIQUE constraint failed")) {
856
+ return { content: [{ type: "text", text: `Project "${name}" already exists.` }], isError: true };
857
+ }
858
+ throw e;
859
+ }
693
860
  return {
694
- content: [{
695
- type: "text",
696
- text: JSON.stringify({ decision_id: id, message: `Decision logged: "${title}" in ${scope}` })
697
- }]
861
+ content: [{ type: "text", text: JSON.stringify({ project_id: id, message: `Project created: "${name}"` }) }]
698
862
  };
699
863
  }
700
864
  );
701
865
  server2.registerTool(
702
- "list_decisions",
866
+ "list_projects",
703
867
  {
704
- title: "List Decisions",
705
- description: "List decisions for a project. Filter by tags to find specific decisions.",
868
+ title: "List Projects",
869
+ description: "List all tracked projects. Filter by status to see active, paused, completed, or archived projects.",
706
870
  inputSchema: {
707
- project: z3.string().optional().describe("Project name or ID"),
708
- tag: z3.string().optional().describe("Filter by tag"),
709
- limit: z3.number().optional().describe("Max number of decisions to return (default: 20)")
871
+ status: z.enum(["active", "paused", "completed", "archived"]).optional().describe("Filter by project status")
710
872
  }
711
873
  },
712
- async ({ project, tag, limit }) => {
713
- const resolved = resolveProjectOrDefault(project);
714
- if (!resolved) {
715
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
716
- }
874
+ async ({ status }) => {
717
875
  const db2 = getDb();
718
- let sql;
719
- const params = [resolved.id];
720
- if (tag) {
721
- sql = `SELECT * FROM decisions WHERE project_id = ? AND tags LIKE ? ORDER BY created_at DESC LIMIT ?`;
722
- params.push(`%"${tag}"%`, limit ?? 20);
876
+ let rows;
877
+ if (status) {
878
+ rows = db2.prepare("SELECT * FROM projects WHERE status = ? ORDER BY updated_at DESC").all(status);
723
879
  } else {
724
- sql = `SELECT * FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`;
725
- params.push(limit ?? 20);
726
- }
727
- const rows = db2.prepare(sql).all(...params);
728
- return {
729
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, decisions: rows }, null, 2) }]
730
- };
731
- }
732
- );
733
- }
734
-
735
- // src/tools/notes.ts
736
- import { z as z4 } from "zod/v4";
737
- function registerNoteTools(server2) {
738
- server2.registerTool(
739
- "add_note",
740
- {
741
- title: "Add Note",
742
- description: "Add a note to a project or task. Proactively use this when the user shares context about architecture, bugs, ideas, research findings, or any important information worth remembering.",
743
- inputSchema: {
744
- project: z4.string().optional().describe("Project name or ID"),
745
- content: z4.string().describe("The note content"),
746
- category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Note category (default: general)"),
747
- task_id: z4.string().optional().describe("Link this note to a specific task"),
748
- tags: z4.array(z4.string()).optional().describe("Tags for categorization")
749
- }
750
- },
751
- async ({ project, content, category, task_id, tags }) => {
752
- const resolved = resolveProjectOrDefault(project);
753
- if (!resolved) {
754
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
880
+ rows = db2.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all();
755
881
  }
756
- const db2 = getDb();
757
- const id = generateId();
758
- db2.prepare(
759
- `INSERT INTO notes (id, project_id, task_id, content, category, tags) VALUES (?, ?, ?, ?, ?, ?)`
760
- ).run(
761
- id,
762
- resolved.id,
763
- task_id ?? null,
764
- content,
765
- category ?? "general",
766
- tags ? JSON.stringify(tags) : null
767
- );
768
882
  return {
769
- content: [{
770
- type: "text",
771
- text: JSON.stringify({ note_id: id, message: `Note added to ${resolved.name} (${category ?? "general"})` })
772
- }]
883
+ content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
773
884
  };
774
885
  }
775
886
  );
776
887
  server2.registerTool(
777
- "search_notes",
888
+ "get_project_status",
778
889
  {
779
- title: "Search Notes",
780
- description: "Full-text search across notes for a project.",
890
+ title: "Get Project Status",
891
+ description: "Get a full overview of a project: active tasks, recent decisions, blockers, and last session summary. Great for getting up to speed.",
781
892
  inputSchema: {
782
- project: z4.string().optional().describe("Project name or ID"),
783
- query: z4.string().describe("Search query"),
784
- category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Filter by category")
893
+ project: z.string().describe("Project name or ID")
785
894
  }
786
895
  },
787
- async ({ project, query, category }) => {
788
- const resolved = resolveProjectOrDefault(project);
789
- if (!resolved) {
790
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
791
- }
896
+ async ({ project }) => {
792
897
  const db2 = getDb();
793
- const conditions = ["project_id = ?"];
794
- const params = [resolved.id];
795
- conditions.push("content LIKE '%' || ? || '%'");
796
- params.push(query);
797
- if (category) {
798
- conditions.push("category = ?");
799
- params.push(category);
898
+ const projectId = resolveProjectId(project);
899
+ if (!projectId) {
900
+ return { content: [{ type: "text", text: `Project "${project}" not found.` }], isError: true };
800
901
  }
801
- const sql = `SELECT * FROM notes WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
802
- const rows = db2.prepare(sql).all(...params);
902
+ const sessionPreamble = maybeAutoSession(projectId);
903
+ const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
904
+ const activeTasks = db2.prepare("SELECT id, title, status, priority, tags FROM tasks WHERE project_id = ? AND status NOT IN ('done', 'cancelled') ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END").all(projectId);
905
+ const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(projectId);
906
+ const recentDecisions = db2.prepare("SELECT id, title, decision, created_at FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT 5").all(projectId);
907
+ const lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(projectId);
908
+ const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(projectId);
909
+ const result = {
910
+ project: projectRow,
911
+ task_summary: taskCounts,
912
+ active_tasks: activeTasks,
913
+ blocked_tasks: blockedTasks,
914
+ recent_decisions: recentDecisions,
915
+ last_session: lastSession
916
+ };
917
+ const resultText = JSON.stringify(result, null, 2);
803
918
  return {
804
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, results: rows }, null, 2) }]
919
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
920
+
921
+ ---
922
+
923
+ ${resultText}` : resultText }]
805
924
  };
806
925
  }
807
926
  );
927
+ }
928
+
929
+ // src/tools/tasks.ts
930
+ import { z as z2 } from "zod/v4";
931
+ function registerTaskTools(server2) {
808
932
  server2.registerTool(
809
- "set_context",
933
+ "create_task",
810
934
  {
811
- title: "Set Context",
812
- description: "Set a key-value context pair for a project (upsert). Proactively use this when the user shares important project context like architecture decisions, config values, conventions, or constraints.",
935
+ title: "Create Task",
936
+ description: "Create a new task in a project. Proactively use this when the user mentions something that needs to be done, a bug to fix, or a feature to build.",
813
937
  inputSchema: {
814
- project: z4.string().optional().describe("Project name or ID"),
815
- key: z4.string().describe('Context key, e.g. "auth_approach", "deployment_target", "api_base_url"'),
816
- value: z4.string().describe("Context value"),
817
- category: z4.string().optional().describe('Category like "architecture", "config", "convention", "constraint"')
938
+ project: z2.string().optional().describe("Project name or ID (defaults to most recent active project)"),
939
+ title: z2.string().describe("Short task title"),
940
+ description: z2.string().optional().describe("Detailed description of the task"),
941
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Task priority (default: medium)"),
942
+ tags: z2.array(z2.string()).optional().describe('Tags like "backend", "auth", "bug"'),
943
+ parent_task_id: z2.string().optional().describe("Parent task ID for sub-tasks")
818
944
  }
819
945
  },
820
- async ({ project, key, value, category }) => {
946
+ async ({ project, title, description, priority, tags, parent_task_id }) => {
821
947
  const resolved = resolveProjectOrDefault(project);
822
948
  if (!resolved) {
823
949
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
824
950
  }
951
+ const sessionPreamble = maybeAutoSession(resolved.id);
825
952
  const db2 = getDb();
826
953
  const id = generateId();
954
+ const seqRow = db2.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM tasks WHERE project_id = ?").get(resolved.id);
955
+ const seq = seqRow.next_seq;
827
956
  db2.prepare(
828
- `INSERT INTO context (id, project_id, key, value, category)
829
- VALUES (?, ?, ?, ?, ?)
830
- ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, category = excluded.category`
831
- ).run(id, resolved.id, key, value, category ?? "general");
957
+ `INSERT INTO tasks (id, project_id, seq, title, description, priority, tags, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
958
+ ).run(
959
+ id,
960
+ resolved.id,
961
+ seq,
962
+ title,
963
+ description ?? null,
964
+ priority ?? "medium",
965
+ tags ? JSON.stringify(tags) : null,
966
+ parent_task_id ?? null
967
+ );
968
+ const proj = db2.prepare("SELECT slug FROM projects WHERE id = ?").get(resolved.id);
969
+ const short_id = proj?.slug ? `${proj.slug}-${seq}` : null;
970
+ const resultText = JSON.stringify({
971
+ task_id: id,
972
+ short_id,
973
+ message: `Task created: "${title}" in ${resolved.name} (priority: ${priority ?? "medium"})`
974
+ });
832
975
  return {
833
976
  content: [{
834
977
  type: "text",
835
- text: JSON.stringify({ message: `Context set: "${key}" = "${value}" in ${resolved.name}` })
978
+ text: sessionPreamble ? `${sessionPreamble}
979
+
980
+ ---
981
+
982
+ ${resultText}` : resultText
836
983
  }]
837
984
  };
838
985
  }
839
986
  );
840
987
  server2.registerTool(
841
- "get_context",
988
+ "update_task",
842
989
  {
843
- title: "Get Context",
844
- description: "Get context by key or list all context for a project.",
990
+ title: "Update Task",
991
+ description: "Update any field of a task. Proactively use this when a task status changes, priorities shift, or new information comes in.",
845
992
  inputSchema: {
846
- project: z4.string().optional().describe("Project name or ID"),
847
- key: z4.string().optional().describe("Specific context key to retrieve. If omitted, returns all context.")
993
+ task_id: z2.string().describe("Task ID to update"),
994
+ title: z2.string().optional().describe("New title"),
995
+ description: z2.string().optional().describe("New description"),
996
+ status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("New status"),
997
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("New priority"),
998
+ tags: z2.array(z2.string()).optional().describe("New tags (replaces existing)"),
999
+ blocked_by: z2.array(z2.string()).optional().describe("Task IDs that block this task")
848
1000
  }
849
1001
  },
850
- async ({ project, key }) => {
851
- const resolved = resolveProjectOrDefault(project);
852
- if (!resolved) {
853
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
854
- }
1002
+ async ({ task_id, title, description, status, priority, tags, blocked_by }) => {
855
1003
  const db2 = getDb();
856
- let rows;
857
- if (key) {
858
- rows = db2.prepare("SELECT * FROM context WHERE project_id = ? AND key = ?").all(resolved.id, key);
859
- } else {
860
- rows = db2.prepare("SELECT * FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
1004
+ const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(task_id);
1005
+ if (!existing) {
1006
+ return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
1007
+ }
1008
+ const sessionPreamble = maybeAutoSession(existing.project_id);
1009
+ const updates = [];
1010
+ const params = [];
1011
+ if (title !== void 0) {
1012
+ updates.push("title = ?");
1013
+ params.push(title);
1014
+ }
1015
+ if (description !== void 0) {
1016
+ updates.push("description = ?");
1017
+ params.push(description);
1018
+ }
1019
+ if (status !== void 0) {
1020
+ updates.push("status = ?");
1021
+ params.push(status);
1022
+ if (status === "done") {
1023
+ updates.push("completed_at = CURRENT_TIMESTAMP");
1024
+ }
1025
+ }
1026
+ if (priority !== void 0) {
1027
+ updates.push("priority = ?");
1028
+ params.push(priority);
1029
+ }
1030
+ if (tags !== void 0) {
1031
+ updates.push("tags = ?");
1032
+ params.push(JSON.stringify(tags));
1033
+ }
1034
+ if (blocked_by !== void 0) {
1035
+ updates.push("blocked_by = ?");
1036
+ params.push(JSON.stringify(blocked_by));
1037
+ if (blocked_by.length > 0 && status === void 0) {
1038
+ updates.push("status = 'blocked'");
1039
+ }
861
1040
  }
1041
+ if (updates.length === 0) {
1042
+ return { content: [{ type: "text", text: "No updates provided." }], isError: true };
1043
+ }
1044
+ params.push(task_id);
1045
+ db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...params);
1046
+ const resultText = JSON.stringify({ task_id, message: `Task "${existing.title}" updated.` });
862
1047
  return {
863
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, context: rows }, null, 2) }]
864
- };
865
- }
866
- );
867
- }
868
-
869
- // src/tools/sessions.ts
870
- import { z as z5 } from "zod/v4";
1048
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
871
1049
 
872
- // src/server/http.ts
873
- import { createServer } from "http";
874
- import { readFile } from "fs/promises";
875
- import { join, extname } from "path";
876
- import { fileURLToPath } from "url";
877
- import { dirname as dirname2 } from "path";
878
- import { spawn } from "child_process";
1050
+ ---
879
1051
 
880
- // src/server/routes.ts
881
- var listProjects = async (_req, res) => {
882
- const db2 = getDb();
883
- const url = new URL(_req.url || "/", "http://localhost");
884
- const status = url.searchParams.get("status");
885
- const sql = `
886
- SELECT p.*,
887
- (SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND status NOT IN ('done','cancelled')) AS active_task_count,
888
- (SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND status = 'done') AS done_task_count
889
- FROM projects p
890
- ${status ? "WHERE p.status = ?" : ""}
891
- ORDER BY p.updated_at DESC
892
- `;
893
- const rows = status ? db2.prepare(sql).all(status) : db2.prepare(sql).all();
894
- sendJson(res, 200, rows);
895
- };
896
- var getProject = async (_req, res, params) => {
897
- const db2 = getDb();
898
- const project = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
899
- if (!project) {
900
- sendJson(res, 404, { error: "Project not found" });
901
- return;
902
- }
903
- const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(params.id);
904
- sendJson(res, 200, { ...project, task_counts: taskCounts });
905
- };
906
- var updateProject = async (req, res, params) => {
907
- const db2 = getDb();
908
- const body = await parseBody(req);
909
- const existing = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
910
- if (!existing) {
911
- sendJson(res, 404, { error: "Project not found" });
912
- return;
913
- }
914
- const updates = [];
915
- const sqlParams = [];
916
- if (body.name !== void 0) {
917
- updates.push("name = ?");
918
- sqlParams.push(body.name);
919
- }
920
- if (body.description !== void 0) {
921
- updates.push("description = ?");
922
- sqlParams.push(body.description);
923
- }
924
- if (body.status !== void 0) {
925
- updates.push("status = ?");
926
- sqlParams.push(body.status);
927
- }
928
- if (updates.length === 0) {
929
- sendJson(res, 400, { error: "No updates provided" });
930
- return;
931
- }
932
- sqlParams.push(params.id);
933
- try {
934
- db2.prepare(`UPDATE projects SET ${updates.join(", ")} WHERE id = ?`).run(...sqlParams);
935
- } catch (e) {
936
- if (e.message?.includes("UNIQUE constraint failed")) {
937
- sendJson(res, 409, { error: "A project with that name already exists" });
938
- return;
1052
+ ${resultText}` : resultText }]
1053
+ };
939
1054
  }
940
- throw e;
941
- }
942
- const updated = db2.prepare("SELECT * FROM projects WHERE id = ?").get(params.id);
943
- sendJson(res, 200, updated);
944
- };
945
- var listTasks = async (req, res, params) => {
946
- const db2 = getDb();
947
- const url = new URL(req.url || "/", "http://localhost");
948
- const includeDone = url.searchParams.get("include_done") === "true";
949
- let sql = "SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.project_id = ?";
950
- if (!includeDone) {
951
- sql += " AND t.status NOT IN ('done', 'cancelled')";
952
- }
953
- sql += " ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, t.created_at DESC";
954
- const rows = db2.prepare(sql).all(params.pid);
955
- sendJson(res, 200, rows);
956
- };
957
- var createTask = async (req, res, params) => {
958
- const db2 = getDb();
959
- const body = await parseBody(req);
960
- if (!body.title || typeof body.title !== "string") {
961
- sendJson(res, 400, { error: "title is required" });
962
- return;
963
- }
964
- const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(params.pid);
965
- if (!project) {
966
- sendJson(res, 404, { error: "Project not found" });
967
- return;
968
- }
969
- const id = generateId();
970
- const priority = body.priority || "medium";
971
- const tags = Array.isArray(body.tags) ? JSON.stringify(body.tags) : null;
972
- const seqRow = db2.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next_seq FROM tasks WHERE project_id = ?").get(params.pid);
973
- const seq = seqRow.next_seq;
974
- db2.prepare(
975
- "INSERT INTO tasks (id, project_id, seq, title, description, priority, tags, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
976
- ).run(
977
- id,
978
- params.pid,
979
- seq,
980
- body.title,
981
- body.description ?? null,
982
- priority,
983
- tags,
984
- body.parent_task_id ?? null
985
1055
  );
986
- const task = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.id = ?").get(id);
987
- recordTaskHistory(id, "created", null, JSON.stringify({ status: priority === "medium" ? "todo" : priority, priority }));
988
- sendJson(res, 201, task);
989
- };
990
- var updateTask = async (req, res, params) => {
991
- const db2 = getDb();
992
- const body = await parseBody(req);
993
- const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
994
- if (!existing) {
995
- sendJson(res, 404, { error: "Task not found" });
996
- return;
997
- }
998
- const updates = [];
999
- const sqlParams = [];
1000
- if (body.title !== void 0) {
1001
- updates.push("title = ?");
1002
- sqlParams.push(body.title);
1003
- }
1004
- if (body.description !== void 0) {
1005
- updates.push("description = ?");
1006
- sqlParams.push(body.description);
1007
- }
1008
- if (body.status !== void 0) {
1009
- updates.push("status = ?");
1010
- sqlParams.push(body.status);
1011
- if (body.status === "done") {
1012
- updates.push("completed_at = CURRENT_TIMESTAMP");
1013
- } else {
1014
- updates.push("completed_at = NULL");
1015
- }
1016
- }
1017
- if (body.priority !== void 0) {
1018
- updates.push("priority = ?");
1019
- sqlParams.push(body.priority);
1020
- }
1021
- if (body.tags !== void 0) {
1022
- updates.push("tags = ?");
1023
- sqlParams.push(Array.isArray(body.tags) ? JSON.stringify(body.tags) : null);
1024
- }
1025
- if (body.blocked_by !== void 0) {
1026
- updates.push("blocked_by = ?");
1027
- sqlParams.push(Array.isArray(body.blocked_by) ? JSON.stringify(body.blocked_by) : null);
1028
- if (Array.isArray(body.blocked_by) && body.blocked_by.length > 0 && body.status === void 0) {
1029
- updates.push("status = 'blocked'");
1030
- }
1031
- }
1032
- if (updates.length === 0) {
1033
- sendJson(res, 400, { error: "No updates provided" });
1034
- return;
1035
- }
1036
- sqlParams.push(params.id);
1037
- db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...sqlParams);
1038
- if (body.status !== void 0 && body.status !== existing.status) {
1039
- recordTaskHistory(params.id, "status_changed", existing.status, body.status);
1040
- }
1041
- if (body.priority !== void 0 && body.priority !== existing.priority) {
1042
- recordTaskHistory(params.id, "priority_changed", existing.priority, body.priority);
1043
- }
1044
- if (body.title !== void 0 && body.title !== existing.title) {
1045
- recordTaskHistory(params.id, "title_changed", existing.title, body.title);
1046
- }
1047
- const updated = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.id = ?").get(params.id);
1048
- sendJson(res, 200, updated);
1049
- };
1050
- var deleteTask = async (_req, res, params) => {
1051
- const db2 = getDb();
1052
- const existing = db2.prepare("SELECT * FROM tasks WHERE id = ?").get(params.id);
1053
- if (!existing) {
1054
- sendJson(res, 404, { error: "Task not found" });
1055
- return;
1056
- }
1057
- const deleteTransaction = db2.transaction((taskId) => {
1058
- const subtasks = db2.prepare("SELECT id FROM tasks WHERE parent_task_id = ?").all(taskId);
1059
- for (const sub of subtasks) {
1060
- db2.prepare("DELETE FROM task_history WHERE task_id = ?").run(sub.id);
1061
- db2.prepare("DELETE FROM notes WHERE task_id = ?").run(sub.id);
1062
- db2.prepare("DELETE FROM tasks WHERE id = ?").run(sub.id);
1056
+ server2.registerTool(
1057
+ "list_tasks",
1058
+ {
1059
+ title: "List Tasks",
1060
+ description: "List tasks with filters. Defaults to showing non-completed tasks for the most recent active project.",
1061
+ inputSchema: {
1062
+ project: z2.string().optional().describe("Project name or ID"),
1063
+ status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("Filter by status"),
1064
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority"),
1065
+ tag: z2.string().optional().describe("Filter by tag"),
1066
+ include_done: z2.boolean().optional().describe("Include completed tasks (default: false)")
1067
+ }
1068
+ },
1069
+ async ({ project, status, priority, tag, include_done }) => {
1070
+ const resolved = resolveProjectOrDefault(project);
1071
+ if (!resolved) {
1072
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1073
+ }
1074
+ const sessionPreamble = maybeAutoSession(resolved.id);
1075
+ const db2 = getDb();
1076
+ const conditions = ["t.project_id = @projectId"];
1077
+ const params = { projectId: resolved.id };
1078
+ if (status) {
1079
+ conditions.push("t.status = @status");
1080
+ params.status = status;
1081
+ } else if (!include_done) {
1082
+ conditions.push("t.status NOT IN ('done', 'cancelled')");
1083
+ }
1084
+ if (priority) {
1085
+ conditions.push("t.priority = @priority");
1086
+ params.priority = priority;
1087
+ }
1088
+ if (tag) {
1089
+ conditions.push("t.tags LIKE '%' || @tag || '%'");
1090
+ params.tag = `"${tag}"`;
1091
+ }
1092
+ const sql = `SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE ${conditions.join(" AND ")} ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, t.created_at DESC`;
1093
+ const rows = db2.prepare(sql).all(params);
1094
+ const resultText = JSON.stringify({ project: resolved.name, tasks: rows }, null, 2);
1095
+ return {
1096
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1097
+
1098
+ ---
1099
+
1100
+ ${resultText}` : resultText }]
1101
+ };
1063
1102
  }
1064
- db2.prepare("DELETE FROM task_history WHERE task_id = ?").run(taskId);
1065
- db2.prepare("DELETE FROM notes WHERE task_id = ?").run(taskId);
1066
- db2.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
1067
- });
1068
- deleteTransaction(params.id);
1069
- sendJson(res, 200, { message: "Task deleted" });
1070
- };
1071
- var getTaskHistory = async (_req, res, params) => {
1072
- const db2 = getDb();
1073
- const rows = db2.prepare(
1074
- "SELECT * FROM task_history WHERE task_id = ? ORDER BY created_at ASC"
1075
- ).all(params.id);
1076
- sendJson(res, 200, rows);
1077
- };
1078
- var createSession = async (req, res, params) => {
1079
- const db2 = getDb();
1080
- const body = await parseBody(req);
1081
- if (!body.summary || typeof body.summary !== "string") {
1082
- sendJson(res, 400, { error: "summary is required" });
1083
- return;
1084
- }
1085
- const project = db2.prepare("SELECT id FROM projects WHERE id = ?").get(params.pid);
1086
- if (!project) {
1087
- sendJson(res, 404, { error: "Project not found" });
1088
- return;
1089
- }
1090
- const id = generateId();
1091
- db2.prepare(
1092
- "INSERT INTO sessions (id, project_id, summary, next_steps) VALUES (?, ?, ?, ?)"
1093
- ).run(id, params.pid, body.summary, body.next_steps ?? null);
1094
- const session = db2.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
1095
- sendJson(res, 201, session);
1096
- };
1097
- var listDecisions = async (_req, res, params) => {
1098
- const db2 = getDb();
1099
- const rows = db2.prepare("SELECT * FROM decisions WHERE project_id = ? ORDER BY created_at DESC").all(params.pid);
1100
- sendJson(res, 200, rows);
1101
- };
1102
- var routes = [
1103
- { method: "GET", pattern: "/api/projects", handler: listProjects },
1104
- { method: "GET", pattern: "/api/projects/:id", handler: getProject },
1105
- { method: "PATCH", pattern: "/api/projects/:id", handler: updateProject },
1106
- { method: "POST", pattern: "/api/projects/:pid/sessions", handler: createSession },
1107
- { method: "GET", pattern: "/api/projects/:pid/decisions", handler: listDecisions },
1108
- { method: "GET", pattern: "/api/projects/:pid/tasks", handler: listTasks },
1109
- { method: "POST", pattern: "/api/projects/:pid/tasks", handler: createTask },
1110
- { method: "PATCH", pattern: "/api/tasks/:id", handler: updateTask },
1111
- { method: "DELETE", pattern: "/api/tasks/:id", handler: deleteTask },
1112
- { method: "GET", pattern: "/api/tasks/:id/history", handler: getTaskHistory }
1113
- ];
1114
- async function handleApiRequest(req, res) {
1115
- const url = new URL(req.url || "/", "http://localhost");
1116
- const method = req.method || "GET";
1117
- for (const route of routes) {
1118
- if (route.method !== method) continue;
1119
- const params = matchRoute(route.pattern, url.pathname);
1120
- if (params) {
1121
- await route.handler(req, res, params);
1122
- return;
1103
+ );
1104
+ server2.registerTool(
1105
+ "get_task",
1106
+ {
1107
+ title: "Get Task",
1108
+ description: "Get full detail for a specific task including sub-tasks and related notes.",
1109
+ inputSchema: {
1110
+ task_id: z2.string().describe("Task ID")
1111
+ }
1112
+ },
1113
+ async ({ task_id }) => {
1114
+ const db2 = getDb();
1115
+ const task = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.id = ?").get(task_id);
1116
+ if (!task) {
1117
+ return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
1118
+ }
1119
+ const sessionPreamble = maybeAutoSession(task.project_id);
1120
+ const subtasks = db2.prepare("SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.parent_task_id = ?").all(task_id);
1121
+ const notes = db2.prepare("SELECT * FROM notes WHERE task_id = ? ORDER BY created_at DESC").all(task_id);
1122
+ const resultText = JSON.stringify({ task, subtasks, notes }, null, 2);
1123
+ return {
1124
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1125
+
1126
+ ---
1127
+
1128
+ ${resultText}` : resultText }]
1129
+ };
1123
1130
  }
1124
- }
1125
- sendJson(res, 404, { error: "Not found" });
1126
- }
1131
+ );
1132
+ server2.registerTool(
1133
+ "get_next_tasks",
1134
+ {
1135
+ title: "Get Next Tasks",
1136
+ description: "Smart query: what should be worked on next? Returns highest priority non-blocked tasks for a project.",
1137
+ inputSchema: {
1138
+ project: z2.string().optional().describe("Project name or ID"),
1139
+ limit: z2.number().optional().describe("Max number of tasks to return (default: 5)")
1140
+ }
1141
+ },
1142
+ async ({ project, limit }) => {
1143
+ const resolved = resolveProjectOrDefault(project);
1144
+ if (!resolved) {
1145
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1146
+ }
1147
+ const sessionPreamble = maybeAutoSession(resolved.id);
1148
+ const db2 = getDb();
1149
+ const rows = db2.prepare(
1150
+ `SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id
1151
+ WHERE t.project_id = ? AND t.status IN ('todo', 'in_progress')
1152
+ ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1153
+ t.created_at ASC
1154
+ LIMIT ?`
1155
+ ).all(resolved.id, limit ?? 5);
1156
+ const resultText = JSON.stringify({ project: resolved.name, next_tasks: rows }, null, 2);
1157
+ return {
1158
+ content: [{
1159
+ type: "text",
1160
+ text: sessionPreamble ? `${sessionPreamble}
1127
1161
 
1128
- // src/server/http.ts
1129
- function openBrowser(url) {
1130
- const platform = process.platform;
1131
- const cmd = platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
1132
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1133
- spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
1134
- }
1135
- var __filename = fileURLToPath(import.meta.url);
1136
- var __dirname = dirname2(__filename);
1137
- function resolveStaticDir() {
1138
- return join(__dirname, "ui");
1162
+ ---
1163
+
1164
+ ${resultText}` : resultText
1165
+ }]
1166
+ };
1167
+ }
1168
+ );
1139
1169
  }
1140
- var MIME_TYPES = {
1141
- ".html": "text/html; charset=utf-8",
1142
- ".js": "application/javascript; charset=utf-8",
1143
- ".css": "text/css; charset=utf-8",
1144
- ".json": "application/json; charset=utf-8",
1145
- ".svg": "image/svg+xml",
1146
- ".png": "image/png",
1147
- ".jpg": "image/jpeg",
1148
- ".ico": "image/x-icon",
1149
- ".woff": "font/woff",
1150
- ".woff2": "font/woff2",
1151
- ".ttf": "font/ttf"
1152
- };
1153
- function parseBody(req) {
1154
- return new Promise((resolve2, reject) => {
1155
- const chunks = [];
1156
- req.on("data", (chunk) => chunks.push(chunk));
1157
- req.on("end", () => {
1158
- if (chunks.length === 0) {
1159
- resolve2({});
1160
- return;
1170
+
1171
+ // src/tools/decisions.ts
1172
+ import { z as z3 } from "zod/v4";
1173
+ function registerDecisionTools(server2) {
1174
+ server2.registerTool(
1175
+ "log_decision",
1176
+ {
1177
+ title: "Log Decision",
1178
+ description: "Record a decision with reasoning and alternatives considered. Proactively use this when the user makes a technical decision, chooses between options, or settles a debate.",
1179
+ inputSchema: {
1180
+ project: z3.string().optional().describe("Project name or ID"),
1181
+ task_id: z3.string().optional().describe("Task ID to associate this decision with (omit for project-level)"),
1182
+ title: z3.string().describe("Short title for the decision"),
1183
+ decision: z3.string().describe("What was decided"),
1184
+ reasoning: z3.string().optional().describe("Why this was decided"),
1185
+ alternatives: z3.array(z3.string()).optional().describe("Rejected alternatives"),
1186
+ tags: z3.array(z3.string()).optional().describe('Tags like "architecture", "database", "api"')
1161
1187
  }
1162
- try {
1163
- resolve2(JSON.parse(Buffer.concat(chunks).toString()));
1164
- } catch {
1165
- resolve2({});
1188
+ },
1189
+ async ({ project, task_id, title, decision, reasoning, alternatives, tags }) => {
1190
+ const resolved = resolveProjectOrDefault(project);
1191
+ if (!resolved) {
1192
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1166
1193
  }
1167
- });
1168
- req.on("error", reject);
1169
- });
1170
- }
1171
- function matchRoute(pattern, pathname) {
1172
- const patternParts = pattern.split("/").filter(Boolean);
1173
- const pathParts = pathname.split("/").filter(Boolean);
1174
- if (patternParts.length !== pathParts.length) return null;
1175
- const params = {};
1176
- for (let i = 0; i < patternParts.length; i++) {
1177
- if (patternParts[i].startsWith(":")) {
1178
- params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
1179
- } else if (patternParts[i] !== pathParts[i]) {
1180
- return null;
1194
+ const sessionPreamble = maybeAutoSession(resolved.id);
1195
+ const db2 = getDb();
1196
+ const id = generateId();
1197
+ db2.prepare(
1198
+ `INSERT INTO decisions (id, project_id, task_id, title, decision, reasoning, alternatives, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1199
+ ).run(
1200
+ id,
1201
+ resolved.id,
1202
+ task_id ?? null,
1203
+ title,
1204
+ decision,
1205
+ reasoning ?? null,
1206
+ alternatives ? JSON.stringify(alternatives) : null,
1207
+ tags ? JSON.stringify(tags) : null
1208
+ );
1209
+ const scope = task_id ? `task ${task_id} in ${resolved.name}` : resolved.name;
1210
+ const resultText = JSON.stringify({ decision_id: id, message: `Decision logged: "${title}" in ${scope}` });
1211
+ return {
1212
+ content: [{
1213
+ type: "text",
1214
+ text: sessionPreamble ? `${sessionPreamble}
1215
+
1216
+ ---
1217
+
1218
+ ${resultText}` : resultText
1219
+ }]
1220
+ };
1221
+ }
1222
+ );
1223
+ server2.registerTool(
1224
+ "list_decisions",
1225
+ {
1226
+ title: "List Decisions",
1227
+ description: "List decisions for a project. Filter by tags to find specific decisions.",
1228
+ inputSchema: {
1229
+ project: z3.string().optional().describe("Project name or ID"),
1230
+ tag: z3.string().optional().describe("Filter by tag"),
1231
+ limit: z3.number().optional().describe("Max number of decisions to return (default: 20)")
1232
+ }
1233
+ },
1234
+ async ({ project, tag, limit }) => {
1235
+ const resolved = resolveProjectOrDefault(project);
1236
+ if (!resolved) {
1237
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1238
+ }
1239
+ const sessionPreamble = maybeAutoSession(resolved.id);
1240
+ const db2 = getDb();
1241
+ let sql;
1242
+ const params = [resolved.id];
1243
+ if (tag) {
1244
+ sql = `SELECT * FROM decisions WHERE project_id = ? AND tags LIKE ? ORDER BY created_at DESC LIMIT ?`;
1245
+ params.push(`%"${tag}"%`, limit ?? 20);
1246
+ } else {
1247
+ sql = `SELECT * FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`;
1248
+ params.push(limit ?? 20);
1249
+ }
1250
+ const rows = db2.prepare(sql).all(...params);
1251
+ const resultText = JSON.stringify({ project: resolved.name, decisions: rows }, null, 2);
1252
+ return {
1253
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1254
+
1255
+ ---
1256
+
1257
+ ${resultText}` : resultText }]
1258
+ };
1181
1259
  }
1182
- }
1183
- return params;
1184
- }
1185
- function sendJson(res, status, data) {
1186
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
1187
- res.end(JSON.stringify(data));
1260
+ );
1188
1261
  }
1189
- async function serveStatic(req, res) {
1190
- const staticDir = resolveStaticDir();
1191
- const url = new URL(req.url || "/", "http://localhost");
1192
- let filePath = join(staticDir, url.pathname === "/" ? "index.html" : url.pathname);
1193
- try {
1194
- const content = await readFile(filePath);
1195
- const ext = extname(filePath);
1196
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
1197
- res.writeHead(200, { "Content-Type": contentType });
1198
- res.end(content);
1199
- } catch {
1200
- try {
1201
- const indexPath = join(staticDir, "index.html");
1202
- const content = await readFile(indexPath);
1203
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1204
- res.end(content);
1205
- } catch {
1206
- res.writeHead(503, { "Content-Type": "text/html; charset=utf-8" });
1207
- res.end(
1208
- "<html><body><h1>mindpm UI not built</h1><p>Run <code>npm run build:ui</code> to build the Kanban UI.</p></body></html>"
1262
+
1263
+ // src/tools/notes.ts
1264
+ import { z as z4 } from "zod/v4";
1265
+ function registerNoteTools(server2) {
1266
+ server2.registerTool(
1267
+ "add_note",
1268
+ {
1269
+ title: "Add Note",
1270
+ description: "Add a note to a project or task. Proactively use this when the user shares context about architecture, bugs, ideas, research findings, or any important information worth remembering.",
1271
+ inputSchema: {
1272
+ project: z4.string().optional().describe("Project name or ID"),
1273
+ content: z4.string().describe("The note content"),
1274
+ category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Note category (default: general)"),
1275
+ task_id: z4.string().optional().describe("Link this note to a specific task"),
1276
+ tags: z4.array(z4.string()).optional().describe("Tags for categorization")
1277
+ }
1278
+ },
1279
+ async ({ project, content, category, task_id, tags }) => {
1280
+ const resolved = resolveProjectOrDefault(project);
1281
+ if (!resolved) {
1282
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1283
+ }
1284
+ const sessionPreamble = maybeAutoSession(resolved.id);
1285
+ const db2 = getDb();
1286
+ const id = generateId();
1287
+ db2.prepare(
1288
+ `INSERT INTO notes (id, project_id, task_id, content, category, tags) VALUES (?, ?, ?, ?, ?, ?)`
1289
+ ).run(
1290
+ id,
1291
+ resolved.id,
1292
+ task_id ?? null,
1293
+ content,
1294
+ category ?? "general",
1295
+ tags ? JSON.stringify(tags) : null
1209
1296
  );
1297
+ const resultText = JSON.stringify({ note_id: id, message: `Note added to ${resolved.name} (${category ?? "general"})` });
1298
+ return {
1299
+ content: [{
1300
+ type: "text",
1301
+ text: sessionPreamble ? `${sessionPreamble}
1302
+
1303
+ ---
1304
+
1305
+ ${resultText}` : resultText
1306
+ }]
1307
+ };
1210
1308
  }
1211
- }
1212
- }
1213
- var _httpPort = null;
1214
- function getHttpPort() {
1215
- return _httpPort;
1216
- }
1217
- function startHttpServer(port) {
1218
- _httpPort = port;
1219
- const server2 = createServer(async (req, res) => {
1220
- try {
1221
- if (req.url?.startsWith("/api/")) {
1222
- process.stderr.write(`[mindpm] API request: ${req.method} ${req.url}
1223
- `);
1224
- await handleApiRequest(req, res);
1225
- } else {
1226
- await serveStatic(req, res);
1309
+ );
1310
+ server2.registerTool(
1311
+ "search_notes",
1312
+ {
1313
+ title: "Search Notes",
1314
+ description: "Full-text search across notes for a project.",
1315
+ inputSchema: {
1316
+ project: z4.string().optional().describe("Project name or ID"),
1317
+ query: z4.string().describe("Search query"),
1318
+ category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Filter by category")
1227
1319
  }
1228
- } catch (err) {
1229
- process.stderr.write(`[mindpm] HTTP error on ${req.method} ${req.url}: ${err}
1230
- `);
1231
- if (!res.headersSent) {
1232
- sendJson(res, 500, { error: "Internal server error", detail: String(err) });
1320
+ },
1321
+ async ({ project, query, category }) => {
1322
+ const resolved = resolveProjectOrDefault(project);
1323
+ if (!resolved) {
1324
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1325
+ }
1326
+ const sessionPreamble = maybeAutoSession(resolved.id);
1327
+ const db2 = getDb();
1328
+ const conditions = ["project_id = ?"];
1329
+ const params = [resolved.id];
1330
+ conditions.push("content LIKE '%' || ? || '%'");
1331
+ params.push(query);
1332
+ if (category) {
1333
+ conditions.push("category = ?");
1334
+ params.push(category);
1233
1335
  }
1336
+ const sql = `SELECT * FROM notes WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
1337
+ const rows = db2.prepare(sql).all(...params);
1338
+ const resultText = JSON.stringify({ project: resolved.name, results: rows }, null, 2);
1339
+ return {
1340
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1341
+
1342
+ ---
1343
+
1344
+ ${resultText}` : resultText }]
1345
+ };
1234
1346
  }
1235
- });
1236
- server2.on("error", (err) => {
1237
- if (err.code === "EADDRINUSE") {
1238
- process.stderr.write(
1239
- `[mindpm] Port ${port} already in use. Kanban UI served by existing process at http://localhost:${port}
1240
- `
1241
- );
1242
- } else {
1243
- _httpPort = null;
1244
- process.stderr.write(`[mindpm] HTTP server error: ${err.message}
1245
- `);
1347
+ );
1348
+ server2.registerTool(
1349
+ "set_context",
1350
+ {
1351
+ title: "Set Context",
1352
+ description: "Set a key-value context pair for a project (upsert). Proactively use this when the user shares important project context like architecture decisions, config values, conventions, or constraints.",
1353
+ inputSchema: {
1354
+ project: z4.string().optional().describe("Project name or ID"),
1355
+ key: z4.string().describe('Context key, e.g. "auth_approach", "deployment_target", "api_base_url"'),
1356
+ value: z4.string().describe("Context value"),
1357
+ category: z4.string().optional().describe('Category like "architecture", "config", "convention", "constraint"')
1358
+ }
1359
+ },
1360
+ async ({ project, key, value, category }) => {
1361
+ const resolved = resolveProjectOrDefault(project);
1362
+ if (!resolved) {
1363
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1364
+ }
1365
+ const sessionPreamble = maybeAutoSession(resolved.id);
1366
+ const db2 = getDb();
1367
+ const id = generateId();
1368
+ db2.prepare(
1369
+ `INSERT INTO context (id, project_id, key, value, category)
1370
+ VALUES (?, ?, ?, ?, ?)
1371
+ ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, category = excluded.category`
1372
+ ).run(id, resolved.id, key, value, category ?? "general");
1373
+ const resultText = JSON.stringify({ message: `Context set: "${key}" = "${value}" in ${resolved.name}` });
1374
+ return {
1375
+ content: [{
1376
+ type: "text",
1377
+ text: sessionPreamble ? `${sessionPreamble}
1378
+
1379
+ ---
1380
+
1381
+ ${resultText}` : resultText
1382
+ }]
1383
+ };
1246
1384
  }
1247
- });
1248
- server2.listen(port, () => {
1249
- const url = `http://localhost:${port}`;
1250
- process.stderr.write(`[mindpm] Kanban UI available at ${url}
1251
- `);
1252
- if (process.env.MINDPM_OPEN_BROWSER === "1") {
1253
- openBrowser(url);
1385
+ );
1386
+ server2.registerTool(
1387
+ "get_context",
1388
+ {
1389
+ title: "Get Context",
1390
+ description: "Get context by key or list all context for a project.",
1391
+ inputSchema: {
1392
+ project: z4.string().optional().describe("Project name or ID"),
1393
+ key: z4.string().optional().describe("Specific context key to retrieve. If omitted, returns all context.")
1394
+ }
1395
+ },
1396
+ async ({ project, key }) => {
1397
+ const resolved = resolveProjectOrDefault(project);
1398
+ if (!resolved) {
1399
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1400
+ }
1401
+ const sessionPreamble = maybeAutoSession(resolved.id);
1402
+ const db2 = getDb();
1403
+ let rows;
1404
+ if (key) {
1405
+ rows = db2.prepare("SELECT * FROM context WHERE project_id = ? AND key = ?").all(resolved.id, key);
1406
+ } else {
1407
+ rows = db2.prepare("SELECT * FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
1408
+ }
1409
+ const resultText = JSON.stringify({ project: resolved.name, context: rows }, null, 2);
1410
+ return {
1411
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1412
+
1413
+ ---
1414
+
1415
+ ${resultText}` : resultText }]
1416
+ };
1254
1417
  }
1255
- });
1256
- return server2;
1418
+ );
1257
1419
  }
1258
1420
 
1259
1421
  // src/tools/sessions.ts
1260
- function getActivitySince(db2, projectId, cutoffTime) {
1261
- return db2.prepare(`
1262
- SELECT 'task_created' as type, id, title, created_at as timestamp
1263
- FROM tasks
1264
- WHERE project_id = ? AND created_at > ?
1265
- UNION ALL
1266
- SELECT 'task_updated' as type, id, title, updated_at as timestamp
1267
- FROM tasks
1268
- WHERE project_id = ? AND updated_at > ? AND updated_at != created_at
1269
- UNION ALL
1270
- SELECT 'decision' as type, id, title, created_at as timestamp
1271
- FROM decisions
1272
- WHERE project_id = ? AND created_at > ?
1273
- UNION ALL
1274
- SELECT 'note' as type, id, substr(content, 1, 80) as title, created_at as timestamp
1275
- FROM notes
1276
- WHERE project_id = ? AND created_at > ?
1277
- ORDER BY timestamp DESC
1278
- `).all(
1279
- projectId,
1280
- cutoffTime,
1281
- projectId,
1282
- cutoffTime,
1283
- projectId,
1284
- cutoffTime,
1285
- projectId,
1286
- cutoffTime
1287
- );
1288
- }
1422
+ import { z as z5 } from "zod/v4";
1289
1423
  function registerSessionTools(server2) {
1290
1424
  server2.registerTool(
1291
1425
  "start_session",
@@ -1301,62 +1435,9 @@ function registerSessionTools(server2) {
1301
1435
  if (!resolved) {
1302
1436
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1303
1437
  }
1304
- const db2 = getDb();
1305
- const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(resolved.id);
1306
- let lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
1307
- const cutoffTime = lastSession?.created_at ?? "1970-01-01";
1308
- const recentActivity = getActivitySince(db2, resolved.id, cutoffTime);
1309
- if (recentActivity.length > 0) {
1310
- const taskIds = [...new Set(
1311
- recentActivity.filter((a) => a.type === "task_created" || a.type === "task_updated").map((a) => a.id)
1312
- )];
1313
- const decisionIds = [...new Set(
1314
- recentActivity.filter((a) => a.type === "decision").map((a) => a.id)
1315
- )];
1316
- const syntheticId = generateId();
1317
- db2.prepare(
1318
- `INSERT INTO sessions (id, project_id, summary, tasks_worked_on, decisions_made) VALUES (?, ?, ?, ?, ?)`
1319
- ).run(
1320
- syntheticId,
1321
- resolved.id,
1322
- `Auto-generated: ${recentActivity.length} activities since last session`,
1323
- taskIds.length > 0 ? JSON.stringify(taskIds) : null,
1324
- decisionIds.length > 0 ? JSON.stringify(decisionIds) : null
1325
- );
1326
- lastSession = db2.prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY created_at DESC LIMIT 1").get(resolved.id);
1327
- }
1328
- const activeTasks = db2.prepare(
1329
- `SELECT id, title, status, priority, tags FROM tasks
1330
- WHERE project_id = ? AND status NOT IN ('done', 'cancelled')
1331
- ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`
1332
- ).all(resolved.id);
1333
- const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
1334
- 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);
1335
- const taskCounts = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
1336
- const contextItems = db2.prepare("SELECT key, value, category FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
1337
- db2.prepare("UPDATE projects SET status = status WHERE id = ?").run(resolved.id);
1338
- const port = getHttpPort();
1339
- const kanbanUrl = port ? `http://localhost:${port}?project=${resolved.id}` : null;
1340
- const result = {
1341
- kanban_url: kanbanUrl,
1342
- project: projectRow,
1343
- last_session: lastSession ? {
1344
- summary: lastSession.summary,
1345
- next_steps: lastSession.next_steps,
1346
- when: lastSession.created_at
1347
- } : null,
1348
- recent_activity: recentActivity.slice(0, 20),
1349
- task_summary: taskCounts,
1350
- active_tasks: activeTasks,
1351
- blocked_tasks: blockedTasks,
1352
- recent_decisions: recentDecisions,
1353
- context: contextItems
1354
- };
1355
- const kanbanLine = kanbanUrl ? `Kanban board: ${kanbanUrl}` : "Kanban board: unavailable (HTTP server not running)";
1438
+ markSessionStarted(resolved.id);
1356
1439
  return {
1357
- content: [{ type: "text", text: `${kanbanLine}
1358
-
1359
- ${JSON.stringify(result, null, 2)}` }]
1440
+ content: [{ type: "text", text: buildSessionText(resolved.id) }]
1360
1441
  };
1361
1442
  }
1362
1443
  );
@@ -1446,6 +1527,7 @@ function registerQueryTools(server2) {
1446
1527
  if (!resolved) {
1447
1528
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1448
1529
  }
1530
+ const sessionPreamble = maybeAutoSession(resolved.id);
1449
1531
  const db2 = getDb();
1450
1532
  const tasksByStatus = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
1451
1533
  const blockers = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
@@ -1467,21 +1549,26 @@ function registerQueryTools(server2) {
1467
1549
  const totalNotes = db2.prepare("SELECT COUNT(*) as count FROM notes WHERE project_id = ?").get(resolved.id);
1468
1550
  const totalDecisions = db2.prepare("SELECT COUNT(*) as count FROM decisions WHERE project_id = ?").get(resolved.id);
1469
1551
  const totalSessions = db2.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(resolved.id);
1552
+ const resultText = JSON.stringify(
1553
+ {
1554
+ project: resolved.name,
1555
+ tasks_by_status: tasksByStatus,
1556
+ blockers,
1557
+ upcoming_priorities: upcomingPriorities,
1558
+ recent_activity: recentActivity,
1559
+ totals: { notes: totalNotes.count, decisions: totalDecisions.count, sessions: totalSessions.count }
1560
+ },
1561
+ null,
1562
+ 2
1563
+ );
1470
1564
  return {
1471
1565
  content: [{
1472
1566
  type: "text",
1473
- text: JSON.stringify(
1474
- {
1475
- project: resolved.name,
1476
- tasks_by_status: tasksByStatus,
1477
- blockers,
1478
- upcoming_priorities: upcomingPriorities,
1479
- recent_activity: recentActivity,
1480
- totals: { notes: totalNotes.count, decisions: totalDecisions.count, sessions: totalSessions.count }
1481
- },
1482
- null,
1483
- 2
1484
- )
1567
+ text: sessionPreamble ? `${sessionPreamble}
1568
+
1569
+ ---
1570
+
1571
+ ${resultText}` : resultText
1485
1572
  }]
1486
1573
  };
1487
1574
  }
@@ -1500,6 +1587,7 @@ function registerQueryTools(server2) {
1500
1587
  if (!resolved) {
1501
1588
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1502
1589
  }
1590
+ const sessionPreamble = maybeAutoSession(resolved.id);
1503
1591
  const db2 = getDb();
1504
1592
  const blockers = db2.prepare("SELECT * FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
1505
1593
  const enriched = blockers.map((task) => {
@@ -1516,8 +1604,13 @@ function registerQueryTools(server2) {
1516
1604
  }
1517
1605
  return { ...task, blocking_tasks: blockingTasks };
1518
1606
  });
1607
+ const resultText = JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2);
1519
1608
  return {
1520
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2) }]
1609
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1610
+
1611
+ ---
1612
+
1613
+ ${resultText}` : resultText }]
1521
1614
  };
1522
1615
  }
1523
1616
  );
@@ -1536,24 +1629,30 @@ function registerQueryTools(server2) {
1536
1629
  if (!resolved) {
1537
1630
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1538
1631
  }
1632
+ const sessionPreamble = maybeAutoSession(resolved.id);
1539
1633
  const db2 = getDb();
1540
1634
  const pattern = `%${query}%`;
1541
1635
  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);
1542
1636
  const notes = db2.prepare("SELECT id, content, category, 'note' as type FROM notes WHERE project_id = ? AND content LIKE ?").all(resolved.id, pattern);
1543
1637
  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);
1638
+ const resultText = JSON.stringify(
1639
+ {
1640
+ project: resolved.name,
1641
+ query,
1642
+ results: { tasks, notes, decisions },
1643
+ total: tasks.length + notes.length + decisions.length
1644
+ },
1645
+ null,
1646
+ 2
1647
+ );
1544
1648
  return {
1545
1649
  content: [{
1546
1650
  type: "text",
1547
- text: JSON.stringify(
1548
- {
1549
- project: resolved.name,
1550
- query,
1551
- results: { tasks, notes, decisions },
1552
- total: tasks.length + notes.length + decisions.length
1553
- },
1554
- null,
1555
- 2
1556
- )
1651
+ text: sessionPreamble ? `${sessionPreamble}
1652
+
1653
+ ---
1654
+
1655
+ ${resultText}` : resultText
1557
1656
  }]
1558
1657
  };
1559
1658
  }