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.
Files changed (4) hide show
  1. package/README.md +17 -4
  2. package/dist/cli.js +483 -167
  3. package/dist/index.js +187 -64
  4. 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/dedupe.ts
537
- function normalizeText(text) {
538
- return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
539
- }
540
- function tokenize(text) {
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
- const exact = db.prepare(
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 DEFAULT_THRESHOLD, DEFAULT_CANDIDATE_LIMIT;
618
- var init_dedupe = __esm({
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, process.cwd(), db);
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 row = db.prepare("SELECT id FROM memories WHERE id = ?").get(id);
822
- if (!row) {
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((memoryId) => {
834
- db.prepare("DELETE FROM memories WHERE id = ?").run(memoryId);
958
+ const deleteTx = db.transaction((rowId) => {
959
+ db.prepare("DELETE FROM memories WHERE rowid = ?").run(rowId);
835
960
  });
836
- deleteTx(id);
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
- id: z2.string().trim().min(1, "id is required")
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, process.cwd(), db);
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, process.cwd(), db);
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(memoryId, pinned) {
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, memoryId);
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(memoryId);
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 memory = setPinnedState(id, 1);
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 memory = setPinnedState(id, 0);
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: z5.number().int().positive()
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, process.cwd(), db);
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() || process.cwd();
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 normalizeFtsQuery(query) {
1708
- const terms = query.trim().split(/\s+/).map((term) => term.replaceAll('"', '""')).filter(Boolean);
1709
- if (terms.length === 0) {
1710
- throw new Error("query must contain searchable text");
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 terms.map((term) => `"${term}"`).join(" AND ");
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 ftsQuery = normalizeFtsQuery(query);
1733
- const resolvedRepo = repo ? resolveRepoArg(repo, process.cwd(), db).canonical : void 0;
1734
- const rows = resolvedRepo ? db.prepare(
1735
- `
1736
- 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
1737
- FROM memories_fts
1738
- JOIN memories AS m ON m.rowid = memories_fts.rowid
1739
- WHERE memories_fts MATCH ? AND m.repo = ?
1740
- ORDER BY rank
1741
- LIMIT ?
1742
- `
1743
- ).all(ftsQuery, resolvedRepo, limit) : db.prepare(
1744
- `
1745
- 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
1746
- FROM memories_fts
1747
- JOIN memories AS m ON m.rowid = memories_fts.rowid
1748
- WHERE memories_fts MATCH ?
1749
- ORDER BY rank
1750
- LIMIT ?
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: `Search results for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}:
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, process.cwd(), db);
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, process.cwd(), db);
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 id with partial fields.",
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 existing = db.prepare(
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, id);
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, id);
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(id);
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: z11.number().int().positive(),
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(process.cwd(), db);
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.0"
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. Use store_context to save context.";
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.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim()
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 runInit() {
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) and ${mergedAliases} alias row(s) into ${resolved.canonical}.`
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 runDoctor() {
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(homedir2(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
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
- closeDb();
2415
- console.log(lines.join("\n"));
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
- console.log(ok ? "Status: OK" : "Status: needs attention (see \u26A0 lines above)");
2418
- if (!ok) {
2419
- process.exitCode = 1;
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
- runInit();
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
- runDoctor();
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) => {