@ancleto/spec 0.6.13 → 0.6.16

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
@@ -101,25 +101,23 @@ ancleto install --no-mcp
101
101
  ancleto update
102
102
  ```
103
103
 
104
- > El instalador configura por defecto los MCP locales **engram** y **caveman**. Si un binario no está en tu sistema, se omite con un warning sin interrumpir el flujo.
104
+ > El instalador configura por defecto el MCP de **memoria propia** (`ancleto-memory`) y **caveman**. El MCP externo **engram** ya no se agrega por defecto: sumalo con `--with-engram` si además querés esa memoria. Si un binario no está en tu sistema, se omite con un warning sin interrumpir el flujo.
105
105
 
106
106
  ### Costo en tokens de los MCP
107
107
 
108
- Los MCP no son gratis en contexto: la lista de herramientas de cada servidor viaja en **cada** request, se usen o no. Medición local con el perfil mínimo de cada uno:
108
+ Los MCP no son gratis en contexto: la lista de herramientas de cada servidor viaja en **cada** request, se usen o no. Medición local:
109
109
 
110
110
  | MCP | Herramientas | Tokens por request |
111
111
  |---|---|---|
112
- | engram (`--tools=agent`) | 18 | ~4.900 |
112
+ | `ancleto-memory` (memoria propia) | 3 | ~500 |
113
113
  | caveman | 5 | ~830 |
114
- | **Total** | 23 | **~5.800** |
114
+ | engram (solo con `--with-engram`) | 18 | ~4.900 |
115
115
 
116
- Cómo bajarlo si no los usás:
116
+ Cómo bajarlo:
117
117
 
118
- - Instalá con `--no-mcp`.
119
- - O deshabilitá un servidor en tu `opencode.json`: `"mcp": { "engram": { "enabled": false } }`.
120
- - Engram permite elegir herramientas puntuales: `--tools=agent` ya es el perfil mínimo; las más pesadas de ese perfil son `mem_save` (~920), `mem_compare` (~590) y `mem_session_summary` (~490).
121
-
122
- > La memoria del framework (`.ancleto/memory.db`) es independiente de engram: funciona sin él. En los tiers `minimo` y `gratis` este overhead pesa proporcionalmente más que en `normal`.
118
+ - Instalá con `--no-mcp` si no querés ninguno.
119
+ - Evitá `--with-engram` salvo que uses esa memoria: es el que más pesa (9× la memoria propia).
120
+ - Podés deshabilitar cualquier servidor en tu `opencode.json`: `"mcp": { "engram": { "enabled": false } }`.
123
121
 
124
122
  ---
125
123
 
@@ -192,6 +190,8 @@ ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
192
190
  ancleto memory doctor [--rebuild]
193
191
  # Diagnostica la base de memoria y reconstruye el índice FTS5
194
192
 
193
+ ancleto mcp # Servidor MCP de memoria propia (stdio): lo consume tu IDE
194
+
195
195
  ancleto discovery [--compress] [--include <glob>] [--ignore <glob>] [--token-budget <n>]
196
196
  # Empaqueta el repo con Repomix según tu tier
197
197
  ```
@@ -200,17 +200,25 @@ ancleto discovery [--compress] [--include <glob>] [--ignore <glob>] [--token-bud
200
200
 
201
201
  ## Motor de memoria persistente
202
202
 
203
- No hay bases de datos vectoriales: es **SQLite nativo** con búsqueda full-text (FTS5) y ranking BM25.
203
+ No hay bases de datos vectoriales: es **SQLite nativo** con búsqueda full-text (FTS5) y ranking BM25, en `.ancleto/memory.db`.
204
204
 
205
- El LLM dispone de tres herramientas:
205
+ Funciona en tres piezas que comparten el mismo archivo:
206
+
207
+ | Pieza | Qué hace |
208
+ |---|---|
209
+ | **Almacenamiento** | Reglas y decisiones con supersesión atómica por `memory_key` (una sola activa por clave). |
210
+ | **Tools del LLM** (`ancleto mcp`) | Servidor MCP local, sin dependencias, que expone tres herramientas a tu IDE. |
211
+ | **CLI** | `ancleto memory context` materializa el bloque `<ProjectMemoryRules>` y `ancleto memory doctor` verifica integridad y reconstruye el índice FTS5. |
206
212
 
207
213
  | Herramienta | Función |
208
214
  |---|---|
209
- | `searchMemory` | Recupera contexto de decisiones previas. |
210
- | `recordRule` | Guarda una regla arquitectónica de forma jerárquica (proyecto → feature → tarea). |
211
- | `recordDecision` | Registra el *por qué* de un cambio en el código. |
215
+ | `searchMemory` | Recupera contexto de decisiones y reglas previas (búsqueda léxica BM25). |
216
+ | `recordRule` | Guarda una regla o restricción permanente. |
217
+ | `recordDecision` | Registra el *por qué* de una decisión. |
218
+
219
+ Cada entrada tiene un `scope`: `project` (default, entra en `<ProjectMemoryRules>`), `feature` o `task`. El orquestador inyecta los bloques `<ProjectMemoryRules>` y `<ProjectTopology>` en el system prompt de tu agente desde el día cero —incluso sin reglas previas— para que no repita errores ya resueltos.
212
220
 
213
- El orquestador inyecta los bloques `<ProjectMemoryRules>` y `<ProjectTopology>` en el system prompt de tu agente desde el día cero —incluso sin reglas previas— para que no repita errores ya resueltos.
221
+ > El mismo motor se puede consultar a mano desde la terminal: `ancleto memory context --scope project`.
214
222
 
215
223
  ---
216
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ancleto/spec",
3
- "version": "0.6.13",
3
+ "version": "0.6.16",
4
4
  "description": "Orquestador SDD liviano con subagentes optimizados para costo/tokens",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.js CHANGED
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url'
9
9
  import { homedir, tmpdir } from 'node:os'
10
10
  import { createMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
11
11
  import { memoryDoctor } from '../core/memory/doctor.js'
12
+ import { serveMemoryMcp } from '../core/memory/mcp-server.js'
12
13
  import { writeDiscoveryMap } from '../core/discovery.js'
13
14
  import { readProjectTier, buildRepomixArgs, tierTokenBudget } from '../core/repomix-tier.js'
14
15
  import { showBanner, selectOption, stopBanner } from './ui.js'
@@ -26,8 +27,9 @@ const HELP = `ancleto - orquestador SDD liviano con subagentes optimizados para
26
27
  Uso:
27
28
  ancleto install [--project <dir>] Instala agents/commands/skills en opencode
28
29
  (global por defecto, o en .opencode/ del proyecto)
29
- Configura los MCP engram + caveman por defecto
30
+ Configura el MCP de memoria propia + caveman por defecto
30
31
  ancleto install --no-mcp Igual que install pero sin tocar config MCP
32
+ ancleto install --with-engram Ademas configura el MCP externo engram (memoria opcional)
31
33
  ancleto install --tier <nivel> normal | minimo | gratis (wizard interactivo en TTY)
32
34
  ancleto install --agent <nombre> opencode | vscode | antigravity | cursor | roo (wizard si no esta guardado)
33
35
  ancleto update [--project <dir>] Alias de install (re-instala sobre lo existente)
@@ -42,6 +44,8 @@ Uso:
42
44
  Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
43
45
  ancleto memory doctor [--rebuild] Diagnostica .ancleto/memory.db (integridad, FTS5, unicidad)
44
46
  y reconstruye el indice FTS5 con --rebuild
47
+ ancleto mcp Servidor MCP de memoria propia (stdio) para tu IDE
48
+ expone searchMemory, recordRule y recordDecision
45
49
  ancleto check Verifica integridad de archivos instalados vs manifiesto
46
50
  ancleto doctor Diagnostica el entorno (Node, node:sqlite, opencode.json)
47
51
  ancleto --help Esta ayuda
@@ -107,18 +111,27 @@ function resolveBin(name, fallbacks = []) {
107
111
  return null
108
112
  }
109
113
 
110
- function buildDefaultMcp() {
114
+ function resolveSelfCommand() {
115
+ return [process.execPath, join(__dirname, 'index.js'), 'mcp']
116
+ }
117
+
118
+ function buildDefaultMcp({ withEngram = false } = {}) {
111
119
  const mcp = {}
112
120
 
113
- const engramFallbacks = [
114
- join(homedir(), 'go', 'bin', binName('engram')),
115
- join(homedir(), '.local', 'bin', binName('engram'))
116
- ]
117
- const engramBin = resolveBin('engram', engramFallbacks)
118
- if (engramBin) {
119
- mcp.engram = { type: 'local', enabled: true, command: [engramBin, 'mcp', '--tools=agent'] }
120
- } else {
121
- console.warn('ancleto: no se encontro engram (memoria) en PATH; se omitio su MCP')
121
+ // Memoria propia del framework: SQLite (.ancleto/memory.db) expuesta como tools MCP
122
+ mcp['ancleto-memory'] = { type: 'local', enabled: true, command: resolveSelfCommand() }
123
+
124
+ if (withEngram) {
125
+ const engramFallbacks = [
126
+ join(homedir(), 'go', 'bin', binName('engram')),
127
+ join(homedir(), '.local', 'bin', binName('engram'))
128
+ ]
129
+ const engramBin = resolveBin('engram', engramFallbacks)
130
+ if (engramBin) {
131
+ mcp.engram = { type: 'local', enabled: true, command: [engramBin, 'mcp', '--tools=agent'] }
132
+ } else {
133
+ console.warn('ancleto: no se encontro engram en PATH; se omitio su MCP')
134
+ }
122
135
  }
123
136
 
124
137
  const cavemanFallbacks = [
@@ -368,7 +381,7 @@ async function install(args) {
368
381
  const pi = args.indexOf('--project')
369
382
  const project = pi >= 0 ? args[pi + 1] : null
370
383
  const withMcp = !args.includes('--no-mcp')
371
- let mcpMap = withMcp ? buildDefaultMcp() : {}
384
+ let mcpMap = withMcp ? buildDefaultMcp({ withEngram: args.includes('--with-engram') }) : {}
372
385
 
373
386
  const agentFlag = scanAgentFlag(args)
374
387
  let tier = scanTierFlag(args)
@@ -915,6 +928,9 @@ switch (cmd) {
915
928
  case 'doctor':
916
929
  await doctorCommand()
917
930
  break
931
+ case 'mcp':
932
+ await serveMemoryMcp()
933
+ break
918
934
  case '--version':
919
935
  case '-v':
920
936
  console.log(`ancleto ${await packageVersion()}`)
@@ -7,7 +7,7 @@ const MIGRATIONS = [
7
7
  id TEXT PRIMARY KEY,
8
8
  memory_key TEXT NOT NULL,
9
9
  type TEXT NOT NULL CHECK (type IN ('rule', 'decision')),
10
- scope TEXT NOT NULL DEFAULT 'repo',
10
+ scope TEXT NOT NULL DEFAULT 'project',
11
11
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'superseded', 'deleted')),
12
12
  content TEXT NOT NULL,
13
13
  justification TEXT NOT NULL DEFAULT '',
@@ -106,7 +106,7 @@ export function createMemoryEngine(dbPath = defaultMemoryDbPath()) {
106
106
  const type = input.type
107
107
  const content = String(input.content || '')
108
108
  const justification = String(input.justification || '')
109
- const scope = String(input.scope || 'repo')
109
+ const scope = String(input.scope || 'project')
110
110
  if (!memory_key) throw new Error('memory_key es obligatorio')
111
111
  if (type !== 'rule' && type !== 'decision') throw new Error(`type invalido: ${type}`)
112
112
  if (!content.trim()) throw new Error('content es obligatorio')
@@ -0,0 +1,98 @@
1
+ import { createMemoryEngine, defaultMemoryDbPath } from './engine.js'
2
+ import { memoryTools, createMemoryToolHandlers } from './tools.js'
3
+
4
+ const PROTOCOL_VERSION = '2024-11-05'
5
+ const SERVER_INFO = { name: 'ancleto-memory', version: '1.0.0' }
6
+
7
+ export function createMemoryServer({ dbPath = defaultMemoryDbPath() } = {}) {
8
+ let engine = null
9
+ let handlers = null
10
+
11
+ function toolkit() {
12
+ if (!engine) {
13
+ engine = createMemoryEngine(dbPath)
14
+ handlers = createMemoryToolHandlers(engine, { source: 'mcp:ancleto-memory' })
15
+ }
16
+ return handlers
17
+ }
18
+
19
+ const respond = (id, result) => ({ jsonrpc: '2.0', id, result })
20
+ const fail = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } })
21
+
22
+ function handle(message) {
23
+ if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') return null
24
+ const { id, method, params } = message
25
+ if (typeof method === 'string' && method.startsWith('notifications/')) return null
26
+ if (id === undefined || id === null) return null
27
+
28
+ switch (method) {
29
+ case 'initialize':
30
+ return respond(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO })
31
+ case 'ping':
32
+ return respond(id, {})
33
+ case 'tools/list':
34
+ return respond(id, { tools: memoryTools })
35
+ case 'tools/call': {
36
+ const name = params && params.name
37
+ const args = (params && params.arguments) || {}
38
+ const tool = toolkit()[name]
39
+ if (!tool) return fail(id, -32602, `tool desconocida: ${name}`)
40
+ try {
41
+ const result = tool(args)
42
+ return respond(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] })
43
+ } catch (err) {
44
+ return respond(id, { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true })
45
+ }
46
+ }
47
+ default:
48
+ return fail(id, -32601, `metodo no soportado: ${method}`)
49
+ }
50
+ }
51
+
52
+ function close() {
53
+ if (engine) {
54
+ engine.close()
55
+ engine = null
56
+ handlers = null
57
+ }
58
+ }
59
+
60
+ return { handle, close, dbPath }
61
+ }
62
+
63
+ export async function serveMemoryMcp({ dbPath = defaultMemoryDbPath(), input = process.stdin, output = process.stdout } = {}) {
64
+ const server = createMemoryServer({ dbPath })
65
+ const write = (obj) => output.write(JSON.stringify(obj) + '\n')
66
+
67
+ let stopped = false
68
+ const stop = () => {
69
+ if (stopped) return
70
+ stopped = true
71
+ server.close()
72
+ try { input.pause() } catch {}
73
+ }
74
+
75
+ input.setEncoding('utf8')
76
+ let buffer = ''
77
+ input.on('data', (chunk) => {
78
+ buffer += chunk
79
+ const lines = buffer.split('\n')
80
+ buffer = lines.pop()
81
+ for (const line of lines) {
82
+ const text = line.trim()
83
+ if (!text) continue
84
+ let message
85
+ try {
86
+ message = JSON.parse(text)
87
+ } catch {
88
+ write({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'JSON invalido' } })
89
+ continue
90
+ }
91
+ const response = server.handle(message)
92
+ if (response) write(response)
93
+ }
94
+ })
95
+ input.on('end', stop)
96
+ process.on('SIGINT', () => { stop(); process.exit(0) })
97
+ process.on('SIGTERM', () => { stop(); process.exit(0) })
98
+ }
@@ -2,6 +2,12 @@ import { createMemoryEngine } from './engine.js'
2
2
 
3
3
  const KEY_DESCRIPTION = 'Clave conceptual estable (ej. "api-error-format"). Reusala para actualizar: la nueva version supersede automaticamente la anterior.'
4
4
 
5
+ const SCOPE_SCHEMA = {
6
+ type: 'string',
7
+ enum: ['project', 'feature', 'task'],
8
+ description: 'Ambito: project (default, entra en <ProjectMemoryRules>), feature o task para algo mas acotado.'
9
+ }
10
+
5
11
  const SEARCH_SCHEMA = {
6
12
  type: 'object',
7
13
  properties: {
@@ -19,7 +25,7 @@ const RECORD_RULE_SCHEMA = {
19
25
  memory_key: { type: 'string', description: KEY_DESCRIPTION },
20
26
  content: { type: 'string', description: 'Texto de la regla.' },
21
27
  justification: { type: 'string', description: 'Por que existe esta regla.' },
22
- scope: { type: 'string', description: 'Ambito de aplicacion (default "repo").' }
28
+ scope: SCOPE_SCHEMA
23
29
  },
24
30
  required: ['memory_key', 'content'],
25
31
  additionalProperties: false
@@ -31,7 +37,7 @@ const RECORD_DECISION_SCHEMA = {
31
37
  memory_key: { type: 'string', description: KEY_DESCRIPTION },
32
38
  content: { type: 'string', description: 'La decision tomada.' },
33
39
  justification: { type: 'string', description: 'Justificacion de la decision.' },
34
- scope: { type: 'string', description: 'Ambito de aplicacion (default "repo").' }
40
+ scope: SCOPE_SCHEMA
35
41
  },
36
42
  required: ['memory_key', 'content'],
37
43
  additionalProperties: false