fossel 1.1.0 → 1.1.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/README.md +17 -4
- package/dist/cli.js +483 -167
- package/dist/index.js +187 -64
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -215,6 +215,96 @@ var init_client = __esm({
|
|
|
215
215
|
}
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
// src/lib/dedupe.ts
|
|
219
|
+
function normalizeText(text) {
|
|
220
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
221
|
+
}
|
|
222
|
+
function tokenize(text) {
|
|
223
|
+
return normalizeText(text).split(" ").filter((token) => token.length >= 2);
|
|
224
|
+
}
|
|
225
|
+
function trigrams(text) {
|
|
226
|
+
const padded = ` ${text} `;
|
|
227
|
+
const grams = /* @__PURE__ */ new Set();
|
|
228
|
+
for (let i = 0; i < padded.length - 2; i += 1) {
|
|
229
|
+
grams.add(padded.slice(i, i + 3));
|
|
230
|
+
}
|
|
231
|
+
return grams;
|
|
232
|
+
}
|
|
233
|
+
function jaccard(a, b) {
|
|
234
|
+
if (a.size === 0 && b.size === 0) {
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
let intersection = 0;
|
|
238
|
+
for (const value of a) {
|
|
239
|
+
if (b.has(value)) {
|
|
240
|
+
intersection += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const union = a.size + b.size - intersection;
|
|
244
|
+
return union === 0 ? 0 : intersection / union;
|
|
245
|
+
}
|
|
246
|
+
function similarity(a, b) {
|
|
247
|
+
const normalizedA = normalizeText(a);
|
|
248
|
+
const normalizedB = normalizeText(b);
|
|
249
|
+
if (!normalizedA && !normalizedB) {
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
if (!normalizedA || !normalizedB) {
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
if (normalizedA === normalizedB) {
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
const wordScore = jaccard(new Set(tokenize(normalizedA)), new Set(tokenize(normalizedB)));
|
|
259
|
+
const triScore = jaccard(trigrams(normalizedA), trigrams(normalizedB));
|
|
260
|
+
return wordScore * 0.55 + triScore * 0.45;
|
|
261
|
+
}
|
|
262
|
+
function findDuplicate(db, repo, note, options = {}) {
|
|
263
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
264
|
+
const limit = options.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT;
|
|
265
|
+
const normalized = normalizeText(note);
|
|
266
|
+
if (!normalized) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const exact = db.prepare(
|
|
270
|
+
`
|
|
271
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
272
|
+
FROM memories
|
|
273
|
+
WHERE repo = ? AND note_normalized = ?
|
|
274
|
+
ORDER BY updated_at DESC
|
|
275
|
+
LIMIT 1
|
|
276
|
+
`
|
|
277
|
+
).get(repo, normalized);
|
|
278
|
+
if (exact) {
|
|
279
|
+
return { memory: exact, similarity: 1 };
|
|
280
|
+
}
|
|
281
|
+
const candidates = db.prepare(
|
|
282
|
+
`
|
|
283
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
284
|
+
FROM memories
|
|
285
|
+
WHERE repo = ?
|
|
286
|
+
ORDER BY updated_at DESC
|
|
287
|
+
LIMIT ?
|
|
288
|
+
`
|
|
289
|
+
).all(repo, limit);
|
|
290
|
+
let best = null;
|
|
291
|
+
for (const candidate of candidates) {
|
|
292
|
+
const score = similarity(note, candidate.note);
|
|
293
|
+
if (score >= threshold && (!best || score > best.similarity)) {
|
|
294
|
+
best = { memory: candidate, similarity: score };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return best;
|
|
298
|
+
}
|
|
299
|
+
var DEFAULT_THRESHOLD, DEFAULT_CANDIDATE_LIMIT;
|
|
300
|
+
var init_dedupe = __esm({
|
|
301
|
+
"src/lib/dedupe.ts"() {
|
|
302
|
+
"use strict";
|
|
303
|
+
DEFAULT_THRESHOLD = 0.82;
|
|
304
|
+
DEFAULT_CANDIDATE_LIMIT = 200;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
218
308
|
// src/lib/repo.ts
|
|
219
309
|
import { spawnSync } from "child_process";
|
|
220
310
|
import { basename } from "path";
|
|
@@ -341,7 +431,7 @@ function resolveRepoArg(input, cwd, db) {
|
|
|
341
431
|
}
|
|
342
432
|
function mergeRepoKeys(db, from, to) {
|
|
343
433
|
if (from === to) {
|
|
344
|
-
return { movedAliases: 0, movedMemories: 0 };
|
|
434
|
+
return { movedAliases: 0, movedMemories: 0, rewrittenNotes: 0 };
|
|
345
435
|
}
|
|
346
436
|
const tx = db.transaction(() => {
|
|
347
437
|
const aliasResult = db.prepare("UPDATE repo_aliases SET canonical = ? WHERE canonical = ?").run(to, from);
|
|
@@ -358,15 +448,79 @@ function mergeRepoKeys(db, from, to) {
|
|
|
358
448
|
movedMemories += result.changes;
|
|
359
449
|
}
|
|
360
450
|
movedMemories += updateMemories.run(to, from).changes;
|
|
451
|
+
const rewrittenNotes = rewriteStaleRepoMentions(db, from, to);
|
|
361
452
|
upsertAlias(db, from, to);
|
|
362
453
|
upsertAlias(db, to, to);
|
|
363
454
|
return {
|
|
364
455
|
movedAliases: aliasResult.changes,
|
|
365
|
-
movedMemories
|
|
456
|
+
movedMemories,
|
|
457
|
+
rewrittenNotes
|
|
366
458
|
};
|
|
367
459
|
});
|
|
368
460
|
return tx();
|
|
369
461
|
}
|
|
462
|
+
function tokenBoundaryReplace(text, from, to) {
|
|
463
|
+
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
464
|
+
const pattern = new RegExp(`(^|[^\\w/-])(${escaped})(?=$|[^\\w/-])`, "g");
|
|
465
|
+
return text.replace(pattern, (_match, prefix) => `${prefix}${to}`);
|
|
466
|
+
}
|
|
467
|
+
function rewriteStaleRepoMentions(db, from, to) {
|
|
468
|
+
const candidates = db.prepare(
|
|
469
|
+
`
|
|
470
|
+
SELECT rowid AS row_id, note, metadata_json
|
|
471
|
+
FROM memories
|
|
472
|
+
WHERE note LIKE ?
|
|
473
|
+
`
|
|
474
|
+
).all(`%${from}%`);
|
|
475
|
+
if (candidates.length === 0) {
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
const update = db.prepare(
|
|
479
|
+
`
|
|
480
|
+
UPDATE memories
|
|
481
|
+
SET note = ?, note_normalized = ?, metadata_json = ?, updated_at = ?
|
|
482
|
+
WHERE rowid = ?
|
|
483
|
+
`
|
|
484
|
+
);
|
|
485
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
486
|
+
let rewritten = 0;
|
|
487
|
+
for (const row of candidates) {
|
|
488
|
+
const next = tokenBoundaryReplace(row.note, from, to);
|
|
489
|
+
if (next === row.note) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const normalized = next.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
493
|
+
let metadata;
|
|
494
|
+
try {
|
|
495
|
+
const parsed = JSON.parse(row.metadata_json);
|
|
496
|
+
metadata = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
497
|
+
} catch {
|
|
498
|
+
metadata = {};
|
|
499
|
+
}
|
|
500
|
+
metadata.changelog = metadata.changelog ?? [];
|
|
501
|
+
metadata.changelog.push({
|
|
502
|
+
at: now,
|
|
503
|
+
action: "alias_rewrite",
|
|
504
|
+
previous_note: row.note,
|
|
505
|
+
rewrote_alias: from
|
|
506
|
+
});
|
|
507
|
+
update.run(next, normalized, JSON.stringify(metadata), now, row.row_id);
|
|
508
|
+
rewritten += 1;
|
|
509
|
+
}
|
|
510
|
+
return rewritten;
|
|
511
|
+
}
|
|
512
|
+
function findMemoriesMentioningAlias(db, alias, canonical) {
|
|
513
|
+
const rows = db.prepare(
|
|
514
|
+
`
|
|
515
|
+
SELECT rowid AS row_id, repo, note
|
|
516
|
+
FROM memories
|
|
517
|
+
WHERE repo = ? AND note LIKE ?
|
|
518
|
+
`
|
|
519
|
+
).all(canonical, `%${alias}%`);
|
|
520
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
521
|
+
const pattern = new RegExp(`(^|[^\\w/-])${escaped}(?=$|[^\\w/-])`);
|
|
522
|
+
return rows.filter((row) => pattern.test(row.note));
|
|
523
|
+
}
|
|
370
524
|
var REMOTE_PATTERNS;
|
|
371
525
|
var init_repo = __esm({
|
|
372
526
|
"src/lib/repo.ts"() {
|
|
@@ -393,6 +547,9 @@ function parseTags(raw) {
|
|
|
393
547
|
return [];
|
|
394
548
|
}
|
|
395
549
|
}
|
|
550
|
+
function normalizeNoteForReadDedupe(text) {
|
|
551
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
552
|
+
}
|
|
396
553
|
function buildFtsQuery(query) {
|
|
397
554
|
const terms = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter((term) => term.length > 0);
|
|
398
555
|
if (terms.length === 0) {
|
|
@@ -403,11 +560,19 @@ function buildFtsQuery(query) {
|
|
|
403
560
|
function fetchRepoContext(db, repo, limit, query) {
|
|
404
561
|
const rows = [];
|
|
405
562
|
const seen = /* @__PURE__ */ new Set();
|
|
563
|
+
const seenNormalized = /* @__PURE__ */ new Set();
|
|
406
564
|
const push = (memory, source, rank) => {
|
|
407
565
|
if (seen.has(memory.row_id)) {
|
|
408
566
|
return;
|
|
409
567
|
}
|
|
568
|
+
const normalized = normalizeNoteForReadDedupe(memory.note);
|
|
569
|
+
if (normalized && seenNormalized.has(normalized)) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
410
572
|
seen.add(memory.row_id);
|
|
573
|
+
if (normalized) {
|
|
574
|
+
seenNormalized.add(normalized);
|
|
575
|
+
}
|
|
411
576
|
rows.push({ ...memory, source, rank });
|
|
412
577
|
};
|
|
413
578
|
const pinned = db.prepare(
|
|
@@ -533,93 +698,17 @@ var init_context = __esm({
|
|
|
533
698
|
}
|
|
534
699
|
});
|
|
535
700
|
|
|
536
|
-
// src/lib/
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
return normalizeText(text).split(" ").filter((token) => token.length >= 2);
|
|
542
|
-
}
|
|
543
|
-
function trigrams(text) {
|
|
544
|
-
const padded = ` ${text} `;
|
|
545
|
-
const grams = /* @__PURE__ */ new Set();
|
|
546
|
-
for (let i = 0; i < padded.length - 2; i += 1) {
|
|
547
|
-
grams.add(padded.slice(i, i + 3));
|
|
548
|
-
}
|
|
549
|
-
return grams;
|
|
550
|
-
}
|
|
551
|
-
function jaccard(a, b) {
|
|
552
|
-
if (a.size === 0 && b.size === 0) {
|
|
553
|
-
return 1;
|
|
554
|
-
}
|
|
555
|
-
let intersection = 0;
|
|
556
|
-
for (const value of a) {
|
|
557
|
-
if (b.has(value)) {
|
|
558
|
-
intersection += 1;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
const union = a.size + b.size - intersection;
|
|
562
|
-
return union === 0 ? 0 : intersection / union;
|
|
563
|
-
}
|
|
564
|
-
function similarity(a, b) {
|
|
565
|
-
const normalizedA = normalizeText(a);
|
|
566
|
-
const normalizedB = normalizeText(b);
|
|
567
|
-
if (!normalizedA && !normalizedB) {
|
|
568
|
-
return 1;
|
|
569
|
-
}
|
|
570
|
-
if (!normalizedA || !normalizedB) {
|
|
571
|
-
return 0;
|
|
572
|
-
}
|
|
573
|
-
if (normalizedA === normalizedB) {
|
|
574
|
-
return 1;
|
|
575
|
-
}
|
|
576
|
-
const wordScore = jaccard(new Set(tokenize(normalizedA)), new Set(tokenize(normalizedB)));
|
|
577
|
-
const triScore = jaccard(trigrams(normalizedA), trigrams(normalizedB));
|
|
578
|
-
return wordScore * 0.55 + triScore * 0.45;
|
|
579
|
-
}
|
|
580
|
-
function findDuplicate(db, repo, note, options = {}) {
|
|
581
|
-
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
582
|
-
const limit = options.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT;
|
|
583
|
-
const normalized = normalizeText(note);
|
|
584
|
-
if (!normalized) {
|
|
585
|
-
return null;
|
|
701
|
+
// src/lib/workspace.ts
|
|
702
|
+
function getWorkspaceRoot() {
|
|
703
|
+
const fromEnv = process.env.FOSSEL_WORKSPACE?.trim();
|
|
704
|
+
if (fromEnv) {
|
|
705
|
+
return fromEnv;
|
|
586
706
|
}
|
|
587
|
-
|
|
588
|
-
`
|
|
589
|
-
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
590
|
-
FROM memories
|
|
591
|
-
WHERE repo = ? AND note_normalized = ?
|
|
592
|
-
ORDER BY updated_at DESC
|
|
593
|
-
LIMIT 1
|
|
594
|
-
`
|
|
595
|
-
).get(repo, normalized);
|
|
596
|
-
if (exact) {
|
|
597
|
-
return { memory: exact, similarity: 1 };
|
|
598
|
-
}
|
|
599
|
-
const candidates = db.prepare(
|
|
600
|
-
`
|
|
601
|
-
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
602
|
-
FROM memories
|
|
603
|
-
WHERE repo = ?
|
|
604
|
-
ORDER BY updated_at DESC
|
|
605
|
-
LIMIT ?
|
|
606
|
-
`
|
|
607
|
-
).all(repo, limit);
|
|
608
|
-
let best = null;
|
|
609
|
-
for (const candidate of candidates) {
|
|
610
|
-
const score = similarity(note, candidate.note);
|
|
611
|
-
if (score >= threshold && (!best || score > best.similarity)) {
|
|
612
|
-
best = { memory: candidate, similarity: score };
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return best;
|
|
707
|
+
return process.cwd();
|
|
616
708
|
}
|
|
617
|
-
var
|
|
618
|
-
|
|
619
|
-
"src/lib/dedupe.ts"() {
|
|
709
|
+
var init_workspace = __esm({
|
|
710
|
+
"src/lib/workspace.ts"() {
|
|
620
711
|
"use strict";
|
|
621
|
-
DEFAULT_THRESHOLD = 0.82;
|
|
622
|
-
DEFAULT_CANDIDATE_LIMIT = 200;
|
|
623
712
|
}
|
|
624
713
|
});
|
|
625
714
|
|
|
@@ -666,7 +755,7 @@ function registerDedupeRepoTool(server) {
|
|
|
666
755
|
async ({ repo, threshold, apply }) => {
|
|
667
756
|
try {
|
|
668
757
|
const db = getDb();
|
|
669
|
-
const resolved = resolveRepoArg(repo,
|
|
758
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
670
759
|
const rows = db.prepare(
|
|
671
760
|
`
|
|
672
761
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json
|
|
@@ -798,6 +887,7 @@ var init_dedupe_repo = __esm({
|
|
|
798
887
|
init_client();
|
|
799
888
|
init_dedupe();
|
|
800
889
|
init_repo();
|
|
890
|
+
init_workspace();
|
|
801
891
|
dedupeRepoInputSchema = {
|
|
802
892
|
repo: z.string().trim().min(1).optional(),
|
|
803
893
|
threshold: z.number().min(0.5).max(1).default(0.85),
|
|
@@ -806,20 +896,55 @@ var init_dedupe_repo = __esm({
|
|
|
806
896
|
}
|
|
807
897
|
});
|
|
808
898
|
|
|
899
|
+
// src/lib/memory.ts
|
|
900
|
+
function findMemoryByAnyId(db, input) {
|
|
901
|
+
const numeric = typeof input === "number" ? input : Number(input);
|
|
902
|
+
const isNumericId = Number.isInteger(numeric) && numeric > 0 && String(numeric) === String(input).trim();
|
|
903
|
+
if (isNumericId) {
|
|
904
|
+
const row = db.prepare(
|
|
905
|
+
`
|
|
906
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
907
|
+
FROM memories
|
|
908
|
+
WHERE rowid = ?
|
|
909
|
+
`
|
|
910
|
+
).get(numeric);
|
|
911
|
+
if (row) {
|
|
912
|
+
return row;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const stringInput = String(input).trim();
|
|
916
|
+
if (stringInput.length === 0) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const stringRow = db.prepare(
|
|
920
|
+
`
|
|
921
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
922
|
+
FROM memories
|
|
923
|
+
WHERE id = ?
|
|
924
|
+
`
|
|
925
|
+
).get(stringInput);
|
|
926
|
+
return stringRow ?? null;
|
|
927
|
+
}
|
|
928
|
+
var init_memory = __esm({
|
|
929
|
+
"src/lib/memory.ts"() {
|
|
930
|
+
"use strict";
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
|
|
809
934
|
// src/tools/delete.ts
|
|
810
935
|
import { z as z2 } from "zod";
|
|
811
936
|
function registerDeleteMemoryTool(server) {
|
|
812
937
|
server.registerTool(
|
|
813
938
|
"delete_memory",
|
|
814
939
|
{
|
|
815
|
-
description: "Delete a memory from storage by id.",
|
|
940
|
+
description: "Delete a memory from storage by id. Accepts either the numeric row id or the legacy string id.",
|
|
816
941
|
inputSchema: deleteMemoryInputSchema
|
|
817
942
|
},
|
|
818
943
|
async ({ id }) => {
|
|
819
944
|
try {
|
|
820
945
|
const db = getDb();
|
|
821
|
-
const
|
|
822
|
-
if (!
|
|
946
|
+
const memory = findMemoryByAnyId(db, id);
|
|
947
|
+
if (!memory) {
|
|
823
948
|
return {
|
|
824
949
|
isError: true,
|
|
825
950
|
content: [
|
|
@@ -830,15 +955,15 @@ function registerDeleteMemoryTool(server) {
|
|
|
830
955
|
]
|
|
831
956
|
};
|
|
832
957
|
}
|
|
833
|
-
const deleteTx = db.transaction((
|
|
834
|
-
db.prepare("DELETE FROM memories WHERE
|
|
958
|
+
const deleteTx = db.transaction((rowId) => {
|
|
959
|
+
db.prepare("DELETE FROM memories WHERE rowid = ?").run(rowId);
|
|
835
960
|
});
|
|
836
|
-
deleteTx(
|
|
961
|
+
deleteTx(memory.row_id);
|
|
837
962
|
return {
|
|
838
963
|
content: [
|
|
839
964
|
{
|
|
840
965
|
type: "text",
|
|
841
|
-
text: `Deleted memory ${id}.`
|
|
966
|
+
text: `Deleted memory ${memory.row_id} (legacy: ${memory.id}).`
|
|
842
967
|
}
|
|
843
968
|
]
|
|
844
969
|
};
|
|
@@ -862,8 +987,12 @@ var init_delete = __esm({
|
|
|
862
987
|
"src/tools/delete.ts"() {
|
|
863
988
|
"use strict";
|
|
864
989
|
init_client();
|
|
990
|
+
init_memory();
|
|
865
991
|
deleteMemoryInputSchema = {
|
|
866
|
-
|
|
992
|
+
// Accept either the numeric row_id or the legacy nanoid string. Tools used
|
|
993
|
+
// to disagree about which form to take; this unifies them so callers can
|
|
994
|
+
// paste whichever id they have in front of them.
|
|
995
|
+
id: z2.union([z2.number().int().positive(), z2.string().trim().min(1)])
|
|
867
996
|
};
|
|
868
997
|
}
|
|
869
998
|
});
|
|
@@ -880,7 +1009,7 @@ function registerGetContextTool(server) {
|
|
|
880
1009
|
async ({ repo, query, limit, format }) => {
|
|
881
1010
|
try {
|
|
882
1011
|
const db = getDb();
|
|
883
|
-
const resolved = resolveRepoArg(repo,
|
|
1012
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
884
1013
|
const rows = fetchRepoContext(db, resolved.canonical, limit, query);
|
|
885
1014
|
const text = formatContext(rows, {
|
|
886
1015
|
repo: resolved.canonical,
|
|
@@ -917,6 +1046,7 @@ var init_get_context = __esm({
|
|
|
917
1046
|
init_client();
|
|
918
1047
|
init_context();
|
|
919
1048
|
init_repo();
|
|
1049
|
+
init_workspace();
|
|
920
1050
|
getContextInputSchema = {
|
|
921
1051
|
repo: z3.string().trim().min(1).optional(),
|
|
922
1052
|
query: z3.string().trim().min(1).optional(),
|
|
@@ -949,7 +1079,7 @@ function registerGetRepoContextTool(server) {
|
|
|
949
1079
|
async ({ repo, limit }) => {
|
|
950
1080
|
try {
|
|
951
1081
|
const db = getDb();
|
|
952
|
-
const resolved = resolveRepoArg(repo,
|
|
1082
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
953
1083
|
const rows = db.prepare(
|
|
954
1084
|
`
|
|
955
1085
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
@@ -1020,6 +1150,7 @@ var init_get_repo = __esm({
|
|
|
1020
1150
|
"use strict";
|
|
1021
1151
|
init_client();
|
|
1022
1152
|
init_repo();
|
|
1153
|
+
init_workspace();
|
|
1023
1154
|
getRepoContextInputSchema = {
|
|
1024
1155
|
repo: z4.string().trim().min(1).optional(),
|
|
1025
1156
|
limit: z4.number().int().positive().max(100).default(10)
|
|
@@ -1029,7 +1160,7 @@ var init_get_repo = __esm({
|
|
|
1029
1160
|
|
|
1030
1161
|
// src/tools/pin.ts
|
|
1031
1162
|
import { z as z5 } from "zod";
|
|
1032
|
-
function setPinnedState(
|
|
1163
|
+
function setPinnedState(rowId, pinned) {
|
|
1033
1164
|
const db = getDb();
|
|
1034
1165
|
const now = Math.floor(Date.now() / 1e3);
|
|
1035
1166
|
const updateResult = db.prepare(
|
|
@@ -1038,7 +1169,7 @@ function setPinnedState(memoryId, pinned) {
|
|
|
1038
1169
|
SET pinned = ?, updated_at = ?
|
|
1039
1170
|
WHERE rowid = ?
|
|
1040
1171
|
`
|
|
1041
|
-
).run(pinned, now,
|
|
1172
|
+
).run(pinned, now, rowId);
|
|
1042
1173
|
if (updateResult.changes === 0) {
|
|
1043
1174
|
return null;
|
|
1044
1175
|
}
|
|
@@ -1048,7 +1179,7 @@ function setPinnedState(memoryId, pinned) {
|
|
|
1048
1179
|
FROM memories
|
|
1049
1180
|
WHERE rowid = ?
|
|
1050
1181
|
`
|
|
1051
|
-
).get(
|
|
1182
|
+
).get(rowId);
|
|
1052
1183
|
}
|
|
1053
1184
|
function registerPinMemoryTool(server) {
|
|
1054
1185
|
server.registerTool(
|
|
@@ -1059,7 +1190,20 @@ function registerPinMemoryTool(server) {
|
|
|
1059
1190
|
},
|
|
1060
1191
|
async ({ id }) => {
|
|
1061
1192
|
try {
|
|
1062
|
-
const
|
|
1193
|
+
const db = getDb();
|
|
1194
|
+
const target = findMemoryByAnyId(db, id);
|
|
1195
|
+
if (!target) {
|
|
1196
|
+
return {
|
|
1197
|
+
isError: true,
|
|
1198
|
+
content: [
|
|
1199
|
+
{
|
|
1200
|
+
type: "text",
|
|
1201
|
+
text: `Memory ${id} not found.`
|
|
1202
|
+
}
|
|
1203
|
+
]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
const memory = setPinnedState(target.row_id, 1);
|
|
1063
1207
|
if (!memory) {
|
|
1064
1208
|
return {
|
|
1065
1209
|
isError: true,
|
|
@@ -1103,7 +1247,20 @@ function registerUnpinMemoryTool(server) {
|
|
|
1103
1247
|
},
|
|
1104
1248
|
async ({ id }) => {
|
|
1105
1249
|
try {
|
|
1106
|
-
const
|
|
1250
|
+
const db = getDb();
|
|
1251
|
+
const target = findMemoryByAnyId(db, id);
|
|
1252
|
+
if (!target) {
|
|
1253
|
+
return {
|
|
1254
|
+
isError: true,
|
|
1255
|
+
content: [
|
|
1256
|
+
{
|
|
1257
|
+
type: "text",
|
|
1258
|
+
text: `Memory ${id} not found.`
|
|
1259
|
+
}
|
|
1260
|
+
]
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
const memory = setPinnedState(target.row_id, 0);
|
|
1107
1264
|
if (!memory) {
|
|
1108
1265
|
return {
|
|
1109
1266
|
isError: true,
|
|
@@ -1143,8 +1300,10 @@ var init_pin = __esm({
|
|
|
1143
1300
|
"src/tools/pin.ts"() {
|
|
1144
1301
|
"use strict";
|
|
1145
1302
|
init_client();
|
|
1303
|
+
init_memory();
|
|
1146
1304
|
pinInputSchema = {
|
|
1147
|
-
id
|
|
1305
|
+
// Accept numeric row_id or legacy string id for parity with the other tools.
|
|
1306
|
+
id: z5.union([z5.number().int().positive(), z5.string().trim().min(1)])
|
|
1148
1307
|
};
|
|
1149
1308
|
}
|
|
1150
1309
|
});
|
|
@@ -1528,7 +1687,7 @@ function registerRememberTool(server) {
|
|
|
1528
1687
|
async ({ note, repo, type, tags }) => {
|
|
1529
1688
|
try {
|
|
1530
1689
|
const db = getDb();
|
|
1531
|
-
const resolved = resolveRepoArg(repo,
|
|
1690
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1532
1691
|
const inferred = inferMemoryFromNote(note);
|
|
1533
1692
|
const finalType = type ?? inferred.type;
|
|
1534
1693
|
const finalTags = mergeTagLists2(tags, inferred.tags).slice(0, 5);
|
|
@@ -1637,6 +1796,7 @@ var init_remember = __esm({
|
|
|
1637
1796
|
init_dedupe();
|
|
1638
1797
|
init_inference();
|
|
1639
1798
|
init_repo();
|
|
1799
|
+
init_workspace();
|
|
1640
1800
|
rememberInputSchema = {
|
|
1641
1801
|
note: z6.string().trim().min(1, "note is required"),
|
|
1642
1802
|
repo: z6.string().trim().min(1).optional(),
|
|
@@ -1658,7 +1818,7 @@ function registerResolveRepoTool(server) {
|
|
|
1658
1818
|
async ({ cwd }) => {
|
|
1659
1819
|
try {
|
|
1660
1820
|
const db = getDb();
|
|
1661
|
-
const target = cwd?.trim() ||
|
|
1821
|
+
const target = cwd?.trim() || getWorkspaceRoot();
|
|
1662
1822
|
const resolved = resolveRepo(target, db);
|
|
1663
1823
|
const payload = {
|
|
1664
1824
|
canonical: resolved.canonical,
|
|
@@ -1696,6 +1856,7 @@ var init_resolve_repo = __esm({
|
|
|
1696
1856
|
"use strict";
|
|
1697
1857
|
init_client();
|
|
1698
1858
|
init_repo();
|
|
1859
|
+
init_workspace();
|
|
1699
1860
|
resolveRepoInputSchema = {
|
|
1700
1861
|
cwd: z7.string().trim().min(1).optional()
|
|
1701
1862
|
};
|
|
@@ -1704,12 +1865,20 @@ var init_resolve_repo = __esm({
|
|
|
1704
1865
|
|
|
1705
1866
|
// src/tools/search.ts
|
|
1706
1867
|
import { z as z8 } from "zod";
|
|
1707
|
-
function
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1868
|
+
function tokenizeQuery(query) {
|
|
1869
|
+
return query.toLowerCase().replace(/["()]/g, " ").split(/[\s/_\-.,;:!?]+/).map((token) => token.replace(/[^a-z0-9*]/g, "")).filter((token) => token.length >= 2);
|
|
1870
|
+
}
|
|
1871
|
+
function buildFtsQuery2(tokens) {
|
|
1872
|
+
if (tokens.length === 0) {
|
|
1873
|
+
return null;
|
|
1711
1874
|
}
|
|
1712
|
-
return
|
|
1875
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" AND ");
|
|
1876
|
+
}
|
|
1877
|
+
function buildFtsQueryOr(tokens) {
|
|
1878
|
+
if (tokens.length === 0) {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" OR ");
|
|
1713
1882
|
}
|
|
1714
1883
|
function parseTags4(raw) {
|
|
1715
1884
|
try {
|
|
@@ -1719,37 +1888,66 @@ function parseTags4(raw) {
|
|
|
1719
1888
|
return [];
|
|
1720
1889
|
}
|
|
1721
1890
|
}
|
|
1891
|
+
function runFts(ftsQuery, resolvedRepo, limit) {
|
|
1892
|
+
const db = getDb();
|
|
1893
|
+
try {
|
|
1894
|
+
if (resolvedRepo) {
|
|
1895
|
+
return db.prepare(
|
|
1896
|
+
`
|
|
1897
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
1898
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
1899
|
+
FROM memories_fts
|
|
1900
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
1901
|
+
WHERE memories_fts MATCH ? AND m.repo = ?
|
|
1902
|
+
ORDER BY rank
|
|
1903
|
+
LIMIT ?
|
|
1904
|
+
`
|
|
1905
|
+
).all(ftsQuery, resolvedRepo, limit);
|
|
1906
|
+
}
|
|
1907
|
+
return db.prepare(
|
|
1908
|
+
`
|
|
1909
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
1910
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
1911
|
+
FROM memories_fts
|
|
1912
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
1913
|
+
WHERE memories_fts MATCH ?
|
|
1914
|
+
ORDER BY rank
|
|
1915
|
+
LIMIT ?
|
|
1916
|
+
`
|
|
1917
|
+
).all(ftsQuery, limit);
|
|
1918
|
+
} catch {
|
|
1919
|
+
return [];
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1722
1922
|
function registerSearchMemoryTool(server) {
|
|
1723
1923
|
server.registerTool(
|
|
1724
1924
|
"search_memory",
|
|
1725
1925
|
{
|
|
1726
|
-
description: "Search memories using full-text search with optional repository filtering.",
|
|
1926
|
+
description: "Search memories using full-text search with optional repository filtering. Falls back to recent + pinned context when the query has no exact matches.",
|
|
1727
1927
|
inputSchema: searchMemoryInputSchema
|
|
1728
1928
|
},
|
|
1729
1929
|
async ({ query, repo, limit }) => {
|
|
1730
1930
|
try {
|
|
1731
1931
|
const db = getDb();
|
|
1732
|
-
const
|
|
1733
|
-
const resolvedRepo = repo ? resolveRepoArg(repo,
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
`
|
|
1752
|
-
).all(ftsQuery, limit);
|
|
1932
|
+
const tokens = tokenizeQuery(query);
|
|
1933
|
+
const resolvedRepo = repo ? resolveRepoArg(repo, getWorkspaceRoot(), db).canonical : void 0;
|
|
1934
|
+
const andQuery = buildFtsQuery2(tokens);
|
|
1935
|
+
let rows = [];
|
|
1936
|
+
if (andQuery) {
|
|
1937
|
+
rows = runFts(andQuery, resolvedRepo, limit);
|
|
1938
|
+
}
|
|
1939
|
+
if (rows.length === 0 && tokens.length > 1) {
|
|
1940
|
+
const orQuery = buildFtsQueryOr(tokens);
|
|
1941
|
+
if (orQuery) {
|
|
1942
|
+
rows = runFts(orQuery, resolvedRepo, limit);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
let usedFallback = false;
|
|
1946
|
+
if (rows.length === 0 && resolvedRepo) {
|
|
1947
|
+
const fallback = fetchRepoContext(db, resolvedRepo, limit);
|
|
1948
|
+
rows = fallback.map((row) => ({ ...row, rank: 0 }));
|
|
1949
|
+
usedFallback = fallback.length > 0;
|
|
1950
|
+
}
|
|
1753
1951
|
if (rows.length === 0) {
|
|
1754
1952
|
return {
|
|
1755
1953
|
content: [
|
|
@@ -1767,11 +1965,12 @@ function registerSearchMemoryTool(server) {
|
|
|
1767
1965
|
return `${index + 1}. [${row.repo}] ${row.type} (${row.row_id} | legacy: ${row.id})
|
|
1768
1966
|
${pinPrefix}${row.note}${tagsText}`;
|
|
1769
1967
|
}).join("\n\n");
|
|
1968
|
+
const header = usedFallback ? `No exact match for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}; showing recent + pinned context:` : `Search results for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}:`;
|
|
1770
1969
|
return {
|
|
1771
1970
|
content: [
|
|
1772
1971
|
{
|
|
1773
1972
|
type: "text",
|
|
1774
|
-
text:
|
|
1973
|
+
text: `${header}
|
|
1775
1974
|
|
|
1776
1975
|
${formatted}`
|
|
1777
1976
|
}
|
|
@@ -1797,7 +1996,9 @@ var init_search = __esm({
|
|
|
1797
1996
|
"src/tools/search.ts"() {
|
|
1798
1997
|
"use strict";
|
|
1799
1998
|
init_client();
|
|
1999
|
+
init_context();
|
|
1800
2000
|
init_repo();
|
|
2001
|
+
init_workspace();
|
|
1801
2002
|
searchMemoryInputSchema = {
|
|
1802
2003
|
query: z8.string().trim().min(1, "query is required"),
|
|
1803
2004
|
repo: z8.string().trim().min(1).optional(),
|
|
@@ -1819,7 +2020,7 @@ function registerStoreContextTool(server) {
|
|
|
1819
2020
|
async ({ repo, type, note, tags }) => {
|
|
1820
2021
|
try {
|
|
1821
2022
|
const db = getDb();
|
|
1822
|
-
const resolved = resolveRepoArg(repo,
|
|
2023
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1823
2024
|
const now = Math.floor(Date.now() / 1e3);
|
|
1824
2025
|
const id = nanoid2();
|
|
1825
2026
|
const normalizedTags = Array.from(
|
|
@@ -1877,6 +2078,7 @@ var init_store = __esm({
|
|
|
1877
2078
|
init_client();
|
|
1878
2079
|
init_dedupe();
|
|
1879
2080
|
init_repo();
|
|
2081
|
+
init_workspace();
|
|
1880
2082
|
storeContextInputSchema = {
|
|
1881
2083
|
repo: z9.string().trim().min(1).optional(),
|
|
1882
2084
|
type: z9.enum(MEMORY_TYPES),
|
|
@@ -1898,7 +2100,7 @@ function registerSummarizeRepoContextTool(server) {
|
|
|
1898
2100
|
async ({ repo }) => {
|
|
1899
2101
|
try {
|
|
1900
2102
|
const db = getDb();
|
|
1901
|
-
const resolved = resolveRepoArg(repo,
|
|
2103
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1902
2104
|
const rows = db.prepare(
|
|
1903
2105
|
`
|
|
1904
2106
|
SELECT rowid AS row_id, type, note, pinned
|
|
@@ -1962,6 +2164,7 @@ var init_summarize = __esm({
|
|
|
1962
2164
|
"use strict";
|
|
1963
2165
|
init_client();
|
|
1964
2166
|
init_repo();
|
|
2167
|
+
init_workspace();
|
|
1965
2168
|
summarizeRepoContextInputSchema = {
|
|
1966
2169
|
repo: z10.string().trim().min(1).optional()
|
|
1967
2170
|
};
|
|
@@ -2006,7 +2209,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
2006
2209
|
server.registerTool(
|
|
2007
2210
|
"update_memory",
|
|
2008
2211
|
{
|
|
2009
|
-
description: "Update an existing memory by numeric
|
|
2212
|
+
description: "Update an existing memory by id (numeric or legacy string) with partial fields.",
|
|
2010
2213
|
inputSchema: updateMemoryInputSchema
|
|
2011
2214
|
},
|
|
2012
2215
|
async ({ id, content, memory_type }) => {
|
|
@@ -2023,14 +2226,8 @@ function registerUpdateMemoryTool(server) {
|
|
|
2023
2226
|
};
|
|
2024
2227
|
}
|
|
2025
2228
|
const db = getDb();
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
2029
|
-
FROM memories
|
|
2030
|
-
WHERE rowid = ?
|
|
2031
|
-
`
|
|
2032
|
-
).get(id);
|
|
2033
|
-
if (!existing) {
|
|
2229
|
+
const target = findMemoryByAnyId(db, id);
|
|
2230
|
+
if (!target) {
|
|
2034
2231
|
return {
|
|
2035
2232
|
isError: true,
|
|
2036
2233
|
content: [
|
|
@@ -2041,6 +2238,13 @@ function registerUpdateMemoryTool(server) {
|
|
|
2041
2238
|
]
|
|
2042
2239
|
};
|
|
2043
2240
|
}
|
|
2241
|
+
const existing = db.prepare(
|
|
2242
|
+
`
|
|
2243
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
2244
|
+
FROM memories
|
|
2245
|
+
WHERE rowid = ?
|
|
2246
|
+
`
|
|
2247
|
+
).get(target.row_id);
|
|
2044
2248
|
const now = Math.floor(Date.now() / 1e3);
|
|
2045
2249
|
const nextType = memory_type ?? existing.type;
|
|
2046
2250
|
const nextNote = content ?? existing.note;
|
|
@@ -2052,7 +2256,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
2052
2256
|
SET type = ?, note = ?, note_normalized = ?, updated_at = ?
|
|
2053
2257
|
WHERE rowid = ?
|
|
2054
2258
|
`
|
|
2055
|
-
).run(nextType, nextNote, nextNormalized, now,
|
|
2259
|
+
).run(nextType, nextNote, nextNormalized, now, existing.row_id);
|
|
2056
2260
|
} else {
|
|
2057
2261
|
db.prepare(
|
|
2058
2262
|
`
|
|
@@ -2060,7 +2264,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
2060
2264
|
SET type = ?, note = ?, updated_at = ?
|
|
2061
2265
|
WHERE rowid = ?
|
|
2062
2266
|
`
|
|
2063
|
-
).run(nextType, nextNote, now,
|
|
2267
|
+
).run(nextType, nextNote, now, existing.row_id);
|
|
2064
2268
|
}
|
|
2065
2269
|
const updated = db.prepare(
|
|
2066
2270
|
`
|
|
@@ -2068,7 +2272,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
2068
2272
|
FROM memories
|
|
2069
2273
|
WHERE rowid = ?
|
|
2070
2274
|
`
|
|
2071
|
-
).get(
|
|
2275
|
+
).get(existing.row_id);
|
|
2072
2276
|
if (!updated) {
|
|
2073
2277
|
return {
|
|
2074
2278
|
isError: true,
|
|
@@ -2109,8 +2313,11 @@ var init_update = __esm({
|
|
|
2109
2313
|
"use strict";
|
|
2110
2314
|
init_client();
|
|
2111
2315
|
init_dedupe();
|
|
2316
|
+
init_memory();
|
|
2112
2317
|
updateMemoryInputSchema = {
|
|
2113
|
-
id
|
|
2318
|
+
// Accept numeric row_id or legacy string id so callers can paste whichever
|
|
2319
|
+
// form they have.
|
|
2320
|
+
id: z11.union([z11.number().int().positive(), z11.string().trim().min(1)]),
|
|
2114
2321
|
content: z11.string().trim().min(1).optional(),
|
|
2115
2322
|
memory_type: z11.enum(MEMORY_TYPES).optional()
|
|
2116
2323
|
};
|
|
@@ -2143,7 +2350,7 @@ function registerStartupContextResource(server) {
|
|
|
2143
2350
|
async (uri) => {
|
|
2144
2351
|
try {
|
|
2145
2352
|
const db = getDb();
|
|
2146
|
-
const resolved = resolveRepo(
|
|
2353
|
+
const resolved = resolveRepo(getWorkspaceRoot(), db);
|
|
2147
2354
|
const rows = fetchRepoContext(db, resolved.canonical, 5);
|
|
2148
2355
|
const text = formatContext(rows, {
|
|
2149
2356
|
repo: resolved.canonical,
|
|
@@ -2180,7 +2387,7 @@ async function startServer() {
|
|
|
2180
2387
|
initDb(dbPath);
|
|
2181
2388
|
const server = new McpServer({
|
|
2182
2389
|
name: "fossel",
|
|
2183
|
-
version: "1.1.
|
|
2390
|
+
version: "1.1.1"
|
|
2184
2391
|
});
|
|
2185
2392
|
registerRememberTool(server);
|
|
2186
2393
|
registerGetContextTool(server);
|
|
@@ -2205,6 +2412,7 @@ var init_index = __esm({
|
|
|
2205
2412
|
init_client();
|
|
2206
2413
|
init_context();
|
|
2207
2414
|
init_repo();
|
|
2415
|
+
init_workspace();
|
|
2208
2416
|
init_dedupe_repo();
|
|
2209
2417
|
init_delete();
|
|
2210
2418
|
init_get_context();
|
|
@@ -2230,13 +2438,14 @@ var init_index = __esm({
|
|
|
2230
2438
|
|
|
2231
2439
|
// src/cli.ts
|
|
2232
2440
|
init_client();
|
|
2441
|
+
init_dedupe();
|
|
2233
2442
|
init_repo();
|
|
2234
2443
|
import { homedir as homedir2 } from "os";
|
|
2235
2444
|
import { join as join2 } from "path";
|
|
2236
2445
|
import { statSync } from "fs";
|
|
2237
2446
|
import { nanoid as nanoid3 } from "nanoid";
|
|
2238
2447
|
var DEFAULT_DB_PATH = join2(homedir2(), ".fossel", "memory.db");
|
|
2239
|
-
var INIT_MEMORY_TEXT = "Fossel is active for this repo.
|
|
2448
|
+
var INIT_MEMORY_TEXT = "Fossel is active for this repo. Say 'remember this' or call get_context to retrieve repo memories.";
|
|
2240
2449
|
function resolveDbPath2() {
|
|
2241
2450
|
return process.env.FOSSEL_DB_PATH?.trim() || DEFAULT_DB_PATH;
|
|
2242
2451
|
}
|
|
@@ -2260,7 +2469,7 @@ function ensureSampleMemoryIfEmpty(repo) {
|
|
|
2260
2469
|
"[]",
|
|
2261
2470
|
now,
|
|
2262
2471
|
now,
|
|
2263
|
-
INIT_MEMORY_TEXT
|
|
2472
|
+
normalizeText(INIT_MEMORY_TEXT)
|
|
2264
2473
|
);
|
|
2265
2474
|
return true;
|
|
2266
2475
|
}
|
|
@@ -2269,7 +2478,12 @@ var MCP_CONFIG_SNIPPET = JSON.stringify(
|
|
|
2269
2478
|
mcpServers: {
|
|
2270
2479
|
fossel: {
|
|
2271
2480
|
command: "npx",
|
|
2272
|
-
args: ["-y", "fossel"]
|
|
2481
|
+
args: ["-y", "fossel"],
|
|
2482
|
+
// FOSSEL_WORKSPACE pins the workspace root so the server detects the
|
|
2483
|
+
// right repo even when the IDE launches MCP servers from another cwd.
|
|
2484
|
+
env: {
|
|
2485
|
+
FOSSEL_WORKSPACE: "${workspaceFolder}"
|
|
2486
|
+
}
|
|
2273
2487
|
}
|
|
2274
2488
|
}
|
|
2275
2489
|
},
|
|
@@ -2293,7 +2507,44 @@ function findMergeCandidates(canonical) {
|
|
|
2293
2507
|
return otherTail === tail || otherTail === canonical || row.repo === tail;
|
|
2294
2508
|
});
|
|
2295
2509
|
}
|
|
2296
|
-
function
|
|
2510
|
+
function autoDedupeExact(repo) {
|
|
2511
|
+
const db = getDb();
|
|
2512
|
+
const groups = db.prepare(
|
|
2513
|
+
`
|
|
2514
|
+
SELECT note_normalized, type, COUNT(*) AS count
|
|
2515
|
+
FROM memories
|
|
2516
|
+
WHERE repo = ? AND note_normalized != ''
|
|
2517
|
+
GROUP BY note_normalized, type
|
|
2518
|
+
HAVING COUNT(*) > 1
|
|
2519
|
+
`
|
|
2520
|
+
).all(repo);
|
|
2521
|
+
if (groups.length === 0) {
|
|
2522
|
+
return 0;
|
|
2523
|
+
}
|
|
2524
|
+
let removed = 0;
|
|
2525
|
+
const tx = db.transaction(() => {
|
|
2526
|
+
for (const group of groups) {
|
|
2527
|
+
const rows = db.prepare(
|
|
2528
|
+
`
|
|
2529
|
+
SELECT rowid AS row_id, pinned, updated_at
|
|
2530
|
+
FROM memories
|
|
2531
|
+
WHERE repo = ? AND note_normalized = ? AND type = ?
|
|
2532
|
+
ORDER BY pinned DESC, updated_at DESC, rowid DESC
|
|
2533
|
+
`
|
|
2534
|
+
).all(repo, group.note_normalized, group.type);
|
|
2535
|
+
const [keep, ...rest] = rows;
|
|
2536
|
+
if (!keep) continue;
|
|
2537
|
+
const drop = db.prepare("DELETE FROM memories WHERE rowid = ?");
|
|
2538
|
+
for (const row of rest) {
|
|
2539
|
+
drop.run(row.row_id);
|
|
2540
|
+
removed += 1;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
2544
|
+
tx();
|
|
2545
|
+
return removed;
|
|
2546
|
+
}
|
|
2547
|
+
function runInit(options) {
|
|
2297
2548
|
const dbPath = resolveDbPath2();
|
|
2298
2549
|
initDb(dbPath);
|
|
2299
2550
|
const db = getDb();
|
|
@@ -2301,12 +2552,18 @@ function runInit() {
|
|
|
2301
2552
|
const candidates = findMergeCandidates(resolved.canonical);
|
|
2302
2553
|
let mergedAliases = 0;
|
|
2303
2554
|
let mergedMemories = 0;
|
|
2555
|
+
let rewrittenNotes = 0;
|
|
2304
2556
|
for (const candidate of candidates) {
|
|
2305
2557
|
const result = mergeRepoKeys(db, candidate.repo, resolved.canonical);
|
|
2306
2558
|
mergedAliases += result.movedAliases;
|
|
2307
2559
|
mergedMemories += result.movedMemories;
|
|
2560
|
+
rewrittenNotes += result.rewrittenNotes;
|
|
2308
2561
|
}
|
|
2309
2562
|
const sampleAdded = ensureSampleMemoryIfEmpty(resolved.canonical);
|
|
2563
|
+
let autoMerged = 0;
|
|
2564
|
+
if (options.autoDedupe) {
|
|
2565
|
+
autoMerged = autoDedupeExact(resolved.canonical);
|
|
2566
|
+
}
|
|
2310
2567
|
const countRow = db.prepare("SELECT COUNT(*) AS count FROM memories WHERE repo = ?").get(resolved.canonical);
|
|
2311
2568
|
console.log("Fossel \u2014 local-first MCP memory for your repos.\n");
|
|
2312
2569
|
console.log(`Canonical repo key: ${resolved.canonical}`);
|
|
@@ -2318,12 +2575,16 @@ function runInit() {
|
|
|
2318
2575
|
console.log(` aliases: ${resolved.aliases.join(", ")}`);
|
|
2319
2576
|
}
|
|
2320
2577
|
console.log("");
|
|
2321
|
-
if (mergedAliases > 0 || mergedMemories > 0) {
|
|
2578
|
+
if (mergedAliases > 0 || mergedMemories > 0 || rewrittenNotes > 0) {
|
|
2322
2579
|
console.log(
|
|
2323
|
-
`Merged ${mergedMemories} memory row(s)
|
|
2580
|
+
`Merged ${mergedMemories} memory row(s), ${mergedAliases} alias row(s), and rewrote ${rewrittenNotes} stale mention(s) into ${resolved.canonical}.`
|
|
2324
2581
|
);
|
|
2325
2582
|
console.log("");
|
|
2326
2583
|
}
|
|
2584
|
+
if (autoMerged > 0) {
|
|
2585
|
+
console.log(`Auto-deduped ${autoMerged} exact duplicate row(s).`);
|
|
2586
|
+
console.log("");
|
|
2587
|
+
}
|
|
2327
2588
|
console.log("MCP config (Cursor: ~/.cursor/mcp.json, Claude Desktop: settings):");
|
|
2328
2589
|
console.log(MCP_CONFIG_SNIPPET);
|
|
2329
2590
|
console.log("");
|
|
@@ -2339,9 +2600,11 @@ function runInit() {
|
|
|
2339
2600
|
console.log(" resolve_repo \u2014 show which repo key Fossel will use");
|
|
2340
2601
|
console.log(" store_context \u2014 explicit save (advanced)");
|
|
2341
2602
|
console.log(" dedupe_repo \u2014 merge near-duplicate memories");
|
|
2603
|
+
console.log("");
|
|
2604
|
+
console.log("Set FOSSEL_WORKSPACE in your MCP config to your project root if Fossel detects the wrong repo.");
|
|
2342
2605
|
closeDb();
|
|
2343
2606
|
}
|
|
2344
|
-
function
|
|
2607
|
+
function gatherDoctorReport() {
|
|
2345
2608
|
const dbPath = resolveDbPath2();
|
|
2346
2609
|
const lines = [];
|
|
2347
2610
|
let ok = true;
|
|
@@ -2366,6 +2629,22 @@ function runDoctor() {
|
|
|
2366
2629
|
} else {
|
|
2367
2630
|
lines.push("No sibling repo keys.");
|
|
2368
2631
|
}
|
|
2632
|
+
const staleMentions = [];
|
|
2633
|
+
for (const alias of resolved.aliases) {
|
|
2634
|
+
if (alias === resolved.canonical) continue;
|
|
2635
|
+
const found2 = findMemoriesMentioningAlias(db, alias, resolved.canonical);
|
|
2636
|
+
for (const row of found2) {
|
|
2637
|
+
staleMentions.push({ alias, row_id: row.row_id, note: row.note });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (staleMentions.length > 0) {
|
|
2641
|
+
ok = false;
|
|
2642
|
+
lines.push(
|
|
2643
|
+
`\u26A0 ${staleMentions.length} memory note(s) still mention a deprecated repo key. Run \`fossel doctor --fix\` to rewrite them.`
|
|
2644
|
+
);
|
|
2645
|
+
} else {
|
|
2646
|
+
lines.push("No memory notes reference deprecated repo keys.");
|
|
2647
|
+
}
|
|
2369
2648
|
const duplicateRows = db.prepare(
|
|
2370
2649
|
`
|
|
2371
2650
|
SELECT note_normalized, COUNT(*) AS count
|
|
@@ -2379,7 +2658,7 @@ function runDoctor() {
|
|
|
2379
2658
|
ok = false;
|
|
2380
2659
|
const total = duplicateRows.reduce((sum, row) => sum + row.count - 1, 0);
|
|
2381
2660
|
lines.push(
|
|
2382
|
-
`\u26A0 ${duplicateRows.length} duplicate clusters covering ${total} extra row(s). Run \`dedupe_repo\` with apply=true.`
|
|
2661
|
+
`\u26A0 ${duplicateRows.length} duplicate clusters covering ${total} extra row(s). Run \`fossel doctor --fix\` (or \`dedupe_repo\` with apply=true) to merge.`
|
|
2383
2662
|
);
|
|
2384
2663
|
} else {
|
|
2385
2664
|
lines.push("No exact-duplicate memory clusters.");
|
|
@@ -2393,7 +2672,13 @@ function runDoctor() {
|
|
|
2393
2672
|
"Claude",
|
|
2394
2673
|
"claude_desktop_config.json"
|
|
2395
2674
|
),
|
|
2396
|
-
join2(
|
|
2675
|
+
join2(
|
|
2676
|
+
homedir2(),
|
|
2677
|
+
"Library",
|
|
2678
|
+
"Application Support",
|
|
2679
|
+
"Claude",
|
|
2680
|
+
"claude_desktop_config.json"
|
|
2681
|
+
)
|
|
2397
2682
|
];
|
|
2398
2683
|
const found = mcpConfigCandidates.filter((path) => {
|
|
2399
2684
|
try {
|
|
@@ -2411,13 +2696,41 @@ function runDoctor() {
|
|
|
2411
2696
|
}
|
|
2412
2697
|
const totalRow = db.prepare("SELECT COUNT(*) AS count FROM memories").get();
|
|
2413
2698
|
lines.push(`Total memories across all repos: ${totalRow.count}`);
|
|
2414
|
-
|
|
2415
|
-
|
|
2699
|
+
return { ok, lines, duplicateClusters: duplicateRows.length, staleMentions };
|
|
2700
|
+
}
|
|
2701
|
+
function runDoctor(options) {
|
|
2702
|
+
const report = gatherDoctorReport();
|
|
2703
|
+
console.log(report.lines.join("\n"));
|
|
2416
2704
|
console.log("");
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2705
|
+
if (!options.fix) {
|
|
2706
|
+
console.log(report.ok ? "Status: OK" : "Status: needs attention (see \u26A0 lines above)");
|
|
2707
|
+
if (!report.ok) {
|
|
2708
|
+
process.exitCode = 1;
|
|
2709
|
+
}
|
|
2710
|
+
closeDb();
|
|
2711
|
+
return;
|
|
2420
2712
|
}
|
|
2713
|
+
const db = getDb();
|
|
2714
|
+
const resolved = resolveRepo(process.cwd(), db);
|
|
2715
|
+
const candidates = findMergeCandidates(resolved.canonical);
|
|
2716
|
+
let movedMemories = 0;
|
|
2717
|
+
let rewrittenNotes = 0;
|
|
2718
|
+
for (const candidate of candidates) {
|
|
2719
|
+
const result = mergeRepoKeys(db, candidate.repo, resolved.canonical);
|
|
2720
|
+
movedMemories += result.movedMemories;
|
|
2721
|
+
rewrittenNotes += result.rewrittenNotes;
|
|
2722
|
+
}
|
|
2723
|
+
const removed = autoDedupeExact(resolved.canonical);
|
|
2724
|
+
console.log("Applied fixes:");
|
|
2725
|
+
console.log(` merged repo memory rows: ${movedMemories}`);
|
|
2726
|
+
console.log(` rewrote stale mentions: ${rewrittenNotes}`);
|
|
2727
|
+
console.log(` removed exact duplicates: ${removed}`);
|
|
2728
|
+
console.log("");
|
|
2729
|
+
console.log("Re-run `fossel doctor` to verify.");
|
|
2730
|
+
closeDb();
|
|
2731
|
+
}
|
|
2732
|
+
function parseFlag(args, name) {
|
|
2733
|
+
return args.includes(`--${name}`);
|
|
2421
2734
|
}
|
|
2422
2735
|
async function main() {
|
|
2423
2736
|
const command = process.argv[2];
|
|
@@ -2427,15 +2740,18 @@ async function main() {
|
|
|
2427
2740
|
return;
|
|
2428
2741
|
}
|
|
2429
2742
|
if (command === "init") {
|
|
2430
|
-
|
|
2743
|
+
const args = process.argv.slice(3);
|
|
2744
|
+
const autoDedupe = !parseFlag(args, "no-dedupe");
|
|
2745
|
+
runInit({ autoDedupe });
|
|
2431
2746
|
return;
|
|
2432
2747
|
}
|
|
2433
2748
|
if (command === "doctor") {
|
|
2434
|
-
|
|
2749
|
+
const args = process.argv.slice(3);
|
|
2750
|
+
runDoctor({ fix: parseFlag(args, "fix") });
|
|
2435
2751
|
return;
|
|
2436
2752
|
}
|
|
2437
2753
|
console.error(`Unknown command: ${command}`);
|
|
2438
|
-
console.error("Usage: fossel [init | doctor]");
|
|
2754
|
+
console.error("Usage: fossel [init [--no-dedupe] | doctor [--fix]]");
|
|
2439
2755
|
process.exit(1);
|
|
2440
2756
|
}
|
|
2441
2757
|
main().catch((error) => {
|