local-agent-memory 1.0.9 → 1.0.11

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/README.md CHANGED
@@ -31,6 +31,21 @@ Inicializa el entorno de memoria en el repositorio actual. Esto crea las bases d
31
31
  memory init --rules all --docs ./docs
32
32
  ```
33
33
 
34
+ #### Actualización de Reglas en Proyectos Existentes
35
+ Si ya tienes proyectos donde se inicializó la memoria y deseas migrar o incorporar el protocolo actualizado para tus agentes de IA sin perder tus reglas de `core.md` ni alterar el `project_id`:
36
+
37
+ ```bash
38
+ # Para actualizar las reglas de Antigravity (IDE y CLI)
39
+ memory init --rules=antigravity
40
+
41
+ # O si utilizas múltiples herramientas (Cursor, Claude, Antigravity, etc.)
42
+ memory init --rules=all
43
+ ```
44
+
45
+ **¿Por qué es necesario?**
46
+ - **Garantiza la adherencia del agente:** Inyecta en los archivos de instrucciones (`.agents/rules/memory.md`, `GEMINI.md`, `AGENTS.md`, `.cursorrules`, etc.) las directivas imperativas obligatorias (`memory_recall_context` al inicio y `memory_record_decision` al finalizar).
47
+ - **Actualización no destructiva:** Preserva intactos el archivo `core.md`, las decisiones previas (`.memory/decisions/`) y la identidad del proyecto (`project_id`).
48
+
34
49
  ### 2. `sync`
35
50
  Sincroniza los archivos Markdown del proyecto en el índice de memoria local. Escanea los documentos y actualiza la base de datos para que los agentes cuenten con el contexto más reciente.
36
51
 
@@ -39,12 +54,21 @@ memory sync
39
54
  ```
40
55
 
41
56
  ### 3. `status`
42
- Comprueba el estado de sincronización del proyecto de memoria actual, mostrando detalles sobre archivos indexados y pendienetes.
57
+ Comprueba el estado de sincronización del proyecto de memoria actual y presenta el nuevo panel de métricas y auditoría de actividad MCP.
43
58
 
44
59
  ```bash
60
+ # Verificar que la BD se actualizó y ver el nuevo panel de métricas
45
61
  memory status
46
62
  ```
47
63
 
64
+ **¿Por qué es necesario?**
65
+ - **Verificación de la Base de Datos:** Valida que la base de datos local SQLite (`.memory/local.sqlite`) esté operando en modo WAL de alta concurrencia y que todos los documentos Markdown estén indexados.
66
+ - **Panel de Métricas MCP:** Muestra la telemetría en tiempo real sobre la actividad del agente:
67
+ - Conteo acumulado de lecturas (`memory_recall_context`, `search_memory`, `get_memory_document`).
68
+ - Conteo acumulado de escrituras (`memory_record_decision`, `update_core_rules`, `end_session`).
69
+ - Marca temporal de la última actividad del agente.
70
+ - Registro de las decisiones y resúmenes de sesión más recientes.
71
+
48
72
  ### 4. `list` (o `projects`)
49
73
  Muestra de forma interactiva una lista de todos los proyectos registrados globalmente en tu máquina. Permite seleccionar un proyecto y ver detalles como el total de archivos indexados, los registros de decisiones (ADRs) y los resúmenes de sesión (Ledgers) para ese proyecto.
50
74
 
@@ -112,6 +136,17 @@ memory-mcp
112
136
 
113
137
  *(Si configuras MCP manualmente, deberás establecer que tu cliente ejecute `memory-mcp` sin argumentos extra)*
114
138
 
139
+ ### Herramientas MCP Expuestas al Agente
140
+
141
+ | Herramienta (y Alias) | Tipo | Descripción |
142
+ | :--- | :--- | :--- |
143
+ | `memory_recall_context`<br>*(alias: `get_project_context`)* | Lectura | **Paso Obligatorio al Iniciar:** Recupera `core.md`, las decisiones más recientes y contexto relevante antes de modificar código. |
144
+ | `memory_record_decision`<br>*(alias: `log_decision`)* | Escritura | **Paso Obligatorio al Finalizar:** Registra una decisión técnica o solución de bug en `.memory/decisions/`. |
145
+ | `search_memory` | Lectura | Búsqueda híbrida (vectorial + FTS5) sin cortes arbitrarios. Soporta `full_content: true` para obtener documentos completos. |
146
+ | `get_memory_document`<br>*(alias: `read_memory_document`)* | Lectura | Recupera el texto íntegro e intacto de cualquier documento en `.memory/`. |
147
+ | `update_core_rules` | Escritura | Agrega permanentemente reglas o directivas a `core.md`. |
148
+ | `end_session` | Escritura | Registra el resumen de cierre de sesión en `.memory/ledger/`. |
149
+
115
150
  ---
116
151
 
117
152
  ## Licencia
@@ -28,8 +28,8 @@ async function installGitHook() {
28
28
 
29
29
  # Add a reminder to the commit message template
30
30
  echo "" >> "$1"
31
- echo "# 🧠 Remember to update the local-agent-memory ledger if you made architectural changes." >> "$1"
32
- echo "# You can run: memory record-session --summary \\"..."\\"" >> "$1"
31
+ echo '# 🧠 Remember to update the local-agent-memory ledger if you made architectural changes.' >> "$1"
32
+ echo '# You can run: memory record-session --summary "..."' >> "$1"
33
33
  `;
34
34
  // Write file
35
35
  await node_fs_1.default.promises.writeFile(hookPath, hookContent, { mode: 0o755 });
@@ -43,15 +43,21 @@ echo "# You can run: memory record-session --summary \\"..."\\"" >> "$1"
43
43
  const postHookPath = node_path_1.default.join(hooksDir, postHookName);
44
44
  const postHookContent = `#!/bin/sh
45
45
  # local-agent-memory post-commit hook
46
- # Automatically records a memory session for this commit.
46
+ # Automatically records a memory session for this commit with changed files.
47
47
 
48
48
  COMMIT_MSG=$(git log -1 --pretty=%B)
49
+ CHANGED_FILES=$(git diff-tree --no-commit-id --name-status -r HEAD)
50
+
51
+ SUMMARY="Commit: $COMMIT_MSG
52
+
53
+ Modified files:
54
+ $CHANGED_FILES"
49
55
 
50
56
  # If memory CLI exists, run it
51
57
  if command -v memory >/dev/null 2>&1; then
52
- memory record-session --summary "Commit: $COMMIT_MSG" > /dev/null 2>&1 &
58
+ memory record-session --summary "$SUMMARY" > /dev/null 2>&1 &
53
59
  elif npx --no-install memory --version >/dev/null 2>&1; then
54
- npx --no-install memory record-session --summary "Commit: $COMMIT_MSG" > /dev/null 2>&1 &
60
+ npx --no-install memory record-session --summary "$SUMMARY" > /dev/null 2>&1 &
55
61
  fi
56
62
  `;
57
63
  await node_fs_1.default.promises.writeFile(postHookPath, postHookContent, { mode: 0o755 });
@@ -22,18 +22,12 @@ async function initProject(options = {}) {
22
22
  const spinner = (0, ora_1.default)("Initializing project...").start();
23
23
  try {
24
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)) {
26
- if (!options.force) {
27
- spinner.fail("Project already initialized. Use --force to overwrite.");
28
- process.exit(1);
29
- }
30
- else {
31
- // If force, we just continue (or we might want to clear it, but prompt says "a menos que se use --force", meaning we can overwrite or just proceed)
32
- }
33
- }
34
- else {
35
- await node_fs_1.default.promises.mkdir(memoryDir, { recursive: true });
25
+ const isAlreadyInitialized = node_fs_1.default.existsSync(memoryDir) && node_fs_1.default.existsSync(configFile);
26
+ if (isAlreadyInitialized && !options.force && !options.rules) {
27
+ spinner.fail("Project already initialized. Use --rules <targets> to update agent rules, or --force to overwrite.");
28
+ process.exit(1);
36
29
  }
30
+ await node_fs_1.default.promises.mkdir(memoryDir, { recursive: true });
37
31
  // Subdirectories
38
32
  await node_fs_1.default.promises.mkdir(node_path_1.default.join(memoryDir, "decisions"), {
39
33
  recursive: true,
@@ -41,22 +35,48 @@ async function initProject(options = {}) {
41
35
  await node_fs_1.default.promises.mkdir(node_path_1.default.join(memoryDir, "context"), {
42
36
  recursive: true,
43
37
  });
44
- // UUID
45
- const projectId = node_crypto_1.default.randomUUID();
38
+ let projectId = "";
39
+ if (isAlreadyInitialized && !options.force) {
40
+ try {
41
+ const rawConfig = await node_fs_1.default.promises.readFile(configFile, "utf8");
42
+ const parsed = JSON.parse(rawConfig);
43
+ projectId = parsed.project_id || node_crypto_1.default.randomUUID();
44
+ }
45
+ catch {
46
+ projectId = node_crypto_1.default.randomUUID();
47
+ }
48
+ }
49
+ else {
50
+ projectId = node_crypto_1.default.randomUUID();
51
+ }
46
52
  // Config file
47
- const configData = {
53
+ let configData = {
48
54
  project_id: projectId,
49
55
  created_at: new Date().toISOString(),
50
56
  include_paths: [".memory"],
51
57
  log_usage: options.log !== false,
52
58
  };
53
- if (options.docs) {
59
+ if (isAlreadyInitialized && !options.force) {
60
+ try {
61
+ const rawConfig = await node_fs_1.default.promises.readFile(configFile, "utf8");
62
+ configData = {
63
+ ...JSON.parse(rawConfig),
64
+ log_usage: options.log !== false,
65
+ };
66
+ }
67
+ catch { }
68
+ }
69
+ if (options.docs && !configData.include_paths?.includes(options.docs)) {
70
+ configData.include_paths = configData.include_paths || [".memory"];
54
71
  configData.include_paths.push(options.docs);
55
72
  }
56
- await node_fs_1.default.promises.writeFile(node_path_1.default.join(memoryDir, "config.json"), JSON.stringify(configData, null, 2));
57
- // Core markdown
58
- const coreMd = `# Core Rules\n\nThis file contains the core behavior and rules for this project.\n`;
59
- await node_fs_1.default.promises.writeFile(node_path_1.default.join(memoryDir, "core.md"), coreMd);
73
+ await node_fs_1.default.promises.writeFile(configFile, JSON.stringify(configData, null, 2));
74
+ // Core markdown: preserve existing core.md if project already initialized
75
+ const corePath = node_path_1.default.join(memoryDir, "core.md");
76
+ if (!node_fs_1.default.existsSync(corePath) || options.force) {
77
+ const coreMd = `# Core Rules\n\nThis file contains the core behavior and rules for this project.\n`;
78
+ await node_fs_1.default.promises.writeFile(corePath, coreMd);
79
+ }
60
80
  // Initialize local DB
61
81
  const localDbPath = (0, project_1.getLocalDbPath)(cwd);
62
82
  const localDb = (0, project_1.initLocalDb)(localDbPath);
@@ -80,34 +100,49 @@ async function initProject(options = {}) {
80
100
  });
81
101
  });
82
102
  await (0, registry_1.registerProject)(projectId, cwd);
83
- // Inject agent instructions
103
+ // Inject agent instructions with mandatory protocol
84
104
  const memoryInstructions = `
85
- <!-- local-agent-memory rules -->
86
- - Consulta la herramienta MCP \`search_memory\` antes de proponer cambios de arquitectura, interfaces o decisiones de diseño.
87
- - Utiliza \`log_decision\` cada vez que se adopte una convención de código, biblioteca o patrón técnico nuevo.
88
- - Utiliza la herramienta de sesión para resumir acuerdos antes de dar por terminada una tarea.
105
+ <!-- START LOCAL-AGENT-MEMORY PROTOCOL -->
106
+ # Protocolo Obligatorio de Memoria Local (\`local-agent-memory\`)
107
+
108
+ Este proyecto cuenta con un sistema de memoria persistente conectado vía MCP. Su uso NO es opcional.
109
+
110
+ 1. **Al iniciar cualquier tarea (Lectura):**
111
+ - Antes de proponer código, modificar archivos o responder dudas sobre el proyecto, **DEBES** ejecutar la herramienta MCP \`memory_recall_context\` (o \`get_project_context\`) pasando el \`projectPath\` actual.
112
+ 2. **Al finalizar cualquier cambio (Escritura):**
113
+ - Antes de dar por concluida tu respuesta tras haber editado código, resuelto un bug o definido arquitectura, **DEBES** ejecutar la herramienta MCP \`memory_record_decision\` (o \`log_decision\`) documentando el cambio y su justificación técnica.
114
+ 3. **Para dudas sobre reglas o patrones existentes:**
115
+ - Utiliza la herramienta MCP \`search_memory\` para consultar el histórico y las decisiones previas (pasa \`full_content: true\` para obtener documentos enteros en vez de fragmentos).
116
+ 4. **Para leer un documento de memoria completo:**
117
+ - Utiliza la herramienta MCP \`get_memory_document\` (o \`read_memory_document\`) especificando la ruta o nombre del archivo (ej. \`decisions/2026-10-14-xxx.md\` o \`core.md\`).
118
+ <!-- END LOCAL-AGENT-MEMORY PROTOCOL -->
89
119
  `;
90
120
  spinner.stop();
91
- let tools = [];
121
+ let selectedKeys = [];
92
122
  const availableRules = {
93
- cursor: ".cursorrules",
94
- windsurf: ".windsurfrules",
95
- copilot: ".github/copilot-instructions.md",
96
- claude: "CLAUDE.md",
97
- "antigravity-ide": ".agents/rules/memory.md",
98
- "antigravity-cli": ".agents/rules/memory.md",
123
+ cursor: [".cursorrules", ".cursor/rules/memory-protocol.mdc"],
124
+ windsurf: [".windsurfrules"],
125
+ copilot: [".github/copilot-instructions.md"],
126
+ claude: ["CLAUDE.md"],
127
+ "antigravity-ide": [".agents/rules/memory.md", "GEMINI.md"],
128
+ "antigravity-cli": ["AGENTS.md"],
129
+ antigravity: [".agents/rules/memory.md", "GEMINI.md", "AGENTS.md"],
130
+ ".cursorrules": [".cursorrules"],
131
+ ".windsurfrules": [".windsurfrules"],
132
+ ".github/copilot-instructions.md": [".github/copilot-instructions.md"],
133
+ "claude.md": ["CLAUDE.md"],
134
+ ".agents/rules/memory.md": [".agents/rules/memory.md"],
135
+ "gemini.md": ["GEMINI.md"],
136
+ "agents.md": ["AGENTS.md"],
99
137
  };
100
138
  if (options.rules) {
101
139
  if (options.rules.toLowerCase() === "all") {
102
- tools = Array.from(new Set(Object.values(availableRules)));
140
+ selectedKeys = Object.keys(availableRules);
103
141
  }
104
142
  else {
105
- const rulesList = options.rules
143
+ selectedKeys = options.rules
106
144
  .split(",")
107
145
  .map((r) => r.trim().toLowerCase());
108
- tools = Array.from(new Set(rulesList
109
- .map((r) => availableRules[r])
110
- .filter((r) => Boolean(r))));
111
146
  }
112
147
  }
113
148
  else {
@@ -117,32 +152,62 @@ async function initProject(options = {}) {
117
152
  name: "tools",
118
153
  message: "Select the IDEs/CLIs to generate or inject memory rules into:",
119
154
  choices: [
120
- { name: "Cursor (.cursorrules)", value: ".cursorrules" },
121
- { name: "Windsurf (.windsurfrules)", value: ".windsurfrules" },
155
+ {
156
+ name: "Cursor (.cursorrules, .cursor/rules/memory-protocol.mdc)",
157
+ value: "cursor",
158
+ },
159
+ { name: "Windsurf (.windsurfrules)", value: "windsurf" },
122
160
  {
123
161
  name: "GitHub Copilot (.github/copilot-instructions.md)",
124
- value: ".github/copilot-instructions.md",
162
+ value: "copilot",
163
+ },
164
+ { name: "Claude Code (CLAUDE.md)", value: "claude" },
165
+ {
166
+ name: "Antigravity IDE (.agents/rules/memory.md, GEMINI.md)",
167
+ value: "antigravity-ide",
125
168
  },
126
- { name: "Claude Code (CLAUDE.md)", value: "CLAUDE.md" },
127
169
  {
128
- name: "Antigravity IDE / CLI (.agents/rules/memory.md)",
129
- value: ".agents/rules/memory.md",
170
+ name: "Antigravity CLI (AGENTS.md)",
171
+ value: "antigravity-cli",
130
172
  },
131
173
  ],
132
174
  },
133
175
  ]);
134
- tools = answers.tools;
176
+ selectedKeys = answers.tools;
135
177
  }
136
178
  spinner.start("Finishing initialization...");
137
- for (const file of tools) {
179
+ const filesToInject = new Set();
180
+ for (const key of selectedKeys) {
181
+ const paths = availableRules[key.toLowerCase()];
182
+ if (paths) {
183
+ for (const p of paths)
184
+ filesToInject.add(p);
185
+ }
186
+ else if (key.endsWith(".md") ||
187
+ key.endsWith(".mdc") ||
188
+ key.startsWith(".")) {
189
+ filesToInject.add(key);
190
+ }
191
+ }
192
+ for (const file of Array.from(filesToInject)) {
138
193
  const filePath = node_path_1.default.join(cwd, file);
139
194
  const dir = node_path_1.default.dirname(filePath);
140
195
  if (!node_fs_1.default.existsSync(dir)) {
141
196
  await node_fs_1.default.promises.mkdir(dir, { recursive: true });
142
197
  }
143
198
  if (node_fs_1.default.existsSync(filePath)) {
144
- const content = await node_fs_1.default.promises.readFile(filePath, "utf8");
145
- if (!content.includes("<!-- local-agent-memory rules -->")) {
199
+ let content = await node_fs_1.default.promises.readFile(filePath, "utf8");
200
+ const protocolRegex = /<!-- START LOCAL-AGENT-MEMORY PROTOCOL -->[\s\S]*?<!-- END LOCAL-AGENT-MEMORY PROTOCOL -->/;
201
+ const legacyRegex = /<!-- local-agent-memory rules -->[\s\S]*?(?=\n\n|$)/;
202
+ if (protocolRegex.test(content)) {
203
+ content = content.replace(protocolRegex, memoryInstructions.trim());
204
+ await node_fs_1.default.promises.writeFile(filePath, content);
205
+ }
206
+ else if (legacyRegex.test(content)) {
207
+ content = content.replace(legacyRegex, memoryInstructions.trim());
208
+ await node_fs_1.default.promises.writeFile(filePath, content);
209
+ }
210
+ else {
146
211
  await node_fs_1.default.promises.appendFile(filePath, `\n${memoryInstructions}`);
147
212
  }
148
213
  }
@@ -57,6 +57,16 @@ async function projectStatus() {
57
57
  manifestMap.delete(relativePath);
58
58
  }
59
59
  delCount = manifestMap.size;
60
+ // Check MCP usage stats if DB exists
61
+ let mcpStats = null;
62
+ if (node_fs_1.default.existsSync(localDbPath)) {
63
+ try {
64
+ mcpStats = await (0, project_1.getMcpUsageStats)(localDbPath);
65
+ }
66
+ catch (_e) {
67
+ // Table may not exist yet in older databases
68
+ }
69
+ }
60
70
  spinner.stop();
61
71
  console.log(`\nProject ID: ${projectId || "Unknown (Not in global registry)"}`);
62
72
  console.log(`Total MD files tracking: ${currentFiles.length}`);
@@ -66,6 +76,20 @@ async function projectStatus() {
66
76
  console.log(`Modified (unsynced): ${modCount}`);
67
77
  console.log(`Deleted (orphaned in db): ${delCount}`);
68
78
  console.log(`\nRun \`memory sync\` to update index.`);
79
+ if (mcpStats) {
80
+ console.log(`\nAgent & MCP Activity:`);
81
+ console.log(`---------------------------------`);
82
+ console.log(`Total MCP tool invocations: ${mcpStats.totalCalls}`);
83
+ console.log(` - Context & search recalls: ${mcpStats.readCalls}`);
84
+ console.log(` - Decisions & rules recorded: ${mcpStats.writeCalls}`);
85
+ console.log(`Last agent activity: ${mcpStats.lastConnection || "No recorded activity yet"}`);
86
+ if (mcpStats.recentQueries && mcpStats.recentQueries.length > 0) {
87
+ console.log(`\nRecent agent actions:`);
88
+ for (const q of mcpStats.recentQueries) {
89
+ console.log(` [${q.timestamp}] ${q.tool_name || q.event_type}: ${q.query_or_summary || "success"}`);
90
+ }
91
+ }
92
+ }
69
93
  }
70
94
  catch (err) {
71
95
  spinner.fail("Status check failed.");
@@ -10,6 +10,12 @@ export interface SearchResult extends Chunk {
10
10
  * or punctuation (like *, AND, OR, NEAR).
11
11
  */
12
12
  export declare function sanitizeFtsQuery(query: string): string;
13
+ /**
14
+ * Calculates a time decay multiplier for search results:
15
+ * Final_Score = RRF_Score * (1 / (1 + lambda * delta_days))
16
+ * Core rules or pinned files have lambda = 0 (multiplier = 1.0).
17
+ */
18
+ export declare function calculateTimeDecayMultiplier(filePath: string, nowMs?: number, mtimeMs?: number): number;
13
19
  /**
14
20
  * Performs a hybrid search combining FTS5 lexical search and sqlite-vec vector search.
15
21
  * Uses Reciprocal Rank Fusion (RRF) to combine results.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitizeFtsQuery = sanitizeFtsQuery;
4
+ exports.calculateTimeDecayMultiplier = calculateTimeDecayMultiplier;
4
5
  exports.hybridSearch = hybridSearch;
5
6
  const embeddings_1 = require("./embeddings");
6
7
  const sync_1 = require("./sync");
@@ -17,6 +18,31 @@ function sanitizeFtsQuery(query) {
17
18
  return "";
18
19
  return terms.map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
19
20
  }
21
+ /**
22
+ * Calculates a time decay multiplier for search results:
23
+ * Final_Score = RRF_Score * (1 / (1 + lambda * delta_days))
24
+ * Core rules or pinned files have lambda = 0 (multiplier = 1.0).
25
+ */
26
+ function calculateTimeDecayMultiplier(filePath, nowMs = Date.now(), mtimeMs) {
27
+ if (filePath.endsWith("core.md") || filePath.includes("pinned")) {
28
+ return 1.0;
29
+ }
30
+ // Check if filename starts with YYYY-MM-DD (e.g. ADRs or ledger sessions)
31
+ const baseName = filePath.split(/[/\\]/).pop() || "";
32
+ const match = baseName.match(/^(\d{4})-(\d{2})-(\d{2})/);
33
+ let ageDays = 0;
34
+ if (match) {
35
+ const fileDate = new Date(`${match[1]}-${match[2]}-${match[3]}`).getTime();
36
+ if (!Number.isNaN(fileDate)) {
37
+ ageDays = Math.max(0, (nowMs - fileDate) / (1000 * 60 * 60 * 24));
38
+ }
39
+ }
40
+ else if (mtimeMs) {
41
+ ageDays = Math.max(0, (nowMs - mtimeMs) / (1000 * 60 * 60 * 24));
42
+ }
43
+ const LAMBDA = 0.005; // Gentle decay factor
44
+ return 1 / (1 + LAMBDA * ageDays);
45
+ }
20
46
  /**
21
47
  * Performs a hybrid search combining FTS5 lexical search and sqlite-vec vector search.
22
48
  * Uses Reciprocal Rank Fusion (RRF) to combine results.
@@ -129,6 +155,7 @@ async function hybridSearch(query, limit, db, filter) {
129
155
  row = await asyncDb.get("SELECT * FROM chunks WHERE id = ?", [id]);
130
156
  }
131
157
  if (row) {
158
+ const decay = calculateTimeDecayMultiplier(row.path);
132
159
  finalResults.push({
133
160
  id: row.id,
134
161
  path: row.path,
@@ -136,9 +163,10 @@ async function hybridSearch(query, limit, db, filter) {
136
163
  breadcrumbs: row.breadcrumbs,
137
164
  startLine: row.startLine,
138
165
  endLine: row.endLine,
139
- score,
166
+ score: score * decay,
140
167
  });
141
168
  }
142
169
  }
170
+ finalResults.sort((a, b) => b.score - a.score);
143
171
  return finalResults;
144
172
  }
@@ -11,3 +11,28 @@ export declare function upsertFileManifest(dbOrPath: string | sqlite3.Database,
11
11
  export declare function getFileManifest(dbOrPath: string | sqlite3.Database, file_path: string): Promise<FileManifestRow | null>;
12
12
  export declare function getAllManifests(dbOrPath: string | sqlite3.Database): Promise<FileManifestRow[]>;
13
13
  export declare function removeFileManifest(dbOrPath: string | sqlite3.Database, file_path: string): Promise<void>;
14
+ export interface McpUsageLogRow {
15
+ id?: number;
16
+ timestamp?: string;
17
+ project_path: string;
18
+ event_type: string;
19
+ tool_name?: string | null | undefined;
20
+ query_or_summary?: string | null | undefined;
21
+ execution_time_ms?: number | null | undefined;
22
+ success?: boolean | null | undefined;
23
+ }
24
+ export interface McpUsageStats {
25
+ totalCalls: number;
26
+ readCalls: number;
27
+ writeCalls: number;
28
+ lastConnection: string | null;
29
+ recentQueries: Array<{
30
+ timestamp: string;
31
+ event_type: string;
32
+ tool_name: string | null;
33
+ query_or_summary: string | null;
34
+ success: boolean;
35
+ }>;
36
+ }
37
+ export declare function insertMcpUsageLog(dbOrPath: string | sqlite3.Database, entry: Omit<McpUsageLogRow, "id" | "timestamp">): Promise<void>;
38
+ export declare function getMcpUsageStats(dbOrPath: string | sqlite3.Database): Promise<McpUsageStats>;
@@ -10,6 +10,8 @@ exports.upsertFileManifest = upsertFileManifest;
10
10
  exports.getFileManifest = getFileManifest;
11
11
  exports.getAllManifests = getAllManifests;
12
12
  exports.removeFileManifest = removeFileManifest;
13
+ exports.insertMcpUsageLog = insertMcpUsageLog;
14
+ exports.getMcpUsageStats = getMcpUsageStats;
13
15
  const node_fs_1 = __importDefault(require("node:fs"));
14
16
  const node_os_1 = __importDefault(require("node:os"));
15
17
  const node_path_1 = __importDefault(require("node:path"));
@@ -34,11 +36,26 @@ function getLocalDbPath(projectRoot) {
34
36
  return node_path_1.default.join(projectRoot, ".memory", "local.sqlite");
35
37
  }
36
38
  function initLocalDb(dbPath) {
37
- const db = new sqlite3_1.default.Database(dbPath);
39
+ const db = new sqlite3_1.default.Database(dbPath, () => { });
40
+ db.on("error", () => { });
38
41
  db.configure("busyTimeout", 5000);
39
42
  db.serialize(() => {
40
43
  db.run("PRAGMA journal_mode = WAL;");
41
44
  db.run("PRAGMA busy_timeout = 5000;");
45
+ db.run("PRAGMA synchronous = NORMAL;");
46
+ db.run("PRAGMA foreign_keys = ON;");
47
+ db.run(`
48
+ CREATE TABLE IF NOT EXISTS mcp_usage_logs (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
51
+ project_path TEXT,
52
+ event_type TEXT NOT NULL,
53
+ tool_name TEXT,
54
+ query_or_summary TEXT,
55
+ execution_time_ms INTEGER,
56
+ success BOOLEAN
57
+ )
58
+ `);
42
59
  db.run(`
43
60
  CREATE TABLE IF NOT EXISTS file_manifest (
44
61
  file_path TEXT PRIMARY KEY,
@@ -161,3 +178,96 @@ async function removeFileManifest(dbOrPath, file_path) {
161
178
  });
162
179
  });
163
180
  }
181
+ async function insertMcpUsageLog(dbOrPath, entry) {
182
+ return new Promise((resolve) => {
183
+ try {
184
+ const isDb = typeof dbOrPath !== "string";
185
+ const db = isDb ? dbOrPath : initLocalDb(dbOrPath);
186
+ db.run(`INSERT INTO mcp_usage_logs (project_path, event_type, tool_name, query_or_summary, execution_time_ms, success)
187
+ VALUES (?, ?, ?, ?, ?, ?)`, [
188
+ entry.project_path,
189
+ entry.event_type,
190
+ entry.tool_name ?? null,
191
+ entry.query_or_summary ?? null,
192
+ entry.execution_time_ms ?? null,
193
+ entry.success ?? true,
194
+ ], (_err) => {
195
+ if (!isDb) {
196
+ try {
197
+ db.close();
198
+ }
199
+ catch { }
200
+ }
201
+ resolve();
202
+ });
203
+ }
204
+ catch {
205
+ resolve();
206
+ }
207
+ });
208
+ }
209
+ async function getMcpUsageStats(dbOrPath) {
210
+ return new Promise((resolve) => {
211
+ const emptyStats = {
212
+ totalCalls: 0,
213
+ readCalls: 0,
214
+ writeCalls: 0,
215
+ lastConnection: null,
216
+ recentQueries: [],
217
+ };
218
+ try {
219
+ const isDb = typeof dbOrPath !== "string";
220
+ const db = isDb ? dbOrPath : initLocalDb(dbOrPath);
221
+ db.serialize(() => {
222
+ db.all(`SELECT
223
+ COUNT(*) as total_count,
224
+ SUM(CASE WHEN tool_name IN ('get_project_context', 'memory_recall_context', 'search_memory') THEN 1 ELSE 0 END) as read_count,
225
+ SUM(CASE WHEN tool_name IN ('log_decision', 'memory_record_decision', 'update_core_rules', 'end_session') THEN 1 ELSE 0 END) as write_count,
226
+ MAX(timestamp) as last_conn
227
+ FROM mcp_usage_logs`, [], (err, statsRows) => {
228
+ if (err) {
229
+ if (!isDb) {
230
+ try {
231
+ db.close();
232
+ }
233
+ catch { }
234
+ }
235
+ return resolve(emptyStats);
236
+ }
237
+ db.all(`SELECT timestamp, event_type, tool_name, query_or_summary, success
238
+ FROM mcp_usage_logs
239
+ WHERE event_type = 'tool_call'
240
+ ORDER BY id DESC
241
+ LIMIT 5`, [], (err2, recentRows) => {
242
+ if (!isDb) {
243
+ try {
244
+ db.close();
245
+ }
246
+ catch { }
247
+ }
248
+ if (err2) {
249
+ return resolve(emptyStats);
250
+ }
251
+ const row = statsRows?.[0] || {};
252
+ resolve({
253
+ totalCalls: row.total_count || 0,
254
+ readCalls: row.read_count || 0,
255
+ writeCalls: row.write_count || 0,
256
+ lastConnection: row.last_conn || null,
257
+ recentQueries: (recentRows || []).map((r) => ({
258
+ timestamp: r.timestamp,
259
+ event_type: r.event_type,
260
+ tool_name: r.tool_name,
261
+ query_or_summary: r.query_or_summary,
262
+ success: Boolean(r.success),
263
+ })),
264
+ });
265
+ });
266
+ });
267
+ });
268
+ }
269
+ catch {
270
+ resolve(emptyStats);
271
+ }
272
+ });
273
+ }
@@ -34,6 +34,8 @@ function initRegistryDb(dbPath = getRegistryDbPath()) {
34
34
  db.serialize(() => {
35
35
  db.run("PRAGMA journal_mode = WAL;");
36
36
  db.run("PRAGMA busy_timeout = 5000;");
37
+ db.run("PRAGMA synchronous = NORMAL;");
38
+ db.run("PRAGMA foreign_keys = ON;");
37
39
  db.run(`
38
40
  CREATE TABLE IF NOT EXISTS projects (
39
41
  project_id TEXT PRIMARY KEY,
@@ -1,3 +1,4 @@
1
+ export declare const getLocalMemoryDir: () => string;
1
2
  export interface MemoryConfig {
2
3
  project_id?: string;
3
4
  created_at?: string;
@@ -8,3 +9,4 @@ export interface MemoryConfig {
8
9
  }
9
10
  export declare function getMemoryConfig(cwd: string): MemoryConfig | null;
10
11
  export declare function logUsage(cwd: string, action: string, details?: Record<string, unknown>): void;
12
+ export declare function recordMcpEvent(cwd: string, eventType: string, toolName?: string, queryOrSummary?: string, executionTimeMs?: number, success?: boolean): Promise<void>;