fossel 1.0.6 → 1.0.9

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.
Files changed (4) hide show
  1. package/README.md +125 -35
  2. package/dist/cli.js +1020 -0
  3. package/dist/index.js +469 -60
  4. package/package.json +26 -3
package/dist/cli.js ADDED
@@ -0,0 +1,1020 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/db/migrate.ts
13
+ function hasColumn(db, tableName, columnName) {
14
+ const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
15
+ return columns.some((column) => column.name === columnName);
16
+ }
17
+ function runMigrations(db) {
18
+ db.exec(`
19
+ CREATE TABLE IF NOT EXISTS migrations (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ name TEXT NOT NULL UNIQUE,
22
+ applied_at INTEGER NOT NULL
23
+ );
24
+ `);
25
+ const appliedRows = db.prepare("SELECT name FROM migrations").all();
26
+ const applied = new Set(appliedRows.map((row) => row.name));
27
+ const insertMigration = db.prepare(`
28
+ INSERT INTO migrations (name, applied_at)
29
+ VALUES (?, ?)
30
+ `);
31
+ for (const migration of migrations) {
32
+ if (applied.has(migration.name)) {
33
+ continue;
34
+ }
35
+ const applyTx = db.transaction(() => {
36
+ migration.apply(db);
37
+ insertMigration.run(migration.name, Math.floor(Date.now() / 1e3));
38
+ });
39
+ applyTx();
40
+ }
41
+ }
42
+ var migrations;
43
+ var init_migrate = __esm({
44
+ "src/db/migrate.ts"() {
45
+ "use strict";
46
+ migrations = [
47
+ {
48
+ name: "001_init_memories_schema",
49
+ apply: (db) => {
50
+ db.exec(`
51
+ CREATE TABLE IF NOT EXISTS memories (
52
+ id TEXT PRIMARY KEY,
53
+ repo TEXT NOT NULL,
54
+ type TEXT NOT NULL CHECK (type IN ('convention', 'bug_fix', 'reviewer_pattern', 'decision', 'issue', 'general')),
55
+ note TEXT NOT NULL,
56
+ tags TEXT NOT NULL DEFAULT '[]',
57
+ created_at INTEGER NOT NULL
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories (repo);
61
+ CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories (created_at DESC);
62
+
63
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
64
+ repo,
65
+ note,
66
+ content = 'memories',
67
+ content_rowid = 'rowid'
68
+ );
69
+
70
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
71
+ INSERT INTO memories_fts(rowid, repo, note) VALUES (new.rowid, new.repo, new.note);
72
+ END;
73
+
74
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
75
+ INSERT INTO memories_fts(memories_fts, rowid, repo, note) VALUES ('delete', old.rowid, old.repo, old.note);
76
+ END;
77
+
78
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
79
+ INSERT INTO memories_fts(memories_fts, rowid, repo, note) VALUES ('delete', old.rowid, old.repo, old.note);
80
+ INSERT INTO memories_fts(rowid, repo, note) VALUES (new.rowid, new.repo, new.note);
81
+ END;
82
+ `);
83
+ }
84
+ },
85
+ {
86
+ name: "002_add_memories_updated_at",
87
+ apply: (db) => {
88
+ if (!hasColumn(db, "memories", "updated_at")) {
89
+ db.exec(`
90
+ ALTER TABLE memories
91
+ ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0;
92
+ `);
93
+ db.exec(`
94
+ UPDATE memories
95
+ SET updated_at = created_at
96
+ WHERE updated_at = 0;
97
+ `);
98
+ }
99
+ }
100
+ },
101
+ {
102
+ name: "003_add_memories_pinned",
103
+ apply: (db) => {
104
+ if (!hasColumn(db, "memories", "pinned")) {
105
+ db.exec(`
106
+ ALTER TABLE memories
107
+ ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
108
+ `);
109
+ }
110
+ }
111
+ }
112
+ ];
113
+ }
114
+ });
115
+
116
+ // src/db/client.ts
117
+ import Database from "better-sqlite3";
118
+ import { mkdirSync } from "fs";
119
+ import { dirname } from "path";
120
+ function initDb(dbPath) {
121
+ if (dbInstance) {
122
+ return dbInstance;
123
+ }
124
+ mkdirSync(dirname(dbPath), { recursive: true });
125
+ const db = new Database(dbPath);
126
+ db.pragma("journal_mode = WAL");
127
+ db.pragma("foreign_keys = ON");
128
+ runMigrations(db);
129
+ dbInstance = db;
130
+ return db;
131
+ }
132
+ function getDb() {
133
+ if (!dbInstance) {
134
+ throw new Error("Database has not been initialized. Call initDb() first.");
135
+ }
136
+ return dbInstance;
137
+ }
138
+ function closeDb() {
139
+ if (!dbInstance) {
140
+ return;
141
+ }
142
+ dbInstance.close();
143
+ dbInstance = null;
144
+ }
145
+ var MEMORY_TYPES, dbInstance;
146
+ var init_client = __esm({
147
+ "src/db/client.ts"() {
148
+ "use strict";
149
+ init_migrate();
150
+ MEMORY_TYPES = [
151
+ "convention",
152
+ "bug_fix",
153
+ "reviewer_pattern",
154
+ "decision",
155
+ "issue",
156
+ "general"
157
+ ];
158
+ dbInstance = null;
159
+ }
160
+ });
161
+
162
+ // src/tools/delete.ts
163
+ import { z } from "zod";
164
+ function registerDeleteMemoryTool(server) {
165
+ server.registerTool(
166
+ "delete_memory",
167
+ {
168
+ description: "Delete a memory from storage by id.",
169
+ inputSchema: deleteMemoryInputSchema
170
+ },
171
+ async ({ id }) => {
172
+ try {
173
+ const db = getDb();
174
+ const row = db.prepare("SELECT id FROM memories WHERE id = ?").get(id);
175
+ if (!row) {
176
+ return {
177
+ isError: true,
178
+ content: [
179
+ {
180
+ type: "text",
181
+ text: `Memory ${id} not found.`
182
+ }
183
+ ]
184
+ };
185
+ }
186
+ const deleteTx = db.transaction((memoryId) => {
187
+ db.prepare("DELETE FROM memories WHERE id = ?").run(memoryId);
188
+ });
189
+ deleteTx(id);
190
+ return {
191
+ content: [
192
+ {
193
+ type: "text",
194
+ text: `Deleted memory ${id}.`
195
+ }
196
+ ]
197
+ };
198
+ } catch (error) {
199
+ const message = error instanceof Error ? error.message : "Unknown error while deleting memory.";
200
+ return {
201
+ isError: true,
202
+ content: [
203
+ {
204
+ type: "text",
205
+ text: `Failed to delete memory: ${message}`
206
+ }
207
+ ]
208
+ };
209
+ }
210
+ }
211
+ );
212
+ }
213
+ var deleteMemoryInputSchema;
214
+ var init_delete = __esm({
215
+ "src/tools/delete.ts"() {
216
+ "use strict";
217
+ init_client();
218
+ deleteMemoryInputSchema = {
219
+ id: z.string().trim().min(1, "id is required")
220
+ };
221
+ }
222
+ });
223
+
224
+ // src/tools/get-repo.ts
225
+ import { z as z2 } from "zod";
226
+ function parseTags(raw) {
227
+ try {
228
+ const parsed = JSON.parse(raw);
229
+ return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+ function formatTypeHeading(type) {
235
+ return type.split("_").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
236
+ }
237
+ function registerGetRepoContextTool(server) {
238
+ server.registerTool(
239
+ "get_repo_context",
240
+ {
241
+ description: "Get recent memories for a repository grouped by memory type.",
242
+ inputSchema: getRepoContextInputSchema
243
+ },
244
+ async ({ repo, limit }) => {
245
+ try {
246
+ const db = getDb();
247
+ const rows = db.prepare(
248
+ `
249
+ SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
250
+ FROM memories
251
+ WHERE repo = ?
252
+ ORDER BY pinned DESC, updated_at DESC
253
+ LIMIT ?
254
+ `
255
+ ).all(repo, limit);
256
+ if (rows.length === 0) {
257
+ return {
258
+ content: [
259
+ {
260
+ type: "text",
261
+ text: `No memories found for ${repo}.`
262
+ }
263
+ ]
264
+ };
265
+ }
266
+ const grouped = /* @__PURE__ */ new Map();
267
+ for (const memory of rows) {
268
+ const tags = parseTags(memory.tags);
269
+ const tagSuffix = tags.length > 0 ? ` [tags: ${tags.join(", ")}]` : "";
270
+ const pinPrefix = memory.pinned ? "\u{1F4CC} Pinned " : "";
271
+ const item = `- (${memory.row_id} | legacy: ${memory.id}) ${pinPrefix}${memory.note}${tagSuffix}`;
272
+ const existing = grouped.get(memory.type) ?? [];
273
+ existing.push(item);
274
+ grouped.set(memory.type, existing);
275
+ }
276
+ const sections = [];
277
+ for (const type of MEMORY_TYPES) {
278
+ const entries = grouped.get(type);
279
+ if (!entries || entries.length === 0) {
280
+ continue;
281
+ }
282
+ sections.push(`${formatTypeHeading(type)}
283
+ ${entries.join("\n")}`);
284
+ }
285
+ return {
286
+ content: [
287
+ {
288
+ type: "text",
289
+ text: `Repository context for ${repo}
290
+ Total memories: ${rows.length}
291
+
292
+ ${sections.join("\n\n")}`
293
+ }
294
+ ]
295
+ };
296
+ } catch (error) {
297
+ const message = error instanceof Error ? error.message : "Unknown error while retrieving repository context.";
298
+ return {
299
+ isError: true,
300
+ content: [
301
+ {
302
+ type: "text",
303
+ text: `Failed to fetch repository context: ${message}`
304
+ }
305
+ ]
306
+ };
307
+ }
308
+ }
309
+ );
310
+ }
311
+ var getRepoContextInputSchema;
312
+ var init_get_repo = __esm({
313
+ "src/tools/get-repo.ts"() {
314
+ "use strict";
315
+ init_client();
316
+ getRepoContextInputSchema = {
317
+ repo: z2.string().trim().min(1, "repo is required"),
318
+ limit: z2.number().int().positive().max(100).default(10)
319
+ };
320
+ }
321
+ });
322
+
323
+ // src/tools/pin.ts
324
+ import { z as z3 } from "zod";
325
+ function setPinnedState(memoryId, pinned) {
326
+ const db = getDb();
327
+ const now = Math.floor(Date.now() / 1e3);
328
+ const updateResult = db.prepare(
329
+ `
330
+ UPDATE memories
331
+ SET pinned = ?, updated_at = ?
332
+ WHERE rowid = ?
333
+ `
334
+ ).run(pinned, now, memoryId);
335
+ if (updateResult.changes === 0) {
336
+ return null;
337
+ }
338
+ return db.prepare(
339
+ `
340
+ SELECT rowid AS row_id, note, pinned
341
+ FROM memories
342
+ WHERE rowid = ?
343
+ `
344
+ ).get(memoryId);
345
+ }
346
+ function registerPinMemoryTool(server) {
347
+ server.registerTool(
348
+ "pin_memory",
349
+ {
350
+ description: "Pin a memory to keep it at the top of repository context.",
351
+ inputSchema: pinInputSchema
352
+ },
353
+ async ({ id }) => {
354
+ try {
355
+ const memory = setPinnedState(id, 1);
356
+ if (!memory) {
357
+ return {
358
+ isError: true,
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: `Memory ${id} not found.`
363
+ }
364
+ ]
365
+ };
366
+ }
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: `Pinned memory ${memory.row_id}: ${memory.note}`
372
+ }
373
+ ]
374
+ };
375
+ } catch (error) {
376
+ const message = error instanceof Error ? error.message : "Unknown error while pinning memory.";
377
+ return {
378
+ isError: true,
379
+ content: [
380
+ {
381
+ type: "text",
382
+ text: `Failed to pin memory: ${message}`
383
+ }
384
+ ]
385
+ };
386
+ }
387
+ }
388
+ );
389
+ }
390
+ function registerUnpinMemoryTool(server) {
391
+ server.registerTool(
392
+ "unpin_memory",
393
+ {
394
+ description: "Unpin a previously pinned memory.",
395
+ inputSchema: pinInputSchema
396
+ },
397
+ async ({ id }) => {
398
+ try {
399
+ const memory = setPinnedState(id, 0);
400
+ if (!memory) {
401
+ return {
402
+ isError: true,
403
+ content: [
404
+ {
405
+ type: "text",
406
+ text: `Memory ${id} not found.`
407
+ }
408
+ ]
409
+ };
410
+ }
411
+ return {
412
+ content: [
413
+ {
414
+ type: "text",
415
+ text: `Unpinned memory ${memory.row_id}.`
416
+ }
417
+ ]
418
+ };
419
+ } catch (error) {
420
+ const message = error instanceof Error ? error.message : "Unknown error while unpinning memory.";
421
+ return {
422
+ isError: true,
423
+ content: [
424
+ {
425
+ type: "text",
426
+ text: `Failed to unpin memory: ${message}`
427
+ }
428
+ ]
429
+ };
430
+ }
431
+ }
432
+ );
433
+ }
434
+ var pinInputSchema;
435
+ var init_pin = __esm({
436
+ "src/tools/pin.ts"() {
437
+ "use strict";
438
+ init_client();
439
+ pinInputSchema = {
440
+ id: z3.number().int().positive()
441
+ };
442
+ }
443
+ });
444
+
445
+ // src/tools/search.ts
446
+ import { z as z4 } from "zod";
447
+ function normalizeFtsQuery(query) {
448
+ const terms = query.trim().split(/\s+/).map((term) => term.replaceAll('"', '""')).filter(Boolean);
449
+ if (terms.length === 0) {
450
+ throw new Error("query must contain searchable text");
451
+ }
452
+ return terms.map((term) => `"${term}"`).join(" AND ");
453
+ }
454
+ function parseTags2(raw) {
455
+ try {
456
+ const parsed = JSON.parse(raw);
457
+ return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
458
+ } catch {
459
+ return [];
460
+ }
461
+ }
462
+ function registerSearchMemoryTool(server) {
463
+ server.registerTool(
464
+ "search_memory",
465
+ {
466
+ description: "Search memories using full-text search with optional repository filtering.",
467
+ inputSchema: searchMemoryInputSchema
468
+ },
469
+ async ({ query, repo, limit }) => {
470
+ try {
471
+ const db = getDb();
472
+ const ftsQuery = normalizeFtsQuery(query);
473
+ const rows = repo ? db.prepare(
474
+ `
475
+ 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
476
+ FROM memories_fts
477
+ JOIN memories AS m ON m.rowid = memories_fts.rowid
478
+ WHERE memories_fts MATCH ? AND m.repo = ?
479
+ ORDER BY rank
480
+ LIMIT ?
481
+ `
482
+ ).all(ftsQuery, repo, limit) : db.prepare(
483
+ `
484
+ 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
485
+ FROM memories_fts
486
+ JOIN memories AS m ON m.rowid = memories_fts.rowid
487
+ WHERE memories_fts MATCH ?
488
+ ORDER BY rank
489
+ LIMIT ?
490
+ `
491
+ ).all(ftsQuery, limit);
492
+ if (rows.length === 0) {
493
+ return {
494
+ content: [
495
+ {
496
+ type: "text",
497
+ text: repo ? `No memories matched "${query}" in ${repo}.` : `No memories matched "${query}".`
498
+ }
499
+ ]
500
+ };
501
+ }
502
+ const formatted = rows.map((row, index) => {
503
+ const tags = parseTags2(row.tags);
504
+ const tagsText = tags.length > 0 ? ` | tags: ${tags.join(", ")}` : "";
505
+ const pinPrefix = row.pinned ? "\u{1F4CC} Pinned " : "";
506
+ return `${index + 1}. [${row.repo}] ${row.type} (${row.row_id} | legacy: ${row.id})
507
+ ${pinPrefix}${row.note}${tagsText}`;
508
+ }).join("\n\n");
509
+ return {
510
+ content: [
511
+ {
512
+ type: "text",
513
+ text: `Search results for "${query}"${repo ? ` in ${repo}` : ""}:
514
+
515
+ ${formatted}`
516
+ }
517
+ ]
518
+ };
519
+ } catch (error) {
520
+ const message = error instanceof Error ? error.message : "Unknown error while searching memory.";
521
+ return {
522
+ isError: true,
523
+ content: [
524
+ {
525
+ type: "text",
526
+ text: `Failed to search memories: ${message}`
527
+ }
528
+ ]
529
+ };
530
+ }
531
+ }
532
+ );
533
+ }
534
+ var searchMemoryInputSchema;
535
+ var init_search = __esm({
536
+ "src/tools/search.ts"() {
537
+ "use strict";
538
+ init_client();
539
+ searchMemoryInputSchema = {
540
+ query: z4.string().trim().min(1, "query is required"),
541
+ repo: z4.string().trim().min(1).optional(),
542
+ limit: z4.number().int().positive().max(50).default(5)
543
+ };
544
+ }
545
+ });
546
+
547
+ // src/tools/store.ts
548
+ import { nanoid } from "nanoid";
549
+ import { z as z5 } from "zod";
550
+ function registerStoreContextTool(server) {
551
+ server.registerTool(
552
+ "store_context",
553
+ {
554
+ description: "Store repository-specific contributor context such as bug fixes, conventions, and decisions.",
555
+ inputSchema: storeContextInputSchema
556
+ },
557
+ async ({ repo, type, note, tags }) => {
558
+ try {
559
+ const db = getDb();
560
+ const now = Math.floor(Date.now() / 1e3);
561
+ const id = nanoid();
562
+ const normalizedTags = Array.from(
563
+ new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))
564
+ );
565
+ db.prepare(
566
+ `
567
+ INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at)
568
+ VALUES (?, ?, ?, ?, ?, ?, ?)
569
+ `
570
+ ).run(id, repo, type, note, JSON.stringify(normalizedTags), now, now);
571
+ const stored = db.prepare(
572
+ `
573
+ SELECT rowid AS row_id, id
574
+ FROM memories
575
+ WHERE id = ?
576
+ `
577
+ ).get(id);
578
+ return {
579
+ content: [
580
+ {
581
+ type: "text",
582
+ text: `Stored memory ${id} (numeric id: ${stored?.row_id ?? "unknown"}) for ${repo} (${type}).`
583
+ }
584
+ ]
585
+ };
586
+ } catch (error) {
587
+ const message = error instanceof Error ? error.message : "Unknown error while storing memory.";
588
+ return {
589
+ isError: true,
590
+ content: [
591
+ {
592
+ type: "text",
593
+ text: `Failed to store memory: ${message}`
594
+ }
595
+ ]
596
+ };
597
+ }
598
+ }
599
+ );
600
+ }
601
+ var storeContextInputSchema;
602
+ var init_store = __esm({
603
+ "src/tools/store.ts"() {
604
+ "use strict";
605
+ init_client();
606
+ storeContextInputSchema = {
607
+ repo: z5.string().trim().min(1, "repo is required"),
608
+ type: z5.enum(MEMORY_TYPES),
609
+ note: z5.string().trim().min(1, "note is required"),
610
+ tags: z5.array(z5.string().trim().min(1)).optional()
611
+ };
612
+ }
613
+ });
614
+
615
+ // src/tools/summarize.ts
616
+ import { z as z6 } from "zod";
617
+ function registerSummarizeRepoContextTool(server) {
618
+ server.registerTool(
619
+ "summarize_repo_context",
620
+ {
621
+ description: "Generate a structured markdown summary of all memories for a repository.",
622
+ inputSchema: summarizeRepoContextInputSchema
623
+ },
624
+ async ({ repo }) => {
625
+ try {
626
+ const db = getDb();
627
+ const rows = db.prepare(
628
+ `
629
+ SELECT rowid AS row_id, type, note, pinned
630
+ FROM memories
631
+ WHERE repo = ?
632
+ ORDER BY pinned DESC, updated_at DESC
633
+ `
634
+ ).all(repo);
635
+ if (rows.length === 0) {
636
+ return {
637
+ content: [
638
+ {
639
+ type: "text",
640
+ text: `Fossel Context Summary: ${repo}
641
+
642
+ No memories found.`
643
+ }
644
+ ]
645
+ };
646
+ }
647
+ const pinnedLines = rows.filter((row) => row.pinned === 1).map((row) => `- (${row.row_id}) ${row.note}`);
648
+ const sections = [`Fossel Context Summary: ${repo}`];
649
+ if (pinnedLines.length > 0) {
650
+ sections.push(`\u{1F4CC} Pinned
651
+ ${pinnedLines.join("\n")}`);
652
+ }
653
+ for (const type of MEMORY_TYPES) {
654
+ const entries = rows.filter((row) => row.type === type).map((row) => `- (${row.row_id}) ${row.note}`);
655
+ if (entries.length === 0) {
656
+ continue;
657
+ }
658
+ sections.push(`${sectionTitleByType[type]}
659
+ ${entries.join("\n")}`);
660
+ }
661
+ return {
662
+ content: [
663
+ {
664
+ type: "text",
665
+ text: sections.join("\n\n")
666
+ }
667
+ ]
668
+ };
669
+ } catch (error) {
670
+ const message = error instanceof Error ? error.message : "Unknown error while summarizing repository context.";
671
+ return {
672
+ isError: true,
673
+ content: [
674
+ {
675
+ type: "text",
676
+ text: `Failed to summarize repository context: ${message}`
677
+ }
678
+ ]
679
+ };
680
+ }
681
+ }
682
+ );
683
+ }
684
+ var summarizeRepoContextInputSchema, sectionTitleByType;
685
+ var init_summarize = __esm({
686
+ "src/tools/summarize.ts"() {
687
+ "use strict";
688
+ init_client();
689
+ summarizeRepoContextInputSchema = {
690
+ repo: z6.string().trim().min(1, "repo is required")
691
+ };
692
+ sectionTitleByType = {
693
+ convention: "Conventions",
694
+ bug_fix: "Bug Fixes",
695
+ reviewer_pattern: "Reviewer Patterns",
696
+ decision: "Decisions",
697
+ issue: "Issues",
698
+ general: "General"
699
+ };
700
+ }
701
+ });
702
+
703
+ // src/tools/update.ts
704
+ import { z as z7 } from "zod";
705
+ function parseTags3(raw) {
706
+ try {
707
+ const parsed = JSON.parse(raw);
708
+ return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
709
+ } catch {
710
+ return [];
711
+ }
712
+ }
713
+ function formatMemory(memory) {
714
+ const tags = parseTags3(memory.tags);
715
+ const tagsLine = tags.length > 0 ? tags.join(", ") : "(none)";
716
+ return [
717
+ `Memory ${memory.row_id} updated successfully.`,
718
+ `id: ${memory.row_id}`,
719
+ `legacy_id: ${memory.id}`,
720
+ `repo: ${memory.repo}`,
721
+ `memory_type: ${memory.type}`,
722
+ `content: ${memory.note}`,
723
+ `tags: ${tagsLine}`,
724
+ `pinned: ${memory.pinned === 1 ? "true" : "false"}`,
725
+ `created_at: ${memory.created_at}`,
726
+ `updated_at: ${memory.updated_at}`
727
+ ].join("\n");
728
+ }
729
+ function registerUpdateMemoryTool(server) {
730
+ server.registerTool(
731
+ "update_memory",
732
+ {
733
+ description: "Update an existing memory by numeric id with partial fields.",
734
+ inputSchema: updateMemoryInputSchema
735
+ },
736
+ async ({ id, content, memory_type }) => {
737
+ try {
738
+ if (!content && !memory_type) {
739
+ return {
740
+ isError: true,
741
+ content: [
742
+ {
743
+ type: "text",
744
+ text: "Provide at least one field to update: content or memory_type."
745
+ }
746
+ ]
747
+ };
748
+ }
749
+ const db = getDb();
750
+ const existing = db.prepare(
751
+ `
752
+ SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
753
+ FROM memories
754
+ WHERE rowid = ?
755
+ `
756
+ ).get(id);
757
+ if (!existing) {
758
+ return {
759
+ isError: true,
760
+ content: [
761
+ {
762
+ type: "text",
763
+ text: `Memory ${id} not found.`
764
+ }
765
+ ]
766
+ };
767
+ }
768
+ const now = Math.floor(Date.now() / 1e3);
769
+ const nextType = memory_type ?? existing.type;
770
+ const nextNote = content ?? existing.note;
771
+ db.prepare(
772
+ `
773
+ UPDATE memories
774
+ SET type = ?, note = ?, updated_at = ?
775
+ WHERE rowid = ?
776
+ `
777
+ ).run(nextType, nextNote, now, id);
778
+ const updated = db.prepare(
779
+ `
780
+ SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
781
+ FROM memories
782
+ WHERE rowid = ?
783
+ `
784
+ ).get(id);
785
+ if (!updated) {
786
+ return {
787
+ isError: true,
788
+ content: [
789
+ {
790
+ type: "text",
791
+ text: `Memory ${id} could not be loaded after update.`
792
+ }
793
+ ]
794
+ };
795
+ }
796
+ return {
797
+ content: [
798
+ {
799
+ type: "text",
800
+ text: formatMemory(updated)
801
+ }
802
+ ]
803
+ };
804
+ } catch (error) {
805
+ const message = error instanceof Error ? error.message : "Unknown error while updating memory.";
806
+ return {
807
+ isError: true,
808
+ content: [
809
+ {
810
+ type: "text",
811
+ text: `Failed to update memory: ${message}`
812
+ }
813
+ ]
814
+ };
815
+ }
816
+ }
817
+ );
818
+ }
819
+ var updateMemoryInputSchema;
820
+ var init_update = __esm({
821
+ "src/tools/update.ts"() {
822
+ "use strict";
823
+ init_client();
824
+ updateMemoryInputSchema = {
825
+ id: z7.number().int().positive(),
826
+ content: z7.string().trim().min(1).optional(),
827
+ memory_type: z7.enum(MEMORY_TYPES).optional()
828
+ };
829
+ }
830
+ });
831
+
832
+ // src/index.ts
833
+ var index_exports = {};
834
+ __export(index_exports, {
835
+ resolveDbPath: () => resolveDbPath,
836
+ startServer: () => startServer
837
+ });
838
+ import { homedir } from "os";
839
+ import { join, resolve } from "path";
840
+ import { fileURLToPath } from "url";
841
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
842
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
843
+ function resolveDbPath() {
844
+ return process.env.FOSSEL_DB_PATH?.trim() || join(homedir(), ".fossel", "memory.db");
845
+ }
846
+ async function startServer() {
847
+ const dbPath = resolveDbPath();
848
+ initDb(dbPath);
849
+ const server = new McpServer({
850
+ name: "fossel",
851
+ version: "1.0.0"
852
+ });
853
+ registerStoreContextTool(server);
854
+ registerGetRepoContextTool(server);
855
+ registerSearchMemoryTool(server);
856
+ registerDeleteMemoryTool(server);
857
+ registerUpdateMemoryTool(server);
858
+ registerPinMemoryTool(server);
859
+ registerUnpinMemoryTool(server);
860
+ registerSummarizeRepoContextTool(server);
861
+ const transport = new StdioServerTransport();
862
+ await server.connect(transport);
863
+ }
864
+ var entryPath, currentPath;
865
+ var init_index = __esm({
866
+ "src/index.ts"() {
867
+ "use strict";
868
+ init_client();
869
+ init_delete();
870
+ init_get_repo();
871
+ init_pin();
872
+ init_search();
873
+ init_store();
874
+ init_summarize();
875
+ init_update();
876
+ entryPath = process.argv[1];
877
+ currentPath = fileURLToPath(import.meta.url);
878
+ if (entryPath && currentPath === resolve(entryPath)) {
879
+ startServer().catch((error) => {
880
+ const message = error instanceof Error ? error.message : String(error);
881
+ console.error(`Fossel server failed to start: ${message}`);
882
+ process.exit(1);
883
+ });
884
+ }
885
+ }
886
+ });
887
+
888
+ // src/cli.ts
889
+ init_client();
890
+ import { homedir as homedir2 } from "os";
891
+ import { basename, join as join2 } from "path";
892
+ import { spawnSync } from "child_process";
893
+ import { nanoid as nanoid2 } from "nanoid";
894
+ var DEFAULT_DB_PATH = join2(homedir2(), ".fossel", "memory.db");
895
+ var INIT_MEMORY_TEXT = "Fossel is active for this repo. Use store_context to save context.";
896
+ function resolveDbPath2() {
897
+ return process.env.FOSSEL_DB_PATH?.trim() || DEFAULT_DB_PATH;
898
+ }
899
+ function detectRepoFromRemote(cwd) {
900
+ const result = spawnSync("git", ["remote", "get-url", "origin"], {
901
+ cwd,
902
+ encoding: "utf8"
903
+ });
904
+ if (result.status !== 0) {
905
+ return null;
906
+ }
907
+ const remote = result.stdout.trim();
908
+ if (!remote) {
909
+ return null;
910
+ }
911
+ const normalized = remote.replace(/\\/g, "/").replace(/\/+$/, "");
912
+ const lastSegment = normalized.split("/").at(-1);
913
+ if (!lastSegment) {
914
+ return null;
915
+ }
916
+ return lastSegment.replace(/\.git$/i, "");
917
+ }
918
+ function detectRepoName(cwd) {
919
+ return detectRepoFromRemote(cwd) ?? basename(cwd);
920
+ }
921
+ function ensureSampleMemory(repo) {
922
+ const db = getDb();
923
+ const existing = db.prepare(
924
+ `
925
+ SELECT rowid AS row_id
926
+ FROM memories
927
+ WHERE repo = ? AND type = 'convention' AND note = ?
928
+ LIMIT 1
929
+ `
930
+ ).get(repo, INIT_MEMORY_TEXT);
931
+ if (existing) {
932
+ return;
933
+ }
934
+ const now = Math.floor(Date.now() / 1e3);
935
+ db.prepare(
936
+ `
937
+ INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned)
938
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
939
+ `
940
+ ).run(nanoid2(), repo, "convention", INIT_MEMORY_TEXT, "[]", now, now, 0);
941
+ }
942
+ function formatCursorConfig() {
943
+ return JSON.stringify(
944
+ {
945
+ mcpServers: {
946
+ fossel: {
947
+ command: "npx",
948
+ args: ["-y", "fossel"]
949
+ }
950
+ }
951
+ },
952
+ null,
953
+ 2
954
+ );
955
+ }
956
+ function formatClaudeDesktopConfig() {
957
+ return JSON.stringify(
958
+ {
959
+ mcpServers: {
960
+ fossel: {
961
+ command: "npx",
962
+ args: ["-y", "fossel"]
963
+ }
964
+ }
965
+ },
966
+ null,
967
+ 2
968
+ );
969
+ }
970
+ function printInitOutput(repo, dbPath) {
971
+ const db = getDb();
972
+ const countRow = db.prepare("SELECT COUNT(*) AS count FROM memories").get();
973
+ console.log("Fossel remembers project context locally for each repository.");
974
+ console.log("Store conventions, fixes, and decisions once; retrieve them when needed.");
975
+ console.log("Everything stays in your local SQLite database.\n");
976
+ console.log(`Detected repository: ${repo}
977
+ `);
978
+ console.log("Cursor MCP config (~/.cursor/mcp.json):");
979
+ console.log(formatCursorConfig());
980
+ console.log("");
981
+ console.log("Claude Desktop MCP config:");
982
+ console.log(formatClaudeDesktopConfig());
983
+ console.log("");
984
+ console.log(`DB Path: ${dbPath}`);
985
+ console.log(`Total memories: ${countRow.count}`);
986
+ console.log("");
987
+ console.log("Quick commands");
988
+ console.log("Command Description");
989
+ console.log("----------------------- --------------------------------------------");
990
+ console.log("npx -y fossel Start Fossel MCP server over stdio");
991
+ console.log("npx -y fossel init Initialize Fossel for current repository");
992
+ console.log("store_context Save context memory");
993
+ console.log("get_repo_context Retrieve recent repo memories");
994
+ console.log("summarize_repo_context Generate markdown context summary");
995
+ }
996
+ async function main() {
997
+ const command = process.argv[2];
998
+ if (!command) {
999
+ const { startServer: startServer2 } = await Promise.resolve().then(() => (init_index(), index_exports));
1000
+ await startServer2();
1001
+ return;
1002
+ }
1003
+ if (command === "init") {
1004
+ const dbPath = resolveDbPath2();
1005
+ initDb(dbPath);
1006
+ const repo = detectRepoName(process.cwd());
1007
+ ensureSampleMemory(repo);
1008
+ printInitOutput(repo, dbPath);
1009
+ closeDb();
1010
+ return;
1011
+ }
1012
+ console.error(`Unknown command: ${command}`);
1013
+ console.error("Usage: fossel [init]");
1014
+ process.exit(1);
1015
+ }
1016
+ main().catch((error) => {
1017
+ const message = error instanceof Error ? error.message : String(error);
1018
+ console.error(`Fossel command failed: ${message}`);
1019
+ process.exit(1);
1020
+ });