minimem 0.0.7 → 0.1.0

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
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ Minimem,
4
+ addFrontmatter,
5
+ parseFrontmatter
6
+ } from "./chunk-BIYUNXYX.js";
2
7
 
3
8
  // src/cli/index.ts
4
9
  import { program } from "commander";
5
10
 
6
11
  // src/cli/commands/init.ts
7
- import fs3 from "fs/promises";
8
- import path3 from "path";
12
+ import fs5 from "fs/promises";
13
+ import path5 from "path";
9
14
 
10
15
  // src/cli/config.ts
11
16
  import fs2 from "fs/promises";
@@ -313,2768 +318,304 @@ function buildMinimemConfig(memoryDir, cliConfig, options) {
313
318
  local: merged.embedding?.local
314
319
  };
315
320
  return {
316
- memoryDir,
317
- embedding,
318
- hybrid: merged.hybrid,
319
- query: merged.query,
320
- chunking: merged.chunking,
321
- watch: {
322
- enabled: options?.watch ?? false
323
- // Disable watching by default in CLI
324
- }
325
- };
326
- }
327
- async function isInitialized(memoryDir) {
328
- const configPath = getConfigPath(memoryDir);
329
- try {
330
- await fs2.access(configPath);
331
- return true;
332
- } catch {
333
- return false;
334
- }
335
- }
336
- function formatPath(filePath) {
337
- const home = os2.homedir();
338
- if (filePath.startsWith(home)) {
339
- return "~" + filePath.slice(home.length);
340
- }
341
- return filePath;
342
- }
343
-
344
- // src/cli/commands/init.ts
345
- var MEMORY_TEMPLATE = `# Memory
346
-
347
- This is your memory file. Add notes, decisions, and context here.
348
-
349
- ## Quick Start
350
-
351
- - Add daily logs in the \`memory/\` directory (e.g., \`memory/2024-01-15.md\`)
352
- - Use \`minimem search <query>\` to find relevant memories
353
- - Use \`minimem append <text>\` to quickly add to today's log
354
-
355
- ## Notes
356
-
357
- `;
358
- async function init(dir, options) {
359
- const memoryDir = resolveMemoryDir({ dir, global: options.global });
360
- const displayPath = formatPath(memoryDir);
361
- if (!options.force && await isInitialized(memoryDir)) {
362
- console.log(`Already initialized: ${displayPath}`);
363
- console.log("Use --force to reinitialize");
364
- return;
365
- }
366
- console.log(`Initializing minimem in ${displayPath}...`);
367
- await fs3.mkdir(memoryDir, { recursive: true });
368
- await fs3.mkdir(path3.join(memoryDir, "memory"), { recursive: true });
369
- const memoryFilePath = path3.join(memoryDir, "MEMORY.md");
370
- try {
371
- await fs3.access(memoryFilePath);
372
- console.log(" MEMORY.md already exists, skipping");
373
- } catch {
374
- await fs3.writeFile(memoryFilePath, MEMORY_TEMPLATE, "utf-8");
375
- console.log(" Created MEMORY.md");
376
- }
377
- const config2 = getInitConfig();
378
- const configPath = path3.join(memoryDir, "config.json");
379
- await fs3.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
380
- console.log(" Created config.json");
381
- const gitignorePath = path3.join(memoryDir, ".gitignore");
382
- await fs3.writeFile(gitignorePath, "index.db\nindex.db-*\n", "utf-8");
383
- console.log(" Created .gitignore");
384
- console.log();
385
- console.log("Done! Your memory directory is ready.");
386
- console.log();
387
- console.log("Next steps:");
388
- console.log(` 1. Set your embedding API key:`);
389
- console.log(` export OPENAI_API_KEY=your-key`);
390
- console.log(` # or: export GOOGLE_API_KEY=your-key`);
391
- console.log();
392
- console.log(` 2. Add some memories to MEMORY.md or memory/*.md`);
393
- console.log();
394
- console.log(` 3. Search your memories:`);
395
- console.log(` minimem search "your query"${dir ? ` --dir ${dir}` : ""}`);
396
- }
397
-
398
- // src/minimem.ts
399
- import { randomUUID } from "crypto";
400
- import fs5 from "fs/promises";
401
- import fsSync4 from "fs";
402
- import path6 from "path";
403
- import { DatabaseSync } from "node:sqlite";
404
- import chokidar from "chokidar";
405
-
406
- // src/internal.ts
407
- import crypto2 from "crypto";
408
- import fsSync2 from "fs";
409
- import fs4 from "fs/promises";
410
- import path4 from "path";
411
- function logError2(context, error, debug) {
412
- if (!debug) return;
413
- const message = error instanceof Error ? error.message : String(error);
414
- debug(`[${context}] Error: ${message}`);
415
- }
416
- function ensureDir(dir, debug) {
417
- try {
418
- fsSync2.mkdirSync(dir, { recursive: true });
419
- } catch (error) {
420
- const nodeError = error;
421
- if (nodeError.code !== "EEXIST") {
422
- logError2("ensureDir", error, debug);
423
- }
424
- }
425
- return dir;
426
- }
427
- async function exists(filePath) {
428
- try {
429
- await fs4.access(filePath);
430
- return true;
431
- } catch {
432
- return false;
433
- }
434
- }
435
- async function walkDir(dir, files) {
436
- const entries = await fs4.readdir(dir, { withFileTypes: true });
437
- for (const entry of entries) {
438
- const full = path4.join(dir, entry.name);
439
- if (entry.isDirectory()) {
440
- await walkDir(full, files);
441
- continue;
442
- }
443
- if (!entry.isFile()) continue;
444
- if (!entry.name.endsWith(".md")) continue;
445
- files.push(full);
446
- }
447
- }
448
- async function listMemoryFiles(memoryDir) {
449
- const result = [];
450
- const memoryFile = path4.join(memoryDir, "MEMORY.md");
451
- const altMemoryFile = path4.join(memoryDir, "memory.md");
452
- const hasUpper = await exists(memoryFile);
453
- const hasLower = await exists(altMemoryFile);
454
- if (hasUpper && hasLower) {
455
- let upperReal = memoryFile;
456
- let lowerReal = altMemoryFile;
457
- try {
458
- upperReal = await fs4.realpath(memoryFile);
459
- } catch {
460
- }
461
- try {
462
- lowerReal = await fs4.realpath(altMemoryFile);
463
- } catch {
464
- }
465
- if (upperReal !== lowerReal) {
466
- throw new Error(
467
- `Both MEMORY.md and memory.md exist in ${memoryDir}. Please remove one to avoid ambiguity.`
468
- );
469
- }
470
- result.push(memoryFile);
471
- } else if (hasUpper) {
472
- result.push(memoryFile);
473
- } else if (hasLower) {
474
- result.push(altMemoryFile);
475
- }
476
- const memorySubDir = path4.join(memoryDir, "memory");
477
- if (await exists(memorySubDir)) {
478
- await walkDir(memorySubDir, result);
479
- }
480
- if (result.length <= 1) return result;
481
- const seen = /* @__PURE__ */ new Set();
482
- const deduped = [];
483
- for (const entry of result) {
484
- let key = entry;
485
- try {
486
- key = await fs4.realpath(entry);
487
- } catch {
488
- }
489
- if (seen.has(key)) continue;
490
- seen.add(key);
491
- deduped.push(entry);
492
- }
493
- return deduped;
494
- }
495
- function hashText(value) {
496
- return crypto2.createHash("sha256").update(value).digest("hex");
497
- }
498
- async function buildFileEntry(absPath, memoryDir) {
499
- const stat = await fs4.stat(absPath);
500
- const content = await fs4.readFile(absPath, "utf-8");
501
- const hash = hashText(content);
502
- return {
503
- path: path4.relative(memoryDir, absPath).replace(/\\/g, "/"),
504
- absPath,
505
- mtimeMs: stat.mtimeMs,
506
- size: stat.size,
507
- hash
508
- };
509
- }
510
- function stripPrivateContent(content) {
511
- return content.replace(/<private>[\s\S]*?<\/private>/gi, (match) => {
512
- const lineCount = match.split("\n").length;
513
- return "\n".repeat(lineCount - 1);
514
- });
515
- }
516
- function chunkMarkdown(content, chunking) {
517
- const stripped = stripPrivateContent(content);
518
- const lines = stripped.split("\n");
519
- if (lines.length === 0) return [];
520
- const maxChars = Math.max(32, chunking.tokens * 4);
521
- const overlapChars = Math.max(0, chunking.overlap * 4);
522
- const chunks = [];
523
- let current = [];
524
- let currentChars = 0;
525
- const flush = () => {
526
- if (current.length === 0) return;
527
- const firstEntry = current[0];
528
- const lastEntry = current[current.length - 1];
529
- if (!firstEntry || !lastEntry) return;
530
- const text = current.map((entry) => entry.line).join("\n");
531
- const startLine = firstEntry.lineNo;
532
- const endLine = lastEntry.lineNo;
533
- chunks.push({
534
- startLine,
535
- endLine,
536
- text,
537
- hash: hashText(text)
538
- });
539
- };
540
- const carryOverlap = () => {
541
- if (overlapChars <= 0 || current.length === 0) {
542
- current = [];
543
- currentChars = 0;
544
- return;
545
- }
546
- let acc = 0;
547
- const kept = [];
548
- for (let i = current.length - 1; i >= 0; i -= 1) {
549
- const entry = current[i];
550
- if (!entry) continue;
551
- acc += entry.line.length + 1;
552
- kept.unshift(entry);
553
- if (acc >= overlapChars) break;
554
- }
555
- current = kept;
556
- currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0);
557
- };
558
- for (let i = 0; i < lines.length; i += 1) {
559
- const line = lines[i] ?? "";
560
- const lineNo = i + 1;
561
- const segments = [];
562
- if (line.length === 0) {
563
- segments.push("");
564
- } else {
565
- for (let start = 0; start < line.length; start += maxChars) {
566
- segments.push(line.slice(start, start + maxChars));
567
- }
568
- }
569
- for (const segment of segments) {
570
- const lineSize = segment.length + 1;
571
- if (currentChars + lineSize > maxChars && current.length > 0) {
572
- flush();
573
- carryOverlap();
574
- }
575
- current.push({ line: segment, lineNo });
576
- currentChars += lineSize;
577
- }
578
- }
579
- flush();
580
- return chunks;
581
- }
582
- function extractChunkMetadata(text) {
583
- const typeMatch = text.match(/<!--\s*type:\s*([\w-]+)\s*-->/i);
584
- return typeMatch ? { type: typeMatch[1].toLowerCase() } : {};
585
- }
586
- function parseEmbedding(raw) {
587
- try {
588
- const parsed = JSON.parse(raw);
589
- return Array.isArray(parsed) ? parsed : [];
590
- } catch {
591
- return [];
592
- }
593
- }
594
- function cosineSimilarity(a, b) {
595
- if (a.length === 0 || b.length === 0) return 0;
596
- const len = Math.min(a.length, b.length);
597
- let dot = 0;
598
- let normA = 0;
599
- let normB = 0;
600
- for (let i = 0; i < len; i += 1) {
601
- const av = a[i] ?? 0;
602
- const bv = b[i] ?? 0;
603
- dot += av * bv;
604
- normA += av * av;
605
- normB += bv * bv;
606
- }
607
- if (normA === 0 || normB === 0) return 0;
608
- return dot / (Math.sqrt(normA) * Math.sqrt(normB));
609
- }
610
- function truncateUtf16Safe(text, maxChars) {
611
- if (text.length <= maxChars) return text;
612
- return text.slice(0, maxChars);
613
- }
614
- function vectorToBlob(embedding) {
615
- return Buffer.from(new Float32Array(embedding).buffer);
616
- }
617
-
618
- // src/search/hybrid.ts
619
- function buildFtsQuery(raw) {
620
- const tokens = raw.match(/[A-Za-z0-9_]+/g)?.map((t) => t.trim()).filter(Boolean) ?? [];
621
- if (tokens.length === 0) return null;
622
- const quoted = tokens.map((t) => `"${t.replaceAll('"', "")}"`);
623
- return quoted.join(" AND ");
624
- }
625
- function bm25RankToScore(rank) {
626
- if (!Number.isFinite(rank)) {
627
- return 0;
628
- }
629
- const absRank = Math.abs(rank);
630
- return 1 / (1 + absRank);
631
- }
632
- function mergeHybridResults(params) {
633
- const byId = /* @__PURE__ */ new Map();
634
- for (const r of params.vector) {
635
- byId.set(r.id, {
636
- id: r.id,
637
- path: r.path,
638
- startLine: r.startLine,
639
- endLine: r.endLine,
640
- source: r.source,
641
- snippet: r.snippet,
642
- vectorScore: r.vectorScore,
643
- textScore: 0
644
- });
645
- }
646
- for (const r of params.keyword) {
647
- const existing = byId.get(r.id);
648
- if (existing) {
649
- existing.textScore = r.textScore;
650
- if (r.snippet && r.snippet.length > 0) existing.snippet = r.snippet;
651
- } else {
652
- byId.set(r.id, {
653
- id: r.id,
654
- path: r.path,
655
- startLine: r.startLine,
656
- endLine: r.endLine,
657
- source: r.source,
658
- snippet: r.snippet,
659
- vectorScore: 0,
660
- textScore: r.textScore
661
- });
662
- }
663
- }
664
- let vw = params.vectorWeight;
665
- let tw = params.textWeight;
666
- if (params.vector.length === 0 && params.keyword.length > 0) {
667
- vw = 0;
668
- tw = 1;
669
- } else if (params.keyword.length === 0 && params.vector.length > 0) {
670
- vw = 1;
671
- tw = 0;
672
- }
673
- const merged = Array.from(byId.values()).map((entry) => {
674
- const score = vw * entry.vectorScore + tw * entry.textScore;
675
- return {
676
- path: entry.path,
677
- startLine: entry.startLine,
678
- endLine: entry.endLine,
679
- score,
680
- snippet: entry.snippet,
681
- source: entry.source
682
- };
683
- });
684
- return merged.sort((a, b) => b.score - a.score);
685
- }
686
-
687
- // src/search/search.ts
688
- function buildKnowledgeFilterSql(opts) {
689
- const clauses = [];
690
- const params = [];
691
- if (opts.knowledgeType) {
692
- clauses.push(` AND c.knowledge_type = ?`);
693
- params.push(opts.knowledgeType);
694
- }
695
- if (opts.minConfidence !== void 0) {
696
- clauses.push(` AND c.confidence >= ?`);
697
- params.push(opts.minConfidence);
698
- }
699
- if (opts.domain && opts.domain.length > 0) {
700
- const domainPlaceholders = opts.domain.map(() => "?").join(", ");
701
- clauses.push(
702
- ` AND EXISTS (SELECT 1 FROM json_each(c.domains) AS d WHERE d.value IN (${domainPlaceholders}))`
703
- );
704
- params.push(...opts.domain);
705
- }
706
- if (opts.entities && opts.entities.length > 0) {
707
- const entityPlaceholders = opts.entities.map(() => "?").join(", ");
708
- clauses.push(
709
- ` AND EXISTS (SELECT 1 FROM json_each(c.entities) AS e WHERE e.value IN (${entityPlaceholders}))`
710
- );
711
- params.push(...opts.entities);
712
- }
713
- return { sql: clauses.join(""), params };
714
- }
715
- async function searchVector(params) {
716
- if (params.queryVec.length === 0 || params.limit <= 0) return [];
717
- if (await params.ensureVectorReady(params.queryVec.length)) {
718
- const rows = params.db.prepare(
719
- `SELECT c.id, c.path, c.start_line, c.end_line, c.text,
720
- c.source,
721
- vec_distance_cosine(v.embedding, ?) AS dist
722
- FROM ${params.vectorTable} v
723
- JOIN chunks c ON c.id = v.id
724
- WHERE c.model = ?${params.sourceFilterVec.sql}
725
- ORDER BY dist ASC
726
- LIMIT ?`
727
- ).all(
728
- vectorToBlob(params.queryVec),
729
- params.providerModel,
730
- ...params.sourceFilterVec.params,
731
- params.limit
732
- );
733
- return rows.map((row) => ({
734
- id: row.id,
735
- path: row.path,
736
- startLine: row.start_line,
737
- endLine: row.end_line,
738
- score: 1 - row.dist,
739
- snippet: truncateUtf16Safe(row.text, params.snippetMaxChars),
740
- source: row.source
741
- }));
742
- }
743
- const candidates = listChunks({
744
- db: params.db,
745
- providerModel: params.providerModel,
746
- sourceFilter: params.sourceFilterChunks
747
- });
748
- const scored = candidates.map((chunk) => ({
749
- chunk,
750
- score: cosineSimilarity(params.queryVec, chunk.embedding)
751
- })).filter((entry) => Number.isFinite(entry.score));
752
- return scored.sort((a, b) => b.score - a.score).slice(0, params.limit).map((entry) => ({
753
- id: entry.chunk.id,
754
- path: entry.chunk.path,
755
- startLine: entry.chunk.startLine,
756
- endLine: entry.chunk.endLine,
757
- score: entry.score,
758
- snippet: truncateUtf16Safe(entry.chunk.text, params.snippetMaxChars),
759
- source: entry.chunk.source
760
- }));
761
- }
762
- function listChunks(params) {
763
- const rows = params.db.prepare(
764
- `SELECT id, path, start_line, end_line, text, embedding, source
765
- FROM chunks
766
- WHERE model = ?${params.sourceFilter.sql}`
767
- ).all(params.providerModel, ...params.sourceFilter.params);
768
- return rows.map((row) => ({
769
- id: row.id,
770
- path: row.path,
771
- startLine: row.start_line,
772
- endLine: row.end_line,
773
- text: row.text,
774
- embedding: parseEmbedding(row.embedding),
775
- source: row.source
776
- }));
777
- }
778
- async function searchKeyword(params) {
779
- if (params.limit <= 0) return [];
780
- const ftsQuery = params.buildFtsQuery(params.query);
781
- if (!ftsQuery) return [];
782
- const rows = params.db.prepare(
783
- `SELECT id, path, source, start_line, end_line, text,
784
- bm25(${params.ftsTable}) AS rank
785
- FROM ${params.ftsTable}
786
- WHERE ${params.ftsTable} MATCH ? AND model = ?${params.sourceFilter.sql}
787
- ORDER BY rank ASC
788
- LIMIT ?`
789
- ).all(ftsQuery, params.providerModel, ...params.sourceFilter.params, params.limit);
790
- return rows.map((row) => {
791
- const textScore = params.bm25RankToScore(row.rank);
792
- return {
793
- id: row.id,
794
- path: row.path,
795
- startLine: row.start_line,
796
- endLine: row.end_line,
797
- score: textScore,
798
- textScore,
799
- snippet: truncateUtf16Safe(row.text, params.snippetMaxChars),
800
- source: row.source
801
- };
802
- });
803
- }
804
-
805
- // src/db/schema.ts
806
- var SCHEMA_VERSION = 4;
807
- function ensureMemoryIndexSchema(params) {
808
- params.db.exec(`
809
- CREATE TABLE IF NOT EXISTS meta (
810
- key TEXT PRIMARY KEY,
811
- value TEXT NOT NULL
812
- );
813
- `);
814
- const migrated = migrateIfNeeded(params.db, params.ftsTable);
815
- params.db.exec(`
816
- CREATE TABLE IF NOT EXISTS files (
817
- path TEXT PRIMARY KEY,
818
- source TEXT NOT NULL DEFAULT 'memory',
819
- hash TEXT NOT NULL,
820
- mtime INTEGER NOT NULL,
821
- size INTEGER NOT NULL
822
- );
823
- `);
824
- params.db.exec(`
825
- CREATE TABLE IF NOT EXISTS chunks (
826
- id TEXT PRIMARY KEY,
827
- path TEXT NOT NULL,
828
- source TEXT NOT NULL DEFAULT 'memory',
829
- start_line INTEGER NOT NULL,
830
- end_line INTEGER NOT NULL,
831
- hash TEXT NOT NULL,
832
- model TEXT NOT NULL,
833
- text TEXT NOT NULL,
834
- embedding TEXT NOT NULL,
835
- updated_at INTEGER NOT NULL
836
- );
837
- `);
838
- params.db.exec(`
839
- CREATE TABLE IF NOT EXISTS ${params.embeddingCacheTable} (
840
- provider TEXT NOT NULL,
841
- model TEXT NOT NULL,
842
- provider_key TEXT NOT NULL,
843
- hash TEXT NOT NULL,
844
- embedding TEXT NOT NULL,
845
- dims INTEGER,
846
- updated_at INTEGER NOT NULL,
847
- PRIMARY KEY (provider, model, provider_key, hash)
848
- );
849
- `);
850
- params.db.exec(
851
- `CREATE INDEX IF NOT EXISTS idx_embedding_cache_updated_at ON ${params.embeddingCacheTable}(updated_at);`
852
- );
853
- let ftsAvailable = false;
854
- let ftsError;
855
- if (params.ftsEnabled) {
856
- try {
857
- params.db.exec(
858
- `CREATE VIRTUAL TABLE IF NOT EXISTS ${params.ftsTable} USING fts5(
859
- text,
860
- id UNINDEXED,
861
- path UNINDEXED,
862
- source UNINDEXED,
863
- model UNINDEXED,
864
- start_line UNINDEXED,
865
- end_line UNINDEXED
866
- );`
867
- );
868
- ftsAvailable = true;
869
- } catch (err) {
870
- const message = err instanceof Error ? err.message : String(err);
871
- ftsAvailable = false;
872
- ftsError = message;
873
- }
874
- }
875
- ensureColumn(params.db, "files", "source", "TEXT NOT NULL DEFAULT 'memory'");
876
- ensureColumn(params.db, "chunks", "source", "TEXT NOT NULL DEFAULT 'memory'");
877
- ensureColumn(params.db, "chunks", "type", "TEXT");
878
- ensureColumn(params.db, "chunks", "knowledge_type", "TEXT");
879
- ensureColumn(params.db, "chunks", "knowledge_id", "TEXT");
880
- ensureColumn(params.db, "chunks", "domains", "TEXT");
881
- ensureColumn(params.db, "chunks", "entities", "TEXT");
882
- ensureColumn(params.db, "chunks", "confidence", "REAL");
883
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_path ON chunks(path);`);
884
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source);`);
885
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_type ON chunks(type);`);
886
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_knowledge_type ON chunks(knowledge_type);`);
887
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_knowledge_id ON chunks(knowledge_id);`);
888
- params.db.exec(`
889
- CREATE TABLE IF NOT EXISTS knowledge_links (
890
- from_id TEXT NOT NULL,
891
- to_id TEXT NOT NULL,
892
- relation TEXT NOT NULL,
893
- layer TEXT,
894
- weight REAL DEFAULT 0.5,
895
- source_path TEXT,
896
- created_at INTEGER,
897
- PRIMARY KEY (from_id, to_id, relation)
898
- );
899
- `);
900
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_kl_from ON knowledge_links(from_id);`);
901
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_kl_to ON knowledge_links(to_id);`);
902
- params.db.exec(`CREATE INDEX IF NOT EXISTS idx_kl_layer ON knowledge_links(layer);`);
903
- params.db.prepare(
904
- `INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)`
905
- ).run(String(SCHEMA_VERSION));
906
- return { ftsAvailable, ...ftsError ? { ftsError } : {}, ...migrated ? { migrated } : {} };
907
- }
908
- function migrateIfNeeded(db, ftsTable) {
909
- let storedVersion = 0;
910
- try {
911
- const row = db.prepare(
912
- `SELECT value FROM meta WHERE key = 'schema_version'`
913
- ).get();
914
- if (row) {
915
- storedVersion = parseInt(row.value, 10) || 0;
916
- }
917
- } catch {
918
- storedVersion = 0;
919
- }
920
- if (storedVersion >= SCHEMA_VERSION) return false;
921
- if (storedVersion > 0 && storedVersion < SCHEMA_VERSION) {
922
- db.exec(`DROP TABLE IF EXISTS files`);
923
- db.exec(`DROP TABLE IF EXISTS chunks`);
924
- db.exec(`DROP TABLE IF EXISTS knowledge_links`);
925
- db.exec(`DROP TABLE IF EXISTS ${ftsTable}`);
926
- try {
927
- db.exec(`DROP TABLE IF EXISTS chunks_vec`);
928
- } catch {
929
- }
930
- }
931
- return storedVersion > 0;
932
- }
933
- function ensureColumn(db, table, column, definition) {
934
- const rows = db.prepare(`PRAGMA table_info(${table})`).all();
935
- if (rows.some((row) => row.name === column)) return;
936
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
937
- }
938
-
939
- // src/session.ts
940
- import * as os3 from "os";
941
- function parseFrontmatter(content) {
942
- const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
943
- const match = content.match(frontmatterRegex);
944
- if (!match) {
945
- return { frontmatter: void 0, body: content };
946
- }
947
- const yamlContent = match[1];
948
- const body = content.slice(match[0].length);
949
- try {
950
- const frontmatter = parseSimpleYaml(yamlContent);
951
- return { frontmatter, body };
952
- } catch {
953
- return { frontmatter: void 0, body: content };
954
- }
955
- }
956
- function parseSimpleYaml(yaml) {
957
- const lines = yaml.split("\n");
958
- return parseYamlBlock(lines, 0, 0, lines.length).value;
959
- }
960
- function parseYamlBlock(lines, indent, startIdx, endIdx) {
961
- const result = {};
962
- let i = startIdx;
963
- while (i < endIdx) {
964
- const line = lines[i];
965
- if (!line || !line.trim()) {
966
- i++;
967
- continue;
968
- }
969
- const lineIndent = getIndent(line);
970
- if (lineIndent < indent) break;
971
- if (lineIndent > indent) {
972
- i++;
973
- continue;
974
- }
975
- const keyMatch = line.match(/^(\s*)([\w-]+):\s*(.*)?$/);
976
- if (!keyMatch) {
977
- i++;
978
- continue;
979
- }
980
- const [, , key, rawValue] = keyMatch;
981
- const value = rawValue?.trim() ?? "";
982
- if (value === "" || value === void 0) {
983
- const nextNonEmpty = findNextNonEmptyLine(lines, i + 1, endIdx);
984
- if (nextNonEmpty < endIdx) {
985
- const nextLine = lines[nextNonEmpty];
986
- const nextIndent = getIndent(nextLine);
987
- if (nextIndent > indent) {
988
- if (nextLine.trimStart().startsWith("- ")) {
989
- const listResult = parseYamlList(lines, nextIndent, i + 1, endIdx);
990
- result[key] = listResult.value;
991
- i = listResult.nextIdx;
992
- } else {
993
- const blockResult = parseYamlBlock(lines, nextIndent, i + 1, endIdx);
994
- result[key] = blockResult.value;
995
- i = blockResult.nextIdx;
996
- }
997
- continue;
998
- }
999
- }
1000
- result[key] = null;
1001
- i++;
1002
- } else {
1003
- result[key] = parseYamlValue(value);
1004
- i++;
1005
- }
1006
- }
1007
- return { value: result, nextIdx: i };
1008
- }
1009
- function parseYamlList(lines, indent, startIdx, endIdx) {
1010
- const result = [];
1011
- let i = startIdx;
1012
- while (i < endIdx) {
1013
- const line = lines[i];
1014
- if (!line || !line.trim()) {
1015
- i++;
1016
- continue;
1017
- }
1018
- const lineIndent = getIndent(line);
1019
- if (lineIndent < indent) break;
1020
- if (lineIndent > indent) {
1021
- i++;
1022
- continue;
1023
- }
1024
- const trimmed = line.trimStart();
1025
- if (!trimmed.startsWith("- ")) break;
1026
- const itemContent = trimmed.slice(2).trim();
1027
- if (itemContent === "" || itemContent === void 0) {
1028
- const nextNonEmpty = findNextNonEmptyLine(lines, i + 1, endIdx);
1029
- if (nextNonEmpty < endIdx) {
1030
- const nextIndent = getIndent(lines[nextNonEmpty]);
1031
- if (nextIndent > indent) {
1032
- const blockResult = parseYamlBlock(lines, nextIndent, i + 1, endIdx);
1033
- result.push(blockResult.value);
1034
- i = blockResult.nextIdx;
1035
- continue;
1036
- }
1037
- }
1038
- result.push(null);
1039
- i++;
1040
- } else {
1041
- const kvMatch = itemContent.match(/^([\w-]+):\s*(.*)$/);
1042
- if (kvMatch) {
1043
- const obj = {};
1044
- const [, firstKey, firstVal] = kvMatch;
1045
- obj[firstKey] = parseYamlValue(firstVal?.trim() ?? "");
1046
- const itemKeyIndent = indent + 2;
1047
- let j = i + 1;
1048
- while (j < endIdx) {
1049
- const nextLine = lines[j];
1050
- if (!nextLine || !nextLine.trim()) {
1051
- j++;
1052
- continue;
1053
- }
1054
- const nextLineIndent = getIndent(nextLine);
1055
- if (nextLineIndent < itemKeyIndent) break;
1056
- if (nextLineIndent === itemKeyIndent) {
1057
- const nextKv = nextLine.match(/^\s*([\w-]+):\s*(.*)$/);
1058
- if (nextKv) {
1059
- const [, nk, nv] = nextKv;
1060
- obj[nk] = parseYamlValue(nv?.trim() ?? "");
1061
- j++;
1062
- continue;
1063
- }
1064
- }
1065
- break;
1066
- }
1067
- result.push(obj);
1068
- i = j;
1069
- } else {
1070
- result.push(parseYamlValue(itemContent));
1071
- i++;
1072
- }
1073
- }
1074
- }
1075
- return { value: result, nextIdx: i };
1076
- }
1077
- function getIndent(line) {
1078
- const match = line.match(/^(\s*)/);
1079
- return match ? match[1].length : 0;
1080
- }
1081
- function findNextNonEmptyLine(lines, from, end) {
1082
- for (let i = from; i < end; i++) {
1083
- if (lines[i]?.trim()) return i;
1084
- }
1085
- return end;
1086
- }
1087
- function parseYamlValue(value) {
1088
- if (value === "") return null;
1089
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1090
- return value.slice(1, -1);
1091
- }
1092
- if (value === "null" || value === "~") return null;
1093
- if (value === "true") return true;
1094
- if (value === "false") return false;
1095
- const num = Number(value);
1096
- if (!isNaN(num) && value !== "") return num;
1097
- if (value.startsWith("[") && value.endsWith("]")) {
1098
- const inner = value.slice(1, -1);
1099
- if (inner.trim() === "") return [];
1100
- return inner.split(",").map((s) => parseYamlValue(s.trim()));
1101
- }
1102
- return value;
1103
- }
1104
- function serializeFrontmatter(frontmatter) {
1105
- const lines = ["---"];
1106
- if (frontmatter.id) {
1107
- lines.push(`id: ${frontmatter.id}`);
1108
- }
1109
- if (frontmatter.type) {
1110
- lines.push(`type: ${frontmatter.type}`);
1111
- }
1112
- if (frontmatter.session) {
1113
- lines.push("session:");
1114
- const session = frontmatter.session;
1115
- if (session.id) lines.push(` id: ${session.id}`);
1116
- if (session.source) lines.push(` source: ${session.source}`);
1117
- if (session.project) lines.push(` project: ${formatPath2(session.project)}`);
1118
- if (session.transcript) lines.push(` transcript: ${formatPath2(session.transcript)}`);
1119
- }
1120
- if (frontmatter.created) {
1121
- lines.push(`created: ${frontmatter.created}`);
1122
- }
1123
- if (frontmatter.updated) {
1124
- lines.push(`updated: ${frontmatter.updated}`);
1125
- }
1126
- if (frontmatter.tags && frontmatter.tags.length > 0) {
1127
- lines.push(`tags: [${frontmatter.tags.join(", ")}]`);
1128
- }
1129
- if (frontmatter.domain && frontmatter.domain.length > 0) {
1130
- lines.push(`domain: [${frontmatter.domain.join(", ")}]`);
1131
- }
1132
- if (frontmatter.entities && frontmatter.entities.length > 0) {
1133
- lines.push(`entities: [${frontmatter.entities.join(", ")}]`);
1134
- }
1135
- if (frontmatter.confidence !== void 0) {
1136
- lines.push(`confidence: ${frontmatter.confidence}`);
1137
- }
1138
- if (frontmatter.source) {
1139
- lines.push("source:");
1140
- if (frontmatter.source.origin) lines.push(` origin: ${frontmatter.source.origin}`);
1141
- if (frontmatter.source.trajectories && frontmatter.source.trajectories.length > 0) {
1142
- lines.push(` trajectories: [${frontmatter.source.trajectories.join(", ")}]`);
1143
- }
1144
- if (frontmatter.source.agentId) lines.push(` agentId: ${frontmatter.source.agentId}`);
1145
- }
1146
- if (frontmatter.links && frontmatter.links.length > 0) {
1147
- lines.push("links:");
1148
- for (const link of frontmatter.links) {
1149
- lines.push(` - target: ${link.target}`);
1150
- lines.push(` relation: ${link.relation}`);
1151
- if (link.layer) lines.push(` layer: ${link.layer}`);
1152
- }
1153
- }
1154
- if (frontmatter.supersedes !== void 0) {
1155
- lines.push(`supersedes: ${frontmatter.supersedes === null ? "~" : frontmatter.supersedes}`);
1156
- }
1157
- lines.push("---");
1158
- return lines.join("\n") + "\n";
1159
- }
1160
- function addFrontmatter(content, frontmatter) {
1161
- const { frontmatter: existing, body } = parseFrontmatter(content);
1162
- const merged = {
1163
- ...existing,
1164
- ...frontmatter,
1165
- session: {
1166
- ...existing?.session,
1167
- ...frontmatter.session
1168
- }
1169
- };
1170
- if (!merged.created) {
1171
- merged.created = (/* @__PURE__ */ new Date()).toISOString();
1172
- }
1173
- merged.updated = (/* @__PURE__ */ new Date()).toISOString();
1174
- return serializeFrontmatter(merged) + body;
1175
- }
1176
- function formatPath2(filePath) {
1177
- const home = os3.homedir();
1178
- if (filePath.startsWith(home)) {
1179
- return "~" + filePath.slice(home.length);
1180
- }
1181
- return filePath;
1182
- }
1183
-
1184
- // src/search/graph.ts
1185
- function getLinksFrom(db, fromId, opts) {
1186
- let sql = `SELECT from_id, to_id, relation, layer, weight, source_path FROM knowledge_links WHERE from_id = ?`;
1187
- const params = [fromId];
1188
- if (opts?.relation) {
1189
- sql += ` AND relation = ?`;
1190
- params.push(opts.relation);
1191
- }
1192
- if (opts?.layer) {
1193
- sql += ` AND layer = ?`;
1194
- params.push(opts.layer);
1195
- }
1196
- const rows = db.prepare(sql).all(...params);
1197
- return rows.map(toGraphLink);
1198
- }
1199
- function getLinksTo(db, toId, opts) {
1200
- let sql = `SELECT from_id, to_id, relation, layer, weight, source_path FROM knowledge_links WHERE to_id = ?`;
1201
- const params = [toId];
1202
- if (opts?.relation) {
1203
- sql += ` AND relation = ?`;
1204
- params.push(opts.relation);
1205
- }
1206
- if (opts?.layer) {
1207
- sql += ` AND layer = ?`;
1208
- params.push(opts.layer);
1209
- }
1210
- const rows = db.prepare(sql).all(...params);
1211
- return rows.map(toGraphLink);
1212
- }
1213
- function getNeighbors(db, startId, depth = 1, opts) {
1214
- const visited = /* @__PURE__ */ new Set([startId]);
1215
- const result = [];
1216
- let frontier = [startId];
1217
- for (let d = 1; d <= depth; d++) {
1218
- const nextFrontier = [];
1219
- for (const nodeId of frontier) {
1220
- const outgoing = getLinksFrom(db, nodeId, opts);
1221
- for (const link of outgoing) {
1222
- if (!visited.has(link.toId)) {
1223
- visited.add(link.toId);
1224
- nextFrontier.push(link.toId);
1225
- result.push({ id: link.toId, depth: d, link });
1226
- }
1227
- }
1228
- const incoming = getLinksTo(db, nodeId, opts);
1229
- for (const link of incoming) {
1230
- if (!visited.has(link.fromId)) {
1231
- visited.add(link.fromId);
1232
- nextFrontier.push(link.fromId);
1233
- result.push({ id: link.fromId, depth: d, link });
1234
- }
1235
- }
1236
- }
1237
- frontier = nextFrontier;
1238
- if (frontier.length === 0) break;
1239
- }
1240
- return result;
1241
- }
1242
- function getPathBetween(db, fromId, toId, maxDepth = 3) {
1243
- if (fromId === toId) return [];
1244
- const visited = /* @__PURE__ */ new Set([fromId]);
1245
- const parentLink = /* @__PURE__ */ new Map();
1246
- let frontier = [fromId];
1247
- for (let d = 0; d < maxDepth; d++) {
1248
- const nextFrontier = [];
1249
- for (const nodeId of frontier) {
1250
- const outgoing = getLinksFrom(db, nodeId);
1251
- for (const link of outgoing) {
1252
- if (!visited.has(link.toId)) {
1253
- visited.add(link.toId);
1254
- parentLink.set(link.toId, link);
1255
- if (link.toId === toId) {
1256
- return reconstructPath(parentLink, fromId, toId);
1257
- }
1258
- nextFrontier.push(link.toId);
1259
- }
1260
- }
1261
- const incoming = getLinksTo(db, nodeId);
1262
- for (const link of incoming) {
1263
- if (!visited.has(link.fromId)) {
1264
- visited.add(link.fromId);
1265
- parentLink.set(link.fromId, link);
1266
- if (link.fromId === toId) {
1267
- return reconstructPath(parentLink, fromId, toId);
1268
- }
1269
- nextFrontier.push(link.fromId);
1270
- }
1271
- }
1272
- }
1273
- frontier = nextFrontier;
1274
- if (frontier.length === 0) break;
1275
- }
1276
- return [];
1277
- }
1278
- function reconstructPath(parentLink, fromId, toId) {
1279
- const path20 = [];
1280
- let current = toId;
1281
- while (current !== fromId) {
1282
- const link = parentLink.get(current);
1283
- if (!link) break;
1284
- path20.unshift(link);
1285
- current = link.toId === current ? link.fromId : link.toId;
1286
- }
1287
- return path20;
1288
- }
1289
- function toGraphLink(row) {
1290
- return {
1291
- fromId: row.from_id,
1292
- toId: row.to_id,
1293
- relation: row.relation,
1294
- layer: row.layer,
1295
- weight: row.weight,
1296
- sourcePath: row.source_path
1297
- };
1298
- }
1299
-
1300
- // src/db/sqlite-vec.ts
1301
- async function loadSqliteVecExtension(params) {
1302
- try {
1303
- const sqliteVec = await import("sqlite-vec");
1304
- const resolvedPath = params.extensionPath?.trim() ? params.extensionPath.trim() : void 0;
1305
- const extensionPath = resolvedPath ?? sqliteVec.getLoadablePath();
1306
- params.db.enableLoadExtension(true);
1307
- if (resolvedPath) {
1308
- params.db.loadExtension(extensionPath);
1309
- } else {
1310
- sqliteVec.load(params.db);
1311
- }
1312
- return { ok: true, extensionPath };
1313
- } catch (err) {
1314
- const message = err instanceof Error ? err.message : String(err);
1315
- return { ok: false, error: message };
1316
- }
1317
- }
1318
-
1319
- // src/embeddings/embeddings.ts
1320
- import fsSync3 from "fs";
1321
- import path5 from "path";
1322
- import os4 from "os";
1323
- var DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
1324
- var DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-small";
1325
- var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
1326
- var DEFAULT_GEMINI_EMBEDDING_MODEL = "gemini-embedding-001";
1327
- var DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
1328
- function createNoOpEmbeddingProvider() {
1329
- return {
1330
- id: "none",
1331
- model: "bm25-only",
1332
- embedQuery: async () => [],
1333
- embedBatch: async (texts) => texts.map(() => [])
1334
- };
1335
- }
1336
- function resolveUserPath(filePath) {
1337
- if (filePath.startsWith("~/")) {
1338
- return path5.join(os4.homedir(), filePath.slice(2));
1339
- }
1340
- return filePath;
1341
- }
1342
- function canAutoSelectLocal(options) {
1343
- const modelPath = options.local?.modelPath?.trim();
1344
- if (!modelPath) return false;
1345
- if (/^(hf:|https?:)/i.test(modelPath)) return false;
1346
- const resolved = resolveUserPath(modelPath);
1347
- try {
1348
- return fsSync3.statSync(resolved).isFile();
1349
- } catch {
1350
- return false;
1351
- }
1352
- }
1353
- function isMissingApiKeyError(err) {
1354
- const message = formatError(err);
1355
- return message.includes("API key") || message.includes("apiKey");
1356
- }
1357
- async function importNodeLlamaCpp() {
1358
- const llama = await import("node-llama-cpp");
1359
- return llama;
1360
- }
1361
- async function createLocalEmbeddingProvider(options) {
1362
- const modelPath = options.local?.modelPath?.trim() || DEFAULT_LOCAL_MODEL;
1363
- const modelCacheDir = options.local?.modelCacheDir?.trim();
1364
- const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp();
1365
- let llama = null;
1366
- let embeddingModel = null;
1367
- let embeddingContext = null;
1368
- const ensureContext = async () => {
1369
- if (!llama) {
1370
- llama = await getLlama({ logLevel: LlamaLogLevel.error });
1371
- }
1372
- if (!embeddingModel) {
1373
- const resolved = await resolveModelFile(modelPath, modelCacheDir || void 0);
1374
- embeddingModel = await llama.loadModel({ modelPath: resolved });
1375
- }
1376
- if (!embeddingContext) {
1377
- embeddingContext = await embeddingModel.createEmbeddingContext();
1378
- }
1379
- return embeddingContext;
1380
- };
1381
- return {
1382
- id: "local",
1383
- model: modelPath,
1384
- embedQuery: async (text) => {
1385
- const ctx = await ensureContext();
1386
- const embedding = await ctx.getEmbeddingFor(text);
1387
- return Array.from(embedding.vector);
1388
- },
1389
- embedBatch: async (texts) => {
1390
- const ctx = await ensureContext();
1391
- const embeddings = await Promise.all(
1392
- texts.map(async (text) => {
1393
- const embedding = await ctx.getEmbeddingFor(text);
1394
- return Array.from(embedding.vector);
1395
- })
1396
- );
1397
- return embeddings;
1398
- }
1399
- };
1400
- }
1401
- function normalizeOpenAiModel(model) {
1402
- const trimmed = model.trim();
1403
- if (!trimmed) return DEFAULT_OPENAI_EMBEDDING_MODEL;
1404
- if (trimmed.startsWith("openai/")) return trimmed.slice("openai/".length);
1405
- return trimmed;
1406
- }
1407
- function resolveOpenAiApiKey(options) {
1408
- const apiKey = options.openai?.apiKey?.trim();
1409
- if (apiKey) return apiKey;
1410
- const envKey = process.env.OPENAI_API_KEY?.trim();
1411
- if (envKey) return envKey;
1412
- throw new Error("OpenAI API key not found. Set OPENAI_API_KEY env var or pass openai.apiKey option.");
1413
- }
1414
- async function createOpenAiEmbeddingProvider(options) {
1415
- const apiKey = resolveOpenAiApiKey(options);
1416
- const baseUrl = options.openai?.baseUrl?.trim() || DEFAULT_OPENAI_BASE_URL;
1417
- const headerOverrides = options.openai?.headers ?? {};
1418
- const headers = {
1419
- "Content-Type": "application/json",
1420
- Authorization: `Bearer ${apiKey}`,
1421
- ...headerOverrides
1422
- };
1423
- const model = normalizeOpenAiModel(options.model || "");
1424
- const client = { baseUrl, headers, model };
1425
- const url = `${baseUrl.replace(/\/$/, "")}/embeddings`;
1426
- const embed = async (input) => {
1427
- if (input.length === 0) return [];
1428
- const res = await fetch(url, {
1429
- method: "POST",
1430
- headers: client.headers,
1431
- body: JSON.stringify({ model: client.model, input })
1432
- });
1433
- if (!res.ok) {
1434
- const text = await res.text();
1435
- throw new Error(`openai embeddings failed: ${res.status} ${text}`);
1436
- }
1437
- const payload = await res.json();
1438
- const data = payload.data ?? [];
1439
- return data.map((entry) => entry.embedding ?? []);
1440
- };
1441
- return {
1442
- provider: {
1443
- id: "openai",
1444
- model: client.model,
1445
- embedQuery: async (text) => {
1446
- const [vec] = await embed([text]);
1447
- return vec ?? [];
1448
- },
1449
- embedBatch: embed
1450
- },
1451
- client
1452
- };
1453
- }
1454
- function normalizeGeminiModel(model) {
1455
- const trimmed = model.trim();
1456
- if (!trimmed) return DEFAULT_GEMINI_EMBEDDING_MODEL;
1457
- const withoutPrefix = trimmed.replace(/^models\//, "");
1458
- if (withoutPrefix.startsWith("gemini/")) return withoutPrefix.slice("gemini/".length);
1459
- if (withoutPrefix.startsWith("google/")) return withoutPrefix.slice("google/".length);
1460
- return withoutPrefix;
1461
- }
1462
- function normalizeGeminiBaseUrl(raw) {
1463
- const trimmed = raw.replace(/\/+$/, "");
1464
- const openAiIndex = trimmed.indexOf("/openai");
1465
- if (openAiIndex > -1) return trimmed.slice(0, openAiIndex);
1466
- return trimmed;
1467
- }
1468
- function buildGeminiModelPath(model) {
1469
- return model.startsWith("models/") ? model : `models/${model}`;
1470
- }
1471
- function resolveGeminiApiKey(options) {
1472
- const apiKey = options.gemini?.apiKey?.trim();
1473
- if (apiKey) return apiKey;
1474
- const googleKey = process.env.GOOGLE_API_KEY?.trim();
1475
- if (googleKey) return googleKey;
1476
- const geminiKey = process.env.GEMINI_API_KEY?.trim();
1477
- if (geminiKey) return geminiKey;
1478
- throw new Error("Gemini API key not found. Set GOOGLE_API_KEY or GEMINI_API_KEY env var or pass gemini.apiKey option.");
1479
- }
1480
- async function createGeminiEmbeddingProvider(options) {
1481
- const apiKey = resolveGeminiApiKey(options);
1482
- const rawBaseUrl = options.gemini?.baseUrl?.trim() || DEFAULT_GEMINI_BASE_URL;
1483
- const baseUrl = normalizeGeminiBaseUrl(rawBaseUrl);
1484
- const headerOverrides = options.gemini?.headers ?? {};
1485
- const headers = {
1486
- "Content-Type": "application/json",
1487
- "x-goog-api-key": apiKey,
1488
- ...headerOverrides
1489
- };
1490
- const model = normalizeGeminiModel(options.model || "");
1491
- const modelPath = buildGeminiModelPath(model);
1492
- const client = { baseUrl, headers, model, modelPath };
1493
- const embedUrl = `${baseUrl}/${modelPath}:embedContent`;
1494
- const batchUrl = `${baseUrl}/${modelPath}:batchEmbedContents`;
1495
- const embedQuery = async (text) => {
1496
- if (!text.trim()) return [];
1497
- const res = await fetch(embedUrl, {
1498
- method: "POST",
1499
- headers: client.headers,
1500
- body: JSON.stringify({
1501
- content: { parts: [{ text }] },
1502
- taskType: "RETRIEVAL_QUERY"
1503
- })
1504
- });
1505
- if (!res.ok) {
1506
- const payload2 = await res.text();
1507
- throw new Error(`gemini embeddings failed: ${res.status} ${payload2}`);
1508
- }
1509
- const payload = await res.json();
1510
- return payload.embedding?.values ?? [];
1511
- };
1512
- const embedBatch = async (texts) => {
1513
- if (texts.length === 0) return [];
1514
- const requests = texts.map((text) => ({
1515
- model: modelPath,
1516
- content: { parts: [{ text }] },
1517
- taskType: "RETRIEVAL_DOCUMENT"
1518
- }));
1519
- const res = await fetch(batchUrl, {
1520
- method: "POST",
1521
- headers: client.headers,
1522
- body: JSON.stringify({ requests })
1523
- });
1524
- if (!res.ok) {
1525
- const payload2 = await res.text();
1526
- throw new Error(`gemini embeddings failed: ${res.status} ${payload2}`);
1527
- }
1528
- const payload = await res.json();
1529
- const embeddings = Array.isArray(payload.embeddings) ? payload.embeddings : [];
1530
- return texts.map((_, index) => embeddings[index]?.values ?? []);
1531
- };
1532
- return {
1533
- provider: {
1534
- id: "gemini",
1535
- model: client.model,
1536
- embedQuery,
1537
- embedBatch
1538
- },
1539
- client
1540
- };
1541
- }
1542
- async function createEmbeddingProvider(options) {
1543
- const requestedProvider = options.provider;
1544
- const fallback = options.fallback ?? "none";
1545
- if (requestedProvider === "none") {
1546
- return {
1547
- provider: createNoOpEmbeddingProvider(),
1548
- requestedProvider: "none"
1549
- };
1550
- }
1551
- const createProvider = async (id) => {
1552
- if (id === "local") {
1553
- const provider2 = await createLocalEmbeddingProvider(options);
1554
- return { provider: provider2 };
1555
- }
1556
- if (id === "gemini") {
1557
- const { provider: provider2, client: client2 } = await createGeminiEmbeddingProvider(options);
1558
- return { provider: provider2, gemini: client2 };
1559
- }
1560
- const { provider, client } = await createOpenAiEmbeddingProvider(options);
1561
- return { provider, openAi: client };
1562
- };
1563
- const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatError(err);
1564
- if (requestedProvider === "auto") {
1565
- const missingKeyErrors = [];
1566
- let localError = null;
1567
- if (canAutoSelectLocal(options)) {
1568
- try {
1569
- const local = await createProvider("local");
1570
- return { ...local, requestedProvider };
1571
- } catch (err) {
1572
- localError = formatLocalSetupError(err);
1573
- }
1574
- }
1575
- for (const provider of ["openai", "gemini"]) {
1576
- try {
1577
- const result = await createProvider(provider);
1578
- return { ...result, requestedProvider };
1579
- } catch (err) {
1580
- const message = formatPrimaryError(err, provider);
1581
- if (isMissingApiKeyError(err)) {
1582
- missingKeyErrors.push(message);
1583
- continue;
1584
- }
1585
- throw new Error(message);
1586
- }
1587
- }
1588
- return {
1589
- provider: createNoOpEmbeddingProvider(),
1590
- requestedProvider,
1591
- fallbackFrom: "auto",
1592
- fallbackReason: "No embedding API available. Using BM25 full-text search only."
1593
- };
1594
- }
1595
- try {
1596
- const primary = await createProvider(requestedProvider);
1597
- return { ...primary, requestedProvider };
1598
- } catch (primaryErr) {
1599
- const reason = formatPrimaryError(primaryErr, requestedProvider);
1600
- if (fallback && fallback !== "none" && fallback !== requestedProvider) {
1601
- try {
1602
- const fallbackResult = await createProvider(fallback);
1603
- return {
1604
- ...fallbackResult,
1605
- requestedProvider,
1606
- fallbackFrom: requestedProvider,
1607
- fallbackReason: reason
1608
- };
1609
- } catch (fallbackErr) {
1610
- throw new Error(`${reason}
1611
-
1612
- Fallback to ${fallback} failed: ${formatError(fallbackErr)}`);
1613
- }
1614
- }
1615
- throw new Error(reason);
1616
- }
1617
- }
1618
- function formatError(err) {
1619
- if (err instanceof Error) return err.message;
1620
- return String(err);
1621
- }
1622
- function isNodeLlamaCppMissing(err) {
1623
- if (!(err instanceof Error)) return false;
1624
- const code = err.code;
1625
- if (code === "ERR_MODULE_NOT_FOUND") {
1626
- return err.message.includes("node-llama-cpp");
1627
- }
1628
- return false;
1629
- }
1630
- function formatLocalSetupError(err) {
1631
- const detail = formatError(err);
1632
- const missing = isNodeLlamaCppMissing(err);
1633
- return [
1634
- "Local embeddings unavailable.",
1635
- missing ? "Reason: optional dependency node-llama-cpp is missing (or failed to install)." : detail ? `Reason: ${detail}` : void 0,
1636
- missing && detail ? `Detail: ${detail}` : null,
1637
- "To enable local embeddings:",
1638
- "1) Use Node 22 LTS (recommended for installs/updates)",
1639
- missing ? "2) Install node-llama-cpp: npm install node-llama-cpp" : null,
1640
- "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
1641
- 'Or set provider = "openai" or "gemini" (remote).'
1642
- ].filter(Boolean).join("\n");
1643
- }
1644
-
1645
- // src/embeddings/batch-openai.ts
1646
- var OPENAI_BATCH_ENDPOINT = "/v1/embeddings";
1647
- var OPENAI_BATCH_COMPLETION_WINDOW = "24h";
1648
- var OPENAI_BATCH_MAX_REQUESTS = 5e4;
1649
- function getOpenAiBaseUrl(openAi) {
1650
- return openAi.baseUrl?.replace(/\/$/, "") ?? "";
1651
- }
1652
- function getOpenAiHeaders(openAi, params) {
1653
- const headers = openAi.headers ? { ...openAi.headers } : {};
1654
- if (params.json) {
1655
- if (!headers["Content-Type"] && !headers["content-type"]) {
1656
- headers["Content-Type"] = "application/json";
1657
- }
1658
- } else {
1659
- delete headers["Content-Type"];
1660
- delete headers["content-type"];
1661
- }
1662
- return headers;
1663
- }
1664
- function splitOpenAiBatchRequests(requests) {
1665
- if (requests.length <= OPENAI_BATCH_MAX_REQUESTS) return [requests];
1666
- const groups = [];
1667
- for (let i = 0; i < requests.length; i += OPENAI_BATCH_MAX_REQUESTS) {
1668
- groups.push(requests.slice(i, i + OPENAI_BATCH_MAX_REQUESTS));
1669
- }
1670
- return groups;
1671
- }
1672
- async function retryAsync(fn, opts) {
1673
- let lastError;
1674
- for (let attempt = 0; attempt < opts.attempts; attempt++) {
1675
- try {
1676
- return await fn();
1677
- } catch (err) {
1678
- lastError = err;
1679
- if (!opts.shouldRetry(err) || attempt === opts.attempts - 1) {
1680
- throw err;
1681
- }
1682
- const delay = Math.min(
1683
- opts.maxDelayMs,
1684
- opts.minDelayMs * Math.pow(2, attempt) * (1 + Math.random() * opts.jitter)
1685
- );
1686
- await new Promise((resolve3) => setTimeout(resolve3, delay));
1687
- }
1688
- }
1689
- throw lastError;
1690
- }
1691
- async function submitOpenAiBatch(params) {
1692
- const baseUrl = getOpenAiBaseUrl(params.openAi);
1693
- const jsonl = params.requests.map((request) => JSON.stringify(request)).join("\n");
1694
- const form = new FormData();
1695
- form.append("purpose", "batch");
1696
- form.append(
1697
- "file",
1698
- new Blob([jsonl], { type: "application/jsonl" }),
1699
- `memory-embeddings.${hashText(String(Date.now()))}.jsonl`
1700
- );
1701
- const fileRes = await fetch(`${baseUrl}/files`, {
1702
- method: "POST",
1703
- headers: getOpenAiHeaders(params.openAi, { json: false }),
1704
- body: form
1705
- });
1706
- if (!fileRes.ok) {
1707
- const text = await fileRes.text();
1708
- throw new Error(`openai batch file upload failed: ${fileRes.status} ${text}`);
1709
- }
1710
- const filePayload = await fileRes.json();
1711
- if (!filePayload.id) {
1712
- throw new Error("openai batch file upload failed: missing file id");
1713
- }
1714
- const batchRes = await retryAsync(
1715
- async () => {
1716
- const res = await fetch(`${baseUrl}/batches`, {
1717
- method: "POST",
1718
- headers: getOpenAiHeaders(params.openAi, { json: true }),
1719
- body: JSON.stringify({
1720
- input_file_id: filePayload.id,
1721
- endpoint: OPENAI_BATCH_ENDPOINT,
1722
- completion_window: OPENAI_BATCH_COMPLETION_WINDOW,
1723
- metadata: {
1724
- source: params.source
1725
- }
1726
- })
1727
- });
1728
- if (!res.ok) {
1729
- const text = await res.text();
1730
- const err = new Error(`openai batch create failed: ${res.status} ${text}`);
1731
- err.status = res.status;
1732
- throw err;
1733
- }
1734
- return res;
1735
- },
1736
- {
1737
- attempts: 3,
1738
- minDelayMs: 300,
1739
- maxDelayMs: 2e3,
1740
- jitter: 0.2,
1741
- shouldRetry: (err) => {
1742
- const status2 = err.status;
1743
- return status2 === 429 || typeof status2 === "number" && status2 >= 500;
1744
- }
1745
- }
1746
- );
1747
- return await batchRes.json();
1748
- }
1749
- async function fetchOpenAiBatchStatus(params) {
1750
- const baseUrl = getOpenAiBaseUrl(params.openAi);
1751
- const res = await fetch(`${baseUrl}/batches/${params.batchId}`, {
1752
- headers: getOpenAiHeaders(params.openAi, { json: true })
1753
- });
1754
- if (!res.ok) {
1755
- const text = await res.text();
1756
- throw new Error(`openai batch status failed: ${res.status} ${text}`);
1757
- }
1758
- return await res.json();
1759
- }
1760
- async function fetchOpenAiFileContent(params) {
1761
- const baseUrl = getOpenAiBaseUrl(params.openAi);
1762
- const res = await fetch(`${baseUrl}/files/${params.fileId}/content`, {
1763
- headers: getOpenAiHeaders(params.openAi, { json: true })
1764
- });
1765
- if (!res.ok) {
1766
- const text = await res.text();
1767
- throw new Error(`openai batch file content failed: ${res.status} ${text}`);
1768
- }
1769
- return await res.text();
1770
- }
1771
- function parseOpenAiBatchOutput(text) {
1772
- if (!text.trim()) return [];
1773
- return text.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
1774
- }
1775
- async function readOpenAiBatchError(params) {
1776
- try {
1777
- const content = await fetchOpenAiFileContent({
1778
- openAi: params.openAi,
1779
- fileId: params.errorFileId
1780
- });
1781
- const lines = parseOpenAiBatchOutput(content);
1782
- const first = lines.find((line) => line.error?.message || line.response?.body?.error);
1783
- const message = first?.error?.message ?? (typeof first?.response?.body?.error?.message === "string" ? first?.response?.body?.error?.message : void 0);
1784
- return message;
1785
- } catch (err) {
1786
- const message = err instanceof Error ? err.message : String(err);
1787
- return message ? `error file unavailable: ${message}` : void 0;
1788
- }
1789
- }
1790
- async function waitForOpenAiBatch(params) {
1791
- const start = Date.now();
1792
- let current = params.initial;
1793
- while (true) {
1794
- const status2 = current ?? await fetchOpenAiBatchStatus({
1795
- openAi: params.openAi,
1796
- batchId: params.batchId
1797
- });
1798
- const state = status2.status ?? "unknown";
1799
- if (state === "completed") {
1800
- if (!status2.output_file_id) {
1801
- throw new Error(`openai batch ${params.batchId} completed without output file`);
1802
- }
1803
- return {
1804
- outputFileId: status2.output_file_id,
1805
- errorFileId: status2.error_file_id ?? void 0
1806
- };
1807
- }
1808
- if (["failed", "expired", "cancelled", "canceled"].includes(state)) {
1809
- const detail = status2.error_file_id ? await readOpenAiBatchError({ openAi: params.openAi, errorFileId: status2.error_file_id }) : void 0;
1810
- const suffix = detail ? `: ${detail}` : "";
1811
- throw new Error(`openai batch ${params.batchId} ${state}${suffix}`);
1812
- }
1813
- if (!params.wait) {
1814
- throw new Error(`openai batch ${params.batchId} still ${state}; wait disabled`);
1815
- }
1816
- if (Date.now() - start > params.timeoutMs) {
1817
- throw new Error(`openai batch ${params.batchId} timed out after ${params.timeoutMs}ms`);
1818
- }
1819
- params.debug?.(`openai batch ${params.batchId} ${state}; waiting ${params.pollIntervalMs}ms`);
1820
- await new Promise((resolve3) => setTimeout(resolve3, params.pollIntervalMs));
1821
- current = void 0;
1822
- }
1823
- }
1824
- async function runWithConcurrency(tasks, limit) {
1825
- if (tasks.length === 0) return [];
1826
- const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
1827
- const results = Array.from({ length: tasks.length });
1828
- let next = 0;
1829
- let firstError = null;
1830
- const workers = Array.from({ length: resolvedLimit }, async () => {
1831
- while (true) {
1832
- if (firstError) return;
1833
- const index = next;
1834
- next += 1;
1835
- if (index >= tasks.length) return;
1836
- try {
1837
- results[index] = await tasks[index]();
1838
- } catch (err) {
1839
- firstError = err;
1840
- return;
1841
- }
1842
- }
1843
- });
1844
- await Promise.allSettled(workers);
1845
- if (firstError) throw firstError;
1846
- return results;
1847
- }
1848
- async function runOpenAiEmbeddingBatches(params) {
1849
- if (params.requests.length === 0) return /* @__PURE__ */ new Map();
1850
- const groups = splitOpenAiBatchRequests(params.requests);
1851
- const byCustomId = /* @__PURE__ */ new Map();
1852
- const tasks = groups.map((group, groupIndex) => async () => {
1853
- const batchInfo = await submitOpenAiBatch({
1854
- openAi: params.openAi,
1855
- requests: group,
1856
- source: params.source
1857
- });
1858
- if (!batchInfo.id) {
1859
- throw new Error("openai batch create failed: missing batch id");
1860
- }
1861
- params.debug?.("memory embeddings: openai batch created", {
1862
- batchId: batchInfo.id,
1863
- status: batchInfo.status,
1864
- group: groupIndex + 1,
1865
- groups: groups.length,
1866
- requests: group.length
1867
- });
1868
- if (!params.wait && batchInfo.status !== "completed") {
1869
- throw new Error(
1870
- `openai batch ${batchInfo.id} submitted; enable batch.wait to await completion`
1871
- );
1872
- }
1873
- const completed = batchInfo.status === "completed" ? {
1874
- outputFileId: batchInfo.output_file_id ?? "",
1875
- errorFileId: batchInfo.error_file_id ?? void 0
1876
- } : await waitForOpenAiBatch({
1877
- openAi: params.openAi,
1878
- batchId: batchInfo.id,
1879
- wait: params.wait,
1880
- pollIntervalMs: params.pollIntervalMs,
1881
- timeoutMs: params.timeoutMs,
1882
- debug: params.debug,
1883
- initial: batchInfo
1884
- });
1885
- if (!completed.outputFileId) {
1886
- throw new Error(`openai batch ${batchInfo.id} completed without output file`);
1887
- }
1888
- const content = await fetchOpenAiFileContent({
1889
- openAi: params.openAi,
1890
- fileId: completed.outputFileId
1891
- });
1892
- const outputLines = parseOpenAiBatchOutput(content);
1893
- const errors = [];
1894
- const remaining = new Set(group.map((request) => request.custom_id));
1895
- for (const line of outputLines) {
1896
- const customId = line.custom_id;
1897
- if (!customId) continue;
1898
- remaining.delete(customId);
1899
- if (line.error?.message) {
1900
- errors.push(`${customId}: ${line.error.message}`);
1901
- continue;
1902
- }
1903
- const response = line.response;
1904
- const statusCode = response?.status_code ?? 0;
1905
- if (statusCode >= 400) {
1906
- const message = response?.body?.error?.message ?? (typeof response?.body === "string" ? response.body : void 0) ?? "unknown error";
1907
- errors.push(`${customId}: ${message}`);
1908
- continue;
1909
- }
1910
- const data = response?.body?.data ?? [];
1911
- const embedding = data[0]?.embedding ?? [];
1912
- if (embedding.length === 0) {
1913
- errors.push(`${customId}: empty embedding`);
1914
- continue;
1915
- }
1916
- byCustomId.set(customId, embedding);
1917
- }
1918
- if (errors.length > 0) {
1919
- throw new Error(`openai batch ${batchInfo.id} failed: ${errors.join("; ")}`);
1920
- }
1921
- if (remaining.size > 0) {
1922
- throw new Error(`openai batch ${batchInfo.id} missing ${remaining.size} embedding responses`);
1923
- }
1924
- });
1925
- params.debug?.("memory embeddings: openai batch submit", {
1926
- requests: params.requests.length,
1927
- groups: groups.length,
1928
- wait: params.wait,
1929
- concurrency: params.concurrency,
1930
- pollIntervalMs: params.pollIntervalMs,
1931
- timeoutMs: params.timeoutMs
1932
- });
1933
- await runWithConcurrency(tasks, params.concurrency);
1934
- return byCustomId;
1935
- }
1936
-
1937
- // src/embeddings/batch-gemini.ts
1938
- var GEMINI_BATCH_MAX_REQUESTS = 5e4;
1939
- function getGeminiBaseUrl(gemini) {
1940
- return gemini.baseUrl?.replace(/\/$/, "") ?? "";
1941
- }
1942
- function getGeminiHeaders(gemini, params) {
1943
- const headers = gemini.headers ? { ...gemini.headers } : {};
1944
- if (params.json) {
1945
- if (!headers["Content-Type"] && !headers["content-type"]) {
1946
- headers["Content-Type"] = "application/json";
1947
- }
1948
- } else {
1949
- delete headers["Content-Type"];
1950
- delete headers["content-type"];
1951
- }
1952
- return headers;
1953
- }
1954
- function getGeminiUploadUrl(baseUrl) {
1955
- if (baseUrl.includes("/v1beta")) {
1956
- return baseUrl.replace(/\/v1beta\/?$/, "/upload/v1beta");
1957
- }
1958
- return `${baseUrl.replace(/\/$/, "")}/upload`;
1959
- }
1960
- function splitGeminiBatchRequests(requests) {
1961
- if (requests.length <= GEMINI_BATCH_MAX_REQUESTS) return [requests];
1962
- const groups = [];
1963
- for (let i = 0; i < requests.length; i += GEMINI_BATCH_MAX_REQUESTS) {
1964
- groups.push(requests.slice(i, i + GEMINI_BATCH_MAX_REQUESTS));
1965
- }
1966
- return groups;
1967
- }
1968
- function buildGeminiUploadBody(params) {
1969
- const boundary = `minimem-${hashText(params.displayName)}`;
1970
- const jsonPart = JSON.stringify({
1971
- file: {
1972
- displayName: params.displayName,
1973
- mimeType: "application/jsonl"
1974
- }
1975
- });
1976
- const delimiter = `--${boundary}\r
1977
- `;
1978
- const closeDelimiter = `--${boundary}--\r
1979
- `;
1980
- const parts = [
1981
- `${delimiter}Content-Type: application/json; charset=UTF-8\r
1982
- \r
1983
- ${jsonPart}\r
1984
- `,
1985
- `${delimiter}Content-Type: application/jsonl; charset=UTF-8\r
1986
- \r
1987
- ${params.jsonl}\r
1988
- `,
1989
- closeDelimiter
1990
- ];
1991
- const body = new Blob([parts.join("")], { type: "multipart/related" });
1992
- return {
1993
- body,
1994
- contentType: `multipart/related; boundary=${boundary}`
1995
- };
1996
- }
1997
- async function submitGeminiBatch(params) {
1998
- const baseUrl = getGeminiBaseUrl(params.gemini);
1999
- const jsonl = params.requests.map(
2000
- (request) => JSON.stringify({
2001
- key: request.custom_id,
2002
- request: {
2003
- content: request.content,
2004
- task_type: request.taskType
2005
- }
2006
- })
2007
- ).join("\n");
2008
- const displayName = `memory-embeddings-${hashText(String(Date.now()))}`;
2009
- const uploadPayload = buildGeminiUploadBody({ jsonl, displayName });
2010
- const uploadUrl = `${getGeminiUploadUrl(baseUrl)}/files?uploadType=multipart`;
2011
- const fileRes = await fetch(uploadUrl, {
2012
- method: "POST",
2013
- headers: {
2014
- ...getGeminiHeaders(params.gemini, { json: false }),
2015
- "Content-Type": uploadPayload.contentType
2016
- },
2017
- body: uploadPayload.body
2018
- });
2019
- if (!fileRes.ok) {
2020
- const text2 = await fileRes.text();
2021
- throw new Error(`gemini batch file upload failed: ${fileRes.status} ${text2}`);
2022
- }
2023
- const filePayload = await fileRes.json();
2024
- const fileId = filePayload.name ?? filePayload.file?.name;
2025
- if (!fileId) {
2026
- throw new Error("gemini batch file upload failed: missing file id");
2027
- }
2028
- const batchBody = {
2029
- batch: {
2030
- displayName: `memory-embeddings-${params.source}`,
2031
- inputConfig: {
2032
- file_name: fileId
2033
- }
2034
- }
2035
- };
2036
- const batchEndpoint = `${baseUrl}/${params.gemini.modelPath}:asyncBatchEmbedContent`;
2037
- const batchRes = await fetch(batchEndpoint, {
2038
- method: "POST",
2039
- headers: getGeminiHeaders(params.gemini, { json: true }),
2040
- body: JSON.stringify(batchBody)
2041
- });
2042
- if (batchRes.ok) {
2043
- return await batchRes.json();
2044
- }
2045
- const text = await batchRes.text();
2046
- if (batchRes.status === 404) {
2047
- throw new Error(
2048
- "gemini batch create failed: 404 (asyncBatchEmbedContent not available for this model/baseUrl). Disable batch.enabled or switch providers."
2049
- );
2050
- }
2051
- throw new Error(`gemini batch create failed: ${batchRes.status} ${text}`);
2052
- }
2053
- async function fetchGeminiBatchStatus(params) {
2054
- const baseUrl = getGeminiBaseUrl(params.gemini);
2055
- const name = params.batchName.startsWith("batches/") ? params.batchName : `batches/${params.batchName}`;
2056
- const statusUrl = `${baseUrl}/${name}`;
2057
- const res = await fetch(statusUrl, {
2058
- headers: getGeminiHeaders(params.gemini, { json: true })
2059
- });
2060
- if (!res.ok) {
2061
- const text = await res.text();
2062
- throw new Error(`gemini batch status failed: ${res.status} ${text}`);
2063
- }
2064
- return await res.json();
2065
- }
2066
- async function fetchGeminiFileContent(params) {
2067
- const baseUrl = getGeminiBaseUrl(params.gemini);
2068
- const file = params.fileId.startsWith("files/") ? params.fileId : `files/${params.fileId}`;
2069
- const downloadUrl = `${baseUrl}/${file}:download`;
2070
- const res = await fetch(downloadUrl, {
2071
- headers: getGeminiHeaders(params.gemini, { json: true })
2072
- });
2073
- if (!res.ok) {
2074
- const text = await res.text();
2075
- throw new Error(`gemini batch file content failed: ${res.status} ${text}`);
2076
- }
2077
- return await res.text();
2078
- }
2079
- function parseGeminiBatchOutput(text) {
2080
- if (!text.trim()) return [];
2081
- return text.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line));
2082
- }
2083
- async function waitForGeminiBatch(params) {
2084
- const start = Date.now();
2085
- let current = params.initial;
2086
- while (true) {
2087
- const status2 = current ?? await fetchGeminiBatchStatus({
2088
- gemini: params.gemini,
2089
- batchName: params.batchName
2090
- });
2091
- const state = status2.state ?? "UNKNOWN";
2092
- if (["SUCCEEDED", "COMPLETED", "DONE"].includes(state)) {
2093
- const outputFileId = status2.outputConfig?.file ?? status2.outputConfig?.fileId ?? status2.metadata?.output?.responsesFile;
2094
- if (!outputFileId) {
2095
- throw new Error(`gemini batch ${params.batchName} completed without output file`);
2096
- }
2097
- return { outputFileId };
2098
- }
2099
- if (["FAILED", "CANCELLED", "CANCELED", "EXPIRED"].includes(state)) {
2100
- const message = status2.error?.message ?? "unknown error";
2101
- throw new Error(`gemini batch ${params.batchName} ${state}: ${message}`);
2102
- }
2103
- if (!params.wait) {
2104
- throw new Error(`gemini batch ${params.batchName} still ${state}; wait disabled`);
2105
- }
2106
- if (Date.now() - start > params.timeoutMs) {
2107
- throw new Error(`gemini batch ${params.batchName} timed out after ${params.timeoutMs}ms`);
2108
- }
2109
- params.debug?.(`gemini batch ${params.batchName} ${state}; waiting ${params.pollIntervalMs}ms`);
2110
- await new Promise((resolve3) => setTimeout(resolve3, params.pollIntervalMs));
2111
- current = void 0;
2112
- }
2113
- }
2114
- async function runWithConcurrency2(tasks, limit) {
2115
- if (tasks.length === 0) return [];
2116
- const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
2117
- const results = Array.from({ length: tasks.length });
2118
- let next = 0;
2119
- let firstError = null;
2120
- const workers = Array.from({ length: resolvedLimit }, async () => {
2121
- while (true) {
2122
- if (firstError) return;
2123
- const index = next;
2124
- next += 1;
2125
- if (index >= tasks.length) return;
2126
- try {
2127
- results[index] = await tasks[index]();
2128
- } catch (err) {
2129
- firstError = err;
2130
- return;
2131
- }
2132
- }
2133
- });
2134
- await Promise.allSettled(workers);
2135
- if (firstError) throw firstError;
2136
- return results;
2137
- }
2138
- async function runGeminiEmbeddingBatches(params) {
2139
- if (params.requests.length === 0) return /* @__PURE__ */ new Map();
2140
- const groups = splitGeminiBatchRequests(params.requests);
2141
- const byCustomId = /* @__PURE__ */ new Map();
2142
- const tasks = groups.map((group, groupIndex) => async () => {
2143
- const batchInfo = await submitGeminiBatch({
2144
- gemini: params.gemini,
2145
- requests: group,
2146
- source: params.source
2147
- });
2148
- const batchName = batchInfo.name ?? "";
2149
- if (!batchName) {
2150
- throw new Error("gemini batch create failed: missing batch name");
2151
- }
2152
- params.debug?.("memory embeddings: gemini batch created", {
2153
- batchName,
2154
- state: batchInfo.state,
2155
- group: groupIndex + 1,
2156
- groups: groups.length,
2157
- requests: group.length
2158
- });
2159
- if (!params.wait && batchInfo.state && !["SUCCEEDED", "COMPLETED", "DONE"].includes(batchInfo.state)) {
2160
- throw new Error(
2161
- `gemini batch ${batchName} submitted; enable batch.wait to await completion`
2162
- );
2163
- }
2164
- const completed = batchInfo.state && ["SUCCEEDED", "COMPLETED", "DONE"].includes(batchInfo.state) ? {
2165
- outputFileId: batchInfo.outputConfig?.file ?? batchInfo.outputConfig?.fileId ?? batchInfo.metadata?.output?.responsesFile ?? ""
2166
- } : await waitForGeminiBatch({
2167
- gemini: params.gemini,
2168
- batchName,
2169
- wait: params.wait,
2170
- pollIntervalMs: params.pollIntervalMs,
2171
- timeoutMs: params.timeoutMs,
2172
- debug: params.debug,
2173
- initial: batchInfo
2174
- });
2175
- if (!completed.outputFileId) {
2176
- throw new Error(`gemini batch ${batchName} completed without output file`);
2177
- }
2178
- const content = await fetchGeminiFileContent({
2179
- gemini: params.gemini,
2180
- fileId: completed.outputFileId
2181
- });
2182
- const outputLines = parseGeminiBatchOutput(content);
2183
- const errors = [];
2184
- const remaining = new Set(group.map((request) => request.custom_id));
2185
- for (const line of outputLines) {
2186
- const customId = line.key ?? line.custom_id ?? line.request_id;
2187
- if (!customId) continue;
2188
- remaining.delete(customId);
2189
- if (line.error?.message) {
2190
- errors.push(`${customId}: ${line.error.message}`);
2191
- continue;
2192
- }
2193
- if (line.response?.error?.message) {
2194
- errors.push(`${customId}: ${line.response.error.message}`);
2195
- continue;
2196
- }
2197
- const embedding = line.embedding?.values ?? line.response?.embedding?.values ?? [];
2198
- if (embedding.length === 0) {
2199
- errors.push(`${customId}: empty embedding`);
2200
- continue;
2201
- }
2202
- byCustomId.set(customId, embedding);
2203
- }
2204
- if (errors.length > 0) {
2205
- throw new Error(`gemini batch ${batchName} failed: ${errors.join("; ")}`);
2206
- }
2207
- if (remaining.size > 0) {
2208
- throw new Error(`gemini batch ${batchName} missing ${remaining.size} embedding responses`);
2209
- }
2210
- });
2211
- params.debug?.("memory embeddings: gemini batch submit", {
2212
- requests: params.requests.length,
2213
- groups: groups.length,
2214
- wait: params.wait,
2215
- concurrency: params.concurrency,
2216
- pollIntervalMs: params.pollIntervalMs,
2217
- timeoutMs: params.timeoutMs
2218
- });
2219
- await runWithConcurrency2(tasks, params.concurrency);
2220
- return byCustomId;
2221
- }
2222
-
2223
- // src/minimem.ts
2224
- function resolveMinimemSubdir(memoryDir) {
2225
- const envDir = process.env.MINIMEM_CONFIG_DIR;
2226
- if (envDir) return envDir;
2227
- if (fsSync4.existsSync(path6.join(memoryDir, "config.json"))) return ".";
2228
- const swarmDir = path6.join(memoryDir, ".swarm", "minimem");
2229
- if (fsSync4.existsSync(path6.join(swarmDir, "config.json"))) return path6.join(".swarm", "minimem");
2230
- return ".minimem";
2231
- }
2232
- var META_KEY = "memory_index_meta_v1";
2233
- var SNIPPET_MAX_CHARS = 700;
2234
- var VECTOR_TABLE = "chunks_vec";
2235
- var FTS_TABLE = "chunks_fts";
2236
- var EMBEDDING_CACHE_TABLE = "embedding_cache";
2237
- var EMBEDDING_RETRY_MAX_ATTEMPTS = 3;
2238
- var EMBEDDING_RETRY_BASE_DELAY_MS = 500;
2239
- var EMBEDDING_RETRY_MAX_DELAY_MS = 8e3;
2240
- var EMBEDDING_QUERY_TIMEOUT_REMOTE_MS = 6e4;
2241
- var EMBEDDING_QUERY_TIMEOUT_LOCAL_MS = 5 * 6e4;
2242
- var Minimem = class _Minimem {
2243
- memoryDir;
2244
- dbPath;
2245
- chunking;
2246
- cache;
2247
- hybrid;
2248
- queryConfig;
2249
- watchConfig;
2250
- batchConfig;
2251
- vectorExtensionPath;
2252
- debug;
2253
- provider;
2254
- openAi;
2255
- gemini;
2256
- providerKey = "";
2257
- providerFallbackReason;
2258
- db;
2259
- vector;
2260
- fts;
2261
- vectorReady = null;
2262
- watcher = null;
2263
- watchTimer = null;
2264
- closed = false;
2265
- dirty = true;
2266
- syncing = null;
2267
- syncLock = false;
2268
- embeddingOptions;
2269
- constructor(config2) {
2270
- this.memoryDir = path6.resolve(config2.memoryDir);
2271
- this.dbPath = config2.dbPath ?? path6.join(this.memoryDir, resolveMinimemSubdir(this.memoryDir), "index.db");
2272
- this.chunking = {
2273
- tokens: config2.chunking?.tokens ?? 256,
2274
- overlap: config2.chunking?.overlap ?? 32
2275
- };
2276
- this.cache = {
2277
- enabled: config2.cache?.enabled ?? true,
2278
- maxEntries: config2.cache?.maxEntries ?? 1e4
2279
- };
2280
- this.hybrid = {
2281
- enabled: config2.hybrid?.enabled ?? true,
2282
- vectorWeight: config2.hybrid?.vectorWeight ?? 0.7,
2283
- textWeight: config2.hybrid?.textWeight ?? 0.3,
2284
- candidateMultiplier: config2.hybrid?.candidateMultiplier ?? 2
2285
- };
2286
- this.queryConfig = {
2287
- maxResults: config2.query?.maxResults ?? 10,
2288
- minScore: config2.query?.minScore ?? 0.3
2289
- };
2290
- this.watchConfig = {
2291
- enabled: config2.watch?.enabled ?? true,
2292
- debounceMs: config2.watch?.debounceMs ?? 1e3
2293
- };
2294
- this.batchConfig = {
2295
- enabled: config2.batch?.enabled ?? false,
2296
- wait: config2.batch?.wait ?? true,
2297
- concurrency: config2.batch?.concurrency ?? 2,
2298
- pollIntervalMs: config2.batch?.pollIntervalMs ?? 2e3,
2299
- timeoutMs: config2.batch?.timeoutMs ?? 60 * 60 * 1e3
2300
- };
2301
- this.vectorExtensionPath = config2.vectorExtensionPath;
2302
- this.debug = config2.debug;
2303
- this.embeddingOptions = config2.embedding;
2304
- this.vector = {
2305
- enabled: true,
2306
- available: null,
2307
- extensionPath: this.vectorExtensionPath
2308
- };
2309
- this.fts = { enabled: this.hybrid.enabled, available: false };
2310
- }
2311
- static async create(config2) {
2312
- const instance = new _Minimem(config2);
2313
- await instance.initialize();
2314
- return instance;
2315
- }
2316
- async initialize() {
2317
- const providerResult = await createEmbeddingProvider(this.embeddingOptions);
2318
- this.provider = providerResult.provider;
2319
- this.openAi = providerResult.openAi;
2320
- this.gemini = providerResult.gemini;
2321
- this.providerKey = this.computeProviderKey();
2322
- this.providerFallbackReason = providerResult.fallbackReason;
2323
- if (this.provider.id === "none") {
2324
- this.debug?.("Running in BM25-only mode (no embedding API available)");
2325
- }
2326
- this.db = this.openDatabase();
2327
- this.ensureSchema();
2328
- const meta = this.readMeta();
2329
- if (meta?.vectorDims) {
2330
- this.vector.dims = meta.vectorDims;
2331
- }
2332
- if (this.watchConfig.enabled) {
2333
- this.ensureWatcher();
2334
- }
2335
- }
2336
- openDatabase() {
2337
- const dbDir = path6.dirname(this.dbPath);
2338
- ensureDir(dbDir);
2339
- return new DatabaseSync(this.dbPath);
2340
- }
2341
- ensureSchema() {
2342
- const result = ensureMemoryIndexSchema({
2343
- db: this.db,
2344
- embeddingCacheTable: EMBEDDING_CACHE_TABLE,
2345
- ftsTable: FTS_TABLE,
2346
- ftsEnabled: this.fts.enabled
2347
- });
2348
- this.fts.available = result.ftsAvailable;
2349
- if (result.ftsError) {
2350
- this.fts.loadError = result.ftsError;
2351
- }
2352
- }
2353
- computeProviderKey() {
2354
- const parts = [this.provider.id, this.provider.model];
2355
- if (this.openAi) {
2356
- parts.push(this.openAi.baseUrl);
2357
- }
2358
- if (this.gemini) {
2359
- parts.push(this.gemini.baseUrl);
2360
- }
2361
- return hashText(parts.join(":"));
2362
- }
2363
- readMeta() {
2364
- try {
2365
- const row = this.db.prepare(`SELECT value FROM meta WHERE key = ?`).get(META_KEY);
2366
- if (!row?.value) return null;
2367
- return JSON.parse(row.value);
2368
- } catch {
2369
- return null;
2370
- }
2371
- }
2372
- writeMeta(meta) {
2373
- this.db.prepare(`INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)`).run(META_KEY, JSON.stringify(meta));
2374
- }
2375
- ensureWatcher() {
2376
- if (this.watcher) return;
2377
- const memorySubDir = path6.join(this.memoryDir, "memory");
2378
- const memoryFile = path6.join(this.memoryDir, "MEMORY.md");
2379
- this.watcher = chokidar.watch([memoryFile, memorySubDir], {
2380
- ignoreInitial: true,
2381
- persistent: true,
2382
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
2383
- });
2384
- const scheduleSync = () => {
2385
- this.dirty = true;
2386
- if (this.watchTimer) clearTimeout(this.watchTimer);
2387
- this.watchTimer = setTimeout(() => {
2388
- void this.sync({ reason: "watch" }).catch((err) => {
2389
- this.debug?.(`memory sync failed (watch): ${String(err)}`);
2390
- });
2391
- }, this.watchConfig.debounceMs);
2392
- };
2393
- this.watcher.on("add", scheduleSync);
2394
- this.watcher.on("change", scheduleSync);
2395
- this.watcher.on("unlink", scheduleSync);
2396
- }
2397
- /**
2398
- * Check if the index is stale by comparing file mtimes against stored values.
2399
- * This is a lightweight check (stat calls only, no file reads).
2400
- */
2401
- async isStale() {
2402
- try {
2403
- const files = await listMemoryFiles(this.memoryDir);
2404
- const stored = this.db.prepare(`SELECT path, mtime FROM files WHERE source = ?`).all("memory");
2405
- if (files.length !== stored.length) {
2406
- this.debug?.(`Stale: file count changed (${stored.length} -> ${files.length})`);
2407
- return true;
2408
- }
2409
- const storedMap = new Map(stored.map((f) => [f.path, f.mtime]));
2410
- for (const absPath of files) {
2411
- const relPath = path6.relative(this.memoryDir, absPath).replace(/\\/g, "/");
2412
- const storedMtime = storedMap.get(relPath);
2413
- if (storedMtime === void 0) {
2414
- this.debug?.(`Stale: new file ${relPath}`);
2415
- return true;
2416
- }
2417
- const stat = await fs5.stat(absPath);
2418
- const currentMtime = Math.floor(stat.mtimeMs);
2419
- if (currentMtime !== storedMtime) {
2420
- this.debug?.(`Stale: mtime changed for ${relPath}`);
2421
- return true;
2422
- }
2423
- }
2424
- return false;
2425
- } catch (err) {
2426
- this.debug?.(`Stale check failed: ${String(err)}`);
2427
- return true;
2428
- }
2429
- }
2430
- async search(query, opts) {
2431
- if (this.dirty || !this.watchConfig.enabled && await this.isStale()) {
2432
- await this.sync({ reason: "search" });
2433
- }
2434
- const cleaned = query.trim();
2435
- if (!cleaned) return [];
2436
- const minScore = opts?.minScore ?? this.queryConfig.minScore;
2437
- const maxResults = opts?.maxResults ?? this.queryConfig.maxResults;
2438
- const candidates = Math.min(
2439
- 200,
2440
- Math.max(1, Math.floor(maxResults * this.hybrid.candidateMultiplier))
2441
- );
2442
- const sourceFilter = { sql: "", params: [] };
2443
- const keywordResults = this.hybrid.enabled && this.fts.available ? await searchKeyword({
2444
- db: this.db,
2445
- ftsTable: FTS_TABLE,
2446
- providerModel: this.provider.model,
2447
- query: cleaned,
2448
- limit: candidates,
2449
- snippetMaxChars: SNIPPET_MAX_CHARS,
2450
- sourceFilter,
2451
- buildFtsQuery,
2452
- bm25RankToScore
2453
- }).catch(() => []) : [];
2454
- const queryVec = await this.embedQueryWithTimeout(cleaned);
2455
- const hasVector = queryVec.some((v) => v !== 0);
2456
- const vectorResults = hasVector ? await searchVector({
2457
- db: this.db,
2458
- vectorTable: VECTOR_TABLE,
2459
- providerModel: this.provider.model,
2460
- queryVec,
2461
- limit: candidates,
2462
- snippetMaxChars: SNIPPET_MAX_CHARS,
2463
- ensureVectorReady: (dims) => this.ensureVectorReady(dims),
2464
- sourceFilterVec: sourceFilter,
2465
- sourceFilterChunks: sourceFilter
2466
- }).catch(() => []) : [];
2467
- const typeFilterFn = opts?.type ? (id) => {
2468
- const row = this.db.prepare(`SELECT type FROM chunks WHERE id = ?`).get(id);
2469
- return row?.type === opts.type;
2470
- } : void 0;
2471
- if (!this.hybrid.enabled) {
2472
- let results = vectorResults;
2473
- if (typeFilterFn) results = results.filter((r) => typeFilterFn(r.id));
2474
- return results.filter((entry) => entry.score >= minScore).slice(0, maxResults).map((r) => ({
2475
- path: r.path,
2476
- startLine: r.startLine,
2477
- endLine: r.endLine,
2478
- score: r.score,
2479
- snippet: r.snippet
2480
- }));
2481
- }
2482
- let filteredVector = vectorResults;
2483
- let filteredKeyword = keywordResults;
2484
- if (typeFilterFn) {
2485
- filteredVector = vectorResults.filter((r) => typeFilterFn(r.id));
2486
- filteredKeyword = keywordResults.filter((r) => typeFilterFn(r.id));
2487
- }
2488
- const merged = mergeHybridResults({
2489
- vector: filteredVector.map((r) => ({
2490
- id: r.id,
2491
- path: r.path,
2492
- startLine: r.startLine,
2493
- endLine: r.endLine,
2494
- source: r.source,
2495
- snippet: r.snippet,
2496
- vectorScore: r.score
2497
- })),
2498
- keyword: filteredKeyword.map((r) => ({
2499
- id: r.id,
2500
- path: r.path,
2501
- startLine: r.startLine,
2502
- endLine: r.endLine,
2503
- source: r.source,
2504
- snippet: r.snippet,
2505
- textScore: r.textScore
2506
- })),
2507
- vectorWeight: this.hybrid.vectorWeight,
2508
- textWeight: this.hybrid.textWeight
2509
- });
2510
- return merged.filter((entry) => entry.score >= minScore).slice(0, maxResults).map((r) => ({
2511
- path: r.path,
2512
- startLine: r.startLine,
2513
- endLine: r.endLine,
2514
- score: r.score,
2515
- snippet: r.snippet
2516
- }));
2517
- }
2518
- async sync(opts) {
2519
- if (this.syncing) {
2520
- await this.syncing;
2521
- return;
2522
- }
2523
- if (this.syncLock) {
2524
- return;
2525
- }
2526
- this.syncLock = true;
2527
- this.syncing = this.runSync(opts);
2528
- try {
2529
- await this.syncing;
2530
- } finally {
2531
- this.syncing = null;
2532
- this.syncLock = false;
2533
- }
2534
- }
2535
- async runSync(opts) {
2536
- this.debug?.(`memory sync starting`, { reason: opts?.reason });
2537
- await this.ensureVectorReady();
2538
- const meta = this.readMeta();
2539
- const needsFullReindex = opts?.force || !meta || meta.model !== this.provider.model || meta.provider !== this.provider.id || meta.providerKey !== this.providerKey || meta.chunkTokens !== this.chunking.tokens || meta.chunkOverlap !== this.chunking.overlap || this.vector.available && !meta?.vectorDims;
2540
- const files = await listMemoryFiles(this.memoryDir);
2541
- const activePaths = /* @__PURE__ */ new Set();
2542
- for (const absPath of files) {
2543
- const entry = await buildFileEntry(absPath, this.memoryDir);
2544
- activePaths.add(entry.path);
2545
- const record = this.db.prepare(`SELECT hash FROM files WHERE path = ? AND source = ?`).get(entry.path, "memory");
2546
- if (!needsFullReindex && record?.hash === entry.hash) {
2547
- continue;
2548
- }
2549
- await this.indexFile(entry);
2550
- }
2551
- const staleRows = this.db.prepare(`SELECT path FROM files WHERE source = ?`).all("memory");
2552
- for (const stale of staleRows) {
2553
- if (activePaths.has(stale.path)) continue;
2554
- this.db.prepare(`DELETE FROM files WHERE path = ? AND source = ?`).run(stale.path, "memory");
2555
- try {
2556
- this.db.prepare(
2557
- `DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM chunks WHERE path = ? AND source = ?)`
2558
- ).run(stale.path, "memory");
2559
- } catch (err) {
2560
- logError2("deleteStaleVectorEntries", err, this.debug);
2561
- }
2562
- this.db.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`).run(stale.path, "memory");
2563
- this.db.prepare(`DELETE FROM knowledge_links WHERE source_path = ?`).run(stale.path);
2564
- if (this.fts.enabled && this.fts.available) {
2565
- try {
2566
- this.db.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`).run(stale.path, "memory", this.provider.model);
2567
- } catch (err) {
2568
- logError2("deleteStaleFtsEntries", err, this.debug);
2569
- }
2570
- }
2571
- }
2572
- this.writeMeta({
2573
- model: this.provider.model,
2574
- provider: this.provider.id,
2575
- providerKey: this.providerKey,
2576
- chunkTokens: this.chunking.tokens,
2577
- chunkOverlap: this.chunking.overlap,
2578
- vectorDims: this.vector.dims
2579
- });
2580
- this.pruneEmbeddingCacheIfNeeded();
2581
- this.dirty = false;
2582
- this.debug?.(`memory sync complete`, { files: files.length });
2583
- }
2584
- async indexFile(entry) {
2585
- const content = await fs5.readFile(entry.absPath, "utf-8");
2586
- const chunks = chunkMarkdown(content, this.chunking);
2587
- const { frontmatter } = parseFrontmatter(content);
2588
- const knowledgeType = frontmatter?.type ?? null;
2589
- const knowledgeId = frontmatter?.id ?? null;
2590
- const domains = frontmatter?.domain ?? null;
2591
- const entities = frontmatter?.entities ?? null;
2592
- const confidence = frontmatter?.confidence ?? null;
2593
- const links = frontmatter?.links ?? null;
2594
- const embeddings = await this.embedChunks(chunks);
2595
- this.db.prepare(
2596
- `INSERT OR REPLACE INTO files (path, source, hash, mtime, size) VALUES (?, ?, ?, ?, ?)`
2597
- ).run(entry.path, "memory", entry.hash, Math.floor(entry.mtimeMs), entry.size);
2598
- try {
2599
- this.db.prepare(
2600
- `DELETE FROM ${VECTOR_TABLE} WHERE id IN (SELECT id FROM chunks WHERE path = ? AND source = ?)`
2601
- ).run(entry.path, "memory");
2602
- } catch (err) {
2603
- logError2("deleteOldVectorChunks", err, this.debug);
2604
- }
2605
- this.db.prepare(`DELETE FROM chunks WHERE path = ? AND source = ?`).run(entry.path, "memory");
2606
- if (this.fts.enabled && this.fts.available) {
2607
- try {
2608
- this.db.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`).run(entry.path, "memory", this.provider.model);
2609
- } catch (err) {
2610
- logError2("deleteOldFtsChunks", err, this.debug);
2611
- }
2612
- }
2613
- this.db.prepare(`DELETE FROM knowledge_links WHERE source_path = ?`).run(entry.path);
2614
- const now = Date.now();
2615
- for (let i = 0; i < chunks.length; i++) {
2616
- const chunk = chunks[i];
2617
- const embedding = embeddings[i] ?? [];
2618
- const chunkId = randomUUID();
2619
- const meta = extractChunkMetadata(chunk.text);
2620
- this.db.prepare(
2621
- `INSERT INTO chunks (id, path, source, start_line, end_line, hash, model, text, embedding, updated_at, type, knowledge_type, knowledge_id, domains, entities, confidence)
2622
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2623
- ).run(
2624
- chunkId,
2625
- entry.path,
2626
- "memory",
2627
- chunk.startLine,
2628
- chunk.endLine,
2629
- chunk.hash,
2630
- this.provider.model,
2631
- chunk.text,
2632
- JSON.stringify(embedding),
2633
- now,
2634
- meta.type ?? null,
2635
- knowledgeType,
2636
- knowledgeId,
2637
- domains ? JSON.stringify(domains) : null,
2638
- entities ? JSON.stringify(entities) : null,
2639
- confidence
2640
- );
2641
- if (this.vector.available && embedding.length > 0) {
2642
- if (!this.vector.dims) {
2643
- this.vector.dims = embedding.length;
2644
- this.ensureVectorTable(embedding.length);
2645
- }
2646
- try {
2647
- this.db.prepare(`INSERT INTO ${VECTOR_TABLE} (id, embedding) VALUES (?, ?)`).run(chunkId, vectorToBlob(embedding));
2648
- } catch (err) {
2649
- logError2("insertVectorChunk", err, this.debug);
2650
- }
2651
- }
2652
- if (this.fts.enabled && this.fts.available) {
2653
- try {
2654
- this.db.prepare(
2655
- `INSERT INTO ${FTS_TABLE} (text, id, path, source, model, start_line, end_line)
2656
- VALUES (?, ?, ?, ?, ?, ?, ?)`
2657
- ).run(
2658
- chunk.text,
2659
- chunkId,
2660
- entry.path,
2661
- "memory",
2662
- this.provider.model,
2663
- chunk.startLine,
2664
- chunk.endLine
2665
- );
2666
- } catch (err) {
2667
- logError2("insertFtsChunk", err, this.debug);
2668
- }
2669
- }
2670
- }
2671
- if (links && knowledgeId) {
2672
- const upsertLink = this.db.prepare(
2673
- `INSERT OR REPLACE INTO knowledge_links (from_id, to_id, relation, layer, weight, source_path, created_at)
2674
- VALUES (?, ?, ?, ?, ?, ?, ?)`
2675
- );
2676
- for (const link of links) {
2677
- upsertLink.run(
2678
- knowledgeId,
2679
- link.target,
2680
- link.relation,
2681
- link.layer ?? null,
2682
- 0.5,
2683
- entry.path,
2684
- now
2685
- );
2686
- }
2687
- }
2688
- }
2689
- async embedChunks(chunks) {
2690
- if (chunks.length === 0) return [];
2691
- const hashes = chunks.map((c) => c.hash);
2692
- const cached = this.loadEmbeddingCache(hashes);
2693
- const missing = [];
2694
- for (let i = 0; i < chunks.length; i++) {
2695
- if (!cached.has(hashes[i])) {
2696
- missing.push({ index: i, chunk: chunks[i] });
2697
- }
2698
- }
2699
- if (missing.length > 0) {
2700
- const texts = missing.map((m) => m.chunk.text);
2701
- const newEmbeddings = await this.embedBatchWithRetry(texts);
2702
- for (let i = 0; i < missing.length; i++) {
2703
- const hash = missing[i].chunk.hash;
2704
- const embedding = newEmbeddings[i] ?? [];
2705
- cached.set(hash, embedding);
2706
- this.upsertEmbeddingCache(hash, embedding);
2707
- }
2708
- }
2709
- return hashes.map((h) => cached.get(h) ?? []);
2710
- }
2711
- async embedBatchWithRetry(texts) {
2712
- if (texts.length === 0) return [];
2713
- if (this.batchConfig.enabled) {
2714
- try {
2715
- return await this.embedWithBatchApi(texts);
2716
- } catch (err) {
2717
- this.debug?.(`batch embedding failed, falling back to direct: ${String(err)}`);
2718
- }
2719
- }
2720
- let lastError = null;
2721
- for (let attempt = 0; attempt < EMBEDDING_RETRY_MAX_ATTEMPTS; attempt++) {
2722
- try {
2723
- return await this.provider.embedBatch(texts);
2724
- } catch (err) {
2725
- lastError = err instanceof Error ? err : new Error(String(err));
2726
- if (attempt < EMBEDDING_RETRY_MAX_ATTEMPTS - 1) {
2727
- const delay = Math.min(
2728
- EMBEDDING_RETRY_MAX_DELAY_MS,
2729
- EMBEDDING_RETRY_BASE_DELAY_MS * Math.pow(2, attempt)
2730
- );
2731
- await new Promise((resolve3) => setTimeout(resolve3, delay));
2732
- }
2733
- }
2734
- }
2735
- throw lastError;
2736
- }
2737
- async embedWithBatchApi(texts) {
2738
- if (this.openAi) {
2739
- const requests = texts.map((text, i) => ({
2740
- custom_id: `chunk-${i}`,
2741
- method: "POST",
2742
- url: OPENAI_BATCH_ENDPOINT,
2743
- body: { model: this.openAi.model, input: text }
2744
- }));
2745
- const results = await runOpenAiEmbeddingBatches({
2746
- openAi: this.openAi,
2747
- source: "minimem",
2748
- requests,
2749
- wait: this.batchConfig.wait,
2750
- pollIntervalMs: this.batchConfig.pollIntervalMs,
2751
- timeoutMs: this.batchConfig.timeoutMs,
2752
- concurrency: this.batchConfig.concurrency,
2753
- debug: this.debug
2754
- });
2755
- return texts.map((_, i) => results.get(`chunk-${i}`) ?? []);
2756
- }
2757
- if (this.gemini) {
2758
- const requests = texts.map((text, i) => ({
2759
- custom_id: `chunk-${i}`,
2760
- content: { parts: [{ text }] },
2761
- taskType: "RETRIEVAL_DOCUMENT"
2762
- }));
2763
- const results = await runGeminiEmbeddingBatches({
2764
- gemini: this.gemini,
2765
- source: "minimem",
2766
- requests,
2767
- wait: this.batchConfig.wait,
2768
- pollIntervalMs: this.batchConfig.pollIntervalMs,
2769
- timeoutMs: this.batchConfig.timeoutMs,
2770
- concurrency: this.batchConfig.concurrency,
2771
- debug: this.debug
2772
- });
2773
- return texts.map((_, i) => results.get(`chunk-${i}`) ?? []);
2774
- }
2775
- throw new Error("Batch API not available for local embeddings");
2776
- }
2777
- async embedQueryWithTimeout(text) {
2778
- const timeout = this.provider.id === "local" ? EMBEDDING_QUERY_TIMEOUT_LOCAL_MS : EMBEDDING_QUERY_TIMEOUT_REMOTE_MS;
2779
- const ac = new AbortController();
2780
- const timer = setTimeout(() => ac.abort(), timeout);
2781
- try {
2782
- const result = await Promise.race([
2783
- this.provider.embedQuery(text),
2784
- new Promise((_, reject) => {
2785
- ac.signal.addEventListener(
2786
- "abort",
2787
- () => reject(new Error("embedding query timeout"))
2788
- );
2789
- })
2790
- ]);
2791
- return result;
2792
- } finally {
2793
- clearTimeout(timer);
2794
- }
2795
- }
2796
- loadEmbeddingCache(hashes) {
2797
- const result = /* @__PURE__ */ new Map();
2798
- if (!this.cache.enabled || hashes.length === 0) return result;
2799
- const placeholders = hashes.map(() => "?").join(",");
2800
- const rows = this.db.prepare(
2801
- `SELECT hash, embedding FROM ${EMBEDDING_CACHE_TABLE}
2802
- WHERE provider = ? AND model = ? AND provider_key = ? AND hash IN (${placeholders})`
2803
- ).all(this.provider.id, this.provider.model, this.providerKey, ...hashes);
2804
- const now = Date.now();
2805
- for (const row of rows) {
2806
- result.set(row.hash, parseEmbedding(row.embedding));
2807
- this.db.prepare(
2808
- `UPDATE ${EMBEDDING_CACHE_TABLE} SET updated_at = ?
2809
- WHERE provider = ? AND model = ? AND provider_key = ? AND hash = ?`
2810
- ).run(now, this.provider.id, this.provider.model, this.providerKey, row.hash);
321
+ memoryDir,
322
+ embedding,
323
+ hybrid: merged.hybrid,
324
+ query: merged.query,
325
+ chunking: merged.chunking,
326
+ watch: {
327
+ enabled: options?.watch ?? false
328
+ // Disable watching by default in CLI
2811
329
  }
2812
- return result;
2813
- }
2814
- upsertEmbeddingCache(hash, embedding) {
2815
- if (!this.cache.enabled) return;
2816
- const now = Date.now();
2817
- this.db.prepare(
2818
- `INSERT OR REPLACE INTO ${EMBEDDING_CACHE_TABLE}
2819
- (provider, model, provider_key, hash, embedding, dims, updated_at)
2820
- VALUES (?, ?, ?, ?, ?, ?, ?)`
2821
- ).run(
2822
- this.provider.id,
2823
- this.provider.model,
2824
- this.providerKey,
2825
- hash,
2826
- JSON.stringify(embedding),
2827
- embedding.length,
2828
- now
2829
- );
330
+ };
331
+ }
332
+ async function isInitialized(memoryDir) {
333
+ const configPath = getConfigPath(memoryDir);
334
+ try {
335
+ await fs2.access(configPath);
336
+ return true;
337
+ } catch {
338
+ return false;
2830
339
  }
2831
- pruneEmbeddingCacheIfNeeded() {
2832
- if (!this.cache.enabled) return;
2833
- const row = this.db.prepare(`SELECT COUNT(*) as count FROM ${EMBEDDING_CACHE_TABLE}`).get();
2834
- if (row.count <= this.cache.maxEntries) return;
2835
- const excess = row.count - this.cache.maxEntries;
2836
- this.db.prepare(
2837
- `DELETE FROM ${EMBEDDING_CACHE_TABLE}
2838
- WHERE rowid IN (
2839
- SELECT rowid FROM ${EMBEDDING_CACHE_TABLE}
2840
- ORDER BY updated_at ASC
2841
- LIMIT ?
2842
- )`
2843
- ).run(excess);
2844
- }
2845
- async ensureVectorReady(dimensions) {
2846
- if (this.vector.available === true) return true;
2847
- if (this.vector.available === false) return false;
2848
- if (!this.vectorReady) {
2849
- this.vectorReady = this.loadVectorExtension();
2850
- }
2851
- const ready = await this.vectorReady;
2852
- if (ready && dimensions && !this.vector.dims) {
2853
- this.vector.dims = dimensions;
2854
- this.ensureVectorTable(dimensions);
2855
- }
2856
- return ready;
2857
- }
2858
- async loadVectorExtension() {
2859
- const result = await loadSqliteVecExtension({
2860
- db: this.db,
2861
- extensionPath: this.vectorExtensionPath
2862
- });
2863
- this.vector.available = result.ok;
2864
- if (result.error) {
2865
- this.vector.loadError = result.error;
2866
- this.debug?.(`sqlite-vec load failed: ${result.error}`);
2867
- }
2868
- if (result.extensionPath) {
2869
- this.vector.extensionPath = result.extensionPath;
2870
- }
2871
- return result.ok;
340
+ }
341
+ function formatPath(filePath) {
342
+ const home = os2.homedir();
343
+ if (filePath.startsWith(home)) {
344
+ return "~" + filePath.slice(home.length);
2872
345
  }
2873
- ensureVectorTable(dimensions) {
2874
- if (!this.vector.available) return;
2875
- try {
2876
- this.db.exec(
2877
- `CREATE VIRTUAL TABLE IF NOT EXISTS ${VECTOR_TABLE} USING vec0(
2878
- id TEXT PRIMARY KEY,
2879
- embedding FLOAT[${dimensions}]
2880
- )`
2881
- );
2882
- } catch (err) {
2883
- this.debug?.(`vector table creation failed: ${String(err)}`);
346
+ return filePath;
347
+ }
348
+
349
+ // src/store/manifest.ts
350
+ import fs3 from "fs/promises";
351
+ import fsSync2 from "fs";
352
+ import path3 from "path";
353
+ import os3 from "os";
354
+ var GLOBAL_MANIFEST_PATH = path3.join(
355
+ os3.homedir(),
356
+ ".config",
357
+ "minimem",
358
+ "stores.json"
359
+ );
360
+ var LINKS_FILENAME = "links.json";
361
+ async function loadManifest(manifestPath) {
362
+ const filePath = manifestPath ?? GLOBAL_MANIFEST_PATH;
363
+ try {
364
+ const content = await fs3.readFile(filePath, "utf-8");
365
+ const parsed = JSON.parse(content);
366
+ for (const [name, def] of Object.entries(parsed.stores ?? {})) {
367
+ parsed.stores[name] = {
368
+ ...def,
369
+ path: expandHome(def.path)
370
+ };
2884
371
  }
372
+ return { stores: parsed.stores ?? {} };
373
+ } catch {
374
+ return { stores: {} };
2885
375
  }
2886
- async readFile(relativePath) {
2887
- const absPath = path6.join(this.memoryDir, relativePath);
376
+ }
377
+ async function saveManifest(manifest, manifestPath) {
378
+ const filePath = manifestPath ?? GLOBAL_MANIFEST_PATH;
379
+ await fs3.mkdir(path3.dirname(filePath), { recursive: true });
380
+ await fs3.writeFile(filePath, JSON.stringify(manifest, null, 2), "utf-8");
381
+ }
382
+ async function loadStoreLinks(memoryDir) {
383
+ const candidates = [
384
+ path3.join(memoryDir, ".minimem", LINKS_FILENAME),
385
+ path3.join(memoryDir, ".swarm", "minimem", LINKS_FILENAME),
386
+ path3.join(memoryDir, LINKS_FILENAME)
387
+ ];
388
+ for (const candidate of candidates) {
2888
389
  try {
2889
- return await fs5.readFile(absPath, "utf-8");
390
+ const content = await fs3.readFile(candidate, "utf-8");
391
+ const parsed = JSON.parse(content);
392
+ return { links: parsed.links ?? [] };
2890
393
  } catch {
2891
- return null;
394
+ continue;
2892
395
  }
2893
396
  }
2894
- /**
2895
- * Read specific lines from a memory file
2896
- */
2897
- async readLines(relativePath, opts) {
2898
- const content = await this.readFile(relativePath);
2899
- if (content === null) return null;
2900
- const allLines = content.split("\n");
2901
- const from = Math.max(1, opts?.from ?? 1);
2902
- const lines = opts?.lines ?? allLines.length;
2903
- const startIdx = from - 1;
2904
- const endIdx = Math.min(startIdx + lines, allLines.length);
2905
- const selectedLines = allLines.slice(startIdx, endIdx);
2906
- return {
2907
- content: selectedLines.join("\n"),
2908
- startLine: from,
2909
- endLine: startIdx + selectedLines.length
2910
- };
2911
- }
2912
- /**
2913
- * Write content to a memory file (creates or overwrites)
2914
- */
2915
- async writeFile(relativePath, content) {
2916
- this.validateMemoryPath(relativePath);
2917
- const absPath = path6.join(this.memoryDir, relativePath);
2918
- const dir = path6.dirname(absPath);
2919
- await fs5.mkdir(dir, { recursive: true });
2920
- await fs5.writeFile(absPath, content, "utf-8");
2921
- this.dirty = true;
2922
- this.debug?.(`memory write: ${relativePath}`);
2923
- }
2924
- /**
2925
- * Append content to a memory file (creates if doesn't exist)
2926
- */
2927
- async appendFile(relativePath, content) {
2928
- this.validateMemoryPath(relativePath);
2929
- const absPath = path6.join(this.memoryDir, relativePath);
2930
- const dir = path6.dirname(absPath);
2931
- await fs5.mkdir(dir, { recursive: true });
2932
- let toAppend = content;
2933
- try {
2934
- const existing = await fs5.readFile(absPath, "utf-8");
2935
- if (existing.length > 0 && !existing.endsWith("\n")) {
2936
- toAppend = "\n" + content;
2937
- }
2938
- } catch {
397
+ return { links: [] };
398
+ }
399
+ async function saveStoreLinks(memoryDir, links) {
400
+ const candidates = [
401
+ path3.join(memoryDir, ".minimem"),
402
+ path3.join(memoryDir, ".swarm", "minimem"),
403
+ memoryDir
404
+ // contained layout
405
+ ];
406
+ let targetDir = candidates[0];
407
+ for (const candidate of candidates) {
408
+ if (fsSync2.existsSync(candidate)) {
409
+ targetDir = candidate;
410
+ break;
411
+ }
412
+ }
413
+ await fs3.mkdir(targetDir, { recursive: true });
414
+ await fs3.writeFile(
415
+ path3.join(targetDir, LINKS_FILENAME),
416
+ JSON.stringify(links, null, 2),
417
+ "utf-8"
418
+ );
419
+ }
420
+ function resolveStore(manifest, storeName) {
421
+ return manifest.stores[storeName] ?? null;
422
+ }
423
+ function resolveStoreName(manifest, dirPath) {
424
+ const resolved = path3.resolve(dirPath);
425
+ for (const [name, def] of Object.entries(manifest.stores)) {
426
+ if (path3.resolve(def.path) === resolved) {
427
+ return name;
2939
428
  }
2940
- await fs5.appendFile(absPath, toAppend, "utf-8");
2941
- this.dirty = true;
2942
- this.debug?.(`memory append: ${relativePath}`);
2943
429
  }
2944
- /**
2945
- * Append content to today's daily log (memory/YYYY-MM-DD.md)
2946
- */
2947
- async appendToday(content) {
2948
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2949
- const relativePath = `memory/${today}.md`;
2950
- await this.appendFile(relativePath, content);
2951
- return relativePath;
430
+ return null;
431
+ }
432
+ async function getLinkedStoreNames(manifest, storeName) {
433
+ const storeDef = manifest.stores[storeName];
434
+ if (!storeDef) return [];
435
+ const storeLinks = await loadStoreLinks(storeDef.path);
436
+ return [...new Set(storeLinks.links)];
437
+ }
438
+ function getManifestPath() {
439
+ return GLOBAL_MANIFEST_PATH;
440
+ }
441
+ function expandHome(filePath) {
442
+ if (filePath.startsWith("~/")) {
443
+ return path3.join(os3.homedir(), filePath.slice(2));
2952
444
  }
2953
- /**
2954
- * List all memory files
2955
- */
2956
- async listFiles() {
2957
- const files = await listMemoryFiles(this.memoryDir);
2958
- return files.map((f) => path6.relative(this.memoryDir, f).replace(/\\/g, "/"));
445
+ return filePath;
446
+ }
447
+
448
+ // src/store/materialize.ts
449
+ import fs4 from "fs/promises";
450
+ import fsSync3 from "fs";
451
+ import path4 from "path";
452
+ import os4 from "os";
453
+ import { execFile } from "child_process";
454
+ import { promisify } from "util";
455
+ var execFileAsync = promisify(execFile);
456
+ var CACHE_BASE = path4.join(os4.homedir(), ".cache", "minimem", "stores");
457
+ async function materializeStore(storeName, storeDef, opts) {
458
+ if (fsSync3.existsSync(storeDef.path)) {
459
+ return materializeLocal(storeName, storeDef.path);
2959
460
  }
2960
- /**
2961
- * Validate that a path is within allowed memory locations
2962
- */
2963
- validateMemoryPath(relativePath) {
2964
- const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
2965
- if (normalized === "MEMORY.md" || normalized === "memory.md") {
2966
- return;
461
+ if (storeDef.remote) {
462
+ return materializeRemote(storeName, storeDef, opts?.refresh ?? false);
463
+ }
464
+ return null;
465
+ }
466
+ async function materializeLocal(storeName, storePath) {
467
+ const tmpBase = await fs4.mkdtemp(
468
+ path4.join(os4.tmpdir(), "minimem-stores-")
469
+ );
470
+ const symlinkDir = path4.join(tmpBase, storeName);
471
+ await fs4.symlink(storePath, symlinkDir, "dir");
472
+ return {
473
+ // The Minimem instance should use the original path for its DB,
474
+ // but the symlink path can be used for discovery/resolution.
475
+ // We return the original path since Minimem works directly with it.
476
+ path: storePath,
477
+ strategy: "symlink",
478
+ cleanup: async () => {
479
+ try {
480
+ await fs4.unlink(symlinkDir);
481
+ await fs4.rmdir(tmpBase);
482
+ } catch {
483
+ }
2967
484
  }
2968
- if (normalized.startsWith("memory/") && normalized.endsWith(".md")) {
2969
- if (normalized.includes("..")) {
2970
- throw new Error(`Invalid memory path: ${relativePath} (path traversal not allowed)`);
485
+ };
486
+ }
487
+ async function materializeRemote(storeName, storeDef, refresh) {
488
+ const cacheDir = path4.join(CACHE_BASE, storeName);
489
+ try {
490
+ if (fsSync3.existsSync(path4.join(cacheDir, ".git"))) {
491
+ if (refresh) {
492
+ await gitFetch(cacheDir);
2971
493
  }
2972
- return;
494
+ } else {
495
+ await gitClone(storeDef.remote, cacheDir);
2973
496
  }
2974
- throw new Error(
2975
- `Invalid memory path: ${relativePath}. Must be MEMORY.md or memory/*.md`
2976
- );
2977
- }
2978
- async status() {
2979
- const fileRow = this.db.prepare(`SELECT COUNT(*) as count FROM files`).get();
2980
- const chunkRow = this.db.prepare(`SELECT COUNT(*) as count FROM chunks`).get();
2981
- const cacheRow = this.db.prepare(`SELECT COUNT(*) as count FROM ${EMBEDDING_CACHE_TABLE}`).get();
2982
497
  return {
2983
- memoryDir: this.memoryDir,
2984
- dbPath: this.dbPath,
2985
- provider: this.provider.id,
2986
- model: this.provider.model,
2987
- vectorAvailable: this.vector.available === true,
2988
- ftsAvailable: this.fts.available,
2989
- bm25Only: this.provider.id === "none",
2990
- fallbackReason: this.providerFallbackReason,
2991
- fileCount: fileRow.count,
2992
- chunkCount: chunkRow.count,
2993
- cacheCount: cacheRow.count
498
+ path: cacheDir,
499
+ strategy: "remote",
500
+ cleanup: async () => {
501
+ }
2994
502
  };
503
+ } catch {
504
+ return null;
2995
505
  }
2996
- /**
2997
- * Search with knowledge metadata filters (domain, entities, confidence, type).
2998
- * Runs a standard search then post-filters by knowledge columns.
2999
- */
3000
- async knowledgeSearch(query, opts) {
3001
- if (this.dirty || !this.watchConfig.enabled && await this.isStale()) {
3002
- await this.sync({ reason: "knowledgeSearch" });
3003
- }
3004
- const cleaned = query.trim();
3005
- if (!cleaned) return [];
3006
- const minScore = opts?.minScore ?? this.queryConfig.minScore;
3007
- const maxResults = opts?.maxResults ?? this.queryConfig.maxResults;
3008
- const { sql: knowledgeWhere, params: knowledgeParams } = buildKnowledgeFilterSql({
3009
- domain: opts?.domain,
3010
- entities: opts?.entities,
3011
- minConfidence: opts?.minConfidence,
3012
- knowledgeType: opts?.knowledgeType
3013
- });
3014
- if (!knowledgeWhere) {
3015
- return this.search(query, { maxResults, minScore });
3016
- }
3017
- const matchingRows = this.db.prepare(
3018
- `SELECT id FROM chunks c WHERE c.model = ? AND c.source = 'memory'${knowledgeWhere}`
3019
- ).all(this.provider.model, ...knowledgeParams);
3020
- const matchingIds = new Set(matchingRows.map((r) => r.id));
3021
- if (matchingIds.size === 0) return [];
3022
- const overFetch = Math.max(maxResults * 3, 30);
3023
- const results = await this.search(query, {
3024
- maxResults: overFetch,
3025
- minScore
3026
- });
3027
- const filtered = [];
3028
- for (const r of results) {
3029
- const row = this.db.prepare(
3030
- `SELECT id FROM chunks WHERE path = ? AND start_line = ? AND end_line = ? AND model = ?`
3031
- ).get(r.path, r.startLine, r.endLine, this.provider.model);
3032
- if (row && matchingIds.has(row.id)) {
3033
- filtered.push(r);
3034
- if (filtered.length >= maxResults) break;
3035
- }
3036
- }
3037
- return filtered;
506
+ }
507
+ async function gitClone(remote, targetDir) {
508
+ await fs4.mkdir(path4.dirname(targetDir), { recursive: true });
509
+ await execFileAsync("git", ["clone", "--depth", "1", remote, targetDir], {
510
+ timeout: 6e4
511
+ });
512
+ }
513
+ async function gitFetch(cacheDir) {
514
+ await execFileAsync("git", ["fetch", "--depth", "1", "origin"], {
515
+ cwd: cacheDir,
516
+ timeout: 6e4
517
+ });
518
+ await execFileAsync("git", ["reset", "--hard", "origin/HEAD"], {
519
+ cwd: cacheDir,
520
+ timeout: 3e4
521
+ });
522
+ }
523
+
524
+ // src/cli/commands/init.ts
525
+ var MEMORY_TEMPLATE = `# Memory
526
+
527
+ This is your memory file. Add notes, decisions, and context here.
528
+
529
+ ## Quick Start
530
+
531
+ - Add daily logs in the \`memory/\` directory (e.g., \`memory/2024-01-15.md\`)
532
+ - Use \`minimem search <query>\` to find relevant memories
533
+ - Use \`minimem append <text>\` to quickly add to today's log
534
+
535
+ ## Notes
536
+
537
+ `;
538
+ async function init(dir, options) {
539
+ const memoryDir = resolveMemoryDir({ dir, global: options.global });
540
+ const displayPath = formatPath(memoryDir);
541
+ if (!options.force && await isInitialized(memoryDir)) {
542
+ console.log(`Already initialized: ${displayPath}`);
543
+ console.log("Use --force to reinitialize");
544
+ return;
3038
545
  }
3039
- /**
3040
- * Get knowledge graph links from or to a node.
3041
- */
3042
- getLinks(nodeId, direction = "from", opts) {
3043
- if (direction === "from") {
3044
- return getLinksFrom(this.db, nodeId, opts);
3045
- }
3046
- return getLinksTo(this.db, nodeId, opts);
546
+ console.log(`Initializing minimem in ${displayPath}...`);
547
+ await fs5.mkdir(memoryDir, { recursive: true });
548
+ await fs5.mkdir(path5.join(memoryDir, "memory"), { recursive: true });
549
+ const memoryFilePath = path5.join(memoryDir, "MEMORY.md");
550
+ try {
551
+ await fs5.access(memoryFilePath);
552
+ console.log(" MEMORY.md already exists, skipping");
553
+ } catch {
554
+ await fs5.writeFile(memoryFilePath, MEMORY_TEMPLATE, "utf-8");
555
+ console.log(" Created MEMORY.md");
3047
556
  }
3048
- /**
3049
- * Get neighbor nodes via BFS traversal.
3050
- */
3051
- getGraphNeighbors(nodeId, depth = 1, opts) {
3052
- return getNeighbors(this.db, nodeId, depth, opts);
557
+ const config2 = getInitConfig();
558
+ const configPath = path5.join(memoryDir, "config.json");
559
+ await fs5.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
560
+ console.log(" Created config.json");
561
+ const gitignorePath = path5.join(memoryDir, ".gitignore");
562
+ await fs5.writeFile(gitignorePath, "index.db\nindex.db-*\n", "utf-8");
563
+ console.log(" Created .gitignore");
564
+ await materializeLinkedStores(memoryDir);
565
+ if (!options.skipSync) {
566
+ await initialSync(memoryDir);
3053
567
  }
3054
- /**
3055
- * Find shortest path between two knowledge nodes.
3056
- */
3057
- getGraphPath(fromId, toId, maxDepth = 3) {
3058
- return getPathBetween(this.db, fromId, toId, maxDepth);
568
+ console.log();
569
+ console.log("Done! Your memory directory is ready.");
570
+ console.log(`Search your memories with: minimem search "your query"${dir ? ` --dir ${dir}` : ""}`);
571
+ }
572
+ async function initialSync(memoryDir) {
573
+ console.log();
574
+ console.log("Indexing memory files...");
575
+ const cliConfig = await loadConfig(memoryDir);
576
+ const config2 = buildMinimemConfig(memoryDir, cliConfig, {
577
+ watch: false
578
+ });
579
+ const { Minimem: Minimem2 } = await import("./minimem-MQXSBGNG.js");
580
+ let minimem = null;
581
+ try {
582
+ minimem = await Minimem2.create(config2);
583
+ await minimem.sync({ force: false });
584
+ const status2 = await minimem.status();
585
+ console.log(` Indexed ${status2.fileCount} file(s), ${status2.chunkCount} chunk(s)`);
586
+ } catch (err) {
587
+ console.log(" Skipped indexing (set an embedding API key to enable)");
588
+ } finally {
589
+ minimem?.close();
3059
590
  }
3060
- close() {
3061
- if (this.closed) return;
3062
- this.closed = true;
3063
- if (this.watchTimer) {
3064
- clearTimeout(this.watchTimer);
3065
- this.watchTimer = null;
3066
- }
3067
- if (this.watcher) {
3068
- void this.watcher.close();
3069
- this.watcher = null;
591
+ }
592
+ async function materializeLinkedStores(memoryDir) {
593
+ try {
594
+ const manifest = await loadManifest();
595
+ if (Object.keys(manifest.stores).length === 0) return;
596
+ const storeName = resolveStoreName(manifest, memoryDir);
597
+ if (!storeName) return;
598
+ const linkedNames = await getLinkedStoreNames(manifest, storeName);
599
+ if (linkedNames.length === 0) return;
600
+ let materialized = 0;
601
+ for (const linkedName of linkedNames) {
602
+ const linkedDef = resolveStore(manifest, linkedName);
603
+ if (!linkedDef) continue;
604
+ const result = await materializeStore(linkedName, linkedDef);
605
+ if (result) {
606
+ if (result.strategy === "remote") {
607
+ console.log(` Materialized linked store "${linkedName}" from remote`);
608
+ }
609
+ await result.cleanup();
610
+ materialized++;
611
+ }
3070
612
  }
3071
- try {
3072
- this.db.close();
3073
- } catch (err) {
3074
- logError2("dbClose", err, this.debug);
613
+ if (materialized > 0) {
614
+ console.log(` ${materialized} linked store(s) ready`);
3075
615
  }
616
+ } catch {
3076
617
  }
3077
- };
618
+ }
3078
619
 
3079
620
  // src/cli/commands/search.ts
3080
621
  async function search(query, options) {
@@ -3096,13 +637,14 @@ async function search(query, options) {
3096
637
  if (validDirs.length === 0) {
3097
638
  exitWithError("No valid initialized memory directories found.");
3098
639
  }
640
+ const { dirs: allDirs, cleanups } = options.noLinks ? { dirs: validDirs, cleanups: [] } : await resolveLinkedDirs(validDirs);
3099
641
  const maxResults = options.max ? parseInt(options.max, 10) : 10;
3100
642
  const minScore = options.minScore ? parseFloat(options.minScore) : void 0;
3101
643
  const allResults = [];
3102
644
  const instances = [];
3103
645
  let warnedBm25 = false;
3104
646
  try {
3105
- for (const memoryDir of validDirs) {
647
+ for (const memoryDir of allDirs) {
3106
648
  const cliConfig = await loadConfig(memoryDir);
3107
649
  const config2 = buildMinimemConfig(memoryDir, cliConfig, {
3108
650
  provider: options.provider,
@@ -3137,7 +679,7 @@ async function search(query, options) {
3137
679
  console.log(JSON.stringify(topResults, null, 2));
3138
680
  return;
3139
681
  }
3140
- const showSource = validDirs.length > 1;
682
+ const showSource = allDirs.length > 1;
3141
683
  for (const result of topResults) {
3142
684
  const score = (result.score * 100).toFixed(1);
3143
685
  const location = `${result.path}:${result.startLine}-${result.endLine}`;
@@ -3150,12 +692,46 @@ async function search(query, options) {
3150
692
  console.log(formatSnippet(result.snippet));
3151
693
  console.log();
3152
694
  }
3153
- const dirSummary = validDirs.length > 1 ? ` across ${validDirs.length} directories` : "";
695
+ const dirSummary = allDirs.length > 1 ? ` across ${allDirs.length} directories` : "";
3154
696
  console.log(`Found ${topResults.length} result${topResults.length === 1 ? "" : "s"}${dirSummary}`);
3155
697
  } finally {
3156
698
  for (const instance of instances) {
3157
699
  instance.close();
3158
700
  }
701
+ for (const cleanup of cleanups) {
702
+ await cleanup().catch(() => {
703
+ });
704
+ }
705
+ }
706
+ }
707
+ async function resolveLinkedDirs(dirs) {
708
+ const cleanups = [];
709
+ try {
710
+ const manifest = await loadManifest();
711
+ if (Object.keys(manifest.stores).length === 0) {
712
+ return { dirs, cleanups };
713
+ }
714
+ const allDirs = new Set(dirs);
715
+ for (const dir of dirs) {
716
+ const storeName = resolveStoreName(manifest, dir);
717
+ if (!storeName) continue;
718
+ const linkedNames = await getLinkedStoreNames(manifest, storeName);
719
+ for (const linkedName of linkedNames) {
720
+ const linkedDef = resolveStore(manifest, linkedName);
721
+ if (!linkedDef) continue;
722
+ try {
723
+ const result = await materializeStore(linkedName, linkedDef);
724
+ if (result) {
725
+ allDirs.add(result.path);
726
+ cleanups.push(result.cleanup);
727
+ }
728
+ } catch {
729
+ }
730
+ }
731
+ }
732
+ return { dirs: [...allDirs], cleanups };
733
+ } catch {
734
+ return { dirs, cleanups };
3159
735
  }
3160
736
  }
3161
737
  function formatSnippet(snippet) {
@@ -3305,7 +881,7 @@ ${text}`;
3305
881
 
3306
882
  // src/cli/commands/upsert.ts
3307
883
  import * as fs6 from "fs/promises";
3308
- import * as path7 from "path";
884
+ import * as path6 from "path";
3309
885
  async function upsert(file, content, options) {
3310
886
  const memoryDir = resolveMemoryDir({ dir: options.dir, global: options.global });
3311
887
  if (!await isInitialized(memoryDir)) {
@@ -3330,15 +906,15 @@ async function upsert(file, content, options) {
3330
906
  project: process.cwd()
3331
907
  } : void 0;
3332
908
  const filePath = resolveFilePath(file, memoryDir);
3333
- const resolvedPath = path7.resolve(filePath);
3334
- const resolvedMemoryDir = path7.resolve(memoryDir);
3335
- if (!resolvedPath.startsWith(resolvedMemoryDir + path7.sep) && resolvedPath !== resolvedMemoryDir) {
909
+ const resolvedPath = path6.resolve(filePath);
910
+ const resolvedMemoryDir = path6.resolve(memoryDir);
911
+ if (!resolvedPath.startsWith(resolvedMemoryDir + path6.sep) && resolvedPath !== resolvedMemoryDir) {
3336
912
  exitWithError(
3337
913
  "File path must be within the memory directory.",
3338
914
  `Memory dir: ${formatPath(memoryDir)}, File: ${formatPath(filePath)}`
3339
915
  );
3340
916
  }
3341
- const parentDir = path7.dirname(filePath);
917
+ const parentDir = path6.dirname(filePath);
3342
918
  await fs6.mkdir(parentDir, { recursive: true });
3343
919
  let isUpdate = false;
3344
920
  let existingContent;
@@ -3374,7 +950,7 @@ async function upsert(file, content, options) {
3374
950
  }
3375
951
  }
3376
952
  await fs6.writeFile(filePath, contentToWrite, "utf-8");
3377
- const relativePath = path7.relative(memoryDir, filePath);
953
+ const relativePath = path6.relative(memoryDir, filePath);
3378
954
  const action = isUpdate ? "Updated" : "Created";
3379
955
  console.log(`${action}: ${relativePath}`);
3380
956
  console.log(` in ${formatPath(memoryDir)}`);
@@ -3398,10 +974,10 @@ async function upsert(file, content, options) {
3398
974
  }
3399
975
  }
3400
976
  function resolveFilePath(file, memoryDir) {
3401
- if (path7.isAbsolute(file)) {
977
+ if (path6.isAbsolute(file)) {
3402
978
  return file;
3403
979
  }
3404
- return path7.join(memoryDir, file);
980
+ return path6.join(memoryDir, file);
3405
981
  }
3406
982
  async function readStdin() {
3407
983
  const chunks = [];
@@ -3414,7 +990,7 @@ async function readStdin() {
3414
990
 
3415
991
  // src/cli/commands/mcp.ts
3416
992
  import * as fs7 from "fs/promises";
3417
- import * as path8 from "path";
993
+ import * as path7 from "path";
3418
994
 
3419
995
  // src/server/mcp.ts
3420
996
  import * as readline from "readline";
@@ -4046,9 +1622,10 @@ async function mcp(options) {
4046
1622
  if (includesGlobal && !await isInitialized(globalDir)) {
4047
1623
  await ensureGlobalInitialized(globalDir);
4048
1624
  }
1625
+ const expandedDirs = await resolveLinkedDirsForMcp(directories);
4049
1626
  const instances = [];
4050
1627
  const minimemInstances = [];
4051
- for (const memoryDir of directories) {
1628
+ for (const memoryDir of expandedDirs) {
4052
1629
  const isGlobal = memoryDir === globalDir;
4053
1630
  if (!await isInitialized(memoryDir)) {
4054
1631
  warn(`${formatPath(memoryDir)} is not initialized, skipping.`);
@@ -4111,8 +1688,8 @@ async function mcp(options) {
4111
1688
  async function ensureGlobalInitialized(globalDir) {
4112
1689
  console.error(`Auto-initializing global memory directory (~/.minimem)...`);
4113
1690
  await fs7.mkdir(globalDir, { recursive: true });
4114
- await fs7.mkdir(path8.join(globalDir, "memory"), { recursive: true });
4115
- const memoryFilePath = path8.join(globalDir, "MEMORY.md");
1691
+ await fs7.mkdir(path7.join(globalDir, "memory"), { recursive: true });
1692
+ const memoryFilePath = path7.join(globalDir, "MEMORY.md");
4116
1693
  try {
4117
1694
  await fs7.access(memoryFilePath);
4118
1695
  } catch {
@@ -4128,12 +1705,38 @@ Notes stored here are available across all projects.
4128
1705
  await fs7.writeFile(memoryFilePath, template, "utf-8");
4129
1706
  }
4130
1707
  const config2 = getInitConfig();
4131
- const configPath = path8.join(globalDir, "config.json");
1708
+ const configPath = path7.join(globalDir, "config.json");
4132
1709
  await fs7.writeFile(configPath, JSON.stringify(config2, null, 2), "utf-8");
4133
- const gitignorePath = path8.join(globalDir, ".gitignore");
1710
+ const gitignorePath = path7.join(globalDir, ".gitignore");
4134
1711
  await fs7.writeFile(gitignorePath, "index.db\nindex.db-*\n", "utf-8");
4135
1712
  console.error(` Created ~/.minimem with default configuration.`);
4136
1713
  }
1714
+ async function resolveLinkedDirsForMcp(dirs) {
1715
+ try {
1716
+ const manifest = await loadManifest();
1717
+ if (Object.keys(manifest.stores).length === 0) return dirs;
1718
+ const allDirs = new Set(dirs);
1719
+ for (const dir of dirs) {
1720
+ const storeName = resolveStoreName(manifest, dir);
1721
+ if (!storeName) continue;
1722
+ const linkedNames = await getLinkedStoreNames(manifest, storeName);
1723
+ for (const linkedName of linkedNames) {
1724
+ const linkedDef = resolveStore(manifest, linkedName);
1725
+ if (!linkedDef) continue;
1726
+ try {
1727
+ const result = await materializeStore(linkedName, linkedDef);
1728
+ if (result) {
1729
+ allDirs.add(result.path);
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ }
1734
+ }
1735
+ return [...allDirs];
1736
+ } catch {
1737
+ return dirs;
1738
+ }
1739
+ }
4137
1740
 
4138
1741
  // src/cli/commands/config.ts
4139
1742
  async function config(options) {
@@ -4326,21 +1929,21 @@ function unsetConfigValue(config2, keyPath) {
4326
1929
 
4327
1930
  // src/cli/commands/sync-init.ts
4328
1931
  import fs11 from "fs/promises";
4329
- import path12 from "path";
1932
+ import path11 from "path";
4330
1933
 
4331
1934
  // src/cli/sync/central.ts
4332
1935
  import fs10 from "fs/promises";
4333
- import path11 from "path";
1936
+ import path10 from "path";
4334
1937
  import { execSync } from "child_process";
4335
1938
 
4336
1939
  // src/cli/sync/registry.ts
4337
1940
  import fs8 from "fs/promises";
4338
- import path9 from "path";
1941
+ import path8 from "path";
4339
1942
  import os5 from "os";
4340
- import crypto3 from "crypto";
1943
+ import crypto2 from "crypto";
4341
1944
  var REGISTRY_FILENAME = ".minimem-registry.json";
4342
1945
  function getRegistryPath(centralRepo) {
4343
- return path9.join(centralRepo, REGISTRY_FILENAME);
1946
+ return path8.join(centralRepo, REGISTRY_FILENAME);
4344
1947
  }
4345
1948
  function createEmptyRegistry() {
4346
1949
  return {
@@ -4363,23 +1966,23 @@ async function readRegistry(centralRepo) {
4363
1966
  }
4364
1967
  async function writeRegistry(centralRepo, registry) {
4365
1968
  const registryPath = getRegistryPath(centralRepo);
4366
- const tempPath = `${registryPath}.${crypto3.randomBytes(4).toString("hex")}.tmp`;
1969
+ const tempPath = `${registryPath}.${crypto2.randomBytes(4).toString("hex")}.tmp`;
4367
1970
  registry.version = registry.version || 1;
4368
1971
  await fs8.writeFile(tempPath, JSON.stringify(registry, null, 2), "utf-8");
4369
1972
  await fs8.rename(tempPath, registryPath);
4370
1973
  }
4371
1974
  function normalizePath(filePath) {
4372
1975
  if (filePath.startsWith("~/")) {
4373
- return path9.resolve(os5.homedir(), filePath.slice(2));
1976
+ return path8.resolve(os5.homedir(), filePath.slice(2));
4374
1977
  }
4375
1978
  if (filePath === "~") {
4376
1979
  return os5.homedir();
4377
1980
  }
4378
- return path9.resolve(filePath);
1981
+ return path8.resolve(filePath);
4379
1982
  }
4380
1983
  function compressPath(filePath) {
4381
1984
  const home = os5.homedir();
4382
- const resolved = path9.resolve(filePath);
1985
+ const resolved = path8.resolve(filePath);
4383
1986
  if (resolved.startsWith(home)) {
4384
1987
  return "~" + resolved.slice(home.length);
4385
1988
  }
@@ -4445,20 +2048,20 @@ function updateLastSync(registry, centralPath, machineId) {
4445
2048
 
4446
2049
  // src/cli/sync/detection.ts
4447
2050
  import fs9 from "fs/promises";
4448
- import path10 from "path";
2051
+ import path9 from "path";
4449
2052
  async function isInsideGitRepo(dir) {
4450
- let current = path10.resolve(dir);
4451
- const root = path10.parse(current).root;
2053
+ let current = path9.resolve(dir);
2054
+ const root = path9.parse(current).root;
4452
2055
  while (current !== root) {
4453
2056
  try {
4454
- const gitPath = path10.join(current, ".git");
2057
+ const gitPath = path9.join(current, ".git");
4455
2058
  const stat = await fs9.stat(gitPath);
4456
2059
  if (stat.isDirectory() || stat.isFile()) {
4457
2060
  return true;
4458
2061
  }
4459
2062
  } catch {
4460
2063
  }
4461
- current = path10.dirname(current);
2064
+ current = path9.dirname(current);
4462
2065
  }
4463
2066
  return false;
4464
2067
  }
@@ -4534,7 +2137,7 @@ minimem sync init --path <directory-name>/
4534
2137
  `;
4535
2138
  async function isWritable(dirPath) {
4536
2139
  try {
4537
- const testFile = path11.join(dirPath, `.minimem-write-test-${Date.now()}`);
2140
+ const testFile = path10.join(dirPath, `.minimem-write-test-${Date.now()}`);
4538
2141
  await fs10.writeFile(testFile, "test");
4539
2142
  await fs10.unlink(testFile);
4540
2143
  return true;
@@ -4555,7 +2158,7 @@ async function initGitRepo(dirPath) {
4555
2158
  }
4556
2159
  async function initCentralRepo(repoPath) {
4557
2160
  const expandedPath = expandPath(repoPath);
4558
- const resolvedPath = path11.resolve(expandedPath);
2161
+ const resolvedPath = path10.resolve(expandedPath);
4559
2162
  if (!isGitAvailable()) {
4560
2163
  return {
4561
2164
  success: false,
@@ -4606,7 +2209,7 @@ async function initCentralRepo(repoPath) {
4606
2209
  };
4607
2210
  }
4608
2211
  }
4609
- const gitignorePath = path11.join(resolvedPath, ".gitignore");
2212
+ const gitignorePath = path10.join(resolvedPath, ".gitignore");
4610
2213
  try {
4611
2214
  await fs10.access(gitignorePath);
4612
2215
  } catch {
@@ -4618,7 +2221,7 @@ async function initCentralRepo(repoPath) {
4618
2221
  } catch {
4619
2222
  await writeRegistry(resolvedPath, createEmptyRegistry());
4620
2223
  }
4621
- const readmePath = path11.join(resolvedPath, "README.md");
2224
+ const readmePath = path10.join(resolvedPath, "README.md");
4622
2225
  try {
4623
2226
  await fs10.access(readmePath);
4624
2227
  } catch {
@@ -4636,7 +2239,7 @@ async function initCentralRepo(repoPath) {
4636
2239
  }
4637
2240
  async function validateCentralRepo(repoPath) {
4638
2241
  const expandedPath = expandPath(repoPath);
4639
- const resolvedPath = path11.resolve(expandedPath);
2242
+ const resolvedPath = path10.resolve(expandedPath);
4640
2243
  const warnings = [];
4641
2244
  const errors = [];
4642
2245
  try {
@@ -4663,7 +2266,7 @@ async function validateCentralRepo(repoPath) {
4663
2266
  } catch {
4664
2267
  warnings.push("Registry file is missing - will be created on first sync");
4665
2268
  }
4666
- const gitignorePath = path11.join(resolvedPath, ".gitignore");
2269
+ const gitignorePath = path10.join(resolvedPath, ".gitignore");
4667
2270
  try {
4668
2271
  const gitignore = await fs10.readFile(gitignorePath, "utf-8");
4669
2272
  if (!gitignore.includes("*.db")) {
@@ -4684,7 +2287,7 @@ async function validateCentralRepo(repoPath) {
4684
2287
  async function getCentralRepoPath() {
4685
2288
  const globalConfig = await loadXdgConfig();
4686
2289
  if (globalConfig.centralRepo) {
4687
- return path11.resolve(expandPath(globalConfig.centralRepo));
2290
+ return path10.resolve(expandPath(globalConfig.centralRepo));
4688
2291
  }
4689
2292
  return void 0;
4690
2293
  }
@@ -4790,7 +2393,7 @@ async function syncInit(options) {
4790
2393
  });
4791
2394
  await writeRegistry(centralRepo, updatedRegistry);
4792
2395
  console.log(" Registered mapping in central repository");
4793
- const centralDir = path12.join(centralRepo, centralPath);
2396
+ const centralDir = path11.join(centralRepo, centralPath);
4794
2397
  try {
4795
2398
  await fs11.mkdir(centralDir, { recursive: true });
4796
2399
  } catch {
@@ -4865,23 +2468,23 @@ async function syncRemove(options) {
4865
2468
  console.log(`Removed mapping from central registry`);
4866
2469
  console.log();
4867
2470
  console.log("Note: Files in the central repository were NOT deleted.");
4868
- console.log(`They remain at: ${formatPath(path12.join(centralRepo, centralPath))}`);
2471
+ console.log(`They remain at: ${formatPath(path11.join(centralRepo, centralPath))}`);
4869
2472
  }
4870
2473
 
4871
2474
  // src/cli/sync/operations.ts
4872
2475
  import fs15 from "fs/promises";
4873
- import path16 from "path";
4874
- import crypto5 from "crypto";
2476
+ import path15 from "path";
2477
+ import crypto4 from "crypto";
4875
2478
 
4876
2479
  // src/cli/sync/state.ts
4877
2480
  import fs12 from "fs/promises";
4878
- import path13 from "path";
4879
- import crypto4 from "crypto";
2481
+ import path12 from "path";
2482
+ import crypto3 from "crypto";
4880
2483
  import { minimatch } from "minimatch";
4881
2484
  var STATE_FILENAME = "sync-state.json";
4882
2485
  function getSyncStatePath(dir) {
4883
2486
  const subdir = resolveConfigSubdir(dir);
4884
- return path13.join(dir, subdir, STATE_FILENAME);
2487
+ return path12.join(dir, subdir, STATE_FILENAME);
4885
2488
  }
4886
2489
  function createEmptySyncState(centralPath) {
4887
2490
  return {
@@ -4915,8 +2518,8 @@ async function loadSyncState(dir, centralPath) {
4915
2518
  }
4916
2519
  async function saveSyncState(dir, state) {
4917
2520
  const statePath = getSyncStatePath(dir);
4918
- const stateDir = path13.dirname(statePath);
4919
- const tempPath = `${statePath}.${crypto4.randomBytes(4).toString("hex")}.tmp`;
2521
+ const stateDir = path12.dirname(statePath);
2522
+ const tempPath = `${statePath}.${crypto3.randomBytes(4).toString("hex")}.tmp`;
4920
2523
  await fs12.mkdir(stateDir, { recursive: true });
4921
2524
  state.version = state.version || 2;
4922
2525
  await fs12.writeFile(tempPath, JSON.stringify(state, null, 2), "utf-8");
@@ -4924,19 +2527,19 @@ async function saveSyncState(dir, state) {
4924
2527
  }
4925
2528
  async function computeFileHash(filePath) {
4926
2529
  const content = await fs12.readFile(filePath);
4927
- return crypto4.createHash("sha256").update(content).digest("hex");
2530
+ return crypto3.createHash("sha256").update(content).digest("hex");
4928
2531
  }
4929
2532
  async function listSyncableFiles(dir, include, exclude) {
4930
2533
  const files = [];
4931
- async function walkDir2(currentDir, relativePath = "") {
2534
+ async function walkDir(currentDir, relativePath = "") {
4932
2535
  const entries = await fs12.readdir(currentDir, { withFileTypes: true });
4933
2536
  for (const entry of entries) {
4934
- const entryPath = path13.join(currentDir, entry.name);
2537
+ const entryPath = path12.join(currentDir, entry.name);
4935
2538
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
4936
2539
  if (entry.name === ".minimem" || entry.name === ".swarm") continue;
4937
2540
  if (entry.name === "index.db" || entry.name.startsWith("index.db-")) continue;
4938
2541
  if (entry.isDirectory()) {
4939
- await walkDir2(entryPath, relPath);
2542
+ await walkDir(entryPath, relPath);
4940
2543
  } else if (entry.isFile()) {
4941
2544
  const matchesInclude = include.some(
4942
2545
  (pattern) => minimatch(relPath, pattern)
@@ -4951,7 +2554,7 @@ async function listSyncableFiles(dir, include, exclude) {
4951
2554
  }
4952
2555
  }
4953
2556
  try {
4954
- await walkDir2(dir);
2557
+ await walkDir(dir);
4955
2558
  } catch (error) {
4956
2559
  if (error.code !== "ENOENT") {
4957
2560
  throw error;
@@ -4990,16 +2593,16 @@ function getFileSyncStatus(localHash, remoteHash) {
4990
2593
 
4991
2594
  // src/cli/commands/conflicts.ts
4992
2595
  import fs14 from "fs/promises";
4993
- import path15 from "path";
2596
+ import path14 from "path";
4994
2597
  import { spawn } from "child_process";
4995
2598
 
4996
2599
  // src/cli/sync/conflicts.ts
4997
2600
  import fs13 from "fs/promises";
4998
- import path14 from "path";
2601
+ import path13 from "path";
4999
2602
  var CONFLICTS_DIR = "conflicts";
5000
2603
  function getConflictsDir(memoryDir) {
5001
2604
  const subdir = resolveConfigSubdir(memoryDir);
5002
- return path14.join(memoryDir, subdir, CONFLICTS_DIR);
2605
+ return path13.join(memoryDir, subdir, CONFLICTS_DIR);
5003
2606
  }
5004
2607
  async function listQuarantinedConflicts(memoryDir) {
5005
2608
  const conflictsDir = getConflictsDir(memoryDir);
@@ -5008,7 +2611,7 @@ async function listQuarantinedConflicts(memoryDir) {
5008
2611
  const entries = await fs13.readdir(conflictsDir, { withFileTypes: true });
5009
2612
  for (const entry of entries) {
5010
2613
  if (entry.isDirectory()) {
5011
- const dirPath = path14.join(conflictsDir, entry.name);
2614
+ const dirPath = path13.join(conflictsDir, entry.name);
5012
2615
  const files = await fs13.readdir(dirPath);
5013
2616
  const uniqueFiles = /* @__PURE__ */ new Set();
5014
2617
  for (const file of files) {
@@ -5034,7 +2637,7 @@ async function detectChanges(memoryDir) {
5034
2637
  if (!syncConfig.path) {
5035
2638
  throw new Error("Directory is not configured for sync");
5036
2639
  }
5037
- const remotePath = path14.join(centralRepo, syncConfig.path);
2640
+ const remotePath = path13.join(centralRepo, syncConfig.path);
5038
2641
  const [localFiles, remoteFiles] = await Promise.all([
5039
2642
  listSyncableFiles(memoryDir, syncConfig.include, syncConfig.exclude),
5040
2643
  listSyncableFiles(remotePath, syncConfig.include, syncConfig.exclude)
@@ -5049,8 +2652,8 @@ async function detectChanges(memoryDir) {
5049
2652
  localModified: 0
5050
2653
  };
5051
2654
  for (const file of allFiles) {
5052
- const localPath = path14.join(memoryDir, file);
5053
- const remoteFilePath = path14.join(remotePath, file);
2655
+ const localPath = path13.join(memoryDir, file);
2656
+ const remoteFilePath = path13.join(remotePath, file);
5054
2657
  const [localInfo, remoteInfo] = await Promise.all([
5055
2658
  getFileHashInfo(localPath),
5056
2659
  getFileHashInfo(remoteFilePath)
@@ -5130,7 +2733,7 @@ async function resolveCommand(timestamp, options) {
5130
2733
  if (!await isInitialized(memoryDir)) {
5131
2734
  exitWithError(`${formatPath(memoryDir)} is not initialized.`);
5132
2735
  }
5133
- const conflictDir = path15.join(getConflictsDir(memoryDir), timestamp);
2736
+ const conflictDir = path14.join(getConflictsDir(memoryDir), timestamp);
5134
2737
  try {
5135
2738
  await fs14.access(conflictDir);
5136
2739
  } catch {
@@ -5150,7 +2753,7 @@ async function resolveCommand(timestamp, options) {
5150
2753
  fileGroups.set(baseName, {});
5151
2754
  }
5152
2755
  const group = fileGroups.get(baseName);
5153
- group[type] = path15.join(conflictDir, file);
2756
+ group[type] = path14.join(conflictDir, file);
5154
2757
  }
5155
2758
  }
5156
2759
  if (fileGroups.size === 0) {
@@ -5237,7 +2840,7 @@ async function cleanupCommand(options) {
5237
2840
  console.log(` Skipping invalid timestamp: ${entry}`);
5238
2841
  continue;
5239
2842
  }
5240
- const entryPath = path15.join(conflictsDir, entry);
2843
+ const entryPath = path14.join(conflictsDir, entry);
5241
2844
  if (entryDate < cutoffDate) {
5242
2845
  if (options.dryRun) {
5243
2846
  console.log(` Would remove: ${entry}`);
@@ -5266,12 +2869,12 @@ var SYNC_LOG_FILE = "sync.log";
5266
2869
  var MAX_LOG_ENTRIES = 1e3;
5267
2870
  function getSyncLogPath(memoryDir) {
5268
2871
  const subdir = resolveConfigSubdir(memoryDir);
5269
- return path15.join(memoryDir, subdir, SYNC_LOG_FILE);
2872
+ return path14.join(memoryDir, subdir, SYNC_LOG_FILE);
5270
2873
  }
5271
2874
  async function appendSyncLog(memoryDir, entry) {
5272
2875
  const logPath = getSyncLogPath(memoryDir);
5273
2876
  try {
5274
- await fs14.mkdir(path15.dirname(logPath), { recursive: true });
2877
+ await fs14.mkdir(path14.dirname(logPath), { recursive: true });
5275
2878
  let entries = [];
5276
2879
  try {
5277
2880
  const content2 = await fs14.readFile(logPath, "utf-8");
@@ -5341,13 +2944,13 @@ async function logCommand(options) {
5341
2944
  }
5342
2945
 
5343
2946
  // src/cli/sync/operations.ts
5344
- async function ensureDir2(dirPath) {
2947
+ async function ensureDir(dirPath) {
5345
2948
  await fs15.mkdir(dirPath, { recursive: true });
5346
2949
  }
5347
2950
  async function copyFileAtomic(src, dest) {
5348
- const destDir = path16.dirname(dest);
5349
- await ensureDir2(destDir);
5350
- const tempDest = `${dest}.${crypto5.randomBytes(4).toString("hex")}.tmp`;
2951
+ const destDir = path15.dirname(dest);
2952
+ await ensureDir(destDir);
2953
+ const tempDest = `${dest}.${crypto4.randomBytes(4).toString("hex")}.tmp`;
5351
2954
  try {
5352
2955
  await fs15.copyFile(src, tempDest);
5353
2956
  await fs15.rename(tempDest, dest);
@@ -5379,7 +2982,7 @@ async function push(memoryDir, options = {}) {
5379
2982
  result.errors.push("No central repository configured");
5380
2983
  return result;
5381
2984
  }
5382
- const remotePath = path16.join(centralRepo, syncConfig.path);
2985
+ const remotePath = path15.join(centralRepo, syncConfig.path);
5383
2986
  const state = await loadSyncState(memoryDir, syncConfig.path);
5384
2987
  const localFiles = await listSyncableFiles(
5385
2988
  memoryDir,
@@ -5393,8 +2996,8 @@ async function push(memoryDir, options = {}) {
5393
2996
  );
5394
2997
  const allFiles = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles]);
5395
2998
  for (const file of allFiles) {
5396
- const localPath = path16.join(memoryDir, file);
5397
- const remoteFilePath = path16.join(remotePath, file);
2999
+ const localPath = path15.join(memoryDir, file);
3000
+ const remoteFilePath = path15.join(remotePath, file);
5398
3001
  try {
5399
3002
  const [localInfo, remoteInfo] = await Promise.all([
5400
3003
  getFileHashInfo(localPath),
@@ -5473,7 +3076,7 @@ async function pull(memoryDir, options = {}) {
5473
3076
  result.errors.push("No central repository configured");
5474
3077
  return result;
5475
3078
  }
5476
- const remotePath = path16.join(centralRepo, syncConfig.path);
3079
+ const remotePath = path15.join(centralRepo, syncConfig.path);
5477
3080
  const state = await loadSyncState(memoryDir, syncConfig.path);
5478
3081
  const localFiles = await listSyncableFiles(
5479
3082
  memoryDir,
@@ -5487,8 +3090,8 @@ async function pull(memoryDir, options = {}) {
5487
3090
  );
5488
3091
  const allFiles = /* @__PURE__ */ new Set([...localFiles, ...remoteFiles]);
5489
3092
  for (const file of allFiles) {
5490
- const localPath = path16.join(memoryDir, file);
5491
- const remoteFilePath = path16.join(remotePath, file);
3093
+ const localPath = path15.join(memoryDir, file);
3094
+ const remoteFilePath = path15.join(remotePath, file);
5492
3095
  try {
5493
3096
  const [localInfo, remoteInfo] = await Promise.all([
5494
3097
  getFileHashInfo(localPath),
@@ -5730,12 +3333,12 @@ import fs18 from "fs/promises";
5730
3333
 
5731
3334
  // src/cli/sync/daemon.ts
5732
3335
  import fs17 from "fs/promises";
5733
- import path19 from "path";
3336
+ import path18 from "path";
5734
3337
  import os6 from "os";
5735
3338
 
5736
3339
  // src/cli/sync/watcher.ts
5737
- import chokidar2 from "chokidar";
5738
- import path17 from "path";
3340
+ import chokidar from "chokidar";
3341
+ import path16 from "path";
5739
3342
  import { EventEmitter } from "events";
5740
3343
  var DEFAULT_DEBOUNCE_MS = 2e3;
5741
3344
  var DEFAULT_POLL_INTERVAL = 1e3;
@@ -5765,7 +3368,7 @@ function createFileWatcher(memoryDir, options = {}) {
5765
3368
  debounceTimer = setTimeout(flushChanges, debounceMs);
5766
3369
  };
5767
3370
  const handleEvent = (event, filePath) => {
5768
- const relativePath = path17.relative(memoryDir, filePath);
3371
+ const relativePath = path16.relative(memoryDir, filePath);
5769
3372
  if (relativePath.startsWith(".minimem") || relativePath.startsWith(".swarm")) {
5770
3373
  return;
5771
3374
  }
@@ -5799,9 +3402,9 @@ function createFileWatcher(memoryDir, options = {}) {
5799
3402
  pendingChanges.set(relativePath, { event, file: relativePath });
5800
3403
  scheduleFlush();
5801
3404
  };
5802
- const watchPaths = include.map((pattern) => path17.join(memoryDir, pattern));
5803
- watcher = chokidar2.watch(watchPaths, {
5804
- ignored: exclude.map((pattern) => path17.join(memoryDir, pattern)),
3405
+ const watchPaths = include.map((pattern) => path16.join(memoryDir, pattern));
3406
+ watcher = chokidar.watch(watchPaths, {
3407
+ ignored: exclude.map((pattern) => path16.join(memoryDir, pattern)),
5805
3408
  persistent: true,
5806
3409
  ignoreInitial: true,
5807
3410
  usePolling: options.usePolling ?? false,
@@ -5842,7 +3445,7 @@ function createFileWatcher(memoryDir, options = {}) {
5842
3445
 
5843
3446
  // src/cli/sync/validation.ts
5844
3447
  import fs16 from "fs/promises";
5845
- import path18 from "path";
3448
+ import path17 from "path";
5846
3449
  var STALE_THRESHOLD_DAYS = 30;
5847
3450
  async function validateRegistry() {
5848
3451
  const result = {
@@ -5944,7 +3547,7 @@ async function validateRegistry() {
5944
3547
  }
5945
3548
  function expandPath3(p) {
5946
3549
  if (p.startsWith("~/")) {
5947
- return path18.join(process.env.HOME || "", p.slice(2));
3550
+ return path17.join(process.env.HOME || "", p.slice(2));
5948
3551
  }
5949
3552
  return p;
5950
3553
  }
@@ -5984,13 +3587,13 @@ var DAEMON_LOG_FILE = "daemon.log";
5984
3587
  var PID_FILE = "daemon.pid";
5985
3588
  var MAX_LOG_SIZE = 1024 * 1024;
5986
3589
  function getDaemonDir() {
5987
- return path19.join(os6.homedir(), ".minimem");
3590
+ return path18.join(os6.homedir(), ".minimem");
5988
3591
  }
5989
3592
  function getPidFilePath() {
5990
- return path19.join(getDaemonDir(), PID_FILE);
3593
+ return path18.join(getDaemonDir(), PID_FILE);
5991
3594
  }
5992
3595
  function getDaemonLogPath() {
5993
- return path19.join(getDaemonDir(), DAEMON_LOG_FILE);
3596
+ return path18.join(getDaemonDir(), DAEMON_LOG_FILE);
5994
3597
  }
5995
3598
  async function isDaemonRunning() {
5996
3599
  const pidFile = getPidFilePath();
@@ -6031,7 +3634,7 @@ async function writeLog(message, level = "info") {
6031
3634
  const line = `[${timestamp}] [${level.toUpperCase()}] ${message}
6032
3635
  `;
6033
3636
  try {
6034
- await fs17.mkdir(path19.dirname(logPath), { recursive: true });
3637
+ await fs17.mkdir(path18.dirname(logPath), { recursive: true });
6035
3638
  try {
6036
3639
  const stats = await fs17.stat(logPath);
6037
3640
  if (stats.size > MAX_LOG_SIZE) {
@@ -6333,6 +3936,129 @@ async function daemonLogsCommand(options) {
6333
3936
  }
6334
3937
  }
6335
3938
 
3939
+ // src/cli/commands/store.ts
3940
+ import path19 from "path";
3941
+ async function storeList(options) {
3942
+ const manifest = await loadManifest();
3943
+ const storeNames = Object.keys(manifest.stores);
3944
+ if (storeNames.length === 0) {
3945
+ console.log("No stores registered.");
3946
+ console.log(`
3947
+ Register a store with: minimem store:add <name> <path>`);
3948
+ return;
3949
+ }
3950
+ if (options.json) {
3951
+ const result = {};
3952
+ for (const name of storeNames) {
3953
+ const def = manifest.stores[name];
3954
+ const links = await getLinkedStoreNames(manifest, name);
3955
+ result[name] = { path: def.path, remote: def.remote, links };
3956
+ }
3957
+ console.log(JSON.stringify(result, null, 2));
3958
+ return;
3959
+ }
3960
+ console.log(`Registered stores (${getManifestPath()}):
3961
+ `);
3962
+ for (const name of storeNames) {
3963
+ const def = manifest.stores[name];
3964
+ const links = await getLinkedStoreNames(manifest, name);
3965
+ console.log(` ${name}`);
3966
+ console.log(` path: ${formatPath(def.path)}`);
3967
+ if (def.remote) {
3968
+ console.log(` remote: ${def.remote}`);
3969
+ }
3970
+ if (def.description) {
3971
+ console.log(` description: ${def.description}`);
3972
+ }
3973
+ if (links.length > 0) {
3974
+ console.log(` links: ${links.join(", ")}`);
3975
+ }
3976
+ console.log();
3977
+ }
3978
+ }
3979
+ async function storeAdd(name, storePath, options) {
3980
+ const manifest = await loadManifest();
3981
+ const absPath = path19.resolve(storePath);
3982
+ if (manifest.stores[name]) {
3983
+ exitWithError(
3984
+ `Store "${name}" already exists.`,
3985
+ `Use a different name or remove it first with: minimem store:remove ${name}`
3986
+ );
3987
+ }
3988
+ const def = {
3989
+ path: absPath,
3990
+ remote: options.remote,
3991
+ description: options.description
3992
+ };
3993
+ manifest.stores[name] = def;
3994
+ await saveManifest(manifest);
3995
+ console.log(`Registered store "${name}" at ${formatPath(absPath)}`);
3996
+ if (options.remote) {
3997
+ console.log(` remote: ${options.remote}`);
3998
+ }
3999
+ const result = await materializeStore(name, def);
4000
+ if (result) {
4001
+ if (result.strategy === "remote") {
4002
+ console.log(` Cloned remote into ${formatPath(result.path)}`);
4003
+ }
4004
+ await result.cleanup();
4005
+ } else if (options.remote) {
4006
+ console.log(` Warning: could not materialize store (remote clone failed)`);
4007
+ }
4008
+ }
4009
+ async function storeRemove(name) {
4010
+ const manifest = await loadManifest();
4011
+ if (!manifest.stores[name]) {
4012
+ exitWithError(`Store "${name}" not found.`);
4013
+ }
4014
+ delete manifest.stores[name];
4015
+ await saveManifest(manifest);
4016
+ console.log(`Removed store "${name}"`);
4017
+ }
4018
+ async function storeLink(storeName, targetName) {
4019
+ const manifest = await loadManifest();
4020
+ const storeDef = manifest.stores[storeName];
4021
+ if (!storeDef) {
4022
+ exitWithError(
4023
+ `Store "${storeName}" not found.`,
4024
+ `Register it first with: minimem store:add ${storeName} <path>`
4025
+ );
4026
+ }
4027
+ if (!manifest.stores[targetName]) {
4028
+ exitWithError(
4029
+ `Target store "${targetName}" not found.`,
4030
+ `Register it first with: minimem store:add ${targetName} <path>`
4031
+ );
4032
+ }
4033
+ if (storeName === targetName) {
4034
+ exitWithError("Cannot link a store to itself.");
4035
+ }
4036
+ const links = await loadStoreLinks(storeDef.path);
4037
+ if (links.links.includes(targetName)) {
4038
+ console.log(`Store "${storeName}" already links to "${targetName}"`);
4039
+ return;
4040
+ }
4041
+ links.links.push(targetName);
4042
+ await saveStoreLinks(storeDef.path, links);
4043
+ console.log(`Linked "${storeName}" \u2192 "${targetName}"`);
4044
+ console.log(` When searching "${storeName}", "${targetName}" will be included automatically.`);
4045
+ }
4046
+ async function storeUnlink(storeName, targetName) {
4047
+ const manifest = await loadManifest();
4048
+ const storeDef = manifest.stores[storeName];
4049
+ if (!storeDef) {
4050
+ exitWithError(`Store "${storeName}" not found.`);
4051
+ }
4052
+ const links = await loadStoreLinks(storeDef.path);
4053
+ if (!links.links.includes(targetName)) {
4054
+ console.log(`Store "${storeName}" does not link to "${targetName}"`);
4055
+ return;
4056
+ }
4057
+ links.links = links.links.filter((l) => l !== targetName);
4058
+ await saveStoreLinks(storeDef.path, links);
4059
+ console.log(`Unlinked "${storeName}" from "${targetName}"`);
4060
+ }
4061
+
6336
4062
  // src/cli/version.ts
6337
4063
  import { readFileSync } from "fs";
6338
4064
  import { dirname as dirname3, join as join4 } from "path";
@@ -6353,7 +4079,7 @@ var VERSION = getPackageVersion();
6353
4079
  // src/cli/index.ts
6354
4080
  program.name("minimem").description("File-based memory system with vector search for AI agents").version(VERSION);
6355
4081
  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);
6356
- 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);
4082
+ 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").option("--no-links", "Disable linked store resolution").action(search);
6357
4083
  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);
6358
4084
  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);
6359
4085
  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);
@@ -6391,5 +4117,10 @@ program.command("sync:validate").description("Validate registry for collisions a
6391
4117
  exitWithError(message);
6392
4118
  }
6393
4119
  });
4120
+ program.command("store:list").description("List all registered stores and their links").option("--json", "Output as JSON").action(storeList);
4121
+ program.command("store:add <name> <path>").description("Register a store in the global manifest").option("-r, --remote <url>", "Git remote URL for remote materialization").option("--description <text>", "Human-readable description").action(storeAdd);
4122
+ program.command("store:remove <name>").description("Remove a store from the global manifest").action(storeRemove);
4123
+ program.command("store:link <store> <target>").description("Link a store to another (searches include linked stores)").action(storeLink);
4124
+ program.command("store:unlink <store> <target>").description("Remove a link between stores").action(storeUnlink);
6394
4125
  program.parse();
6395
4126
  //# sourceMappingURL=index.js.map