@cocaxcode/logbook-mcp 0.1.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 +485 -0
- package/dist/cli-TYBWWQAH.js +24 -0
- package/dist/index.js +22 -0
- package/dist/server-5ZT2YKCQ.js +958 -0
- package/package.json +57 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
|
|
6
|
+
// src/tools/topics.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
// src/db/connection.ts
|
|
10
|
+
import Database from "better-sqlite3";
|
|
11
|
+
import { existsSync, mkdirSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
|
|
15
|
+
// src/db/schema.ts
|
|
16
|
+
var SCHEMA_SQL = `
|
|
17
|
+
CREATE TABLE IF NOT EXISTS repos (
|
|
18
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19
|
+
name TEXT NOT NULL UNIQUE,
|
|
20
|
+
path TEXT NOT NULL UNIQUE,
|
|
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
|
+
completed_at TEXT,
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_notes_repo ON notes(repo_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_notes_topic ON notes(topic_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(created_at);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_todos_repo ON todos(repo_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_todos_topic ON todos(topic_id);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos(created_at);
|
|
59
|
+
|
|
60
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
61
|
+
content,
|
|
62
|
+
content='notes',
|
|
63
|
+
content_rowid='id'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS todos_fts USING fts5(
|
|
67
|
+
content,
|
|
68
|
+
content='todos',
|
|
69
|
+
content_rowid='id'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
|
|
73
|
+
INSERT INTO notes_fts(rowid, content) VALUES (new.id, new.content);
|
|
74
|
+
END;
|
|
75
|
+
CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
|
|
76
|
+
INSERT INTO notes_fts(notes_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
77
|
+
END;
|
|
78
|
+
CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
|
|
79
|
+
INSERT INTO notes_fts(notes_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
80
|
+
INSERT INTO notes_fts(rowid, content) VALUES (new.id, new.content);
|
|
81
|
+
END;
|
|
82
|
+
|
|
83
|
+
CREATE TRIGGER IF NOT EXISTS todos_ai AFTER INSERT ON todos BEGIN
|
|
84
|
+
INSERT INTO todos_fts(rowid, content) VALUES (new.id, new.content);
|
|
85
|
+
END;
|
|
86
|
+
CREATE TRIGGER IF NOT EXISTS todos_ad AFTER DELETE ON todos BEGIN
|
|
87
|
+
INSERT INTO todos_fts(todos_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
88
|
+
END;
|
|
89
|
+
CREATE TRIGGER IF NOT EXISTS todos_au AFTER UPDATE ON todos BEGIN
|
|
90
|
+
INSERT INTO todos_fts(todos_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
91
|
+
INSERT INTO todos_fts(rowid, content) VALUES (new.id, new.content);
|
|
92
|
+
END;
|
|
93
|
+
`;
|
|
94
|
+
var SEED_TOPICS_SQL = `
|
|
95
|
+
INSERT OR IGNORE INTO topics (name, description, commit_prefix, is_custom) VALUES
|
|
96
|
+
('feature', 'Funcionalidad nueva', 'feat', 0),
|
|
97
|
+
('fix', 'Correcci\xF3n de errores', 'fix', 0),
|
|
98
|
+
('chore', 'Mantenimiento general', 'refactor,docs,ci,build,chore,test,perf', 0),
|
|
99
|
+
('idea', 'Ideas y propuestas futuras', NULL, 0),
|
|
100
|
+
('decision', 'Decisiones tomadas', NULL, 0),
|
|
101
|
+
('blocker', 'Bloqueos activos', NULL, 0);
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
// src/db/connection.ts
|
|
105
|
+
var DB_DIR = join(homedir(), ".logbook");
|
|
106
|
+
var DB_PATH = join(DB_DIR, "logbook.db");
|
|
107
|
+
var db = null;
|
|
108
|
+
function getDb(dbPath) {
|
|
109
|
+
if (db) return db;
|
|
110
|
+
const path = dbPath ?? DB_PATH;
|
|
111
|
+
const dir = dirname(path);
|
|
112
|
+
if (!existsSync(dir)) {
|
|
113
|
+
mkdirSync(dir, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
db = new Database(path);
|
|
116
|
+
db.pragma("journal_mode = WAL");
|
|
117
|
+
db.pragma("foreign_keys = ON");
|
|
118
|
+
db.exec(SCHEMA_SQL);
|
|
119
|
+
db.exec(SEED_TOPICS_SQL);
|
|
120
|
+
return db;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/db/queries.ts
|
|
124
|
+
function insertRepo(db2, name, path) {
|
|
125
|
+
const stmt = db2.prepare(
|
|
126
|
+
"INSERT INTO repos (name, path) VALUES (?, ?)"
|
|
127
|
+
);
|
|
128
|
+
const result = stmt.run(name, path);
|
|
129
|
+
return db2.prepare("SELECT * FROM repos WHERE id = ?").get(result.lastInsertRowid);
|
|
130
|
+
}
|
|
131
|
+
function getRepoByPath(db2, path) {
|
|
132
|
+
return db2.prepare("SELECT * FROM repos WHERE path = ?").get(path);
|
|
133
|
+
}
|
|
134
|
+
function getTopicByName(db2, name) {
|
|
135
|
+
return db2.prepare("SELECT * FROM topics WHERE name = ?").get(name);
|
|
136
|
+
}
|
|
137
|
+
function getAllTopics(db2) {
|
|
138
|
+
return db2.prepare("SELECT * FROM topics ORDER BY is_custom, name").all();
|
|
139
|
+
}
|
|
140
|
+
function insertTopic(db2, name, description) {
|
|
141
|
+
const stmt = db2.prepare(
|
|
142
|
+
"INSERT INTO topics (name, description, is_custom) VALUES (?, ?, 1)"
|
|
143
|
+
);
|
|
144
|
+
const result = stmt.run(name, description ?? null);
|
|
145
|
+
return db2.prepare("SELECT * FROM topics WHERE id = ?").get(result.lastInsertRowid);
|
|
146
|
+
}
|
|
147
|
+
var NOTE_WITH_META_SQL = `
|
|
148
|
+
SELECT n.*, r.name as repo_name, t.name as topic_name
|
|
149
|
+
FROM notes n
|
|
150
|
+
LEFT JOIN repos r ON n.repo_id = r.id
|
|
151
|
+
LEFT JOIN topics t ON n.topic_id = t.id
|
|
152
|
+
`;
|
|
153
|
+
function insertNote(db2, repoId, topicId, content) {
|
|
154
|
+
const stmt = db2.prepare(
|
|
155
|
+
"INSERT INTO notes (repo_id, topic_id, content) VALUES (?, ?, ?)"
|
|
156
|
+
);
|
|
157
|
+
const result = stmt.run(repoId, topicId, content);
|
|
158
|
+
return db2.prepare(`${NOTE_WITH_META_SQL} WHERE n.id = ?`).get(result.lastInsertRowid);
|
|
159
|
+
}
|
|
160
|
+
function getNotes(db2, filters = {}) {
|
|
161
|
+
const conditions = [];
|
|
162
|
+
const params = [];
|
|
163
|
+
if (filters.repoId !== void 0) {
|
|
164
|
+
conditions.push("n.repo_id = ?");
|
|
165
|
+
params.push(filters.repoId);
|
|
166
|
+
}
|
|
167
|
+
if (filters.topicId !== void 0) {
|
|
168
|
+
conditions.push("n.topic_id = ?");
|
|
169
|
+
params.push(filters.topicId);
|
|
170
|
+
}
|
|
171
|
+
if (filters.from) {
|
|
172
|
+
conditions.push("n.created_at >= ?");
|
|
173
|
+
params.push(filters.from);
|
|
174
|
+
}
|
|
175
|
+
if (filters.to) {
|
|
176
|
+
conditions.push("n.created_at < ?");
|
|
177
|
+
params.push(filters.to);
|
|
178
|
+
}
|
|
179
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
180
|
+
const limit = filters.limit ?? 100;
|
|
181
|
+
return db2.prepare(
|
|
182
|
+
`${NOTE_WITH_META_SQL}${where} ORDER BY n.created_at DESC LIMIT ?`
|
|
183
|
+
).all(...params, limit);
|
|
184
|
+
}
|
|
185
|
+
var TODO_WITH_META_SQL = `
|
|
186
|
+
SELECT t.*, r.name as repo_name, tp.name as topic_name, 'manual' as source
|
|
187
|
+
FROM todos t
|
|
188
|
+
LEFT JOIN repos r ON t.repo_id = r.id
|
|
189
|
+
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
190
|
+
`;
|
|
191
|
+
function insertTodo(db2, repoId, topicId, content, priority = "normal") {
|
|
192
|
+
const stmt = db2.prepare(
|
|
193
|
+
"INSERT INTO todos (repo_id, topic_id, content, priority) VALUES (?, ?, ?, ?)"
|
|
194
|
+
);
|
|
195
|
+
const result = stmt.run(repoId, topicId, content, priority);
|
|
196
|
+
return db2.prepare(`${TODO_WITH_META_SQL} WHERE t.id = ?`).get(result.lastInsertRowid);
|
|
197
|
+
}
|
|
198
|
+
function getTodos(db2, filters = {}) {
|
|
199
|
+
const conditions = [];
|
|
200
|
+
const params = [];
|
|
201
|
+
if (filters.status && filters.status !== "all") {
|
|
202
|
+
conditions.push("t.status = ?");
|
|
203
|
+
params.push(filters.status);
|
|
204
|
+
}
|
|
205
|
+
if (filters.repoId !== void 0) {
|
|
206
|
+
conditions.push("t.repo_id = ?");
|
|
207
|
+
params.push(filters.repoId);
|
|
208
|
+
}
|
|
209
|
+
if (filters.topicId !== void 0) {
|
|
210
|
+
conditions.push("t.topic_id = ?");
|
|
211
|
+
params.push(filters.topicId);
|
|
212
|
+
}
|
|
213
|
+
if (filters.priority) {
|
|
214
|
+
conditions.push("t.priority = ?");
|
|
215
|
+
params.push(filters.priority);
|
|
216
|
+
}
|
|
217
|
+
if (filters.from) {
|
|
218
|
+
conditions.push("t.created_at >= ?");
|
|
219
|
+
params.push(filters.from);
|
|
220
|
+
}
|
|
221
|
+
if (filters.to) {
|
|
222
|
+
conditions.push("t.created_at < ?");
|
|
223
|
+
params.push(filters.to);
|
|
224
|
+
}
|
|
225
|
+
const where = conditions.length ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
226
|
+
const limit = filters.limit ?? 100;
|
|
227
|
+
return db2.prepare(
|
|
228
|
+
`${TODO_WITH_META_SQL}${where} ORDER BY t.created_at DESC LIMIT ?`
|
|
229
|
+
).all(...params, limit);
|
|
230
|
+
}
|
|
231
|
+
function updateTodoStatus(db2, ids, status) {
|
|
232
|
+
const completedAt = status === "done" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
233
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
234
|
+
db2.prepare(
|
|
235
|
+
`UPDATE todos SET status = ?, completed_at = ? WHERE id IN (${placeholders})`
|
|
236
|
+
).run(status, completedAt, ...ids);
|
|
237
|
+
return db2.prepare(
|
|
238
|
+
`${TODO_WITH_META_SQL} WHERE t.id IN (${placeholders})`
|
|
239
|
+
).all(...ids);
|
|
240
|
+
}
|
|
241
|
+
function updateTodo(db2, id, fields) {
|
|
242
|
+
const sets = [];
|
|
243
|
+
const params = [];
|
|
244
|
+
if (fields.content !== void 0) {
|
|
245
|
+
sets.push("content = ?");
|
|
246
|
+
params.push(fields.content);
|
|
247
|
+
}
|
|
248
|
+
if (fields.topicId !== void 0) {
|
|
249
|
+
sets.push("topic_id = ?");
|
|
250
|
+
params.push(fields.topicId);
|
|
251
|
+
}
|
|
252
|
+
if (fields.priority !== void 0) {
|
|
253
|
+
sets.push("priority = ?");
|
|
254
|
+
params.push(fields.priority);
|
|
255
|
+
}
|
|
256
|
+
if (sets.length === 0) return void 0;
|
|
257
|
+
params.push(id);
|
|
258
|
+
db2.prepare(`UPDATE todos SET ${sets.join(", ")} WHERE id = ?`).run(
|
|
259
|
+
...params
|
|
260
|
+
);
|
|
261
|
+
return db2.prepare(`${TODO_WITH_META_SQL} WHERE t.id = ?`).get(id);
|
|
262
|
+
}
|
|
263
|
+
function deleteTodos(db2, ids) {
|
|
264
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
265
|
+
const existing = db2.prepare(`SELECT id FROM todos WHERE id IN (${placeholders})`).all(...ids);
|
|
266
|
+
const existingIds = existing.map((r) => r.id);
|
|
267
|
+
if (existingIds.length > 0) {
|
|
268
|
+
const ep = existingIds.map(() => "?").join(",");
|
|
269
|
+
db2.prepare(`DELETE FROM todos WHERE id IN (${ep})`).run(
|
|
270
|
+
...existingIds
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return existingIds;
|
|
274
|
+
}
|
|
275
|
+
function searchNotes(db2, query, filters = {}) {
|
|
276
|
+
const conditions = ["notes_fts MATCH ?"];
|
|
277
|
+
const params = [sanitizeFts(query)];
|
|
278
|
+
if (filters.repoId !== void 0) {
|
|
279
|
+
conditions.push("n.repo_id = ?");
|
|
280
|
+
params.push(filters.repoId);
|
|
281
|
+
}
|
|
282
|
+
if (filters.topicId !== void 0) {
|
|
283
|
+
conditions.push("n.topic_id = ?");
|
|
284
|
+
params.push(filters.topicId);
|
|
285
|
+
}
|
|
286
|
+
const limit = filters.limit ?? 20;
|
|
287
|
+
const where = conditions.join(" AND ");
|
|
288
|
+
return db2.prepare(
|
|
289
|
+
`SELECT n.*, r.name as repo_name, t.name as topic_name, rank
|
|
290
|
+
FROM notes_fts
|
|
291
|
+
JOIN notes n ON n.id = notes_fts.rowid
|
|
292
|
+
LEFT JOIN repos r ON n.repo_id = r.id
|
|
293
|
+
LEFT JOIN topics t ON n.topic_id = t.id
|
|
294
|
+
WHERE ${where}
|
|
295
|
+
ORDER BY rank
|
|
296
|
+
LIMIT ?`
|
|
297
|
+
).all(...params, limit);
|
|
298
|
+
}
|
|
299
|
+
function searchTodos(db2, query, filters = {}) {
|
|
300
|
+
const conditions = ["todos_fts MATCH ?"];
|
|
301
|
+
const params = [sanitizeFts(query)];
|
|
302
|
+
if (filters.repoId !== void 0) {
|
|
303
|
+
conditions.push("t.repo_id = ?");
|
|
304
|
+
params.push(filters.repoId);
|
|
305
|
+
}
|
|
306
|
+
if (filters.topicId !== void 0) {
|
|
307
|
+
conditions.push("t.topic_id = ?");
|
|
308
|
+
params.push(filters.topicId);
|
|
309
|
+
}
|
|
310
|
+
const limit = filters.limit ?? 20;
|
|
311
|
+
const where = conditions.join(" AND ");
|
|
312
|
+
return db2.prepare(
|
|
313
|
+
`SELECT t.*, r.name as repo_name, tp.name as topic_name, 'manual' as source, rank
|
|
314
|
+
FROM todos_fts
|
|
315
|
+
JOIN todos t ON t.id = todos_fts.rowid
|
|
316
|
+
LEFT JOIN repos r ON t.repo_id = r.id
|
|
317
|
+
LEFT JOIN topics tp ON t.topic_id = tp.id
|
|
318
|
+
WHERE ${where}
|
|
319
|
+
ORDER BY rank
|
|
320
|
+
LIMIT ?`
|
|
321
|
+
).all(...params, limit);
|
|
322
|
+
}
|
|
323
|
+
function getCompletedTodos(db2, filters = {}) {
|
|
324
|
+
const conditions = ["t.status = 'done'"];
|
|
325
|
+
const params = [];
|
|
326
|
+
if (filters.repoId !== void 0) {
|
|
327
|
+
conditions.push("t.repo_id = ?");
|
|
328
|
+
params.push(filters.repoId);
|
|
329
|
+
}
|
|
330
|
+
if (filters.topicId !== void 0) {
|
|
331
|
+
conditions.push("t.topic_id = ?");
|
|
332
|
+
params.push(filters.topicId);
|
|
333
|
+
}
|
|
334
|
+
if (filters.from) {
|
|
335
|
+
conditions.push("t.completed_at >= ?");
|
|
336
|
+
params.push(filters.from);
|
|
337
|
+
}
|
|
338
|
+
if (filters.to) {
|
|
339
|
+
conditions.push("t.completed_at < ?");
|
|
340
|
+
params.push(filters.to);
|
|
341
|
+
}
|
|
342
|
+
const where = conditions.join(" AND ");
|
|
343
|
+
return db2.prepare(
|
|
344
|
+
`${TODO_WITH_META_SQL} WHERE ${where} ORDER BY t.completed_at DESC`
|
|
345
|
+
).all(...params);
|
|
346
|
+
}
|
|
347
|
+
function sanitizeFts(query) {
|
|
348
|
+
return query.split(/\s+/).filter(Boolean).map((word) => `"${word.replace(/"/g, "")}"`).join(" ");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/tools/topics.ts
|
|
352
|
+
function registerTopicsTool(server) {
|
|
353
|
+
server.tool(
|
|
354
|
+
"logbook_topics",
|
|
355
|
+
"Lista o crea temas para organizar notas y TODOs. Temas predefinidos: feature, fix, chore, idea, decision, blocker.",
|
|
356
|
+
{
|
|
357
|
+
action: z.enum(["list", "add"]).default("list").describe("Accion: list (ver temas) o add (crear tema custom)"),
|
|
358
|
+
name: z.string().optional().describe("Nombre del nuevo tema (solo para action=add, lowercase, sin espacios)"),
|
|
359
|
+
description: z.string().optional().describe("Descripcion del nuevo tema (solo para action=add)")
|
|
360
|
+
},
|
|
361
|
+
async ({ action, name, description }) => {
|
|
362
|
+
try {
|
|
363
|
+
const db2 = getDb();
|
|
364
|
+
if (action === "add") {
|
|
365
|
+
if (!name) {
|
|
366
|
+
return {
|
|
367
|
+
isError: true,
|
|
368
|
+
content: [{ type: "text", text: 'El parametro "name" es obligatorio para action=add' }]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const normalized = name.toLowerCase().replace(/\s+/g, "-");
|
|
372
|
+
const topic = insertTopic(db2, normalized, description);
|
|
373
|
+
return {
|
|
374
|
+
content: [{ type: "text", text: JSON.stringify(topic) }]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const topics = getAllTopics(db2);
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: JSON.stringify(topics) }]
|
|
380
|
+
};
|
|
381
|
+
} catch (err) {
|
|
382
|
+
return {
|
|
383
|
+
isError: true,
|
|
384
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/tools/note.ts
|
|
392
|
+
import { z as z2 } from "zod";
|
|
393
|
+
|
|
394
|
+
// src/git/detect-repo.ts
|
|
395
|
+
import { execSync } from "child_process";
|
|
396
|
+
import { basename } from "path";
|
|
397
|
+
function detectRepoPath() {
|
|
398
|
+
try {
|
|
399
|
+
const result = execSync("git rev-parse --show-toplevel", {
|
|
400
|
+
encoding: "utf-8",
|
|
401
|
+
timeout: 5e3,
|
|
402
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
403
|
+
});
|
|
404
|
+
return result.trim().replace(/\\/g, "/");
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function autoRegisterRepo(db2) {
|
|
410
|
+
const repoPath = detectRepoPath();
|
|
411
|
+
if (!repoPath) return null;
|
|
412
|
+
const existing = getRepoByPath(db2, repoPath);
|
|
413
|
+
if (existing) return existing;
|
|
414
|
+
const name = basename(repoPath);
|
|
415
|
+
return insertRepo(db2, name, repoPath);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/tools/note.ts
|
|
419
|
+
function registerNoteTool(server) {
|
|
420
|
+
server.tool(
|
|
421
|
+
"logbook_note",
|
|
422
|
+
"A\xF1ade una nota al logbook. Topics: feature, fix, chore, idea, decision, blocker. Si no se pasa topic, queda sin categorizar.",
|
|
423
|
+
{
|
|
424
|
+
content: z2.string().describe("Contenido de la nota"),
|
|
425
|
+
topic: z2.string().optional().describe("Topic: feature, fix, chore, idea, decision, blocker (o custom)")
|
|
426
|
+
},
|
|
427
|
+
async ({ content, topic }) => {
|
|
428
|
+
try {
|
|
429
|
+
const db2 = getDb();
|
|
430
|
+
const repo = autoRegisterRepo(db2);
|
|
431
|
+
let topicId = null;
|
|
432
|
+
if (topic) {
|
|
433
|
+
const topicRow = getTopicByName(db2, topic);
|
|
434
|
+
if (!topicRow) {
|
|
435
|
+
const available = getAllTopics(db2);
|
|
436
|
+
return {
|
|
437
|
+
isError: true,
|
|
438
|
+
content: [{
|
|
439
|
+
type: "text",
|
|
440
|
+
text: `Topic "${topic}" no existe. Disponibles: ${available.map((t) => t.name).join(", ")}`
|
|
441
|
+
}]
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
topicId = topicRow.id;
|
|
445
|
+
}
|
|
446
|
+
const note = insertNote(db2, repo?.id ?? null, topicId, content);
|
|
447
|
+
return {
|
|
448
|
+
content: [{ type: "text", text: JSON.stringify(note) }]
|
|
449
|
+
};
|
|
450
|
+
} catch (err) {
|
|
451
|
+
return {
|
|
452
|
+
isError: true,
|
|
453
|
+
content: [{ type: "text", text: `Error creando nota: ${err.message}` }]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/tools/todo-add.ts
|
|
461
|
+
import { z as z3 } from "zod";
|
|
462
|
+
var priorityEnum = z3.enum(["low", "normal", "high", "urgent"]);
|
|
463
|
+
function registerTodoAddTool(server) {
|
|
464
|
+
server.tool(
|
|
465
|
+
"logbook_todo_add",
|
|
466
|
+
"Crea uno o varios TODOs. Para uno solo usa content. Para varios usa items (array). El topic se puede pasar o dejar que la AI lo infiera.",
|
|
467
|
+
{
|
|
468
|
+
content: z3.string().optional().describe("Contenido del TODO (para crear uno solo)"),
|
|
469
|
+
topic: z3.string().optional().describe("Topic para el TODO individual"),
|
|
470
|
+
priority: priorityEnum.optional().default("normal").describe("Prioridad del TODO individual"),
|
|
471
|
+
items: z3.array(
|
|
472
|
+
z3.object({
|
|
473
|
+
content: z3.string().describe("Contenido del TODO"),
|
|
474
|
+
topic: z3.string().optional().describe("Topic"),
|
|
475
|
+
priority: priorityEnum.optional().default("normal").describe("Prioridad")
|
|
476
|
+
})
|
|
477
|
+
).optional().describe("Array de TODOs para crear varios a la vez")
|
|
478
|
+
},
|
|
479
|
+
async ({ content, topic, priority, items }) => {
|
|
480
|
+
try {
|
|
481
|
+
if (!content && (!items || items.length === 0)) {
|
|
482
|
+
return {
|
|
483
|
+
isError: true,
|
|
484
|
+
content: [{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: 'Debes pasar "content" (para uno) o "items" (para varios)'
|
|
487
|
+
}]
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const db2 = getDb();
|
|
491
|
+
const repo = autoRegisterRepo(db2);
|
|
492
|
+
const repoId = repo?.id ?? null;
|
|
493
|
+
const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal" }];
|
|
494
|
+
const results = [];
|
|
495
|
+
for (const item of todoItems) {
|
|
496
|
+
let topicId = null;
|
|
497
|
+
if (item.topic) {
|
|
498
|
+
const topicRow = getTopicByName(db2, item.topic);
|
|
499
|
+
if (!topicRow) {
|
|
500
|
+
const available = getAllTopics(db2);
|
|
501
|
+
return {
|
|
502
|
+
isError: true,
|
|
503
|
+
content: [{
|
|
504
|
+
type: "text",
|
|
505
|
+
text: `Topic "${item.topic}" no existe. Disponibles: ${available.map((t) => t.name).join(", ")}`
|
|
506
|
+
}]
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
topicId = topicRow.id;
|
|
510
|
+
}
|
|
511
|
+
const todo = insertTodo(
|
|
512
|
+
db2,
|
|
513
|
+
repoId,
|
|
514
|
+
topicId,
|
|
515
|
+
item.content,
|
|
516
|
+
item.priority ?? "normal"
|
|
517
|
+
);
|
|
518
|
+
results.push(todo);
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
content: [{
|
|
522
|
+
type: "text",
|
|
523
|
+
text: JSON.stringify(
|
|
524
|
+
results.length === 1 ? results[0] : { created: results.length, todos: results }
|
|
525
|
+
)
|
|
526
|
+
}]
|
|
527
|
+
};
|
|
528
|
+
} catch (err) {
|
|
529
|
+
return {
|
|
530
|
+
isError: true,
|
|
531
|
+
content: [{ type: "text", text: `Error creando TODO: ${err.message}` }]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/tools/todo-list.ts
|
|
539
|
+
import { z as z4 } from "zod";
|
|
540
|
+
|
|
541
|
+
// src/git/code-todos.ts
|
|
542
|
+
import { execSync as execSync2 } from "child_process";
|
|
543
|
+
var TAG_TO_TOPIC = {
|
|
544
|
+
TODO: "feature",
|
|
545
|
+
FIXME: "fix",
|
|
546
|
+
BUG: "fix",
|
|
547
|
+
HACK: "chore"
|
|
548
|
+
};
|
|
549
|
+
function scanCodeTodos(repoPath) {
|
|
550
|
+
try {
|
|
551
|
+
const output = execSync2(
|
|
552
|
+
`git -C "${repoPath}" grep -n -E "(TODO|FIXME|HACK|BUG):" --no-color`,
|
|
553
|
+
{ encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
|
|
554
|
+
);
|
|
555
|
+
return parseGitGrepOutput(output);
|
|
556
|
+
} catch {
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function parseGitGrepOutput(output) {
|
|
561
|
+
if (!output.trim()) return [];
|
|
562
|
+
const regex = /^(.+?):(\d+):.*?\b(TODO|FIXME|HACK|BUG):\s*(.+)$/;
|
|
563
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
564
|
+
const match = line.match(regex);
|
|
565
|
+
if (!match) return null;
|
|
566
|
+
const [, file, lineNum, tag, content] = match;
|
|
567
|
+
return {
|
|
568
|
+
content: content.trim(),
|
|
569
|
+
source: "code",
|
|
570
|
+
file,
|
|
571
|
+
line: parseInt(lineNum, 10),
|
|
572
|
+
tag,
|
|
573
|
+
topic_name: TAG_TO_TOPIC[tag] ?? "chore"
|
|
574
|
+
};
|
|
575
|
+
}).filter((item) => item !== null);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/tools/todo-list.ts
|
|
579
|
+
function registerTodoListTool(server) {
|
|
580
|
+
server.tool(
|
|
581
|
+
"logbook_todo_list",
|
|
582
|
+
"Lista TODOs agrupados por topic. Incluye TODOs manuales y del codigo (TODO/FIXME/HACK/BUG). Por defecto muestra pendientes del proyecto actual.",
|
|
583
|
+
{
|
|
584
|
+
status: z4.enum(["pending", "done", "all"]).optional().default("pending").describe("Filtrar por estado (default: pending)"),
|
|
585
|
+
topic: z4.string().optional().describe("Filtrar por topic"),
|
|
586
|
+
priority: z4.enum(["low", "normal", "high", "urgent"]).optional().describe("Filtrar por prioridad"),
|
|
587
|
+
source: z4.enum(["all", "manual", "code"]).optional().default("all").describe("Filtrar por origen: manual (DB), code (git grep), all"),
|
|
588
|
+
scope: z4.enum(["project", "global"]).optional().default("project").describe("Scope: project (auto-detecta) o global (todos los proyectos)"),
|
|
589
|
+
from: z4.string().optional().describe("Desde fecha (YYYY-MM-DD)"),
|
|
590
|
+
to: z4.string().optional().describe("Hasta fecha (YYYY-MM-DD)"),
|
|
591
|
+
limit: z4.number().optional().default(100).describe("Maximo resultados manuales")
|
|
592
|
+
},
|
|
593
|
+
async ({ status, topic, priority, source, scope, from, to, limit }) => {
|
|
594
|
+
try {
|
|
595
|
+
const db2 = getDb();
|
|
596
|
+
const repo = scope === "project" ? autoRegisterRepo(db2) : null;
|
|
597
|
+
let topicId;
|
|
598
|
+
if (topic) {
|
|
599
|
+
const topicRow = getTopicByName(db2, topic);
|
|
600
|
+
if (topicRow) topicId = topicRow.id;
|
|
601
|
+
}
|
|
602
|
+
const manualTodos = source === "code" ? [] : getTodos(db2, {
|
|
603
|
+
status,
|
|
604
|
+
repoId: repo?.id,
|
|
605
|
+
topicId,
|
|
606
|
+
priority,
|
|
607
|
+
from,
|
|
608
|
+
to,
|
|
609
|
+
limit
|
|
610
|
+
});
|
|
611
|
+
let codeTodos = [];
|
|
612
|
+
if (source !== "manual" && status !== "done") {
|
|
613
|
+
const repoPath = scope === "project" ? detectRepoPath() : null;
|
|
614
|
+
if (repoPath) {
|
|
615
|
+
codeTodos = scanCodeTodos(repoPath);
|
|
616
|
+
if (topic) {
|
|
617
|
+
codeTodos = codeTodos.filter((ct) => ct.topic_name === topic);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
622
|
+
for (const todo of manualTodos) {
|
|
623
|
+
const key = todo.topic_name ?? "sin-topic";
|
|
624
|
+
if (!groupMap.has(key)) groupMap.set(key, []);
|
|
625
|
+
groupMap.get(key).push(todo);
|
|
626
|
+
}
|
|
627
|
+
for (const ct of codeTodos) {
|
|
628
|
+
const key = ct.topic_name;
|
|
629
|
+
if (!groupMap.has(key)) groupMap.set(key, []);
|
|
630
|
+
groupMap.get(key).push(ct);
|
|
631
|
+
}
|
|
632
|
+
const groups = Array.from(groupMap.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([topicName, items]) => ({ topic: topicName, items }));
|
|
633
|
+
return {
|
|
634
|
+
content: [{
|
|
635
|
+
type: "text",
|
|
636
|
+
text: JSON.stringify({
|
|
637
|
+
groups,
|
|
638
|
+
summary: {
|
|
639
|
+
manual: manualTodos.length,
|
|
640
|
+
code: codeTodos.length,
|
|
641
|
+
total: manualTodos.length + codeTodos.length
|
|
642
|
+
}
|
|
643
|
+
})
|
|
644
|
+
}]
|
|
645
|
+
};
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return {
|
|
648
|
+
isError: true,
|
|
649
|
+
content: [{ type: "text", text: `Error listando TODOs: ${err.message}` }]
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/tools/todo-done.ts
|
|
657
|
+
import { z as z5 } from "zod";
|
|
658
|
+
function registerTodoDoneTool(server) {
|
|
659
|
+
server.tool(
|
|
660
|
+
"logbook_todo_done",
|
|
661
|
+
"Marca TODOs como hechos o los devuelve a pendiente (undo). Acepta uno o varios IDs.",
|
|
662
|
+
{
|
|
663
|
+
ids: z5.union([z5.number(), z5.array(z5.number())]).describe("ID o array de IDs de TODOs a marcar"),
|
|
664
|
+
undo: z5.boolean().optional().default(false).describe("Si true, devuelve a pendiente en vez de marcar como hecho")
|
|
665
|
+
},
|
|
666
|
+
async ({ ids, undo }) => {
|
|
667
|
+
try {
|
|
668
|
+
const db2 = getDb();
|
|
669
|
+
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
670
|
+
const status = undo ? "pending" : "done";
|
|
671
|
+
const updated = updateTodoStatus(db2, idArray, status);
|
|
672
|
+
return {
|
|
673
|
+
content: [{
|
|
674
|
+
type: "text",
|
|
675
|
+
text: JSON.stringify({
|
|
676
|
+
action: undo ? "undo" : "done",
|
|
677
|
+
updated: updated.length,
|
|
678
|
+
todos: updated
|
|
679
|
+
})
|
|
680
|
+
}]
|
|
681
|
+
};
|
|
682
|
+
} catch (err) {
|
|
683
|
+
return {
|
|
684
|
+
isError: true,
|
|
685
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/tools/todo-edit.ts
|
|
693
|
+
import { z as z6 } from "zod";
|
|
694
|
+
function registerTodoEditTool(server) {
|
|
695
|
+
server.tool(
|
|
696
|
+
"logbook_todo_edit",
|
|
697
|
+
"Edita un TODO existente: contenido, topic o prioridad.",
|
|
698
|
+
{
|
|
699
|
+
id: z6.number().describe("ID del TODO a editar"),
|
|
700
|
+
content: z6.string().optional().describe("Nuevo contenido"),
|
|
701
|
+
topic: z6.string().optional().describe("Nuevo topic"),
|
|
702
|
+
priority: z6.enum(["low", "normal", "high", "urgent"]).optional().describe("Nueva prioridad")
|
|
703
|
+
},
|
|
704
|
+
async ({ id, content, topic, priority }) => {
|
|
705
|
+
try {
|
|
706
|
+
const db2 = getDb();
|
|
707
|
+
let topicId;
|
|
708
|
+
if (topic) {
|
|
709
|
+
const topicRow = getTopicByName(db2, topic);
|
|
710
|
+
if (!topicRow) {
|
|
711
|
+
const available = getAllTopics(db2);
|
|
712
|
+
return {
|
|
713
|
+
isError: true,
|
|
714
|
+
content: [{
|
|
715
|
+
type: "text",
|
|
716
|
+
text: `Topic "${topic}" no existe. Disponibles: ${available.map((t) => t.name).join(", ")}`
|
|
717
|
+
}]
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
topicId = topicRow.id;
|
|
721
|
+
}
|
|
722
|
+
const updated = updateTodo(db2, id, { content, topicId, priority });
|
|
723
|
+
if (!updated) {
|
|
724
|
+
return {
|
|
725
|
+
isError: true,
|
|
726
|
+
content: [{ type: "text", text: `TODO #${id} no encontrado o sin cambios` }]
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
content: [{ type: "text", text: JSON.stringify(updated) }]
|
|
731
|
+
};
|
|
732
|
+
} catch (err) {
|
|
733
|
+
return {
|
|
734
|
+
isError: true,
|
|
735
|
+
content: [{ type: "text", text: `Error editando TODO: ${err.message}` }]
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/tools/todo-rm.ts
|
|
743
|
+
import { z as z7 } from "zod";
|
|
744
|
+
function registerTodoRmTool(server) {
|
|
745
|
+
server.tool(
|
|
746
|
+
"logbook_todo_rm",
|
|
747
|
+
"Elimina TODOs por ID. Acepta uno o varios IDs.",
|
|
748
|
+
{
|
|
749
|
+
ids: z7.union([z7.number(), z7.array(z7.number())]).describe("ID o array de IDs de TODOs a eliminar")
|
|
750
|
+
},
|
|
751
|
+
async ({ ids }) => {
|
|
752
|
+
try {
|
|
753
|
+
const db2 = getDb();
|
|
754
|
+
const idArray = Array.isArray(ids) ? ids : [ids];
|
|
755
|
+
const deleted = deleteTodos(db2, idArray);
|
|
756
|
+
return {
|
|
757
|
+
content: [{
|
|
758
|
+
type: "text",
|
|
759
|
+
text: JSON.stringify({
|
|
760
|
+
deleted: deleted.length,
|
|
761
|
+
ids: deleted
|
|
762
|
+
})
|
|
763
|
+
}]
|
|
764
|
+
};
|
|
765
|
+
} catch (err) {
|
|
766
|
+
return {
|
|
767
|
+
isError: true,
|
|
768
|
+
content: [{ type: "text", text: `Error eliminando TODOs: ${err.message}` }]
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/tools/log.ts
|
|
776
|
+
import { z as z8 } from "zod";
|
|
777
|
+
function registerLogTool(server) {
|
|
778
|
+
server.tool(
|
|
779
|
+
"logbook_log",
|
|
780
|
+
"Muestra actividad: notas y TODOs completados para un periodo. Por defecto muestra el dia de hoy del proyecto actual.",
|
|
781
|
+
{
|
|
782
|
+
period: z8.enum(["today", "yesterday", "week", "month"]).optional().describe("Periodo predefinido (default: today)"),
|
|
783
|
+
from: z8.string().optional().describe("Desde fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
784
|
+
to: z8.string().optional().describe("Hasta fecha (YYYY-MM-DD). Sobreescribe period."),
|
|
785
|
+
type: z8.enum(["all", "notes", "todos"]).optional().default("all").describe("Filtrar por tipo: all, notes, todos"),
|
|
786
|
+
topic: z8.string().optional().describe("Filtrar por topic"),
|
|
787
|
+
scope: z8.enum(["project", "global"]).optional().default("project").describe("Scope: project (auto-detecta) o global")
|
|
788
|
+
},
|
|
789
|
+
async ({ period, from, to, type, topic, scope }) => {
|
|
790
|
+
try {
|
|
791
|
+
const db2 = getDb();
|
|
792
|
+
const repo = scope === "project" ? autoRegisterRepo(db2) : null;
|
|
793
|
+
const { dateFrom, dateTo } = resolveDates(period, from, to);
|
|
794
|
+
let topicId;
|
|
795
|
+
if (topic) {
|
|
796
|
+
const topicRow = getTopicByName(db2, topic);
|
|
797
|
+
if (topicRow) topicId = topicRow.id;
|
|
798
|
+
}
|
|
799
|
+
const filters = {
|
|
800
|
+
repoId: repo?.id,
|
|
801
|
+
topicId,
|
|
802
|
+
from: dateFrom,
|
|
803
|
+
to: dateTo
|
|
804
|
+
};
|
|
805
|
+
const notes = type === "todos" ? [] : getNotes(db2, filters);
|
|
806
|
+
const completedTodos = type === "notes" ? [] : getCompletedTodos(db2, filters);
|
|
807
|
+
const entries = [
|
|
808
|
+
...notes.map((n) => ({
|
|
809
|
+
type: "note",
|
|
810
|
+
data: n,
|
|
811
|
+
timestamp: n.created_at
|
|
812
|
+
})),
|
|
813
|
+
...completedTodos.map((t) => ({
|
|
814
|
+
type: "todo",
|
|
815
|
+
data: t,
|
|
816
|
+
timestamp: t.completed_at ?? t.created_at
|
|
817
|
+
}))
|
|
818
|
+
].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
819
|
+
return {
|
|
820
|
+
content: [{
|
|
821
|
+
type: "text",
|
|
822
|
+
text: JSON.stringify({
|
|
823
|
+
period: from ? `${dateFrom} \u2014 ${dateTo}` : period ?? "today",
|
|
824
|
+
scope: scope ?? "project",
|
|
825
|
+
project: repo?.name ?? null,
|
|
826
|
+
entries,
|
|
827
|
+
summary: {
|
|
828
|
+
notes: notes.length,
|
|
829
|
+
todos_completed: completedTodos.length,
|
|
830
|
+
total: entries.length
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
}]
|
|
834
|
+
};
|
|
835
|
+
} catch (err) {
|
|
836
|
+
return {
|
|
837
|
+
isError: true,
|
|
838
|
+
content: [{ type: "text", text: `Error: ${err.message}` }]
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
function resolveDates(period, from, to) {
|
|
845
|
+
if (from) {
|
|
846
|
+
const dateTo = to ?? new Date(Date.now() + 864e5).toISOString().split("T")[0];
|
|
847
|
+
return { dateFrom: from, dateTo };
|
|
848
|
+
}
|
|
849
|
+
const now = /* @__PURE__ */ new Date();
|
|
850
|
+
const today = now.toISOString().split("T")[0];
|
|
851
|
+
const tomorrow = new Date(now.getTime() + 864e5).toISOString().split("T")[0];
|
|
852
|
+
switch (period) {
|
|
853
|
+
case "yesterday": {
|
|
854
|
+
const yesterday = new Date(now.getTime() - 864e5).toISOString().split("T")[0];
|
|
855
|
+
return { dateFrom: yesterday, dateTo: today };
|
|
856
|
+
}
|
|
857
|
+
case "week": {
|
|
858
|
+
const weekAgo = new Date(now.getTime() - 7 * 864e5).toISOString().split("T")[0];
|
|
859
|
+
return { dateFrom: weekAgo, dateTo: tomorrow };
|
|
860
|
+
}
|
|
861
|
+
case "month": {
|
|
862
|
+
const monthAgo = new Date(now.getTime() - 30 * 864e5).toISOString().split("T")[0];
|
|
863
|
+
return { dateFrom: monthAgo, dateTo: tomorrow };
|
|
864
|
+
}
|
|
865
|
+
default:
|
|
866
|
+
return { dateFrom: today, dateTo: tomorrow };
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/tools/search.ts
|
|
871
|
+
import { z as z9 } from "zod";
|
|
872
|
+
function registerSearchTool(server) {
|
|
873
|
+
server.tool(
|
|
874
|
+
"logbook_search",
|
|
875
|
+
"Busqueda full-text en notas y TODOs. Usa FTS5 para busqueda rapida.",
|
|
876
|
+
{
|
|
877
|
+
query: z9.string().describe("Texto a buscar"),
|
|
878
|
+
type: z9.enum(["all", "notes", "todos"]).optional().default("all").describe("Buscar en: all, notes, todos"),
|
|
879
|
+
topic: z9.string().optional().describe("Filtrar por topic"),
|
|
880
|
+
scope: z9.enum(["project", "global"]).optional().default("project").describe("Scope: project o global"),
|
|
881
|
+
limit: z9.number().optional().default(20).describe("Maximo resultados (default: 20)")
|
|
882
|
+
},
|
|
883
|
+
async ({ query, type, topic, scope, limit }) => {
|
|
884
|
+
try {
|
|
885
|
+
const db2 = getDb();
|
|
886
|
+
const repo = scope === "project" ? autoRegisterRepo(db2) : null;
|
|
887
|
+
let topicId;
|
|
888
|
+
if (topic) {
|
|
889
|
+
const topicRow = getTopicByName(db2, topic);
|
|
890
|
+
if (topicRow) topicId = topicRow.id;
|
|
891
|
+
}
|
|
892
|
+
const filters = {
|
|
893
|
+
repoId: repo?.id,
|
|
894
|
+
topicId,
|
|
895
|
+
limit
|
|
896
|
+
};
|
|
897
|
+
const results = [];
|
|
898
|
+
if (type !== "todos") {
|
|
899
|
+
const notes = searchNotes(db2, query, filters);
|
|
900
|
+
results.push(
|
|
901
|
+
...notes.map((n, i) => ({
|
|
902
|
+
type: "note",
|
|
903
|
+
data: n,
|
|
904
|
+
rank: i
|
|
905
|
+
}))
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (type !== "notes") {
|
|
909
|
+
const todos = searchTodos(db2, query, filters);
|
|
910
|
+
results.push(
|
|
911
|
+
...todos.map((t, i) => ({
|
|
912
|
+
type: "todo",
|
|
913
|
+
data: t,
|
|
914
|
+
rank: i
|
|
915
|
+
}))
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
content: [{
|
|
920
|
+
type: "text",
|
|
921
|
+
text: JSON.stringify({
|
|
922
|
+
query,
|
|
923
|
+
results,
|
|
924
|
+
total: results.length
|
|
925
|
+
})
|
|
926
|
+
}]
|
|
927
|
+
};
|
|
928
|
+
} catch (err) {
|
|
929
|
+
return {
|
|
930
|
+
isError: true,
|
|
931
|
+
content: [{ type: "text", text: `Error buscando: ${err.message}` }]
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/server.ts
|
|
939
|
+
var VERSION = true ? "0.1.0" : "0.0.0";
|
|
940
|
+
function createServer() {
|
|
941
|
+
const server = new McpServer({
|
|
942
|
+
name: "logbook-mcp",
|
|
943
|
+
version: VERSION
|
|
944
|
+
});
|
|
945
|
+
registerTopicsTool(server);
|
|
946
|
+
registerNoteTool(server);
|
|
947
|
+
registerTodoAddTool(server);
|
|
948
|
+
registerTodoListTool(server);
|
|
949
|
+
registerTodoDoneTool(server);
|
|
950
|
+
registerTodoEditTool(server);
|
|
951
|
+
registerTodoRmTool(server);
|
|
952
|
+
registerLogTool(server);
|
|
953
|
+
registerSearchTool(server);
|
|
954
|
+
return server;
|
|
955
|
+
}
|
|
956
|
+
export {
|
|
957
|
+
createServer
|
|
958
|
+
};
|