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:
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
"
|
|
1045
|
+
"list_tasks",
|
|
842
1046
|
{
|
|
843
|
-
title: "
|
|
844
|
-
description: "
|
|
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:
|
|
847
|
-
|
|
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,
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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:
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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/
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
if (!
|
|
1232
|
-
|
|
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.
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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.
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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:
|
|
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:
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
}
|