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 +82 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/config.js +100 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/llm.js +158 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/memory.js +180 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/skills.js +32 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/server.js +163 -0
- package/dist/server.js.map +1 -0
- package/dist/skills/chat.js +22 -0
- package/dist/skills/chat.js.map +1 -0
- package/dist/skills/memory-recall.js +30 -0
- package/dist/skills/memory-recall.js.map +1 -0
- package/package.json +47 -0
- package/src/ui/chat.html +123 -0
- package/src/wizard/index.html +54 -0
- package/src/wizard/wizard.js +191 -0
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
|
package/dist/cli.js.map
ADDED
|
@@ -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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"}
|
package/dist/core/llm.js
ADDED
|
@@ -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":"AAAA;;;;;;;GAOG;AAoBH,MAAM,SAAU,SAAQ,KAAK;IACG;IAA5B,YAA4B,MAAc,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QADS,WAAM,GAAN,MAAM,CAAQ;QAEtC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC5B,CAAC;CACJ;AAED,SAAS,WAAW,CAAC,QAAsB;IACvC,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,IAAI,GAAiB,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ;YAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;;YAChD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;AACtD,CAAC;AAED,kCAAkC;AAElC,MAAM,eAAe;IAEW;IAAgC;IADnD,QAAQ,GAAgB,WAAW,CAAC;IAC7C,YAA4B,KAAa,EAAmB,MAAc;QAA9C,UAAK,GAAL,KAAK,CAAQ;QAAmB,WAAM,GAAN,MAAM,CAAQ;IAAG,CAAC;IAE9E,KAAK,CAAC,IAAI,CAAC,QAAsB;QAC7B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,uCAAuC,EAAE;YAC7D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACL,cAAc,EAAE,kBAAkB;gBAClC,WAAW,EAAE,IAAI,CAAC,MAAM;gBACxB,mBAAmB,EAAE,YAAY;aACpC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,IAAI;gBAChB,MAAM,EAAE,MAAM,IAAI,SAAS;gBAC3B,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;aACpE,CAAC;SACL,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACV,MAAM,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAG7B,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO;aACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;aAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,IAAI,CAAC,EAAE,CAAC,CAAC;QACd,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK;YAC1B,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa;YACpD,CAAC,CAAC,SAAS,CAAC;QAChB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC1C,CAAC;CACJ;AAED,uDAAuD;AAEvD,MAAM,iBAAiB;IAEC;IACA;IACC;IACA;IAJrB,YACoB,QAAqB,EACrB,KAAa,EACZ,MAAc,EACd,OAAe;QAHhB,aAAQ,GAAR,QAAQ,CAAa;QACrB,UAAK,GAAL,KAAK,CAAQ;QACZ,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAQ;IACjC,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,QAAsB;QAC7B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,mBAAmB,EAAE;YACxD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACL,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;aACzC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;gBACrE,UAAU,EAAE,IAAI;aACnB,CAAC;SACL,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACV,MAAM,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC3F,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAG7B,CAAC;QACF,OAAO;YACH,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;YAC/C,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,YAAY;SACxC,CAAC;IACN,CAAC;CACJ;AAED,oDAAoD;AAEpD,MAAM,YAAY;IAEc;IAAgC;IADnD,QAAQ,GAAgB,QAAQ,CAAC;IAC1C,YAA4B,KAAa,EAAmB,OAAe;QAA/C,UAAK,GAAL,KAAK,CAAQ;QAAmB,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAE/E,KAAK,CAAC,IAAI,CAAC,QAAsB;QAC7B,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,EAAE;YACnE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,MAAM,EAAE,KAAK;gBACb,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;aACxE,CAAC;SACL,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACV,MAAM,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAuC,CAAC;QACtE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,IAAI,EAAE,EAAE,CAAC;IACpD,CAAC;CACJ;AAED,iCAAiC;AAEjC,MAAM,UAAU,cAAc,CAAC,MAAuB;IAClD,QAAQ,MAAM,CAAC,WAAW,EAAE,CAAC;QACzB,KAAK,WAAW;YACZ,OAAO,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/D,KAAK,QAAQ;YACT,OAAO,IAAI,iBAAiB,CACxB,QAAQ,EACR,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,MAAM,EACb,2BAA2B,CAC9B,CAAC;QACN,KAAK,MAAM;YACP,OAAO,IAAI,iBAAiB,CACxB,MAAM,EACN,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,MAAM,EACb,gCAAgC,CACnC,CAAC;QACN,KAAK,QAAQ;YACT,OAAO,IAAI,YAAY,CACnB,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,aAAa,IAAI,wBAAwB,CACnD,CAAC;IACV,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,aAAa;IAGO;IAFpB,QAAQ,GAAgB,WAAW,CAAC;IACpC,KAAK,GAAG,MAAM,CAAC;IACxB,YAA6B,SAA0C;QAA1C,cAAS,GAAT,SAAS,CAAiC;IAAG,CAAC;IAC3E,KAAK,CAAC,IAAI,CAAC,QAAsB;QAC7B,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;QACjF,MAAM,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QACxE,OAAO,EAAE,OAAO,EAAE,UAAU,QAAQ,EAAE,OAAO,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAC5E,CAAC;CACJ"}
|
|
@@ -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"}
|