@cocaxcode/logbook-mcp 0.3.0 → 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,7 +6,7 @@ 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-YXLXUSPK.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);
@@ -46,6 +46,8 @@ CREATE TABLE IF NOT EXISTS todos (
46
46
  status TEXT NOT NULL DEFAULT 'pending',
47
47
  priority TEXT NOT NULL DEFAULT 'normal',
48
48
  remind_at TEXT,
49
+ remind_pattern TEXT,
50
+ remind_last_done TEXT,
49
51
  completed_at TEXT,
50
52
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
51
53
  );
@@ -210,11 +212,11 @@ var TODO_WITH_META_SQL = `
210
212
  LEFT JOIN repos r ON t.repo_id = r.id
211
213
  LEFT JOIN topics tp ON t.topic_id = tp.id
212
214
  `;
213
- function insertTodo(db2, repoId, topicId, content, priority = "normal", remindAt) {
215
+ function insertTodo(db2, repoId, topicId, content, priority = "normal", remindAt, remindPattern) {
214
216
  const stmt = db2.prepare(
215
- "INSERT INTO todos (repo_id, topic_id, content, priority, remind_at) VALUES (?, ?, ?, ?, ?)"
217
+ "INSERT INTO todos (repo_id, topic_id, content, priority, remind_at, remind_pattern) VALUES (?, ?, ?, ?, ?, ?)"
216
218
  );
217
- const result = stmt.run(repoId, topicId, content, priority, remindAt ?? null);
219
+ const result = stmt.run(repoId, topicId, content, priority, remindAt ?? null, remindPattern ?? null);
218
220
  return db2.prepare(`${TODO_WITH_META_SQL} WHERE t.id = ?`).get(result.lastInsertRowid);
219
221
  }
220
222
  function getTodos(db2, filters = {}) {
@@ -376,13 +378,39 @@ function getDueReminders(db2) {
376
378
  `${TODO_WITH_META_SQL} WHERE t.status = 'pending' AND t.remind_at >= ? AND t.remind_at < ? ORDER BY t.remind_at`
377
379
  ).all(today, tomorrow);
378
380
  const overdueItems = db2.prepare(
379
- `${TODO_WITH_META_SQL} WHERE t.status = 'pending' AND t.remind_at < ? ORDER BY t.remind_at`
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`
380
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;
381
389
  return {
382
390
  today: groupByRepo(todayItems),
383
- overdue: groupByRepo(overdueItems)
391
+ overdue: groupByRepo(overdueItems),
392
+ recurring: groupByRepo(recurringToday)
384
393
  };
385
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
+ }
386
414
  function groupByRepo(items) {
387
415
  const map = /* @__PURE__ */ new Map();
388
416
  for (const item of items) {
@@ -573,17 +601,19 @@ function registerTodoAddTool(server) {
573
601
  content: z3.string().min(1).max(2e3).optional().describe("Contenido del TODO (para crear uno solo)"),
574
602
  topic: z3.string().optional().describe("Topic para el TODO individual"),
575
603
  priority: priorityEnum.optional().default("normal").describe("Prioridad del TODO individual"),
576
- remind_at: z3.string().optional().describe('Fecha recordatorio (YYYY-MM-DD). Se asigna topic "reminder" automaticamente.'),
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"),
577
606
  items: z3.array(
578
607
  z3.object({
579
608
  content: z3.string().min(1).max(2e3).describe("Contenido del TODO"),
580
609
  topic: z3.string().optional().describe("Topic"),
581
610
  priority: priorityEnum.optional().default("normal").describe("Prioridad"),
582
- remind_at: z3.string().optional().describe("Fecha recordatorio (YYYY-MM-DD)")
611
+ remind_at: z3.string().optional().describe("Fecha recordatorio (YYYY-MM-DD)"),
612
+ remind_pattern: z3.string().optional().describe("Patron recurrente")
583
613
  })
584
614
  ).max(50).optional().describe("Array de TODOs para crear varios a la vez (max 50)")
585
615
  },
586
- async ({ content, topic, priority, remind_at, items }) => {
616
+ async ({ content, topic, priority, remind_at, remind_pattern, items }) => {
587
617
  try {
588
618
  if (!content && (!items || items.length === 0)) {
589
619
  return {
@@ -597,10 +627,11 @@ function registerTodoAddTool(server) {
597
627
  const db2 = getDb();
598
628
  const repo = autoRegisterRepo(db2);
599
629
  const repoId = repo?.id ?? null;
600
- const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal", remind_at }];
630
+ const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal", remind_at, remind_pattern }];
601
631
  const results = [];
602
632
  for (const item of todoItems) {
603
- const effectiveTopic = item.remind_at && !item.topic ? "reminder" : item.topic;
633
+ const hasReminder = item.remind_at || item.remind_pattern;
634
+ const effectiveTopic = hasReminder && !item.topic ? "reminder" : item.topic;
604
635
  const topicId = effectiveTopic ? resolveTopicId(db2, effectiveTopic) : null;
605
636
  const todo = insertTodo(
606
637
  db2,
@@ -608,7 +639,8 @@ function registerTodoAddTool(server) {
608
639
  topicId,
609
640
  item.content,
610
641
  item.priority ?? "normal",
611
- item.remind_at
642
+ item.remind_at,
643
+ item.remind_pattern
612
644
  );
613
645
  results.push(todo);
614
646
  }
@@ -757,7 +789,7 @@ import { z as z5 } from "zod";
757
789
  function registerTodoDoneTool(server) {
758
790
  server.tool(
759
791
  "logbook_todo_done",
760
- "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.",
761
793
  {
762
794
  ids: z5.union([z5.number(), z5.array(z5.number())]).describe("ID o array de IDs de TODOs a marcar"),
763
795
  undo: z5.boolean().optional().default(false).describe("Si true, devuelve a pendiente en vez de marcar como hecho")
@@ -766,15 +798,41 @@ function registerTodoDoneTool(server) {
766
798
  try {
767
799
  const db2 = getDb();
768
800
  const idArray = Array.isArray(ids) ? ids : [ids];
769
- const status = undo ? "pending" : "done";
770
- 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
+ }
771
828
  return {
772
829
  content: [{
773
830
  type: "text",
774
831
  text: JSON.stringify({
775
832
  action: undo ? "undo" : "done",
776
- updated: updated.length,
777
- todos: updated
833
+ updated: results.length,
834
+ recurring_acked: recurringIds.length,
835
+ todos: results
778
836
  })
779
837
  }]
780
838
  };
@@ -1034,31 +1092,24 @@ function registerRemindersResource(server) {
1034
1092
  "reminders",
1035
1093
  "logbook://reminders",
1036
1094
  {
1037
- description: "Recordatorios pendientes para hoy y atrasados, agrupados por proyecto",
1095
+ description: "Recordatorios pendientes para hoy y atrasados, agrupados por proyecto. Vacio si no hay ninguno.",
1038
1096
  mimeType: "application/json"
1039
1097
  },
1040
1098
  async (uri) => {
1041
1099
  try {
1042
1100
  const db2 = getDb();
1043
- const { today, overdue } = getDueReminders(db2);
1044
- const hasReminders = today.length > 0 || overdue.length > 0;
1101
+ const result = getDueReminders(db2);
1102
+ if (!result) {
1103
+ return { contents: [] };
1104
+ }
1045
1105
  return {
1046
1106
  contents: [{
1047
1107
  uri: uri.href,
1048
- text: JSON.stringify({
1049
- hasReminders,
1050
- today,
1051
- overdue
1052
- })
1108
+ text: JSON.stringify(result)
1053
1109
  }]
1054
1110
  };
1055
1111
  } catch {
1056
- return {
1057
- contents: [{
1058
- uri: uri.href,
1059
- text: JSON.stringify({ hasReminders: false, today: [], overdue: [] })
1060
- }]
1061
- };
1112
+ return { contents: [] };
1062
1113
  }
1063
1114
  }
1064
1115
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocaxcode/logbook-mcp",
3
- "version": "0.3.0",
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": {