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/dist/server.js ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Servidor Fastify local. Puerto 7777 (configurable con MUNDOGIRU_PORT).
3
+ *
4
+ * Rutas:
5
+ * GET / → redirige a /wizard si no hay config, sino /chat
6
+ * GET /wizard → wizard estático
7
+ * GET /chat → UI de chat
8
+ * GET /api/health → { ok: true, hasConfig }
9
+ * POST /api/config → recibe config del wizard, cifra y guarda
10
+ * GET /api/config/status → { hasConfig, userName, agentName, provider }
11
+ * POST /api/chat → { message } → { reply, skill }
12
+ * GET /api/memory/proposals → pendientes
13
+ * POST /api/memory/proposals/:id/approve | /reject
14
+ */
15
+ import staticPlugin from "@fastify/static";
16
+ import Fastify from "fastify";
17
+ import { mkdirSync } from "node:fs";
18
+ import { readFile } from "node:fs/promises";
19
+ import { dirname, join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { configExists, defaultModelFor, getConfigPaths, loadConfig, saveConfig, } from "./core/config.js";
22
+ import { buildLLMClient } from "./core/llm.js";
23
+ import { appendChatMessage, approveProposal, getDb, listPendingProposals, recentChatMessages, rejectProposal, } from "./core/memory.js";
24
+ import { SkillRegistry } from "./core/skills.js";
25
+ import { chatSkill } from "./skills/chat.js";
26
+ import { memoryRecallSkill } from "./skills/memory-recall.js";
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ function buildRegistry() {
29
+ const registry = new SkillRegistry();
30
+ registry.register(memoryRecallSkill);
31
+ registry.setFallback(chatSkill);
32
+ return registry;
33
+ }
34
+ export async function buildServer() {
35
+ const app = Fastify({ logger: false });
36
+ const state = {
37
+ config: await loadConfig(),
38
+ llm: null,
39
+ registry: buildRegistry(),
40
+ };
41
+ if (state.config)
42
+ state.llm = buildLLMClient(state.config);
43
+ // pre-crea data dir para SQLite
44
+ mkdirSync(dirname(getConfigPaths().db), { recursive: true });
45
+ // dist/ está en build, src/ en dev. __dirname apunta a dist/ (build) o src/ (tsx).
46
+ // Tanto wizard como ui viven una carpeta arriba bajo el mismo nombre.
47
+ // En build, copiamos las carpetas estáticas a dist/ via tsc → ver `files` en package.json.
48
+ // En dev, __dirname=src/, así que `..` no aplica — el `srcRoot` real es __dirname.
49
+ // En build: __dirname = .../mundogiru-agent/dist → estáticos viven en ../src
50
+ // En dev : __dirname = .../mundogiru-agent/src → estáticos viven aquí
51
+ const dirParts = __dirname.split(/[\\/]/);
52
+ const inDist = dirParts[dirParts.length - 1] === "dist";
53
+ const srcRoot = process.env.MUNDOGIRU_STATIC_ROOT ?? (inDist ? join(__dirname, "..", "src") : __dirname);
54
+ await app.register(staticPlugin, {
55
+ root: srcRoot,
56
+ prefix: "/static/",
57
+ decorateReply: false,
58
+ });
59
+ app.get("/", async (_req, reply) => {
60
+ const has = await configExists();
61
+ return reply.redirect(has ? "/chat" : "/wizard");
62
+ });
63
+ app.get("/wizard", async (_req, reply) => {
64
+ const html = await readFile(join(srcRoot, "wizard", "index.html"), "utf8");
65
+ return reply.type("text/html; charset=utf-8").send(html);
66
+ });
67
+ app.get("/wizard/wizard.js", async (_req, reply) => {
68
+ const js = await readFile(join(srcRoot, "wizard", "wizard.js"), "utf8");
69
+ return reply.type("application/javascript; charset=utf-8").send(js);
70
+ });
71
+ app.get("/chat", async (_req, reply) => {
72
+ const html = await readFile(join(srcRoot, "ui", "chat.html"), "utf8");
73
+ return reply.type("text/html; charset=utf-8").send(html);
74
+ });
75
+ app.get("/api/health", async () => ({
76
+ ok: true,
77
+ hasConfig: state.config !== null,
78
+ provider: state.config?.llmProvider ?? null,
79
+ }));
80
+ app.get("/api/config/status", async () => {
81
+ if (!state.config)
82
+ return { hasConfig: false };
83
+ return {
84
+ hasConfig: true,
85
+ userName: state.config.userName,
86
+ agentName: state.config.agentName,
87
+ provider: state.config.llmProvider,
88
+ model: state.config.llmModel,
89
+ };
90
+ });
91
+ app.post("/api/config", async (req, reply) => {
92
+ const body = req.body;
93
+ if (!body || !body.userName || !body.llmProvider) {
94
+ return reply.code(400).send({ error: "Faltan campos requeridos." });
95
+ }
96
+ if (body.llmProvider !== "ollama" && !body.apiKey) {
97
+ return reply.code(400).send({ error: "Falta API key." });
98
+ }
99
+ const config = {
100
+ userName: body.userName.trim(),
101
+ agentName: (body.agentName || "Mundo").trim(),
102
+ llmProvider: body.llmProvider,
103
+ llmModel: (body.llmModel || defaultModelFor(body.llmProvider)).trim(),
104
+ apiKey: body.apiKey || "",
105
+ ollamaBaseUrl: body.ollamaBaseUrl?.trim(),
106
+ createdAt: new Date().toISOString(),
107
+ };
108
+ await saveConfig(config);
109
+ state.config = config;
110
+ state.llm = buildLLMClient(config);
111
+ return { ok: true };
112
+ });
113
+ app.post("/api/chat", async (req, reply) => {
114
+ if (!state.config || !state.llm) {
115
+ return reply.code(409).send({ error: "Configura el agente primero en /wizard." });
116
+ }
117
+ const message = (req.body?.message ?? "").toString().trim();
118
+ if (!message) {
119
+ return reply.code(400).send({ error: "Mensaje vacío." });
120
+ }
121
+ const db = getDb();
122
+ const history = recentChatMessages(db, 20).map((row) => ({
123
+ role: row.role,
124
+ content: row.content,
125
+ }));
126
+ const result = await state.registry.run({
127
+ db,
128
+ llm: state.llm,
129
+ config: state.config,
130
+ userMessage: message,
131
+ history,
132
+ });
133
+ // memory-recall skill no persiste turnos; lo persistimos aquí para mantenerlos en historial.
134
+ if (result.skill !== "chat") {
135
+ appendChatMessage(db, "user", message);
136
+ appendChatMessage(db, "assistant", result.reply);
137
+ }
138
+ return { reply: result.reply, skill: result.skill };
139
+ });
140
+ app.get("/api/memory/proposals", async () => {
141
+ const db = getDb();
142
+ return { proposals: listPendingProposals(db) };
143
+ });
144
+ app.post("/api/memory/proposals/:id/approve", async (req, reply) => {
145
+ const record = approveProposal(getDb(), req.params.id);
146
+ if (!record)
147
+ return reply.code(404).send({ error: "Propuesta no encontrada." });
148
+ return { record };
149
+ });
150
+ app.post("/api/memory/proposals/:id/reject", async (req, reply) => {
151
+ const ok = rejectProposal(getDb(), req.params.id);
152
+ if (!ok)
153
+ return reply.code(404).send({ error: "Propuesta no encontrada." });
154
+ return { ok };
155
+ });
156
+ return app;
157
+ }
158
+ export async function startServer(port, host = "127.0.0.1") {
159
+ const app = await buildServer();
160
+ await app.listen({ port, host });
161
+ return app;
162
+ }
163
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Skill de chat general: fallback que llama al LLM con la conversación.
3
+ */
4
+ import { appendChatMessage } from "../core/memory.js";
5
+ export const chatSkill = {
6
+ name: "chat",
7
+ description: "Conversación general con el LLM.",
8
+ match: () => true, // se usa como fallback
9
+ async handle(ctx) {
10
+ const systemPrompt = `Eres ${ctx.config.agentName}, un asistente IA local de ${ctx.config.userName}.\n` +
11
+ `Responde en español, breve y útil. No inventes datos.`;
12
+ const response = await ctx.llm.chat([
13
+ { role: "system", content: systemPrompt },
14
+ ...ctx.history,
15
+ { role: "user", content: ctx.userMessage },
16
+ ]);
17
+ appendChatMessage(ctx.db, "user", ctx.userMessage);
18
+ appendChatMessage(ctx.db, "assistant", response.content);
19
+ return response.content;
20
+ },
21
+ };
22
+ //# sourceMappingURL=chat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.js","sourceRoot":"","sources":["../../src/skills/chat.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,MAAM,CAAC,MAAM,SAAS,GAAU;IAC5B,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,kCAAkC;IAC/C,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,uBAAuB;IAC1C,KAAK,CAAC,MAAM,CAAC,GAAiB;QAC1B,MAAM,YAAY,GACd,QAAQ,GAAG,CAAC,MAAM,CAAC,SAAS,8BAA8B,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK;YAClF,uDAAuD,CAAC;QAC5D,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YAChC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE;YACzC,GAAG,GAAG,CAAC,OAAO;YACd,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,WAAW,EAAE;SAC7C,CAAC,CAAC;QACH,iBAAiB,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;QACnD,iBAAiB,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzD,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC5B,CAAC;CACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Skill de recuperación de memoria.
3
+ *
4
+ * Triggers en el mensaje del usuario: "recuerda", "recuerdas", "qué sabes de", "memoria".
5
+ */
6
+ import { recallMemory } from "../core/memory.js";
7
+ const TRIGGERS = [
8
+ /\brecuerdas?\b/i,
9
+ /\bque\s+sabes\s+de\b/i,
10
+ /\bqué\s+sabes\s+de\b/i,
11
+ /\bmemoria\b/i,
12
+ ];
13
+ export const memoryRecallSkill = {
14
+ name: "memory-recall",
15
+ description: "Busca en la memoria persistente del agente.",
16
+ match(ctx) {
17
+ return TRIGGERS.some((rx) => rx.test(ctx.userMessage));
18
+ },
19
+ async handle(ctx) {
20
+ const records = recallMemory(ctx.db, ctx.userMessage, 5);
21
+ if (records.length === 0) {
22
+ return "No tengo nada en memoria sobre eso todavía. Cuéntamelo y te lo guardo.";
23
+ }
24
+ const bullets = records
25
+ .map((r) => `• ${r.content} _(${r.type}, importancia ${r.importance.toFixed(2)})_`)
26
+ .join("\n");
27
+ return `Esto es lo que tengo guardado:\n\n${bullets}`;
28
+ },
29
+ };
30
+ //# sourceMappingURL=memory-recall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-recall.js","sourceRoot":"","sources":["../../src/skills/memory-recall.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,MAAM,QAAQ,GAAG;IACb,iBAAiB;IACjB,uBAAuB;IACvB,uBAAuB;IACvB,cAAc;CACjB,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAU;IACpC,IAAI,EAAE,eAAe;IACrB,WAAW,EAAE,6CAA6C;IAC1D,KAAK,CAAC,GAAiB;QACnB,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,GAAiB;QAC1B,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QACzD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,wEAAwE,CAAC;QACpF,CAAC;QACD,MAAM,OAAO,GAAG,OAAO;aAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,IAAI,iBAAiB,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;aAClF,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,qCAAqC,OAAO,EAAE,CAAC;IAC1D,CAAC;CACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "mundogiru-agent",
3
+ "version": "0.1.0",
4
+ "description": "Agente IA local Mundo G.I.R.U — para usuarios no técnicos. npx mundogiru-agent.",
5
+ "license": "MIT",
6
+ "author": "iaflashelite.com",
7
+ "homepage": "https://github.com/darkjarviselit/mundogiru-agent",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/darkjarviselit/mundogiru-agent.git"
11
+ },
12
+ "type": "module",
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "bin": {
17
+ "mundogiru-agent": "./dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist/",
21
+ "src/wizard/",
22
+ "src/ui/",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "dev": "tsx src/cli.ts",
27
+ "build": "tsc",
28
+ "start": "node dist/cli.js",
29
+ "test": "vitest run",
30
+ "typecheck": "tsc --noEmit",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "dependencies": {
34
+ "fastify": "^5.2.0",
35
+ "@fastify/static": "^8.0.0",
36
+ "@fastify/cors": "^11.0.0",
37
+ "better-sqlite3": "^11.7.0",
38
+ "open": "^10.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/better-sqlite3": "^7.6.13",
42
+ "@types/node": "^22.10.0",
43
+ "tsx": "^4.19.2",
44
+ "typescript": "^5.7.2",
45
+ "vitest": "^2.1.8"
46
+ }
47
+ }
@@ -0,0 +1,123 @@
1
+ <!doctype html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Mundo G.I.R.U</title>
7
+ <style>
8
+ :root {
9
+ --bg:#0f172a; --card:#1e293b; --border:#334155;
10
+ --fg:#f1f5f9; --muted:#94a3b8; --accent:#22d3ee;
11
+ --bubble-user:#22d3ee; --bubble-assistant:#1e293b;
12
+ }
13
+ * { box-sizing:border-box; }
14
+ html, body { margin:0; height:100%; background:var(--bg); color:var(--fg);
15
+ font:15px/1.5 -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif; }
16
+ .layout { display:flex; flex-direction:column; height:100vh; }
17
+ header { padding:16px 24px; border-bottom:1px solid var(--border); display:flex;
18
+ align-items:center; justify-content:space-between; }
19
+ header h1 { font-size:18px; margin:0; }
20
+ header .meta { color:var(--muted); font-size:12px; }
21
+ main { flex:1; overflow-y:auto; padding:24px; }
22
+ .bubble { max-width:720px; margin:0 auto 12px; padding:12px 16px; border-radius:14px;
23
+ line-height:1.55; white-space:pre-wrap; word-wrap:break-word; }
24
+ .bubble.user { background:var(--bubble-user); color:#0f172a; margin-left:auto; }
25
+ .bubble.assistant { background:var(--bubble-assistant); border:1px solid var(--border); }
26
+ .bubble .tag { font-size:10px; color:var(--muted); letter-spacing:0.18em;
27
+ text-transform:uppercase; margin-bottom:4px; }
28
+ footer { padding:16px 24px; border-top:1px solid var(--border); }
29
+ .composer { display:flex; gap:8px; max-width:720px; margin:0 auto; }
30
+ .composer textarea { flex:1; resize:none; padding:12px; background:#0b1220;
31
+ border:1px solid var(--border); border-radius:10px; color:var(--fg); font:inherit;
32
+ outline:none; min-height:48px; max-height:160px; }
33
+ .composer textarea:focus { border-color:var(--accent); }
34
+ .composer button { background:var(--accent); color:#0f172a; border:0; padding:0 18px;
35
+ border-radius:10px; font-weight:700; cursor:pointer; }
36
+ .composer button:disabled { opacity:0.5; cursor:wait; }
37
+ .muted { color:var(--muted); }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div class="layout">
42
+ <header>
43
+ <h1 id="title">Mundo G.I.R.U</h1>
44
+ <span class="meta" id="meta">Conectando…</span>
45
+ </header>
46
+ <main id="log"></main>
47
+ <footer>
48
+ <div class="composer">
49
+ <textarea id="input" rows="2" placeholder="Escribe tu mensaje…"></textarea>
50
+ <button id="send">Enviar</button>
51
+ </div>
52
+ </footer>
53
+ </div>
54
+ <script>
55
+ (async function () {
56
+ const log = document.getElementById("log");
57
+ const input = document.getElementById("input");
58
+ const send = document.getElementById("send");
59
+ const meta = document.getElementById("meta");
60
+ const title = document.getElementById("title");
61
+
62
+ function appendBubble(role, text, tag) {
63
+ const div = document.createElement("div");
64
+ div.className = "bubble " + role;
65
+ if (tag) {
66
+ const t = document.createElement("div");
67
+ t.className = "tag"; t.textContent = tag;
68
+ div.appendChild(t);
69
+ }
70
+ const span = document.createElement("div");
71
+ span.textContent = text;
72
+ div.appendChild(span);
73
+ log.appendChild(div);
74
+ log.scrollTop = log.scrollHeight;
75
+ }
76
+
77
+ try {
78
+ const res = await fetch("/api/config/status");
79
+ const json = await res.json();
80
+ if (!json.hasConfig) { window.location.href = "/wizard"; return; }
81
+ title.textContent = json.agentName + " — para " + json.userName;
82
+ meta.textContent = json.provider + " · " + json.model;
83
+ appendBubble("assistant", "Hola " + json.userName + ", soy " + json.agentName + ". ¿En qué te ayudo?");
84
+ } catch (e) {
85
+ meta.textContent = "Sin conexión";
86
+ }
87
+
88
+ async function submit() {
89
+ const text = input.value.trim();
90
+ if (!text) return;
91
+ appendBubble("user", text);
92
+ input.value = "";
93
+ send.disabled = true;
94
+ try {
95
+ const res = await fetch("/api/chat", {
96
+ method: "POST",
97
+ headers: { "content-type": "application/json" },
98
+ body: JSON.stringify({ message: text }),
99
+ });
100
+ if (!res.ok) {
101
+ const err = await res.json().catch(function () { return { error: "Error desconocido." }; });
102
+ appendBubble("assistant", "Error: " + (err.error || "no se pudo procesar."));
103
+ return;
104
+ }
105
+ const json = await res.json();
106
+ appendBubble("assistant", json.reply, json.skill !== "chat" ? json.skill : "");
107
+ } catch (e) {
108
+ appendBubble("assistant", "Error de red: " + e.message);
109
+ } finally {
110
+ send.disabled = false;
111
+ input.focus();
112
+ }
113
+ }
114
+
115
+ send.onclick = submit;
116
+ input.addEventListener("keydown", function (e) {
117
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
118
+ });
119
+ input.focus();
120
+ })();
121
+ </script>
122
+ </body>
123
+ </html>
@@ -0,0 +1,54 @@
1
+ <!doctype html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Mundo G.I.R.U — Configurar</title>
7
+ <style>
8
+ :root {
9
+ --bg:#0f172a; --card:#1e293b; --border:#334155;
10
+ --fg:#f1f5f9; --muted:#94a3b8; --accent:#22d3ee; --accent-2:#06b6d4;
11
+ }
12
+ * { box-sizing:border-box; }
13
+ html, body { margin:0; padding:0; background:var(--bg); color:var(--fg);
14
+ font:15px/1.5 -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif; }
15
+ .wrap { max-width:640px; margin:40px auto; padding:0 24px; }
16
+ .card { background:var(--card); border:1px solid var(--border); border-radius:16px;
17
+ padding:28px; box-shadow:0 24px 48px -24px rgba(0,0,0,0.5); }
18
+ h1 { font-size:26px; margin:0 0 4px; letter-spacing:-0.5px; }
19
+ .muted { color:var(--muted); }
20
+ .step-tag { color:var(--accent); font-size:12px; font-weight:700; letter-spacing:0.18em;
21
+ text-transform:uppercase; margin-bottom:8px; }
22
+ label { display:block; font-size:13px; color:var(--muted); margin:14px 0 6px; font-weight:600; }
23
+ input, select { width:100%; padding:11px 14px; background:#0b1220; border:1px solid var(--border);
24
+ border-radius:10px; color:var(--fg); font:inherit; outline:none; }
25
+ input:focus, select:focus { border-color:var(--accent); }
26
+ .btn { display:inline-flex; align-items:center; gap:8px; padding:11px 18px;
27
+ border-radius:10px; border:0; font:inherit; font-weight:600; cursor:pointer; }
28
+ .btn-primary { background:var(--accent); color:#0f172a; }
29
+ .btn-primary:hover { background:var(--accent-2); }
30
+ .btn-ghost { background:transparent; color:var(--fg); }
31
+ .btn-ghost:hover { background:#0b1220; }
32
+ .row { display:flex; justify-content:space-between; align-items:center; margin-top:24px; }
33
+ .providers { display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; margin-top:8px; }
34
+ .provider-pill { padding:14px; background:#0b1220; border:1px solid var(--border);
35
+ border-radius:10px; text-align:center; cursor:pointer; font-weight:600; }
36
+ .provider-pill.active { border-color:var(--accent); color:var(--accent); }
37
+ .error { color:#f87171; font-size:13px; margin-top:10px; min-height:18px; }
38
+ .success { padding:14px; background:#064e3b; border:1px solid #10b981; border-radius:10px;
39
+ color:#a7f3d0; margin-top:8px; }
40
+ .hint { font-size:12px; color:var(--muted); margin-top:4px; }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <div class="wrap">
45
+ <div class="card">
46
+ <div id="container"></div>
47
+ </div>
48
+ <div class="row" style="justify-content:center; margin-top:20px;">
49
+ <span class="muted">Mundo G.I.R.U · 100% local</span>
50
+ </div>
51
+ </div>
52
+ <script src="/wizard/wizard.js"></script>
53
+ </body>
54
+ </html>
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Wizard de configuración inicial — JS plano sin framework.
3
+ * 4 pasos: bienvenida → LLM/key → nombre agente → confirmación.
4
+ */
5
+ (function () {
6
+ const state = {
7
+ step: 0,
8
+ userName: "",
9
+ llmProvider: "anthropic",
10
+ llmModel: "",
11
+ apiKey: "",
12
+ ollamaBaseUrl: "http://localhost:11434",
13
+ agentName: "Mundo",
14
+ };
15
+
16
+ const PROVIDERS = [
17
+ { id: "anthropic", label: "Anthropic (Claude)", defaultModel: "claude-sonnet-4-6", needsKey: true },
18
+ { id: "openai", label: "OpenAI (GPT)", defaultModel: "gpt-4o-mini", needsKey: true },
19
+ { id: "groq", label: "Groq (Llama rápido)", defaultModel: "llama-3.3-70b-versatile", needsKey: true },
20
+ { id: "ollama", label: "Ollama (local, gratis)", defaultModel: "llama3.2", needsKey: false },
21
+ ];
22
+
23
+ const root = document.getElementById("container");
24
+
25
+ function render() {
26
+ switch (state.step) {
27
+ case 0: return renderStep0();
28
+ case 1: return renderStep1();
29
+ case 2: return renderStep2();
30
+ case 3: return renderStep3();
31
+ }
32
+ }
33
+
34
+ function html(s, vars) {
35
+ return s.replace(/\{\{(\w+)\}\}/g, function (_, k) {
36
+ return vars[k] == null ? "" : String(vars[k]);
37
+ });
38
+ }
39
+
40
+ function showError(msg) {
41
+ const e = document.getElementById("error");
42
+ if (e) e.textContent = msg || "";
43
+ }
44
+
45
+ function renderStep0() {
46
+ root.innerHTML = `
47
+ <div class="step-tag">Paso 1 de 4</div>
48
+ <h1>Hola, bienvenido a Mundo G.I.R.U</h1>
49
+ <p class="muted">Un asistente IA que vive solo en tu equipo. Tus datos y tu API key no salen de aquí.</p>
50
+ <label>¿Cómo te llamas?</label>
51
+ <input id="userName" placeholder="Ej: María" value="${state.userName}" />
52
+ <div class="error" id="error"></div>
53
+ <div class="row">
54
+ <span></span>
55
+ <button class="btn btn-primary" id="next">Siguiente →</button>
56
+ </div>
57
+ `;
58
+ document.getElementById("next").onclick = () => {
59
+ const name = document.getElementById("userName").value.trim();
60
+ if (!name) return showError("Escribe tu nombre.");
61
+ state.userName = name;
62
+ state.step = 1; render();
63
+ };
64
+ }
65
+
66
+ function renderStep1() {
67
+ const pills = PROVIDERS.map(function (p) {
68
+ const cls = p.id === state.llmProvider ? "provider-pill active" : "provider-pill";
69
+ return `<div class="${cls}" data-id="${p.id}">${p.label}</div>`;
70
+ }).join("");
71
+ const current = PROVIDERS.find(function (p) { return p.id === state.llmProvider; });
72
+ const needsKey = current.needsKey;
73
+ const ollamaInput = !needsKey ? `
74
+ <label>URL de Ollama</label>
75
+ <input id="ollamaBaseUrl" value="${state.ollamaBaseUrl}" />
76
+ <div class="hint">Por defecto Ollama escucha en http://localhost:11434.</div>
77
+ ` : "";
78
+ const keyInput = needsKey ? `
79
+ <label>API key</label>
80
+ <input id="apiKey" type="password" placeholder="sk-..." value="${state.apiKey}" />
81
+ <div class="hint">La key se cifra antes de guardarse en disco.</div>
82
+ ` : "";
83
+ root.innerHTML = `
84
+ <div class="step-tag">Paso 2 de 4</div>
85
+ <h1>Elige tu motor de IA</h1>
86
+ <p class="muted">Puedes cambiarlo más tarde.</p>
87
+ <div class="providers">${pills}</div>
88
+ <label>Modelo</label>
89
+ <input id="llmModel" value="${state.llmModel || current.defaultModel}" />
90
+ ${keyInput}${ollamaInput}
91
+ <div class="error" id="error"></div>
92
+ <div class="row">
93
+ <button class="btn btn-ghost" id="back">← Atrás</button>
94
+ <button class="btn btn-primary" id="next">Siguiente →</button>
95
+ </div>
96
+ `;
97
+ document.querySelectorAll(".provider-pill").forEach(function (el) {
98
+ el.onclick = function () {
99
+ state.llmProvider = el.getAttribute("data-id");
100
+ const p = PROVIDERS.find(function (x) { return x.id === state.llmProvider; });
101
+ state.llmModel = p.defaultModel;
102
+ render();
103
+ };
104
+ });
105
+ document.getElementById("back").onclick = function () { state.step = 0; render(); };
106
+ document.getElementById("next").onclick = function () {
107
+ const provider = PROVIDERS.find(function (p) { return p.id === state.llmProvider; });
108
+ state.llmModel = document.getElementById("llmModel").value.trim() || provider.defaultModel;
109
+ if (provider.needsKey) {
110
+ const key = document.getElementById("apiKey").value.trim();
111
+ if (!key) return showError("Falta la API key.");
112
+ state.apiKey = key;
113
+ } else {
114
+ state.ollamaBaseUrl = document.getElementById("ollamaBaseUrl").value.trim() || state.ollamaBaseUrl;
115
+ state.apiKey = "";
116
+ }
117
+ state.step = 2; render();
118
+ };
119
+ }
120
+
121
+ function renderStep2() {
122
+ root.innerHTML = `
123
+ <div class="step-tag">Paso 3 de 4</div>
124
+ <h1>¿Cómo quieres llamar a tu agente?</h1>
125
+ <p class="muted">Es el nombre con el que te responderá.</p>
126
+ <label>Nombre del agente</label>
127
+ <input id="agentName" value="${state.agentName}" />
128
+ <div class="error" id="error"></div>
129
+ <div class="row">
130
+ <button class="btn btn-ghost" id="back">← Atrás</button>
131
+ <button class="btn btn-primary" id="next">Siguiente →</button>
132
+ </div>
133
+ `;
134
+ document.getElementById("back").onclick = function () { state.step = 1; render(); };
135
+ document.getElementById("next").onclick = function () {
136
+ const name = document.getElementById("agentName").value.trim();
137
+ state.agentName = name || "Mundo";
138
+ state.step = 3; render();
139
+ };
140
+ }
141
+
142
+ function renderStep3() {
143
+ root.innerHTML = `
144
+ <div class="step-tag">Paso 4 de 4</div>
145
+ <h1>Listo para arrancar</h1>
146
+ <p class="muted">Revisa la configuración. Si todo cuadra, pulsa "Guardar y empezar".</p>
147
+ <div style="margin-top:12px; line-height:1.9;">
148
+ <div>👤 Usuario: <b>${state.userName}</b></div>
149
+ <div>🤖 Agente: <b>${state.agentName}</b></div>
150
+ <div>🧠 Motor: <b>${state.llmProvider}</b> · ${state.llmModel}</div>
151
+ </div>
152
+ <div class="error" id="error"></div>
153
+ <div id="success" style="display:none;"></div>
154
+ <div class="row">
155
+ <button class="btn btn-ghost" id="back">← Atrás</button>
156
+ <button class="btn btn-primary" id="save">✅ Guardar y empezar</button>
157
+ </div>
158
+ `;
159
+ document.getElementById("back").onclick = function () { state.step = 2; render(); };
160
+ document.getElementById("save").onclick = async function () {
161
+ showError("");
162
+ try {
163
+ const res = await fetch("/api/config", {
164
+ method: "POST",
165
+ headers: { "content-type": "application/json" },
166
+ body: JSON.stringify({
167
+ userName: state.userName,
168
+ agentName: state.agentName,
169
+ llmProvider: state.llmProvider,
170
+ llmModel: state.llmModel,
171
+ apiKey: state.apiKey,
172
+ ollamaBaseUrl: state.ollamaBaseUrl,
173
+ }),
174
+ });
175
+ if (!res.ok) {
176
+ const err = await res.json().catch(function () { return { error: "Error desconocido." }; });
177
+ return showError(err.error || "Error al guardar.");
178
+ }
179
+ const s = document.getElementById("success");
180
+ s.className = "success";
181
+ s.style.display = "block";
182
+ s.textContent = "¡Configuración guardada! Redirigiendo al chat…";
183
+ setTimeout(function () { window.location.href = "/chat"; }, 800);
184
+ } catch (e) {
185
+ showError("No se pudo contactar con el servidor.");
186
+ }
187
+ };
188
+ }
189
+
190
+ render();
191
+ })();