mundogiru-agent 0.1.0

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 ADDED
@@ -0,0 +1,82 @@
1
+ # Mundo G.I.R.U — Agente IA local
2
+
3
+ > Asistente de IA pensado para personas no técnicas. **Un solo comando**
4
+ > para arrancar. Tus datos y tu API key viven solo en tu equipo.
5
+
6
+ ```bash
7
+ npx mundogiru-agent
8
+ ```
9
+
10
+ Se abre un wizard de 4 pasos en tu navegador (`http://localhost:7777/wizard`)
11
+ y, una vez configurado, una ventana de chat (`/chat`).
12
+
13
+ ## Qué hace
14
+
15
+ - **Asistente IA con memoria local** (SQLite, patrón propose-first).
16
+ - **Multi-proveedor LLM**: Anthropic (Claude), OpenAI (GPT), Groq, Ollama.
17
+ - **100% local**: ningún dato sale de tu máquina.
18
+ - **API keys cifradas** en disco con AES-256-GCM (llave derivada de la
19
+ identidad de tu máquina con scrypt).
20
+ - **Cross-plataforma**: macOS, Linux, Windows (Node.js ≥ 20).
21
+
22
+ ## Requisitos
23
+
24
+ - Node.js 20 o superior.
25
+ - Una API key de tu proveedor preferido (no hace falta si usas Ollama
26
+ local).
27
+
28
+ ## Estructura del proyecto
29
+
30
+ ```
31
+ src/
32
+ ├── cli.ts # Entry point (`bin`)
33
+ ├── server.ts # Fastify (puerto 7777)
34
+ ├── core/
35
+ │ ├── config.ts # Carga/guarda config cifrada
36
+ │ ├── memory.ts # SQLite + propose-first
37
+ │ ├── llm.ts # Cliente multi-proveedor
38
+ │ └── skills.ts # Registry de skills
39
+ ├── skills/
40
+ │ ├── chat.ts # Fallback: LLM directo
41
+ │ └── memory-recall.ts # Búsqueda en memoria
42
+ ├── wizard/ # HTML+JS plano (4 pasos)
43
+ └── ui/chat.html # Interfaz de chat
44
+ tests/ # vitest (config, memory, llm, chat e2e)
45
+ ```
46
+
47
+ ## Comandos
48
+
49
+ ```bash
50
+ npm install
51
+ npm run dev # tsx src/cli.ts (hot reload)
52
+ npm run build # tsc → dist/
53
+ npm test # vitest
54
+ npm run typecheck # tsc --noEmit
55
+ ```
56
+
57
+ ## Configuración persistente
58
+
59
+ - Ubicación: `~/.mundogiru/`
60
+ - `config.enc` — config cifrada AES-256-GCM
61
+ - `data/memory.db` — SQLite (memoria + propuestas + historial chat)
62
+
63
+ ## Variables de entorno
64
+
65
+ | Variable | Default | Descripción |
66
+ |----------|---------|-------------|
67
+ | `MUNDOGIRU_PORT` | `7777` | Puerto del servidor Fastify |
68
+ | `MUNDOGIRU_HOME` | `~/.mundogiru` | Carpeta de datos |
69
+ | `MUNDOGIRU_NO_OPEN` | (vacío) | `1` para no abrir el navegador |
70
+ | `MUNDOGIRU_STATIC_ROOT` | auto | Override de estáticos (dev/test) |
71
+
72
+ ## Privacidad
73
+
74
+ - **Cifrado en reposo**: AES-256-GCM con llave derivada de
75
+ `hostname + username` vía scrypt. Si copias `config.enc` a otra máquina,
76
+ no se puede descifrar.
77
+ - **Sin telemetría**: ninguna petición HTTP saliente excepto al LLM que
78
+ hayas configurado tú.
79
+
80
+ ## Licencia
81
+
82
+ MIT — © iaflashelite.com
package/dist/cli.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point invocado por `npx mundogiru-agent`.
4
+ *
5
+ * Comportamiento:
6
+ * 1. Lee config si existe → decide ruta wizard o chat.
7
+ * 2. Arranca Fastify en localhost:7777 (override con MUNDOGIRU_PORT).
8
+ * 3. Abre el navegador automáticamente con `open` cross-platform.
9
+ * 4. Mensajes en consola para humanos no técnicos.
10
+ */
11
+ import open from "open";
12
+ import process from "node:process";
13
+ import { configExists, getConfigPaths } from "./core/config.js";
14
+ import { startServer } from "./server.js";
15
+ async function main() {
16
+ const port = Number.parseInt(process.env.MUNDOGIRU_PORT ?? "7777", 10);
17
+ const paths = getConfigPaths();
18
+ const hasConfig = await configExists();
19
+ console.log("");
20
+ console.log(" ╭─────────────────────────────────────────────╮");
21
+ console.log(" │ Mundo G.I.R.U — Agente local IA │");
22
+ console.log(" ╰─────────────────────────────────────────────╯");
23
+ console.log("");
24
+ console.log(` Datos: ${paths.home}`);
25
+ console.log(` Puerto: ${port}`);
26
+ console.log("");
27
+ try {
28
+ await startServer(port);
29
+ }
30
+ catch (err) {
31
+ const e = err;
32
+ if (e.code === "EADDRINUSE") {
33
+ console.error(` ✗ El puerto ${port} ya está ocupado.`);
34
+ console.error(` Cierra el proceso anterior o exporta MUNDOGIRU_PORT=<otro>.`);
35
+ process.exit(1);
36
+ }
37
+ throw err;
38
+ }
39
+ const url = `http://localhost:${port}`;
40
+ console.log(` ▸ Abriendo ${url}${hasConfig ? "/chat" : "/wizard"} en tu navegador…`);
41
+ console.log(` ▸ Para cerrar el agente: pulsa Ctrl+C aquí.`);
42
+ console.log("");
43
+ if (process.env.MUNDOGIRU_NO_OPEN !== "1") {
44
+ try {
45
+ await open(url);
46
+ }
47
+ catch {
48
+ console.log(` (No pude abrir el navegador automáticamente — visita ${url} manualmente.)`);
49
+ }
50
+ }
51
+ // Mantener vivo el proceso hasta SIGINT.
52
+ process.on("SIGINT", () => {
53
+ console.log("\n ▸ Cerrando agente. Hasta luego.");
54
+ process.exit(0);
55
+ });
56
+ }
57
+ main().catch((err) => {
58
+ console.error("Error arrancando Mundo G.I.R.U:", err);
59
+ process.exit(1);
60
+ });
61
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,KAAK,UAAU,IAAI;IACf,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;IACvE,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAG,MAAM,YAAY,EAAE,CAAC;IAEvC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,IAAI,CAAC;QACD,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,CAAC,GAAG,GAA4B,CAAC;QACvC,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,iBAAiB,IAAI,mBAAmB,CAAC,CAAC;YACxD,OAAO,CAAC,KAAK,CAAC,iEAAiE,CAAC,CAAC;YACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;QACD,MAAM,GAAG,CAAC;IACd,CAAC;IAED,MAAM,GAAG,GAAG,oBAAoB,IAAI,EAAE,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,mBAAmB,CAAC,CAAC;IACtF,OAAO,CAAC,GAAG,CAAC,+CAA+C,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,GAAG,EAAE,CAAC;QACxC,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,CAAC,GAAG,CAAC,0DAA0D,GAAG,gBAAgB,CAAC,CAAC;QAC/F,CAAC;IACL,CAAC;IAED,yCAAyC;IACzC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACtB,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;AACP,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,GAAG,CAAC,CAAC;IACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Configuración persistente cifrada.
3
+ *
4
+ * Ubicación: ~/.mundogiru/config.enc
5
+ * Cifrado: AES-256-GCM
6
+ * Llave: scrypt(machineId + username, randomSalt, 32)
7
+ *
8
+ * La sal se guarda en claro junto al cifrado. Esto NO defiende contra un
9
+ * atacante con acceso al disco del usuario (la llave es derivable), pero
10
+ * sí defiende contra exfiltración del archivo a otra máquina y previene
11
+ * que la API key aparezca grepable en `~/`.
12
+ */
13
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
14
+ import { promises as fsp } from "node:fs";
15
+ import { hostname, userInfo } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+ import process from "node:process";
18
+ const ALG = "aes-256-gcm";
19
+ const SCRYPT_N = 16384;
20
+ const SCRYPT_R = 8;
21
+ const SCRYPT_P = 1;
22
+ const KEY_LEN = 32;
23
+ const SALT_LEN = 16;
24
+ const IV_LEN = 12;
25
+ export function getConfigPaths() {
26
+ const home = process.env.MUNDOGIRU_HOME ?? join(userInfo().homedir, ".mundogiru");
27
+ return {
28
+ home,
29
+ config: join(home, "config.enc"),
30
+ db: join(home, "data", "memory.db"),
31
+ uiDir: join(home, "ui"),
32
+ };
33
+ }
34
+ function machineIdentity() {
35
+ return `${hostname()}:${userInfo().username}`;
36
+ }
37
+ function deriveKey(salt) {
38
+ return scryptSync(machineIdentity(), salt, KEY_LEN, {
39
+ N: SCRYPT_N,
40
+ r: SCRYPT_R,
41
+ p: SCRYPT_P,
42
+ });
43
+ }
44
+ export async function configExists() {
45
+ try {
46
+ await fsp.access(getConfigPaths().config);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ export async function loadConfig() {
54
+ const { config: path } = getConfigPaths();
55
+ try {
56
+ const blob = await fsp.readFile(path);
57
+ if (blob.length < SALT_LEN + IV_LEN + 16)
58
+ return null;
59
+ const salt = blob.subarray(0, SALT_LEN);
60
+ const iv = blob.subarray(SALT_LEN, SALT_LEN + IV_LEN);
61
+ const tag = blob.subarray(blob.length - 16);
62
+ const ciphertext = blob.subarray(SALT_LEN + IV_LEN, blob.length - 16);
63
+ const key = deriveKey(salt);
64
+ const decipher = createDecipheriv(ALG, key, iv);
65
+ decipher.setAuthTag(tag);
66
+ const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
67
+ return JSON.parse(plain.toString("utf8"));
68
+ }
69
+ catch (err) {
70
+ if (err.code === "ENOENT")
71
+ return null;
72
+ throw err;
73
+ }
74
+ }
75
+ export async function saveConfig(config) {
76
+ const paths = getConfigPaths();
77
+ await fsp.mkdir(dirname(paths.config), { recursive: true });
78
+ const salt = randomBytes(SALT_LEN);
79
+ const iv = randomBytes(IV_LEN);
80
+ const key = deriveKey(salt);
81
+ const cipher = createCipheriv(ALG, key, iv);
82
+ const plain = Buffer.from(JSON.stringify(config), "utf8");
83
+ const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]);
84
+ const tag = cipher.getAuthTag();
85
+ const blob = Buffer.concat([salt, iv, ciphertext, tag]);
86
+ await fsp.writeFile(paths.config, blob, { mode: 0o600 });
87
+ }
88
+ export function defaultModelFor(provider) {
89
+ switch (provider) {
90
+ case "anthropic":
91
+ return "claude-sonnet-4-6";
92
+ case "openai":
93
+ return "gpt-4o-mini";
94
+ case "groq":
95
+ return "llama-3.3-70b-versatile";
96
+ case "ollama":
97
+ return "llama3.2";
98
+ }
99
+ }
100
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Cliente LLM unificado para Anthropic, OpenAI, Groq y Ollama.
3
+ *
4
+ * Sin SDKs externos — `fetch` directo a las APIs HTTP. Ahorra ~10 MB
5
+ * de dependencias en el bundle npx y elimina problemas de versiones.
6
+ *
7
+ * Interfaz: `chat(messages) => { content, totalTokens }`.
8
+ */
9
+ class HttpError extends Error {
10
+ status;
11
+ constructor(status, message) {
12
+ super(message);
13
+ this.status = status;
14
+ this.name = "HttpError";
15
+ }
16
+ }
17
+ function splitSystem(messages) {
18
+ const systemParts = [];
19
+ const rest = [];
20
+ for (const m of messages) {
21
+ if (m.role === "system")
22
+ systemParts.push(m.content);
23
+ else
24
+ rest.push(m);
25
+ }
26
+ return { system: systemParts.join("\n\n"), rest };
27
+ }
28
+ // ---------- Anthropic ----------
29
+ class AnthropicClient {
30
+ model;
31
+ apiKey;
32
+ provider = "anthropic";
33
+ constructor(model, apiKey) {
34
+ this.model = model;
35
+ this.apiKey = apiKey;
36
+ }
37
+ async chat(messages) {
38
+ const { system, rest } = splitSystem(messages);
39
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
40
+ method: "POST",
41
+ headers: {
42
+ "content-type": "application/json",
43
+ "x-api-key": this.apiKey,
44
+ "anthropic-version": "2023-06-01",
45
+ },
46
+ body: JSON.stringify({
47
+ model: this.model,
48
+ max_tokens: 1024,
49
+ system: system || undefined,
50
+ messages: rest.map((m) => ({ role: m.role, content: m.content })),
51
+ }),
52
+ });
53
+ if (!res.ok) {
54
+ throw new HttpError(res.status, `Anthropic ${res.status}: ${await res.text()}`);
55
+ }
56
+ const json = (await res.json());
57
+ const text = json.content
58
+ .filter((c) => c.type === "text")
59
+ .map((c) => c.text)
60
+ .join("");
61
+ const totalTokens = json.usage
62
+ ? json.usage.input_tokens + json.usage.output_tokens
63
+ : undefined;
64
+ return { content: text, totalTokens };
65
+ }
66
+ }
67
+ // ---------- OpenAI / Groq (formato OpenAI) ----------
68
+ class OpenAIStyleClient {
69
+ provider;
70
+ model;
71
+ apiKey;
72
+ baseUrl;
73
+ constructor(provider, model, apiKey, baseUrl) {
74
+ this.provider = provider;
75
+ this.model = model;
76
+ this.apiKey = apiKey;
77
+ this.baseUrl = baseUrl;
78
+ }
79
+ async chat(messages) {
80
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
81
+ method: "POST",
82
+ headers: {
83
+ "content-type": "application/json",
84
+ authorization: `Bearer ${this.apiKey}`,
85
+ },
86
+ body: JSON.stringify({
87
+ model: this.model,
88
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
89
+ max_tokens: 1024,
90
+ }),
91
+ });
92
+ if (!res.ok) {
93
+ throw new HttpError(res.status, `${this.provider} ${res.status}: ${await res.text()}`);
94
+ }
95
+ const json = (await res.json());
96
+ return {
97
+ content: json.choices[0]?.message.content ?? "",
98
+ totalTokens: json.usage?.total_tokens,
99
+ };
100
+ }
101
+ }
102
+ // ---------- Ollama (local, sin API key) ----------
103
+ class OllamaClient {
104
+ model;
105
+ baseUrl;
106
+ provider = "ollama";
107
+ constructor(model, baseUrl) {
108
+ this.model = model;
109
+ this.baseUrl = baseUrl;
110
+ }
111
+ async chat(messages) {
112
+ const res = await fetch(`${this.baseUrl.replace(/\/$/, "")}/api/chat`, {
113
+ method: "POST",
114
+ headers: { "content-type": "application/json" },
115
+ body: JSON.stringify({
116
+ model: this.model,
117
+ stream: false,
118
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
119
+ }),
120
+ });
121
+ if (!res.ok) {
122
+ throw new HttpError(res.status, `Ollama ${res.status}: ${await res.text()}`);
123
+ }
124
+ const json = (await res.json());
125
+ return { content: json.message?.content ?? "" };
126
+ }
127
+ }
128
+ // ---------- factoría ----------
129
+ export function buildLLMClient(config) {
130
+ switch (config.llmProvider) {
131
+ case "anthropic":
132
+ return new AnthropicClient(config.llmModel, config.apiKey);
133
+ case "openai":
134
+ return new OpenAIStyleClient("openai", config.llmModel, config.apiKey, "https://api.openai.com/v1");
135
+ case "groq":
136
+ return new OpenAIStyleClient("groq", config.llmModel, config.apiKey, "https://api.groq.com/openai/v1");
137
+ case "ollama":
138
+ return new OllamaClient(config.llmModel, config.ollamaBaseUrl ?? "http://localhost:11434");
139
+ }
140
+ }
141
+ /**
142
+ * Cliente "stub" determinista para tests: hace echo del último mensaje user.
143
+ */
144
+ export class StubLLMClient {
145
+ responder;
146
+ provider = "anthropic";
147
+ model = "stub";
148
+ constructor(responder) {
149
+ this.responder = responder;
150
+ }
151
+ async chat(messages) {
152
+ if (this.responder)
153
+ return { content: this.responder(messages), totalTokens: 0 };
154
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
155
+ return { content: `[stub] ${lastUser?.content ?? ""}`, totalTokens: 0 };
156
+ }
157
+ }
158
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../../src/core/llm.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Memoria persistente con `better-sqlite3` (SÍNCRONA).
3
+ *
4
+ * Patrón propose-first (copiado de giru-agent):
5
+ * 1. `proposeMemory()` crea fila en `memory_proposals` con status=pending.
6
+ * 2. `approveProposal()` la migra a `memory_records`.
7
+ * 3. `rejectProposal()` la marca rechazada sin tocar `memory_records`.
8
+ *
9
+ * Esto evita que el agente meta cosas en memoria por sorpresa: la UI/skill
10
+ * decide explícitamente qué se confirma.
11
+ */
12
+ import Database from "better-sqlite3";
13
+ import { randomUUID } from "node:crypto";
14
+ import { mkdirSync } from "node:fs";
15
+ import { dirname } from "node:path";
16
+ import { getConfigPaths } from "./config.js";
17
+ const SCHEMA = `
18
+ PRAGMA journal_mode = WAL;
19
+ PRAGMA foreign_keys = ON;
20
+
21
+ CREATE TABLE IF NOT EXISTS memory_records (
22
+ id TEXT PRIMARY KEY,
23
+ type TEXT NOT NULL DEFAULT 'semantic',
24
+ content TEXT NOT NULL,
25
+ importance REAL NOT NULL DEFAULT 0.5,
26
+ created_at INTEGER NOT NULL,
27
+ updated_at INTEGER NOT NULL
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_memory_records_type ON memory_records(type);
31
+ CREATE INDEX IF NOT EXISTS idx_memory_records_updated ON memory_records(updated_at DESC);
32
+
33
+ CREATE TABLE IF NOT EXISTS memory_proposals (
34
+ id TEXT PRIMARY KEY,
35
+ op TEXT NOT NULL,
36
+ type TEXT NOT NULL,
37
+ content TEXT NOT NULL,
38
+ importance REAL NOT NULL DEFAULT 0.5,
39
+ status TEXT NOT NULL DEFAULT 'pending',
40
+ target_id TEXT,
41
+ created_at INTEGER NOT NULL
42
+ );
43
+
44
+ CREATE INDEX IF NOT EXISTS idx_memory_proposals_status ON memory_proposals(status);
45
+
46
+ CREATE TABLE IF NOT EXISTS chat_messages (
47
+ id TEXT PRIMARY KEY,
48
+ role TEXT NOT NULL,
49
+ content TEXT NOT NULL,
50
+ created_at INTEGER NOT NULL
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_created ON chat_messages(created_at DESC);
54
+ `;
55
+ let cached = null;
56
+ export function getDb(path) {
57
+ if (cached)
58
+ return cached;
59
+ const dbPath = path ?? getConfigPaths().db;
60
+ if (dbPath !== ":memory:") {
61
+ mkdirSync(dirname(dbPath), { recursive: true });
62
+ }
63
+ const db = new Database(dbPath);
64
+ db.exec(SCHEMA);
65
+ cached = db;
66
+ return db;
67
+ }
68
+ export function resetDbForTests() {
69
+ if (cached)
70
+ cached.close();
71
+ cached = null;
72
+ }
73
+ // ---------- propose-first ----------
74
+ export function proposeMemory(db, args) {
75
+ const proposal = {
76
+ id: randomUUID(),
77
+ op: args.op ?? "create",
78
+ type: args.type ?? "semantic",
79
+ content: args.content,
80
+ importance: args.importance ?? 0.5,
81
+ status: "pending",
82
+ target_id: args.targetId ?? null,
83
+ created_at: Date.now(),
84
+ };
85
+ db.prepare(`INSERT INTO memory_proposals (id, op, type, content, importance, status, target_id, created_at)
86
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(proposal.id, proposal.op, proposal.type, proposal.content, proposal.importance, proposal.status, proposal.target_id, proposal.created_at);
87
+ return proposal;
88
+ }
89
+ export function listPendingProposals(db) {
90
+ return db
91
+ .prepare(`SELECT * FROM memory_proposals WHERE status = 'pending' ORDER BY created_at DESC`)
92
+ .all();
93
+ }
94
+ export function approveProposal(db, proposalId) {
95
+ const proposal = db
96
+ .prepare(`SELECT * FROM memory_proposals WHERE id = ?`)
97
+ .get(proposalId);
98
+ if (!proposal || proposal.status !== "pending")
99
+ return null;
100
+ const now = Date.now();
101
+ const record = {
102
+ id: randomUUID(),
103
+ type: proposal.type,
104
+ content: proposal.content,
105
+ importance: proposal.importance,
106
+ created_at: now,
107
+ updated_at: now,
108
+ };
109
+ const tx = db.transaction(() => {
110
+ db.prepare(`INSERT INTO memory_records (id, type, content, importance, created_at, updated_at)
111
+ VALUES (?, ?, ?, ?, ?, ?)`).run(record.id, record.type, record.content, record.importance, record.created_at, record.updated_at);
112
+ db.prepare(`UPDATE memory_proposals SET status = 'approved' WHERE id = ?`).run(proposalId);
113
+ });
114
+ tx();
115
+ return record;
116
+ }
117
+ export function rejectProposal(db, proposalId) {
118
+ const result = db
119
+ .prepare(`UPDATE memory_proposals SET status = 'rejected' WHERE id = ? AND status = 'pending'`)
120
+ .run(proposalId);
121
+ return result.changes > 0;
122
+ }
123
+ // ---------- consultas ----------
124
+ const STOPWORDS = new Set([
125
+ "que", "qué", "de", "la", "el", "los", "las", "un", "una", "y", "o", "a",
126
+ "en", "del", "al", "por", "para", "con", "sin", "es", "ser", "esta", "estás",
127
+ "estoy", "tu", "tú", "mi", "mí", "me", "te", "se", "lo", "le", "como", "cómo",
128
+ "cuando", "cuándo", "donde", "dónde", "qué", "quien", "quién",
129
+ "recuerda", "recuerdas", "recordar", "memoria", "sabes", "sabe", "saber",
130
+ "the", "is", "of", "and", "to", "what", "do", "you", "know",
131
+ ]);
132
+ const COMBINING_MARKS_RX = /[̀-ͯ]/g;
133
+ function stripDiacritics(text) {
134
+ return text.normalize("NFD").replace(COMBINING_MARKS_RX, "");
135
+ }
136
+ function tokenize(text) {
137
+ return stripDiacritics(text.toLowerCase())
138
+ .split(/[^a-z0-9ñü]+/i)
139
+ .filter((t) => t.length >= 3 && !STOPWORDS.has(t));
140
+ }
141
+ function normalizeForCompare(text) {
142
+ return stripDiacritics(text.toLowerCase());
143
+ }
144
+ export function recallMemory(db, query, limit = 5) {
145
+ const all = db
146
+ .prepare(`SELECT * FROM memory_records ORDER BY importance DESC, updated_at DESC`)
147
+ .all();
148
+ const tokens = tokenize(query);
149
+ if (tokens.length === 0) {
150
+ // Sin tokens útiles → devolvemos los más importantes recientes.
151
+ return all.slice(0, limit);
152
+ }
153
+ const matched = all.filter((r) => {
154
+ const normalized = normalizeForCompare(r.content);
155
+ return tokens.some((t) => normalized.includes(t));
156
+ });
157
+ return matched.slice(0, limit);
158
+ }
159
+ export function listMemory(db, limit = 50) {
160
+ return db
161
+ .prepare(`SELECT * FROM memory_records ORDER BY updated_at DESC LIMIT ?`)
162
+ .all(limit);
163
+ }
164
+ export function appendChatMessage(db, role, content) {
165
+ const row = {
166
+ id: randomUUID(),
167
+ role,
168
+ content,
169
+ created_at: Date.now(),
170
+ };
171
+ db.prepare(`INSERT INTO chat_messages (id, role, content, created_at) VALUES (?, ?, ?, ?)`).run(row.id, row.role, row.content, row.created_at);
172
+ return row;
173
+ }
174
+ export function recentChatMessages(db, limit = 20) {
175
+ const rows = db
176
+ .prepare(`SELECT * FROM chat_messages ORDER BY created_at DESC, rowid DESC LIMIT ?`)
177
+ .all(limit);
178
+ return rows.reverse();
179
+ }
180
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/core/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Sistema de skills: registro modular en memoria.
3
+ *
4
+ * Cada skill expone `match(ctx)` (boolean) y `handle(ctx)` (Promise<string>).
5
+ * La primera que matchea gana. Si ninguna matchea, usamos el fallback.
6
+ */
7
+ export class SkillRegistry {
8
+ skills = [];
9
+ fallback = null;
10
+ register(skill) {
11
+ this.skills.push(skill);
12
+ }
13
+ setFallback(skill) {
14
+ this.fallback = skill;
15
+ }
16
+ list() {
17
+ return [...this.skills, ...(this.fallback ? [this.fallback] : [])];
18
+ }
19
+ async run(ctx) {
20
+ for (const skill of this.skills) {
21
+ if (skill.match(ctx)) {
22
+ const reply = await skill.handle(ctx);
23
+ return { skill: skill.name, reply };
24
+ }
25
+ }
26
+ if (this.fallback) {
27
+ return { skill: this.fallback.name, reply: await this.fallback.handle(ctx) };
28
+ }
29
+ return { skill: "noop", reply: "No tengo skills registradas todavía." };
30
+ }
31
+ }
32
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/core/skills.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsBH,MAAM,OAAO,aAAa;IACd,MAAM,GAAY,EAAE,CAAC;IACrB,QAAQ,GAAiB,IAAI,CAAC;IAEtC,QAAQ,CAAC,KAAY;QACjB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAED,WAAW,CAAC,KAAY;QACpB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,IAAI;QACA,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAiB;QACvB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9B,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnB,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACtC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;YACxC,CAAC;QACL,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACjF,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC;IAC5E,CAAC;CACJ"}