fossel 1.0.1 → 1.0.8
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/LICENSE +21 -21
- package/README.md +108 -13
- package/dist/cli.js +1020 -0
- package/dist/index.js +469 -60
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
-
import { join } from "path";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
6
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
9
|
|
|
@@ -10,6 +11,106 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
10
11
|
import Database from "better-sqlite3";
|
|
11
12
|
import { mkdirSync } from "fs";
|
|
12
13
|
import { dirname } from "path";
|
|
14
|
+
|
|
15
|
+
// src/db/migrate.ts
|
|
16
|
+
function hasColumn(db, tableName, columnName) {
|
|
17
|
+
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
18
|
+
return columns.some((column) => column.name === columnName);
|
|
19
|
+
}
|
|
20
|
+
var migrations = [
|
|
21
|
+
{
|
|
22
|
+
name: "001_init_memories_schema",
|
|
23
|
+
apply: (db) => {
|
|
24
|
+
db.exec(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
repo TEXT NOT NULL,
|
|
28
|
+
type TEXT NOT NULL CHECK (type IN ('convention', 'bug_fix', 'reviewer_pattern', 'decision', 'issue', 'general')),
|
|
29
|
+
note TEXT NOT NULL,
|
|
30
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
31
|
+
created_at INTEGER NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories (repo);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories (created_at DESC);
|
|
36
|
+
|
|
37
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
38
|
+
repo,
|
|
39
|
+
note,
|
|
40
|
+
content = 'memories',
|
|
41
|
+
content_rowid = 'rowid'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
45
|
+
INSERT INTO memories_fts(rowid, repo, note) VALUES (new.rowid, new.repo, new.note);
|
|
46
|
+
END;
|
|
47
|
+
|
|
48
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
49
|
+
INSERT INTO memories_fts(memories_fts, rowid, repo, note) VALUES ('delete', old.rowid, old.repo, old.note);
|
|
50
|
+
END;
|
|
51
|
+
|
|
52
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
53
|
+
INSERT INTO memories_fts(memories_fts, rowid, repo, note) VALUES ('delete', old.rowid, old.repo, old.note);
|
|
54
|
+
INSERT INTO memories_fts(rowid, repo, note) VALUES (new.rowid, new.repo, new.note);
|
|
55
|
+
END;
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "002_add_memories_updated_at",
|
|
61
|
+
apply: (db) => {
|
|
62
|
+
if (!hasColumn(db, "memories", "updated_at")) {
|
|
63
|
+
db.exec(`
|
|
64
|
+
ALTER TABLE memories
|
|
65
|
+
ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;
|
|
66
|
+
`);
|
|
67
|
+
db.exec(`
|
|
68
|
+
UPDATE memories
|
|
69
|
+
SET updated_at = created_at
|
|
70
|
+
WHERE updated_at = 0;
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "003_add_memories_pinned",
|
|
77
|
+
apply: (db) => {
|
|
78
|
+
if (!hasColumn(db, "memories", "pinned")) {
|
|
79
|
+
db.exec(`
|
|
80
|
+
ALTER TABLE memories
|
|
81
|
+
ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
function runMigrations(db) {
|
|
88
|
+
db.exec(`
|
|
89
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
name TEXT NOT NULL UNIQUE,
|
|
92
|
+
applied_at INTEGER NOT NULL
|
|
93
|
+
);
|
|
94
|
+
`);
|
|
95
|
+
const appliedRows = db.prepare("SELECT name FROM migrations").all();
|
|
96
|
+
const applied = new Set(appliedRows.map((row) => row.name));
|
|
97
|
+
const insertMigration = db.prepare(`
|
|
98
|
+
INSERT INTO migrations (name, applied_at)
|
|
99
|
+
VALUES (?, ?)
|
|
100
|
+
`);
|
|
101
|
+
for (const migration of migrations) {
|
|
102
|
+
if (applied.has(migration.name)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const applyTx = db.transaction(() => {
|
|
106
|
+
migration.apply(db);
|
|
107
|
+
insertMigration.run(migration.name, Math.floor(Date.now() / 1e3));
|
|
108
|
+
});
|
|
109
|
+
applyTx();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/db/client.ts
|
|
13
114
|
var MEMORY_TYPES = [
|
|
14
115
|
"convention",
|
|
15
116
|
"bug_fix",
|
|
@@ -19,40 +120,6 @@ var MEMORY_TYPES = [
|
|
|
19
120
|
"general"
|
|
20
121
|
];
|
|
21
122
|
var dbInstance = null;
|
|
22
|
-
var SCHEMA_SQL = `
|
|
23
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
24
|
-
id TEXT PRIMARY KEY,
|
|
25
|
-
repo TEXT NOT NULL,
|
|
26
|
-
type TEXT NOT NULL CHECK (type IN ('convention', 'bug_fix', 'reviewer_pattern', 'decision', 'issue', 'general')),
|
|
27
|
-
note TEXT NOT NULL,
|
|
28
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
29
|
-
created_at INTEGER NOT NULL,
|
|
30
|
-
updated_at INTEGER NOT NULL
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories (repo);
|
|
34
|
-
CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories (created_at DESC);
|
|
35
|
-
|
|
36
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
37
|
-
repo,
|
|
38
|
-
note,
|
|
39
|
-
content = 'memories',
|
|
40
|
-
content_rowid = 'rowid'
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
44
|
-
INSERT INTO memories_fts(rowid, repo, note) VALUES (new.rowid, new.repo, new.note);
|
|
45
|
-
END;
|
|
46
|
-
|
|
47
|
-
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
48
|
-
INSERT INTO memories_fts(memories_fts, rowid, repo, note) VALUES ('delete', old.rowid, old.repo, old.note);
|
|
49
|
-
END;
|
|
50
|
-
|
|
51
|
-
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
52
|
-
INSERT INTO memories_fts(memories_fts, rowid, repo, note) VALUES ('delete', old.rowid, old.repo, old.note);
|
|
53
|
-
INSERT INTO memories_fts(rowid, repo, note) VALUES (new.rowid, new.repo, new.note);
|
|
54
|
-
END;
|
|
55
|
-
`;
|
|
56
123
|
function initDb(dbPath) {
|
|
57
124
|
if (dbInstance) {
|
|
58
125
|
return dbInstance;
|
|
@@ -61,7 +128,7 @@ function initDb(dbPath) {
|
|
|
61
128
|
const db = new Database(dbPath);
|
|
62
129
|
db.pragma("journal_mode = WAL");
|
|
63
130
|
db.pragma("foreign_keys = ON");
|
|
64
|
-
db
|
|
131
|
+
runMigrations(db);
|
|
65
132
|
dbInstance = db;
|
|
66
133
|
return db;
|
|
67
134
|
}
|
|
@@ -156,10 +223,10 @@ function registerGetRepoContextTool(server) {
|
|
|
156
223
|
const db = getDb();
|
|
157
224
|
const rows = db.prepare(
|
|
158
225
|
`
|
|
159
|
-
SELECT id, repo, type, note, tags, created_at, updated_at
|
|
226
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
160
227
|
FROM memories
|
|
161
228
|
WHERE repo = ?
|
|
162
|
-
ORDER BY updated_at DESC
|
|
229
|
+
ORDER BY pinned DESC, updated_at DESC
|
|
163
230
|
LIMIT ?
|
|
164
231
|
`
|
|
165
232
|
).all(repo, limit);
|
|
@@ -177,7 +244,8 @@ function registerGetRepoContextTool(server) {
|
|
|
177
244
|
for (const memory of rows) {
|
|
178
245
|
const tags = parseTags(memory.tags);
|
|
179
246
|
const tagSuffix = tags.length > 0 ? ` [tags: ${tags.join(", ")}]` : "";
|
|
180
|
-
const
|
|
247
|
+
const pinPrefix = memory.pinned ? "\u{1F4CC} Pinned " : "";
|
|
248
|
+
const item = `- (${memory.row_id} | legacy: ${memory.id}) ${pinPrefix}${memory.note}${tagSuffix}`;
|
|
181
249
|
const existing = grouped.get(memory.type) ?? [];
|
|
182
250
|
existing.push(item);
|
|
183
251
|
grouped.set(memory.type, existing);
|
|
@@ -218,12 +286,127 @@ ${sections.join("\n\n")}`
|
|
|
218
286
|
);
|
|
219
287
|
}
|
|
220
288
|
|
|
221
|
-
// src/tools/
|
|
289
|
+
// src/tools/pin.ts
|
|
222
290
|
import { z as z3 } from "zod";
|
|
291
|
+
var pinInputSchema = {
|
|
292
|
+
id: z3.number().int().positive()
|
|
293
|
+
};
|
|
294
|
+
function setPinnedState(memoryId, pinned) {
|
|
295
|
+
const db = getDb();
|
|
296
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
297
|
+
const updateResult = db.prepare(
|
|
298
|
+
`
|
|
299
|
+
UPDATE memories
|
|
300
|
+
SET pinned = ?, updated_at = ?
|
|
301
|
+
WHERE rowid = ?
|
|
302
|
+
`
|
|
303
|
+
).run(pinned, now, memoryId);
|
|
304
|
+
if (updateResult.changes === 0) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return db.prepare(
|
|
308
|
+
`
|
|
309
|
+
SELECT rowid AS row_id, note, pinned
|
|
310
|
+
FROM memories
|
|
311
|
+
WHERE rowid = ?
|
|
312
|
+
`
|
|
313
|
+
).get(memoryId);
|
|
314
|
+
}
|
|
315
|
+
function registerPinMemoryTool(server) {
|
|
316
|
+
server.registerTool(
|
|
317
|
+
"pin_memory",
|
|
318
|
+
{
|
|
319
|
+
description: "Pin a memory to keep it at the top of repository context.",
|
|
320
|
+
inputSchema: pinInputSchema
|
|
321
|
+
},
|
|
322
|
+
async ({ id }) => {
|
|
323
|
+
try {
|
|
324
|
+
const memory = setPinnedState(id, 1);
|
|
325
|
+
if (!memory) {
|
|
326
|
+
return {
|
|
327
|
+
isError: true,
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: `Memory ${id} not found.`
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: "text",
|
|
340
|
+
text: `Pinned memory ${memory.row_id}: ${memory.note}`
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
};
|
|
344
|
+
} catch (error) {
|
|
345
|
+
const message = error instanceof Error ? error.message : "Unknown error while pinning memory.";
|
|
346
|
+
return {
|
|
347
|
+
isError: true,
|
|
348
|
+
content: [
|
|
349
|
+
{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: `Failed to pin memory: ${message}`
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
function registerUnpinMemoryTool(server) {
|
|
360
|
+
server.registerTool(
|
|
361
|
+
"unpin_memory",
|
|
362
|
+
{
|
|
363
|
+
description: "Unpin a previously pinned memory.",
|
|
364
|
+
inputSchema: pinInputSchema
|
|
365
|
+
},
|
|
366
|
+
async ({ id }) => {
|
|
367
|
+
try {
|
|
368
|
+
const memory = setPinnedState(id, 0);
|
|
369
|
+
if (!memory) {
|
|
370
|
+
return {
|
|
371
|
+
isError: true,
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: `Memory ${id} not found.`
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: `Unpinned memory ${memory.row_id}.`
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const message = error instanceof Error ? error.message : "Unknown error while unpinning memory.";
|
|
390
|
+
return {
|
|
391
|
+
isError: true,
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: `Failed to unpin memory: ${message}`
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/tools/search.ts
|
|
405
|
+
import { z as z4 } from "zod";
|
|
223
406
|
var searchMemoryInputSchema = {
|
|
224
|
-
query:
|
|
225
|
-
repo:
|
|
226
|
-
limit:
|
|
407
|
+
query: z4.string().trim().min(1, "query is required"),
|
|
408
|
+
repo: z4.string().trim().min(1).optional(),
|
|
409
|
+
limit: z4.number().int().positive().max(50).default(5)
|
|
227
410
|
};
|
|
228
411
|
function normalizeFtsQuery(query) {
|
|
229
412
|
const terms = query.trim().split(/\s+/).map((term) => term.replaceAll('"', '""')).filter(Boolean);
|
|
@@ -253,7 +436,7 @@ function registerSearchMemoryTool(server) {
|
|
|
253
436
|
const ftsQuery = normalizeFtsQuery(query);
|
|
254
437
|
const rows = repo ? db.prepare(
|
|
255
438
|
`
|
|
256
|
-
SELECT m.id, m.repo, m.type, m.note, m.tags, m.created_at, m.updated_at, bm25(memories_fts) AS rank
|
|
439
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags, m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
257
440
|
FROM memories_fts
|
|
258
441
|
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
259
442
|
WHERE memories_fts MATCH ? AND m.repo = ?
|
|
@@ -262,7 +445,7 @@ function registerSearchMemoryTool(server) {
|
|
|
262
445
|
`
|
|
263
446
|
).all(ftsQuery, repo, limit) : db.prepare(
|
|
264
447
|
`
|
|
265
|
-
SELECT m.id, m.repo, m.type, m.note, m.tags, m.created_at, m.updated_at, bm25(memories_fts) AS rank
|
|
448
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags, m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
266
449
|
FROM memories_fts
|
|
267
450
|
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
268
451
|
WHERE memories_fts MATCH ?
|
|
@@ -283,8 +466,9 @@ function registerSearchMemoryTool(server) {
|
|
|
283
466
|
const formatted = rows.map((row, index) => {
|
|
284
467
|
const tags = parseTags2(row.tags);
|
|
285
468
|
const tagsText = tags.length > 0 ? ` | tags: ${tags.join(", ")}` : "";
|
|
286
|
-
|
|
287
|
-
${row.
|
|
469
|
+
const pinPrefix = row.pinned ? "\u{1F4CC} Pinned " : "";
|
|
470
|
+
return `${index + 1}. [${row.repo}] ${row.type} (${row.row_id} | legacy: ${row.id})
|
|
471
|
+
${pinPrefix}${row.note}${tagsText}`;
|
|
288
472
|
}).join("\n\n");
|
|
289
473
|
return {
|
|
290
474
|
content: [
|
|
@@ -314,12 +498,12 @@ ${formatted}`
|
|
|
314
498
|
|
|
315
499
|
// src/tools/store.ts
|
|
316
500
|
import { nanoid } from "nanoid";
|
|
317
|
-
import { z as
|
|
501
|
+
import { z as z5 } from "zod";
|
|
318
502
|
var storeContextInputSchema = {
|
|
319
|
-
repo:
|
|
320
|
-
type:
|
|
321
|
-
note:
|
|
322
|
-
tags:
|
|
503
|
+
repo: z5.string().trim().min(1, "repo is required"),
|
|
504
|
+
type: z5.enum(MEMORY_TYPES),
|
|
505
|
+
note: z5.string().trim().min(1, "note is required"),
|
|
506
|
+
tags: z5.array(z5.string().trim().min(1)).optional()
|
|
323
507
|
};
|
|
324
508
|
function registerStoreContextTool(server) {
|
|
325
509
|
server.registerTool(
|
|
@@ -342,11 +526,18 @@ function registerStoreContextTool(server) {
|
|
|
342
526
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
343
527
|
`
|
|
344
528
|
).run(id, repo, type, note, JSON.stringify(normalizedTags), now, now);
|
|
529
|
+
const stored = db.prepare(
|
|
530
|
+
`
|
|
531
|
+
SELECT rowid AS row_id, id
|
|
532
|
+
FROM memories
|
|
533
|
+
WHERE id = ?
|
|
534
|
+
`
|
|
535
|
+
).get(id);
|
|
345
536
|
return {
|
|
346
537
|
content: [
|
|
347
538
|
{
|
|
348
539
|
type: "text",
|
|
349
|
-
text: `Stored memory ${id} for ${repo} (${type}).`
|
|
540
|
+
text: `Stored memory ${id} (numeric id: ${stored?.row_id ?? "unknown"}) for ${repo} (${type}).`
|
|
350
541
|
}
|
|
351
542
|
]
|
|
352
543
|
};
|
|
@@ -366,9 +557,215 @@ function registerStoreContextTool(server) {
|
|
|
366
557
|
);
|
|
367
558
|
}
|
|
368
559
|
|
|
560
|
+
// src/tools/summarize.ts
|
|
561
|
+
import { z as z6 } from "zod";
|
|
562
|
+
var summarizeRepoContextInputSchema = {
|
|
563
|
+
repo: z6.string().trim().min(1, "repo is required")
|
|
564
|
+
};
|
|
565
|
+
var sectionTitleByType = {
|
|
566
|
+
convention: "Conventions",
|
|
567
|
+
bug_fix: "Bug Fixes",
|
|
568
|
+
reviewer_pattern: "Reviewer Patterns",
|
|
569
|
+
decision: "Decisions",
|
|
570
|
+
issue: "Issues",
|
|
571
|
+
general: "General"
|
|
572
|
+
};
|
|
573
|
+
function registerSummarizeRepoContextTool(server) {
|
|
574
|
+
server.registerTool(
|
|
575
|
+
"summarize_repo_context",
|
|
576
|
+
{
|
|
577
|
+
description: "Generate a structured markdown summary of all memories for a repository.",
|
|
578
|
+
inputSchema: summarizeRepoContextInputSchema
|
|
579
|
+
},
|
|
580
|
+
async ({ repo }) => {
|
|
581
|
+
try {
|
|
582
|
+
const db = getDb();
|
|
583
|
+
const rows = db.prepare(
|
|
584
|
+
`
|
|
585
|
+
SELECT rowid AS row_id, type, note, pinned
|
|
586
|
+
FROM memories
|
|
587
|
+
WHERE repo = ?
|
|
588
|
+
ORDER BY pinned DESC, updated_at DESC
|
|
589
|
+
`
|
|
590
|
+
).all(repo);
|
|
591
|
+
if (rows.length === 0) {
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: "text",
|
|
596
|
+
text: `Fossel Context Summary: ${repo}
|
|
597
|
+
|
|
598
|
+
No memories found.`
|
|
599
|
+
}
|
|
600
|
+
]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const pinnedLines = rows.filter((row) => row.pinned === 1).map((row) => `- (${row.row_id}) ${row.note}`);
|
|
604
|
+
const sections = [`Fossel Context Summary: ${repo}`];
|
|
605
|
+
if (pinnedLines.length > 0) {
|
|
606
|
+
sections.push(`\u{1F4CC} Pinned
|
|
607
|
+
${pinnedLines.join("\n")}`);
|
|
608
|
+
}
|
|
609
|
+
for (const type of MEMORY_TYPES) {
|
|
610
|
+
const entries = rows.filter((row) => row.type === type).map((row) => `- (${row.row_id}) ${row.note}`);
|
|
611
|
+
if (entries.length === 0) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
sections.push(`${sectionTitleByType[type]}
|
|
615
|
+
${entries.join("\n")}`);
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
content: [
|
|
619
|
+
{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: sections.join("\n\n")
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
};
|
|
625
|
+
} catch (error) {
|
|
626
|
+
const message = error instanceof Error ? error.message : "Unknown error while summarizing repository context.";
|
|
627
|
+
return {
|
|
628
|
+
isError: true,
|
|
629
|
+
content: [
|
|
630
|
+
{
|
|
631
|
+
type: "text",
|
|
632
|
+
text: `Failed to summarize repository context: ${message}`
|
|
633
|
+
}
|
|
634
|
+
]
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/tools/update.ts
|
|
642
|
+
import { z as z7 } from "zod";
|
|
643
|
+
var updateMemoryInputSchema = {
|
|
644
|
+
id: z7.number().int().positive(),
|
|
645
|
+
content: z7.string().trim().min(1).optional(),
|
|
646
|
+
memory_type: z7.enum(MEMORY_TYPES).optional()
|
|
647
|
+
};
|
|
648
|
+
function parseTags3(raw) {
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(raw);
|
|
651
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
652
|
+
} catch {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function formatMemory(memory) {
|
|
657
|
+
const tags = parseTags3(memory.tags);
|
|
658
|
+
const tagsLine = tags.length > 0 ? tags.join(", ") : "(none)";
|
|
659
|
+
return [
|
|
660
|
+
`Memory ${memory.row_id} updated successfully.`,
|
|
661
|
+
`id: ${memory.row_id}`,
|
|
662
|
+
`legacy_id: ${memory.id}`,
|
|
663
|
+
`repo: ${memory.repo}`,
|
|
664
|
+
`memory_type: ${memory.type}`,
|
|
665
|
+
`content: ${memory.note}`,
|
|
666
|
+
`tags: ${tagsLine}`,
|
|
667
|
+
`pinned: ${memory.pinned === 1 ? "true" : "false"}`,
|
|
668
|
+
`created_at: ${memory.created_at}`,
|
|
669
|
+
`updated_at: ${memory.updated_at}`
|
|
670
|
+
].join("\n");
|
|
671
|
+
}
|
|
672
|
+
function registerUpdateMemoryTool(server) {
|
|
673
|
+
server.registerTool(
|
|
674
|
+
"update_memory",
|
|
675
|
+
{
|
|
676
|
+
description: "Update an existing memory by numeric id with partial fields.",
|
|
677
|
+
inputSchema: updateMemoryInputSchema
|
|
678
|
+
},
|
|
679
|
+
async ({ id, content, memory_type }) => {
|
|
680
|
+
try {
|
|
681
|
+
if (!content && !memory_type) {
|
|
682
|
+
return {
|
|
683
|
+
isError: true,
|
|
684
|
+
content: [
|
|
685
|
+
{
|
|
686
|
+
type: "text",
|
|
687
|
+
text: "Provide at least one field to update: content or memory_type."
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const db = getDb();
|
|
693
|
+
const existing = db.prepare(
|
|
694
|
+
`
|
|
695
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
696
|
+
FROM memories
|
|
697
|
+
WHERE rowid = ?
|
|
698
|
+
`
|
|
699
|
+
).get(id);
|
|
700
|
+
if (!existing) {
|
|
701
|
+
return {
|
|
702
|
+
isError: true,
|
|
703
|
+
content: [
|
|
704
|
+
{
|
|
705
|
+
type: "text",
|
|
706
|
+
text: `Memory ${id} not found.`
|
|
707
|
+
}
|
|
708
|
+
]
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
712
|
+
const nextType = memory_type ?? existing.type;
|
|
713
|
+
const nextNote = content ?? existing.note;
|
|
714
|
+
db.prepare(
|
|
715
|
+
`
|
|
716
|
+
UPDATE memories
|
|
717
|
+
SET type = ?, note = ?, updated_at = ?
|
|
718
|
+
WHERE rowid = ?
|
|
719
|
+
`
|
|
720
|
+
).run(nextType, nextNote, now, id);
|
|
721
|
+
const updated = db.prepare(
|
|
722
|
+
`
|
|
723
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
724
|
+
FROM memories
|
|
725
|
+
WHERE rowid = ?
|
|
726
|
+
`
|
|
727
|
+
).get(id);
|
|
728
|
+
if (!updated) {
|
|
729
|
+
return {
|
|
730
|
+
isError: true,
|
|
731
|
+
content: [
|
|
732
|
+
{
|
|
733
|
+
type: "text",
|
|
734
|
+
text: `Memory ${id} could not be loaded after update.`
|
|
735
|
+
}
|
|
736
|
+
]
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
content: [
|
|
741
|
+
{
|
|
742
|
+
type: "text",
|
|
743
|
+
text: formatMemory(updated)
|
|
744
|
+
}
|
|
745
|
+
]
|
|
746
|
+
};
|
|
747
|
+
} catch (error) {
|
|
748
|
+
const message = error instanceof Error ? error.message : "Unknown error while updating memory.";
|
|
749
|
+
return {
|
|
750
|
+
isError: true,
|
|
751
|
+
content: [
|
|
752
|
+
{
|
|
753
|
+
type: "text",
|
|
754
|
+
text: `Failed to update memory: ${message}`
|
|
755
|
+
}
|
|
756
|
+
]
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
369
763
|
// src/index.ts
|
|
370
|
-
|
|
371
|
-
|
|
764
|
+
function resolveDbPath() {
|
|
765
|
+
return process.env.FOSSEL_DB_PATH?.trim() || join(homedir(), ".fossel", "memory.db");
|
|
766
|
+
}
|
|
767
|
+
async function startServer() {
|
|
768
|
+
const dbPath = resolveDbPath();
|
|
372
769
|
initDb(dbPath);
|
|
373
770
|
const server = new McpServer({
|
|
374
771
|
name: "fossel",
|
|
@@ -378,11 +775,23 @@ async function main() {
|
|
|
378
775
|
registerGetRepoContextTool(server);
|
|
379
776
|
registerSearchMemoryTool(server);
|
|
380
777
|
registerDeleteMemoryTool(server);
|
|
778
|
+
registerUpdateMemoryTool(server);
|
|
779
|
+
registerPinMemoryTool(server);
|
|
780
|
+
registerUnpinMemoryTool(server);
|
|
781
|
+
registerSummarizeRepoContextTool(server);
|
|
381
782
|
const transport = new StdioServerTransport();
|
|
382
783
|
await server.connect(transport);
|
|
383
784
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
785
|
+
var entryPath = process.argv[1];
|
|
786
|
+
var currentPath = fileURLToPath(import.meta.url);
|
|
787
|
+
if (entryPath && currentPath === resolve(entryPath)) {
|
|
788
|
+
startServer().catch((error) => {
|
|
789
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
790
|
+
console.error(`Fossel server failed to start: ${message}`);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
export {
|
|
795
|
+
resolveDbPath,
|
|
796
|
+
startServer
|
|
797
|
+
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fossel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Local MCP memory server for open-source contributors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
8
8
|
],
|
|
9
9
|
"bin": {
|
|
10
|
-
"fossel": "dist/
|
|
10
|
+
"fossel": "dist/cli.js"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsup",
|
|
14
14
|
"dev": "tsx src/index.ts",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
|
-
"smoke": "tsx scripts/smoke.ts"
|
|
16
|
+
"smoke": "tsx scripts/smoke.ts",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"ci": "npm run typecheck && npm run build && npm run smoke"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
21
|
"@modelcontextprotocol/sdk": "latest",
|