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