minimem 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -5,20 +5,21 @@ import { program } from "commander";
5
5
 
6
6
  // src/cli/commands/init.ts
7
7
  import fs2 from "fs/promises";
8
- import path2 from "path";
8
+ import path3 from "path";
9
9
 
10
10
  // src/cli/config.ts
11
11
  import fs from "fs/promises";
12
- import path from "path";
13
- import os from "os";
12
+ import path2 from "path";
13
+ import os2 from "os";
14
14
  import crypto from "crypto";
15
- var CONFIG_FILENAME = "config.json";
16
- var CONFIG_DIR = ".minimem";
17
- var GLOBAL_DIR = ".minimem";
18
- var XDG_CONFIG_DIR = ".config/minimem";
15
+
16
+ // src/cli/shared.ts
17
+ import * as path from "path";
18
+ import * as os from "os";
19
19
  function resolveMemoryDir(options) {
20
- if (options.dir) {
21
- return path.resolve(options.dir);
20
+ const dir = Array.isArray(options.dir) ? options.dir[0] : options.dir;
21
+ if (dir) {
22
+ return path.resolve(dir);
22
23
  }
23
24
  const envDir = process.env.MEMORY_DIR;
24
25
  if (envDir) {
@@ -29,27 +30,81 @@ function resolveMemoryDir(options) {
29
30
  }
30
31
  return process.cwd();
31
32
  }
33
+ function resolveMemoryDirs(options) {
34
+ const dirs = [];
35
+ if (options.dir) {
36
+ const dirList = Array.isArray(options.dir) ? options.dir : [options.dir];
37
+ dirs.push(...dirList.map((d) => path.resolve(d)));
38
+ }
39
+ if (dirs.length === 0 && process.env.MEMORY_DIR) {
40
+ dirs.push(path.resolve(process.env.MEMORY_DIR));
41
+ }
42
+ if (options.global) {
43
+ const globalDir = path.join(os.homedir(), ".minimem");
44
+ if (!dirs.includes(globalDir)) {
45
+ dirs.push(globalDir);
46
+ }
47
+ }
48
+ if (dirs.length === 0) {
49
+ dirs.push(process.cwd());
50
+ }
51
+ return [...new Set(dirs)];
52
+ }
53
+ function getGlobalMemoryDir() {
54
+ return path.join(os.homedir(), ".minimem");
55
+ }
56
+ function exitWithError(message, suggestion) {
57
+ console.error(`Error: ${message}`);
58
+ if (suggestion) {
59
+ console.error(` Suggestion: ${suggestion}`);
60
+ }
61
+ process.exit(1);
62
+ }
63
+ function warn(message) {
64
+ console.error(`Warning: ${message}`);
65
+ }
66
+ function note(message) {
67
+ console.error(`Note: ${message}`);
68
+ }
69
+ function getDirName(memoryDir) {
70
+ const home = os.homedir();
71
+ if (memoryDir === path.join(home, ".minimem")) {
72
+ return "global";
73
+ }
74
+ const name = path.basename(memoryDir);
75
+ if (name.startsWith(".")) {
76
+ const parent = path.basename(path.dirname(memoryDir));
77
+ return `${parent}/${name}`;
78
+ }
79
+ return name;
80
+ }
81
+
82
+ // src/cli/config.ts
83
+ var CONFIG_FILENAME = "config.json";
84
+ var CONFIG_DIR = ".minimem";
85
+ var GLOBAL_DIR = ".minimem";
86
+ var XDG_CONFIG_DIR = ".config/minimem";
32
87
  function getGlobalDir() {
33
- return path.join(os.homedir(), GLOBAL_DIR);
88
+ return path2.join(os2.homedir(), GLOBAL_DIR);
34
89
  }
35
90
  function getGlobalConfigPath() {
36
- return path.join(getGlobalDir(), CONFIG_DIR, CONFIG_FILENAME);
91
+ return path2.join(getGlobalDir(), CONFIG_DIR, CONFIG_FILENAME);
37
92
  }
38
93
  function getConfigPath(memoryDir) {
39
- return path.join(memoryDir, CONFIG_DIR, CONFIG_FILENAME);
94
+ return path2.join(memoryDir, CONFIG_DIR, CONFIG_FILENAME);
40
95
  }
41
96
  function getXdgConfigDir() {
42
- return path.join(os.homedir(), XDG_CONFIG_DIR);
97
+ return path2.join(os2.homedir(), XDG_CONFIG_DIR);
43
98
  }
44
99
  function getXdgConfigPath() {
45
- return path.join(getXdgConfigDir(), CONFIG_FILENAME);
100
+ return path2.join(getXdgConfigDir(), CONFIG_FILENAME);
46
101
  }
47
102
  function expandPath(filePath) {
48
103
  if (filePath.startsWith("~/")) {
49
- return path.join(os.homedir(), filePath.slice(2));
104
+ return path2.join(os2.homedir(), filePath.slice(2));
50
105
  }
51
106
  if (filePath === "~") {
52
- return os.homedir();
107
+ return os2.homedir();
53
108
  }
54
109
  return filePath;
55
110
  }
@@ -72,7 +127,7 @@ async function getMachineId() {
72
127
  if (globalConfig.machineId) {
73
128
  return globalConfig.machineId;
74
129
  }
75
- const hostname = os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-");
130
+ const hostname = os2.hostname().toLowerCase().replace(/[^a-z0-9-]/g, "-");
76
131
  const suffix = crypto.randomBytes(2).toString("hex");
77
132
  const machineId = `${hostname}-${suffix}`;
78
133
  await saveXdgConfig({ ...globalConfig, machineId });
@@ -91,7 +146,7 @@ async function loadGlobalConfig() {
91
146
  }
92
147
  async function loadConfig(memoryDir) {
93
148
  const globalDir = getGlobalDir();
94
- const isGlobalDir = path.resolve(memoryDir) === globalDir;
149
+ const isGlobalDir = path2.resolve(memoryDir) === globalDir;
95
150
  const globalConfig = isGlobalDir ? {} : await loadGlobalConfig();
96
151
  const localConfig = await loadConfigFile(getConfigPath(memoryDir));
97
152
  return deepMergeConfig(globalConfig, localConfig);
@@ -119,6 +174,9 @@ function deepMergeConfig(target, source) {
119
174
  if (source.chunking) {
120
175
  result.chunking = { ...target.chunking, ...source.chunking };
121
176
  }
177
+ if (source.hooks) {
178
+ result.hooks = { ...target.hooks, ...source.hooks };
179
+ }
122
180
  if (source.sync) {
123
181
  result.sync = { ...target.sync, ...source.sync };
124
182
  if (source.sync.include) {
@@ -131,7 +189,7 @@ function deepMergeConfig(target, source) {
131
189
  return result;
132
190
  }
133
191
  async function saveConfig(memoryDir, config2) {
134
- const configDir = path.join(memoryDir, CONFIG_DIR);
192
+ const configDir = path2.join(memoryDir, CONFIG_DIR);
135
193
  const configPath = getConfigPath(memoryDir);
136
194
  await fs.mkdir(configDir, { recursive: true });
137
195
  await fs.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
@@ -174,7 +232,7 @@ function getDefaultGlobalSyncConfig() {
174
232
  }
175
233
  async function loadFullConfig(memoryDir) {
176
234
  const globalDir = getGlobalDir();
177
- const isGlobalDir = path.resolve(memoryDir) === globalDir;
235
+ const isGlobalDir = path2.resolve(memoryDir) === globalDir;
178
236
  const xdgConfig = await loadXdgConfig();
179
237
  const legacyGlobalConfig = isGlobalDir ? {} : await loadGlobalConfig();
180
238
  const localConfig = await loadConfigFile(getConfigPath(memoryDir));
@@ -258,7 +316,7 @@ async function isInitialized(memoryDir) {
258
316
  }
259
317
  }
260
318
  function formatPath(filePath) {
261
- const home = os.homedir();
319
+ const home = os2.homedir();
262
320
  if (filePath.startsWith(home)) {
263
321
  return "~" + filePath.slice(home.length);
264
322
  }
@@ -289,9 +347,9 @@ async function init(dir, options) {
289
347
  }
290
348
  console.log(`Initializing minimem in ${displayPath}...`);
291
349
  await fs2.mkdir(memoryDir, { recursive: true });
292
- await fs2.mkdir(path2.join(memoryDir, "memory"), { recursive: true });
293
- await fs2.mkdir(path2.join(memoryDir, ".minimem"), { recursive: true });
294
- const memoryFilePath = path2.join(memoryDir, "MEMORY.md");
350
+ await fs2.mkdir(path3.join(memoryDir, "memory"), { recursive: true });
351
+ await fs2.mkdir(path3.join(memoryDir, ".minimem"), { recursive: true });
352
+ const memoryFilePath = path3.join(memoryDir, "MEMORY.md");
295
353
  try {
296
354
  await fs2.access(memoryFilePath);
297
355
  console.log(" MEMORY.md already exists, skipping");
@@ -302,7 +360,7 @@ async function init(dir, options) {
302
360
  const config2 = getInitConfig();
303
361
  await saveConfig(memoryDir, config2);
304
362
  console.log(" Created .minimem/config.json");
305
- const gitignorePath = path2.join(memoryDir, ".minimem", ".gitignore");
363
+ const gitignorePath = path3.join(memoryDir, ".minimem", ".gitignore");
306
364
  await fs2.writeFile(gitignorePath, "index.db\nindex.db-*\n", "utf-8");
307
365
  console.log(" Created .minimem/.gitignore");
308
366
  console.log();
@@ -319,14 +377,10 @@ async function init(dir, options) {
319
377
  console.log(` minimem search "your query"${dir ? ` --dir ${dir}` : ""}`);
320
378
  }
321
379
 
322
- // src/cli/commands/search.ts
323
- import * as path6 from "path";
324
- import * as os3 from "os";
325
-
326
380
  // src/minimem.ts
327
381
  import { randomUUID } from "crypto";
328
382
  import fs4 from "fs/promises";
329
- import path5 from "path";
383
+ import path6 from "path";
330
384
  import { DatabaseSync } from "node:sqlite";
331
385
  import chokidar from "chokidar";
332
386
 
@@ -334,11 +388,20 @@ import chokidar from "chokidar";
334
388
  import crypto2 from "crypto";
335
389
  import fsSync from "fs";
336
390
  import fs3 from "fs/promises";
337
- import path3 from "path";
338
- function ensureDir(dir) {
391
+ import path4 from "path";
392
+ function logError2(context, error, debug) {
393
+ if (!debug) return;
394
+ const message = error instanceof Error ? error.message : String(error);
395
+ debug(`[${context}] Error: ${message}`);
396
+ }
397
+ function ensureDir(dir, debug) {
339
398
  try {
340
399
  fsSync.mkdirSync(dir, { recursive: true });
341
- } catch {
400
+ } catch (error) {
401
+ const nodeError = error;
402
+ if (nodeError.code !== "EEXIST") {
403
+ logError2("ensureDir", error, debug);
404
+ }
342
405
  }
343
406
  return dir;
344
407
  }
@@ -353,7 +416,7 @@ async function exists(filePath) {
353
416
  async function walkDir(dir, files) {
354
417
  const entries = await fs3.readdir(dir, { withFileTypes: true });
355
418
  for (const entry of entries) {
356
- const full = path3.join(dir, entry.name);
419
+ const full = path4.join(dir, entry.name);
357
420
  if (entry.isDirectory()) {
358
421
  await walkDir(full, files);
359
422
  continue;
@@ -365,11 +428,33 @@ async function walkDir(dir, files) {
365
428
  }
366
429
  async function listMemoryFiles(memoryDir) {
367
430
  const result = [];
368
- const memoryFile = path3.join(memoryDir, "MEMORY.md");
369
- const altMemoryFile = path3.join(memoryDir, "memory.md");
370
- if (await exists(memoryFile)) result.push(memoryFile);
371
- if (await exists(altMemoryFile)) result.push(altMemoryFile);
372
- const memorySubDir = path3.join(memoryDir, "memory");
431
+ const memoryFile = path4.join(memoryDir, "MEMORY.md");
432
+ const altMemoryFile = path4.join(memoryDir, "memory.md");
433
+ const hasUpper = await exists(memoryFile);
434
+ const hasLower = await exists(altMemoryFile);
435
+ if (hasUpper && hasLower) {
436
+ let upperReal = memoryFile;
437
+ let lowerReal = altMemoryFile;
438
+ try {
439
+ upperReal = await fs3.realpath(memoryFile);
440
+ } catch {
441
+ }
442
+ try {
443
+ lowerReal = await fs3.realpath(altMemoryFile);
444
+ } catch {
445
+ }
446
+ if (upperReal !== lowerReal) {
447
+ throw new Error(
448
+ `Both MEMORY.md and memory.md exist in ${memoryDir}. Please remove one to avoid ambiguity.`
449
+ );
450
+ }
451
+ result.push(memoryFile);
452
+ } else if (hasUpper) {
453
+ result.push(memoryFile);
454
+ } else if (hasLower) {
455
+ result.push(altMemoryFile);
456
+ }
457
+ const memorySubDir = path4.join(memoryDir, "memory");
373
458
  if (await exists(memorySubDir)) {
374
459
  await walkDir(memorySubDir, result);
375
460
  }
@@ -396,15 +481,22 @@ async function buildFileEntry(absPath, memoryDir) {
396
481
  const content = await fs3.readFile(absPath, "utf-8");
397
482
  const hash = hashText(content);
398
483
  return {
399
- path: path3.relative(memoryDir, absPath).replace(/\\/g, "/"),
484
+ path: path4.relative(memoryDir, absPath).replace(/\\/g, "/"),
400
485
  absPath,
401
486
  mtimeMs: stat.mtimeMs,
402
487
  size: stat.size,
403
488
  hash
404
489
  };
405
490
  }
491
+ function stripPrivateContent(content) {
492
+ return content.replace(/<private>[\s\S]*?<\/private>/gi, (match) => {
493
+ const lineCount = match.split("\n").length;
494
+ return "\n".repeat(lineCount - 1);
495
+ });
496
+ }
406
497
  function chunkMarkdown(content, chunking) {
407
- const lines = content.split("\n");
498
+ const stripped = stripPrivateContent(content);
499
+ const lines = stripped.split("\n");
408
500
  if (lines.length === 0) return [];
409
501
  const maxChars = Math.max(32, chunking.tokens * 4);
410
502
  const overlapChars = Math.max(0, chunking.overlap * 4);
@@ -468,6 +560,10 @@ function chunkMarkdown(content, chunking) {
468
560
  flush();
469
561
  return chunks;
470
562
  }
563
+ function extractChunkMetadata(text) {
564
+ const typeMatch = text.match(/<!--\s*type:\s*([\w-]+)\s*-->/i);
565
+ return typeMatch ? { type: typeMatch[1].toLowerCase() } : {};
566
+ }
471
567
  function parseEmbedding(raw) {
472
568
  try {
473
569
  const parsed = JSON.parse(raw);
@@ -496,6 +592,9 @@ function truncateUtf16Safe(text, maxChars) {
496
592
  if (text.length <= maxChars) return text;
497
593
  return text.slice(0, maxChars);
498
594
  }
595
+ function vectorToBlob(embedding) {
596
+ return Buffer.from(new Float32Array(embedding).buffer);
597
+ }
499
598
 
500
599
  // src/search/hybrid.ts
501
600
  function buildFtsQuery(raw) {
@@ -505,8 +604,11 @@ function buildFtsQuery(raw) {
505
604
  return quoted.join(" AND ");
506
605
  }
507
606
  function bm25RankToScore(rank) {
508
- const normalized = Number.isFinite(rank) ? Math.max(0, rank) : 999;
509
- return 1 / (1 + normalized);
607
+ if (!Number.isFinite(rank)) {
608
+ return 0;
609
+ }
610
+ const absRank = Math.abs(rank);
611
+ return 1 / (1 + absRank);
510
612
  }
511
613
  function mergeHybridResults(params) {
512
614
  const byId = /* @__PURE__ */ new Map();
@@ -540,8 +642,17 @@ function mergeHybridResults(params) {
540
642
  });
541
643
  }
542
644
  }
645
+ let vw = params.vectorWeight;
646
+ let tw = params.textWeight;
647
+ if (params.vector.length === 0 && params.keyword.length > 0) {
648
+ vw = 0;
649
+ tw = 1;
650
+ } else if (params.keyword.length === 0 && params.vector.length > 0) {
651
+ vw = 1;
652
+ tw = 0;
653
+ }
543
654
  const merged = Array.from(byId.values()).map((entry) => {
544
- const score = params.vectorWeight * entry.vectorScore + params.textWeight * entry.textScore;
655
+ const score = vw * entry.vectorScore + tw * entry.textScore;
545
656
  return {
546
657
  path: entry.path,
547
658
  startLine: entry.startLine,
@@ -555,7 +666,33 @@ function mergeHybridResults(params) {
555
666
  }
556
667
 
557
668
  // src/search/search.ts
558
- var vectorToBlob = (embedding) => Buffer.from(new Float32Array(embedding).buffer);
669
+ function buildKnowledgeFilterSql(opts) {
670
+ const clauses = [];
671
+ const params = [];
672
+ if (opts.knowledgeType) {
673
+ clauses.push(` AND c.knowledge_type = ?`);
674
+ params.push(opts.knowledgeType);
675
+ }
676
+ if (opts.minConfidence !== void 0) {
677
+ clauses.push(` AND c.confidence >= ?`);
678
+ params.push(opts.minConfidence);
679
+ }
680
+ if (opts.domain && opts.domain.length > 0) {
681
+ const domainPlaceholders = opts.domain.map(() => "?").join(", ");
682
+ clauses.push(
683
+ ` AND EXISTS (SELECT 1 FROM json_each(c.domains) AS d WHERE d.value IN (${domainPlaceholders}))`
684
+ );
685
+ params.push(...opts.domain);
686
+ }
687
+ if (opts.entities && opts.entities.length > 0) {
688
+ const entityPlaceholders = opts.entities.map(() => "?").join(", ");
689
+ clauses.push(
690
+ ` AND EXISTS (SELECT 1 FROM json_each(c.entities) AS e WHERE e.value IN (${entityPlaceholders}))`
691
+ );
692
+ params.push(...opts.entities);
693
+ }
694
+ return { sql: clauses.join(""), params };
695
+ }
559
696
  async function searchVector(params) {
560
697
  if (params.queryVec.length === 0 || params.limit <= 0) return [];
561
698
  if (await params.ensureVectorReady(params.queryVec.length)) {
@@ -647,6 +784,7 @@ async function searchKeyword(params) {
647
784
  }
648
785
 
649
786
  // src/db/schema.ts
787
+ var SCHEMA_VERSION = 4;
650
788
  function ensureMemoryIndexSchema(params) {
651
789
  params.db.exec(`
652
790
  CREATE TABLE IF NOT EXISTS meta (
@@ -654,6 +792,7 @@ function ensureMemoryIndexSchema(params) {
654
792
  value TEXT NOT NULL
655
793
  );
656
794
  `);
795
+ const migrated = migrateIfNeeded(params.db, params.ftsTable);
657
796
  params.db.exec(`
658
797
  CREATE TABLE IF NOT EXISTS files (
659
798
  path TEXT PRIMARY KEY,
@@ -716,9 +855,61 @@ function ensureMemoryIndexSchema(params) {
716
855
  }
717
856
  ensureColumn(params.db, "files", "source", "TEXT NOT NULL DEFAULT 'memory'");
718
857
  ensureColumn(params.db, "chunks", "source", "TEXT NOT NULL DEFAULT 'memory'");
858
+ ensureColumn(params.db, "chunks", "type", "TEXT");
859
+ ensureColumn(params.db, "chunks", "knowledge_type", "TEXT");
860
+ ensureColumn(params.db, "chunks", "knowledge_id", "TEXT");
861
+ ensureColumn(params.db, "chunks", "domains", "TEXT");
862
+ ensureColumn(params.db, "chunks", "entities", "TEXT");
863
+ ensureColumn(params.db, "chunks", "confidence", "REAL");
719
864
  params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_path ON chunks(path);`);
720
865
  params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source);`);
721
- return { ftsAvailable, ...ftsError ? { ftsError } : {} };
866
+ params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_type ON chunks(type);`);
867
+ params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_knowledge_type ON chunks(knowledge_type);`);
868
+ params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_knowledge_id ON chunks(knowledge_id);`);
869
+ params.db.exec(`
870
+ CREATE TABLE IF NOT EXISTS knowledge_links (
871
+ from_id TEXT NOT NULL,
872
+ to_id TEXT NOT NULL,
873
+ relation TEXT NOT NULL,
874
+ layer TEXT,
875
+ weight REAL DEFAULT 0.5,
876
+ source_path TEXT,
877
+ created_at INTEGER,
878
+ PRIMARY KEY (from_id, to_id, relation)
879
+ );
880
+ `);
881
+ params.db.exec(`CREATE INDEX IF NOT EXISTS idx_kl_from ON knowledge_links(from_id);`);
882
+ params.db.exec(`CREATE INDEX IF NOT EXISTS idx_kl_to ON knowledge_links(to_id);`);
883
+ params.db.exec(`CREATE INDEX IF NOT EXISTS idx_kl_layer ON knowledge_links(layer);`);
884
+ params.db.prepare(
885
+ `INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)`
886
+ ).run(String(SCHEMA_VERSION));
887
+ return { ftsAvailable, ...ftsError ? { ftsError } : {}, ...migrated ? { migrated } : {} };
888
+ }
889
+ function migrateIfNeeded(db, ftsTable) {
890
+ let storedVersion = 0;
891
+ try {
892
+ const row = db.prepare(
893
+ `SELECT value FROM meta WHERE key = 'schema_version'`
894
+ ).get();
895
+ if (row) {
896
+ storedVersion = parseInt(row.value, 10) || 0;
897
+ }
898
+ } catch {
899
+ storedVersion = 0;
900
+ }
901
+ if (storedVersion >= SCHEMA_VERSION) return false;
902
+ if (storedVersion > 0 && storedVersion < SCHEMA_VERSION) {
903
+ db.exec(`DROP TABLE IF EXISTS files`);
904
+ db.exec(`DROP TABLE IF EXISTS chunks`);
905
+ db.exec(`DROP TABLE IF EXISTS knowledge_links`);
906
+ db.exec(`DROP TABLE IF EXISTS ${ftsTable}`);
907
+ try {
908
+ db.exec(`DROP TABLE IF EXISTS chunks_vec`);
909
+ } catch {
910
+ }
911
+ }
912
+ return storedVersion > 0;
722
913
  }
723
914
  function ensureColumn(db, table, column, definition) {
724
915
  const rows = db.prepare(`PRAGMA table_info(${table})`).all();
@@ -726,6 +917,367 @@ function ensureColumn(db, table, column, definition) {
726
917
  db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
727
918
  }
728
919
 
920
+ // src/session.ts
921
+ import * as os3 from "os";
922
+ function parseFrontmatter(content) {
923
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
924
+ const match = content.match(frontmatterRegex);
925
+ if (!match) {
926
+ return { frontmatter: void 0, body: content };
927
+ }
928
+ const yamlContent = match[1];
929
+ const body = content.slice(match[0].length);
930
+ try {
931
+ const frontmatter = parseSimpleYaml(yamlContent);
932
+ return { frontmatter, body };
933
+ } catch {
934
+ return { frontmatter: void 0, body: content };
935
+ }
936
+ }
937
+ function parseSimpleYaml(yaml) {
938
+ const lines = yaml.split("\n");
939
+ return parseYamlBlock(lines, 0, 0, lines.length).value;
940
+ }
941
+ function parseYamlBlock(lines, indent, startIdx, endIdx) {
942
+ const result = {};
943
+ let i = startIdx;
944
+ while (i < endIdx) {
945
+ const line = lines[i];
946
+ if (!line || !line.trim()) {
947
+ i++;
948
+ continue;
949
+ }
950
+ const lineIndent = getIndent(line);
951
+ if (lineIndent < indent) break;
952
+ if (lineIndent > indent) {
953
+ i++;
954
+ continue;
955
+ }
956
+ const keyMatch = line.match(/^(\s*)([\w-]+):\s*(.*)?$/);
957
+ if (!keyMatch) {
958
+ i++;
959
+ continue;
960
+ }
961
+ const [, , key, rawValue] = keyMatch;
962
+ const value = rawValue?.trim() ?? "";
963
+ if (value === "" || value === void 0) {
964
+ const nextNonEmpty = findNextNonEmptyLine(lines, i + 1, endIdx);
965
+ if (nextNonEmpty < endIdx) {
966
+ const nextLine = lines[nextNonEmpty];
967
+ const nextIndent = getIndent(nextLine);
968
+ if (nextIndent > indent) {
969
+ if (nextLine.trimStart().startsWith("- ")) {
970
+ const listResult = parseYamlList(lines, nextIndent, i + 1, endIdx);
971
+ result[key] = listResult.value;
972
+ i = listResult.nextIdx;
973
+ } else {
974
+ const blockResult = parseYamlBlock(lines, nextIndent, i + 1, endIdx);
975
+ result[key] = blockResult.value;
976
+ i = blockResult.nextIdx;
977
+ }
978
+ continue;
979
+ }
980
+ }
981
+ result[key] = null;
982
+ i++;
983
+ } else {
984
+ result[key] = parseYamlValue(value);
985
+ i++;
986
+ }
987
+ }
988
+ return { value: result, nextIdx: i };
989
+ }
990
+ function parseYamlList(lines, indent, startIdx, endIdx) {
991
+ const result = [];
992
+ let i = startIdx;
993
+ while (i < endIdx) {
994
+ const line = lines[i];
995
+ if (!line || !line.trim()) {
996
+ i++;
997
+ continue;
998
+ }
999
+ const lineIndent = getIndent(line);
1000
+ if (lineIndent < indent) break;
1001
+ if (lineIndent > indent) {
1002
+ i++;
1003
+ continue;
1004
+ }
1005
+ const trimmed = line.trimStart();
1006
+ if (!trimmed.startsWith("- ")) break;
1007
+ const itemContent = trimmed.slice(2).trim();
1008
+ if (itemContent === "" || itemContent === void 0) {
1009
+ const nextNonEmpty = findNextNonEmptyLine(lines, i + 1, endIdx);
1010
+ if (nextNonEmpty < endIdx) {
1011
+ const nextIndent = getIndent(lines[nextNonEmpty]);
1012
+ if (nextIndent > indent) {
1013
+ const blockResult = parseYamlBlock(lines, nextIndent, i + 1, endIdx);
1014
+ result.push(blockResult.value);
1015
+ i = blockResult.nextIdx;
1016
+ continue;
1017
+ }
1018
+ }
1019
+ result.push(null);
1020
+ i++;
1021
+ } else {
1022
+ const kvMatch = itemContent.match(/^([\w-]+):\s*(.*)$/);
1023
+ if (kvMatch) {
1024
+ const obj = {};
1025
+ const [, firstKey, firstVal] = kvMatch;
1026
+ obj[firstKey] = parseYamlValue(firstVal?.trim() ?? "");
1027
+ const itemKeyIndent = indent + 2;
1028
+ let j = i + 1;
1029
+ while (j < endIdx) {
1030
+ const nextLine = lines[j];
1031
+ if (!nextLine || !nextLine.trim()) {
1032
+ j++;
1033
+ continue;
1034
+ }
1035
+ const nextLineIndent = getIndent(nextLine);
1036
+ if (nextLineIndent < itemKeyIndent) break;
1037
+ if (nextLineIndent === itemKeyIndent) {
1038
+ const nextKv = nextLine.match(/^\s*([\w-]+):\s*(.*)$/);
1039
+ if (nextKv) {
1040
+ const [, nk, nv] = nextKv;
1041
+ obj[nk] = parseYamlValue(nv?.trim() ?? "");
1042
+ j++;
1043
+ continue;
1044
+ }
1045
+ }
1046
+ break;
1047
+ }
1048
+ result.push(obj);
1049
+ i = j;
1050
+ } else {
1051
+ result.push(parseYamlValue(itemContent));
1052
+ i++;
1053
+ }
1054
+ }
1055
+ }
1056
+ return { value: result, nextIdx: i };
1057
+ }
1058
+ function getIndent(line) {
1059
+ const match = line.match(/^(\s*)/);
1060
+ return match ? match[1].length : 0;
1061
+ }
1062
+ function findNextNonEmptyLine(lines, from, end) {
1063
+ for (let i = from; i < end; i++) {
1064
+ if (lines[i]?.trim()) return i;
1065
+ }
1066
+ return end;
1067
+ }
1068
+ function parseYamlValue(value) {
1069
+ if (value === "") return null;
1070
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1071
+ return value.slice(1, -1);
1072
+ }
1073
+ if (value === "null" || value === "~") return null;
1074
+ if (value === "true") return true;
1075
+ if (value === "false") return false;
1076
+ const num = Number(value);
1077
+ if (!isNaN(num) && value !== "") return num;
1078
+ if (value.startsWith("[") && value.endsWith("]")) {
1079
+ const inner = value.slice(1, -1);
1080
+ if (inner.trim() === "") return [];
1081
+ return inner.split(",").map((s) => parseYamlValue(s.trim()));
1082
+ }
1083
+ return value;
1084
+ }
1085
+ function serializeFrontmatter(frontmatter) {
1086
+ const lines = ["---"];
1087
+ if (frontmatter.id) {
1088
+ lines.push(`id: ${frontmatter.id}`);
1089
+ }
1090
+ if (frontmatter.type) {
1091
+ lines.push(`type: ${frontmatter.type}`);
1092
+ }
1093
+ if (frontmatter.session) {
1094
+ lines.push("session:");
1095
+ const session = frontmatter.session;
1096
+ if (session.id) lines.push(` id: ${session.id}`);
1097
+ if (session.source) lines.push(` source: ${session.source}`);
1098
+ if (session.project) lines.push(` project: ${formatPath2(session.project)}`);
1099
+ if (session.transcript) lines.push(` transcript: ${formatPath2(session.transcript)}`);
1100
+ }
1101
+ if (frontmatter.created) {
1102
+ lines.push(`created: ${frontmatter.created}`);
1103
+ }
1104
+ if (frontmatter.updated) {
1105
+ lines.push(`updated: ${frontmatter.updated}`);
1106
+ }
1107
+ if (frontmatter.tags && frontmatter.tags.length > 0) {
1108
+ lines.push(`tags: [${frontmatter.tags.join(", ")}]`);
1109
+ }
1110
+ if (frontmatter.domain && frontmatter.domain.length > 0) {
1111
+ lines.push(`domain: [${frontmatter.domain.join(", ")}]`);
1112
+ }
1113
+ if (frontmatter.entities && frontmatter.entities.length > 0) {
1114
+ lines.push(`entities: [${frontmatter.entities.join(", ")}]`);
1115
+ }
1116
+ if (frontmatter.confidence !== void 0) {
1117
+ lines.push(`confidence: ${frontmatter.confidence}`);
1118
+ }
1119
+ if (frontmatter.source) {
1120
+ lines.push("source:");
1121
+ if (frontmatter.source.origin) lines.push(` origin: ${frontmatter.source.origin}`);
1122
+ if (frontmatter.source.trajectories && frontmatter.source.trajectories.length > 0) {
1123
+ lines.push(` trajectories: [${frontmatter.source.trajectories.join(", ")}]`);
1124
+ }
1125
+ if (frontmatter.source.agentId) lines.push(` agentId: ${frontmatter.source.agentId}`);
1126
+ }
1127
+ if (frontmatter.links && frontmatter.links.length > 0) {
1128
+ lines.push("links:");
1129
+ for (const link of frontmatter.links) {
1130
+ lines.push(` - target: ${link.target}`);
1131
+ lines.push(` relation: ${link.relation}`);
1132
+ if (link.layer) lines.push(` layer: ${link.layer}`);
1133
+ }
1134
+ }
1135
+ if (frontmatter.supersedes !== void 0) {
1136
+ lines.push(`supersedes: ${frontmatter.supersedes === null ? "~" : frontmatter.supersedes}`);
1137
+ }
1138
+ lines.push("---");
1139
+ return lines.join("\n") + "\n";
1140
+ }
1141
+ function addFrontmatter(content, frontmatter) {
1142
+ const { frontmatter: existing, body } = parseFrontmatter(content);
1143
+ const merged = {
1144
+ ...existing,
1145
+ ...frontmatter,
1146
+ session: {
1147
+ ...existing?.session,
1148
+ ...frontmatter.session
1149
+ }
1150
+ };
1151
+ if (!merged.created) {
1152
+ merged.created = (/* @__PURE__ */ new Date()).toISOString();
1153
+ }
1154
+ merged.updated = (/* @__PURE__ */ new Date()).toISOString();
1155
+ return serializeFrontmatter(merged) + body;
1156
+ }
1157
+ function formatPath2(filePath) {
1158
+ const home = os3.homedir();
1159
+ if (filePath.startsWith(home)) {
1160
+ return "~" + filePath.slice(home.length);
1161
+ }
1162
+ return filePath;
1163
+ }
1164
+
1165
+ // src/search/graph.ts
1166
+ function getLinksFrom(db, fromId, opts) {
1167
+ let sql = `SELECT from_id, to_id, relation, layer, weight, source_path FROM knowledge_links WHERE from_id = ?`;
1168
+ const params = [fromId];
1169
+ if (opts?.relation) {
1170
+ sql += ` AND relation = ?`;
1171
+ params.push(opts.relation);
1172
+ }
1173
+ if (opts?.layer) {
1174
+ sql += ` AND layer = ?`;
1175
+ params.push(opts.layer);
1176
+ }
1177
+ const rows = db.prepare(sql).all(...params);
1178
+ return rows.map(toGraphLink);
1179
+ }
1180
+ function getLinksTo(db, toId, opts) {
1181
+ let sql = `SELECT from_id, to_id, relation, layer, weight, source_path FROM knowledge_links WHERE to_id = ?`;
1182
+ const params = [toId];
1183
+ if (opts?.relation) {
1184
+ sql += ` AND relation = ?`;
1185
+ params.push(opts.relation);
1186
+ }
1187
+ if (opts?.layer) {
1188
+ sql += ` AND layer = ?`;
1189
+ params.push(opts.layer);
1190
+ }
1191
+ const rows = db.prepare(sql).all(...params);
1192
+ return rows.map(toGraphLink);
1193
+ }
1194
+ function getNeighbors(db, startId, depth = 1, opts) {
1195
+ const visited = /* @__PURE__ */ new Set([startId]);
1196
+ const result = [];
1197
+ let frontier = [startId];
1198
+ for (let d = 1; d <= depth; d++) {
1199
+ const nextFrontier = [];
1200
+ for (const nodeId of frontier) {
1201
+ const outgoing = getLinksFrom(db, nodeId, opts);
1202
+ for (const link of outgoing) {
1203
+ if (!visited.has(link.toId)) {
1204
+ visited.add(link.toId);
1205
+ nextFrontier.push(link.toId);
1206
+ result.push({ id: link.toId, depth: d, link });
1207
+ }
1208
+ }
1209
+ const incoming = getLinksTo(db, nodeId, opts);
1210
+ for (const link of incoming) {
1211
+ if (!visited.has(link.fromId)) {
1212
+ visited.add(link.fromId);
1213
+ nextFrontier.push(link.fromId);
1214
+ result.push({ id: link.fromId, depth: d, link });
1215
+ }
1216
+ }
1217
+ }
1218
+ frontier = nextFrontier;
1219
+ if (frontier.length === 0) break;
1220
+ }
1221
+ return result;
1222
+ }
1223
+ function getPathBetween(db, fromId, toId, maxDepth = 3) {
1224
+ if (fromId === toId) return [];
1225
+ const visited = /* @__PURE__ */ new Set([fromId]);
1226
+ const parentLink = /* @__PURE__ */ new Map();
1227
+ let frontier = [fromId];
1228
+ for (let d = 0; d < maxDepth; d++) {
1229
+ const nextFrontier = [];
1230
+ for (const nodeId of frontier) {
1231
+ const outgoing = getLinksFrom(db, nodeId);
1232
+ for (const link of outgoing) {
1233
+ if (!visited.has(link.toId)) {
1234
+ visited.add(link.toId);
1235
+ parentLink.set(link.toId, link);
1236
+ if (link.toId === toId) {
1237
+ return reconstructPath(parentLink, fromId, toId);
1238
+ }
1239
+ nextFrontier.push(link.toId);
1240
+ }
1241
+ }
1242
+ const incoming = getLinksTo(db, nodeId);
1243
+ for (const link of incoming) {
1244
+ if (!visited.has(link.fromId)) {
1245
+ visited.add(link.fromId);
1246
+ parentLink.set(link.fromId, link);
1247
+ if (link.fromId === toId) {
1248
+ return reconstructPath(parentLink, fromId, toId);
1249
+ }
1250
+ nextFrontier.push(link.fromId);
1251
+ }
1252
+ }
1253
+ }
1254
+ frontier = nextFrontier;
1255
+ if (frontier.length === 0) break;
1256
+ }
1257
+ return [];
1258
+ }
1259
+ function reconstructPath(parentLink, fromId, toId) {
1260
+ const path20 = [];
1261
+ let current = toId;
1262
+ while (current !== fromId) {
1263
+ const link = parentLink.get(current);
1264
+ if (!link) break;
1265
+ path20.unshift(link);
1266
+ current = link.toId === current ? link.fromId : link.toId;
1267
+ }
1268
+ return path20;
1269
+ }
1270
+ function toGraphLink(row) {
1271
+ return {
1272
+ fromId: row.from_id,
1273
+ toId: row.to_id,
1274
+ relation: row.relation,
1275
+ layer: row.layer,
1276
+ weight: row.weight,
1277
+ sourcePath: row.source_path
1278
+ };
1279
+ }
1280
+
729
1281
  // src/db/sqlite-vec.ts
730
1282
  async function loadSqliteVecExtension(params) {
731
1283
  try {
@@ -747,8 +1299,8 @@ async function loadSqliteVecExtension(params) {
747
1299
 
748
1300
  // src/embeddings/embeddings.ts
749
1301
  import fsSync2 from "fs";
750
- import path4 from "path";
751
- import os2 from "os";
1302
+ import path5 from "path";
1303
+ import os4 from "os";
752
1304
  var DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
753
1305
  var DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-small";
754
1306
  var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
@@ -764,7 +1316,7 @@ function createNoOpEmbeddingProvider() {
764
1316
  }
765
1317
  function resolveUserPath(filePath) {
766
1318
  if (filePath.startsWith("~/")) {
767
- return path4.join(os2.homedir(), filePath.slice(2));
1319
+ return path5.join(os4.homedir(), filePath.slice(2));
768
1320
  }
769
1321
  return filePath;
770
1322
  }
@@ -975,7 +1527,6 @@ async function createEmbeddingProvider(options) {
975
1527
  return {
976
1528
  provider: createNoOpEmbeddingProvider(),
977
1529
  requestedProvider: "none"
978
- // Type coercion for compatibility
979
1530
  };
980
1531
  }
981
1532
  const createProvider = async (id) => {
@@ -1019,7 +1570,6 @@ async function createEmbeddingProvider(options) {
1019
1570
  provider: createNoOpEmbeddingProvider(),
1020
1571
  requestedProvider,
1021
1572
  fallbackFrom: "auto",
1022
- // Indicate this is a fallback
1023
1573
  fallbackReason: "No embedding API available. Using BM25 full-text search only."
1024
1574
  };
1025
1575
  }
@@ -1114,7 +1664,7 @@ async function retryAsync(fn, opts) {
1114
1664
  opts.maxDelayMs,
1115
1665
  opts.minDelayMs * Math.pow(2, attempt) * (1 + Math.random() * opts.jitter)
1116
1666
  );
1117
- await new Promise((resolve4) => setTimeout(resolve4, delay));
1667
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
1118
1668
  }
1119
1669
  }
1120
1670
  throw lastError;
@@ -1248,7 +1798,7 @@ async function waitForOpenAiBatch(params) {
1248
1798
  throw new Error(`openai batch ${params.batchId} timed out after ${params.timeoutMs}ms`);
1249
1799
  }
1250
1800
  params.debug?.(`openai batch ${params.batchId} ${state}; waiting ${params.pollIntervalMs}ms`);
1251
- await new Promise((resolve4) => setTimeout(resolve4, params.pollIntervalMs));
1801
+ await new Promise((resolve3) => setTimeout(resolve3, params.pollIntervalMs));
1252
1802
  current = void 0;
1253
1803
  }
1254
1804
  }
@@ -1538,7 +2088,7 @@ async function waitForGeminiBatch(params) {
1538
2088
  throw new Error(`gemini batch ${params.batchName} timed out after ${params.timeoutMs}ms`);
1539
2089
  }
1540
2090
  params.debug?.(`gemini batch ${params.batchName} ${state}; waiting ${params.pollIntervalMs}ms`);
1541
- await new Promise((resolve4) => setTimeout(resolve4, params.pollIntervalMs));
2091
+ await new Promise((resolve3) => setTimeout(resolve3, params.pollIntervalMs));
1542
2092
  current = void 0;
1543
2093
  }
1544
2094
  }
@@ -1662,7 +2212,6 @@ var EMBEDDING_RETRY_BASE_DELAY_MS = 500;
1662
2212
  var EMBEDDING_RETRY_MAX_DELAY_MS = 8e3;
1663
2213
  var EMBEDDING_QUERY_TIMEOUT_REMOTE_MS = 6e4;
1664
2214
  var EMBEDDING_QUERY_TIMEOUT_LOCAL_MS = 5 * 6e4;
1665
- var vectorToBlob2 = (embedding) => Buffer.from(new Float32Array(embedding).buffer);
1666
2215
  var Minimem = class _Minimem {
1667
2216
  memoryDir;
1668
2217
  dbPath;
@@ -1688,10 +2237,11 @@ var Minimem = class _Minimem {
1688
2237
  closed = false;
1689
2238
  dirty = true;
1690
2239
  syncing = null;
2240
+ syncLock = false;
1691
2241
  embeddingOptions;
1692
2242
  constructor(config2) {
1693
- this.memoryDir = path5.resolve(config2.memoryDir);
1694
- this.dbPath = config2.dbPath ?? path5.join(this.memoryDir, ".minimem", "index.db");
2243
+ this.memoryDir = path6.resolve(config2.memoryDir);
2244
+ this.dbPath = config2.dbPath ?? path6.join(this.memoryDir, ".minimem", "index.db");
1695
2245
  this.chunking = {
1696
2246
  tokens: config2.chunking?.tokens ?? 256,
1697
2247
  overlap: config2.chunking?.overlap ?? 32
@@ -1757,7 +2307,7 @@ var Minimem = class _Minimem {
1757
2307
  }
1758
2308
  }
1759
2309
  openDatabase() {
1760
- const dbDir = path5.dirname(this.dbPath);
2310
+ const dbDir = path6.dirname(this.dbPath);
1761
2311
  ensureDir(dbDir);
1762
2312
  return new DatabaseSync(this.dbPath);
1763
2313
  }
@@ -1797,8 +2347,8 @@ var Minimem = class _Minimem {
1797
2347
  }
1798
2348
  ensureWatcher() {
1799
2349
  if (this.watcher) return;
1800
- const memorySubDir = path5.join(this.memoryDir, "memory");
1801
- const memoryFile = path5.join(this.memoryDir, "MEMORY.md");
2350
+ const memorySubDir = path6.join(this.memoryDir, "memory");
2351
+ const memoryFile = path6.join(this.memoryDir, "MEMORY.md");
1802
2352
  this.watcher = chokidar.watch([memoryFile, memorySubDir], {
1803
2353
  ignoreInitial: true,
1804
2354
  persistent: true,
@@ -1831,7 +2381,7 @@ var Minimem = class _Minimem {
1831
2381
  }
1832
2382
  const storedMap = new Map(stored.map((f) => [f.path, f.mtime]));
1833
2383
  for (const absPath of files) {
1834
- const relPath = path5.relative(this.memoryDir, absPath).replace(/\\/g, "/");
2384
+ const relPath = path6.relative(this.memoryDir, absPath).replace(/\\/g, "/");
1835
2385
  const storedMtime = storedMap.get(relPath);
1836
2386
  if (storedMtime === void 0) {
1837
2387
  this.debug?.(`Stale: new file ${relPath}`);
@@ -1887,8 +2437,14 @@ var Minimem = class _Minimem {
1887
2437
  sourceFilterVec: sourceFilter,
1888
2438
  sourceFilterChunks: sourceFilter
1889
2439
  }).catch(() => []) : [];
2440
+ const typeFilterFn = opts?.type ? (id) => {
2441
+ const row = this.db.prepare(`SELECT type FROM chunks WHERE id = ?`).get(id);
2442
+ return row?.type === opts.type;
2443
+ } : void 0;
1890
2444
  if (!this.hybrid.enabled) {
1891
- return vectorResults.filter((entry) => entry.score >= minScore).slice(0, maxResults).map((r) => ({
2445
+ let results = vectorResults;
2446
+ if (typeFilterFn) results = results.filter((r) => typeFilterFn(r.id));
2447
+ return results.filter((entry) => entry.score >= minScore).slice(0, maxResults).map((r) => ({
1892
2448
  path: r.path,
1893
2449
  startLine: r.startLine,
1894
2450
  endLine: r.endLine,
@@ -1896,8 +2452,14 @@ var Minimem = class _Minimem {
1896
2452
  snippet: r.snippet
1897
2453
  }));
1898
2454
  }
2455
+ let filteredVector = vectorResults;
2456
+ let filteredKeyword = keywordResults;
2457
+ if (typeFilterFn) {
2458
+ filteredVector = vectorResults.filter((r) => typeFilterFn(r.id));
2459
+ filteredKeyword = keywordResults.filter((r) => typeFilterFn(r.id));
2460
+ }
1899
2461
  const merged = mergeHybridResults({
1900
- vector: vectorResults.map((r) => ({
2462
+ vector: filteredVector.map((r) => ({
1901
2463
  id: r.id,
1902
2464
  path: r.path,
1903
2465
  startLine: r.startLine,
@@ -1906,7 +2468,7 @@ var Minimem = class _Minimem {
1906
2468
  snippet: r.snippet,
1907
2469
  vectorScore: r.score
1908
2470
  })),
1909
- keyword: keywordResults.map((r) => ({
2471
+ keyword: filteredKeyword.map((r) => ({
1910
2472
  id: r.id,
1911
2473
  path: r.path,
1912
2474
  startLine: r.startLine,
@@ -1931,11 +2493,16 @@ var Minimem = class _Minimem {
1931
2493
  await this.syncing;
1932
2494
  return;
1933
2495
  }
2496
+ if (this.syncLock) {
2497
+ return;
2498
+ }
2499
+ this.syncLock = true;
1934
2500
  this.syncing = this.runSync(opts);
1935
2501
  try {
1936
2502
  await this.syncing;
1937
2503
  } finally {
1938
2504
  this.syncing = null;
2505
+ this.syncLock = false;
1939
2506
  }
1940
2507
  }
1941
2508
  async runSync(opts) {
@@ -1962,13 +2529,16 @@ var Minimem = class _Minimem {
1962
2529
  this.db.prepare(
1963
2530
  `DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM chunks WHERE path = ? AND source = ?)`
1964
2531
  ).run(stale.path, "memory");
1965
- } catch {
2532
+ } catch (err) {
2533
+ logError2("deleteStaleVectorEntries", err, this.debug);
1966
2534
  }
1967
2535
  this.db.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`).run(stale.path, "memory");
2536
+ this.db.prepare(`DELETE FROM knowledge_links WHERE source_path = ?`).run(stale.path);
1968
2537
  if (this.fts.enabled && this.fts.available) {
1969
2538
  try {
1970
2539
  this.db.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`).run(stale.path, "memory", this.provider.model);
1971
- } catch {
2540
+ } catch (err) {
2541
+ logError2("deleteStaleFtsEntries", err, this.debug);
1972
2542
  }
1973
2543
  }
1974
2544
  }
@@ -1987,6 +2557,13 @@ var Minimem = class _Minimem {
1987
2557
  async indexFile(entry) {
1988
2558
  const content = await fs4.readFile(entry.absPath, "utf-8");
1989
2559
  const chunks = chunkMarkdown(content, this.chunking);
2560
+ const { frontmatter } = parseFrontmatter(content);
2561
+ const knowledgeType = frontmatter?.type ?? null;
2562
+ const knowledgeId = frontmatter?.id ?? null;
2563
+ const domains = frontmatter?.domain ?? null;
2564
+ const entities = frontmatter?.entities ?? null;
2565
+ const confidence = frontmatter?.confidence ?? null;
2566
+ const links = frontmatter?.links ?? null;
1990
2567
  const embeddings = await this.embedChunks(chunks);
1991
2568
  this.db.prepare(
1992
2569
  `INSERT OR REPLACE INTO files (path, source, hash, mtime, size) VALUES (?, ?, ?, ?, ?)`
@@ -1995,23 +2572,27 @@ var Minimem = class _Minimem {
1995
2572
  this.db.prepare(
1996
2573
  `DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM chunks WHERE path = ? AND source = ?)`
1997
2574
  ).run(entry.path, "memory");
1998
- } catch {
2575
+ } catch (err) {
2576
+ logError2("deleteOldVectorChunks", err, this.debug);
1999
2577
  }
2000
2578
  this.db.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`).run(entry.path, "memory");
2001
2579
  if (this.fts.enabled && this.fts.available) {
2002
2580
  try {
2003
2581
  this.db.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`).run(entry.path, "memory", this.provider.model);
2004
- } catch {
2582
+ } catch (err) {
2583
+ logError2("deleteOldFtsChunks", err, this.debug);
2005
2584
  }
2006
2585
  }
2586
+ this.db.prepare(`DELETE FROM knowledge_links WHERE source_path = ?`).run(entry.path);
2007
2587
  const now = Date.now();
2008
2588
  for (let i = 0; i < chunks.length; i++) {
2009
2589
  const chunk = chunks[i];
2010
2590
  const embedding = embeddings[i] ?? [];
2011
2591
  const chunkId = randomUUID();
2592
+ const meta = extractChunkMetadata(chunk.text);
2012
2593
  this.db.prepare(
2013
- `INSERT INTO chunks (id, path, source, start_line, end_line, hash, model, text, embedding, updated_at)
2014
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2594
+ `INSERT INTO chunks (id, path, source, start_line, end_line, hash, model, text, embedding, updated_at, type, knowledge_type, knowledge_id, domains, entities, confidence)
2595
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2015
2596
  ).run(
2016
2597
  chunkId,
2017
2598
  entry.path,
@@ -2022,7 +2603,13 @@ var Minimem = class _Minimem {
2022
2603
  this.provider.model,
2023
2604
  chunk.text,
2024
2605
  JSON.stringify(embedding),
2025
- now
2606
+ now,
2607
+ meta.type ?? null,
2608
+ knowledgeType,
2609
+ knowledgeId,
2610
+ domains ? JSON.stringify(domains) : null,
2611
+ entities ? JSON.stringify(entities) : null,
2612
+ confidence
2026
2613
  );
2027
2614
  if (this.vector.available && embedding.length > 0) {
2028
2615
  if (!this.vector.dims) {
@@ -2030,8 +2617,9 @@ var Minimem = class _Minimem {
2030
2617
  this.ensureVectorTable(embedding.length);
2031
2618
  }
2032
2619
  try {
2033
- this.db.prepare(`INSERT INTO ${VECTOR_TABLE} (id, embedding) VALUES (?, ?)`).run(chunkId, vectorToBlob2(embedding));
2034
- } catch {
2620
+ this.db.prepare(`INSERT INTO ${VECTOR_TABLE} (id, embedding) VALUES (?, ?)`).run(chunkId, vectorToBlob(embedding));
2621
+ } catch (err) {
2622
+ logError2("insertVectorChunk", err, this.debug);
2035
2623
  }
2036
2624
  }
2037
2625
  if (this.fts.enabled && this.fts.available) {
@@ -2048,10 +2636,28 @@ var Minimem = class _Minimem {
2048
2636
  chunk.startLine,
2049
2637
  chunk.endLine
2050
2638
  );
2051
- } catch {
2639
+ } catch (err) {
2640
+ logError2("insertFtsChunk", err, this.debug);
2052
2641
  }
2053
2642
  }
2054
2643
  }
2644
+ if (links && knowledgeId) {
2645
+ const upsertLink = this.db.prepare(
2646
+ `INSERT OR REPLACE INTO knowledge_links (from_id, to_id, relation, layer, weight, source_path, created_at)
2647
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
2648
+ );
2649
+ for (const link of links) {
2650
+ upsertLink.run(
2651
+ knowledgeId,
2652
+ link.target,
2653
+ link.relation,
2654
+ link.layer ?? null,
2655
+ 0.5,
2656
+ entry.path,
2657
+ now
2658
+ );
2659
+ }
2660
+ }
2055
2661
  }
2056
2662
  async embedChunks(chunks) {
2057
2663
  if (chunks.length === 0) return [];
@@ -2095,7 +2701,7 @@ var Minimem = class _Minimem {
2095
2701
  EMBEDDING_RETRY_MAX_DELAY_MS,
2096
2702
  EMBEDDING_RETRY_BASE_DELAY_MS * Math.pow(2, attempt)
2097
2703
  );
2098
- await new Promise((resolve4) => setTimeout(resolve4, delay));
2704
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
2099
2705
  }
2100
2706
  }
2101
2707
  }
@@ -2143,12 +2749,22 @@ var Minimem = class _Minimem {
2143
2749
  }
2144
2750
  async embedQueryWithTimeout(text) {
2145
2751
  const timeout = this.provider.id === "local" ? EMBEDDING_QUERY_TIMEOUT_LOCAL_MS : EMBEDDING_QUERY_TIMEOUT_REMOTE_MS;
2146
- return Promise.race([
2147
- this.provider.embedQuery(text),
2148
- new Promise(
2149
- (_, reject) => setTimeout(() => reject(new Error("embedding query timeout")), timeout)
2150
- )
2151
- ]);
2752
+ const ac = new AbortController();
2753
+ const timer = setTimeout(() => ac.abort(), timeout);
2754
+ try {
2755
+ const result = await Promise.race([
2756
+ this.provider.embedQuery(text),
2757
+ new Promise((_, reject) => {
2758
+ ac.signal.addEventListener(
2759
+ "abort",
2760
+ () => reject(new Error("embedding query timeout"))
2761
+ );
2762
+ })
2763
+ ]);
2764
+ return result;
2765
+ } finally {
2766
+ clearTimeout(timer);
2767
+ }
2152
2768
  }
2153
2769
  loadEmbeddingCache(hashes) {
2154
2770
  const result = /* @__PURE__ */ new Map();
@@ -2241,7 +2857,7 @@ var Minimem = class _Minimem {
2241
2857
  }
2242
2858
  }
2243
2859
  async readFile(relativePath) {
2244
- const absPath = path5.join(this.memoryDir, relativePath);
2860
+ const absPath = path6.join(this.memoryDir, relativePath);
2245
2861
  try {
2246
2862
  return await fs4.readFile(absPath, "utf-8");
2247
2863
  } catch {
@@ -2271,8 +2887,8 @@ var Minimem = class _Minimem {
2271
2887
  */
2272
2888
  async writeFile(relativePath, content) {
2273
2889
  this.validateMemoryPath(relativePath);
2274
- const absPath = path5.join(this.memoryDir, relativePath);
2275
- const dir = path5.dirname(absPath);
2890
+ const absPath = path6.join(this.memoryDir, relativePath);
2891
+ const dir = path6.dirname(absPath);
2276
2892
  await fs4.mkdir(dir, { recursive: true });
2277
2893
  await fs4.writeFile(absPath, content, "utf-8");
2278
2894
  this.dirty = true;
@@ -2283,8 +2899,8 @@ var Minimem = class _Minimem {
2283
2899
  */
2284
2900
  async appendFile(relativePath, content) {
2285
2901
  this.validateMemoryPath(relativePath);
2286
- const absPath = path5.join(this.memoryDir, relativePath);
2287
- const dir = path5.dirname(absPath);
2902
+ const absPath = path6.join(this.memoryDir, relativePath);
2903
+ const dir = path6.dirname(absPath);
2288
2904
  await fs4.mkdir(dir, { recursive: true });
2289
2905
  let toAppend = content;
2290
2906
  try {
@@ -2312,7 +2928,7 @@ var Minimem = class _Minimem {
2312
2928
  */
2313
2929
  async listFiles() {
2314
2930
  const files = await listMemoryFiles(this.memoryDir);
2315
- return files.map((f) => path5.relative(this.memoryDir, f).replace(/\\/g, "/"));
2931
+ return files.map((f) => path6.relative(this.memoryDir, f).replace(/\\/g, "/"));
2316
2932
  }
2317
2933
  /**
2318
2934
  * Validate that a path is within allowed memory locations
@@ -2350,6 +2966,70 @@ var Minimem = class _Minimem {
2350
2966
  cacheCount: cacheRow.count
2351
2967
  };
2352
2968
  }
2969
+ /**
2970
+ * Search with knowledge metadata filters (domain, entities, confidence, type).
2971
+ * Runs a standard search then post-filters by knowledge columns.
2972
+ */
2973
+ async knowledgeSearch(query, opts) {
2974
+ if (this.dirty || !this.watchConfig.enabled && await this.isStale()) {
2975
+ await this.sync({ reason: "knowledgeSearch" });
2976
+ }
2977
+ const cleaned = query.trim();
2978
+ if (!cleaned) return [];
2979
+ const minScore = opts?.minScore ?? this.queryConfig.minScore;
2980
+ const maxResults = opts?.maxResults ?? this.queryConfig.maxResults;
2981
+ const { sql: knowledgeWhere, params: knowledgeParams } = buildKnowledgeFilterSql({
2982
+ domain: opts?.domain,
2983
+ entities: opts?.entities,
2984
+ minConfidence: opts?.minConfidence,
2985
+ knowledgeType: opts?.knowledgeType
2986
+ });
2987
+ if (!knowledgeWhere) {
2988
+ return this.search(query, { maxResults, minScore });
2989
+ }
2990
+ const matchingRows = this.db.prepare(
2991
+ `SELECT id FROM chunks c WHERE c.model = ? AND c.source = 'memory'${knowledgeWhere}`
2992
+ ).all(this.provider.model, ...knowledgeParams);
2993
+ const matchingIds = new Set(matchingRows.map((r) => r.id));
2994
+ if (matchingIds.size === 0) return [];
2995
+ const overFetch = Math.max(maxResults * 3, 30);
2996
+ const results = await this.search(query, {
2997
+ maxResults: overFetch,
2998
+ minScore
2999
+ });
3000
+ const filtered = [];
3001
+ for (const r of results) {
3002
+ const row = this.db.prepare(
3003
+ `SELECT id FROM chunks WHERE path = ? AND start_line = ? AND end_line = ? AND model = ?`
3004
+ ).get(r.path, r.startLine, r.endLine, this.provider.model);
3005
+ if (row && matchingIds.has(row.id)) {
3006
+ filtered.push(r);
3007
+ if (filtered.length >= maxResults) break;
3008
+ }
3009
+ }
3010
+ return filtered;
3011
+ }
3012
+ /**
3013
+ * Get knowledge graph links from or to a node.
3014
+ */
3015
+ getLinks(nodeId, direction = "from", opts) {
3016
+ if (direction === "from") {
3017
+ return getLinksFrom(this.db, nodeId, opts);
3018
+ }
3019
+ return getLinksTo(this.db, nodeId, opts);
3020
+ }
3021
+ /**
3022
+ * Get neighbor nodes via BFS traversal.
3023
+ */
3024
+ getGraphNeighbors(nodeId, depth = 1, opts) {
3025
+ return getNeighbors(this.db, nodeId, depth, opts);
3026
+ }
3027
+ /**
3028
+ * Find shortest path between two knowledge nodes.
3029
+ */
3030
+ getGraphPath(fromId, toId, maxDepth = 3) {
3031
+ return getPathBetween(this.db, fromId, toId, maxDepth);
3032
+ }
2353
3033
  close() {
2354
3034
  if (this.closed) return;
2355
3035
  this.closed = true;
@@ -2363,30 +3043,31 @@ var Minimem = class _Minimem {
2363
3043
  }
2364
3044
  try {
2365
3045
  this.db.close();
2366
- } catch {
3046
+ } catch (err) {
3047
+ logError2("dbClose", err, this.debug);
2367
3048
  }
2368
3049
  }
2369
3050
  };
2370
3051
 
2371
3052
  // src/cli/commands/search.ts
2372
3053
  async function search(query, options) {
2373
- const directories = resolveSearchDirectories(options);
3054
+ const directories = resolveMemoryDirs(options);
2374
3055
  if (directories.length === 0) {
2375
- console.error("Error: No memory directories specified.");
2376
- console.error("Use --dir <path> or --global to specify directories to search.");
2377
- process.exit(1);
3056
+ exitWithError(
3057
+ "No memory directories specified.",
3058
+ "Use --dir <path> or --global to specify directories to search."
3059
+ );
2378
3060
  }
2379
3061
  const validDirs = [];
2380
3062
  for (const dir of directories) {
2381
3063
  if (await isInitialized(dir)) {
2382
3064
  validDirs.push(dir);
2383
3065
  } else {
2384
- console.error(`Warning: ${formatPath(dir)} is not initialized, skipping.`);
3066
+ warn(`${formatPath(dir)} is not initialized, skipping.`);
2385
3067
  }
2386
3068
  }
2387
3069
  if (validDirs.length === 0) {
2388
- console.error("Error: No valid initialized memory directories found.");
2389
- process.exit(1);
3070
+ exitWithError("No valid initialized memory directories found.");
2390
3071
  }
2391
3072
  const maxResults = options.max ? parseInt(options.max, 10) : 10;
2392
3073
  const minScore = options.minScore ? parseFloat(options.minScore) : void 0;
@@ -2405,8 +3086,8 @@ async function search(query, options) {
2405
3086
  if (!warnedBm25) {
2406
3087
  const status2 = await minimem.status();
2407
3088
  if (status2.bm25Only) {
2408
- console.error("Note: Running in BM25-only mode (no embedding API configured).");
2409
- console.error(" Results are based on keyword matching only.\n");
3089
+ note("Running in BM25-only mode (no embedding API configured).");
3090
+ note("Results are based on keyword matching only.\n");
2410
3091
  warnedBm25 = true;
2411
3092
  }
2412
3093
  }
@@ -2450,24 +3131,6 @@ async function search(query, options) {
2450
3131
  }
2451
3132
  }
2452
3133
  }
2453
- function resolveSearchDirectories(options) {
2454
- const dirs = [];
2455
- if (options.dir && options.dir.length > 0) {
2456
- for (const dir of options.dir) {
2457
- dirs.push(path6.resolve(dir));
2458
- }
2459
- }
2460
- if (options.global) {
2461
- const globalDir = path6.join(os3.homedir(), ".minimem");
2462
- if (!dirs.includes(globalDir)) {
2463
- dirs.push(globalDir);
2464
- }
2465
- }
2466
- if (dirs.length === 0) {
2467
- dirs.push(process.cwd());
2468
- }
2469
- return dirs;
2470
- }
2471
3134
  function formatSnippet(snippet) {
2472
3135
  const lines = snippet.split("\n");
2473
3136
  const formatted = lines.map((line) => ` ${line}`).join("\n");
@@ -2481,9 +3144,10 @@ function formatSnippet(snippet) {
2481
3144
  async function sync(options) {
2482
3145
  const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
2483
3146
  if (!await isInitialized(memoryDir)) {
2484
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
2485
- console.error(`Run: minimem init${options.dir ? ` ${options.dir}` : ""}`);
2486
- process.exit(1);
3147
+ exitWithError(
3148
+ `${formatPath(memoryDir)} is not initialized.`,
3149
+ `Run: minimem init${options.dir ? ` ${options.dir}` : ""}`
3150
+ );
2487
3151
  }
2488
3152
  console.log(`Syncing ${formatPath(memoryDir)}...`);
2489
3153
  const cliConfig = await loadConfig(memoryDir);
@@ -2519,9 +3183,10 @@ async function sync(options) {
2519
3183
  async function status(options) {
2520
3184
  const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
2521
3185
  if (!await isInitialized(memoryDir)) {
2522
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
2523
- console.error(`Run: minimem init${options.dir ? ` ${options.dir}` : ""}`);
2524
- process.exit(1);
3186
+ exitWithError(
3187
+ `${formatPath(memoryDir)} is not initialized.`,
3188
+ `Run: minimem init${options.dir ? ` ${options.dir}` : ""}`
3189
+ );
2525
3190
  }
2526
3191
  const cliConfig = await loadConfig(memoryDir);
2527
3192
  const config2 = buildMinimemConfig(memoryDir, cliConfig, {
@@ -2575,9 +3240,10 @@ async function status(options) {
2575
3240
  async function append(text, options) {
2576
3241
  const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
2577
3242
  if (!await isInitialized(memoryDir)) {
2578
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
2579
- console.error(`Run: minimem init${options.dir ? ` ${options.dir}` : ""}`);
2580
- process.exit(1);
3243
+ exitWithError(
3244
+ `${formatPath(memoryDir)} is not initialized.`,
3245
+ `Run: minimem init${options.dir ? ` ${options.dir}` : ""}`
3246
+ );
2581
3247
  }
2582
3248
  let finalText = text;
2583
3249
  if (options.session) {
@@ -2613,133 +3279,23 @@ ${text}`;
2613
3279
  // src/cli/commands/upsert.ts
2614
3280
  import * as fs5 from "fs/promises";
2615
3281
  import * as path7 from "path";
2616
- import * as os5 from "os";
2617
-
2618
- // src/session.ts
2619
- import * as os4 from "os";
2620
- function parseFrontmatter(content) {
2621
- const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
2622
- const match = content.match(frontmatterRegex);
2623
- if (!match) {
2624
- return { frontmatter: void 0, body: content };
2625
- }
2626
- const yamlContent = match[1];
2627
- const body = content.slice(match[0].length);
2628
- try {
2629
- const frontmatter = parseSimpleYaml(yamlContent);
2630
- return { frontmatter, body };
2631
- } catch {
2632
- return { frontmatter: void 0, body: content };
2633
- }
2634
- }
2635
- function parseSimpleYaml(yaml) {
2636
- const result = {};
2637
- const lines = yaml.split("\n");
2638
- let currentKey = null;
2639
- let currentObject = null;
2640
- for (const line of lines) {
2641
- if (!line.trim()) continue;
2642
- const indentMatch = line.match(/^(\s*)/);
2643
- const indent = indentMatch ? indentMatch[1].length : 0;
2644
- if (indent === 0) {
2645
- const keyMatch = line.match(/^(\w+):\s*(.*)?$/);
2646
- if (keyMatch) {
2647
- const [, key, value] = keyMatch;
2648
- if (value && value.trim()) {
2649
- result[key] = parseYamlValue(value.trim());
2650
- currentKey = null;
2651
- currentObject = null;
2652
- } else {
2653
- currentKey = key;
2654
- currentObject = {};
2655
- result[key] = currentObject;
2656
- }
2657
- }
2658
- } else if (currentObject && indent >= 2) {
2659
- const nestedMatch = line.match(/^\s+(\w+):\s*(.*)$/);
2660
- if (nestedMatch) {
2661
- const [, key, value] = nestedMatch;
2662
- currentObject[key] = parseYamlValue(value.trim());
2663
- }
2664
- }
2665
- }
2666
- return result;
2667
- }
2668
- function parseYamlValue(value) {
2669
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2670
- return value.slice(1, -1);
2671
- }
2672
- if (value === "true") return true;
2673
- if (value === "false") return false;
2674
- const num = Number(value);
2675
- if (!isNaN(num) && value !== "") return num;
2676
- if (value.startsWith("[") && value.endsWith("]")) {
2677
- const inner = value.slice(1, -1);
2678
- return inner.split(",").map((s) => parseYamlValue(s.trim()));
2679
- }
2680
- return value;
2681
- }
2682
- function serializeFrontmatter(frontmatter) {
2683
- const lines = ["---"];
2684
- if (frontmatter.session) {
2685
- lines.push("session:");
2686
- const session = frontmatter.session;
2687
- if (session.id) lines.push(` id: ${session.id}`);
2688
- if (session.source) lines.push(` source: ${session.source}`);
2689
- if (session.project) lines.push(` project: ${formatPath2(session.project)}`);
2690
- if (session.transcript) lines.push(` transcript: ${formatPath2(session.transcript)}`);
2691
- }
2692
- if (frontmatter.created) {
2693
- lines.push(`created: ${frontmatter.created}`);
2694
- }
2695
- if (frontmatter.updated) {
2696
- lines.push(`updated: ${frontmatter.updated}`);
2697
- }
2698
- if (frontmatter.tags && frontmatter.tags.length > 0) {
2699
- lines.push(`tags: [${frontmatter.tags.join(", ")}]`);
2700
- }
2701
- lines.push("---");
2702
- return lines.join("\n") + "\n";
2703
- }
2704
- function addFrontmatter(content, frontmatter) {
2705
- const { frontmatter: existing, body } = parseFrontmatter(content);
2706
- const merged = {
2707
- ...existing,
2708
- ...frontmatter,
2709
- session: {
2710
- ...existing?.session,
2711
- ...frontmatter.session
2712
- }
2713
- };
2714
- if (!merged.created) {
2715
- merged.created = (/* @__PURE__ */ new Date()).toISOString();
2716
- }
2717
- merged.updated = (/* @__PURE__ */ new Date()).toISOString();
2718
- return serializeFrontmatter(merged) + body;
2719
- }
2720
- function formatPath2(filePath) {
2721
- const home = os4.homedir();
2722
- if (filePath.startsWith(home)) {
2723
- return "~" + filePath.slice(home.length);
2724
- }
2725
- return filePath;
2726
- }
2727
-
2728
- // src/cli/commands/upsert.ts
2729
3282
  async function upsert(file, content, options) {
2730
- const memoryDir = resolveMemoryDir2(options);
3283
+ const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
2731
3284
  if (!await isInitialized(memoryDir)) {
2732
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
2733
- console.error(`Run: minimem init${options.dir ? ` ${options.dir}` : ""}`);
2734
- process.exit(1);
3285
+ exitWithError(
3286
+ `${formatPath(memoryDir)} is not initialized.`,
3287
+ `Run: minimem init${options.dir ? ` ${options.dir}` : ""}`
3288
+ );
2735
3289
  }
2736
3290
  let finalContent = content;
2737
3291
  if (options.stdin) {
2738
3292
  finalContent = await readStdin();
2739
3293
  }
2740
3294
  if (!finalContent) {
2741
- console.error("Error: No content provided. Use --stdin or provide content as argument.");
2742
- process.exit(1);
3295
+ exitWithError(
3296
+ "No content provided.",
3297
+ "Use --stdin or provide content as argument."
3298
+ );
2743
3299
  }
2744
3300
  const session = options.session ? {
2745
3301
  id: options.session,
@@ -2749,11 +3305,11 @@ async function upsert(file, content, options) {
2749
3305
  const filePath = resolveFilePath(file, memoryDir);
2750
3306
  const resolvedPath = path7.resolve(filePath);
2751
3307
  const resolvedMemoryDir = path7.resolve(memoryDir);
2752
- if (!resolvedPath.startsWith(resolvedMemoryDir)) {
2753
- console.error(`Error: File path must be within the memory directory.`);
2754
- console.error(` Memory dir: ${formatPath(memoryDir)}`);
2755
- console.error(` File path: ${formatPath(filePath)}`);
2756
- process.exit(1);
3308
+ if (!resolvedPath.startsWith(resolvedMemoryDir + path7.sep) && resolvedPath !== resolvedMemoryDir) {
3309
+ exitWithError(
3310
+ "File path must be within the memory directory.",
3311
+ `Memory dir: ${formatPath(memoryDir)}, File: ${formatPath(filePath)}`
3312
+ );
2757
3313
  }
2758
3314
  const parentDir = path7.dirname(filePath);
2759
3315
  await fs5.mkdir(parentDir, { recursive: true });
@@ -2808,54 +3364,94 @@ async function upsert(file, content, options) {
2808
3364
  minimem = await Minimem.create(config2);
2809
3365
  await minimem.sync();
2810
3366
  console.log(" Index synced.");
2811
- } catch (error) {
2812
- console.log(" Note: Index not synced (run 'minimem sync' with API key to index).");
3367
+ } catch {
3368
+ note("Index not synced (run 'minimem sync' with API key to index).");
2813
3369
  } finally {
2814
3370
  minimem?.close();
2815
3371
  }
2816
3372
  }
2817
- function resolveMemoryDir2(options) {
2818
- if (options.dir) {
2819
- return path7.resolve(options.dir);
2820
- }
2821
- if (options.global) {
2822
- return path7.join(os5.homedir(), ".minimem");
2823
- }
2824
- return process.cwd();
2825
- }
2826
3373
  function resolveFilePath(file, memoryDir) {
2827
3374
  if (path7.isAbsolute(file)) {
2828
3375
  return file;
2829
3376
  }
2830
- if (file.startsWith("memory/") || file.startsWith("memory\\")) {
2831
- return path7.join(memoryDir, file);
3377
+ return path7.join(memoryDir, file);
3378
+ }
3379
+ async function readStdin() {
3380
+ const chunks = [];
3381
+ return new Promise((resolve3, reject) => {
3382
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
3383
+ process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
3384
+ process.stdin.on("error", reject);
3385
+ });
3386
+ }
3387
+
3388
+ // src/cli/commands/mcp.ts
3389
+ import * as fs6 from "fs/promises";
3390
+ import * as path8 from "path";
3391
+
3392
+ // src/server/mcp.ts
3393
+ import * as readline from "readline";
3394
+
3395
+ // src/server/tools.ts
3396
+ var MEMORY_SEARCH_TOOL = {
3397
+ name: "memory_search",
3398
+ description: "Semantically search through memory files (MEMORY.md and memory/*.md). Use this to recall prior decisions, facts, preferences, people, dates, or context. Returns ranked snippets with file paths and line numbers. When multiple memory directories are configured, searches all by default.",
3399
+ inputSchema: {
3400
+ type: "object",
3401
+ properties: {
3402
+ query: {
3403
+ type: "string",
3404
+ description: "Natural language search query"
3405
+ },
3406
+ maxResults: {
3407
+ type: "number",
3408
+ description: "Maximum number of results to return (default: 10)"
3409
+ },
3410
+ minScore: {
3411
+ type: "number",
3412
+ description: "Minimum relevance score threshold 0-1 (default: 0.3)"
3413
+ },
3414
+ directories: {
3415
+ type: "array",
3416
+ items: { type: "string" },
3417
+ description: "Optional: filter to specific memory directories by name/path. If omitted, searches all configured directories."
3418
+ },
3419
+ detail: {
3420
+ type: "string",
3421
+ enum: ["compact", "full"],
3422
+ description: "Result detail level. 'compact' returns a lightweight index with short previews (~80 chars). 'full' returns complete snippets. Use 'compact' first, then memory_get_details for selected results. (default: 'compact')"
3423
+ },
3424
+ type: {
3425
+ type: "string",
3426
+ description: "Filter by observation type. Matches <!-- type: X --> comments in memory entries. Common types: decision, bugfix, feature, discovery, context, note."
3427
+ }
3428
+ },
3429
+ required: ["query"]
2832
3430
  }
2833
- if (file === "MEMORY.md" || file.endsWith(".md") && !file.includes("/")) {
2834
- return path7.join(memoryDir, file);
3431
+ };
3432
+ var MEMORY_GET_DETAILS_TOOL = {
3433
+ name: "memory_get_details",
3434
+ description: "Fetch full text for specific memory chunks identified by path and line range. Use after memory_search with compact results to get details for selected items only. This two-step approach significantly reduces token usage.",
3435
+ inputSchema: {
3436
+ type: "object",
3437
+ properties: {
3438
+ results: {
3439
+ type: "array",
3440
+ items: { type: "object" },
3441
+ description: "Array of { path, startLine, endLine } objects from compact search results."
3442
+ },
3443
+ directories: {
3444
+ type: "array",
3445
+ items: { type: "string" },
3446
+ description: "Optional: filter to specific memory directories."
3447
+ }
3448
+ },
3449
+ required: ["results"]
2835
3450
  }
2836
- return path7.join(memoryDir, "memory", file);
2837
- }
2838
- async function readStdin() {
2839
- const chunks = [];
2840
- return new Promise((resolve4, reject) => {
2841
- process.stdin.on("data", (chunk) => chunks.push(chunk));
2842
- process.stdin.on("end", () => resolve4(Buffer.concat(chunks).toString("utf-8")));
2843
- process.stdin.on("error", reject);
2844
- });
2845
- }
2846
-
2847
- // src/cli/commands/mcp.ts
2848
- import * as fs6 from "fs/promises";
2849
- import * as path8 from "path";
2850
- import * as os6 from "os";
2851
-
2852
- // src/server/mcp.ts
2853
- import * as readline from "readline";
2854
-
2855
- // src/server/tools.ts
2856
- var MEMORY_SEARCH_TOOL = {
2857
- name: "memory_search",
2858
- description: "Semantically search through memory files (MEMORY.md and memory/*.md). Use this to recall prior decisions, facts, preferences, people, dates, or context. Returns ranked snippets with file paths and line numbers. When multiple memory directories are configured, searches all by default.",
3451
+ };
3452
+ var KNOWLEDGE_SEARCH_TOOL = {
3453
+ name: "knowledge_search",
3454
+ description: "Search memory with knowledge metadata filters. Filter by domain, entities, confidence level, or knowledge type (observation, entity, domain-summary). Combines semantic search with structured knowledge filtering.",
2859
3455
  inputSchema: {
2860
3456
  type: "object",
2861
3457
  properties: {
@@ -2863,24 +3459,108 @@ var MEMORY_SEARCH_TOOL = {
2863
3459
  type: "string",
2864
3460
  description: "Natural language search query"
2865
3461
  },
3462
+ domain: {
3463
+ type: "array",
3464
+ items: { type: "string" },
3465
+ description: "Filter to entries in these knowledge domains"
3466
+ },
3467
+ entities: {
3468
+ type: "array",
3469
+ items: { type: "string" },
3470
+ description: "Filter to entries referencing these entities"
3471
+ },
3472
+ minConfidence: {
3473
+ type: "number",
3474
+ description: "Minimum confidence threshold (0-1)"
3475
+ },
3476
+ knowledgeType: {
3477
+ type: "string",
3478
+ description: "Filter by knowledge type: observation, entity, domain-summary"
3479
+ },
2866
3480
  maxResults: {
2867
3481
  type: "number",
2868
- description: "Maximum number of results to return (default: 10)"
3482
+ description: "Maximum number of results (default: 10)"
2869
3483
  },
2870
3484
  minScore: {
2871
3485
  type: "number",
2872
- description: "Minimum relevance score threshold 0-1 (default: 0.3)"
3486
+ description: "Minimum relevance score 0-1 (default: 0.3)"
2873
3487
  },
2874
3488
  directories: {
2875
3489
  type: "array",
2876
3490
  items: { type: "string" },
2877
- description: "Optional: filter to specific memory directories by name/path. If omitted, searches all configured directories."
3491
+ description: "Optional: filter to specific memory directories"
2878
3492
  }
2879
3493
  },
2880
3494
  required: ["query"]
2881
3495
  }
2882
3496
  };
2883
- var MEMORY_TOOLS = [MEMORY_SEARCH_TOOL];
3497
+ var KNOWLEDGE_GRAPH_TOOL = {
3498
+ name: "knowledge_graph",
3499
+ description: "Traverse knowledge graph links from a note. Returns neighbor nodes connected by typed relationships (e.g., relates-to, supports, contradicts). Use depth parameter for multi-hop traversal.",
3500
+ inputSchema: {
3501
+ type: "object",
3502
+ properties: {
3503
+ nodeId: {
3504
+ type: "string",
3505
+ description: "The knowledge node ID to start traversal from"
3506
+ },
3507
+ depth: {
3508
+ type: "number",
3509
+ description: "Maximum traversal depth (default: 1, max: 3)",
3510
+ default: 1
3511
+ },
3512
+ relation: {
3513
+ type: "string",
3514
+ description: "Optional: filter to specific relation type"
3515
+ },
3516
+ layer: {
3517
+ type: "string",
3518
+ description: "Optional: filter to specific graph layer"
3519
+ },
3520
+ directories: {
3521
+ type: "array",
3522
+ items: { type: "string" },
3523
+ description: "Optional: filter to specific memory directories"
3524
+ }
3525
+ },
3526
+ required: ["nodeId"]
3527
+ }
3528
+ };
3529
+ var KNOWLEDGE_PATH_TOOL = {
3530
+ name: "knowledge_path",
3531
+ description: "Find the shortest path between two knowledge nodes in the graph. Uses BFS traversal up to a configurable max depth. Returns the sequence of links connecting the two nodes.",
3532
+ inputSchema: {
3533
+ type: "object",
3534
+ properties: {
3535
+ fromId: {
3536
+ type: "string",
3537
+ description: "Starting knowledge node ID"
3538
+ },
3539
+ toId: {
3540
+ type: "string",
3541
+ description: "Target knowledge node ID"
3542
+ },
3543
+ maxDepth: {
3544
+ type: "number",
3545
+ description: "Maximum path length (default: 3)",
3546
+ default: 3
3547
+ },
3548
+ directories: {
3549
+ type: "array",
3550
+ items: { type: "string" },
3551
+ description: "Optional: filter to specific memory directories"
3552
+ }
3553
+ },
3554
+ required: ["fromId", "toId"]
3555
+ }
3556
+ };
3557
+ var MEMORY_TOOLS = [
3558
+ MEMORY_SEARCH_TOOL,
3559
+ MEMORY_GET_DETAILS_TOOL,
3560
+ KNOWLEDGE_SEARCH_TOOL,
3561
+ KNOWLEDGE_GRAPH_TOOL,
3562
+ KNOWLEDGE_PATH_TOOL
3563
+ ];
2884
3564
  var MemoryToolExecutor = class {
2885
3565
  instances;
2886
3566
  constructor(instances) {
@@ -2906,6 +3586,14 @@ var MemoryToolExecutor = class {
2906
3586
  switch (toolName) {
2907
3587
  case "memory_search":
2908
3588
  return await this.memorySearch(params);
3589
+ case "memory_get_details":
3590
+ return await this.memoryGetDetails(params);
3591
+ case "knowledge_search":
3592
+ return await this.knowledgeSearch(params);
3593
+ case "knowledge_graph":
3594
+ return await this.knowledgeGraph(params);
3595
+ case "knowledge_path":
3596
+ return await this.knowledgePath(params);
2909
3597
  default:
2910
3598
  return {
2911
3599
  content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
@@ -2920,37 +3608,43 @@ var MemoryToolExecutor = class {
2920
3608
  };
2921
3609
  }
2922
3610
  }
3611
+ /**
3612
+ * Filter instances by directory names/paths
3613
+ */
3614
+ filterInstances(directories) {
3615
+ if (!directories || directories.length === 0) return this.instances;
3616
+ const dirFilter = new Set(directories.map((d) => d.toLowerCase()));
3617
+ const filtered = this.instances.filter((i) => {
3618
+ const name = (i.name ?? i.memoryDir).toLowerCase();
3619
+ const dir = i.memoryDir.toLowerCase();
3620
+ return dirFilter.has(name) || dirFilter.has(dir) || [...dirFilter].some((f) => dir.includes(f) || name.includes(f));
3621
+ });
3622
+ return filtered.length > 0 ? filtered : null;
3623
+ }
2923
3624
  async memorySearch(params) {
2924
3625
  const maxResults = params.maxResults ?? 10;
2925
3626
  const minScore = params.minScore;
2926
- let instancesToSearch = this.instances;
2927
- if (params.directories && params.directories.length > 0) {
2928
- const dirFilter = new Set(params.directories.map((d) => d.toLowerCase()));
2929
- instancesToSearch = this.instances.filter((i) => {
2930
- const name = (i.name ?? i.memoryDir).toLowerCase();
2931
- const dir = i.memoryDir.toLowerCase();
2932
- return dirFilter.has(name) || dirFilter.has(dir) || // Also match partial paths
2933
- [...dirFilter].some((f) => dir.includes(f) || name.includes(f));
2934
- });
2935
- if (instancesToSearch.length === 0) {
2936
- const available = this.getDirectories().join(", ");
2937
- return {
2938
- content: [
2939
- {
2940
- type: "text",
2941
- text: `No matching directories found. Available: ${available}`
2942
- }
2943
- ],
2944
- isError: true
2945
- };
2946
- }
3627
+ const detail = params.detail ?? "compact";
3628
+ const instancesToSearch = this.filterInstances(params.directories);
3629
+ if (!instancesToSearch) {
3630
+ const available = this.getDirectories().join(", ");
3631
+ return {
3632
+ content: [
3633
+ {
3634
+ type: "text",
3635
+ text: `No matching directories found. Available: ${available}`
3636
+ }
3637
+ ],
3638
+ isError: true
3639
+ };
2947
3640
  }
2948
3641
  const allResults = [];
2949
3642
  for (const instance of instancesToSearch) {
2950
3643
  const perDirMax = Math.ceil(maxResults * 1.5);
2951
3644
  const results = await instance.minimem.search(params.query, {
2952
3645
  maxResults: perDirMax,
2953
- minScore
3646
+ minScore,
3647
+ type: params.type
2954
3648
  });
2955
3649
  for (const result of results) {
2956
3650
  allResults.push({
@@ -2967,21 +3661,203 @@ var MemoryToolExecutor = class {
2967
3661
  };
2968
3662
  }
2969
3663
  const showSource = instancesToSearch.length > 1;
2970
- const formatted = topResults.map((r, i) => {
3664
+ if (detail === "compact") {
3665
+ return this.formatCompactResults(topResults, showSource, instancesToSearch.length);
3666
+ }
3667
+ return this.formatFullResults(topResults, showSource, instancesToSearch.length);
3668
+ }
3669
+ formatCompactResults(results, showSource, dirCount) {
3670
+ const formatted = results.map((r, i) => {
3671
+ const location = `${r.path}:${r.startLine}-${r.endLine}`;
3672
+ const score = (r.score * 100).toFixed(0);
3673
+ const source = showSource ? ` [${r.memoryDir}]` : "";
3674
+ const preview = compactPreview(r.snippet);
3675
+ return `[${i}] ${location}${source} (${score}%) \u2014 ${preview}`;
3676
+ }).join("\n");
3677
+ const hint = "\n\nUse memory_get_details to fetch full text for selected results.";
3678
+ const dirSummary = dirCount > 1 ? `
3679
+ (Searched ${dirCount} directories)` : "";
3680
+ return {
3681
+ content: [{ type: "text", text: formatted + dirSummary + hint }]
3682
+ };
3683
+ }
3684
+ formatFullResults(results, showSource, dirCount) {
3685
+ const formatted = results.map((r, i) => {
2971
3686
  const location = `${r.path}:${r.startLine}-${r.endLine}`;
2972
3687
  const score = (r.score * 100).toFixed(1);
2973
3688
  const source = showSource ? ` [${r.memoryDir}]` : "";
2974
3689
  return `[${i + 1}] ${location}${source} (${score}% match)
2975
3690
  ${r.snippet}`;
2976
3691
  }).join("\n\n");
2977
- const dirSummary = instancesToSearch.length > 1 ? `
3692
+ const dirSummary = dirCount > 1 ? `
2978
3693
 
2979
- (Searched ${instancesToSearch.length} directories)` : "";
3694
+ (Searched ${dirCount} directories)` : "";
2980
3695
  return {
2981
3696
  content: [{ type: "text", text: formatted + dirSummary }]
2982
3697
  };
2983
3698
  }
3699
+ async memoryGetDetails(params) {
3700
+ if (!params.results || params.results.length === 0) {
3701
+ return {
3702
+ content: [{ type: "text", text: "No results specified." }],
3703
+ isError: true
3704
+ };
3705
+ }
3706
+ const instancesToSearch = this.filterInstances(params.directories);
3707
+ if (!instancesToSearch) {
3708
+ const available = this.getDirectories().join(", ");
3709
+ return {
3710
+ content: [
3711
+ {
3712
+ type: "text",
3713
+ text: `No matching directories found. Available: ${available}`
3714
+ }
3715
+ ],
3716
+ isError: true
3717
+ };
3718
+ }
3719
+ const details = [];
3720
+ for (const ref of params.results) {
3721
+ let found = false;
3722
+ for (const instance of instancesToSearch) {
3723
+ const lineCount = ref.endLine - ref.startLine + 1;
3724
+ const result = await instance.minimem.readLines(ref.path, {
3725
+ from: ref.startLine,
3726
+ lines: lineCount
3727
+ });
3728
+ if (result) {
3729
+ const location = `${ref.path}:${result.startLine}-${result.endLine}`;
3730
+ details.push(`--- ${location} ---
3731
+ ${result.content}`);
3732
+ found = true;
3733
+ break;
3734
+ }
3735
+ }
3736
+ if (!found) {
3737
+ details.push(`--- ${ref.path}:${ref.startLine}-${ref.endLine} ---
3738
+ (not found)`);
3739
+ }
3740
+ }
3741
+ return {
3742
+ content: [{ type: "text", text: details.join("\n\n") }]
3743
+ };
3744
+ }
3745
+ async knowledgeSearch(params) {
3746
+ const instancesToSearch = this.filterInstances(params.directories);
3747
+ if (!instancesToSearch) {
3748
+ const available = this.getDirectories().join(", ");
3749
+ return {
3750
+ content: [{ type: "text", text: `No matching directories found. Available: ${available}` }],
3751
+ isError: true
3752
+ };
3753
+ }
3754
+ const maxResults = params.maxResults ?? 10;
3755
+ const allResults = [];
3756
+ for (const instance of instancesToSearch) {
3757
+ const results = await instance.minimem.knowledgeSearch(params.query, {
3758
+ maxResults: Math.ceil(maxResults * 1.5),
3759
+ minScore: params.minScore,
3760
+ domain: params.domain,
3761
+ entities: params.entities,
3762
+ minConfidence: params.minConfidence,
3763
+ knowledgeType: params.knowledgeType
3764
+ });
3765
+ for (const result of results) {
3766
+ allResults.push({
3767
+ ...result,
3768
+ memoryDir: instance.name ?? instance.memoryDir
3769
+ });
3770
+ }
3771
+ }
3772
+ allResults.sort((a, b) => b.score - a.score);
3773
+ const topResults = allResults.slice(0, maxResults);
3774
+ if (topResults.length === 0) {
3775
+ return { content: [{ type: "text", text: "No knowledge results found." }] };
3776
+ }
3777
+ const formatted = topResults.map((r, i) => {
3778
+ const location = `${r.path}:${r.startLine}-${r.endLine}`;
3779
+ const score = (r.score * 100).toFixed(0);
3780
+ const preview = compactPreview(r.snippet);
3781
+ return `[${i}] ${location} (${score}%) \u2014 ${preview}`;
3782
+ }).join("\n");
3783
+ return { content: [{ type: "text", text: formatted }] };
3784
+ }
3785
+ async knowledgeGraph(params) {
3786
+ const instancesToSearch = this.filterInstances(params.directories);
3787
+ if (!instancesToSearch) {
3788
+ const available = this.getDirectories().join(", ");
3789
+ return {
3790
+ content: [{ type: "text", text: `No matching directories found. Available: ${available}` }],
3791
+ isError: true
3792
+ };
3793
+ }
3794
+ const depth = Math.min(params.depth ?? 1, 3);
3795
+ const allNeighbors = [];
3796
+ for (const instance of instancesToSearch) {
3797
+ const neighbors = instance.minimem.getGraphNeighbors(params.nodeId, depth, {
3798
+ relation: params.relation,
3799
+ layer: params.layer
3800
+ });
3801
+ for (const n of neighbors) {
3802
+ allNeighbors.push({
3803
+ id: n.id,
3804
+ depth: n.depth,
3805
+ relation: n.link.relation,
3806
+ layer: n.link.layer,
3807
+ memoryDir: instance.name ?? instance.memoryDir
3808
+ });
3809
+ }
3810
+ }
3811
+ if (allNeighbors.length === 0) {
3812
+ return { content: [{ type: "text", text: `No neighbors found for node "${params.nodeId}".` }] };
3813
+ }
3814
+ const formatted = allNeighbors.map((n) => ` [depth=${n.depth}] ${n.id} \u2014(${n.relation})${n.layer ? ` [${n.layer}]` : ""}`).join("\n");
3815
+ return {
3816
+ content: [{ type: "text", text: `Neighbors of "${params.nodeId}":
3817
+ ${formatted}` }]
3818
+ };
3819
+ }
3820
+ async knowledgePath(params) {
3821
+ const instancesToSearch = this.filterInstances(params.directories);
3822
+ if (!instancesToSearch) {
3823
+ const available = this.getDirectories().join(", ");
3824
+ return {
3825
+ content: [{ type: "text", text: `No matching directories found. Available: ${available}` }],
3826
+ isError: true
3827
+ };
3828
+ }
3829
+ const maxDepth = Math.min(params.maxDepth ?? 3, 5);
3830
+ for (const instance of instancesToSearch) {
3831
+ const path20 = instance.minimem.getGraphPath(params.fromId, params.toId, maxDepth);
3832
+ if (path20.length > 0) {
3833
+ const steps = path20.map((link) => ` ${link.fromId} \u2014(${link.relation})\u2192 ${link.toId}`).join("\n");
3834
+ return {
3835
+ content: [{
3836
+ type: "text",
3837
+ text: `Path from "${params.fromId}" to "${params.toId}" (${path20.length} steps):
3838
+ ${steps}`
3839
+ }]
3840
+ };
3841
+ }
3842
+ }
3843
+ return {
3844
+ content: [{
3845
+ type: "text",
3846
+ text: `No path found from "${params.fromId}" to "${params.toId}" within depth ${maxDepth}.`
3847
+ }]
3848
+ };
3849
+ }
2984
3850
  };
3851
+ function compactPreview(snippet) {
3852
+ const maxLen = 80;
3853
+ const lines = snippet.split("\n").filter((l) => l.trim());
3854
+ if (lines.length === 0) return "(empty)";
3855
+ const heading = lines.find((l) => l.startsWith("#"));
3856
+ const text = heading ?? lines[0];
3857
+ const cleaned = text.replace(/^#+\s*/, "").trim();
3858
+ if (cleaned.length <= maxLen) return `"${cleaned}"`;
3859
+ return `"${cleaned.slice(0, maxLen - 3)}..."`;
3860
+ }
2985
3861
 
2986
3862
  // src/server/mcp.ts
2987
3863
  var PROTOCOL_VERSION = "2024-11-05";
@@ -3131,12 +4007,13 @@ async function runMcpServer(server) {
3131
4007
 
3132
4008
  // src/cli/commands/mcp.ts
3133
4009
  async function mcp(options) {
3134
- const directories = resolveDirectories(options);
3135
- const globalDir = path8.join(os6.homedir(), ".minimem");
4010
+ const directories = resolveMemoryDirs(options);
4011
+ const globalDir = getGlobalMemoryDir();
3136
4012
  if (directories.length === 0) {
3137
- console.error("Error: No memory directories specified.");
3138
- console.error("Use --dir <path> or --global to specify directories.");
3139
- process.exit(1);
4013
+ exitWithError(
4014
+ "No memory directories specified.",
4015
+ "Use --dir <path> or --global to specify directories."
4016
+ );
3140
4017
  }
3141
4018
  const includesGlobal = directories.includes(globalDir);
3142
4019
  if (includesGlobal && !await isInitialized(globalDir)) {
@@ -3147,7 +4024,7 @@ async function mcp(options) {
3147
4024
  for (const memoryDir of directories) {
3148
4025
  const isGlobal = memoryDir === globalDir;
3149
4026
  if (!await isInitialized(memoryDir)) {
3150
- console.error(`Warning: ${formatPath(memoryDir)} is not initialized, skipping.`);
4027
+ warn(`${formatPath(memoryDir)} is not initialized, skipping.`);
3151
4028
  if (!isGlobal && !includesGlobal) {
3152
4029
  console.error(` Using global (~/.minimem) as fallback.`);
3153
4030
  if (!await isInitialized(globalDir)) {
@@ -3168,8 +4045,8 @@ async function mcp(options) {
3168
4045
  minimemInstances.push(minimem);
3169
4046
  const status2 = await minimem.status();
3170
4047
  if (status2.bm25Only && instances.length === 0) {
3171
- console.error(`Note: Running in BM25-only mode (no embedding API configured).`);
3172
- console.error(` Search results will be based on keyword matching only.`);
4048
+ note("Running in BM25-only mode (no embedding API configured).");
4049
+ note("Search results will be based on keyword matching only.");
3173
4050
  }
3174
4051
  const name = getDirName(memoryDir);
3175
4052
  instances.push({
@@ -3179,12 +4056,11 @@ async function mcp(options) {
3179
4056
  });
3180
4057
  } catch (error) {
3181
4058
  const message = error instanceof Error ? error.message : String(error);
3182
- console.error(`Warning: Failed to load ${formatPath(memoryDir)}: ${message}`);
4059
+ warn(`Failed to load ${formatPath(memoryDir)}: ${message}`);
3183
4060
  }
3184
4061
  }
3185
4062
  if (instances.length === 0) {
3186
- console.error("Error: No valid memory directories found.");
3187
- process.exit(1);
4063
+ exitWithError("No valid memory directories found.");
3188
4064
  }
3189
4065
  if (instances.length === 1) {
3190
4066
  console.error(`Serving: ${instances[0].name} (${formatPath(instances[0].memoryDir)})`);
@@ -3205,36 +4081,6 @@ async function mcp(options) {
3205
4081
  process.on("SIGTERM", shutdown);
3206
4082
  await runMcpServer(server);
3207
4083
  }
3208
- function resolveDirectories(options) {
3209
- const dirs = [];
3210
- if (options.dir && options.dir.length > 0) {
3211
- for (const dir of options.dir) {
3212
- dirs.push(path8.resolve(dir));
3213
- }
3214
- }
3215
- if (options.global) {
3216
- const globalDir = path8.join(os6.homedir(), ".minimem");
3217
- if (!dirs.includes(globalDir)) {
3218
- dirs.push(globalDir);
3219
- }
3220
- }
3221
- if (dirs.length === 0) {
3222
- dirs.push(process.cwd());
3223
- }
3224
- return dirs;
3225
- }
3226
- function getDirName(memoryDir) {
3227
- const home = os6.homedir();
3228
- if (memoryDir === path8.join(home, ".minimem")) {
3229
- return "global";
3230
- }
3231
- const name = path8.basename(memoryDir);
3232
- if (name.startsWith(".")) {
3233
- const parent = path8.basename(path8.dirname(memoryDir));
3234
- return `${parent}/${name}`;
3235
- }
3236
- return name;
3237
- }
3238
4084
  async function ensureGlobalInitialized(globalDir) {
3239
4085
  console.error(`Auto-initializing global memory directory (~/.minimem)...`);
3240
4086
  await fs6.mkdir(globalDir, { recursive: true });
@@ -3273,8 +4119,7 @@ async function config(options) {
3273
4119
  async function showConfig(options) {
3274
4120
  const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
3275
4121
  if (!await isInitialized(memoryDir)) {
3276
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
3277
- process.exit(1);
4122
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
3278
4123
  }
3279
4124
  const globalConfig = await loadGlobalConfig();
3280
4125
  const xdgConfig = await loadXdgConfig();
@@ -3360,16 +4205,16 @@ async function handleConfigEdit(options) {
3360
4205
  const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
3361
4206
  const configPath = getConfigPath(memoryDir);
3362
4207
  if (!await isInitialized(memoryDir)) {
3363
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
3364
- process.exit(1);
4208
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
3365
4209
  }
3366
4210
  const currentConfig = await loadConfigFile2(configPath);
3367
4211
  if (options.set) {
3368
4212
  const [keyPath, value] = options.set.split("=");
3369
4213
  if (!keyPath || value === void 0) {
3370
- console.error("Error: --set requires format: key.path=value");
3371
- console.error("Example: --set embedding.provider=openai");
3372
- process.exit(1);
4214
+ exitWithError(
4215
+ "--set requires format: key.path=value",
4216
+ "Example: --set embedding.provider=openai"
4217
+ );
3373
4218
  }
3374
4219
  const newConfig = setConfigValue(currentConfig, keyPath, parseValue(value));
3375
4220
  await saveConfig(memoryDir, newConfig);
@@ -3388,9 +4233,10 @@ async function handleXdgConfigEdit(options) {
3388
4233
  if (options.set) {
3389
4234
  const [keyPath, value] = options.set.split("=");
3390
4235
  if (!keyPath || value === void 0) {
3391
- console.error("Error: --set requires format: key.path=value");
3392
- console.error("Example: --set centralRepo=~/memories-repo");
3393
- process.exit(1);
4236
+ exitWithError(
4237
+ "--set requires format: key.path=value",
4238
+ "Example: --set centralRepo=~/memories-repo"
4239
+ );
3394
4240
  }
3395
4241
  const newConfig = setConfigValue(currentConfig, keyPath, parseValue(value));
3396
4242
  await saveXdgConfig(newConfig);
@@ -3463,7 +4309,7 @@ import { execSync } from "child_process";
3463
4309
  // src/cli/sync/registry.ts
3464
4310
  import fs7 from "fs/promises";
3465
4311
  import path9 from "path";
3466
- import os7 from "os";
4312
+ import os5 from "os";
3467
4313
  import crypto3 from "crypto";
3468
4314
  var REGISTRY_FILENAME = ".minimem-registry.json";
3469
4315
  function getRegistryPath(centralRepo) {
@@ -3497,15 +4343,15 @@ async function writeRegistry(centralRepo, registry) {
3497
4343
  }
3498
4344
  function normalizePath(filePath) {
3499
4345
  if (filePath.startsWith("~/")) {
3500
- return path9.resolve(os7.homedir(), filePath.slice(2));
4346
+ return path9.resolve(os5.homedir(), filePath.slice(2));
3501
4347
  }
3502
4348
  if (filePath === "~") {
3503
- return os7.homedir();
4349
+ return os5.homedir();
3504
4350
  }
3505
4351
  return path9.resolve(filePath);
3506
4352
  }
3507
4353
  function compressPath(filePath) {
3508
- const home = os7.homedir();
4354
+ const home = os5.homedir();
3509
4355
  const resolved = path9.resolve(filePath);
3510
4356
  if (resolved.startsWith(home)) {
3511
4357
  return "~" + resolved.slice(home.length);
@@ -3600,19 +4446,15 @@ async function hasSyncConfig(dir) {
3600
4446
  }
3601
4447
  }
3602
4448
  async function detectDirectoryType(dir) {
3603
- const [hasSync, inGit] = await Promise.all([
3604
- hasSyncConfig(dir),
3605
- isInsideGitRepo(dir)
3606
- ]);
3607
- if (hasSync && inGit) {
3608
- return "hybrid";
3609
- } else if (hasSync && !inGit) {
4449
+ const hasSync = await hasSyncConfig(dir);
4450
+ if (hasSync) {
3610
4451
  return "standalone";
3611
- } else if (!hasSync && inGit) {
4452
+ }
4453
+ const inGit = await isInsideGitRepo(dir);
4454
+ if (inGit) {
3612
4455
  return "project-bound";
3613
- } else {
3614
- return "unmanaged";
3615
4456
  }
4457
+ return "standalone";
3616
4458
  }
3617
4459
 
3618
4460
  // src/cli/sync/central.ts
@@ -3825,8 +4667,7 @@ async function syncInitCentral(repoPath, options = {}) {
3825
4667
  console.log(`Initializing central repository at ${formatPath(repoPath)}...`);
3826
4668
  const result = await initCentralRepo(repoPath);
3827
4669
  if (!result.success) {
3828
- console.error(`Error: ${result.message}`);
3829
- process.exit(1);
4670
+ exitWithError(result.message);
3830
4671
  }
3831
4672
  if (result.created) {
3832
4673
  console.log(" Created new git repository");
@@ -3853,30 +4694,30 @@ async function syncInit(options) {
3853
4694
  global: options.global
3854
4695
  });
3855
4696
  if (!await isInitialized(memoryDir)) {
3856
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
3857
- console.error("Run 'minimem init' first.");
3858
- process.exit(1);
4697
+ exitWithError(
4698
+ `${formatPath(memoryDir)} is not initialized.`,
4699
+ "Run 'minimem init' first."
4700
+ );
3859
4701
  }
3860
4702
  if (!options.path) {
3861
- console.error("Error: --path is required.");
3862
- console.error("Example: minimem sync init --path myproject/");
3863
- process.exit(1);
4703
+ exitWithError(
4704
+ "--path is required.",
4705
+ "Example: minimem sync init --path myproject/"
4706
+ );
3864
4707
  }
3865
4708
  const centralPath = options.path;
3866
4709
  const centralRepo = await getCentralRepoPath();
3867
4710
  if (!centralRepo) {
3868
- console.error("Error: No central repository configured.");
3869
- console.error("First initialize a central repository:");
3870
- console.error(" minimem sync init-central ~/memories-repo");
3871
- process.exit(1);
4711
+ exitWithError(
4712
+ "No central repository configured.",
4713
+ "First initialize a central repository: minimem sync init-central ~/memories-repo"
4714
+ );
3872
4715
  }
3873
4716
  const validation = await validateCentralRepo(centralRepo);
3874
4717
  if (!validation.valid) {
3875
- console.error(`Error: Central repository is invalid:`);
3876
- for (const error of validation.errors) {
3877
- console.error(` - ${error}`);
3878
- }
3879
- process.exit(1);
4718
+ exitWithError(
4719
+ `Central repository is invalid: ${validation.errors.join(", ")}`
4720
+ );
3880
4721
  }
3881
4722
  if (validation.warnings.length > 0) {
3882
4723
  console.log("Warnings about central repository:");
@@ -3892,15 +4733,11 @@ async function syncInit(options) {
3892
4733
  const existingMapping = registry.mappings.find(
3893
4734
  (m) => m.path === centralPath || m.path === `${centralPath}/`
3894
4735
  );
3895
- console.error(`Error: Path '${centralPath}' is already mapped by another machine.`);
4736
+ let details = `Path '${centralPath}' is already mapped by another machine.`;
3896
4737
  if (existingMapping) {
3897
- console.error(` Machine: ${existingMapping.machineId}`);
3898
- console.error(` Local path: ${existingMapping.localPath}`);
3899
- console.error(` Last sync: ${existingMapping.lastSync}`);
4738
+ details += ` Machine: ${existingMapping.machineId}, Local: ${existingMapping.localPath}`;
3900
4739
  }
3901
- console.error();
3902
- console.error("Choose a different path or remove the existing mapping.");
3903
- process.exit(1);
4740
+ exitWithError(details, "Choose a different path or remove the existing mapping.");
3904
4741
  }
3905
4742
  const dirType = await detectDirectoryType(memoryDir);
3906
4743
  console.log(`Initializing sync for ${formatPath(memoryDir)}...`);
@@ -3979,13 +4816,11 @@ async function syncRemove(options) {
3979
4816
  });
3980
4817
  const centralRepo = await getCentralRepoPath();
3981
4818
  if (!centralRepo) {
3982
- console.error("Error: No central repository configured.");
3983
- process.exit(1);
4819
+ exitWithError("No central repository configured.");
3984
4820
  }
3985
4821
  const localConfig = await loadConfig(memoryDir);
3986
4822
  if (!localConfig.sync?.path) {
3987
- console.error(`Error: ${formatPath(memoryDir)} is not configured for sync.`);
3988
- process.exit(1);
4823
+ exitWithError(`${formatPath(memoryDir)} is not configured for sync.`);
3989
4824
  }
3990
4825
  const centralPath = localConfig.sync.path;
3991
4826
  const machineId = await getMachineId();
@@ -4023,7 +4858,8 @@ function getSyncStatePath(dir) {
4023
4858
  }
4024
4859
  function createEmptySyncState(centralPath) {
4025
4860
  return {
4026
- version: 1,
4861
+ version: 2,
4862
+ // Bumped version for simplified state
4027
4863
  lastSync: null,
4028
4864
  centralPath,
4029
4865
  files: {}
@@ -4038,6 +4874,13 @@ async function loadSyncState(dir, centralPath) {
4038
4874
  return createEmptySyncState(centralPath);
4039
4875
  }
4040
4876
  state.centralPath = centralPath;
4877
+ if (state.version === 1) {
4878
+ for (const file of Object.keys(state.files)) {
4879
+ const entry = state.files[file];
4880
+ delete entry.lastSyncedHash;
4881
+ }
4882
+ state.version = 2;
4883
+ }
4041
4884
  return state;
4042
4885
  } catch {
4043
4886
  return createEmptySyncState(centralPath);
@@ -4048,7 +4891,7 @@ async function saveSyncState(dir, state) {
4048
4891
  const stateDir = path13.dirname(statePath);
4049
4892
  const tempPath = `${statePath}.${crypto4.randomBytes(4).toString("hex")}.tmp`;
4050
4893
  await fs11.mkdir(stateDir, { recursive: true });
4051
- state.version = state.version || 1;
4894
+ state.version = state.version || 2;
4052
4895
  await fs11.writeFile(tempPath, JSON.stringify(state, null, 2), "utf-8");
4053
4896
  await fs11.rename(tempPath, statePath);
4054
4897
  }
@@ -4101,34 +4944,27 @@ async function getFileHashInfo(filePath) {
4101
4944
  return { exists: false };
4102
4945
  }
4103
4946
  }
4104
- function getFileSyncStatus(localHash, remoteHash, lastSyncedHash) {
4947
+ function getFileSyncStatus(localHash, remoteHash) {
4105
4948
  if (localHash && remoteHash && localHash === remoteHash) {
4106
4949
  return "unchanged";
4107
4950
  }
4108
- if (!lastSyncedHash) {
4109
- if (localHash && !remoteHash) return "new-local";
4110
- if (!localHash && remoteHash) return "new-remote";
4111
- if (localHash && remoteHash && localHash !== remoteHash) return "conflict";
4951
+ if (localHash && remoteHash && localHash !== remoteHash) {
4952
+ return "local-modified";
4112
4953
  }
4113
- if (lastSyncedHash && !localHash && remoteHash === lastSyncedHash) {
4114
- return "deleted-local";
4954
+ if (localHash && !remoteHash) {
4955
+ return "local-only";
4115
4956
  }
4116
- if (lastSyncedHash && !remoteHash && localHash === lastSyncedHash) {
4117
- return "deleted-remote";
4118
- }
4119
- if (lastSyncedHash) {
4120
- const localChanged = localHash !== lastSyncedHash;
4121
- const remoteChanged = remoteHash !== lastSyncedHash;
4122
- if (localChanged && !remoteChanged) return "local-only";
4123
- if (!localChanged && remoteChanged) return "remote-only";
4124
- if (localChanged && remoteChanged) {
4125
- if (localHash !== remoteHash) return "conflict";
4126
- return "unchanged";
4127
- }
4957
+ if (!localHash && remoteHash) {
4958
+ return "remote-only";
4128
4959
  }
4129
4960
  return "unchanged";
4130
4961
  }
4131
4962
 
4963
+ // src/cli/commands/conflicts.ts
4964
+ import fs13 from "fs/promises";
4965
+ import path15 from "path";
4966
+ import { spawn } from "child_process";
4967
+
4132
4968
  // src/cli/sync/conflicts.ts
4133
4969
  import fs12 from "fs/promises";
4134
4970
  import path14 from "path";
@@ -4160,7 +4996,7 @@ async function listQuarantinedConflicts(memoryDir) {
4160
4996
  }
4161
4997
  return conflicts.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
4162
4998
  }
4163
- async function detectConflicts(memoryDir, centralPath) {
4999
+ async function detectChanges(memoryDir) {
4164
5000
  const centralRepo = await getCentralRepoPath();
4165
5001
  if (!centralRepo) {
4166
5002
  throw new Error("No central repository configured");
@@ -4170,8 +5006,6 @@ async function detectConflicts(memoryDir, centralPath) {
4170
5006
  throw new Error("Directory is not configured for sync");
4171
5007
  }
4172
5008
  const remotePath = path14.join(centralRepo, syncConfig.path);
4173
- const effectiveCentralPath = centralPath ?? syncConfig.path;
4174
- const state = await loadSyncState(memoryDir, effectiveCentralPath);
4175
5009
  const [localFiles, remoteFiles] = await Promise.all([
4176
5010
  listSyncableFiles(memoryDir, syncConfig.include, syncConfig.exclude),
4177
5011
  listSyncableFiles(remotePath, syncConfig.include, syncConfig.exclude)
@@ -4183,11 +5017,7 @@ async function detectConflicts(memoryDir, centralPath) {
4183
5017
  unchanged: 0,
4184
5018
  localOnly: 0,
4185
5019
  remoteOnly: 0,
4186
- conflicts: 0,
4187
- newLocal: 0,
4188
- newRemote: 0,
4189
- deletedLocal: 0,
4190
- deletedRemote: 0
5020
+ localModified: 0
4191
5021
  };
4192
5022
  for (const file of allFiles) {
4193
5023
  const localPath = path14.join(memoryDir, file);
@@ -4196,12 +5026,9 @@ async function detectConflicts(memoryDir, centralPath) {
4196
5026
  getFileHashInfo(localPath),
4197
5027
  getFileHashInfo(remoteFilePath)
4198
5028
  ]);
4199
- const existingEntry = state.files[file];
4200
- const baseHash = existingEntry?.lastSyncedHash ?? null;
4201
5029
  const status2 = getFileSyncStatus(
4202
5030
  localInfo.hash ?? null,
4203
- remoteInfo.hash ?? null,
4204
- baseHash
5031
+ remoteInfo.hash ?? null
4205
5032
  );
4206
5033
  if (status2 === "unchanged") {
4207
5034
  unchanged.push(file);
@@ -4211,8 +5038,7 @@ async function detectConflicts(memoryDir, centralPath) {
4211
5038
  file,
4212
5039
  status: status2,
4213
5040
  localHash: localInfo.hash ?? null,
4214
- remoteHash: remoteInfo.hash ?? null,
4215
- baseHash
5041
+ remoteHash: remoteInfo.hash ?? null
4216
5042
  });
4217
5043
  switch (status2) {
4218
5044
  case "local-only":
@@ -4221,39 +5047,24 @@ async function detectConflicts(memoryDir, centralPath) {
4221
5047
  case "remote-only":
4222
5048
  summary.remoteOnly++;
4223
5049
  break;
4224
- case "conflict":
4225
- summary.conflicts++;
4226
- break;
4227
- case "new-local":
4228
- summary.newLocal++;
4229
- break;
4230
- case "new-remote":
4231
- summary.newRemote++;
4232
- break;
4233
- case "deleted-local":
4234
- summary.deletedLocal++;
4235
- break;
4236
- case "deleted-remote":
4237
- summary.deletedRemote++;
5050
+ case "local-modified":
5051
+ summary.localModified++;
4238
5052
  break;
4239
5053
  }
4240
5054
  }
4241
5055
  }
4242
5056
  return { changes, unchanged, summary };
4243
5057
  }
5058
+ var detectConflicts = detectChanges;
4244
5059
 
4245
5060
  // src/cli/commands/conflicts.ts
4246
- import fs13 from "fs/promises";
4247
- import path15 from "path";
4248
- import { spawn } from "child_process";
4249
5061
  async function conflictsCommand(options) {
4250
5062
  const memoryDir = resolveMemoryDir({
4251
5063
  dir: options.dir,
4252
5064
  global: options.global
4253
5065
  });
4254
5066
  if (!await isInitialized(memoryDir)) {
4255
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4256
- process.exit(1);
5067
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4257
5068
  }
4258
5069
  try {
4259
5070
  const conflicts = await listQuarantinedConflicts(memoryDir);
@@ -4278,8 +5089,8 @@ ${conflict.timestamp}:`);
4278
5089
  Total: ${conflicts.length} conflict set(s)`);
4279
5090
  console.log("\nUse 'minimem sync:resolve <timestamp>' to resolve a conflict.");
4280
5091
  } catch (error) {
4281
- console.error(`Error: ${error}`);
4282
- process.exit(1);
5092
+ const message = error instanceof Error ? error.message : String(error);
5093
+ exitWithError(message);
4283
5094
  }
4284
5095
  }
4285
5096
  async function resolveCommand(timestamp, options) {
@@ -4288,16 +5099,16 @@ async function resolveCommand(timestamp, options) {
4288
5099
  global: options.global
4289
5100
  });
4290
5101
  if (!await isInitialized(memoryDir)) {
4291
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4292
- process.exit(1);
5102
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4293
5103
  }
4294
5104
  const conflictDir = path15.join(getConflictsDir(memoryDir), timestamp);
4295
5105
  try {
4296
5106
  await fs13.access(conflictDir);
4297
5107
  } catch {
4298
- console.error(`Error: Conflict '${timestamp}' not found.`);
4299
- console.error("Use 'minimem sync:conflicts' to list available conflicts.");
4300
- process.exit(1);
5108
+ exitWithError(
5109
+ `Conflict '${timestamp}' not found.`,
5110
+ "Use 'minimem sync:conflicts' to list available conflicts."
5111
+ );
4301
5112
  }
4302
5113
  try {
4303
5114
  const files = await fs13.readdir(conflictDir);
@@ -4314,8 +5125,7 @@ async function resolveCommand(timestamp, options) {
4314
5125
  }
4315
5126
  }
4316
5127
  if (fileGroups.size === 0) {
4317
- console.error("No conflict files found in this directory.");
4318
- process.exit(1);
5128
+ exitWithError("No conflict files found in this directory.");
4319
5129
  }
4320
5130
  const mergeTool = options.tool || process.env.MERGE_TOOL || await detectMergeTool();
4321
5131
  if (!mergeTool) {
@@ -4338,10 +5148,10 @@ Resolving: ${fileName}`);
4338
5148
  if (group.local && group.remote) {
4339
5149
  const args = group.base ? [group.local, group.base, group.remote] : [group.local, group.remote];
4340
5150
  const child = spawn(mergeTool, args, { stdio: "inherit" });
4341
- await new Promise((resolve4, reject) => {
5151
+ await new Promise((resolve3, reject) => {
4342
5152
  child.on("close", (code) => {
4343
5153
  if (code === 0) {
4344
- resolve4();
5154
+ resolve3();
4345
5155
  } else {
4346
5156
  reject(new Error(`Merge tool exited with code ${code}`));
4347
5157
  }
@@ -4353,8 +5163,8 @@ Resolving: ${fileName}`);
4353
5163
  console.log("\nMerge complete. Remove conflict directory when satisfied:");
4354
5164
  console.log(` rm -rf "${conflictDir}"`);
4355
5165
  } catch (error) {
4356
- console.error(`Error: ${error}`);
4357
- process.exit(1);
5166
+ const message = error instanceof Error ? error.message : String(error);
5167
+ exitWithError(message);
4358
5168
  }
4359
5169
  }
4360
5170
  async function detectMergeTool() {
@@ -4376,8 +5186,7 @@ async function cleanupCommand(options) {
4376
5186
  global: options.global
4377
5187
  });
4378
5188
  if (!await isInitialized(memoryDir)) {
4379
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4380
- process.exit(1);
5189
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4381
5190
  }
4382
5191
  const maxAgeDays = options.days ?? 30;
4383
5192
  const cutoffDate = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3);
@@ -4420,8 +5229,8 @@ Would remove ${cleaned} conflict(s), keep ${kept}`);
4420
5229
  Removed ${cleaned} conflict(s), kept ${kept}`);
4421
5230
  }
4422
5231
  } catch (error) {
4423
- console.error(`Error: ${error}`);
4424
- process.exit(1);
5232
+ const message = error instanceof Error ? error.message : String(error);
5233
+ exitWithError(message);
4425
5234
  }
4426
5235
  }
4427
5236
  var SYNC_LOG_FILE = "sync.log";
@@ -4446,7 +5255,8 @@ async function appendSyncLog(memoryDir, entry) {
4446
5255
  const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
4447
5256
  await fs13.writeFile(logPath, content);
4448
5257
  } catch (error) {
4449
- console.error(`Warning: Failed to write sync log: ${error}`);
5258
+ const message = error instanceof Error ? error.message : String(error);
5259
+ warn(`Failed to write sync log: ${message}`);
4450
5260
  }
4451
5261
  }
4452
5262
  async function readSyncLog(memoryDir) {
@@ -4464,8 +5274,7 @@ async function logCommand(options) {
4464
5274
  global: options.global
4465
5275
  });
4466
5276
  if (!await isInitialized(memoryDir)) {
4467
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4468
- process.exit(1);
5277
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4469
5278
  }
4470
5279
  try {
4471
5280
  let entries = await readSyncLog(memoryDir);
@@ -4496,8 +5305,8 @@ async function logCommand(options) {
4496
5305
  }
4497
5306
  }
4498
5307
  } catch (error) {
4499
- console.error(`Error: ${error}`);
4500
- process.exit(1);
5308
+ const message = error instanceof Error ? error.message : String(error);
5309
+ exitWithError(message);
4501
5310
  }
4502
5311
  }
4503
5312
 
@@ -4520,20 +5329,11 @@ async function copyFileAtomic(src, dest) {
4520
5329
  throw error;
4521
5330
  }
4522
5331
  }
4523
- function keepBothMerge(localContent, remoteContent, localTimestamp, remoteTimestamp) {
4524
- return `<<<<<<< LOCAL (${localTimestamp})
4525
- ${localContent}
4526
- =======
4527
- ${remoteContent}
4528
- >>>>>>> REMOTE (${remoteTimestamp})
4529
- `;
4530
- }
4531
5332
  async function push(memoryDir, options = {}) {
4532
5333
  const result = {
4533
5334
  success: true,
4534
5335
  pushed: [],
4535
5336
  pulled: [],
4536
- conflicts: [],
4537
5337
  errors: [],
4538
5338
  skipped: []
4539
5339
  };
@@ -4550,69 +5350,56 @@ async function push(memoryDir, options = {}) {
4550
5350
  return result;
4551
5351
  }
4552
5352
  const remotePath = path16.join(centralRepo, syncConfig.path);
4553
- const detection = await detectConflicts(memoryDir);
4554
5353
  const state = await loadSyncState(memoryDir, syncConfig.path);
4555
- for (const change of detection.changes) {
4556
- const localPath = path16.join(memoryDir, change.file);
4557
- const remoteFilePath = path16.join(remotePath, change.file);
5354
+ const localFiles = await listSyncableFiles(
5355
+ memoryDir,
5356
+ syncConfig.include,
5357
+ syncConfig.exclude
5358
+ );
5359
+ const remoteFiles = await listSyncableFiles(
5360
+ remotePath,
5361
+ syncConfig.include,
5362
+ syncConfig.exclude
5363
+ );
5364
+ const allFiles = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles]);
5365
+ for (const file of allFiles) {
5366
+ const localPath = path16.join(memoryDir, file);
5367
+ const remoteFilePath = path16.join(remotePath, file);
4558
5368
  try {
4559
- switch (change.status) {
5369
+ const [localInfo, remoteInfo] = await Promise.all([
5370
+ getFileHashInfo(localPath),
5371
+ getFileHashInfo(remoteFilePath)
5372
+ ]);
5373
+ const status2 = getFileSyncStatus(
5374
+ localInfo.hash ?? null,
5375
+ remoteInfo.hash ?? null
5376
+ );
5377
+ switch (status2) {
5378
+ case "unchanged":
5379
+ break;
4560
5380
  case "local-only":
4561
- case "new-local":
5381
+ case "local-modified":
4562
5382
  if (!options.dryRun) {
4563
5383
  await copyFileAtomic(localPath, remoteFilePath);
4564
- const hash = await computeFileHash(localPath);
4565
- state.files[change.file] = {
5384
+ const hash = localInfo.hash;
5385
+ state.files[file] = {
4566
5386
  localHash: hash,
4567
5387
  remoteHash: hash,
4568
- lastSyncedHash: hash,
4569
5388
  lastModified: (/* @__PURE__ */ new Date()).toISOString()
4570
5389
  };
4571
5390
  }
4572
- result.pushed.push(change.file);
5391
+ result.pushed.push(file);
4573
5392
  break;
4574
- case "deleted-remote":
4575
- result.skipped.push(change.file);
4576
- break;
4577
- case "conflict":
4578
- if (options.force) {
4579
- if (!options.dryRun) {
4580
- await copyFileAtomic(localPath, remoteFilePath);
4581
- const hash = await computeFileHash(localPath);
4582
- state.files[change.file] = {
4583
- localHash: hash,
4584
- remoteHash: hash,
4585
- lastSyncedHash: hash,
4586
- lastModified: (/* @__PURE__ */ new Date()).toISOString()
4587
- };
4588
- }
4589
- result.pushed.push(change.file);
4590
- } else {
4591
- if (!options.dryRun && syncConfig.conflictStrategy === "keep-both") {
4592
- const localContent = await fs14.readFile(localPath, "utf-8");
4593
- const remoteContent = await fs14.readFile(remoteFilePath, "utf-8");
4594
- const merged = keepBothMerge(
4595
- localContent,
4596
- remoteContent,
4597
- (/* @__PURE__ */ new Date()).toISOString(),
4598
- (/* @__PURE__ */ new Date()).toISOString()
4599
- );
4600
- await fs14.writeFile(localPath, merged);
4601
- await fs14.writeFile(remoteFilePath, merged);
4602
- const hash = computeFileHash(localPath);
4603
- result.pushed.push(change.file);
4604
- } else {
4605
- result.conflicts.push(change.file);
4606
- result.success = false;
4607
- }
4608
- }
5393
+ case "remote-only":
5394
+ result.skipped.push(file);
4609
5395
  break;
4610
5396
  default:
4611
- result.skipped.push(change.file);
5397
+ result.skipped.push(file);
4612
5398
  break;
4613
5399
  }
4614
5400
  } catch (error) {
4615
- result.errors.push(`${change.file}: ${error}`);
5401
+ const message = error instanceof Error ? error.message : String(error);
5402
+ result.errors.push(`${file}: ${message}`);
4616
5403
  result.success = false;
4617
5404
  }
4618
5405
  }
@@ -4628,9 +5415,8 @@ async function push(memoryDir, options = {}) {
4628
5415
  const logEntry = {
4629
5416
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4630
5417
  operation: "push",
4631
- result: result.success ? result.conflicts.length > 0 ? "partial" : "success" : "failure",
5418
+ result: result.success ? "success" : "failure",
4632
5419
  pushed: result.pushed.length,
4633
- conflicts: result.conflicts.length,
4634
5420
  errors: result.errors.length > 0 ? result.errors : void 0
4635
5421
  };
4636
5422
  await appendSyncLog(memoryDir, logEntry);
@@ -4642,7 +5428,6 @@ async function pull(memoryDir, options = {}) {
4642
5428
  success: true,
4643
5429
  pushed: [],
4644
5430
  pulled: [],
4645
- conflicts: [],
4646
5431
  errors: [],
4647
5432
  skipped: []
4648
5433
  };
@@ -4659,68 +5444,71 @@ async function pull(memoryDir, options = {}) {
4659
5444
  return result;
4660
5445
  }
4661
5446
  const remotePath = path16.join(centralRepo, syncConfig.path);
4662
- const detection = await detectConflicts(memoryDir);
4663
5447
  const state = await loadSyncState(memoryDir, syncConfig.path);
4664
- for (const change of detection.changes) {
4665
- const localPath = path16.join(memoryDir, change.file);
4666
- const remoteFilePath = path16.join(remotePath, change.file);
5448
+ const localFiles = await listSyncableFiles(
5449
+ memoryDir,
5450
+ syncConfig.include,
5451
+ syncConfig.exclude
5452
+ );
5453
+ const remoteFiles = await listSyncableFiles(
5454
+ remotePath,
5455
+ syncConfig.include,
5456
+ syncConfig.exclude
5457
+ );
5458
+ const allFiles = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles]);
5459
+ for (const file of allFiles) {
5460
+ const localPath = path16.join(memoryDir, file);
5461
+ const remoteFilePath = path16.join(remotePath, file);
4667
5462
  try {
4668
- switch (change.status) {
5463
+ const [localInfo, remoteInfo] = await Promise.all([
5464
+ getFileHashInfo(localPath),
5465
+ getFileHashInfo(remoteFilePath)
5466
+ ]);
5467
+ const status2 = getFileSyncStatus(
5468
+ localInfo.hash ?? null,
5469
+ remoteInfo.hash ?? null
5470
+ );
5471
+ switch (status2) {
5472
+ case "unchanged":
5473
+ break;
4669
5474
  case "remote-only":
4670
- case "new-remote":
4671
5475
  if (!options.dryRun) {
4672
5476
  await copyFileAtomic(remoteFilePath, localPath);
4673
- const hash = await computeFileHash(localPath);
4674
- state.files[change.file] = {
5477
+ const hash = remoteInfo.hash;
5478
+ state.files[file] = {
4675
5479
  localHash: hash,
4676
5480
  remoteHash: hash,
4677
- lastSyncedHash: hash,
4678
5481
  lastModified: (/* @__PURE__ */ new Date()).toISOString()
4679
5482
  };
4680
5483
  }
4681
- result.pulled.push(change.file);
4682
- break;
4683
- case "deleted-local":
4684
- result.skipped.push(change.file);
5484
+ result.pulled.push(file);
4685
5485
  break;
4686
- case "conflict":
4687
- if (options.force) {
5486
+ case "local-modified":
5487
+ if (options.force || !localInfo.exists) {
4688
5488
  if (!options.dryRun) {
4689
5489
  await copyFileAtomic(remoteFilePath, localPath);
4690
- const hash = await computeFileHash(localPath);
4691
- state.files[change.file] = {
5490
+ const hash = remoteInfo.hash;
5491
+ state.files[file] = {
4692
5492
  localHash: hash,
4693
5493
  remoteHash: hash,
4694
- lastSyncedHash: hash,
4695
5494
  lastModified: (/* @__PURE__ */ new Date()).toISOString()
4696
5495
  };
4697
5496
  }
4698
- result.pulled.push(change.file);
5497
+ result.pulled.push(file);
4699
5498
  } else {
4700
- if (!options.dryRun && syncConfig.conflictStrategy === "keep-both") {
4701
- const localContent = await fs14.readFile(localPath, "utf-8");
4702
- const remoteContent = await fs14.readFile(remoteFilePath, "utf-8");
4703
- const merged = keepBothMerge(
4704
- localContent,
4705
- remoteContent,
4706
- (/* @__PURE__ */ new Date()).toISOString(),
4707
- (/* @__PURE__ */ new Date()).toISOString()
4708
- );
4709
- await fs14.writeFile(localPath, merged);
4710
- await fs14.writeFile(remoteFilePath, merged);
4711
- result.pulled.push(change.file);
4712
- } else {
4713
- result.conflicts.push(change.file);
4714
- result.success = false;
4715
- }
5499
+ result.skipped.push(file);
4716
5500
  }
4717
5501
  break;
5502
+ case "local-only":
5503
+ result.skipped.push(file);
5504
+ break;
4718
5505
  default:
4719
- result.skipped.push(change.file);
5506
+ result.skipped.push(file);
4720
5507
  break;
4721
5508
  }
4722
5509
  } catch (error) {
4723
- result.errors.push(`${change.file}: ${error}`);
5510
+ const message = error instanceof Error ? error.message : String(error);
5511
+ result.errors.push(`${file}: ${message}`);
4724
5512
  result.success = false;
4725
5513
  }
4726
5514
  }
@@ -4736,9 +5524,8 @@ async function pull(memoryDir, options = {}) {
4736
5524
  const logEntry = {
4737
5525
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4738
5526
  operation: "pull",
4739
- result: result.success ? result.conflicts.length > 0 ? "partial" : "success" : "failure",
5527
+ result: result.success ? "success" : "failure",
4740
5528
  pulled: result.pulled.length,
4741
- conflicts: result.conflicts.length,
4742
5529
  errors: result.errors.length > 0 ? result.errors : void 0
4743
5530
  };
4744
5531
  await appendSyncLog(memoryDir, logEntry);
@@ -4753,8 +5540,7 @@ async function pushCommand(options) {
4753
5540
  global: options.global
4754
5541
  });
4755
5542
  if (!await isInitialized(memoryDir)) {
4756
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4757
- process.exit(1);
5543
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4758
5544
  }
4759
5545
  console.log(`Pushing from ${formatPath(memoryDir)}...`);
4760
5546
  if (options.dryRun) {
@@ -4798,8 +5584,8 @@ Errors:`);
4798
5584
  console.log("Nothing to push - already in sync");
4799
5585
  }
4800
5586
  } catch (error) {
4801
- console.error(`Error: ${error}`);
4802
- process.exit(1);
5587
+ const message = error instanceof Error ? error.message : String(error);
5588
+ exitWithError(message);
4803
5589
  }
4804
5590
  }
4805
5591
  async function pullCommand(options) {
@@ -4808,8 +5594,7 @@ async function pullCommand(options) {
4808
5594
  global: options.global
4809
5595
  });
4810
5596
  if (!await isInitialized(memoryDir)) {
4811
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4812
- process.exit(1);
5597
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4813
5598
  }
4814
5599
  console.log(`Pulling to ${formatPath(memoryDir)}...`);
4815
5600
  if (options.dryRun) {
@@ -4853,8 +5638,8 @@ Errors:`);
4853
5638
  console.log("Nothing to pull - already in sync");
4854
5639
  }
4855
5640
  } catch (error) {
4856
- console.error(`Error: ${error}`);
4857
- process.exit(1);
5641
+ const message = error instanceof Error ? error.message : String(error);
5642
+ exitWithError(message);
4858
5643
  }
4859
5644
  }
4860
5645
  async function syncStatusCommand(options) {
@@ -4863,8 +5648,7 @@ async function syncStatusCommand(options) {
4863
5648
  global: options.global
4864
5649
  });
4865
5650
  if (!await isInitialized(memoryDir)) {
4866
- console.error(`Error: ${formatPath(memoryDir)} is not initialized.`);
4867
- process.exit(1);
5651
+ exitWithError(`${formatPath(memoryDir)} is not initialized.`);
4868
5652
  }
4869
5653
  try {
4870
5654
  const detection = await detectConflicts(memoryDir);
@@ -4906,8 +5690,8 @@ async function syncStatusCommand(options) {
4906
5690
  }
4907
5691
  }
4908
5692
  } catch (error) {
4909
- console.error(`Error: ${error}`);
4910
- process.exit(1);
5693
+ const message = error instanceof Error ? error.message : String(error);
5694
+ exitWithError(message);
4911
5695
  }
4912
5696
  }
4913
5697
 
@@ -4917,7 +5701,7 @@ import fs17 from "fs/promises";
4917
5701
  // src/cli/sync/daemon.ts
4918
5702
  import fs16 from "fs/promises";
4919
5703
  import path19 from "path";
4920
- import os8 from "os";
5704
+ import os6 from "os";
4921
5705
 
4922
5706
  // src/cli/sync/watcher.ts
4923
5707
  import chokidar2 from "chokidar";
@@ -5107,7 +5891,7 @@ async function validateRegistry() {
5107
5891
  result.stats.activeMappings++;
5108
5892
  }
5109
5893
  if (mapping.machineId === currentMachineId) {
5110
- const localPath = expandPath4(mapping.localPath);
5894
+ const localPath = expandPath3(mapping.localPath);
5111
5895
  try {
5112
5896
  await fs15.access(localPath);
5113
5897
  } catch {
@@ -5125,7 +5909,7 @@ async function validateRegistry() {
5125
5909
  }
5126
5910
  return result;
5127
5911
  }
5128
- function expandPath4(p) {
5912
+ function expandPath3(p) {
5129
5913
  if (p.startsWith("~/")) {
5130
5914
  return path18.join(process.env.HOME || "", p.slice(2));
5131
5915
  }
@@ -5167,7 +5951,7 @@ var DAEMON_LOG_FILE = "daemon.log";
5167
5951
  var PID_FILE = "daemon.pid";
5168
5952
  var MAX_LOG_SIZE = 1024 * 1024;
5169
5953
  function getDaemonDir() {
5170
- return path19.join(os8.homedir(), ".minimem");
5954
+ return path19.join(os6.homedir(), ".minimem");
5171
5955
  }
5172
5956
  function getPidFilePath() {
5173
5957
  return path19.join(getDaemonDir(), PID_FILE);
@@ -5414,8 +6198,8 @@ async function daemonCommand(options) {
5414
6198
  try {
5415
6199
  await startDaemon();
5416
6200
  } catch (error) {
5417
- console.error(`Daemon error: ${error}`);
5418
- process.exit(1);
6201
+ const message = error instanceof Error ? error.message : String(error);
6202
+ exitWithError(`Daemon: ${message}`);
5419
6203
  }
5420
6204
  return;
5421
6205
  }
@@ -5431,8 +6215,8 @@ async function daemonCommand(options) {
5431
6215
  console.log(`Daemon started with PID ${pid}`);
5432
6216
  console.log(`Log file: ${getDaemonLogPath()}`);
5433
6217
  } catch (error) {
5434
- console.error(`Failed to start daemon: ${error}`);
5435
- process.exit(1);
6218
+ const message = error instanceof Error ? error.message : String(error);
6219
+ exitWithError(`Failed to start daemon: ${message}`);
5436
6220
  }
5437
6221
  } else {
5438
6222
  console.log("Starting daemon in foreground...");
@@ -5441,8 +6225,8 @@ async function daemonCommand(options) {
5441
6225
  try {
5442
6226
  await startDaemon();
5443
6227
  } catch (error) {
5444
- console.error(`Daemon error: ${error}`);
5445
- process.exit(1);
6228
+ const message = error instanceof Error ? error.message : String(error);
6229
+ exitWithError(`Daemon: ${message}`);
5446
6230
  }
5447
6231
  }
5448
6232
  }
@@ -5457,8 +6241,7 @@ async function daemonStopCommand() {
5457
6241
  if (stopped) {
5458
6242
  console.log("Daemon stopped.");
5459
6243
  } else {
5460
- console.error("Failed to stop daemon.");
5461
- process.exit(1);
6244
+ exitWithError("Failed to stop daemon.");
5462
6245
  }
5463
6246
  }
5464
6247
  async function daemonStatusCommand() {
@@ -5511,21 +6294,37 @@ async function daemonLogsCommand(options) {
5511
6294
  if (error.code === "ENOENT") {
5512
6295
  console.log("No daemon log found.");
5513
6296
  } else {
5514
- console.error(`Error reading log: ${error}`);
5515
- process.exit(1);
6297
+ const message = error instanceof Error ? error.message : String(error);
6298
+ exitWithError(`Error reading log: ${message}`);
5516
6299
  }
5517
6300
  }
5518
6301
  }
5519
6302
 
6303
+ // src/cli/version.ts
6304
+ import { readFileSync } from "fs";
6305
+ import { dirname as dirname3, join as join4 } from "path";
6306
+ import { fileURLToPath } from "url";
6307
+ function getPackageVersion() {
6308
+ try {
6309
+ const __filename = fileURLToPath(import.meta.url);
6310
+ const __dirname = dirname3(__filename);
6311
+ const packagePath = join4(__dirname, "../../package.json");
6312
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
6313
+ return packageJson.version || "0.0.0";
6314
+ } catch {
6315
+ return "0.0.0";
6316
+ }
6317
+ }
6318
+ var VERSION = getPackageVersion();
6319
+
5520
6320
  // src/cli/index.ts
5521
- var VERSION = "0.0.2";
5522
6321
  program.name("minimem").description("File-based memory system with vector search for AI agents").version(VERSION);
5523
6322
  program.command("init [dir]").description("Initialize a memory directory").option("-g, --global", "Use ~/.minimem as global memory directory").option("-f, --force", "Reinitialize even if already initialized").action(init);
5524
6323
  program.command("search <query>").description("Semantic search through memory files").option("-d, --dir <path...>", "Memory directories (can specify multiple)").option("-g, --global", "Include ~/.minimem in search").option("-n, --max <number>", "Maximum results (default: 10)").option("-s, --min-score <number>", "Minimum score threshold 0-1 (default: 0.3)").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").option("--json", "Output results as JSON").action(search);
5525
6324
  program.command("sync").description("Force re-index memory files").option("-d, --dir <path>", "Memory directory").option("-g, --global", "Use ~/.minimem").option("-f, --force", "Force full re-index").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").action(sync);
5526
6325
  program.command("status").description("Show index stats and provider info").option("-d, --dir <path>", "Memory directory").option("-g, --global", "Use ~/.minimem").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").option("--json", "Output as JSON").action(status);
5527
6326
  program.command("append <text>").description("Append text to today's daily log (memory/YYYY-MM-DD.md)").option("-d, --dir <path>", "Memory directory").option("-g, --global", "Use ~/.minimem").option("-f, --file <path>", "Append to specific file instead of today's log").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").option("-s, --session <id>", "Session ID to associate with this memory").option("--session-source <name>", "Session source (claude-code, vscode, etc.)").action(append);
5528
- program.command("upsert <file> [content]").description("Create or update a memory file").option("-d, --dir <path>", "Memory directory").option("-g, --global", "Use ~/.minimem").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").option("--stdin", "Read content from stdin").option("-s, --session <id>", "Session ID to associate with this memory").option("--session-source <name>", "Session source (claude-code, vscode, etc.)").action(upsert);
6327
+ program.command("upsert <file> [content]").description("Create or update a memory file (file path relative to memory dir)").option("-d, --dir <path>", "Memory directory").option("-g, --global", "Use ~/.minimem").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").option("--stdin", "Read content from stdin").option("-s, --session <id>", "Session ID to associate with this memory").option("--session-source <name>", "Session source (claude-code, vscode, etc.)").action(upsert);
5529
6328
  program.command("mcp").description("Run as MCP server over stdio (for Claude Desktop, Cursor, etc.)").option("-d, --dir <path...>", "Memory directories (can specify multiple)").option("-g, --global", "Include ~/.minimem").option("-p, --provider <name>", "Embedding provider (openai, gemini, local, auto)").action(mcp);
5530
6329
  program.command("config").description("View or modify configuration").option("-d, --dir <path>", "Memory directory").option("-g, --global", "Use ~/.minimem").option("--xdg-global", "Modify ~/.config/minimem/config.json (for sync settings)").option("--json", "Output as JSON").option("--set <key=value>", "Set a config value (e.g., embedding.provider=openai)").option("--unset <key>", "Remove a config value").action(config);
5531
6330
  program.command("sync:init-central <path>").description("Initialize a central repository for syncing memories").option("-f, --force", "Force creation even if directory exists").action(syncInitCentral);
@@ -5555,8 +6354,8 @@ program.command("sync:validate").description("Validate registry for collisions a
5555
6354
  process.exit(1);
5556
6355
  }
5557
6356
  } catch (error) {
5558
- console.error(`Error: ${error}`);
5559
- process.exit(1);
6357
+ const message = error instanceof Error ? error.message : String(error);
6358
+ exitWithError(message);
5560
6359
  }
5561
6360
  });
5562
6361
  program.parse();