@cocaxcode/logbook-mcp 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -6,13 +6,13 @@ async function main() {
|
|
|
6
6
|
const hasMcpFlag = argv.includes("--mcp");
|
|
7
7
|
if (hasMcpFlag) {
|
|
8
8
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
-
const { createServer } = await import("./server-
|
|
9
|
+
const { createServer } = await import("./server-CIZE5MHB.js");
|
|
10
10
|
const server = createServer();
|
|
11
11
|
const transport = new StdioServerTransport();
|
|
12
12
|
await server.connect(transport);
|
|
13
13
|
console.error("logbook-mcp server running on stdio");
|
|
14
14
|
} else {
|
|
15
|
-
const { runCli } = await import("./cli-
|
|
15
|
+
const { runCli } = await import("./cli-UFNP35WT.js");
|
|
16
16
|
await runCli(argv);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -57,6 +57,22 @@ CREATE INDEX IF NOT EXISTS idx_todos_topic ON todos(topic_id);
|
|
|
57
57
|
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
58
58
|
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos(created_at);
|
|
59
59
|
|
|
60
|
+
CREATE TABLE IF NOT EXISTS code_todo_snapshots (
|
|
61
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
|
+
repo_id INTEGER NOT NULL REFERENCES repos(id) ON DELETE CASCADE,
|
|
63
|
+
file TEXT NOT NULL,
|
|
64
|
+
line INTEGER NOT NULL,
|
|
65
|
+
tag TEXT NOT NULL,
|
|
66
|
+
content TEXT NOT NULL,
|
|
67
|
+
topic_name TEXT NOT NULL,
|
|
68
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
69
|
+
resolved_at TEXT,
|
|
70
|
+
UNIQUE(repo_id, file, content)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_code_snapshots_repo ON code_todo_snapshots(repo_id);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_code_snapshots_resolved ON code_todo_snapshots(resolved_at);
|
|
75
|
+
|
|
60
76
|
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
|
|
61
77
|
content,
|
|
62
78
|
content='notes',
|
|
@@ -349,6 +365,60 @@ function getCompletedTodos(db2, filters = {}) {
|
|
|
349
365
|
`${TODO_WITH_META_SQL} WHERE ${where} ORDER BY t.completed_at DESC`
|
|
350
366
|
).all(...params);
|
|
351
367
|
}
|
|
368
|
+
function resolveTopicId(db2, name) {
|
|
369
|
+
const existing = getTopicByName(db2, name);
|
|
370
|
+
if (existing) return existing.id;
|
|
371
|
+
const normalized = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
372
|
+
if (!normalized) return getTopicByName(db2, "chore").id;
|
|
373
|
+
const existingNorm = getTopicByName(db2, normalized);
|
|
374
|
+
if (existingNorm) return existingNorm.id;
|
|
375
|
+
const created = insertTopic(db2, normalized);
|
|
376
|
+
return created.id;
|
|
377
|
+
}
|
|
378
|
+
function syncCodeTodos(db2, repoId, currentTodos) {
|
|
379
|
+
const sync = db2.transaction(() => {
|
|
380
|
+
const existing = db2.prepare("SELECT * FROM code_todo_snapshots WHERE repo_id = ? AND resolved_at IS NULL").all(repoId);
|
|
381
|
+
const currentSet = new Set(currentTodos.map((t) => `${t.file}::${t.content}`));
|
|
382
|
+
let resolved = 0;
|
|
383
|
+
for (const snap of existing) {
|
|
384
|
+
const key = `${snap.file}::${snap.content}`;
|
|
385
|
+
if (!currentSet.has(key)) {
|
|
386
|
+
db2.prepare("UPDATE code_todo_snapshots SET resolved_at = datetime('now') WHERE id = ?").run(snap.id);
|
|
387
|
+
resolved++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const existingSet = new Set(existing.map((s) => `${s.file}::${s.content}`));
|
|
391
|
+
const allSnapshots = db2.prepare("SELECT file, content FROM code_todo_snapshots WHERE repo_id = ?").all(repoId);
|
|
392
|
+
const allSet = new Set(allSnapshots.map((s) => `${s.file}::${s.content}`));
|
|
393
|
+
let added = 0;
|
|
394
|
+
for (const todo of currentTodos) {
|
|
395
|
+
const key = `${todo.file}::${todo.content}`;
|
|
396
|
+
if (!allSet.has(key)) {
|
|
397
|
+
db2.prepare(
|
|
398
|
+
"INSERT INTO code_todo_snapshots (repo_id, file, line, tag, content, topic_name) VALUES (?, ?, ?, ?, ?, ?)"
|
|
399
|
+
).run(repoId, todo.file, todo.line, todo.tag, todo.content, todo.topic_name);
|
|
400
|
+
added++;
|
|
401
|
+
} else if (existingSet.has(key)) {
|
|
402
|
+
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);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return { added, resolved };
|
|
406
|
+
});
|
|
407
|
+
return sync();
|
|
408
|
+
}
|
|
409
|
+
function getResolvedCodeTodos(db2, repoId, from, to) {
|
|
410
|
+
const conditions = ["repo_id = ?", "resolved_at IS NOT NULL"];
|
|
411
|
+
const params = [repoId];
|
|
412
|
+
if (from) {
|
|
413
|
+
conditions.push("resolved_at >= ?");
|
|
414
|
+
params.push(from);
|
|
415
|
+
}
|
|
416
|
+
if (to) {
|
|
417
|
+
conditions.push("resolved_at < ?");
|
|
418
|
+
params.push(to);
|
|
419
|
+
}
|
|
420
|
+
return db2.prepare(`SELECT * FROM code_todo_snapshots WHERE ${conditions.join(" AND ")} ORDER BY resolved_at DESC`).all(...params);
|
|
421
|
+
}
|
|
352
422
|
function sanitizeFts(query) {
|
|
353
423
|
const words = query.replace(/\0/g, "").split(/\s+/).filter(Boolean).map((word) => word.replace(/"/g, "")).filter(Boolean);
|
|
354
424
|
if (words.length === 0) return '""';
|
|
@@ -447,21 +517,7 @@ function registerNoteTool(server) {
|
|
|
447
517
|
try {
|
|
448
518
|
const db2 = getDb();
|
|
449
519
|
const repo = autoRegisterRepo(db2);
|
|
450
|
-
|
|
451
|
-
if (topic) {
|
|
452
|
-
const topicRow = getTopicByName(db2, topic);
|
|
453
|
-
if (!topicRow) {
|
|
454
|
-
const available = getAllTopics(db2);
|
|
455
|
-
return {
|
|
456
|
-
isError: true,
|
|
457
|
-
content: [{
|
|
458
|
-
type: "text",
|
|
459
|
-
text: `Topic "${topic}" no existe. Disponibles: ${available.map((t) => t.name).join(", ")}`
|
|
460
|
-
}]
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
topicId = topicRow.id;
|
|
464
|
-
}
|
|
520
|
+
const topicId = topic ? resolveTopicId(db2, topic) : null;
|
|
465
521
|
const note = insertNote(db2, repo?.id ?? null, topicId, content);
|
|
466
522
|
return {
|
|
467
523
|
content: [{ type: "text", text: JSON.stringify(note) }]
|
|
@@ -512,21 +568,7 @@ function registerTodoAddTool(server) {
|
|
|
512
568
|
const todoItems = items ? items : [{ content, topic, priority: priority ?? "normal" }];
|
|
513
569
|
const results = [];
|
|
514
570
|
for (const item of todoItems) {
|
|
515
|
-
|
|
516
|
-
if (item.topic) {
|
|
517
|
-
const topicRow = getTopicByName(db2, item.topic);
|
|
518
|
-
if (!topicRow) {
|
|
519
|
-
const available = getAllTopics(db2);
|
|
520
|
-
return {
|
|
521
|
-
isError: true,
|
|
522
|
-
content: [{
|
|
523
|
-
type: "text",
|
|
524
|
-
text: `Topic "${item.topic}" no existe. Disponibles: ${available.map((t) => t.name).join(", ")}`
|
|
525
|
-
}]
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
topicId = topicRow.id;
|
|
529
|
-
}
|
|
571
|
+
const topicId = item.topic ? resolveTopicId(db2, item.topic) : null;
|
|
530
572
|
const todo = insertTodo(
|
|
531
573
|
db2,
|
|
532
574
|
repoId,
|
|
@@ -599,7 +641,7 @@ function parseGitGrepOutput(output) {
|
|
|
599
641
|
function registerTodoListTool(server) {
|
|
600
642
|
server.tool(
|
|
601
643
|
"logbook_todo_list",
|
|
602
|
-
"Lista TODOs agrupados por topic. Incluye
|
|
644
|
+
"Lista TODOs agrupados por topic. Incluye manuales y del codigo (TODO/FIXME/HACK/BUG). Sincroniza automaticamente: code TODOs que desaparecen del codigo se marcan como resueltos.",
|
|
603
645
|
{
|
|
604
646
|
status: z4.enum(["pending", "done", "all"]).optional().default("pending").describe("Filtrar por estado (default: pending)"),
|
|
605
647
|
topic: z4.string().optional().describe("Filtrar por topic"),
|
|
@@ -629,10 +671,12 @@ function registerTodoListTool(server) {
|
|
|
629
671
|
limit
|
|
630
672
|
});
|
|
631
673
|
let codeTodos = [];
|
|
674
|
+
let syncResult = null;
|
|
632
675
|
if (source !== "manual" && status !== "done") {
|
|
633
676
|
const repoPath = scope === "project" ? detectRepoPath() : null;
|
|
634
|
-
if (repoPath) {
|
|
677
|
+
if (repoPath && repo) {
|
|
635
678
|
codeTodos = scanCodeTodos(repoPath);
|
|
679
|
+
syncResult = syncCodeTodos(db2, repo.id, codeTodos);
|
|
636
680
|
if (topic) {
|
|
637
681
|
codeTodos = codeTodos.filter((ct) => ct.topic_name === topic);
|
|
638
682
|
}
|
|
@@ -658,7 +702,8 @@ function registerTodoListTool(server) {
|
|
|
658
702
|
summary: {
|
|
659
703
|
manual: manualTodos.length,
|
|
660
704
|
code: codeTodos.length,
|
|
661
|
-
total: manualTodos.length + codeTodos.length
|
|
705
|
+
total: manualTodos.length + codeTodos.length,
|
|
706
|
+
...syncResult && (syncResult.added > 0 || syncResult.resolved > 0) ? { sync: syncResult } : {}
|
|
662
707
|
}
|
|
663
708
|
})
|
|
664
709
|
}]
|
|
@@ -724,21 +769,7 @@ function registerTodoEditTool(server) {
|
|
|
724
769
|
async ({ id, content, topic, priority }) => {
|
|
725
770
|
try {
|
|
726
771
|
const db2 = getDb();
|
|
727
|
-
|
|
728
|
-
if (topic) {
|
|
729
|
-
const topicRow = getTopicByName(db2, topic);
|
|
730
|
-
if (!topicRow) {
|
|
731
|
-
const available = getAllTopics(db2);
|
|
732
|
-
return {
|
|
733
|
-
isError: true,
|
|
734
|
-
content: [{
|
|
735
|
-
type: "text",
|
|
736
|
-
text: `Topic "${topic}" no existe. Disponibles: ${available.map((t) => t.name).join(", ")}`
|
|
737
|
-
}]
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
topicId = topicRow.id;
|
|
741
|
-
}
|
|
772
|
+
const topicId = topic ? resolveTopicId(db2, topic) : void 0;
|
|
742
773
|
const updated = updateTodo(db2, id, { content, topicId, priority });
|
|
743
774
|
if (!updated) {
|
|
744
775
|
return {
|
|
@@ -824,6 +855,7 @@ function registerLogTool(server) {
|
|
|
824
855
|
};
|
|
825
856
|
const notes = type === "todos" ? [] : getNotes(db2, filters);
|
|
826
857
|
const completedTodos = type === "notes" ? [] : getCompletedTodos(db2, filters);
|
|
858
|
+
const resolvedCodeTodos = type === "notes" || !repo ? [] : getResolvedCodeTodos(db2, repo.id, dateFrom, dateTo);
|
|
827
859
|
const entries = [
|
|
828
860
|
...notes.map((n) => ({
|
|
829
861
|
type: "note",
|
|
@@ -834,6 +866,11 @@ function registerLogTool(server) {
|
|
|
834
866
|
type: "todo",
|
|
835
867
|
data: t,
|
|
836
868
|
timestamp: t.completed_at ?? t.created_at
|
|
869
|
+
})),
|
|
870
|
+
...resolvedCodeTodos.map((ct) => ({
|
|
871
|
+
type: "code_todo_resolved",
|
|
872
|
+
data: { file: ct.file, line: ct.line, tag: ct.tag, content: ct.content, topic_name: ct.topic_name },
|
|
873
|
+
timestamp: ct.resolved_at
|
|
837
874
|
}))
|
|
838
875
|
].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
839
876
|
return {
|
|
@@ -847,6 +884,7 @@ function registerLogTool(server) {
|
|
|
847
884
|
summary: {
|
|
848
885
|
notes: notes.length,
|
|
849
886
|
todos_completed: completedTodos.length,
|
|
887
|
+
code_todos_resolved: resolvedCodeTodos.length,
|
|
850
888
|
total: entries.length
|
|
851
889
|
}
|
|
852
890
|
})
|
|
@@ -957,7 +995,7 @@ function registerSearchTool(server) {
|
|
|
957
995
|
}
|
|
958
996
|
|
|
959
997
|
// src/server.ts
|
|
960
|
-
var VERSION = true ? "0.
|
|
998
|
+
var VERSION = true ? "0.2.0" : "0.0.0";
|
|
961
999
|
function createServer() {
|
|
962
1000
|
const server = new McpServer({
|
|
963
1001
|
name: "logbook-mcp",
|