@cocaxcode/logbook-mcp 0.2.1 → 0.4.0
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
|
@@ -6,13 +6,13 @@ async function main() {
|
|
|
6
6
|
const hasMcpFlag = argv.includes("--mcp");
|
|
7
7
|
if (hasMcpFlag) {
|
|
8
8
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
-
const { createServer } = await import("./server-
|
|
9
|
+
const { createServer } = await import("./server-2NMQDQSU.js");
|
|
10
10
|
const server = createServer();
|
|
11
11
|
const transport = new StdioServerTransport();
|
|
12
12
|
await server.connect(transport);
|
|
13
13
|
console.error("logbook-mcp server running on stdio");
|
|
14
14
|
} else {
|
|
15
|
-
const { runCli } = await import("./cli-
|
|
15
|
+
const { runCli } = await import("./cli-VGOVRY45.js");
|
|
16
16
|
await runCli(argv);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -45,6 +45,9 @@ CREATE TABLE IF NOT EXISTS todos (
|
|
|
45
45
|
content TEXT NOT NULL,
|
|
46
46
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
47
47
|
priority TEXT NOT NULL DEFAULT 'normal',
|
|
48
|
+
remind_at TEXT,
|
|
49
|
+
remind_pattern TEXT,
|
|
50
|
+
remind_last_done TEXT,
|
|
48
51
|
completed_at TEXT,
|
|
49
52
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
50
53
|
);
|
|
@@ -56,6 +59,7 @@ CREATE INDEX IF NOT EXISTS idx_todos_repo ON todos(repo_id);
|
|
|
56
59
|
CREATE INDEX IF NOT EXISTS idx_todos_topic ON todos(topic_id);
|
|
57
60
|
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
58
61
|
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos(created_at);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_todos_remind ON todos(remind_at);
|
|
59
63
|
|
|
60
64
|
CREATE TABLE IF NOT EXISTS code_todo_snapshots (
|
|
61
65
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -114,7 +118,8 @@ INSERT OR IGNORE INTO topics (name, description, commit_prefix, is_custom) VALUE
|
|
|
114
118
|
('chore', 'Mantenimiento general', 'refactor,docs,ci,build,chore,test,perf', 0),
|
|
115
119
|
('idea', 'Ideas y propuestas futuras', NULL, 0),
|
|
116
120
|
('decision', 'Decisiones tomadas', NULL, 0),
|
|
117
|
-
('blocker', 'Bloqueos activos', NULL, 0)
|
|
121
|
+
('blocker', 'Bloqueos activos', NULL, 0),
|
|
122
|
+
('reminder', 'Recordatorios con fecha', NULL, 0);
|
|
118
123
|
`;
|
|
119
124
|
|
|
120
125
|
// src/db/connection.ts
|
|
@@ -207,11 +212,11 @@ var TODO_WITH_META_SQL = `
|
|
|
207
212
|
LEFT JOIN repos r ON t.repo_id = r.id
|
|
208
213
|
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
209
214
|
`;
|
|
210
|
-
function insertTodo(db2, repoId, topicId, content, priority = "normal") {
|
|
215
|
+
function insertTodo(db2, repoId, topicId, content, priority = "normal", remindAt, remindPattern) {
|
|
211
216
|
const stmt = db2.prepare(
|
|
212
|
-
"INSERT INTO todos (repo_id, topic_id, content, priority) VALUES (?, ?, ?, ?)"
|
|
217
|
+
"INSERT INTO todos (repo_id, topic_id, content, priority, remind_at, remind_pattern) VALUES (?, ?, ?, ?, ?, ?)"
|
|
213
218
|
);
|
|
214
|
-
const result = stmt.run(repoId, topicId, content, priority);
|
|
219
|
+
const result = stmt.run(repoId, topicId, content, priority, remindAt ?? null, remindPattern ?? null);
|
|
215
220
|
return db2.prepare(`${TODO_WITH_META_SQL} WHERE t.id = ?`).get(result.lastInsertRowid);
|
|
216
221
|
}
|
|
217
222
|
function getTodos(db2, filters = {}) {
|
|
@@ -365,6 +370,59 @@ function getCompletedTodos(db2, filters = {}) {
|
|
|
365
370
|
`${TODO_WITH_META_SQL} WHERE ${where} ORDER BY t.completed_at DESC`
|
|
366
371
|
).all(...params);
|
|
367
372
|
}
|
|
373
|
+
function getDueReminders(db2) {
|
|
374
|
+
const now = /* @__PURE__ */ new Date();
|
|
375
|
+
const today = now.toISOString().split("T")[0];
|
|
376
|
+
const tomorrow = new Date(now.getTime() + 864e5).toISOString().split("T")[0];
|
|
377
|
+
const todayItems = db2.prepare(
|
|
378
|
+
`${TODO_WITH_META_SQL} WHERE t.status = 'pending' AND t.remind_at >= ? AND t.remind_at < ? ORDER BY t.remind_at`
|
|
379
|
+
).all(today, tomorrow);
|
|
380
|
+
const overdueItems = db2.prepare(
|
|
381
|
+
`${TODO_WITH_META_SQL} WHERE t.status = 'pending' AND t.remind_at IS NOT NULL AND t.remind_at < ? AND t.remind_pattern IS NULL ORDER BY t.remind_at`
|
|
382
|
+
).all(today);
|
|
383
|
+
const recurringAll = db2.prepare(
|
|
384
|
+
`${TODO_WITH_META_SQL} WHERE t.remind_pattern IS NOT NULL AND (t.remind_last_done IS NULL OR t.remind_last_done < ?)`
|
|
385
|
+
).all(today);
|
|
386
|
+
const recurringToday = recurringAll.filter((r) => matchesPattern(r.remind_pattern, now));
|
|
387
|
+
const hasAny = todayItems.length > 0 || overdueItems.length > 0 || recurringToday.length > 0;
|
|
388
|
+
if (!hasAny) return null;
|
|
389
|
+
return {
|
|
390
|
+
today: groupByRepo(todayItems),
|
|
391
|
+
overdue: groupByRepo(overdueItems),
|
|
392
|
+
recurring: groupByRepo(recurringToday)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function ackRecurringReminder(db2, id) {
|
|
396
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
397
|
+
db2.prepare("UPDATE todos SET remind_last_done = ? WHERE id = ?").run(today, id);
|
|
398
|
+
}
|
|
399
|
+
function matchesPattern(pattern, date) {
|
|
400
|
+
const dayOfWeek = date.getDay();
|
|
401
|
+
const dayOfMonth = date.getDate();
|
|
402
|
+
if (pattern === "daily") return true;
|
|
403
|
+
if (pattern === "weekdays") return dayOfWeek >= 1 && dayOfWeek <= 5;
|
|
404
|
+
if (pattern.startsWith("weekly:")) {
|
|
405
|
+
const days = pattern.slice(7).split(",").map(Number);
|
|
406
|
+
return days.some((d) => d % 7 === dayOfWeek);
|
|
407
|
+
}
|
|
408
|
+
if (pattern.startsWith("monthly:")) {
|
|
409
|
+
const days = pattern.slice(8).split(",").map(Number);
|
|
410
|
+
return days.includes(dayOfMonth);
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
function groupByRepo(items) {
|
|
415
|
+
const map = /* @__PURE__ */ new Map();
|
|
416
|
+
for (const item of items) {
|
|
417
|
+
const key = item.repo_name ?? "global";
|
|
418
|
+
if (!map.has(key)) map.set(key, []);
|
|
419
|
+
map.get(key).push(item);
|
|
420
|
+
}
|
|
421
|
+
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([repo_name, reminders]) => ({
|
|
422
|
+
repo_name: repo_name === "global" ? null : repo_name,
|
|
423
|
+
reminders
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
368
426
|
function resolveTopicId(db2, name) {
|
|
369
427
|
const existing = getTopicByName(db2, name);
|
|
370
428
|
if (existing) return existing.id;
|
|
@@ -543,15 +601,19 @@ function registerTodoAddTool(server) {
|
|
|
543
601
|
content: z3.string().min(1).max(2e3).optional().describe("Contenido del TODO (para crear uno solo)"),
|
|
544
602
|
topic: z3.string().optional().describe("Topic para el TODO individual"),
|
|
545
603
|
priority: priorityEnum.optional().default("normal").describe("Prioridad del TODO individual"),
|
|
604
|
+
remind_at: z3.string().optional().describe("Fecha recordatorio unica (YYYY-MM-DD)"),
|
|
605
|
+
remind_pattern: z3.string().optional().describe("Patron recurrente: daily, weekdays, weekly:2 (martes), weekly:1,3 (lun+mie), monthly:1 (dia 1), monthly:1,15"),
|
|
546
606
|
items: z3.array(
|
|
547
607
|
z3.object({
|
|
548
608
|
content: z3.string().min(1).max(2e3).describe("Contenido del TODO"),
|
|
549
609
|
topic: z3.string().optional().describe("Topic"),
|
|
550
|
-
priority: priorityEnum.optional().default("normal").describe("Prioridad")
|
|
610
|
+
priority: priorityEnum.optional().default("normal").describe("Prioridad"),
|
|
611
|
+
remind_at: z3.string().optional().describe("Fecha recordatorio (YYYY-MM-DD)"),
|
|
612
|
+
remind_pattern: z3.string().optional().describe("Patron recurrente")
|
|
551
613
|
})
|
|
552
614
|
).max(50).optional().describe("Array de TODOs para crear varios a la vez (max 50)")
|
|
553
615
|
},
|
|
554
|
-
async ({ content, topic, priority, items }) => {
|
|
616
|
+
async ({ content, topic, priority, remind_at, remind_pattern, items }) => {
|
|
555
617
|
try {
|
|
556
618
|
if (!content && (!items || items.length === 0)) {
|
|
557
619
|
return {
|
|
@@ -565,16 +627,20 @@ function registerTodoAddTool(server) {
|
|
|
565
627
|
const db2 = getDb();
|
|
566
628
|
const repo = autoRegisterRepo(db2);
|
|
567
629
|
const repoId = repo?.id ?? null;
|
|
568
|
-
const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal" }];
|
|
630
|
+
const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal", remind_at, remind_pattern }];
|
|
569
631
|
const results = [];
|
|
570
632
|
for (const item of todoItems) {
|
|
571
|
-
const
|
|
633
|
+
const hasReminder = item.remind_at || item.remind_pattern;
|
|
634
|
+
const effectiveTopic = hasReminder && !item.topic ? "reminder" : item.topic;
|
|
635
|
+
const topicId = effectiveTopic ? resolveTopicId(db2, effectiveTopic) : null;
|
|
572
636
|
const todo = insertTodo(
|
|
573
637
|
db2,
|
|
574
638
|
repoId,
|
|
575
639
|
topicId,
|
|
576
640
|
item.content,
|
|
577
|
-
item.priority ?? "normal"
|
|
641
|
+
item.priority ?? "normal",
|
|
642
|
+
item.remind_at,
|
|
643
|
+
item.remind_pattern
|
|
578
644
|
);
|
|
579
645
|
results.push(todo);
|
|
580
646
|
}
|
|
@@ -723,7 +789,7 @@ import { z as z5 } from "zod";
|
|
|
723
789
|
function registerTodoDoneTool(server) {
|
|
724
790
|
server.tool(
|
|
725
791
|
"logbook_todo_done",
|
|
726
|
-
"Marca TODOs como hechos o los devuelve a pendiente (undo).
|
|
792
|
+
"Marca TODOs como hechos o los devuelve a pendiente (undo). Los recordatorios recurrentes se marcan como hechos por hoy y vuelven automaticamente el proximo dia que toque.",
|
|
727
793
|
{
|
|
728
794
|
ids: z5.union([z5.number(), z5.array(z5.number())]).describe("ID o array de IDs de TODOs a marcar"),
|
|
729
795
|
undo: z5.boolean().optional().default(false).describe("Si true, devuelve a pendiente en vez de marcar como hecho")
|
|
@@ -732,15 +798,41 @@ function registerTodoDoneTool(server) {
|
|
|
732
798
|
try {
|
|
733
799
|
const db2 = getDb();
|
|
734
800
|
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
735
|
-
const
|
|
736
|
-
const
|
|
801
|
+
const regularIds = [];
|
|
802
|
+
const recurringIds = [];
|
|
803
|
+
for (const id of idArray) {
|
|
804
|
+
const todo = db2.prepare("SELECT remind_pattern FROM todos WHERE id = ?").get(id);
|
|
805
|
+
if (todo?.remind_pattern && !undo) {
|
|
806
|
+
recurringIds.push(id);
|
|
807
|
+
} else {
|
|
808
|
+
regularIds.push(id);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const results = [];
|
|
812
|
+
if (regularIds.length > 0) {
|
|
813
|
+
const status = undo ? "pending" : "done";
|
|
814
|
+
const updated = updateTodoStatus(db2, regularIds, status);
|
|
815
|
+
results.push(...updated);
|
|
816
|
+
}
|
|
817
|
+
for (const id of recurringIds) {
|
|
818
|
+
ackRecurringReminder(db2, id);
|
|
819
|
+
const todo = db2.prepare(
|
|
820
|
+
`SELECT t.*, r.name as repo_name, tp.name as topic_name, 'manual' as source
|
|
821
|
+
FROM todos t
|
|
822
|
+
LEFT JOIN repos r ON t.repo_id = r.id
|
|
823
|
+
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
824
|
+
WHERE t.id = ?`
|
|
825
|
+
).get(id);
|
|
826
|
+
results.push(todo);
|
|
827
|
+
}
|
|
737
828
|
return {
|
|
738
829
|
content: [{
|
|
739
830
|
type: "text",
|
|
740
831
|
text: JSON.stringify({
|
|
741
832
|
action: undo ? "undo" : "done",
|
|
742
|
-
updated:
|
|
743
|
-
|
|
833
|
+
updated: results.length,
|
|
834
|
+
recurring_acked: recurringIds.length,
|
|
835
|
+
todos: results
|
|
744
836
|
})
|
|
745
837
|
}]
|
|
746
838
|
};
|
|
@@ -994,8 +1086,37 @@ function registerSearchTool(server) {
|
|
|
994
1086
|
);
|
|
995
1087
|
}
|
|
996
1088
|
|
|
1089
|
+
// src/resources/reminders.ts
|
|
1090
|
+
function registerRemindersResource(server) {
|
|
1091
|
+
server.resource(
|
|
1092
|
+
"reminders",
|
|
1093
|
+
"logbook://reminders",
|
|
1094
|
+
{
|
|
1095
|
+
description: "Recordatorios pendientes para hoy y atrasados, agrupados por proyecto. Vacio si no hay ninguno.",
|
|
1096
|
+
mimeType: "application/json"
|
|
1097
|
+
},
|
|
1098
|
+
async (uri) => {
|
|
1099
|
+
try {
|
|
1100
|
+
const db2 = getDb();
|
|
1101
|
+
const result = getDueReminders(db2);
|
|
1102
|
+
if (!result) {
|
|
1103
|
+
return { contents: [] };
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
contents: [{
|
|
1107
|
+
uri: uri.href,
|
|
1108
|
+
text: JSON.stringify(result)
|
|
1109
|
+
}]
|
|
1110
|
+
};
|
|
1111
|
+
} catch {
|
|
1112
|
+
return { contents: [] };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
997
1118
|
// src/server.ts
|
|
998
|
-
var VERSION = true ? "0.
|
|
1119
|
+
var VERSION = true ? "0.3.0" : "0.0.0";
|
|
999
1120
|
function createServer() {
|
|
1000
1121
|
const server = new McpServer({
|
|
1001
1122
|
name: "logbook-mcp",
|
|
@@ -1010,6 +1131,7 @@ function createServer() {
|
|
|
1010
1131
|
registerTodoRmTool(server);
|
|
1011
1132
|
registerLogTool(server);
|
|
1012
1133
|
registerSearchTool(server);
|
|
1134
|
+
registerRemindersResource(server);
|
|
1013
1135
|
return server;
|
|
1014
1136
|
}
|
|
1015
1137
|
export {
|