@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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- var VERSION = true ? "0.2.0" : "0.0.0";
4
+ var VERSION = true ? "0.3.0" : "0.0.0";
5
5
  async function runCli(argv) {
6
6
  if (argv.includes("--version") || argv.includes("-v")) {
7
7
  console.log(`logbook-mcp v${VERSION}`);
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-CIZE5MHB.js");
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-UFNP35WT.js");
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 topicId = item.topic ? resolveTopicId(db2, item.topic) : null;
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). Acepta uno o varios IDs.",
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 status = undo ? "pending" : "done";
736
- const updated = updateTodoStatus(db2, idArray, status);
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: updated.length,
743
- todos: updated
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.2.0" : "0.0.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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocaxcode/logbook-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server — cuaderno de bitácora del developer. Notas, TODOs y code TODOs sin salir de tu AI.",
5
5
  "type": "module",
6
6
  "bin": {