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 +36 -1
- package/dist/cli/commands/hook.js +11 -5
- package/dist/cli/commands/init.js +111 -46
- package/dist/cli/commands/status.js +24 -0
- package/dist/core/search.d.ts +6 -0
- package/dist/core/search.js +29 -1
- package/dist/db/project.d.ts +25 -0
- package/dist/db/project.js +111 -1
- package/dist/db/registry.js +2 -0
- package/dist/mcp/logger.d.ts +2 -0
- package/dist/mcp/logger.js +45 -0
- package/dist/mcp/server.js +58 -1
- package/dist/mcp/tools.js +408 -151
- package/package.json +2 -2
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
|
|
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
|
|
32
|
-
echo
|
|
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 "
|
|
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 "
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
57
|
-
// Core markdown
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
-
<!--
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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": ".
|
|
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
|
-
|
|
140
|
+
selectedKeys = Object.keys(availableRules);
|
|
103
141
|
}
|
|
104
142
|
else {
|
|
105
|
-
|
|
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
|
-
{
|
|
121
|
-
|
|
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: "
|
|
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
|
|
129
|
-
value: "
|
|
170
|
+
name: "Antigravity CLI (AGENTS.md)",
|
|
171
|
+
value: "antigravity-cli",
|
|
130
172
|
},
|
|
131
173
|
],
|
|
132
174
|
},
|
|
133
175
|
]);
|
|
134
|
-
|
|
176
|
+
selectedKeys = answers.tools;
|
|
135
177
|
}
|
|
136
178
|
spinner.start("Finishing initialization...");
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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.");
|
package/dist/core/search.d.ts
CHANGED
|
@@ -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.
|
package/dist/core/search.js
CHANGED
|
@@ -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
|
}
|
package/dist/db/project.d.ts
CHANGED
|
@@ -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>;
|
package/dist/db/project.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/db/registry.js
CHANGED
|
@@ -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,
|
package/dist/mcp/logger.d.ts
CHANGED
|
@@ -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>;
|