mindpm 1.2.26 → 1.2.28

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,6 +343,486 @@ function recordTaskHistory(taskId, event, oldValue, newValue) {
343
343
  ).run(generateId(), taskId, event, oldValue, newValue);
344
344
  }
345
345
 
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;
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
459
+ );
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");
489
+ }
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'");
504
+ }
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);
537
+ }
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;
597
+ }
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;
635
+ }
636
+ try {
637
+ resolve2(JSON.parse(Buffer.concat(chunks).toString()));
638
+ } catch {
639
+ resolve2({});
640
+ }
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;
655
+ }
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
+ );
684
+ }
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);
701
+ }
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) });
707
+ }
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
761
+ );
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
+ }
825
+
346
826
  // src/tools/projects.ts
347
827
  function registerProjectTools(server2) {
348
828
  server2.registerTool(
@@ -419,6 +899,7 @@ function registerProjectTools(server2) {
419
899
  if (!projectId) {
420
900
  return { content: [{ type: "text", text: `Project "${project}" not found.` }], isError: true };
421
901
  }
902
+ const sessionPreamble = maybeAutoSession(projectId);
422
903
  const projectRow = db2.prepare("SELECT * FROM projects WHERE id = ?").get(projectId);
423
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);
424
905
  const blockedTasks = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(projectId);
@@ -433,8 +914,13 @@ function registerProjectTools(server2) {
433
914
  recent_decisions: recentDecisions,
434
915
  last_session: lastSession
435
916
  };
917
+ const resultText = JSON.stringify(result, null, 2);
436
918
  return {
437
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
919
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
920
+
921
+ ---
922
+
923
+ ${resultText}` : resultText }]
438
924
  };
439
925
  }
440
926
  );
@@ -530,762 +1016,380 @@ function registerTaskTools(server2) {
530
1016
  updates.push("completed_at = CURRENT_TIMESTAMP");
531
1017
  }
532
1018
  }
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
- };
556
- }
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;
588
- }
589
- if (tag) {
590
- conditions.push("tags LIKE '%' || @tag || '%'");
591
- params.tag = `"${tag}"`;
592
- }
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
- };
598
- }
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
- };
620
- }
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)")
630
- }
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 };
636
- }
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
- }
652
- );
653
- }
654
-
655
- // src/tools/decisions.ts
656
- import { z as z3 } from "zod/v4";
657
- function registerDecisionTools(server2) {
658
- server2.registerTool(
659
- "log_decision",
660
- {
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.",
663
- 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"')
671
- }
672
- },
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
- }
678
- const db2 = getDb();
679
- 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;
693
- return {
694
- content: [{
695
- type: "text",
696
- text: JSON.stringify({ decision_id: id, message: `Decision logged: "${title}" in ${scope}` })
697
- }]
698
- };
699
- }
700
- );
701
- server2.registerTool(
702
- "list_decisions",
703
- {
704
- title: "List Decisions",
705
- description: "List decisions for a project. Filter by tags to find specific decisions.",
706
- 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)")
710
- }
711
- },
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
- }
717
- 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);
723
- } 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 };
755
- }
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
- return {
769
- content: [{
770
- type: "text",
771
- text: JSON.stringify({ note_id: id, message: `Note added to ${resolved.name} (${category ?? "general"})` })
772
- }]
773
- };
774
- }
775
- );
776
- server2.registerTool(
777
- "search_notes",
778
- {
779
- title: "Search Notes",
780
- description: "Full-text search across notes for a project.",
781
- 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")
785
- }
786
- },
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
- }
792
- 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);
1019
+ if (priority !== void 0) {
1020
+ updates.push("priority = ?");
1021
+ params.push(priority);
800
1022
  }
801
- const sql = `SELECT * FROM notes WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
802
- const rows = db2.prepare(sql).all(...params);
803
- return {
804
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, results: rows }, null, 2) }]
805
- };
806
- }
807
- );
808
- server2.registerTool(
809
- "set_context",
810
- {
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.",
813
- 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"')
1023
+ if (tags !== void 0) {
1024
+ updates.push("tags = ?");
1025
+ params.push(JSON.stringify(tags));
818
1026
  }
819
- },
820
- async ({ project, key, value, category }) => {
821
- const resolved = resolveProjectOrDefault(project);
822
- if (!resolved) {
823
- return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1027
+ if (blocked_by !== void 0) {
1028
+ updates.push("blocked_by = ?");
1029
+ params.push(JSON.stringify(blocked_by));
1030
+ if (blocked_by.length > 0 && status === void 0) {
1031
+ updates.push("status = 'blocked'");
1032
+ }
824
1033
  }
825
- const db2 = getDb();
826
- const id = generateId();
827
- 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");
1034
+ if (updates.length === 0) {
1035
+ return { content: [{ type: "text", text: "No updates provided." }], isError: true };
1036
+ }
1037
+ params.push(task_id);
1038
+ db2.prepare(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`).run(...params);
832
1039
  return {
833
- content: [{
834
- type: "text",
835
- text: JSON.stringify({ message: `Context set: "${key}" = "${value}" in ${resolved.name}` })
836
- }]
1040
+ content: [{ type: "text", text: JSON.stringify({ task_id, message: `Task "${existing.title}" updated.` }) }]
837
1041
  };
838
1042
  }
839
1043
  );
840
1044
  server2.registerTool(
841
- "get_context",
1045
+ "list_tasks",
842
1046
  {
843
- title: "Get Context",
844
- description: "Get context by key or list all context for a project.",
1047
+ title: "List Tasks",
1048
+ description: "List tasks with filters. Defaults to showing non-completed tasks for the most recent active project.",
845
1049
  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.")
1050
+ project: z2.string().optional().describe("Project name or ID"),
1051
+ status: z2.enum(["todo", "in_progress", "blocked", "done", "cancelled"]).optional().describe("Filter by status"),
1052
+ priority: z2.enum(["critical", "high", "medium", "low"]).optional().describe("Filter by priority"),
1053
+ tag: z2.string().optional().describe("Filter by tag"),
1054
+ include_done: z2.boolean().optional().describe("Include completed tasks (default: false)")
848
1055
  }
849
1056
  },
850
- async ({ project, key }) => {
1057
+ async ({ project, status, priority, tag, include_done }) => {
851
1058
  const resolved = resolveProjectOrDefault(project);
852
1059
  if (!resolved) {
853
1060
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
854
1061
  }
1062
+ const sessionPreamble = maybeAutoSession(resolved.id);
855
1063
  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);
1064
+ const conditions = ["t.project_id = @projectId"];
1065
+ const params = { projectId: resolved.id };
1066
+ if (status) {
1067
+ conditions.push("t.status = @status");
1068
+ params.status = status;
1069
+ } else if (!include_done) {
1070
+ conditions.push("t.status NOT IN ('done', 'cancelled')");
1071
+ }
1072
+ if (priority) {
1073
+ conditions.push("t.priority = @priority");
1074
+ params.priority = priority;
1075
+ }
1076
+ if (tag) {
1077
+ conditions.push("t.tags LIKE '%' || @tag || '%'");
1078
+ params.tag = `"${tag}"`;
861
1079
  }
1080
+ 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`;
1081
+ const rows = db2.prepare(sql).all(params);
1082
+ const resultText = JSON.stringify({ project: resolved.name, tasks: rows }, null, 2);
862
1083
  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";
871
-
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";
879
-
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;
939
- }
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
- );
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'");
1084
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1085
+
1086
+ ---
1087
+
1088
+ ${resultText}` : resultText }]
1089
+ };
1030
1090
  }
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);
1091
+ );
1092
+ server2.registerTool(
1093
+ "get_task",
1094
+ {
1095
+ title: "Get Task",
1096
+ description: "Get full detail for a specific task including sub-tasks and related notes.",
1097
+ inputSchema: {
1098
+ task_id: z2.string().describe("Task ID")
1099
+ }
1100
+ },
1101
+ async ({ task_id }) => {
1102
+ const db2 = getDb();
1103
+ 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);
1104
+ if (!task) {
1105
+ return { content: [{ type: "text", text: `Task "${task_id}" not found.` }], isError: true };
1106
+ }
1107
+ const sessionPreamble = maybeAutoSession(task.project_id);
1108
+ 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);
1109
+ const notes = db2.prepare("SELECT * FROM notes WHERE task_id = ? ORDER BY created_at DESC").all(task_id);
1110
+ const resultText = JSON.stringify({ task, subtasks, notes }, null, 2);
1111
+ return {
1112
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1113
+
1114
+ ---
1115
+
1116
+ ${resultText}` : resultText }]
1117
+ };
1063
1118
  }
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;
1119
+ );
1120
+ server2.registerTool(
1121
+ "get_next_tasks",
1122
+ {
1123
+ title: "Get Next Tasks",
1124
+ description: "Smart query: what should be worked on next? Returns highest priority non-blocked tasks for a project.",
1125
+ inputSchema: {
1126
+ project: z2.string().optional().describe("Project name or ID"),
1127
+ limit: z2.number().optional().describe("Max number of tasks to return (default: 5)")
1128
+ }
1129
+ },
1130
+ async ({ project, limit }) => {
1131
+ const resolved = resolveProjectOrDefault(project);
1132
+ if (!resolved) {
1133
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1134
+ }
1135
+ const sessionPreamble = maybeAutoSession(resolved.id);
1136
+ const db2 = getDb();
1137
+ const rows = db2.prepare(
1138
+ `SELECT t.*, p.slug || '-' || t.seq AS short_id FROM tasks t JOIN projects p ON t.project_id = p.id
1139
+ WHERE t.project_id = ? AND t.status IN ('todo', 'in_progress')
1140
+ ORDER BY CASE t.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
1141
+ t.created_at ASC
1142
+ LIMIT ?`
1143
+ ).all(resolved.id, limit ?? 5);
1144
+ const resultText = JSON.stringify({ project: resolved.name, next_tasks: rows }, null, 2);
1145
+ return {
1146
+ content: [{
1147
+ type: "text",
1148
+ text: sessionPreamble ? `${sessionPreamble}
1149
+
1150
+ ---
1151
+
1152
+ ${resultText}` : resultText
1153
+ }]
1154
+ };
1123
1155
  }
1124
- }
1125
- sendJson(res, 404, { error: "Not found" });
1156
+ );
1126
1157
  }
1127
1158
 
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");
1139
- }
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;
1159
+ // src/tools/decisions.ts
1160
+ import { z as z3 } from "zod/v4";
1161
+ function registerDecisionTools(server2) {
1162
+ server2.registerTool(
1163
+ "log_decision",
1164
+ {
1165
+ title: "Log Decision",
1166
+ 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.",
1167
+ inputSchema: {
1168
+ project: z3.string().optional().describe("Project name or ID"),
1169
+ task_id: z3.string().optional().describe("Task ID to associate this decision with (omit for project-level)"),
1170
+ title: z3.string().describe("Short title for the decision"),
1171
+ decision: z3.string().describe("What was decided"),
1172
+ reasoning: z3.string().optional().describe("Why this was decided"),
1173
+ alternatives: z3.array(z3.string()).optional().describe("Rejected alternatives"),
1174
+ tags: z3.array(z3.string()).optional().describe('Tags like "architecture", "database", "api"')
1161
1175
  }
1162
- try {
1163
- resolve2(JSON.parse(Buffer.concat(chunks).toString()));
1164
- } catch {
1165
- resolve2({});
1176
+ },
1177
+ async ({ project, task_id, title, decision, reasoning, alternatives, tags }) => {
1178
+ const resolved = resolveProjectOrDefault(project);
1179
+ if (!resolved) {
1180
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1181
+ }
1182
+ const db2 = getDb();
1183
+ const id = generateId();
1184
+ db2.prepare(
1185
+ `INSERT INTO decisions (id, project_id, task_id, title, decision, reasoning, alternatives, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1186
+ ).run(
1187
+ id,
1188
+ resolved.id,
1189
+ task_id ?? null,
1190
+ title,
1191
+ decision,
1192
+ reasoning ?? null,
1193
+ alternatives ? JSON.stringify(alternatives) : null,
1194
+ tags ? JSON.stringify(tags) : null
1195
+ );
1196
+ const scope = task_id ? `task ${task_id} in ${resolved.name}` : resolved.name;
1197
+ return {
1198
+ content: [{
1199
+ type: "text",
1200
+ text: JSON.stringify({ decision_id: id, message: `Decision logged: "${title}" in ${scope}` })
1201
+ }]
1202
+ };
1203
+ }
1204
+ );
1205
+ server2.registerTool(
1206
+ "list_decisions",
1207
+ {
1208
+ title: "List Decisions",
1209
+ description: "List decisions for a project. Filter by tags to find specific decisions.",
1210
+ inputSchema: {
1211
+ project: z3.string().optional().describe("Project name or ID"),
1212
+ tag: z3.string().optional().describe("Filter by tag"),
1213
+ limit: z3.number().optional().describe("Max number of decisions to return (default: 20)")
1214
+ }
1215
+ },
1216
+ async ({ project, tag, limit }) => {
1217
+ const resolved = resolveProjectOrDefault(project);
1218
+ if (!resolved) {
1219
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1220
+ }
1221
+ const sessionPreamble = maybeAutoSession(resolved.id);
1222
+ const db2 = getDb();
1223
+ let sql;
1224
+ const params = [resolved.id];
1225
+ if (tag) {
1226
+ sql = `SELECT * FROM decisions WHERE project_id = ? AND tags LIKE ? ORDER BY created_at DESC LIMIT ?`;
1227
+ params.push(`%"${tag}"%`, limit ?? 20);
1228
+ } else {
1229
+ sql = `SELECT * FROM decisions WHERE project_id = ? ORDER BY created_at DESC LIMIT ?`;
1230
+ params.push(limit ?? 20);
1166
1231
  }
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;
1232
+ const rows = db2.prepare(sql).all(...params);
1233
+ const resultText = JSON.stringify({ project: resolved.name, decisions: rows }, null, 2);
1234
+ return {
1235
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1236
+
1237
+ ---
1238
+
1239
+ ${resultText}` : resultText }]
1240
+ };
1181
1241
  }
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));
1242
+ );
1188
1243
  }
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>"
1244
+
1245
+ // src/tools/notes.ts
1246
+ import { z as z4 } from "zod/v4";
1247
+ function registerNoteTools(server2) {
1248
+ server2.registerTool(
1249
+ "add_note",
1250
+ {
1251
+ title: "Add Note",
1252
+ 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. Always specify the project parameter when you know which project is active.",
1253
+ inputSchema: {
1254
+ project: z4.string().optional().describe("Project name or ID (always pass this when known \u2014 omitting may target the wrong project)"),
1255
+ content: z4.string().describe("The note content"),
1256
+ category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Note category (default: general)"),
1257
+ task_id: z4.string().optional().describe("Link this note to a specific task"),
1258
+ tags: z4.array(z4.string()).optional().describe("Tags for categorization")
1259
+ }
1260
+ },
1261
+ async ({ project, content, category, task_id, tags }) => {
1262
+ const resolved = resolveProjectOrDefault(project);
1263
+ if (!resolved) {
1264
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1265
+ }
1266
+ const db2 = getDb();
1267
+ const id = generateId();
1268
+ db2.prepare(
1269
+ `INSERT INTO notes (id, project_id, task_id, content, category, tags) VALUES (?, ?, ?, ?, ?, ?)`
1270
+ ).run(
1271
+ id,
1272
+ resolved.id,
1273
+ task_id ?? null,
1274
+ content,
1275
+ category ?? "general",
1276
+ tags ? JSON.stringify(tags) : null
1209
1277
  );
1278
+ return {
1279
+ content: [{
1280
+ type: "text",
1281
+ text: JSON.stringify({ note_id: id, message: `Note added to ${resolved.name} (${category ?? "general"})` })
1282
+ }]
1283
+ };
1210
1284
  }
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);
1285
+ );
1286
+ server2.registerTool(
1287
+ "search_notes",
1288
+ {
1289
+ title: "Search Notes",
1290
+ description: "Full-text search across notes for a project.",
1291
+ inputSchema: {
1292
+ project: z4.string().optional().describe("Project name or ID"),
1293
+ query: z4.string().describe("Search query"),
1294
+ category: z4.enum(["general", "architecture", "bug", "idea", "research", "meeting", "review"]).optional().describe("Filter by category")
1227
1295
  }
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) });
1296
+ },
1297
+ async ({ project, query, category }) => {
1298
+ const resolved = resolveProjectOrDefault(project);
1299
+ if (!resolved) {
1300
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1301
+ }
1302
+ const sessionPreamble = maybeAutoSession(resolved.id);
1303
+ const db2 = getDb();
1304
+ const conditions = ["project_id = ?"];
1305
+ const params = [resolved.id];
1306
+ conditions.push("content LIKE '%' || ? || '%'");
1307
+ params.push(query);
1308
+ if (category) {
1309
+ conditions.push("category = ?");
1310
+ params.push(category);
1233
1311
  }
1312
+ const sql = `SELECT * FROM notes WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
1313
+ const rows = db2.prepare(sql).all(...params);
1314
+ const resultText = JSON.stringify({ project: resolved.name, results: rows }, null, 2);
1315
+ return {
1316
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1317
+
1318
+ ---
1319
+
1320
+ ${resultText}` : resultText }]
1321
+ };
1234
1322
  }
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
- `);
1323
+ );
1324
+ server2.registerTool(
1325
+ "set_context",
1326
+ {
1327
+ title: "Set Context",
1328
+ 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.",
1329
+ inputSchema: {
1330
+ project: z4.string().optional().describe("Project name or ID"),
1331
+ key: z4.string().describe('Context key, e.g. "auth_approach", "deployment_target", "api_base_url"'),
1332
+ value: z4.string().describe("Context value"),
1333
+ category: z4.string().optional().describe('Category like "architecture", "config", "convention", "constraint"')
1334
+ }
1335
+ },
1336
+ async ({ project, key, value, category }) => {
1337
+ const resolved = resolveProjectOrDefault(project);
1338
+ if (!resolved) {
1339
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1340
+ }
1341
+ const db2 = getDb();
1342
+ const id = generateId();
1343
+ db2.prepare(
1344
+ `INSERT INTO context (id, project_id, key, value, category)
1345
+ VALUES (?, ?, ?, ?, ?)
1346
+ ON CONFLICT(project_id, key) DO UPDATE SET value = excluded.value, category = excluded.category`
1347
+ ).run(id, resolved.id, key, value, category ?? "general");
1348
+ return {
1349
+ content: [{
1350
+ type: "text",
1351
+ text: JSON.stringify({ message: `Context set: "${key}" = "${value}" in ${resolved.name}` })
1352
+ }]
1353
+ };
1246
1354
  }
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);
1355
+ );
1356
+ server2.registerTool(
1357
+ "get_context",
1358
+ {
1359
+ title: "Get Context",
1360
+ description: "Get context by key or list all context for a project.",
1361
+ inputSchema: {
1362
+ project: z4.string().optional().describe("Project name or ID"),
1363
+ key: z4.string().optional().describe("Specific context key to retrieve. If omitted, returns all context.")
1364
+ }
1365
+ },
1366
+ async ({ project, key }) => {
1367
+ const resolved = resolveProjectOrDefault(project);
1368
+ if (!resolved) {
1369
+ return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1370
+ }
1371
+ const sessionPreamble = maybeAutoSession(resolved.id);
1372
+ const db2 = getDb();
1373
+ let rows;
1374
+ if (key) {
1375
+ rows = db2.prepare("SELECT * FROM context WHERE project_id = ? AND key = ?").all(resolved.id, key);
1376
+ } else {
1377
+ rows = db2.prepare("SELECT * FROM context WHERE project_id = ? ORDER BY category, key").all(resolved.id);
1378
+ }
1379
+ const resultText = JSON.stringify({ project: resolved.name, context: rows }, null, 2);
1380
+ return {
1381
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1382
+
1383
+ ---
1384
+
1385
+ ${resultText}` : resultText }]
1386
+ };
1254
1387
  }
1255
- });
1256
- return server2;
1388
+ );
1257
1389
  }
1258
1390
 
1259
1391
  // 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
- }
1392
+ import { z as z5 } from "zod/v4";
1289
1393
  function registerSessionTools(server2) {
1290
1394
  server2.registerTool(
1291
1395
  "start_session",
@@ -1301,62 +1405,9 @@ function registerSessionTools(server2) {
1301
1405
  if (!resolved) {
1302
1406
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found. Create a project first." }], isError: true };
1303
1407
  }
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)";
1408
+ markSessionStarted(resolved.id);
1356
1409
  return {
1357
- content: [{ type: "text", text: `${kanbanLine}
1358
-
1359
- ${JSON.stringify(result, null, 2)}` }]
1410
+ content: [{ type: "text", text: buildSessionText(resolved.id) }]
1360
1411
  };
1361
1412
  }
1362
1413
  );
@@ -1446,6 +1497,7 @@ function registerQueryTools(server2) {
1446
1497
  if (!resolved) {
1447
1498
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1448
1499
  }
1500
+ const sessionPreamble = maybeAutoSession(resolved.id);
1449
1501
  const db2 = getDb();
1450
1502
  const tasksByStatus = db2.prepare("SELECT status, COUNT(*) as count FROM tasks WHERE project_id = ? GROUP BY status").all(resolved.id);
1451
1503
  const blockers = db2.prepare("SELECT id, title, blocked_by FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
@@ -1467,21 +1519,26 @@ function registerQueryTools(server2) {
1467
1519
  const totalNotes = db2.prepare("SELECT COUNT(*) as count FROM notes WHERE project_id = ?").get(resolved.id);
1468
1520
  const totalDecisions = db2.prepare("SELECT COUNT(*) as count FROM decisions WHERE project_id = ?").get(resolved.id);
1469
1521
  const totalSessions = db2.prepare("SELECT COUNT(*) as count FROM sessions WHERE project_id = ?").get(resolved.id);
1522
+ const resultText = JSON.stringify(
1523
+ {
1524
+ project: resolved.name,
1525
+ tasks_by_status: tasksByStatus,
1526
+ blockers,
1527
+ upcoming_priorities: upcomingPriorities,
1528
+ recent_activity: recentActivity,
1529
+ totals: { notes: totalNotes.count, decisions: totalDecisions.count, sessions: totalSessions.count }
1530
+ },
1531
+ null,
1532
+ 2
1533
+ );
1470
1534
  return {
1471
1535
  content: [{
1472
1536
  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
- )
1537
+ text: sessionPreamble ? `${sessionPreamble}
1538
+
1539
+ ---
1540
+
1541
+ ${resultText}` : resultText
1485
1542
  }]
1486
1543
  };
1487
1544
  }
@@ -1500,6 +1557,7 @@ function registerQueryTools(server2) {
1500
1557
  if (!resolved) {
1501
1558
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1502
1559
  }
1560
+ const sessionPreamble = maybeAutoSession(resolved.id);
1503
1561
  const db2 = getDb();
1504
1562
  const blockers = db2.prepare("SELECT * FROM tasks WHERE project_id = ? AND status = 'blocked'").all(resolved.id);
1505
1563
  const enriched = blockers.map((task) => {
@@ -1516,8 +1574,13 @@ function registerQueryTools(server2) {
1516
1574
  }
1517
1575
  return { ...task, blocking_tasks: blockingTasks };
1518
1576
  });
1577
+ const resultText = JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2);
1519
1578
  return {
1520
- content: [{ type: "text", text: JSON.stringify({ project: resolved.name, blockers: enriched }, null, 2) }]
1579
+ content: [{ type: "text", text: sessionPreamble ? `${sessionPreamble}
1580
+
1581
+ ---
1582
+
1583
+ ${resultText}` : resultText }]
1521
1584
  };
1522
1585
  }
1523
1586
  );
@@ -1536,24 +1599,30 @@ function registerQueryTools(server2) {
1536
1599
  if (!resolved) {
1537
1600
  return { content: [{ type: "text", text: project ? `Project "${project}" not found.` : "No active projects found." }], isError: true };
1538
1601
  }
1602
+ const sessionPreamble = maybeAutoSession(resolved.id);
1539
1603
  const db2 = getDb();
1540
1604
  const pattern = `%${query}%`;
1541
1605
  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
1606
  const notes = db2.prepare("SELECT id, content, category, 'note' as type FROM notes WHERE project_id = ? AND content LIKE ?").all(resolved.id, pattern);
1543
1607
  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);
1608
+ const resultText = JSON.stringify(
1609
+ {
1610
+ project: resolved.name,
1611
+ query,
1612
+ results: { tasks, notes, decisions },
1613
+ total: tasks.length + notes.length + decisions.length
1614
+ },
1615
+ null,
1616
+ 2
1617
+ );
1544
1618
  return {
1545
1619
  content: [{
1546
1620
  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
- )
1621
+ text: sessionPreamble ? `${sessionPreamble}
1622
+
1623
+ ---
1624
+
1625
+ ${resultText}` : resultText
1557
1626
  }]
1558
1627
  };
1559
1628
  }