local-agent-memory 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/reindex.js +15 -6
- package/dist/cli/commands/sync.js +3 -3
- package/dist/core/search.js +63 -19
- package/dist/db/project.d.ts +5 -4
- package/dist/db/project.js +73 -23
- package/package.json +1 -1
|
@@ -22,12 +22,21 @@ async function reindexProject() {
|
|
|
22
22
|
// Check if db exists, then delete to force complete reindex
|
|
23
23
|
if (node_fs_1.default.existsSync(localDbPath)) {
|
|
24
24
|
spinner.text = "Deleting corrupted or existing local index...";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
node_fs_1.default.
|
|
29
|
-
|
|
30
|
-
node_fs_1.default.
|
|
25
|
+
try {
|
|
26
|
+
node_fs_1.default.unlinkSync(localDbPath);
|
|
27
|
+
// Also delete the WAL and SHM files if they exist
|
|
28
|
+
if (node_fs_1.default.existsSync(`${localDbPath}-wal`))
|
|
29
|
+
node_fs_1.default.unlinkSync(`${localDbPath}-wal`);
|
|
30
|
+
if (node_fs_1.default.existsSync(`${localDbPath}-shm`))
|
|
31
|
+
node_fs_1.default.unlinkSync(`${localDbPath}-shm`);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err.code === "EBUSY" || err.code === "EPERM") {
|
|
35
|
+
spinner.fail("Cannot reindex: local.sqlite is currently open and locked by an active process (e.g. MCP server). Please stop the MCP process and try again.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
31
40
|
}
|
|
32
41
|
spinner.text = "Rebuilding index from scratch...";
|
|
33
42
|
const { newCount } = await (0, sync_1.doSyncProject)(cwd);
|
|
@@ -20,7 +20,7 @@ async function doSyncProject(cwd) {
|
|
|
20
20
|
const localDbPath = (0, project_1.getLocalDbPath)(cwd);
|
|
21
21
|
const db = (0, project_1.initLocalDb)(localDbPath);
|
|
22
22
|
const _asyncDb = new sync_1.AsyncDatabase(db);
|
|
23
|
-
const manifests = await (0, project_1.getAllManifests)(
|
|
23
|
+
const manifests = await (0, project_1.getAllManifests)(db);
|
|
24
24
|
const manifestMap = new Map(manifests.map((m) => [m.file_path, m]));
|
|
25
25
|
const configPath = node_path_1.default.join(memoryDir, "config.json");
|
|
26
26
|
let includePaths = [".memory"];
|
|
@@ -64,7 +64,7 @@ async function doSyncProject(cwd) {
|
|
|
64
64
|
if (!existing) {
|
|
65
65
|
// New file
|
|
66
66
|
const sha256 = await computeSha256(file);
|
|
67
|
-
await (0, project_1.upsertFileManifest)(
|
|
67
|
+
await (0, project_1.upsertFileManifest)(db, relativePath, mtime, sha256);
|
|
68
68
|
const content = await node_fs_1.default.promises.readFile(file, "utf8");
|
|
69
69
|
await (0, sync_1.syncFile)(relativePath, content, db);
|
|
70
70
|
newCount++;
|
|
@@ -73,7 +73,7 @@ async function doSyncProject(cwd) {
|
|
|
73
73
|
// Potentially modified, check hash
|
|
74
74
|
const sha256 = await computeSha256(file);
|
|
75
75
|
if (sha256 !== existing.sha256) {
|
|
76
|
-
await (0, project_1.upsertFileManifest)(
|
|
76
|
+
await (0, project_1.upsertFileManifest)(db, relativePath, mtime, sha256);
|
|
77
77
|
const content = await node_fs_1.default.promises.readFile(file, "utf8");
|
|
78
78
|
await (0, sync_1.syncFile)(relativePath, content, db);
|
|
79
79
|
modCount++;
|
package/dist/core/search.js
CHANGED
|
@@ -27,7 +27,7 @@ async function hybridSearch(query, limit, db, filter) {
|
|
|
27
27
|
const asyncDb = new sync_1.AsyncDatabase(db);
|
|
28
28
|
const RRF_K = 60;
|
|
29
29
|
// 1. FTS5 Search
|
|
30
|
-
const ftsResults = await new Promise((resolve
|
|
30
|
+
const ftsResults = await new Promise((resolve) => {
|
|
31
31
|
// Sanitize query to prevent FTS5 syntax errors
|
|
32
32
|
const ftsQuery = sanitizeFtsQuery(query);
|
|
33
33
|
if (!ftsQuery) {
|
|
@@ -35,27 +35,71 @@ async function hybridSearch(query, limit, db, filter) {
|
|
|
35
35
|
}
|
|
36
36
|
// using bm25() for scoring in FTS5
|
|
37
37
|
db.all(`SELECT rowid as id, rank as score FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100`, [ftsQuery], (err, rows) => {
|
|
38
|
-
if (err)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (err) {
|
|
39
|
+
resolve([]);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
resolve(rows || []);
|
|
43
|
+
}
|
|
42
44
|
});
|
|
43
45
|
});
|
|
44
|
-
// 2. Vector Search
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
// 2. Vector Search (with graceful fallback)
|
|
47
|
+
let vecResults = [];
|
|
48
|
+
try {
|
|
49
|
+
const queryEmbedding = await (0, embeddings_1.generateEmbedding)(query);
|
|
50
|
+
const vectorData = Buffer.from(new Float32Array(queryEmbedding).buffer);
|
|
51
|
+
vecResults = await new Promise((resolve) => {
|
|
52
|
+
// sqlite-vec distance function. distance is lower = better (closer)
|
|
53
|
+
// we want smallest distance first
|
|
54
|
+
db.all(`SELECT rowid as id, distance FROM chunks_vec
|
|
55
|
+
WHERE embedding MATCH ? AND k = 100
|
|
56
|
+
ORDER BY distance`, [vectorData], (err, rows) => {
|
|
57
|
+
if (!err && rows && rows.length > 0) {
|
|
58
|
+
return resolve(rows);
|
|
59
|
+
}
|
|
60
|
+
// Fallback: If chunks_vec is a regular table (no virtual distance/MATCH)
|
|
61
|
+
// or sqlite-vec is not loaded, compute cosine similarity in-memory using stored BLOBs.
|
|
62
|
+
db.all(`SELECT rowid as id, embedding FROM chunks_vec WHERE embedding IS NOT NULL`, [], (fallbackErr, fallbackRows) => {
|
|
63
|
+
if (fallbackErr || !fallbackRows || fallbackRows.length === 0) {
|
|
64
|
+
return resolve([]);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const queryFloats = new Float32Array(queryEmbedding);
|
|
68
|
+
const scored = [];
|
|
69
|
+
for (const row of fallbackRows) {
|
|
70
|
+
if (!row.embedding)
|
|
71
|
+
continue;
|
|
72
|
+
const buf = row.embedding;
|
|
73
|
+
const storedFloats = new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
|
|
74
|
+
if (storedFloats.length !== queryFloats.length)
|
|
75
|
+
continue;
|
|
76
|
+
let dotProduct = 0;
|
|
77
|
+
for (let i = 0; i < queryFloats.length; i++) {
|
|
78
|
+
const q = queryFloats[i];
|
|
79
|
+
const s = storedFloats[i];
|
|
80
|
+
if (q !== undefined && s !== undefined) {
|
|
81
|
+
dotProduct += q * s;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// MiniLM embeddings are normalized, so cosine_distance = 1 - dotProduct
|
|
85
|
+
scored.push({
|
|
86
|
+
id: row.id,
|
|
87
|
+
distance: 1 - dotProduct,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
scored.sort((a, b) => a.distance - b.distance);
|
|
91
|
+
resolve(scored.slice(0, 100));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
resolve([]);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
57
98
|
});
|
|
58
|
-
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
vecResults = [];
|
|
102
|
+
}
|
|
59
103
|
// 3. RRF Fusion
|
|
60
104
|
const rrfScores = new Map();
|
|
61
105
|
// Add FTS scores
|
package/dist/db/project.d.ts
CHANGED
|
@@ -4,9 +4,10 @@ export interface FileManifestRow {
|
|
|
4
4
|
mtime: number;
|
|
5
5
|
sha256: string;
|
|
6
6
|
}
|
|
7
|
+
export declare function getVecExtensionPath(): string | null;
|
|
7
8
|
export declare function getLocalDbPath(projectRoot: string): string;
|
|
8
9
|
export declare function initLocalDb(dbPath: string): sqlite3.Database;
|
|
9
|
-
export declare function upsertFileManifest(
|
|
10
|
-
export declare function getFileManifest(
|
|
11
|
-
export declare function getAllManifests(
|
|
12
|
-
export declare function removeFileManifest(
|
|
10
|
+
export declare function upsertFileManifest(dbOrPath: string | sqlite3.Database, file_path: string, mtime: number, sha256: string): Promise<void>;
|
|
11
|
+
export declare function getFileManifest(dbOrPath: string | sqlite3.Database, file_path: string): Promise<FileManifestRow | null>;
|
|
12
|
+
export declare function getAllManifests(dbOrPath: string | sqlite3.Database): Promise<FileManifestRow[]>;
|
|
13
|
+
export declare function removeFileManifest(dbOrPath: string | sqlite3.Database, file_path: string): Promise<void>;
|
package/dist/db/project.js
CHANGED
|
@@ -3,14 +3,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getVecExtensionPath = getVecExtensionPath;
|
|
6
7
|
exports.getLocalDbPath = getLocalDbPath;
|
|
7
8
|
exports.initLocalDb = initLocalDb;
|
|
8
9
|
exports.upsertFileManifest = upsertFileManifest;
|
|
9
10
|
exports.getFileManifest = getFileManifest;
|
|
10
11
|
exports.getAllManifests = getAllManifests;
|
|
11
12
|
exports.removeFileManifest = removeFileManifest;
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
16
|
const sqlite3_1 = __importDefault(require("sqlite3"));
|
|
17
|
+
function getVecExtensionPath() {
|
|
18
|
+
try {
|
|
19
|
+
const memDir = process.env.MEMORY_GLOBAL_DIR || node_path_1.default.join(node_os_1.default.homedir(), ".local_memory");
|
|
20
|
+
const binDir = node_path_1.default.join(memDir, "bin");
|
|
21
|
+
const extFile = process.platform === "win32"
|
|
22
|
+
? "vec0.dll"
|
|
23
|
+
: process.platform === "darwin"
|
|
24
|
+
? "vec0.dylib"
|
|
25
|
+
: "vec0.so";
|
|
26
|
+
const extPath = node_path_1.default.join(binDir, extFile);
|
|
27
|
+
return node_fs_1.default.existsSync(extPath) ? extPath : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
function getLocalDbPath(projectRoot) {
|
|
15
34
|
return node_path_1.default.join(projectRoot, ".memory", "local.sqlite");
|
|
16
35
|
}
|
|
@@ -44,30 +63,52 @@ function initLocalDb(dbPath) {
|
|
|
44
63
|
endLine INTEGER
|
|
45
64
|
)
|
|
46
65
|
`);
|
|
47
|
-
// Note: for real usage, chunks_fts and chunks_vec need FTS5 and vec0 extensions enabled.
|
|
48
|
-
// Standard sqlite3 node driver has FTS5, but vec0 must be loaded.
|
|
49
|
-
// Assuming they are available or using regular tables for E2E.
|
|
50
66
|
db.run(`
|
|
51
67
|
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
52
68
|
path, text, breadcrumbs, content='chunks', content_rowid='id'
|
|
53
69
|
)
|
|
54
70
|
`);
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
// Load sqlite-vec extension if downloaded, and create virtual table vec0.
|
|
72
|
+
// Falls back to regular table if extension is not present or fails to load.
|
|
73
|
+
const extPath = getVecExtensionPath();
|
|
74
|
+
if (extPath) {
|
|
75
|
+
db.loadExtension(extPath, (err) => {
|
|
76
|
+
if (!err) {
|
|
77
|
+
db.run(`
|
|
78
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(
|
|
79
|
+
embedding float[384]
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
db.run(`
|
|
85
|
+
CREATE TABLE IF NOT EXISTS chunks_vec (
|
|
86
|
+
rowid INTEGER,
|
|
87
|
+
embedding BLOB
|
|
88
|
+
)
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
db.run(`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS chunks_vec (
|
|
96
|
+
rowid INTEGER,
|
|
97
|
+
embedding BLOB
|
|
98
|
+
)
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
63
101
|
});
|
|
64
102
|
return db;
|
|
65
103
|
}
|
|
66
|
-
async function upsertFileManifest(
|
|
104
|
+
async function upsertFileManifest(dbOrPath, file_path, mtime, sha256) {
|
|
67
105
|
return new Promise((resolve, reject) => {
|
|
68
|
-
const
|
|
106
|
+
const isDb = typeof dbOrPath !== "string";
|
|
107
|
+
const db = isDb ? dbOrPath : initLocalDb(dbOrPath);
|
|
69
108
|
db.run("INSERT OR REPLACE INTO file_manifest (file_path, mtime, sha256, last_synced) VALUES (?, ?, ?, CURRENT_TIMESTAMP)", [file_path, mtime, sha256], (err) => {
|
|
70
|
-
|
|
109
|
+
if (!isDb) {
|
|
110
|
+
db.close();
|
|
111
|
+
}
|
|
71
112
|
if (err)
|
|
72
113
|
reject(err);
|
|
73
114
|
else
|
|
@@ -75,11 +116,14 @@ async function upsertFileManifest(dbPath, file_path, mtime, sha256) {
|
|
|
75
116
|
});
|
|
76
117
|
});
|
|
77
118
|
}
|
|
78
|
-
async function getFileManifest(
|
|
119
|
+
async function getFileManifest(dbOrPath, file_path) {
|
|
79
120
|
return new Promise((resolve, reject) => {
|
|
80
|
-
const
|
|
121
|
+
const isDb = typeof dbOrPath !== "string";
|
|
122
|
+
const db = isDb ? dbOrPath : initLocalDb(dbOrPath);
|
|
81
123
|
db.get("SELECT file_path, mtime, sha256 FROM file_manifest WHERE file_path = ?", [file_path], (err, row) => {
|
|
82
|
-
|
|
124
|
+
if (!isDb) {
|
|
125
|
+
db.close();
|
|
126
|
+
}
|
|
83
127
|
if (err)
|
|
84
128
|
reject(err);
|
|
85
129
|
else
|
|
@@ -87,11 +131,14 @@ async function getFileManifest(dbPath, file_path) {
|
|
|
87
131
|
});
|
|
88
132
|
});
|
|
89
133
|
}
|
|
90
|
-
async function getAllManifests(
|
|
134
|
+
async function getAllManifests(dbOrPath) {
|
|
91
135
|
return new Promise((resolve, reject) => {
|
|
92
|
-
const
|
|
136
|
+
const isDb = typeof dbOrPath !== "string";
|
|
137
|
+
const db = isDb ? dbOrPath : initLocalDb(dbOrPath);
|
|
93
138
|
db.all("SELECT file_path, mtime, sha256 FROM file_manifest", (err, rows) => {
|
|
94
|
-
|
|
139
|
+
if (!isDb) {
|
|
140
|
+
db.close();
|
|
141
|
+
}
|
|
95
142
|
if (err)
|
|
96
143
|
reject(err);
|
|
97
144
|
else
|
|
@@ -99,11 +146,14 @@ async function getAllManifests(dbPath) {
|
|
|
99
146
|
});
|
|
100
147
|
});
|
|
101
148
|
}
|
|
102
|
-
async function removeFileManifest(
|
|
149
|
+
async function removeFileManifest(dbOrPath, file_path) {
|
|
103
150
|
return new Promise((resolve, reject) => {
|
|
104
|
-
const
|
|
151
|
+
const isDb = typeof dbOrPath !== "string";
|
|
152
|
+
const db = isDb ? dbOrPath : initLocalDb(dbOrPath);
|
|
105
153
|
db.run("DELETE FROM file_manifest WHERE file_path = ?", [file_path], (err) => {
|
|
106
|
-
|
|
154
|
+
if (!isDb) {
|
|
155
|
+
db.close();
|
|
156
|
+
}
|
|
107
157
|
if (err)
|
|
108
158
|
reject(err);
|
|
109
159
|
else
|