local-agent-memory 1.0.8 → 1.0.10

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
@@ -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
  }
@@ -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
- node_fs_1.default.unlinkSync(localDbPath);
26
- // Also delete the WAL and SHM files if they exist
27
- if (node_fs_1.default.existsSync(`${localDbPath}-wal`))
28
- node_fs_1.default.unlinkSync(`${localDbPath}-wal`);
29
- if (node_fs_1.default.existsSync(`${localDbPath}-shm`))
30
- node_fs_1.default.unlinkSync(`${localDbPath}-shm`);
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);
@@ -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.");
@@ -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)(localDbPath);
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)(localDbPath, relativePath, mtime, sha256);
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)(localDbPath, relativePath, mtime, sha256);
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++;
@@ -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
  }
@@ -7,7 +7,32 @@ export interface FileManifestRow {
7
7
  export declare function getVecExtensionPath(): string | null;
8
8
  export declare function getLocalDbPath(projectRoot: string): string;
9
9
  export declare function initLocalDb(dbPath: string): sqlite3.Database;
10
- export declare function upsertFileManifest(dbPath: string, file_path: string, mtime: number, sha256: string): Promise<void>;
11
- export declare function getFileManifest(dbPath: string, file_path: string): Promise<FileManifestRow | null>;
12
- export declare function getAllManifests(dbPath: string): Promise<FileManifestRow[]>;
13
- export declare function removeFileManifest(dbPath: string, file_path: string): Promise<void>;
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>;
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>;