local-agent-memory 1.0.6 → 1.0.8

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.
@@ -21,7 +21,8 @@ async function initProject(options = {}) {
21
21
  }
22
22
  const spinner = (0, ora_1.default)("Initializing project...").start();
23
23
  try {
24
- if (node_fs_1.default.existsSync(memoryDir)) {
24
+ const configFile = node_path_1.default.join(memoryDir, "config.json");
25
+ if (node_fs_1.default.existsSync(memoryDir) && node_fs_1.default.existsSync(configFile)) {
25
26
  if (!options.force) {
26
27
  spinner.fail("Project already initialized. Use --force to overwrite.");
27
28
  process.exit(1);
@@ -13,8 +13,20 @@ const registry_1 = require("../../db/registry");
13
13
  async function listProjects() {
14
14
  const spinner = (0, ora_1.default)("Cargando proyectos registrados...").start();
15
15
  try {
16
- const projects = await (0, registry_1.getAllProjectsFromRegistry)();
17
- if (!projects || projects.length === 0) {
16
+ const rawProjects = await (0, registry_1.getAllProjectsFromRegistry)();
17
+ // Auto-prune projects whose directories no longer exist on disk
18
+ const projects = [];
19
+ if (rawProjects && rawProjects.length > 0) {
20
+ for (const p of rawProjects) {
21
+ if (node_fs_1.default.existsSync(p.file_path)) {
22
+ projects.push(p);
23
+ }
24
+ else {
25
+ await (0, registry_1.unregisterProject)(p.project_id);
26
+ }
27
+ }
28
+ }
29
+ if (projects.length === 0) {
18
30
  spinner.info("No hay proyectos registrados en la memoria global.");
19
31
  return;
20
32
  }
@@ -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, reject) => {
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
- reject(err);
40
- else
41
- resolve(rows);
38
+ if (err) {
39
+ resolve([]);
40
+ }
41
+ else {
42
+ resolve(rows || []);
43
+ }
42
44
  });
43
45
  });
44
- // 2. Vector Search
45
- const queryEmbedding = await (0, embeddings_1.generateEmbedding)(query);
46
- const vectorData = Buffer.from(new Float32Array(queryEmbedding).buffer);
47
- const vecResults = await new Promise((resolve, reject) => {
48
- // sqlite-vec distance function. distance is lower = better (closer)
49
- // we want smallest distance first
50
- db.all(`SELECT rowid as id, distance FROM chunks_vec
51
- WHERE embedding MATCH ? AND k = 100
52
- ORDER BY distance`, [vectorData], (err, rows) => {
53
- if (err)
54
- reject(err);
55
- else
56
- resolve(rows);
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
@@ -4,6 +4,7 @@ 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
10
  export declare function upsertFileManifest(dbPath: string, file_path: string, mtime: number, sha256: string): Promise<void>;
@@ -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,22 +63,41 @@ 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
- // Create a regular table for chunks_vec to prevent crashes in tests or environments
56
- // where the sqlite-vec extension is not loaded yet.
57
- db.run(`
58
- CREATE TABLE IF NOT EXISTS chunks_vec (
59
- rowid INTEGER,
60
- embedding BLOB
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
  }
@@ -13,3 +13,4 @@ export interface RegistryProject {
13
13
  created_at: string;
14
14
  }
15
15
  export declare function getAllProjectsFromRegistry(dbPath?: string): Promise<RegistryProject[]>;
16
+ export declare function unregisterProject(projectId: string, dbPath?: string): Promise<void>;
@@ -9,12 +9,20 @@ exports.registerProject = registerProject;
9
9
  exports.getProjectByPath = getProjectByPath;
10
10
  exports.getProjectRootFromRegistry = getProjectRootFromRegistry;
11
11
  exports.getAllProjectsFromRegistry = getAllProjectsFromRegistry;
12
+ exports.unregisterProject = unregisterProject;
12
13
  const node_fs_1 = __importDefault(require("node:fs"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
14
15
  const sqlite3_1 = __importDefault(require("sqlite3"));
15
16
  const initGlobal_1 = require("../setup/initGlobal");
16
17
  function getRegistryDbPath() {
17
- const memDir = (0, initGlobal_1.getLocalMemoryDir)();
18
+ if (process.env.MEMORY_REGISTRY_PATH) {
19
+ const dir = node_path_1.default.dirname(process.env.MEMORY_REGISTRY_PATH);
20
+ if (!node_fs_1.default.existsSync(dir)) {
21
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
22
+ }
23
+ return process.env.MEMORY_REGISTRY_PATH;
24
+ }
25
+ const memDir = process.env.MEMORY_GLOBAL_DIR || (0, initGlobal_1.getLocalMemoryDir)();
18
26
  if (!node_fs_1.default.existsSync(memDir)) {
19
27
  node_fs_1.default.mkdirSync(memDir, { recursive: true });
20
28
  }
@@ -103,3 +111,15 @@ async function getAllProjectsFromRegistry(dbPath = getRegistryDbPath()) {
103
111
  });
104
112
  });
105
113
  }
114
+ async function unregisterProject(projectId, dbPath = getRegistryDbPath()) {
115
+ return new Promise((resolve, reject) => {
116
+ const db = initRegistryDb(dbPath);
117
+ db.run("DELETE FROM projects WHERE project_id = ?", [projectId], (err) => {
118
+ db.close();
119
+ if (err)
120
+ reject(err);
121
+ else
122
+ resolve();
123
+ });
124
+ });
125
+ }
@@ -39,18 +39,23 @@ const fs = __importStar(require("node:fs"));
39
39
  const path = __importStar(require("node:path"));
40
40
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
41
41
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
42
+ const initGlobal_1 = require("../setup/initGlobal");
42
43
  const logger_1 = require("./logger");
43
44
  const tools_1 = require("./tools");
44
45
  const utils_1 = require("./utils");
45
46
  async function setupMCPServer() {
46
- const { cwd } = await (0, utils_1.resolveCwd)();
47
- const memoryDir = path.join(cwd, ".memory");
48
- if (!fs.existsSync(memoryDir)) {
49
- fs.mkdirSync(memoryDir, { recursive: true });
47
+ const globalMemDir = (0, initGlobal_1.getLocalMemoryDir)();
48
+ if (!fs.existsSync(globalMemDir)) {
49
+ fs.mkdirSync(globalMemDir, { recursive: true });
50
50
  }
51
- const logFile = path.join(memoryDir, "mcp-debug.log");
51
+ const logFile = path.join(globalMemDir, "mcp-debug.log");
52
52
  const log = (msg) => {
53
- fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${msg}\n`);
53
+ try {
54
+ fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${msg}\n`);
55
+ }
56
+ catch (_err) {
57
+ // Ignore write errors to prevent server crash
58
+ }
54
59
  };
55
60
  log("Starting MCP Server");
56
61
  const server = new mcp_js_1.McpServer({
@@ -121,7 +126,9 @@ function setupProcessLifecycle(transport, log = () => { }) {
121
126
  transport.onclose = () => exitHandler("transport closed");
122
127
  process.on("SIGINT", () => exitHandler("SIGINT received"));
123
128
  process.on("SIGTERM", () => exitHandler("SIGTERM received"));
124
- if (process.ppid && process.ppid !== 1) {
129
+ // Only watch ppid on POSIX platforms where signal 0 check is reliable
130
+ // and process.ppid is not a short-lived batch shell wrapper like cmd.exe on Windows.
131
+ if (process.platform !== "win32" && process.ppid && process.ppid !== 1) {
125
132
  const parentCheckInterval = setInterval(() => {
126
133
  try {
127
134
  process.kill(process.ppid, 0);
@@ -46,7 +46,7 @@ const node_os_1 = __importDefault(require("node:os"));
46
46
  const node_path_1 = __importDefault(require("node:path"));
47
47
  const transformers_1 = require("@xenova/transformers");
48
48
  const tar = __importStar(require("tar"));
49
- const getLocalMemoryDir = () => node_path_1.default.join(node_os_1.default.homedir(), ".local_memory");
49
+ const getLocalMemoryDir = () => process.env.MEMORY_GLOBAL_DIR || node_path_1.default.join(node_os_1.default.homedir(), ".local_memory");
50
50
  exports.getLocalMemoryDir = getLocalMemoryDir;
51
51
  const getModelsDir = () => node_path_1.default.join((0, exports.getLocalMemoryDir)(), "models");
52
52
  exports.getModelsDir = getModelsDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-agent-memory",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",