@lotargo/memory_plugin 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ export function sanitizeFtsQuery(query) {
11
11
  return words.join(" OR ");
12
12
  }
13
13
 
14
- export function bm25Search(db, query, limit = 30) {
14
+ export async function bm25Search(db, query, limit = 30) {
15
15
  const ftsQuery = sanitizeFtsQuery(query);
16
16
  if (!ftsQuery) return [];
17
17
 
@@ -23,7 +23,7 @@ export function bm25Search(db, query, limit = 30) {
23
23
  ORDER BY rank
24
24
  LIMIT ?;
25
25
  `);
26
- const rows = stmt.all(ftsQuery, limit);
26
+ const rows = await stmt.all(ftsQuery, limit);
27
27
  return rows.map((r, i) => ({
28
28
  id: r.id,
29
29
  content: r.content,
@@ -37,7 +37,7 @@ export function bm25Search(db, query, limit = 30) {
37
37
  }
38
38
  }
39
39
 
40
- export function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
40
+ export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
41
41
  if (!queryVector || queryVector.length === 0) return [];
42
42
 
43
43
  const vectorDim = queryVector.length;
@@ -52,8 +52,17 @@ export function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
52
52
  `);
53
53
 
54
54
  const scored = [];
55
- for (const r of stmt.iterate()) {
56
- tempView.set(r.vector.subarray(0, vectorDim * 4));
55
+ const rows = await stmt.all();
56
+ for (const r of rows) {
57
+ let vecSub = r.vector;
58
+ if (typeof vecSub === "string") {
59
+ vecSub = Buffer.from(vecSub, "base64");
60
+ } else if (vecSub.type === "Buffer" && Array.isArray(vecSub.data)) {
61
+ vecSub = Buffer.from(vecSub.data);
62
+ } else if (Array.isArray(vecSub)) {
63
+ vecSub = Buffer.from(vecSub);
64
+ }
65
+ tempView.set(vecSub.subarray(0, vectorDim * 4));
57
66
 
58
67
  const sim = cosineSimilarity(queryVector, tempVec);
59
68
  if (!isNaN(sim) && sim >= minSim) {
@@ -205,7 +214,7 @@ export async function hybridQuery({
205
214
  instruction = null,
206
215
  generateEmbeddings = true,
207
216
  }) {
208
- const db = customDb || getDatabase();
217
+ const db = customDb || await getDatabase();
209
218
  const activeConfig = getConfig();
210
219
 
211
220
  // If embeddings are disabled (e.g. fast/offline test mode or model not cached),
@@ -223,28 +232,28 @@ export async function hybridQuery({
223
232
  let fusedHits = [];
224
233
 
225
234
  if (algo === "lexical_only" || algo === "bm25_only") {
226
- const bm25Hits = bm25Search(db, query, limit * 4);
235
+ const bm25Hits = await bm25Search(db, query, limit * 4);
227
236
  fusedHits = bm25Hits.map((hit) => ({
228
237
  ...hit,
229
238
  score: 1.0 / hit.bm25_rank,
230
239
  }));
231
240
  } else if (algo === "semantic_only" || algo === "vector_only") {
232
241
  const queryVector = await embedText(query, true, embModel, null, instruction);
233
- const vectorHits = vectorSearch(db, queryVector, limit * 4, 0.10);
242
+ const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10);
234
243
  fusedHits = vectorHits.map((hit) => ({
235
244
  ...hit,
236
245
  score: hit.cosine_sim,
237
246
  }));
238
247
  } else if (algo === "rrf") {
239
- const bm25Hits = bm25Search(db, query, 30);
248
+ const bm25Hits = await bm25Search(db, query, 30);
240
249
  const queryVector = await embedText(query, true, embModel, null, instruction);
241
- const vectorHits = vectorSearch(db, queryVector, 30, 0.10);
250
+ const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
242
251
  fusedHits = rrfFusion(bm25Hits, vectorHits, 60, scoreThreshold);
243
252
  } else {
244
253
  // Default: RSF
245
- const bm25Hits = bm25Search(db, query, 30);
254
+ const bm25Hits = await bm25Search(db, query, 30);
246
255
  const queryVector = await embedText(query, true, embModel, null, instruction);
247
- const vectorHits = vectorSearch(db, queryVector, 30, 0.10);
256
+ const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
248
257
  fusedHits = rsfFusion(bm25Hits, vectorHits, alphaWeight, scoreThreshold);
249
258
  }
250
259
 
@@ -260,7 +269,7 @@ export async function hybridQuery({
260
269
  `);
261
270
 
262
271
  for (const hit of fusedHits) {
263
- const row = parentLookupStmt.get(hit.id);
272
+ const row = await parentLookupStmt.get(hit.id);
264
273
  const parentKey = row ? (row.medium_id || row.section_id) : hit.id;
265
274
  if (!seenParents.has(parentKey)) {
266
275
  seenParents.add(parentKey);
@@ -282,12 +291,12 @@ export async function hybridQuery({
282
291
  `);
283
292
 
284
293
  for (const hit of topHits) {
285
- const detail = secStmt.get(hit.id);
294
+ const detail = await secStmt.get(hit.id);
286
295
  if (!detail) continue;
287
296
 
288
297
  let symbols = [];
289
298
  if (includeGraphContext) {
290
- symbols = getRelatedSymbols(db, detail.section_id);
299
+ symbols = await getRelatedSymbols(db, detail.section_id);
291
300
  }
292
301
 
293
302
  results.push({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lotargo/memory_plugin",
3
- "version": "1.3.1",
4
- "description": "Persistent memory agent for coding AI tools remembers user preferences and project context across sessions. Works with Antigravity, OpenCode, Claude Code, and Codex.",
3
+ "version": "1.4.0",
4
+ "description": "100% local hybrid RAG memory for AI coding agents (OpenCode, Claude Code, Codex, Antigravity). MCP server + plugin: persistent user facts, document ingestion, vector + SQLite FTS5 retrieval across sessions.",
5
5
  "type": "module",
6
6
  "main": "opencode-plugin/index.js",
7
7
  "scripts": {
@@ -33,14 +33,29 @@
33
33
  "skills"
34
34
  ],
35
35
  "keywords": [
36
+ "memory",
37
+ "persistent-memory",
38
+ "ai-memory",
39
+ "ai-agent",
40
+ "ai-agents",
41
+ "rag",
42
+ "knowledge-base",
43
+ "llm",
44
+ "mcp",
45
+ "mcp-server",
46
+ "model-context-protocol",
47
+ "mcp-tools",
36
48
  "opencode",
37
49
  "claude-code",
38
50
  "codex",
39
51
  "antigravity",
40
52
  "plugin",
41
- "memory",
42
- "mcp",
43
- "ai",
53
+ "vector-search",
54
+ "hybrid-search",
55
+ "semantic-search",
56
+ "embeddings",
57
+ "sqlite",
58
+ "fts5",
44
59
  "context"
45
60
  ],
46
61
  "author": "Lotargo",
@@ -53,8 +68,12 @@
53
68
  "url": "https://github.com/Lotargo/memory_pugin.git"
54
69
  },
55
70
  "dependencies": {
56
- "@modelcontextprotocol/sdk": "^1.29.0",
57
71
  "@huggingface/transformers": "^3.3.3",
72
+ "@libsql/client": "^0.17.4",
73
+ "@modelcontextprotocol/sdk": "^1.29.0",
74
+ "mammoth": "^1.12.0",
75
+ "pdf-parse": "^2.4.5",
76
+ "xlsx": "^0.18.5",
58
77
  "zod": "^4.1.0"
59
78
  }
60
79
  }
@@ -1,228 +0,0 @@
1
- import { createServer } from "node:http";
2
- import { readFileSync, existsSync, statSync } from "node:fs";
3
- import { join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { getDatabase, DB_PATH, BLOBS_DIR } from "../db/database.js";
6
- import { ingestDocument, deleteDocument } from "../ingest/pipeline.js";
7
- import { hybridQuery } from "../retrieval/retriever.js";
8
- import { exportSnapshot, importSnapshot } from "./snapshot.js";
9
- import { readBlob } from "../storage/blob_store.js";
10
-
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
13
-
14
- export function findAvailablePort(startPort = 8765, maxPort = 8785) {
15
- return new Promise((resolve, reject) => {
16
- let port = startPort;
17
- const tryPort = () => {
18
- if (port > maxPort) {
19
- return reject(new Error(`No free port found between ${startPort} and ${maxPort}`));
20
- }
21
- const server = createServer();
22
- server.listen(port, () => {
23
- server.close(() => resolve(port));
24
- });
25
- server.on("error", () => {
26
- port++;
27
- tryPort();
28
- });
29
- };
30
- tryPort();
31
- });
32
- }
33
-
34
- function parseJsonBody(req) {
35
- return new Promise((resolve, reject) => {
36
- let body = "";
37
- req.on("data", (chunk) => {
38
- body += chunk.toString();
39
- });
40
- req.on("end", () => {
41
- try {
42
- resolve(body ? JSON.parse(body) : {});
43
- } catch (err) {
44
- reject(err);
45
- }
46
- });
47
- req.on("error", reject);
48
- });
49
- }
50
-
51
- export async function startAdminServer({ port = null, customDb = null, customBlobDir = BLOBS_DIR } = {}) {
52
- const db = customDb || getDatabase();
53
- const selectedPort = port || (await findAvailablePort());
54
- const htmlPath = join(__dirname, "index.html");
55
-
56
- const server = createServer(async (req, res) => {
57
- const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
58
- const pathname = url.pathname;
59
-
60
- // Helper for CORS and JSON response
61
- const sendJson = (data, status = 200) => {
62
- res.writeHead(status, {
63
- "Content-Type": "application/json",
64
- "Access-Control-Allow-Origin": "*",
65
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
66
- "Access-Control-Allow-Headers": "Content-Type",
67
- });
68
- res.end(JSON.stringify(data));
69
- };
70
-
71
- if (req.method === "OPTIONS") {
72
- res.writeHead(204, {
73
- "Access-Control-Allow-Origin": "*",
74
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
75
- "Access-Control-Allow-Headers": "Content-Type",
76
- });
77
- return res.end();
78
- }
79
-
80
- try {
81
- // 1. Static HTML SPA
82
- if (pathname === "/" || pathname === "/index.html") {
83
- if (!existsSync(htmlPath)) {
84
- res.writeHead(404, { "Content-Type": "text/plain" });
85
- return res.end("index.html not found");
86
- }
87
- const html = readFileSync(htmlPath, "utf-8");
88
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
89
- return res.end(html);
90
- }
91
-
92
- // 2. API: Stats
93
- if (pathname === "/api/stats" && req.method === "GET") {
94
- const docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
95
- const secCount = db.prepare("SELECT COUNT(*) as cnt FROM sections").get().cnt;
96
- const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
97
- const edgeCount = db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get().cnt;
98
- let dbSize = 0;
99
- if (existsSync(DB_PATH)) {
100
- try {
101
- dbSize = statSync(DB_PATH).size;
102
- } catch {}
103
- }
104
- return sendJson({
105
- documents: docCount,
106
- sections: secCount,
107
- micro_chunks: chunkCount,
108
- graph_edges: edgeCount,
109
- db_size_bytes: dbSize,
110
- });
111
- }
112
-
113
- // 3. API: Documents List
114
- if (pathname === "/api/documents" && req.method === "GET") {
115
- const docs = db.prepare("SELECT * FROM documents ORDER BY updated_at DESC").all();
116
- return sendJson(docs);
117
- }
118
-
119
- // 4. API: Document Detail
120
- if (pathname.startsWith("/api/documents/") && req.method === "GET") {
121
- const docId = pathname.replace("/api/documents/", "");
122
- const doc = db.prepare("SELECT * FROM documents WHERE id = ?").get(docId);
123
- if (!doc) return sendJson({ error: "Document not found" }, 404);
124
-
125
- const sections = db.prepare("SELECT * FROM sections WHERE doc_id = ?").all(docId);
126
- const microChunks = db.prepare("SELECT id, section_id, token_count FROM micro_chunks WHERE doc_id = ?").all(docId);
127
- const edges = db.prepare("SELECT * FROM graph_edges WHERE source_id = ? OR target_id = ?").all(docId, docId);
128
-
129
- let blobContent = null;
130
- if (doc.blob_hash) {
131
- try {
132
- blobContent = await readBlob(doc.blob_hash, customBlobDir);
133
- } catch {}
134
- }
135
-
136
- return sendJson({ doc, sections, microChunks, edges, blobContent });
137
- }
138
-
139
- // 5. API: Delete Document
140
- if (pathname.startsWith("/api/documents/") && req.method === "DELETE") {
141
- const docId = pathname.replace("/api/documents/", "");
142
- const result = await deleteDocument(docId, db, customBlobDir);
143
- return sendJson(result);
144
- }
145
-
146
- // 6. API: Ingest Document
147
- if (pathname === "/api/ingest" && req.method === "POST") {
148
- const body = await parseJsonBody(req);
149
- const result = await ingestDocument({
150
- content: body.content,
151
- type: body.type || "text",
152
- path: body.path || null,
153
- title: body.title || null,
154
- generateEmbeddings: body.generateEmbeddings !== false,
155
- customDb: db,
156
- customBlobDir,
157
- });
158
- return sendJson(result, 201);
159
- }
160
-
161
- // 7. API: Query Knowledge Base
162
- if (pathname === "/api/query" && req.method === "POST") {
163
- const body = await parseJsonBody(req);
164
- const results = await hybridQuery({
165
- query: body.query,
166
- limit: body.limit || 5,
167
- generateEmbeddings: body.generateEmbeddings !== false,
168
- customDb: db,
169
- });
170
- return sendJson({ results });
171
- }
172
-
173
- // 8. API: Graph Visualizer Data
174
- if (pathname === "/api/graph" && req.method === "GET") {
175
- const docs = db.prepare("SELECT id, title, path FROM documents").all();
176
- const edges = db.prepare("SELECT * FROM graph_edges").all();
177
-
178
- const nodes = docs.map((d) => ({
179
- id: d.id,
180
- label: d.title || d.path || d.id,
181
- type: "DOCUMENT",
182
- }));
183
-
184
- // Add code symbol nodes
185
- const symbolEdges = edges.filter((e) => e.relation_type === "DEFINES_SYMBOL");
186
- for (const se of symbolEdges) {
187
- if (!nodes.some((n) => n.id === se.target_id)) {
188
- nodes.push({
189
- id: se.target_id,
190
- label: se.target_id,
191
- type: "CODE_SYMBOL",
192
- });
193
- }
194
- }
195
-
196
- return sendJson({ nodes, edges });
197
- }
198
-
199
- // 9. API: Export Snapshot
200
- if (pathname === "/api/snapshot/export" && (req.method === "GET" || req.method === "POST")) {
201
- const snapshot = await exportSnapshot({ customDb: db, customBlobDir });
202
- return sendJson(snapshot);
203
- }
204
-
205
- // 10. API: Import Snapshot
206
- if (pathname === "/api/snapshot/import" && req.method === "POST") {
207
- const body = await parseJsonBody(req);
208
- const result = await importSnapshot({ customDb: db, customBlobDir, snapshotPathOrData: body });
209
- return sendJson(result);
210
- }
211
-
212
- // 404 Fallback
213
- res.writeHead(404, { "Content-Type": "text/plain" });
214
- res.end("Not Found");
215
- } catch (err) {
216
- console.error("Admin server error:", err);
217
- sendJson({ error: err.message }, 500);
218
- }
219
- });
220
-
221
- return new Promise((resolve) => {
222
- server.listen(selectedPort, () => {
223
- const url = `http://localhost:${selectedPort}`;
224
- console.log(`🚀 memory-agent Web Admin Dashboard running at ${url}`);
225
- resolve({ server, port: selectedPort, url });
226
- });
227
- });
228
- }