ratacode 0.2.5

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.
Files changed (58) hide show
  1. package/CREDITS.md +80 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/apreton/README.md +54 -0
  5. package/apreton/handshake.md +33 -0
  6. package/apreton/headless.md +58 -0
  7. package/apreton/mcp.md +221 -0
  8. package/apreton/navegador.md +58 -0
  9. package/bin/instalacion.js +148 -0
  10. package/bin/ratacode.js +1251 -0
  11. package/fabrica/settings.yaml +247 -0
  12. package/mcp/README.md +253 -0
  13. package/mcp/bin/ratacode-mcp.js +215 -0
  14. package/mcp/lib/actividad.js +49 -0
  15. package/mcp/lib/casa.js +126 -0
  16. package/mcp/lib/claves.js +148 -0
  17. package/mcp/lib/espacios.js +117 -0
  18. package/mcp/lib/http.js +184 -0
  19. package/mcp/lib/lectura.js +210 -0
  20. package/mcp/lib/modelos.js +235 -0
  21. package/mcp/lib/nucleo.js +332 -0
  22. package/mcp/lib/registro.js +21 -0
  23. package/mcp/lib/seguridad.js +283 -0
  24. package/mcp/lib/servidor.js +397 -0
  25. package/mcp/lib/tareas.js +406 -0
  26. package/mcp/package.json +22 -0
  27. package/mcp/tunel.mjs +209 -0
  28. package/modos/arquitecto/agent.cordis.yml +120 -0
  29. package/modos/arquitecto/preset.yml +3 -0
  30. package/modos/capataz/agent.cordis.yml +120 -0
  31. package/modos/capataz/preset.yml +3 -0
  32. package/modos/faro/agent.cordis.yml +120 -0
  33. package/modos/faro/preset.yml +3 -0
  34. package/modos/gepeto/agent.cordis.yml +191 -0
  35. package/modos/gepeto/preset.yml +3 -0
  36. package/modos/hero/agent.cordis.yml +198 -0
  37. package/modos/hero/preset.yml +3 -0
  38. package/modos/modo-rata/agent.cordis.yml +198 -0
  39. package/modos/modo-rata/preset.yml +3 -0
  40. package/modos/nex/agent.cordis.yml +213 -0
  41. package/modos/nex/preset.yml +3 -0
  42. package/modos/nex/skills/cordis-plugin-development/SKILL.md +420 -0
  43. package/modos/nex/skills/editing-cordis-compositions/SKILL.md +165 -0
  44. package/modos/pix/agent.cordis.yml +207 -0
  45. package/modos/pix/preset.yml +3 -0
  46. package/modos/tirita/agent.cordis.yml +130 -0
  47. package/modos/tirita/preset.yml +3 -0
  48. package/package.json +49 -0
  49. package/piel/activos/ratacode-emblema.svg +14 -0
  50. package/piel/activos/ratacode-es.js +1331 -0
  51. package/piel/activos/ratacode-identidad.css +50 -0
  52. package/piel/activos/ratacode-piel.css +217 -0
  53. package/piel/activos/ratacode-piel.js +487 -0
  54. package/piel/activos/ratacode-vida.js +424 -0
  55. package/piel/cordis.patch.yml +6 -0
  56. package/piel/lib/cliente.js +1020 -0
  57. package/piel/lib/index.js +1230 -0
  58. package/piel/package.json +36 -0
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ratacode-mcp — el servidor MCP de RATACODE.
4
+ *
5
+ * ratacode-mcp habla MCP por stdio (lo que espera un cliente)
6
+ * ratacode-mcp --status enseña el estado y sale (sin arrancar el motor)
7
+ * ratacode-mcp --http también por Streamable HTTP en 127.0.0.1:<puerto>/mcp/<clave>
8
+ * ratacode-mcp --home <ruta> usa otra casa (por defecto %USERPROFILE%\.ratacode)
9
+ * ratacode-mcp --dsh <ruta> usa otro binario del motor (para pruebas)
10
+ *
11
+ * Por stdio, stdout es del protocolo: aquí TODO lo que contamos va a stderr.
12
+ * Por HTTP, la clave va en la propia URL y se guarda en la casa (nunca en el
13
+ * repositorio). El puerto por defecto es 3778; el tope de tareas por hora es 30.
14
+ *
15
+ * Y CADA TAREA VA ENCERRADA (R25): dentro de las carpetas de `mcp.workspaces`,
16
+ * sin terminal, sin red, sin subagentes y sin guiones, y con la LECTURA
17
+ * encerrada por el gancho `PreToolUse` (mira `lib/lectura.js`). Por eso el HTTP
18
+ * ya NO pide ninguna bandera de aceptación: no hay nada que aceptar.
19
+ */
20
+ import { randomBytes } from 'node:crypto';
21
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
+ import { dirname, join } from 'node:path';
23
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
24
+ import { binDelMotor, resolverCasa } from '../lib/casa.js';
25
+ import { aviso, fallo } from '../lib/registro.js';
26
+ import { montarServidor } from '../lib/servidor.js';
27
+ import { iniciarServidorHttp } from '../lib/http.js';
28
+ import { estadoDeLaCasa } from '../lib/tareas.js';
29
+
30
+ /** Puerto por defecto del transporte HTTP. */
31
+ const PUERTO_HTTP_DEFECTO = 3778;
32
+ /** Tope de tareas por hora por defecto. */
33
+ const TAREAS_POR_HORA_DEFECTO = 30;
34
+
35
+ /** La línea de órdenes. */
36
+ function uso() {
37
+ return [
38
+ 'RATACODE-MCP · el servidor MCP de RATACODE',
39
+ '',
40
+ ' ratacode-mcp [opciones]',
41
+ '',
42
+ ' --home <ruta> dónde vive la casa de RATACODE (por defecto %USERPROFILE%\\.ratacode)',
43
+ ' --dsh <ruta> motor a usar (por defecto, el del paquete instalado)',
44
+ ' --status enseña el estado del MCP y sale',
45
+ ' --http también por Streamable HTTP en 127.0.0.1:<puerto>/mcp/<clave>',
46
+ ' --acepto-lectura-total ya NO hace falta (y no hace nada): la lectura va encerrada',
47
+ ' en las carpetas de `mcp.workspaces`. Se acepta por no romper',
48
+ ' los guiones antiguos',
49
+ ' --nueva-clave estrena una clave HTTP nueva (por defecto se reutiliza la guardada)',
50
+ ' --port <n> puerto HTTP (por defecto ' + PUERTO_HTTP_DEFECTO + ')',
51
+ ' --tareas-por-hora <n> tope de tareas por hora (por defecto ' + TAREAS_POR_HORA_DEFECTO + ')',
52
+ ' -h, --help esto',
53
+ '',
54
+ 'Sin opciones, habla MCP por stdio (lo que espera cualquier cliente MCP local).',
55
+ 'Con --http, habla por los dos a la vez; la clave de la URL se genera y se',
56
+ 'guarda en la casa (en <casa>\\mcp\\http-secret.txt), nunca en el repositorio.',
57
+ '',
58
+ 'CADA TAREA VA ENCERRADA en las carpetas de `mcp.workspaces`: lee y escribe',
59
+ 'sólo ahí, sin terminal, sin red, sin subagentes y sin guiones. Fuera de esas',
60
+ 'carpetas la herramienta se para y lo dice. El detalle, en lib/lectura.js.',
61
+ '',
62
+ ].join('\n');
63
+ }
64
+
65
+ /** Leer la línea de órdenes. */
66
+ function leerOrdenes(argv) {
67
+ const ordenes = { casa: undefined, motor: undefined, estado: false, ayuda: false, http: false, aceptoLecturaTotal: false, nuevaClave: false, puerto: PUERTO_HTTP_DEFECTO, tareasPorHora: TAREAS_POR_HORA_DEFECTO };
68
+ for (let i = 0; i < argv.length; i += 1) {
69
+ const a = argv[i];
70
+ if (a === '-h' || a === '--help') ordenes.ayuda = true;
71
+ else if (a === '--status') ordenes.estado = true;
72
+ else if (a === '--http') ordenes.http = true;
73
+ else if (a === '--acepto-lectura-total') ordenes.aceptoLecturaTotal = true; // ya no hace falta; se acepta y se ignora
74
+ else if (a === '--nueva-clave') ordenes.nuevaClave = true;
75
+ else if (a === '--port' || a.startsWith('--port=')) {
76
+ const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
77
+ const n = Number(valor);
78
+ if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error('--port debe ser un puerto 1-65535');
79
+ ordenes.puerto = n;
80
+ } else if (a === '--tareas-por-hora' || a.startsWith('--tareas-por-hora=')) {
81
+ const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
82
+ const n = Number(valor);
83
+ if (!Number.isInteger(n) || n < 1) throw new Error('--tareas-por-hora debe ser un entero positivo');
84
+ ordenes.tareasPorHora = n;
85
+ } else if (a === '--home' || a.startsWith('--home=')) {
86
+ const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
87
+ if (!valor) throw new Error('--home necesita una ruta');
88
+ ordenes.casa = valor;
89
+ } else if (a === '--dsh' || a.startsWith('--dsh=')) {
90
+ const valor = a.includes('=') ? a.slice(a.indexOf('=') + 1) : argv[++i];
91
+ if (!valor) throw new Error('--dsh necesita una ruta');
92
+ ordenes.motor = valor;
93
+ } else throw new Error('no entiendo «' + a + '» (mira: ratacode-mcp --help)');
94
+ }
95
+ return ordenes;
96
+ }
97
+
98
+ /**
99
+ * La clave larga de la URL HTTP. Se guarda en la casa (nunca en el repositorio)
100
+ * con permisos de sólo-dueño; si ya existe se reutiliza, salvo que se pida una
101
+ * nueva (`--nueva-clave`, y `tunel.mjs` la estrena al abrir el túnel).
102
+ * @param {string} casa - la casa de RATACODE.
103
+ * @param {boolean} nueva - true para estrenar clave aunque ya haya una.
104
+ * @returns {string} la clave (hex).
105
+ */
106
+ function claveHttp(casa, nueva = false) {
107
+ const ruta = join(casa, 'mcp', 'http-secret.txt');
108
+ if (nueva !== true) {
109
+ try {
110
+ const leida = readFileSync(ruta, 'utf8').trim();
111
+ if (/^[0-9a-f]{16,}$/.test(leida)) return leida;
112
+ } catch { /* la generamos */ }
113
+ }
114
+ const clave = randomBytes(32).toString('hex');
115
+ escribirSecreto(ruta, clave + '\n');
116
+ return clave;
117
+ }
118
+
119
+ /**
120
+ * Escribir un fichero de secretos con permisos de sólo-dueño. En POSIX el `mode`
121
+ * de `writeFileSync` no se aplica si el fichero YA existía, así que después se
122
+ * fuerza con `chmod` (en Windows el modo es casi decorativo, pero se pone igual).
123
+ */
124
+ function escribirSecreto(ruta, texto) {
125
+ mkdirSync(dirname(ruta), { recursive: true });
126
+ writeFileSync(ruta, texto, { mode: 0o600 });
127
+ try { chmodSync(ruta, 0o600); } catch { /* Windows, o sin permisos: da igual */ }
128
+ }
129
+
130
+ async function main() {
131
+ const ordenes = leerOrdenes(process.argv.slice(2));
132
+ if (ordenes.ayuda) {
133
+ process.stdout.write(uso());
134
+ return;
135
+ }
136
+ const casa = resolverCasa(ordenes.casa);
137
+
138
+ if (ordenes.estado) {
139
+ process.stdout.write(JSON.stringify(estadoDeLaCasa(casa), null, 2) + '\n');
140
+ return;
141
+ }
142
+
143
+ // R25 · AQUÍ ESTABA LA PUERTA DE LA LECTURA. Ya no hay puerta que abrir: cada
144
+ // tarea va encerrada (parche del MCP + gancho `lib/lectura.js`), así que
145
+ // `--acepto-lectura-total` es un no-op que se acepta por no romper guiones.
146
+ if (ordenes.aceptoLecturaTotal) {
147
+ aviso('--acepto-lectura-total ya no hace falta: las tareas leen y escriben sólo dentro de las carpetas autorizadas');
148
+ }
149
+
150
+ const dshBin = binDelMotor(ordenes.motor);
151
+ const { servidor, tareas, fabricaServidor } = montarServidor({
152
+ casa,
153
+ dshBin,
154
+ cwdPorDefecto: process.cwd(),
155
+ tareasPorHora: ordenes.tareasPorHora,
156
+ // En modo HTTP el espacio se aprieta: hacen falta `mcp.workspaces` y no vale
157
+ // ni la raíz del disco ni la carpeta del usuario (mira `resolverEspacio`).
158
+ http: ordenes.http,
159
+ });
160
+
161
+ // stdio: siempre (es el transporte de siempre).
162
+ const transporte = new StdioServerTransport();
163
+ await servidor.connect(transporte);
164
+
165
+ aviso('en marcha · casa: ' + casa);
166
+ aviso('en marcha · motor: ' + dshBin);
167
+ aviso('en marcha · herramientas: list_providers, list_models, run_task, get_task_status, get_task_result, cancel_task, ratacode_status');
168
+ aviso('en marcha · tope de tareas: ' + ordenes.tareasPorHora + '/h');
169
+ tareas.resumir();
170
+
171
+ // HTTP: además del stdio, si se pidió.
172
+ if (ordenes.http) {
173
+ const rutaClave = join(casa, 'mcp', 'http-secret.txt');
174
+ const rutaUrl = join(casa, 'mcp', 'http-url.txt');
175
+ const clave = claveHttp(casa, ordenes.nuevaClave);
176
+ /** La URL con la clave, al fichero. Se reescribe si la clave rota. */
177
+ const guardarUrl = (c) => {
178
+ try { escribirSecreto(rutaUrl, 'http://127.0.0.1:' + ordenes.puerto + '/mcp/' + c + '\n'); }
179
+ catch { /* no es crítico */ }
180
+ };
181
+ const http = await iniciarServidorHttp({
182
+ fabricaServidor,
183
+ puerto: ordenes.puerto,
184
+ clave,
185
+ rutaClave,
186
+ alRotar: guardarUrl,
187
+ host: '127.0.0.1',
188
+ });
189
+ // La clave NO se escribe en stderr: los clientes MCP guardan ese stderr en
190
+ // sus registros. Se dice dónde está y ya.
191
+ aviso('en marcha · HTTP en http://127.0.0.1:' + ordenes.puerto + '/mcp/<oculta>');
192
+ aviso('en marcha · la URL COMPLETA (con la clave) está en ' + rutaUrl);
193
+ guardarUrl(http.claveActual());
194
+ http.servidor.on('clientError', () => {});
195
+ }
196
+
197
+ // Al irnos, no dejamos tareas huérfanas trabajando por detrás.
198
+ const recoger = () => {
199
+ for (const recibo of tareas.listar()) {
200
+ if (recibo.estado === 'queued' || recibo.estado === 'running') {
201
+ try { tareas.cancelar(recibo.task_id); } catch { /* ya no está */ }
202
+ }
203
+ }
204
+ };
205
+ process.on('SIGINT', () => { recoger(); process.exit(130); });
206
+ process.on('SIGTERM', () => { recoger(); process.exit(143); });
207
+ process.on('exit', recoger);
208
+ }
209
+
210
+ try {
211
+ await main();
212
+ } catch (e) {
213
+ fallo('no pude arrancar', e);
214
+ process.exitCode = 1;
215
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * actividad — el cuaderno de lo que pasa por el MCP.
3
+ *
4
+ * Una línea JSON por tarea, en `<casa>/mcp/actividad.jsonl`. Es lo que luego
5
+ * enseña el panel (hora, cliente, modelo, proveedor, tarea, duración, tokens,
6
+ * coste, estado) y es también lo que permite mirar atrás sin pantalla.
7
+ *
8
+ * Regla de la casa: aquí se escribe el NOMBRE de la credencial si hace falta,
9
+ * nunca su valor. Y el prompt se guarda recortado, para que un encargo enorme
10
+ * no infle el cuaderno.
11
+ */
12
+ import { appendFileSync, mkdirSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+
15
+ /** Cuántos caracteres del encargo se guardan en el cuaderno. */
16
+ const TOPE_TAREA = 120;
17
+
18
+ /**
19
+ * Apuntar una tarea terminada.
20
+ * @param {string} casa - la casa de RATACODE.
21
+ * @param {object} fila - los campos de la línea.
22
+ * @returns {void}
23
+ */
24
+ export function anotar(casa, fila) {
25
+ const carpeta = join(casa, 'mcp');
26
+ mkdirSync(carpeta, { recursive: true });
27
+ const linea = {
28
+ hora: new Date().toISOString(),
29
+ cliente: fila.cliente ?? 'MCP',
30
+ modelo: fila.modelo ?? null,
31
+ proveedor: fila.proveedor ?? null,
32
+ tarea: recortar(fila.tarea),
33
+ duracion_ms: fila.duracion_ms ?? null,
34
+ tokens_input: fila.tokens_input ?? null,
35
+ tokens_output: fila.tokens_output ?? null,
36
+ coste: fila.coste ?? null,
37
+ estado: fila.estado ?? null,
38
+ espacio: fila.espacio ?? null,
39
+ task_id: fila.task_id ?? null,
40
+ };
41
+ appendFileSync(join(carpeta, 'actividad.jsonl'), JSON.stringify(linea) + '\n');
42
+ }
43
+
44
+ /** Recortar un texto a una línea corta. */
45
+ function recortar(texto) {
46
+ if (typeof texto !== 'string') return null;
47
+ const plano = texto.replace(/\s+/g, ' ').trim();
48
+ return plano.length <= TOPE_TAREA ? plano : plano.slice(0, TOPE_TAREA - 1) + '…';
49
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * casa — dónde vive RATACODE y dónde está su motor.
3
+ *
4
+ * Se resuelve EXACTAMENTE igual que en `bin/ratacode.js`, para que el MCP mire
5
+ * la misma casa (mismos proveedores, mismos modelos, mismas claves) que la web.
6
+ * Si aquí se resolviera distinto, tendríamos dos RATACODE y eso es justo lo que
7
+ * no queremos.
8
+ *
9
+ * casa = --home > $RATACODE_HOME > %USERPROFILE%\.ratacode
10
+ * motor = --dsh > @deepseek-ai/dsh del paquete (como bin/ratacode.js)
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join, resolve } from 'node:path';
16
+ import yaml from 'js-yaml';
17
+
18
+ /**
19
+ * La casa de RATACODE.
20
+ * @param {string|undefined} indicada - valor de `--home`, si lo hay.
21
+ * @returns {string} ruta absoluta.
22
+ */
23
+ export function resolverCasa(indicada) {
24
+ return resolve(indicada ?? process.env.RATACODE_HOME ?? join(homedir(), '.ratacode'));
25
+ }
26
+
27
+ /**
28
+ * El binario del motor DSH. Mismo criterio que `binDelMotor()` de ratacode.js:
29
+ * se pregunta al paquete instalado, no se adivina una ruta.
30
+ * @param {string|undefined} indicada - valor de `--dsh`, si lo hay.
31
+ * @returns {string} ruta absoluta a `lib/bin.js`.
32
+ */
33
+ export function binDelMotor(indicada) {
34
+ if (indicada !== undefined && indicada !== null && indicada !== '') {
35
+ const ruta = resolve(indicada);
36
+ if (!existsSync(ruta)) throw new Error('no encuentro el motor en ' + ruta);
37
+ return ruta;
38
+ }
39
+ const requerir = createRequire(import.meta.url);
40
+ const manifiesto = requerir.resolve('@deepseek-ai/dsh/package.json');
41
+ const pkg = JSON.parse(readFileSync(manifiesto, 'utf8'));
42
+ const rel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.dsh;
43
+ if (rel === undefined || rel === null) throw new Error('el motor no declara su binario (¿instalación a medias?)');
44
+ const bin = join(dirname(manifiesto), rel);
45
+ if (!existsSync(bin)) throw new Error('no encuentro el motor en ' + bin);
46
+ return bin;
47
+ }
48
+
49
+ /**
50
+ * Los ajustes de la casa (`settings.yaml`). Es el MISMO documento que lee el
51
+ * core (dsh-settings-file), así que leerlo aquí no duplica lógica: es leer la
52
+ * configuración del usuario, que es exactamente lo que hace la web.
53
+ * @param {string} casa - la casa de RATACODE.
54
+ * @returns {{documento: object, ruta: string, error: string|null}}
55
+ */
56
+ export function leerAjustes(casa) {
57
+ const ruta = join(casa, 'settings.yaml');
58
+ if (!existsSync(ruta)) return { documento: {}, ruta, error: null };
59
+ try {
60
+ const cargado = yaml.load(readFileSync(ruta, 'utf8'));
61
+ if (cargado === null || cargado === undefined) return { documento: {}, ruta, error: null };
62
+ if (typeof cargado !== 'object' || Array.isArray(cargado)) {
63
+ return { documento: {}, ruta, error: 'settings.yaml no es un documento de ajustes (se esperaba un mapa)' };
64
+ }
65
+ return { documento: cargado, ruta, error: null };
66
+ } catch (e) {
67
+ return { documento: {}, ruta, error: 'no pude leer settings.yaml: ' + (e instanceof Error ? e.message : String(e)) };
68
+ }
69
+ }
70
+
71
+ /** Un texto no vacío, o undefined. */
72
+ function texto(valor) {
73
+ return typeof valor === 'string' && valor.trim() !== '' ? valor : undefined;
74
+ }
75
+
76
+ /**
77
+ * La sección `mcp:` de los ajustes, con sus valores por defecto.
78
+ * Todo lo que limita el daño se lee de aquí, para que el humano lo pueda
79
+ * cambiar sin tocar código.
80
+ * @param {string} casa - la casa de RATACODE.
81
+ * @returns {{workspaces: string[], workspacePorDefecto: string|undefined, permitirPeligroso: boolean, precios: object, timeoutPorDefectoMs: number, timeoutMaximoMs: number, tareasALaVez: number, promptMaxCaracteres: number, avisos: string[]}}
82
+ */
83
+ export function ajustesMcp(casa) {
84
+ const { documento, error } = leerAjustes(casa);
85
+ const avisos = error === null ? [] : [error];
86
+ const bruto = documento.mcp;
87
+ const seccion = bruto !== null && typeof bruto === 'object' && !Array.isArray(bruto) ? bruto : {};
88
+ const listaBruta = Array.isArray(seccion.workspaces) ? seccion.workspaces : [];
89
+ const workspaces = [];
90
+ for (const entrada of listaBruta) {
91
+ const ruta = texto(entrada);
92
+ if (ruta === undefined) {
93
+ avisos.push('mcp.workspaces tiene una entrada que no es una ruta de texto; la salto');
94
+ continue;
95
+ }
96
+ workspaces.push(resolve(ruta));
97
+ }
98
+ const porDefecto = texto(seccion.workspace_por_defecto);
99
+ const precios = seccion.precios !== null && typeof seccion.precios === 'object' && !Array.isArray(seccion.precios) ? seccion.precios : {};
100
+ return {
101
+ workspaces,
102
+ workspacePorDefecto: porDefecto === undefined ? undefined : resolve(porDefecto),
103
+ permitirPeligroso: seccion.permitir_peligroso === true,
104
+ precios,
105
+ timeoutPorDefectoMs: enteroPositivo(seccion.timeout_por_defecto_ms, TIMEOUT_POR_DEFECTO_MS),
106
+ timeoutMaximoMs: enteroPositivo(seccion.timeout_maximo_ms, TIMEOUT_MAXIMO_MS),
107
+ tareasALaVez: enteroPositivo(seccion.tareas_a_la_vez, TAREAS_A_LA_VEZ),
108
+ promptMaxCaracteres: enteroPositivo(seccion.prompt_max_caracteres, PROMPT_MAX_CARACTERES),
109
+ avisos,
110
+ };
111
+ }
112
+
113
+ /** Los topes de fábrica del MCP. Se pueden cambiar en `mcp:` de settings.yaml. */
114
+ /** Media hora por tarea si el cliente no dice otra cosa. */
115
+ export const TIMEOUT_POR_DEFECTO_MS = 1_800_000;
116
+ /** Y una hora como techo, aunque el cliente pida más. */
117
+ export const TIMEOUT_MAXIMO_MS = 3_600_000;
118
+ /** Cuántas tareas pueden estar en marcha a la vez. */
119
+ export const TAREAS_A_LA_VEZ = 3;
120
+ /** Tope del encargo (caracteres). Un prompt de verdad no llega ni de lejos. */
121
+ export const PROMPT_MAX_CARACTERES = 100_000;
122
+
123
+ /** Un entero positivo de los ajustes, o el valor de fábrica. */
124
+ function enteroPositivo(valor, porDefecto) {
125
+ return typeof valor === 'number' && Number.isInteger(valor) && valor > 0 ? valor : porDefecto;
126
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * claves — ¿está la credencial? Sólo sí o no, y sin leer nunca el valor.
3
+ *
4
+ * ── REGLA DE LA CASA (R22, 28-sep) ──────────────────────────────────────────
5
+ * La ÚNICA fuente de claves de RATACODE es el almacén de la casa
6
+ * (`<casa>\.credentials.yaml`), que es lo que escribe Ajustes › Models. Este
7
+ * servidor NO mira el entorno del cliente MCP —daba igual que la clave fuera
8
+ * vieja o estuviera rotada, el motor la daba por puesta— y tampoco lee ficheros
9
+ * de claves por su cuenta.
10
+ *
11
+ * La pregunta la contesta el MOTOR, con su propio código: se carga su
12
+ * `LocalCredentialProvider` (el mismo que resuelve las claves cuando corre una
13
+ * tarea) y se le pregunta `describe(<variable>)` → `configured: sí/no`. De ahí
14
+ * no sale ningún valor: sólo si está o no está.
15
+ *
16
+ * Y el hijo del motor que ejecuta cada tarea arranca SIN esas variables (ver
17
+ * {@link variablesDeClaves} y `nucleo.js`), para que resuelva las de la casa.
18
+ */
19
+ import { createRequire } from 'node:module';
20
+ import { readFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { pathToFileURL } from 'node:url';
23
+
24
+ /** Las variables de claves que RATACODE conoce de fábrica. */
25
+ export const VARIABLES_CONOCIDAS = [
26
+ 'B_AI_API_KEY', 'BAI_API_KEY', 'OPENROUTER_API_KEY', 'DEEPSEEK_API_KEY', 'GROQ_API_KEY',
27
+ 'GEMINI_API_KEY', 'NVIDIA_API_KEY', 'SAMBANOVA_API_KEY', 'CLOUDFLARE_API_KEY', 'CLOUDFLARE_API_TOKEN',
28
+ ];
29
+
30
+ /** Cómo detener el hijo: si el paquete del motor no se puede cargar, se dice. */
31
+ let avisoDelMotor = null;
32
+
33
+ /**
34
+ * La ruta del `package.json` del motor instalado (la misma que usa `casa.js`).
35
+ * @returns {string} ruta absoluta.
36
+ */
37
+ function manifiestoDelMotor() {
38
+ const requerir = createRequire(import.meta.url);
39
+ return requerir.resolve('@deepseek-ai/dsh/package.json');
40
+ }
41
+
42
+ /** El `LocalCredentialProvider` del motor, cargado de SU paquete. Se carga una vez. */
43
+ let proveedorPrometido = null;
44
+ function proveedorDelMotor() {
45
+ if (proveedorPrometido === null) {
46
+ proveedorPrometido = (async () => {
47
+ const manifiesto = manifiestoDelMotor();
48
+ const requerir = createRequire(manifiesto);
49
+ const ruta = requerir.resolve('@deepseek-ai/dsh-credentials-local');
50
+ const modulo = await import(pathToFileURL(ruta).href);
51
+ return modulo.LocalCredentialProvider;
52
+ })();
53
+ }
54
+ return proveedorPrometido;
55
+ }
56
+
57
+ /**
58
+ * Un entorno de arranque VACÍO: el almacén del motor mira primero las variables
59
+ * del proceso que lo arranca, y aquí lo que queremos es justo lo contrario —
60
+ * que sólo cuente lo guardado en la casa (Ajustes › Models).
61
+ */
62
+ const SIN_ENTORNO = {
63
+ get: () => undefined,
64
+ getFrom: () => undefined,
65
+ };
66
+
67
+ /** Lo mínimo que toca el constructor del servicio del motor (no se registra en ningún sitio). */
68
+ const CONTEXTO_SUELTO = {
69
+ get: (clave) => (clave === 'launchEnvironment' ? SIN_ENTORNO : undefined),
70
+ reflect: { provide() { /* un servicio suelto: no se registra */ } },
71
+ logger: { info() {}, warn() {}, error() {} },
72
+ effect: () => {},
73
+ };
74
+
75
+ /**
76
+ * ¿Está esta credencial guardada en la casa? Se lo pregunta al almacén del
77
+ * motor, con su `describe`: nunca se lee el fichero a mano y nunca sale un valor.
78
+ * @param {string} casa - la casa de RATACODE.
79
+ * @param {string} nombre - nombre de la variable (p. ej. `B_AI_API_KEY`).
80
+ * @returns {Promise<{configurada: boolean, motivo: string|null}>} `motivo` sólo
81
+ * cuando NO se ha podido preguntar (y entonces no se puede afirmar nada).
82
+ */
83
+ export async function describeEnLaCasa(casa, nombre) {
84
+ if (typeof nombre !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(nombre)) {
85
+ return { configurada: false, motivo: '«' + String(nombre) + '» no es un nombre de variable' };
86
+ }
87
+ try {
88
+ const Proveedor = await proveedorDelMotor();
89
+ const proveedor = new Proveedor(CONTEXTO_SUELTO, {
90
+ path: join(casa, '.credentials.yaml'),
91
+ watch: false,
92
+ });
93
+ await proveedor.loadInitial();
94
+ const dicho = await proveedor.describe(nombre);
95
+ return { configurada: dicho?.configured === true, motivo: null };
96
+ } catch (e) {
97
+ avisoDelMotor = e instanceof Error ? e.message : String(e);
98
+ return { configurada: false, motivo: 'no pude preguntar al almacén de la casa: ' + avisoDelMotor };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * El mensaje exacto cuando falta una clave, con el nombre que ve el humano.
104
+ * @param {string} nombreVisible - el nombre del proveedor («B.AI») o, si no se
105
+ * sabe, el de la variable.
106
+ * @returns {string} p. ej. `Falta la clave de B.AI. Pégala en RATACODE › Ajustes › Models.`
107
+ */
108
+ export function faltaLaClave(nombreVisible) {
109
+ return 'Falta la clave de ' + nombreVisible + '. Pégala en RATACODE › Ajustes › Models.';
110
+ }
111
+
112
+ /**
113
+ * Las variables de claves que declara ESTA casa (más las conocidas de fábrica):
114
+ * el hijo del motor que ejecuta una tarea arranca sin ninguna de ellas, para
115
+ * que resuelva las claves del almacén de la casa y no las del entorno.
116
+ * @param {string} casa - la casa de RATACODE.
117
+ * @returns {string[]} nombres de variable (nunca un valor).
118
+ */
119
+ export function variablesDeClaves(casa) {
120
+ const nombres = new Set(VARIABLES_CONOCIDAS);
121
+ try {
122
+ const texto = readFileSync(join(casa, 'settings.yaml'), 'utf8');
123
+ for (const linea of texto.split('\n')) {
124
+ const m = /^\s*apiKeyEnv\s*:\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/.exec(linea);
125
+ if (m !== null) nombres.add(m[1]);
126
+ }
127
+ } catch { /* sin settings.yaml: quedan las conocidas */ }
128
+ return [...nombres];
129
+ }
130
+
131
+ /**
132
+ * El entorno de un hijo del motor SIN las variables de claves (se borran por
133
+ * nombre, en mayúsculas: en Windows no distinguen mayúsculas de minúsculas).
134
+ * @param {string} casa - la casa de RATACODE.
135
+ * @param {object} base - el entorno de partida.
136
+ * @returns {object} una copia sin esas variables.
137
+ */
138
+ export function entornoDelMotorSinClaves(casa, base) {
139
+ const fuera = new Set(variablesDeClaves(casa).map((n) => n.toUpperCase()));
140
+ const entorno = { ...base };
141
+ for (const nombre of Object.keys(entorno)) {
142
+ if (fuera.has(nombre.toUpperCase())) delete entorno[nombre];
143
+ }
144
+ return entorno;
145
+ }
146
+
147
+ /** La ruta del manifiesto del motor (para mensajes y para las pruebas). */
148
+ export { manifiestoDelMotor };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * espacios — que una tarea del MCP SALGA en la barra lateral del panel.
3
+ *
4
+ * ── EL PROBLEMA (medido en R12) ─────────────────────────────────────────────
5
+ * Una tarea del MCP SÍ crea su sesión de motor
6
+ * (`<casa>\sessions\<espacio>\mcp-<task>\session.v3.jsonl.zstd`, con su
7
+ * conversación dentro), pero en el panel NO salía: la barra lateral lista las
8
+ * sesiones que el REGISTRO DE ESPACIOS tiene apuntadas, y nadie apuntaba las del
9
+ * MCP. El MCP era una caja negra.
10
+ *
11
+ * ── LA VÍA (la que usa el propio motor) ─────────────────────────────────────
12
+ * El registro es `<casa>\storages\workspace.json`, el documento del dominio
13
+ * `workspace` versión 2 de `dsh-workspace` (`lib/index.js`: spec con
14
+ * `global.workspaceIds` —el orden de la barra— y la tabla `workspaces` con
15
+ * `{path, title, sessionIds, createdAt, updatedAt}`; `path` es el `fs.realpath`
16
+ * del sitio y `sessionIds` es la cuenta de propiedad EN ORDEN DE PANTALLA).
17
+ * `bin/ratacode.js` ya escribe este mismo fichero para registrar el espacio;
18
+ * aquí sólo se añade la sesión a la cuenta del espacio que le toca.
19
+ *
20
+ * La escritura es atómica (fichero temporal + rename) y NUNCA pisa lo que haya:
21
+ * si el documento no es el que sabemos leer, se deja como está y se dice por qué.
22
+ */
23
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
24
+ import { randomUUID } from 'node:crypto';
25
+ import { basename, dirname, join, resolve } from 'node:path';
26
+
27
+ /** La ruta canónica de una carpeta (la única canon de identidad del registro). */
28
+ function canonica(ruta) {
29
+ try {
30
+ return realpathSync(ruta);
31
+ } catch {
32
+ return resolve(ruta).replace(/[\\/]+$/, '');
33
+ }
34
+ }
35
+
36
+ /** El fichero del registro de espacios de una casa. */
37
+ export function rutaDelRegistro(casa) {
38
+ return join(casa, 'storages', 'workspace.json');
39
+ }
40
+
41
+ /**
42
+ * Apuntar la sesión de una tarea en el espacio de su carpeta de trabajo.
43
+ *
44
+ * @param {string} casa - la casa de RATACODE.
45
+ * @param {string} espacio - la carpeta de trabajo de la tarea (`cwd` del motor).
46
+ * @param {string} sessionId - el id de la sesión del motor (aquí, `mcp-<task>`).
47
+ * @returns {{apuntada: boolean, nueva?: boolean, espacioId?: string, motivo?: string}}
48
+ */
49
+ export function apuntarSesionEnEspacio(casa, espacio, sessionId) {
50
+ if (typeof sessionId !== 'string' || sessionId.trim() === '') {
51
+ return { apuntada: false, motivo: 'sin id de sesión que apuntar' };
52
+ }
53
+ if (typeof espacio !== 'string' || espacio.trim() === '') {
54
+ return { apuntada: false, motivo: 'la tarea no trae carpeta de trabajo' };
55
+ }
56
+ const ruta = rutaDelRegistro(casa);
57
+ let doc = null;
58
+ if (existsSync(ruta)) {
59
+ try {
60
+ doc = JSON.parse(readFileSync(ruta, 'utf8'));
61
+ } catch {
62
+ doc = null;
63
+ }
64
+ }
65
+ if (doc !== null && doc?.unit?.name !== 'workspace') doc = null;
66
+ if (doc !== null && doc.unit.version !== 2) {
67
+ return {
68
+ apuntada: false,
69
+ motivo: 'el registro de espacios de esta casa es versión ' + doc.unit.version
70
+ + ', y yo sé escribir la 2: no lo toco',
71
+ };
72
+ }
73
+ if (doc === null) {
74
+ doc = {
75
+ unit: { name: 'workspace', version: 2 },
76
+ global: { initialized: true, workspaceIds: [], archivedSessionIds: [] },
77
+ tables: { workspaces: {} },
78
+ };
79
+ }
80
+ doc.global = doc.global ?? {};
81
+ if (!Array.isArray(doc.global.workspaceIds)) doc.global.workspaceIds = [];
82
+ if (!Array.isArray(doc.global.archivedSessionIds)) doc.global.archivedSessionIds = [];
83
+ doc.tables = doc.tables ?? {};
84
+ doc.tables.workspaces = doc.tables.workspaces ?? {};
85
+
86
+ const canon = canonica(espacio);
87
+ let espacioId = Object.keys(doc.tables.workspaces)
88
+ .find((clave) => doc.tables.workspaces[clave]?.path === canon);
89
+ const ahora = new Date().toISOString();
90
+ let nuevoEspacio = false;
91
+ if (espacioId === undefined) {
92
+ espacioId = randomUUID();
93
+ doc.tables.workspaces[espacioId] = {
94
+ path: canon,
95
+ title: basename(canon) || canon,
96
+ sessionIds: [],
97
+ createdAt: ahora,
98
+ updatedAt: ahora,
99
+ };
100
+ doc.global.workspaceIds = [espacioId, ...doc.global.workspaceIds];
101
+ doc.global.initialized = true;
102
+ nuevoEspacio = true;
103
+ }
104
+ const registro = doc.tables.workspaces[espacioId];
105
+ if (!Array.isArray(registro.sessionIds)) registro.sessionIds = [];
106
+ if (registro.sessionIds.includes(sessionId)) {
107
+ return { apuntada: true, nueva: false, espacioId, nuevoEspacio };
108
+ }
109
+ registro.sessionIds = [sessionId, ...registro.sessionIds];
110
+ registro.updatedAt = ahora;
111
+
112
+ mkdirSync(dirname(ruta), { recursive: true });
113
+ const temporal = ruta + '.' + process.pid + '.tmp';
114
+ writeFileSync(temporal, JSON.stringify(doc, null, 2) + '\n', { mode: 0o600 });
115
+ renameSync(temporal, ruta);
116
+ return { apuntada: true, nueva: true, espacioId, nuevoEspacio };
117
+ }