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