@cocaxcode/logbook-mcp 0.4.12 → 0.5.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/README.md +79 -25
- package/dist/{cli-FYV7XES5.js → cli-7NK5XS7T.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-YCNK5TAH.js → server-WVSGAZ2E.js} +649 -789
- package/package.json +2 -2
|
@@ -6,499 +6,37 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
6
6
|
// src/tools/topics.ts
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
CREATE TABLE IF NOT EXISTS topics (
|
|
25
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
-
name TEXT NOT NULL UNIQUE,
|
|
27
|
-
description TEXT,
|
|
28
|
-
commit_prefix TEXT,
|
|
29
|
-
is_custom INTEGER NOT NULL DEFAULT 0,
|
|
30
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
CREATE TABLE IF NOT EXISTS notes (
|
|
34
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
-
repo_id INTEGER REFERENCES repos(id) ON DELETE SET NULL,
|
|
36
|
-
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
|
37
|
-
content TEXT NOT NULL,
|
|
38
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
CREATE TABLE IF NOT EXISTS todos (
|
|
42
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
-
repo_id INTEGER REFERENCES repos(id) ON DELETE SET NULL,
|
|
44
|
-
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
|
45
|
-
content TEXT NOT NULL,
|
|
46
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
47
|
-
priority TEXT NOT NULL DEFAULT 'normal',
|
|
48
|
-
remind_at TEXT,
|
|
49
|
-
remind_pattern TEXT,
|
|
50
|
-
remind_last_done TEXT,
|
|
51
|
-
completed_at TEXT,
|
|
52
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
CREATE INDEX IF NOT EXISTS idx_notes_repo ON notes(repo_id);
|
|
56
|
-
CREATE INDEX IF NOT EXISTS idx_notes_topic ON notes(topic_id);
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(created_at);
|
|
58
|
-
CREATE INDEX IF NOT EXISTS idx_todos_repo ON todos(repo_id);
|
|
59
|
-
CREATE INDEX IF NOT EXISTS idx_todos_topic ON todos(topic_id);
|
|
60
|
-
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos(created_at);
|
|
62
|
-
CREATE TABLE IF NOT EXISTS code_todo_snapshots (
|
|
63
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
-
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
65
|
-
file TEXT NOT NULL,
|
|
66
|
-
line INTEGER NOT NULL,
|
|
67
|
-
tag TEXT NOT NULL,
|
|
68
|
-
content TEXT NOT NULL,
|
|
69
|
-
topic_name TEXT NOT NULL,
|
|
70
|
-
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
71
|
-
resolved_at TEXT,
|
|
72
|
-
UNIQUE(repo_id, file, content)
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_code_snapshots_repo ON code_todo_snapshots(repo_id);
|
|
76
|
-
CREATE INDEX IF NOT EXISTS idx_code_snapshots_resolved ON code_todo_snapshots(resolved_at);
|
|
77
|
-
|
|
78
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
79
|
-
content,
|
|
80
|
-
content='notes',
|
|
81
|
-
content_rowid='id'
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS todos_fts USING fts5(
|
|
85
|
-
content,
|
|
86
|
-
content='todos',
|
|
87
|
-
content_rowid='id'
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
|
|
91
|
-
INSERT INTO notes_fts(rowid, content) VALUES (new.id, new.content);
|
|
92
|
-
END;
|
|
93
|
-
CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
|
|
94
|
-
INSERT INTO notes_fts(notes_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
95
|
-
END;
|
|
96
|
-
CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
|
|
97
|
-
INSERT INTO notes_fts(notes_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
98
|
-
INSERT INTO notes_fts(rowid, content) VALUES (new.id, new.content);
|
|
99
|
-
END;
|
|
100
|
-
|
|
101
|
-
CREATE TRIGGER IF NOT EXISTS todos_ai AFTER INSERT ON todos BEGIN
|
|
102
|
-
INSERT INTO todos_fts(rowid, content) VALUES (new.id, new.content);
|
|
103
|
-
END;
|
|
104
|
-
CREATE TRIGGER IF NOT EXISTS todos_ad AFTER DELETE ON todos BEGIN
|
|
105
|
-
INSERT INTO todos_fts(todos_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
106
|
-
END;
|
|
107
|
-
CREATE TRIGGER IF NOT EXISTS todos_au AFTER UPDATE ON todos BEGIN
|
|
108
|
-
INSERT INTO todos_fts(todos_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
109
|
-
INSERT INTO todos_fts(rowid, content) VALUES (new.id, new.content);
|
|
110
|
-
END;
|
|
111
|
-
`;
|
|
112
|
-
var MIGRATIONS_SQL = `
|
|
113
|
-
-- Add reminder columns to existing todos tables (safe to run multiple times)
|
|
114
|
-
ALTER TABLE todos ADD COLUMN remind_at TEXT;
|
|
115
|
-
ALTER TABLE todos ADD COLUMN remind_pattern TEXT;
|
|
116
|
-
ALTER TABLE todos ADD COLUMN remind_last_done TEXT;
|
|
117
|
-
`;
|
|
118
|
-
var POST_MIGRATION_SQL = `
|
|
119
|
-
CREATE INDEX IF NOT EXISTS idx_todos_remind ON todos(remind_at);
|
|
120
|
-
`;
|
|
121
|
-
var SEED_TOPICS_SQL = `
|
|
122
|
-
INSERT OR IGNORE INTO topics (name, description, commit_prefix, is_custom) VALUES
|
|
123
|
-
('feature', 'Funcionalidad nueva', 'feat', 0),
|
|
124
|
-
('fix', 'Correcci\xF3n de errores', 'fix', 0),
|
|
125
|
-
('chore', 'Mantenimiento general', 'refactor,docs,ci,build,chore,test,perf', 0),
|
|
126
|
-
('idea', 'Ideas y propuestas futuras', NULL, 0),
|
|
127
|
-
('decision', 'Decisiones tomadas', NULL, 0),
|
|
128
|
-
('blocker', 'Bloqueos activos', NULL, 0),
|
|
129
|
-
('reminder', 'Recordatorios con fecha', NULL, 0);
|
|
130
|
-
`;
|
|
131
|
-
|
|
132
|
-
// src/db/connection.ts
|
|
133
|
-
var DB_DIR = join(homedir(), ".logbook");
|
|
134
|
-
var DB_PATH = join(DB_DIR, "logbook.db");
|
|
135
|
-
var db = null;
|
|
136
|
-
function getDb(dbPath) {
|
|
137
|
-
if (db) return db;
|
|
138
|
-
const path = dbPath ?? DB_PATH;
|
|
139
|
-
const dir = dirname(path);
|
|
140
|
-
if (!existsSync(dir)) {
|
|
141
|
-
mkdirSync(dir, { recursive: true });
|
|
142
|
-
}
|
|
143
|
-
db = new Database(path);
|
|
144
|
-
db.pragma("journal_mode = WAL");
|
|
145
|
-
db.pragma("foreign_keys = ON");
|
|
146
|
-
db.exec(SCHEMA_SQL);
|
|
147
|
-
for (const stmt of MIGRATIONS_SQL.split(";").map((s) => s.trim()).filter(Boolean)) {
|
|
148
|
-
try {
|
|
149
|
-
db.exec(stmt);
|
|
150
|
-
} catch (e) {
|
|
151
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
152
|
-
if (!msg.includes("duplicate column")) {
|
|
153
|
-
console.error(`Migration warning: ${msg}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
db.exec(POST_MIGRATION_SQL);
|
|
158
|
-
db.exec(SEED_TOPICS_SQL);
|
|
159
|
-
return db;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// src/db/queries.ts
|
|
163
|
-
function insertRepo(db2, name, path) {
|
|
164
|
-
const stmt = db2.prepare(
|
|
165
|
-
"INSERT INTO repos (name, path) VALUES (?, ?)"
|
|
166
|
-
);
|
|
167
|
-
const result = stmt.run(name, path);
|
|
168
|
-
return db2.prepare("SELECT * FROM repos WHERE id = ?").get(result.lastInsertRowid);
|
|
169
|
-
}
|
|
170
|
-
function getRepoByPath(db2, path) {
|
|
171
|
-
return db2.prepare("SELECT * FROM repos WHERE path = ?").get(path);
|
|
172
|
-
}
|
|
173
|
-
function getRepoByName(db2, name) {
|
|
174
|
-
return db2.prepare("SELECT * FROM repos WHERE name = ?").get(name);
|
|
175
|
-
}
|
|
176
|
-
function getTopicByName(db2, name) {
|
|
177
|
-
return db2.prepare("SELECT * FROM topics WHERE name = ?").get(name);
|
|
178
|
-
}
|
|
179
|
-
function getAllTopics(db2) {
|
|
180
|
-
return db2.prepare("SELECT * FROM topics ORDER BY is_custom, name").all();
|
|
181
|
-
}
|
|
182
|
-
function insertTopic(db2, name, description) {
|
|
183
|
-
const stmt = db2.prepare(
|
|
184
|
-
"INSERT INTO topics (name, description, is_custom) VALUES (?, ?, 1)"
|
|
185
|
-
);
|
|
186
|
-
const result = stmt.run(name, description ?? null);
|
|
187
|
-
return db2.prepare("SELECT * FROM topics WHERE id = ?").get(result.lastInsertRowid);
|
|
188
|
-
}
|
|
189
|
-
var NOTE_WITH_META_SQL = `
|
|
190
|
-
SELECT n.*, r.name as repo_name, t.name as topic_name
|
|
191
|
-
FROM notes n
|
|
192
|
-
LEFT JOIN repos r ON n.repo_id = r.id
|
|
193
|
-
LEFT JOIN topics t ON n.topic_id = t.id
|
|
194
|
-
`;
|
|
195
|
-
function insertNote(db2, repoId, topicId, content) {
|
|
196
|
-
const stmt = db2.prepare(
|
|
197
|
-
"INSERT INTO notes (repo_id, topic_id, content) VALUES (?, ?, ?)"
|
|
9
|
+
// src/storage/index.ts
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
var require2 = createRequire(import.meta.url);
|
|
12
|
+
var instance = null;
|
|
13
|
+
var VALID_MODES = ["sqlite", "obsidian"];
|
|
14
|
+
function getStorageMode() {
|
|
15
|
+
const mode = process.env.LOGBOOK_STORAGE?.toLowerCase();
|
|
16
|
+
if (!mode) return "sqlite";
|
|
17
|
+
if (mode === "obsidian") return "obsidian";
|
|
18
|
+
if (mode === "sqlite") return "sqlite";
|
|
19
|
+
throw new Error(
|
|
20
|
+
`LOGBOOK_STORAGE invalido: "${mode}". Valores validos: ${VALID_MODES.join(", ")}`
|
|
198
21
|
);
|
|
199
|
-
const result = stmt.run(repoId, topicId, content);
|
|
200
|
-
return db2.prepare(`${NOTE_WITH_META_SQL} WHERE n.id = ?`).get(result.lastInsertRowid);
|
|
201
22
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
params.push(filters.topicId);
|
|
212
|
-
}
|
|
213
|
-
if (filters.from) {
|
|
214
|
-
conditions.push("n.created_at >= ?");
|
|
215
|
-
params.push(filters.from);
|
|
216
|
-
}
|
|
217
|
-
if (filters.to) {
|
|
218
|
-
conditions.push("n.created_at < ?");
|
|
219
|
-
params.push(filters.to);
|
|
220
|
-
}
|
|
221
|
-
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
222
|
-
const limit = filters.limit ?? 100;
|
|
223
|
-
return db2.prepare(
|
|
224
|
-
`${NOTE_WITH_META_SQL}${where} ORDER BY n.created_at DESC LIMIT ?`
|
|
225
|
-
).all(...params, limit);
|
|
226
|
-
}
|
|
227
|
-
var TODO_WITH_META_SQL = `
|
|
228
|
-
SELECT t.*, r.name as repo_name, tp.name as topic_name, 'manual' as source
|
|
229
|
-
FROM todos t
|
|
230
|
-
LEFT JOIN repos r ON t.repo_id = r.id
|
|
231
|
-
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
232
|
-
`;
|
|
233
|
-
function insertTodo(db2, repoId, topicId, content, priority = "normal", remindAt, remindPattern) {
|
|
234
|
-
const stmt = db2.prepare(
|
|
235
|
-
"INSERT INTO todos (repo_id, topic_id, content, priority, remind_at, remind_pattern) VALUES (?, ?, ?, ?, ?, ?)"
|
|
236
|
-
);
|
|
237
|
-
const result = stmt.run(repoId, topicId, content, priority, remindAt ?? null, remindPattern ?? null);
|
|
238
|
-
return db2.prepare(`${TODO_WITH_META_SQL} WHERE t.id = ?`).get(result.lastInsertRowid);
|
|
239
|
-
}
|
|
240
|
-
function getTodos(db2, filters = {}) {
|
|
241
|
-
const conditions = [];
|
|
242
|
-
const params = [];
|
|
243
|
-
if (filters.status && filters.status !== "all") {
|
|
244
|
-
conditions.push("t.status = ?");
|
|
245
|
-
params.push(filters.status);
|
|
246
|
-
}
|
|
247
|
-
if (filters.repoId !== void 0) {
|
|
248
|
-
conditions.push("t.repo_id = ?");
|
|
249
|
-
params.push(filters.repoId);
|
|
250
|
-
}
|
|
251
|
-
if (filters.topicId !== void 0) {
|
|
252
|
-
conditions.push("t.topic_id = ?");
|
|
253
|
-
params.push(filters.topicId);
|
|
254
|
-
}
|
|
255
|
-
if (filters.priority) {
|
|
256
|
-
conditions.push("t.priority = ?");
|
|
257
|
-
params.push(filters.priority);
|
|
258
|
-
}
|
|
259
|
-
if (filters.from) {
|
|
260
|
-
conditions.push("t.created_at >= ?");
|
|
261
|
-
params.push(filters.from);
|
|
262
|
-
}
|
|
263
|
-
if (filters.to) {
|
|
264
|
-
conditions.push("t.created_at < ?");
|
|
265
|
-
params.push(filters.to);
|
|
266
|
-
}
|
|
267
|
-
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
268
|
-
const limit = filters.limit ?? 100;
|
|
269
|
-
return db2.prepare(
|
|
270
|
-
`${TODO_WITH_META_SQL}${where} ORDER BY t.created_at DESC LIMIT ?`
|
|
271
|
-
).all(...params, limit);
|
|
272
|
-
}
|
|
273
|
-
function updateTodoStatus(db2, ids, status) {
|
|
274
|
-
if (ids.length === 0) return [];
|
|
275
|
-
const completedAt = status === "done" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
276
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
277
|
-
db2.prepare(
|
|
278
|
-
`UPDATE todos SET status = ?, completed_at = ? WHERE id IN (${placeholders})`
|
|
279
|
-
).run(status, completedAt, ...ids);
|
|
280
|
-
return db2.prepare(
|
|
281
|
-
`${TODO_WITH_META_SQL} WHERE t.id IN (${placeholders})`
|
|
282
|
-
).all(...ids);
|
|
283
|
-
}
|
|
284
|
-
function updateTodo(db2, id, fields) {
|
|
285
|
-
const sets = [];
|
|
286
|
-
const params = [];
|
|
287
|
-
if (fields.content !== void 0) {
|
|
288
|
-
sets.push("content = ?");
|
|
289
|
-
params.push(fields.content);
|
|
290
|
-
}
|
|
291
|
-
if (fields.topicId !== void 0) {
|
|
292
|
-
sets.push("topic_id = ?");
|
|
293
|
-
params.push(fields.topicId);
|
|
294
|
-
}
|
|
295
|
-
if (fields.priority !== void 0) {
|
|
296
|
-
sets.push("priority = ?");
|
|
297
|
-
params.push(fields.priority);
|
|
298
|
-
}
|
|
299
|
-
if (sets.length === 0) return void 0;
|
|
300
|
-
params.push(id);
|
|
301
|
-
db2.prepare(`UPDATE todos SET ${sets.join(", ")} WHERE id = ?`).run(
|
|
302
|
-
...params
|
|
303
|
-
);
|
|
304
|
-
return db2.prepare(`${TODO_WITH_META_SQL} WHERE t.id = ?`).get(id);
|
|
305
|
-
}
|
|
306
|
-
function deleteTodos(db2, ids) {
|
|
307
|
-
if (ids.length === 0) return [];
|
|
308
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
309
|
-
const existing = db2.prepare(`SELECT id FROM todos WHERE id IN (${placeholders})`).all(...ids);
|
|
310
|
-
const existingIds = existing.map((r) => r.id);
|
|
311
|
-
if (existingIds.length > 0) {
|
|
312
|
-
const ep = existingIds.map(() => "?").join(",");
|
|
313
|
-
db2.prepare(`DELETE FROM todos WHERE id IN (${ep})`).run(
|
|
314
|
-
...existingIds
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
return existingIds;
|
|
318
|
-
}
|
|
319
|
-
function searchNotes(db2, query, filters = {}) {
|
|
320
|
-
const conditions = ["notes_fts MATCH ?"];
|
|
321
|
-
const params = [sanitizeFts(query)];
|
|
322
|
-
if (filters.repoId !== void 0) {
|
|
323
|
-
conditions.push("n.repo_id = ?");
|
|
324
|
-
params.push(filters.repoId);
|
|
325
|
-
}
|
|
326
|
-
if (filters.topicId !== void 0) {
|
|
327
|
-
conditions.push("n.topic_id = ?");
|
|
328
|
-
params.push(filters.topicId);
|
|
329
|
-
}
|
|
330
|
-
const limit = filters.limit ?? 20;
|
|
331
|
-
const where = conditions.join(" AND ");
|
|
332
|
-
return db2.prepare(
|
|
333
|
-
`SELECT n.*, r.name as repo_name, t.name as topic_name, rank
|
|
334
|
-
FROM notes_fts
|
|
335
|
-
JOIN notes n ON n.id = notes_fts.rowid
|
|
336
|
-
LEFT JOIN repos r ON n.repo_id = r.id
|
|
337
|
-
LEFT JOIN topics t ON n.topic_id = t.id
|
|
338
|
-
WHERE ${where}
|
|
339
|
-
ORDER BY rank
|
|
340
|
-
LIMIT ?`
|
|
341
|
-
).all(...params, limit);
|
|
342
|
-
}
|
|
343
|
-
function searchTodos(db2, query, filters = {}) {
|
|
344
|
-
const conditions = ["todos_fts MATCH ?"];
|
|
345
|
-
const params = [sanitizeFts(query)];
|
|
346
|
-
if (filters.repoId !== void 0) {
|
|
347
|
-
conditions.push("t.repo_id = ?");
|
|
348
|
-
params.push(filters.repoId);
|
|
349
|
-
}
|
|
350
|
-
if (filters.topicId !== void 0) {
|
|
351
|
-
conditions.push("t.topic_id = ?");
|
|
352
|
-
params.push(filters.topicId);
|
|
353
|
-
}
|
|
354
|
-
const limit = filters.limit ?? 20;
|
|
355
|
-
const where = conditions.join(" AND ");
|
|
356
|
-
return db2.prepare(
|
|
357
|
-
`SELECT t.*, r.name as repo_name, tp.name as topic_name, 'manual' as source, rank
|
|
358
|
-
FROM todos_fts
|
|
359
|
-
JOIN todos t ON t.id = todos_fts.rowid
|
|
360
|
-
LEFT JOIN repos r ON t.repo_id = r.id
|
|
361
|
-
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
362
|
-
WHERE ${where}
|
|
363
|
-
ORDER BY rank
|
|
364
|
-
LIMIT ?`
|
|
365
|
-
).all(...params, limit);
|
|
366
|
-
}
|
|
367
|
-
function getCompletedTodos(db2, filters = {}) {
|
|
368
|
-
const conditions = ["t.status = 'done'"];
|
|
369
|
-
const params = [];
|
|
370
|
-
if (filters.repoId !== void 0) {
|
|
371
|
-
conditions.push("t.repo_id = ?");
|
|
372
|
-
params.push(filters.repoId);
|
|
373
|
-
}
|
|
374
|
-
if (filters.topicId !== void 0) {
|
|
375
|
-
conditions.push("t.topic_id = ?");
|
|
376
|
-
params.push(filters.topicId);
|
|
377
|
-
}
|
|
378
|
-
if (filters.from) {
|
|
379
|
-
conditions.push("t.completed_at >= ?");
|
|
380
|
-
params.push(filters.from);
|
|
381
|
-
}
|
|
382
|
-
if (filters.to) {
|
|
383
|
-
conditions.push("t.completed_at < ?");
|
|
384
|
-
params.push(filters.to);
|
|
385
|
-
}
|
|
386
|
-
const where = conditions.join(" AND ");
|
|
387
|
-
return db2.prepare(
|
|
388
|
-
`${TODO_WITH_META_SQL} WHERE ${where} ORDER BY t.completed_at DESC`
|
|
389
|
-
).all(...params);
|
|
390
|
-
}
|
|
391
|
-
function getDueReminders(db2) {
|
|
392
|
-
const now = /* @__PURE__ */ new Date();
|
|
393
|
-
const today = now.toISOString().split("T")[0];
|
|
394
|
-
const tomorrow = new Date(now.getTime() + 864e5).toISOString().split("T")[0];
|
|
395
|
-
const todayItems = db2.prepare(
|
|
396
|
-
`${TODO_WITH_META_SQL} WHERE t.status = 'pending' AND t.remind_at >= ? AND t.remind_at < ? ORDER BY t.remind_at`
|
|
397
|
-
).all(today, tomorrow);
|
|
398
|
-
const overdueItems = db2.prepare(
|
|
399
|
-
`${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`
|
|
400
|
-
).all(today);
|
|
401
|
-
const recurringAll = db2.prepare(
|
|
402
|
-
`${TODO_WITH_META_SQL} WHERE t.remind_pattern IS NOT NULL AND (t.remind_last_done IS NULL OR t.remind_last_done < ?)`
|
|
403
|
-
).all(today);
|
|
404
|
-
const recurringToday = recurringAll.filter((r) => matchesPattern(r.remind_pattern, now));
|
|
405
|
-
const hasAny = todayItems.length > 0 || overdueItems.length > 0 || recurringToday.length > 0;
|
|
406
|
-
if (!hasAny) return null;
|
|
407
|
-
return {
|
|
408
|
-
today: groupByRepo(todayItems),
|
|
409
|
-
overdue: groupByRepo(overdueItems),
|
|
410
|
-
recurring: groupByRepo(recurringToday)
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
function ackRecurringReminder(db2, id) {
|
|
414
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
415
|
-
db2.prepare("UPDATE todos SET remind_last_done = ? WHERE id = ?").run(today, id);
|
|
416
|
-
}
|
|
417
|
-
function matchesPattern(pattern, date) {
|
|
418
|
-
const dayOfWeek = date.getDay();
|
|
419
|
-
const dayOfMonth = date.getDate();
|
|
420
|
-
if (pattern === "daily") return true;
|
|
421
|
-
if (pattern === "weekdays") return dayOfWeek >= 1 && dayOfWeek <= 5;
|
|
422
|
-
if (pattern.startsWith("weekly:")) {
|
|
423
|
-
const days = pattern.slice(7).split(",").map(Number);
|
|
424
|
-
return days.some((d) => d % 7 === dayOfWeek);
|
|
425
|
-
}
|
|
426
|
-
if (pattern.startsWith("monthly:")) {
|
|
427
|
-
const days = pattern.slice(8).split(",").map(Number);
|
|
428
|
-
return days.includes(dayOfMonth);
|
|
429
|
-
}
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
function groupByRepo(items) {
|
|
433
|
-
const map = /* @__PURE__ */ new Map();
|
|
434
|
-
for (const item of items) {
|
|
435
|
-
const key = item.repo_name ?? "global";
|
|
436
|
-
if (!map.has(key)) map.set(key, []);
|
|
437
|
-
map.get(key).push(item);
|
|
438
|
-
}
|
|
439
|
-
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([repo_name, reminders]) => ({
|
|
440
|
-
repo_name: repo_name === "global" ? null : repo_name,
|
|
441
|
-
reminders
|
|
442
|
-
}));
|
|
443
|
-
}
|
|
444
|
-
function resolveTopicId(db2, name) {
|
|
445
|
-
const existing = getTopicByName(db2, name);
|
|
446
|
-
if (existing) return existing.id;
|
|
447
|
-
const normalized = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
448
|
-
if (!normalized) return getTopicByName(db2, "chore").id;
|
|
449
|
-
const existingNorm = getTopicByName(db2, normalized);
|
|
450
|
-
if (existingNorm) return existingNorm.id;
|
|
451
|
-
const created = insertTopic(db2, normalized);
|
|
452
|
-
return created.id;
|
|
453
|
-
}
|
|
454
|
-
function syncCodeTodos(db2, repoId, currentTodos) {
|
|
455
|
-
const sync = db2.transaction(() => {
|
|
456
|
-
const existing = db2.prepare("SELECT * FROM code_todo_snapshots WHERE repo_id = ? AND resolved_at IS NULL").all(repoId);
|
|
457
|
-
const currentSet = new Set(currentTodos.map((t) => `${t.file}::${t.content}`));
|
|
458
|
-
let resolved = 0;
|
|
459
|
-
for (const snap of existing) {
|
|
460
|
-
const key = `${snap.file}::${snap.content}`;
|
|
461
|
-
if (!currentSet.has(key)) {
|
|
462
|
-
db2.prepare("UPDATE code_todo_snapshots SET resolved_at = datetime('now') WHERE id = ?").run(snap.id);
|
|
463
|
-
resolved++;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
const existingSet = new Set(existing.map((s) => `${s.file}::${s.content}`));
|
|
467
|
-
const allSnapshots = db2.prepare("SELECT file, content FROM code_todo_snapshots WHERE repo_id = ?").all(repoId);
|
|
468
|
-
const allSet = new Set(allSnapshots.map((s) => `${s.file}::${s.content}`));
|
|
469
|
-
let added = 0;
|
|
470
|
-
for (const todo of currentTodos) {
|
|
471
|
-
const key = `${todo.file}::${todo.content}`;
|
|
472
|
-
if (!allSet.has(key)) {
|
|
473
|
-
db2.prepare(
|
|
474
|
-
"INSERT INTO code_todo_snapshots (repo_id, file, line, tag, content, topic_name) VALUES (?, ?, ?, ?, ?, ?)"
|
|
475
|
-
).run(repoId, todo.file, todo.line, todo.tag, todo.content, todo.topic_name);
|
|
476
|
-
added++;
|
|
477
|
-
} else if (existingSet.has(key)) {
|
|
478
|
-
db2.prepare("UPDATE code_todo_snapshots SET line = ? WHERE repo_id = ? AND file = ? AND content = ? AND resolved_at IS NULL").run(todo.line, repoId, todo.file, todo.content);
|
|
479
|
-
}
|
|
23
|
+
function getStorage() {
|
|
24
|
+
if (instance) return instance;
|
|
25
|
+
const mode = getStorageMode();
|
|
26
|
+
if (mode === "obsidian") {
|
|
27
|
+
const dir = process.env.LOGBOOK_DIR;
|
|
28
|
+
if (!dir) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"LOGBOOK_DIR es requerido cuando LOGBOOK_STORAGE=obsidian. Debe apuntar a la carpeta del vault de Obsidian."
|
|
31
|
+
);
|
|
480
32
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const conditions = ["repo_id = ?", "resolved_at IS NOT NULL"];
|
|
487
|
-
const params = [repoId];
|
|
488
|
-
if (from) {
|
|
489
|
-
conditions.push("resolved_at >= ?");
|
|
490
|
-
params.push(from);
|
|
491
|
-
}
|
|
492
|
-
if (to) {
|
|
493
|
-
conditions.push("resolved_at < ?");
|
|
494
|
-
params.push(to);
|
|
33
|
+
const mod = require2("./obsidian/index.js");
|
|
34
|
+
instance = new mod.ObsidianStorage(dir);
|
|
35
|
+
} else {
|
|
36
|
+
const mod = require2("./sqlite/index.js");
|
|
37
|
+
instance = new mod.SqliteStorage();
|
|
495
38
|
}
|
|
496
|
-
return
|
|
497
|
-
}
|
|
498
|
-
function sanitizeFts(query) {
|
|
499
|
-
const words = query.replace(/\0/g, "").split(/\s+/).filter(Boolean).map((word) => word.replace(/"/g, "")).filter(Boolean);
|
|
500
|
-
if (words.length === 0) return '""';
|
|
501
|
-
return words.map((word) => `"${word}"`).join(" ");
|
|
39
|
+
return instance;
|
|
502
40
|
}
|
|
503
41
|
|
|
504
42
|
// src/tools/topics.ts
|
|
@@ -513,7 +51,7 @@ function registerTopicsTool(server) {
|
|
|
513
51
|
},
|
|
514
52
|
async ({ action, name, description }) => {
|
|
515
53
|
try {
|
|
516
|
-
const
|
|
54
|
+
const storage = getStorage();
|
|
517
55
|
if (action === "add") {
|
|
518
56
|
if (!name) {
|
|
519
57
|
return {
|
|
@@ -521,66 +59,34 @@ function registerTopicsTool(server) {
|
|
|
521
59
|
content: [{ type: "text", text: 'El parametro "name" es obligatorio para action=add' }]
|
|
522
60
|
};
|
|
523
61
|
}
|
|
524
|
-
const
|
|
525
|
-
if (
|
|
62
|
+
const topics2 = storage.getTopics();
|
|
63
|
+
if (topics2.some((t) => t.name === name)) {
|
|
526
64
|
return {
|
|
527
65
|
isError: true,
|
|
528
66
|
content: [{ type: "text", text: `El topic "${name}" ya existe` }]
|
|
529
67
|
};
|
|
530
|
-
}
|
|
531
|
-
const topic = insertTopic(
|
|
532
|
-
return {
|
|
533
|
-
content: [{ type: "text", text: JSON.stringify(topic) }]
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
const topics =
|
|
537
|
-
return {
|
|
538
|
-
content: [{ type: "text", text: JSON.stringify(topics) }]
|
|
539
|
-
};
|
|
540
|
-
} catch (err) {
|
|
541
|
-
return {
|
|
542
|
-
isError: true,
|
|
543
|
-
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }]
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// src/tools/note.ts
|
|
551
|
-
import { z as z2 } from "zod";
|
|
552
|
-
|
|
553
|
-
// src/git/detect-repo.ts
|
|
554
|
-
import { execFileSync } from "child_process";
|
|
555
|
-
import { basename } from "path";
|
|
556
|
-
function detectRepoPath() {
|
|
557
|
-
try {
|
|
558
|
-
const result = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
559
|
-
encoding: "utf-8",
|
|
560
|
-
timeout: 5e3,
|
|
561
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
562
|
-
});
|
|
563
|
-
return result.trim().replace(/\\/g, "/");
|
|
564
|
-
} catch {
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
function autoRegisterRepo(db2) {
|
|
569
|
-
const repoPath = detectRepoPath();
|
|
570
|
-
if (!repoPath) return null;
|
|
571
|
-
const existing = getRepoByPath(db2, repoPath);
|
|
572
|
-
if (existing) return existing;
|
|
573
|
-
let name = basename(repoPath);
|
|
574
|
-
const nameConflict = getRepoByName(db2, name);
|
|
575
|
-
if (nameConflict) {
|
|
576
|
-
const parts = repoPath.split("/");
|
|
577
|
-
const parent = parts.length >= 2 ? parts[parts.length - 2] : "repo";
|
|
578
|
-
name = `${parent}-${name}`;
|
|
579
|
-
}
|
|
580
|
-
return insertRepo(db2, name, repoPath);
|
|
68
|
+
}
|
|
69
|
+
const topic = storage.insertTopic(name, description);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(topic) }]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const topics = storage.getTopics();
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: JSON.stringify(topics) }]
|
|
77
|
+
};
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
isError: true,
|
|
81
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
581
86
|
}
|
|
582
87
|
|
|
583
88
|
// src/tools/note.ts
|
|
89
|
+
import { z as z2 } from "zod";
|
|
584
90
|
function registerNoteTool(server) {
|
|
585
91
|
server.tool(
|
|
586
92
|
"logbook_note",
|
|
@@ -591,10 +97,9 @@ function registerNoteTool(server) {
|
|
|
591
97
|
},
|
|
592
98
|
async ({ content, topic }) => {
|
|
593
99
|
try {
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
const note = insertNote(db2, repo?.id ?? null, topicId, content);
|
|
100
|
+
const storage = getStorage();
|
|
101
|
+
storage.autoRegisterRepo();
|
|
102
|
+
const note = storage.insertNote(content, topic);
|
|
598
103
|
return {
|
|
599
104
|
content: [{ type: "text", text: JSON.stringify(note) }]
|
|
600
105
|
};
|
|
@@ -642,24 +147,19 @@ function registerTodoAddTool(server) {
|
|
|
642
147
|
}]
|
|
643
148
|
};
|
|
644
149
|
}
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
const repoId = repo?.id ?? null;
|
|
150
|
+
const storage = getStorage();
|
|
151
|
+
storage.autoRegisterRepo();
|
|
648
152
|
const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal", remind_at, remind_pattern }];
|
|
649
153
|
const results = [];
|
|
650
154
|
for (const item of todoItems) {
|
|
651
155
|
const hasReminder = item.remind_at || item.remind_pattern;
|
|
652
156
|
const effectiveTopic = hasReminder && !item.topic ? "reminder" : item.topic;
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
item.priority ?? "normal",
|
|
660
|
-
item.remind_at,
|
|
661
|
-
item.remind_pattern
|
|
662
|
-
);
|
|
157
|
+
const todo = storage.insertTodo(item.content, {
|
|
158
|
+
topic: effectiveTopic,
|
|
159
|
+
priority: item.priority ?? "normal",
|
|
160
|
+
remind_at: item.remind_at,
|
|
161
|
+
remind_pattern: item.remind_pattern
|
|
162
|
+
});
|
|
663
163
|
results.push(todo);
|
|
664
164
|
}
|
|
665
165
|
return {
|
|
@@ -683,43 +183,95 @@ function registerTodoAddTool(server) {
|
|
|
683
183
|
// src/tools/todo-list.ts
|
|
684
184
|
import { z as z4 } from "zod";
|
|
685
185
|
|
|
686
|
-
// src/git/
|
|
687
|
-
import { execFileSync
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
186
|
+
// src/git/detect-repo.ts
|
|
187
|
+
import { execFileSync } from "child_process";
|
|
188
|
+
import { basename } from "path";
|
|
189
|
+
|
|
190
|
+
// src/db/queries.ts
|
|
191
|
+
var NOTE_WITH_META_SQL = `
|
|
192
|
+
SELECT n.*, r.name as repo_name, t.name as topic_name
|
|
193
|
+
FROM notes n
|
|
194
|
+
LEFT JOIN repos r ON n.repo_id = r.id
|
|
195
|
+
LEFT JOIN topics t ON n.topic_id = t.id
|
|
196
|
+
`;
|
|
197
|
+
function getNotes(db2, filters = {}) {
|
|
198
|
+
const conditions = [];
|
|
199
|
+
const params = [];
|
|
200
|
+
if (filters.repoId !== void 0) {
|
|
201
|
+
conditions.push("n.repo_id = ?");
|
|
202
|
+
params.push(filters.repoId);
|
|
203
|
+
}
|
|
204
|
+
if (filters.topicId !== void 0) {
|
|
205
|
+
conditions.push("n.topic_id = ?");
|
|
206
|
+
params.push(filters.topicId);
|
|
207
|
+
}
|
|
208
|
+
if (filters.from) {
|
|
209
|
+
conditions.push("n.created_at >= ?");
|
|
210
|
+
params.push(filters.from);
|
|
211
|
+
}
|
|
212
|
+
if (filters.to) {
|
|
213
|
+
conditions.push("n.created_at < ?");
|
|
214
|
+
params.push(filters.to);
|
|
215
|
+
}
|
|
216
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
217
|
+
const limit = filters.limit ?? 100;
|
|
218
|
+
return db2.prepare(
|
|
219
|
+
`${NOTE_WITH_META_SQL}${where} ORDER BY n.created_at DESC LIMIT ?`
|
|
220
|
+
).all(...params, limit);
|
|
221
|
+
}
|
|
222
|
+
var TODO_WITH_META_SQL = `
|
|
223
|
+
SELECT t.*, r.name as repo_name, tp.name as topic_name, 'manual' as source
|
|
224
|
+
FROM todos t
|
|
225
|
+
LEFT JOIN repos r ON t.repo_id = r.id
|
|
226
|
+
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
227
|
+
`;
|
|
228
|
+
function getTodos(db2, filters = {}) {
|
|
229
|
+
const conditions = [];
|
|
230
|
+
const params = [];
|
|
231
|
+
if (filters.status && filters.status !== "all") {
|
|
232
|
+
conditions.push("t.status = ?");
|
|
233
|
+
params.push(filters.status);
|
|
234
|
+
}
|
|
235
|
+
if (filters.repoId !== void 0) {
|
|
236
|
+
conditions.push("t.repo_id = ?");
|
|
237
|
+
params.push(filters.repoId);
|
|
238
|
+
}
|
|
239
|
+
if (filters.topicId !== void 0) {
|
|
240
|
+
conditions.push("t.topic_id = ?");
|
|
241
|
+
params.push(filters.topicId);
|
|
242
|
+
}
|
|
243
|
+
if (filters.priority) {
|
|
244
|
+
conditions.push("t.priority = ?");
|
|
245
|
+
params.push(filters.priority);
|
|
246
|
+
}
|
|
247
|
+
if (filters.from) {
|
|
248
|
+
conditions.push("t.created_at >= ?");
|
|
249
|
+
params.push(filters.from);
|
|
250
|
+
}
|
|
251
|
+
if (filters.to) {
|
|
252
|
+
conditions.push("t.created_at < ?");
|
|
253
|
+
params.push(filters.to);
|
|
254
|
+
}
|
|
255
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
256
|
+
const limit = filters.limit ?? 100;
|
|
257
|
+
return db2.prepare(
|
|
258
|
+
`${TODO_WITH_META_SQL}${where} ORDER BY t.created_at DESC LIMIT ?`
|
|
259
|
+
).all(...params, limit);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/git/detect-repo.ts
|
|
263
|
+
function detectRepoPath() {
|
|
695
264
|
try {
|
|
696
|
-
const
|
|
697
|
-
"
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
);
|
|
701
|
-
return
|
|
265
|
+
const result = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
266
|
+
encoding: "utf-8",
|
|
267
|
+
timeout: 5e3,
|
|
268
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
269
|
+
});
|
|
270
|
+
return result.trim().replace(/\\/g, "/");
|
|
702
271
|
} catch {
|
|
703
|
-
return
|
|
272
|
+
return null;
|
|
704
273
|
}
|
|
705
274
|
}
|
|
706
|
-
function parseGitGrepOutput(output) {
|
|
707
|
-
if (!output.trim()) return [];
|
|
708
|
-
const regex = /^(.+?):(\d+):.*?\b(TODO|FIXME|HACK|BUG):\s*(.+)$/;
|
|
709
|
-
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
710
|
-
const match = line.match(regex);
|
|
711
|
-
if (!match) return null;
|
|
712
|
-
const [, file, lineNum, tag, content] = match;
|
|
713
|
-
return {
|
|
714
|
-
content: content.trim(),
|
|
715
|
-
source: "code",
|
|
716
|
-
file,
|
|
717
|
-
line: parseInt(lineNum, 10),
|
|
718
|
-
tag,
|
|
719
|
-
topic_name: TAG_TO_TOPIC[tag] ?? "chore"
|
|
720
|
-
};
|
|
721
|
-
}).filter((item) => item !== null);
|
|
722
|
-
}
|
|
723
275
|
|
|
724
276
|
// src/tools/todo-list.ts
|
|
725
277
|
function registerTodoListTool(server) {
|
|
@@ -738,17 +290,11 @@ function registerTodoListTool(server) {
|
|
|
738
290
|
},
|
|
739
291
|
async ({ status, topic, priority, source, scope, from, to, limit }) => {
|
|
740
292
|
try {
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (topic) {
|
|
745
|
-
const topicRow = getTopicByName(db2, topic);
|
|
746
|
-
if (topicRow) topicId = topicRow.id;
|
|
747
|
-
}
|
|
748
|
-
const manualTodos = source === "code" ? [] : getTodos(db2, {
|
|
293
|
+
const storage = getStorage();
|
|
294
|
+
if (scope === "project") storage.autoRegisterRepo();
|
|
295
|
+
const manualTodos = source === "code" ? [] : storage.getTodos({
|
|
749
296
|
status,
|
|
750
|
-
|
|
751
|
-
topicId,
|
|
297
|
+
topicId: topic,
|
|
752
298
|
priority,
|
|
753
299
|
from,
|
|
754
300
|
to,
|
|
@@ -758,9 +304,9 @@ function registerTodoListTool(server) {
|
|
|
758
304
|
let syncResult = null;
|
|
759
305
|
if (source !== "manual" && status !== "done") {
|
|
760
306
|
const repoPath = scope === "project" ? detectRepoPath() : null;
|
|
761
|
-
if (repoPath
|
|
762
|
-
codeTodos =
|
|
763
|
-
syncResult = syncCodeTodos(
|
|
307
|
+
if (repoPath) {
|
|
308
|
+
codeTodos = storage.getCodeTodos(repoPath);
|
|
309
|
+
syncResult = storage.syncCodeTodos(repoPath, codeTodos);
|
|
764
310
|
if (topic) {
|
|
765
311
|
codeTodos = codeTodos.filter((ct) => ct.topic_name === topic);
|
|
766
312
|
}
|
|
@@ -768,7 +314,7 @@ function registerTodoListTool(server) {
|
|
|
768
314
|
}
|
|
769
315
|
const groupMap = /* @__PURE__ */ new Map();
|
|
770
316
|
for (const todo of manualTodos) {
|
|
771
|
-
const key = todo.
|
|
317
|
+
const key = todo.topic ?? "sin-topic";
|
|
772
318
|
if (!groupMap.has(key)) groupMap.set(key, []);
|
|
773
319
|
groupMap.get(key).push(todo);
|
|
774
320
|
}
|
|
@@ -809,17 +355,20 @@ function registerTodoDoneTool(server) {
|
|
|
809
355
|
"logbook_todo_done",
|
|
810
356
|
"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.",
|
|
811
357
|
{
|
|
812
|
-
ids: z5.union([z5.number(), z5.array(z5.number())]).describe("ID o array de IDs de TODOs a marcar"),
|
|
358
|
+
ids: z5.union([z5.number(), z5.string(), z5.array(z5.union([z5.number(), z5.string()]))]).describe("ID o array de IDs de TODOs a marcar"),
|
|
813
359
|
undo: z5.boolean().optional().default(false).describe("Si true, devuelve a pendiente en vez de marcar como hecho")
|
|
814
360
|
},
|
|
815
361
|
async ({ ids, undo }) => {
|
|
816
362
|
try {
|
|
817
|
-
const
|
|
818
|
-
|
|
363
|
+
const storage = getStorage();
|
|
364
|
+
storage.autoRegisterRepo();
|
|
365
|
+
const idArray = (Array.isArray(ids) ? ids : [ids]).map(String);
|
|
366
|
+
const allTodos = storage.getTodos({ status: "all" });
|
|
367
|
+
const todoMap = new Map(allTodos.map((t) => [t.id, t]));
|
|
819
368
|
const regularIds = [];
|
|
820
369
|
const recurringIds = [];
|
|
821
370
|
for (const id of idArray) {
|
|
822
|
-
const todo =
|
|
371
|
+
const todo = todoMap.get(id);
|
|
823
372
|
if (todo?.remind_pattern && !undo) {
|
|
824
373
|
recurringIds.push(id);
|
|
825
374
|
} else {
|
|
@@ -829,19 +378,13 @@ function registerTodoDoneTool(server) {
|
|
|
829
378
|
const results = [];
|
|
830
379
|
if (regularIds.length > 0) {
|
|
831
380
|
const status = undo ? "pending" : "done";
|
|
832
|
-
const updated = updateTodoStatus(
|
|
381
|
+
const updated = storage.updateTodoStatus(regularIds, status);
|
|
833
382
|
results.push(...updated);
|
|
834
383
|
}
|
|
835
384
|
for (const id of recurringIds) {
|
|
836
|
-
ackRecurringReminder(
|
|
837
|
-
const todo =
|
|
838
|
-
|
|
839
|
-
FROM todos t
|
|
840
|
-
LEFT JOIN repos r ON t.repo_id = r.id
|
|
841
|
-
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
842
|
-
WHERE t.id = ?`
|
|
843
|
-
).get(id);
|
|
844
|
-
results.push(todo);
|
|
385
|
+
storage.ackRecurringReminder(id);
|
|
386
|
+
const todo = todoMap.get(id);
|
|
387
|
+
if (todo) results.push(todo);
|
|
845
388
|
}
|
|
846
389
|
return {
|
|
847
390
|
content: [{
|
|
@@ -857,247 +400,557 @@ function registerTodoDoneTool(server) {
|
|
|
857
400
|
} catch (err) {
|
|
858
401
|
return {
|
|
859
402
|
isError: true,
|
|
860
|
-
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }]
|
|
403
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }]
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/tools/todo-edit.ts
|
|
411
|
+
import { z as z6 } from "zod";
|
|
412
|
+
function registerTodoEditTool(server) {
|
|
413
|
+
server.tool(
|
|
414
|
+
"logbook_todo_edit",
|
|
415
|
+
"Edita un TODO existente: contenido, topic o prioridad.",
|
|
416
|
+
{
|
|
417
|
+
id: z6.union([z6.number(), z6.string()]).describe("ID del TODO a editar"),
|
|
418
|
+
content: z6.string().optional().describe("Nuevo contenido"),
|
|
419
|
+
topic: z6.string().optional().describe("Nuevo topic"),
|
|
420
|
+
priority: z6.enum(["low", "normal", "high", "urgent"]).optional().describe("Nueva prioridad")
|
|
421
|
+
},
|
|
422
|
+
async ({ id, content, topic, priority }) => {
|
|
423
|
+
try {
|
|
424
|
+
const storage = getStorage();
|
|
425
|
+
storage.autoRegisterRepo();
|
|
426
|
+
const updated = storage.updateTodo(String(id), { content, topic, priority });
|
|
427
|
+
if (!updated) {
|
|
428
|
+
return {
|
|
429
|
+
isError: true,
|
|
430
|
+
content: [{ type: "text", text: `TODO #${id} no encontrado o sin cambios` }]
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: JSON.stringify(updated) }]
|
|
435
|
+
};
|
|
436
|
+
} catch (err) {
|
|
437
|
+
return {
|
|
438
|
+
isError: true,
|
|
439
|
+
content: [{ type: "text", text: `Error editando TODO: ${err instanceof Error ? err.message : String(err)}` }]
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/tools/todo-rm.ts
|
|
447
|
+
import { z as z7 } from "zod";
|
|
448
|
+
function registerTodoRmTool(server) {
|
|
449
|
+
server.tool(
|
|
450
|
+
"logbook_todo_rm",
|
|
451
|
+
"Elimina TODOs por ID. Acepta uno o varios IDs.",
|
|
452
|
+
{
|
|
453
|
+
ids: z7.union([z7.number(), z7.string(), z7.array(z7.union([z7.number(), z7.string()]))]).describe("ID o array de IDs de TODOs a eliminar")
|
|
454
|
+
},
|
|
455
|
+
async ({ ids }) => {
|
|
456
|
+
try {
|
|
457
|
+
const storage = getStorage();
|
|
458
|
+
storage.autoRegisterRepo();
|
|
459
|
+
const idArray = (Array.isArray(ids) ? ids : [ids]).map(String);
|
|
460
|
+
const deleted = storage.deleteTodos(idArray);
|
|
461
|
+
return {
|
|
462
|
+
content: [{
|
|
463
|
+
type: "text",
|
|
464
|
+
text: JSON.stringify({
|
|
465
|
+
deleted: deleted.length,
|
|
466
|
+
ids: deleted
|
|
467
|
+
})
|
|
468
|
+
}]
|
|
469
|
+
};
|
|
470
|
+
} catch (err) {
|
|
471
|
+
return {
|
|
472
|
+
isError: true,
|
|
473
|
+
content: [{ type: "text", text: `Error eliminando TODOs: ${err instanceof Error ? err.message : String(err)}` }]
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/tools/log.ts
|
|
481
|
+
import { z as z8 } from "zod";
|
|
482
|
+
function registerLogTool(server) {
|
|
483
|
+
server.tool(
|
|
484
|
+
"logbook_log",
|
|
485
|
+
"Muestra actividad: notas y TODOs completados para un periodo. Por defecto muestra el dia de hoy del proyecto actual.",
|
|
486
|
+
{
|
|
487
|
+
period: z8.enum(["today", "yesterday", "week", "month"]).optional().describe("Periodo predefinido (default: today)"),
|
|
488
|
+
from: z8.string().optional().describe("Desde fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
489
|
+
to: z8.string().optional().describe("Hasta fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
490
|
+
type: z8.enum(["all", "notes", "todos"]).optional().default("all").describe("Filtrar por tipo: all, notes, todos"),
|
|
491
|
+
topic: z8.string().optional().describe("Filtrar por topic"),
|
|
492
|
+
scope: z8.enum(["project", "global"]).optional().default("project").describe("Scope: project (auto-detecta) o global")
|
|
493
|
+
},
|
|
494
|
+
async ({ period, from, to, type, topic, scope }) => {
|
|
495
|
+
try {
|
|
496
|
+
const storage = getStorage();
|
|
497
|
+
const repo = scope === "project" ? storage.autoRegisterRepo() : null;
|
|
498
|
+
const entries = storage.getLog({
|
|
499
|
+
period,
|
|
500
|
+
from,
|
|
501
|
+
to,
|
|
502
|
+
type,
|
|
503
|
+
topic,
|
|
504
|
+
scope
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
content: [{
|
|
508
|
+
type: "text",
|
|
509
|
+
text: JSON.stringify({
|
|
510
|
+
period: from ? `${from} \u2014 ${to || from}` : period ?? "today",
|
|
511
|
+
scope: scope ?? "project",
|
|
512
|
+
project: repo?.name ?? null,
|
|
513
|
+
entries,
|
|
514
|
+
summary: {
|
|
515
|
+
total: entries.length
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
}]
|
|
519
|
+
};
|
|
520
|
+
} catch (err) {
|
|
521
|
+
return {
|
|
522
|
+
isError: true,
|
|
523
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }]
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/tools/search.ts
|
|
531
|
+
import { z as z9 } from "zod";
|
|
532
|
+
function registerSearchTool(server) {
|
|
533
|
+
server.tool(
|
|
534
|
+
"logbook_search",
|
|
535
|
+
"Busqueda full-text en notas y TODOs. Usa FTS5 para busqueda rapida.",
|
|
536
|
+
{
|
|
537
|
+
query: z9.string().min(1).max(500).describe("Texto a buscar"),
|
|
538
|
+
type: z9.enum(["all", "notes", "todos"]).optional().default("all").describe("Buscar en: all, notes, todos"),
|
|
539
|
+
topic: z9.string().optional().describe("Filtrar por topic"),
|
|
540
|
+
scope: z9.enum(["project", "global"]).optional().default("project").describe("Scope: project o global"),
|
|
541
|
+
limit: z9.number().optional().default(20).describe("Maximo resultados (default: 20)")
|
|
542
|
+
},
|
|
543
|
+
async ({ query, type, topic, scope, limit }) => {
|
|
544
|
+
try {
|
|
545
|
+
const storage = getStorage();
|
|
546
|
+
if (scope === "project") storage.autoRegisterRepo();
|
|
547
|
+
const results = storage.search(query, { type, topic, scope, limit });
|
|
548
|
+
return {
|
|
549
|
+
content: [{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: JSON.stringify({
|
|
552
|
+
query,
|
|
553
|
+
results,
|
|
554
|
+
total: results.length
|
|
555
|
+
})
|
|
556
|
+
}]
|
|
557
|
+
};
|
|
558
|
+
} catch (err) {
|
|
559
|
+
return {
|
|
560
|
+
isError: true,
|
|
561
|
+
content: [{ type: "text", text: `Error buscando: ${err instanceof Error ? err.message : String(err)}` }]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/tools/standup.ts
|
|
569
|
+
import { z as z10 } from "zod";
|
|
570
|
+
function registerStandupTool(server) {
|
|
571
|
+
server.tool(
|
|
572
|
+
"logbook_standup",
|
|
573
|
+
"Registra un standup diario con lo que se hizo ayer, lo de hoy y blockers. Ideal para Daily Notes en Obsidian.",
|
|
574
|
+
{
|
|
575
|
+
yesterday: z10.string().min(1).max(2e3).describe("Lo que se hizo ayer"),
|
|
576
|
+
today: z10.string().min(1).max(2e3).describe("Lo que se va a hacer hoy"),
|
|
577
|
+
blockers: z10.string().max(2e3).optional().default("").describe("Blockers actuales (opcional)"),
|
|
578
|
+
topic: z10.string().optional().describe("Topic (opcional)")
|
|
579
|
+
},
|
|
580
|
+
async ({ yesterday, today, blockers, topic }) => {
|
|
581
|
+
try {
|
|
582
|
+
const storage = getStorage();
|
|
583
|
+
storage.autoRegisterRepo();
|
|
584
|
+
const standup = storage.insertStandup(yesterday, today, blockers, topic);
|
|
585
|
+
return {
|
|
586
|
+
content: [{ type: "text", text: JSON.stringify(standup) }]
|
|
587
|
+
};
|
|
588
|
+
} catch (err) {
|
|
589
|
+
return {
|
|
590
|
+
isError: true,
|
|
591
|
+
content: [{ type: "text", text: `Error creando standup: ${err instanceof Error ? err.message : String(err)}` }]
|
|
861
592
|
};
|
|
862
593
|
}
|
|
863
594
|
}
|
|
864
595
|
);
|
|
865
596
|
}
|
|
866
597
|
|
|
867
|
-
// src/tools/
|
|
868
|
-
import { z as
|
|
869
|
-
function
|
|
598
|
+
// src/tools/decision.ts
|
|
599
|
+
import { z as z11 } from "zod";
|
|
600
|
+
function registerDecisionTool(server) {
|
|
870
601
|
server.tool(
|
|
871
|
-
"
|
|
872
|
-
"
|
|
602
|
+
"logbook_decision",
|
|
603
|
+
"Registra una decision arquitectonica o tecnica (ADR). Incluye contexto, opciones, decision y consecuencias.",
|
|
873
604
|
{
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
605
|
+
title: z11.string().min(1).max(200).describe("Titulo de la decision"),
|
|
606
|
+
context: z11.string().min(1).max(3e3).describe("Contexto: por que se necesita esta decision"),
|
|
607
|
+
options: z11.array(z11.string().min(1).max(500)).min(1).max(10).describe("Opciones consideradas"),
|
|
608
|
+
decision: z11.string().min(1).max(2e3).describe("Decision tomada"),
|
|
609
|
+
consequences: z11.string().min(1).max(2e3).describe("Consecuencias de la decision"),
|
|
610
|
+
topic: z11.string().optional().describe("Topic (default: decision)")
|
|
878
611
|
},
|
|
879
|
-
async ({
|
|
612
|
+
async ({ title, context, options, decision, consequences, topic }) => {
|
|
880
613
|
try {
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
const
|
|
884
|
-
if (!updated) {
|
|
885
|
-
return {
|
|
886
|
-
isError: true,
|
|
887
|
-
content: [{ type: "text", text: `TODO #${id} no encontrado o sin cambios` }]
|
|
888
|
-
};
|
|
889
|
-
}
|
|
614
|
+
const storage = getStorage();
|
|
615
|
+
storage.autoRegisterRepo();
|
|
616
|
+
const entry = storage.insertDecision(title, context, options, decision, consequences, topic);
|
|
890
617
|
return {
|
|
891
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
618
|
+
content: [{ type: "text", text: JSON.stringify(entry) }]
|
|
892
619
|
};
|
|
893
620
|
} catch (err) {
|
|
894
621
|
return {
|
|
895
622
|
isError: true,
|
|
896
|
-
content: [{ type: "text", text: `Error
|
|
623
|
+
content: [{ type: "text", text: `Error creando decision: ${err instanceof Error ? err.message : String(err)}` }]
|
|
897
624
|
};
|
|
898
625
|
}
|
|
899
626
|
}
|
|
900
627
|
);
|
|
901
628
|
}
|
|
902
629
|
|
|
903
|
-
// src/tools/
|
|
904
|
-
import { z as
|
|
905
|
-
function
|
|
630
|
+
// src/tools/debug.ts
|
|
631
|
+
import { z as z12 } from "zod";
|
|
632
|
+
function registerDebugTool(server) {
|
|
906
633
|
server.tool(
|
|
907
|
-
"
|
|
908
|
-
"
|
|
634
|
+
"logbook_debug",
|
|
635
|
+
"Registra una sesion de debug: error, causa, fix y archivo adjunto opcional. En Obsidian se muestra como callout [!bug].",
|
|
909
636
|
{
|
|
910
|
-
|
|
637
|
+
title: z12.string().min(1).max(200).describe("Titulo del bug/error"),
|
|
638
|
+
error: z12.string().min(1).max(3e3).describe("Descripcion del error"),
|
|
639
|
+
cause: z12.string().min(1).max(3e3).describe("Causa raiz identificada"),
|
|
640
|
+
fix: z12.string().min(1).max(3e3).describe("Solucion aplicada"),
|
|
641
|
+
file: z12.string().optional().describe("Ruta a archivo adjunto (screenshot, log). Se copia al vault."),
|
|
642
|
+
topic: z12.string().optional().describe("Topic (default: fix)")
|
|
911
643
|
},
|
|
912
|
-
async ({
|
|
644
|
+
async ({ title, error, cause, fix, file, topic }) => {
|
|
913
645
|
try {
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
const
|
|
646
|
+
const storage = getStorage();
|
|
647
|
+
storage.autoRegisterRepo();
|
|
648
|
+
const entry = storage.insertDebug(title, error, cause, fix, file, topic);
|
|
917
649
|
return {
|
|
918
|
-
content: [{
|
|
919
|
-
type: "text",
|
|
920
|
-
text: JSON.stringify({
|
|
921
|
-
deleted: deleted.length,
|
|
922
|
-
ids: deleted
|
|
923
|
-
})
|
|
924
|
-
}]
|
|
650
|
+
content: [{ type: "text", text: JSON.stringify(entry) }]
|
|
925
651
|
};
|
|
926
652
|
} catch (err) {
|
|
927
653
|
return {
|
|
928
654
|
isError: true,
|
|
929
|
-
content: [{ type: "text", text: `Error
|
|
655
|
+
content: [{ type: "text", text: `Error creando debug entry: ${err instanceof Error ? err.message : String(err)}` }]
|
|
930
656
|
};
|
|
931
657
|
}
|
|
932
658
|
}
|
|
933
659
|
);
|
|
934
660
|
}
|
|
935
661
|
|
|
936
|
-
// src/tools/
|
|
937
|
-
import { z as
|
|
938
|
-
function
|
|
662
|
+
// src/tools/tags.ts
|
|
663
|
+
import { z as z13 } from "zod";
|
|
664
|
+
function registerTagsTool(server) {
|
|
939
665
|
server.tool(
|
|
940
|
-
"
|
|
941
|
-
"
|
|
666
|
+
"logbook_tags",
|
|
667
|
+
"Lista todos los tags usados en el logbook con su conteo. Opcionalmente filtra por un tag especifico.",
|
|
942
668
|
{
|
|
943
|
-
|
|
944
|
-
from: z8.string().optional().describe("Desde fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
945
|
-
to: z8.string().optional().describe("Hasta fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
946
|
-
type: z8.enum(["all", "notes", "todos"]).optional().default("all").describe("Filtrar por tipo: all, notes, todos"),
|
|
947
|
-
topic: z8.string().optional().describe("Filtrar por topic"),
|
|
948
|
-
scope: z8.enum(["project", "global"]).optional().default("project").describe("Scope: project (auto-detecta) o global")
|
|
669
|
+
filter: z13.string().optional().describe("Filtrar por tag especifico (opcional)")
|
|
949
670
|
},
|
|
950
|
-
async ({
|
|
671
|
+
async ({ filter }) => {
|
|
951
672
|
try {
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
topicId,
|
|
963
|
-
from: dateFrom,
|
|
964
|
-
to: dateTo
|
|
673
|
+
const storage = getStorage();
|
|
674
|
+
storage.autoRegisterRepo();
|
|
675
|
+
const tags = storage.getTags(filter);
|
|
676
|
+
return {
|
|
677
|
+
content: [{ type: "text", text: JSON.stringify({ tags, total: tags.length }) }]
|
|
678
|
+
};
|
|
679
|
+
} catch (err) {
|
|
680
|
+
return {
|
|
681
|
+
isError: true,
|
|
682
|
+
content: [{ type: "text", text: `Error listando tags: ${err instanceof Error ? err.message : String(err)}` }]
|
|
965
683
|
};
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/tools/timeline.ts
|
|
690
|
+
import { z as z14 } from "zod";
|
|
691
|
+
function registerTimelineTool(server) {
|
|
692
|
+
server.tool(
|
|
693
|
+
"logbook_timeline",
|
|
694
|
+
"Timeline de actividad cruzando proyectos y workspaces. Muestra un resumen cronologico de toda la actividad.",
|
|
695
|
+
{
|
|
696
|
+
period: z14.enum(["today", "yesterday", "week", "month"]).optional().default("week").describe("Periodo (default: week)"),
|
|
697
|
+
from: z14.string().optional().describe("Desde fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
698
|
+
to: z14.string().optional().describe("Hasta fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
699
|
+
workspace: z14.string().optional().describe("Filtrar por workspace (opcional)")
|
|
700
|
+
},
|
|
701
|
+
async ({ period, from, to, workspace }) => {
|
|
702
|
+
try {
|
|
703
|
+
const storage = getStorage();
|
|
704
|
+
storage.autoRegisterRepo();
|
|
705
|
+
const entries = storage.getTimeline({ period, from, to, workspace });
|
|
986
706
|
return {
|
|
987
707
|
content: [{
|
|
988
708
|
type: "text",
|
|
989
709
|
text: JSON.stringify({
|
|
990
|
-
period: from ? `${
|
|
991
|
-
scope: scope ?? "project",
|
|
992
|
-
project: repo?.name ?? null,
|
|
710
|
+
period: from ? `${from} \u2014 ${to || from}` : period,
|
|
993
711
|
entries,
|
|
994
|
-
|
|
995
|
-
notes: notes.length,
|
|
996
|
-
todos_completed: completedTodos.length,
|
|
997
|
-
code_todos_resolved: resolvedCodeTodos.length,
|
|
998
|
-
total: entries.length
|
|
999
|
-
}
|
|
712
|
+
total: entries.length
|
|
1000
713
|
})
|
|
1001
714
|
}]
|
|
1002
715
|
};
|
|
1003
716
|
} catch (err) {
|
|
1004
717
|
return {
|
|
1005
718
|
isError: true,
|
|
1006
|
-
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }]
|
|
719
|
+
content: [{ type: "text", text: `Error generando timeline: ${err instanceof Error ? err.message : String(err)}` }]
|
|
1007
720
|
};
|
|
1008
721
|
}
|
|
1009
722
|
}
|
|
1010
723
|
);
|
|
1011
724
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
725
|
+
|
|
726
|
+
// src/db/connection.ts
|
|
727
|
+
import Database from "better-sqlite3";
|
|
728
|
+
import { existsSync, mkdirSync } from "fs";
|
|
729
|
+
import { homedir } from "os";
|
|
730
|
+
import { dirname, join } from "path";
|
|
731
|
+
|
|
732
|
+
// src/db/schema.ts
|
|
733
|
+
var SCHEMA_SQL = `
|
|
734
|
+
CREATE TABLE IF NOT EXISTS repos (
|
|
735
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
736
|
+
name TEXT NOT NULL UNIQUE,
|
|
737
|
+
path TEXT NOT NULL UNIQUE,
|
|
738
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
CREATE TABLE IF NOT EXISTS topics (
|
|
742
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
743
|
+
name TEXT NOT NULL UNIQUE,
|
|
744
|
+
description TEXT,
|
|
745
|
+
commit_prefix TEXT,
|
|
746
|
+
is_custom INTEGER NOT NULL DEFAULT 0,
|
|
747
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
751
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
752
|
+
repo_id INTEGER REFERENCES repos(id) ON DELETE SET NULL,
|
|
753
|
+
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
|
754
|
+
content TEXT NOT NULL,
|
|
755
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
759
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
760
|
+
repo_id INTEGER REFERENCES repos(id) ON DELETE SET NULL,
|
|
761
|
+
topic_id INTEGER REFERENCES topics(id) ON DELETE SET NULL,
|
|
762
|
+
content TEXT NOT NULL,
|
|
763
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
764
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
765
|
+
remind_at TEXT,
|
|
766
|
+
remind_pattern TEXT,
|
|
767
|
+
remind_last_done TEXT,
|
|
768
|
+
completed_at TEXT,
|
|
769
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
CREATE INDEX IF NOT EXISTS idx_notes_repo ON notes(repo_id);
|
|
773
|
+
CREATE INDEX IF NOT EXISTS idx_notes_topic ON notes(topic_id);
|
|
774
|
+
CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(created_at);
|
|
775
|
+
CREATE INDEX IF NOT EXISTS idx_todos_repo ON todos(repo_id);
|
|
776
|
+
CREATE INDEX IF NOT EXISTS idx_todos_topic ON todos(topic_id);
|
|
777
|
+
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
778
|
+
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos(created_at);
|
|
779
|
+
CREATE TABLE IF NOT EXISTS code_todo_snapshots (
|
|
780
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
781
|
+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
782
|
+
file TEXT NOT NULL,
|
|
783
|
+
line INTEGER NOT NULL,
|
|
784
|
+
tag TEXT NOT NULL,
|
|
785
|
+
content TEXT NOT NULL,
|
|
786
|
+
topic_name TEXT NOT NULL,
|
|
787
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
788
|
+
resolved_at TEXT,
|
|
789
|
+
UNIQUE(repo_id, file, content)
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
CREATE INDEX IF NOT EXISTS idx_code_snapshots_repo ON code_todo_snapshots(repo_id);
|
|
793
|
+
CREATE INDEX IF NOT EXISTS idx_code_snapshots_resolved ON code_todo_snapshots(resolved_at);
|
|
794
|
+
|
|
795
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
796
|
+
content,
|
|
797
|
+
content='notes',
|
|
798
|
+
content_rowid='id'
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS todos_fts USING fts5(
|
|
802
|
+
content,
|
|
803
|
+
content='todos',
|
|
804
|
+
content_rowid='id'
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
|
|
808
|
+
INSERT INTO notes_fts(rowid, content) VALUES (new.id, new.content);
|
|
809
|
+
END;
|
|
810
|
+
CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
|
|
811
|
+
INSERT INTO notes_fts(notes_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
812
|
+
END;
|
|
813
|
+
CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
|
|
814
|
+
INSERT INTO notes_fts(notes_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
815
|
+
INSERT INTO notes_fts(rowid, content) VALUES (new.id, new.content);
|
|
816
|
+
END;
|
|
817
|
+
|
|
818
|
+
CREATE TRIGGER IF NOT EXISTS todos_ai AFTER INSERT ON todos BEGIN
|
|
819
|
+
INSERT INTO todos_fts(rowid, content) VALUES (new.id, new.content);
|
|
820
|
+
END;
|
|
821
|
+
CREATE TRIGGER IF NOT EXISTS todos_ad AFTER DELETE ON todos BEGIN
|
|
822
|
+
INSERT INTO todos_fts(todos_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
823
|
+
END;
|
|
824
|
+
CREATE TRIGGER IF NOT EXISTS todos_au AFTER UPDATE ON todos BEGIN
|
|
825
|
+
INSERT INTO todos_fts(todos_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
826
|
+
INSERT INTO todos_fts(rowid, content) VALUES (new.id, new.content);
|
|
827
|
+
END;
|
|
828
|
+
`;
|
|
829
|
+
var MIGRATIONS_SQL = `
|
|
830
|
+
-- Add reminder columns to existing todos tables (safe to run multiple times)
|
|
831
|
+
ALTER TABLE todos ADD COLUMN remind_at TEXT;
|
|
832
|
+
ALTER TABLE todos ADD COLUMN remind_pattern TEXT;
|
|
833
|
+
ALTER TABLE todos ADD COLUMN remind_last_done TEXT;
|
|
834
|
+
`;
|
|
835
|
+
var POST_MIGRATION_SQL = `
|
|
836
|
+
CREATE INDEX IF NOT EXISTS idx_todos_remind ON todos(remind_at);
|
|
837
|
+
`;
|
|
838
|
+
var SEED_TOPICS_SQL = `
|
|
839
|
+
INSERT OR IGNORE INTO topics (name, description, commit_prefix, is_custom) VALUES
|
|
840
|
+
('feature', 'Funcionalidad nueva', 'feat', 0),
|
|
841
|
+
('fix', 'Correcci\xF3n de errores', 'fix', 0),
|
|
842
|
+
('chore', 'Mantenimiento general', 'refactor,docs,ci,build,chore,test,perf', 0),
|
|
843
|
+
('idea', 'Ideas y propuestas futuras', NULL, 0),
|
|
844
|
+
('decision', 'Decisiones tomadas', NULL, 0),
|
|
845
|
+
('blocker', 'Bloqueos activos', NULL, 0),
|
|
846
|
+
('reminder', 'Recordatorios con fecha', NULL, 0);
|
|
847
|
+
`;
|
|
848
|
+
|
|
849
|
+
// src/db/connection.ts
|
|
850
|
+
var DB_DIR = join(homedir(), ".logbook");
|
|
851
|
+
var DB_PATH = join(DB_DIR, "logbook.db");
|
|
852
|
+
var db = null;
|
|
853
|
+
function getDb(dbPath) {
|
|
854
|
+
if (db) return db;
|
|
855
|
+
const path = dbPath ?? DB_PATH;
|
|
856
|
+
const dir = dirname(path);
|
|
857
|
+
if (!existsSync(dir)) {
|
|
858
|
+
mkdirSync(dir, { recursive: true });
|
|
1017
859
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
case "month": {
|
|
1031
|
-
const monthAgo = new Date(now.getTime() - 30 * 864e5).toISOString().split("T")[0];
|
|
1032
|
-
return { dateFrom: monthAgo, dateTo: tomorrow };
|
|
860
|
+
db = new Database(path);
|
|
861
|
+
db.pragma("journal_mode = WAL");
|
|
862
|
+
db.pragma("foreign_keys = ON");
|
|
863
|
+
db.exec(SCHEMA_SQL);
|
|
864
|
+
for (const stmt of MIGRATIONS_SQL.split(";").map((s) => s.trim()).filter(Boolean)) {
|
|
865
|
+
try {
|
|
866
|
+
db.exec(stmt);
|
|
867
|
+
} catch (e) {
|
|
868
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
869
|
+
if (!msg.includes("duplicate column")) {
|
|
870
|
+
console.error(`Migration warning: ${msg}`);
|
|
871
|
+
}
|
|
1033
872
|
}
|
|
1034
|
-
default:
|
|
1035
|
-
return { dateFrom: today, dateTo: tomorrow };
|
|
1036
873
|
}
|
|
874
|
+
db.exec(POST_MIGRATION_SQL);
|
|
875
|
+
db.exec(SEED_TOPICS_SQL);
|
|
876
|
+
return db;
|
|
1037
877
|
}
|
|
1038
878
|
|
|
1039
|
-
// src/tools/
|
|
1040
|
-
|
|
1041
|
-
function registerSearchTool(server) {
|
|
879
|
+
// src/tools/migrate.ts
|
|
880
|
+
function registerMigrateTool(server) {
|
|
1042
881
|
server.tool(
|
|
1043
|
-
"
|
|
1044
|
-
"
|
|
1045
|
-
{
|
|
1046
|
-
|
|
1047
|
-
type: z9.enum(["all", "notes", "todos"]).optional().default("all").describe("Buscar en: all, notes, todos"),
|
|
1048
|
-
topic: z9.string().optional().describe("Filtrar por topic"),
|
|
1049
|
-
scope: z9.enum(["project", "global"]).optional().default("project").describe("Scope: project o global"),
|
|
1050
|
-
limit: z9.number().optional().default(20).describe("Maximo resultados (default: 20)")
|
|
1051
|
-
},
|
|
1052
|
-
async ({ query, type, topic, scope, limit }) => {
|
|
882
|
+
"logbook_migrate",
|
|
883
|
+
"Convierte datos existentes de SQLite a archivos markdown para Obsidian. Requiere LOGBOOK_STORAGE=obsidian y que exista la base de datos SQLite.",
|
|
884
|
+
{},
|
|
885
|
+
async () => {
|
|
1053
886
|
try {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
887
|
+
if (getStorageMode() !== "obsidian") {
|
|
888
|
+
return {
|
|
889
|
+
isError: true,
|
|
890
|
+
content: [{
|
|
891
|
+
type: "text",
|
|
892
|
+
text: "logbook_migrate requiere LOGBOOK_STORAGE=obsidian. Configura las variables de entorno antes de migrar."
|
|
893
|
+
}]
|
|
894
|
+
};
|
|
1060
895
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
data: n,
|
|
1073
|
-
rank: i
|
|
1074
|
-
}))
|
|
1075
|
-
);
|
|
896
|
+
let db2;
|
|
897
|
+
try {
|
|
898
|
+
db2 = getDb();
|
|
899
|
+
} catch {
|
|
900
|
+
return {
|
|
901
|
+
isError: true,
|
|
902
|
+
content: [{
|
|
903
|
+
type: "text",
|
|
904
|
+
text: "No se encontro la base de datos SQLite en ~/.logbook/logbook.db"
|
|
905
|
+
}]
|
|
906
|
+
};
|
|
1076
907
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
908
|
+
const notes = getNotes(db2, { limit: 1e4 });
|
|
909
|
+
const todos = getTodos(db2, { limit: 1e4 });
|
|
910
|
+
const storage = getStorage();
|
|
911
|
+
storage.autoRegisterRepo();
|
|
912
|
+
let migratedNotes = 0;
|
|
913
|
+
let migratedTodos = 0;
|
|
914
|
+
for (const note of notes) {
|
|
915
|
+
try {
|
|
916
|
+
storage.insertNote(note.content, note.topic_name || void 0);
|
|
917
|
+
migratedNotes++;
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
for (const todo of todos) {
|
|
922
|
+
try {
|
|
923
|
+
const entry = storage.insertTodo(todo.content, {
|
|
924
|
+
topic: todo.topic_name || void 0,
|
|
925
|
+
priority: todo.priority,
|
|
926
|
+
remind_at: todo.remind_at || void 0,
|
|
927
|
+
remind_pattern: todo.remind_pattern || void 0
|
|
928
|
+
});
|
|
929
|
+
if (todo.status === "done") {
|
|
930
|
+
storage.updateTodoStatus([entry.id], "done");
|
|
931
|
+
}
|
|
932
|
+
migratedTodos++;
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
1086
935
|
}
|
|
1087
936
|
return {
|
|
1088
937
|
content: [{
|
|
1089
938
|
type: "text",
|
|
1090
939
|
text: JSON.stringify({
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
940
|
+
migrated: {
|
|
941
|
+
notes: migratedNotes,
|
|
942
|
+
todos: migratedTodos,
|
|
943
|
+
total: migratedNotes + migratedTodos
|
|
944
|
+
},
|
|
945
|
+
source: "sqlite (~/.logbook/logbook.db)",
|
|
946
|
+
destination: process.env.LOGBOOK_DIR
|
|
1094
947
|
})
|
|
1095
948
|
}]
|
|
1096
949
|
};
|
|
1097
950
|
} catch (err) {
|
|
1098
951
|
return {
|
|
1099
952
|
isError: true,
|
|
1100
|
-
content: [{ type: "text", text: `Error
|
|
953
|
+
content: [{ type: "text", text: `Error migrando: ${err instanceof Error ? err.message : String(err)}` }]
|
|
1101
954
|
};
|
|
1102
955
|
}
|
|
1103
956
|
}
|
|
@@ -1115,14 +968,15 @@ function registerRemindersResource(server) {
|
|
|
1115
968
|
},
|
|
1116
969
|
async (uri) => {
|
|
1117
970
|
try {
|
|
1118
|
-
const
|
|
1119
|
-
|
|
971
|
+
const storage = getStorage();
|
|
972
|
+
storage.autoRegisterRepo();
|
|
973
|
+
const result = storage.getDueReminders();
|
|
1120
974
|
if (!result) {
|
|
1121
975
|
return { contents: [] };
|
|
1122
976
|
}
|
|
1123
977
|
for (const group of result.recurring) {
|
|
1124
978
|
for (const reminder of group.reminders) {
|
|
1125
|
-
ackRecurringReminder(
|
|
979
|
+
storage.ackRecurringReminder(reminder.id);
|
|
1126
980
|
}
|
|
1127
981
|
}
|
|
1128
982
|
return {
|
|
@@ -1139,7 +993,7 @@ function registerRemindersResource(server) {
|
|
|
1139
993
|
}
|
|
1140
994
|
|
|
1141
995
|
// src/server.ts
|
|
1142
|
-
var VERSION = true ? "0.
|
|
996
|
+
var VERSION = true ? "0.5.0" : "0.0.0";
|
|
1143
997
|
function createServer() {
|
|
1144
998
|
const server = new McpServer({
|
|
1145
999
|
name: "logbook-mcp",
|
|
@@ -1154,6 +1008,12 @@ function createServer() {
|
|
|
1154
1008
|
registerTodoRmTool(server);
|
|
1155
1009
|
registerLogTool(server);
|
|
1156
1010
|
registerSearchTool(server);
|
|
1011
|
+
registerStandupTool(server);
|
|
1012
|
+
registerDecisionTool(server);
|
|
1013
|
+
registerDebugTool(server);
|
|
1014
|
+
registerTagsTool(server);
|
|
1015
|
+
registerTimelineTool(server);
|
|
1016
|
+
registerMigrateTool(server);
|
|
1157
1017
|
registerRemindersResource(server);
|
|
1158
1018
|
return server;
|
|
1159
1019
|
}
|