@danieltmn/tamnora 0.16.1 → 0.18.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +58 -4
  3. package/SECURITY.md +8 -0
  4. package/harness/packages/cli/src/commands/mcp.js +31 -4
  5. package/harness/packages/cli/src/commands/repl.js +7 -2
  6. package/harness/packages/cli/src/commands/run.js +5 -2
  7. package/harness/packages/cli/src/commands/tui.js +9 -2
  8. package/harness/packages/cli/src/lib/runtime.js +23 -1
  9. package/harness/packages/cli/test/tunnel.test.js +13 -1
  10. package/harness/packages/core/src/agent/modes.js +3 -0
  11. package/harness/packages/core/src/config/index.js +3 -1
  12. package/harness/packages/core/src/index.js +17 -3
  13. package/harness/packages/core/src/mcp/catalog.js +79 -0
  14. package/harness/packages/core/src/mcp/index.js +14 -2
  15. package/harness/packages/core/src/mcp/registry.js +29 -1
  16. package/harness/packages/core/src/procs/detect.js +59 -8
  17. package/harness/packages/core/src/procs/kill.js +26 -5
  18. package/harness/packages/core/src/redact.js +25 -0
  19. package/harness/packages/core/src/skills/catalog.js +78 -0
  20. package/harness/packages/core/src/skills/remote.js +51 -0
  21. package/harness/packages/core/src/tools/executors.js +93 -2
  22. package/harness/packages/core/src/tools/index.js +27 -6
  23. package/harness/packages/core/src/tools/labels.js +2 -0
  24. package/harness/packages/core/src/tools/mcp.js +91 -0
  25. package/harness/packages/core/src/tools/permissions.js +53 -8
  26. package/harness/packages/core/src/tools/registry.js +14 -7
  27. package/harness/packages/core/src/tools/schema.js +86 -0
  28. package/harness/packages/core/src/tts/edge.js +28 -7
  29. package/harness/packages/core/src/tunnel.js +51 -5
  30. package/harness/packages/core/test/compact.test.js +2 -0
  31. package/harness/packages/core/test/labels.test.js +1 -0
  32. package/harness/packages/core/test/mcp-registry.test.js +37 -1
  33. package/harness/packages/core/test/mcp-routing.test.js +115 -0
  34. package/harness/packages/core/test/procs.test.js +70 -2
  35. package/harness/packages/core/test/redact.test.js +22 -0
  36. package/harness/packages/core/test/skills.test.js +11 -0
  37. package/harness/packages/core/test/tools.test.js +97 -3
  38. package/harness/packages/server/public/app.js +629 -28
  39. package/harness/packages/server/public/index.html +44 -2
  40. package/harness/packages/server/public/styles.css +106 -7
  41. package/harness/packages/server/src/lib/agentTools.js +45 -3
  42. package/harness/packages/server/src/lib/channels/telegram.js +2 -0
  43. package/harness/packages/server/src/lib/procs.js +9 -3
  44. package/harness/packages/server/src/lib/prompt.js +4 -1
  45. package/harness/packages/server/src/lib/questions.js +72 -0
  46. package/harness/packages/server/src/lib/runManager.js +29 -2
  47. package/harness/packages/server/src/lib/scheduler.js +12 -1
  48. package/harness/packages/server/src/lib/tasks.js +3 -1
  49. package/harness/packages/server/src/routes/chat.js +40 -3
  50. package/harness/packages/server/src/routes/mcp.js +54 -2
  51. package/harness/packages/server/src/routes/preview.js +49 -28
  52. package/harness/packages/server/src/routes/procs.js +20 -3
  53. package/harness/packages/server/src/routes/providers.js +12 -0
  54. package/harness/packages/server/src/routes/sessions.js +3 -1
  55. package/harness/packages/server/src/routes/skills.js +48 -1
  56. package/harness/packages/server/src/routes/state.js +8 -1
  57. package/harness/packages/server/src/server.js +37 -1
  58. package/harness/packages/server/test/preview.test.js +111 -0
  59. package/harness/packages/server/test/server.test.js +285 -34
  60. package/harness/packages/server/test/telegram-features.test.js +2 -0
  61. package/harness/packages/server/test/telegram-voz.test.js +2 -0
  62. package/harness/packages/server/test/telegram.test.js +2 -0
  63. package/harness/tamnora.example.json +3 -1
  64. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,51 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
5
5
  El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
6
6
  y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
7
7
 
8
+ ## [0.18.0] - 2026-10-12
9
+
10
+ ### Agregado
11
+
12
+ - **UI interactiva**: el agente pide un **formulario** (`ask_user`), ofrece
13
+ **botones** (`offer_choices`) o muestra un **checklist** (`show_plan`); la
14
+ consola los renderiza (el formulario pausa el turno, los botones se mandan como
15
+ mensaje, el checklist tiene estado) y los reconstruye al recargar la sesión.
16
+ - **MCP con carga diferida**: cuando hay muchas tools, se expone un índice y un
17
+ router `mcp_call` en vez de inyectar todos los esquemas (`mcpMode` `auto` /
18
+ `eager` / `lazy`; `mcpPinned` fija tools o servidores). Ajustes → MCP muestra
19
+ modo, fijas y conteo en contexto.
20
+ - **Catálogo y búsqueda de MCP y skills**: catálogo curado en "sugeridos" (se
21
+ agregan deshabilitados) y búsqueda en el registro oficial de MCP y en GitHub. El
22
+ agente suma `search_mcp` / `install_mcp` y `search_skills` / `install_skill`.
23
+ - **Preview remoto**: la pestaña preview publica el dev server por Tailscale
24
+ (`tailscale serve --https=8443`) para verlo desde otro dispositivo.
25
+
26
+ ### Cambiado
27
+
28
+ - **Voz (Edge)**: la síntesis usa el host nuevo
29
+ (`api.msedgeservices.com/.../websocket/v1`) con el token como
30
+ `Ocp-Apim-Subscription-Key`; el host viejo respondía 403.
31
+ - **Asesor**: la tarjeta cambia de color y etiqueta según el tipo
32
+ (código / explicación / consejo / resumen).
33
+
34
+ ### Seguridad
35
+
36
+ - **Tools MCP por permisos**: ahora pasan por el mismo control que las locales
37
+ (default `ask`); antes se ejecutaban sin consultar. Se habilita por tool con
38
+ `permissions: { "mcp__...": "allow" }`.
39
+ - **Endurecimiento de `bash`**: piso de seguridad que **deniega** los comandos
40
+ catastróficos aunque la política sea `allow` o `--yes` (`rm -rf /`/`~`, fork
41
+ bomb, `mkfs`, `dd of=/dev/`, formatear/borrar una unidad) y marca como `ask` los
42
+ irreversibles (`git push --force`, `git reset --hard`, `git clean -f`).
43
+ - **Scrub de entorno**: `bash` y los `git` locales ya no heredan las variables con
44
+ nombre de credencial (`*_API_KEY`, `*_TOKEN`, `*PASSWORD`, ...).
45
+
46
+ ## [0.17.0] - 2026-10-11
47
+
48
+ ### Cambios
49
+
50
+ - docs: procesos de desarrollo e interrumpir el turno
51
+ - feat: procesos por proyecto, /port y /portkill, e interrumpir el turno
52
+
8
53
  ## [0.16.1] - 2026-10-10
9
54
 
10
55
  ### Cambios
package/README.md CHANGED
@@ -22,8 +22,11 @@ con TUI, consola web, tools locales, MCP y evaluacion de modelos.
22
22
  que se inyectan solo cuando el agente toca el archivo.
23
23
  - **Automatizacion**: tareas programadas (`every`, `daily`, cron) que corren solas
24
24
  y **deshacer** un turno (restaura los archivos a como estaban).
25
- - **Contexto y costo**: skills on-demand, auto-compactacion, `max_tokens` por ronda
26
- y prompt caching.
25
+ - **Contexto y costo**: skills on-demand, MCP con **carga diferida** (índice +
26
+ router `mcp_call`), auto-compactacion, `max_tokens` por ronda y prompt caching.
27
+ - **UI interactiva**: el agente pide un **formulario** (`ask_user`), ofrece
28
+ **botones** (`offer_choices`) o muestra un **checklist** (`show_plan`); la
29
+ consola los renderiza.
27
30
  - **Plataforma**: hooks y plugins locales (`.tamnora/hooks`, `.tamnora/plugins`).
28
31
 
29
32
  ## Instalacion
@@ -117,6 +120,10 @@ menu "mas" (los tres puntitos). En el chat:
117
120
  volver, la consola **reconecta** y reconstruye el progreso. Para cortarlo se usa
118
121
  el boton de detener (o `POST /api/sessions/:id/stop`), que ademas corta el stream
119
122
  del modelo y mata las tools de larga duracion (p. ej. `bash`) en curso.
123
+ - **Encolar o interrumpir**: si el agente esta trabajando, lo que envias queda en
124
+ una **cola** visible y se manda al terminar. Para cortarlo y mandar el mensaje
125
+ **ya**, usa el boton **interrumpir** (⚡) o **Ctrl/Cmd+Enter**: cancela el turno en
126
+ curso y arranca el nuevo apenas se desenrolla.
120
127
  - **Solo lo pedido (estricto)**: el toggle de la pestaña Agente (o `PUT
121
128
  /api/settings` con `strictScope`) hace que el agente se limite a lo solicitado,
122
129
  sin mejoras ni cambios extra; se combina con Plan o Build.
@@ -128,7 +135,8 @@ menu "mas" (los tres puntitos). En el chat:
128
135
  - **Actualizar**: cuando npm tiene una version nueva aparece un banner para
129
136
  actualizar con un click; el server se reinicia solo.
130
137
  - **Comandos**: el input muestra un menu al escribir `/` (`/new`, `/init`, `/goal`,
131
- `/resumen`, `/audioresumen`, `/compact`, `/clear`, `/cwd`, `/kill`, `/help`).
138
+ `/resumen`, `/audioresumen`, `/compact`, `/clear`, `/cwd`, `/port`, `/portkill`,
139
+ `/help`).
132
140
  - **Capturas**: el tool `screenshot` muestra la imagen inline en su tarjeta.
133
141
 
134
142
  ### Memoria, reglas y automatizacion
@@ -149,6 +157,20 @@ menu "mas" (los tres puntitos). En el chat:
149
157
  archivo; con **deshacer cambios** (menú `⋮` de cada respuesta) se restauran los
150
158
  archivos y se borra el turno del historial.
151
159
 
160
+ ### UI interactiva
161
+
162
+ El agente no numera preguntas en el texto: cuando necesita decidir algo, usa un
163
+ componente que la consola dibuja.
164
+
165
+ - **Formulario** (`ask_user`): pausa el turno y devuelve las respuestas (texto,
166
+ textarea, select o confirmar). En Telegram/CLI cae a una pregunta en prosa.
167
+ - **Botones** (`offer_choices`): opciones tappables; al tocar una se manda como tu
168
+ mensaje (bifurcaciones A/B/C). No pausa el turno.
169
+ - **Checklist** (`show_plan`): pasos con estado (`pending`/`active`/`done`); es de
170
+ solo lectura y el agente lo reemite para actualizarlo.
171
+
172
+ Los tres se renderizan en vivo y se reconstruyen al recargar la consola.
173
+
152
174
  ### Contexto y costo
153
175
 
154
176
  - **Skills on-demand**: en vez de inyectar todas las skills, el prompt lleva un
@@ -174,6 +196,21 @@ Laravel, PHP) y abrir un tunel al puerto para verlo desde el celular. Para sitio
174
196
  **estaticos** (un `index.html` sin script de dev) ofrece un chip "sitio estatico"
175
197
  que los sirve con un servidor sin dependencias (`harness/scripts/static-server.mjs`).
176
198
 
199
+ En un **monorepo** donde cada parte arranca distinto, el proyecto declara sus
200
+ procesos en `.tamnora/procs.json` (relativo al proyecto seleccionado); salen
201
+ primero, se suman a los detectados y **solo** se pueden lanzar los que pasan por
202
+ la allowlist `processes.allow`:
203
+
204
+ ```jsonc
205
+ { "processes": [
206
+ { "label": "backend", "dir": "backend", "command": "node", "args": ["index.js"], "port": 3000 },
207
+ { "label": "frontend", "dir": "frontend", "command": "npm", "args": ["run", "dev"], "port": 5173 }
208
+ ] }
209
+ ```
210
+
211
+ Los puertos se consultan y se liberan desde el chat: `/port <puerto>` dice si esta
212
+ activo (y que PID lo usa) y `/portkill <puerto>` mata el proceso que lo escucha.
213
+
177
214
  El visor de archivos tiene **vista previa**: `.html`/`.svg` se sirven con sus CSS,
178
215
  JS e imagenes relativas, y `.md` se renderiza como markdown. Ambas vistas suman
179
216
  selector de tamano (movil/tablet/laptop/pc/adaptable) y boton para maximizar.
@@ -249,14 +286,31 @@ Los MCP stdio **no heredan `process.env`**: reciben un baseline minimo mas el
249
286
  (ver [`SECURITY.md`](SECURITY.md)). Para un servidor de confianza que necesite el
250
287
  entorno completo, `"inheritEnv": true`.
251
288
 
289
+ Cuando hay **muchas** tools MCP, el harness no inyecta todos los esquemas en cada
290
+ turno: expone un **indice** y una tool router `mcp_call({ tool, args })`, y deja
291
+ solo las "calientes" con esquema completo (`mcpMode`: `auto` | `eager` | `lazy`;
292
+ `mcpPinned` fija tools o servidores enteros). Ademas, las tools MCP pasan por los
293
+ **mismos permisos** que las locales (default `ask`): se habilita por tool con
294
+ `permissions: { "mcp__postgres__query": "allow" }`. Se ajustan en Ajustes → MCP.
295
+
296
+ Desde Ajustes → MCP tambien hay un **catalogo de sugeridos** y busqueda en el
297
+ registro oficial; se agregan deshabilitados y los prendes cuando quieras.
298
+
252
299
  ## Tools
253
300
 
254
301
  Tools locales confinadas al workspace (`read`, `list`, `glob`, `grep`, `write`,
255
302
  `edit`, `bash`, `fetch_url`), de navegador (`screenshot`, `console_errors`,
256
- `preview_url`), de voz (`speak`), de Git (`git_status`, `git_diff`, `git_log`,
303
+ `preview_url`), de voz (`speak`), de interfaz interactiva (`ask_user`,
304
+ `offer_choices`, `show_plan`), de Git (`git_status`, `git_diff`, `git_log`,
257
305
  `git_commit`) y de conocimiento (`memory`, `skill`), con permisos
258
306
  `allow`/`ask`/`deny` por tool o por patron.
259
307
 
308
+ `bash` esta **endurecido**: un piso de seguridad **deniega** los comandos
309
+ catastroficos (`rm -rf /`/`~`, fork bomb, `mkfs`, `dd of=/dev/`, formatear/borrar
310
+ una unidad) aunque la politica sea `allow`, marca como `ask` los irreversibles
311
+ (`git push --force`, `git reset --hard`) y el shell **no hereda** las variables con
312
+ nombre de credencial (`scrubEnv`), asi que no filtra tus API keys.
313
+
260
314
  `fetch_url` lee URLs (HTML a texto) y avisa cuando un sitio exige JavaScript o
261
315
  esta detras de Cloudflare; para esos casos las tools `screenshot` / `console_errors`
262
316
  usan el **Chrome/Edge/Chromium del sistema** como navegador headless (sin
package/SECURITY.md CHANGED
@@ -35,6 +35,14 @@ asi que se lo debe tratar como codigo con acceso a tu usuario.
35
35
  servidores de confianza. Un tool tipo `get-env` no puede leer tus secretos del
36
36
  shell, y los resultados MCP se **redactan** (`redactSecrets`) por si devuelven un
37
37
  token. No pases credenciales en los argumentos de las tools: van por `env`.
38
+ - **Tools locales y `bash`**: `bash` corre en tu maquina como tu usuario, asi que
39
+ hay un piso de seguridad que **no se puede saltear** ni con `bash: "allow"` ni
40
+ con `--yes`: los comandos catastroficos (`rm -rf /`/`~`, fork bomb, `mkfs`,
41
+ `dd of=/dev/`, formatear/borrar una unidad) quedan **denegados**, y los
42
+ irreversibles (`rm -rf *`/`.`, `git push --force`, `git reset --hard`,
43
+ `git clean -f`) al menos `ask`. Ademas el shell y los `git` locales **no heredan**
44
+ las variables con nombre de credencial (`scrubEnv` quita `*_API_KEY`, `*_TOKEN`,
45
+ `*PASSWORD`, ...), asi que `env`/`printenv` no filtran tus claves del proceso.
38
46
 
39
47
  ## Buenas practicas
40
48
 
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { loadConfig, buildMcpRegistry, mcpTransport } from '../lib/core.js';
2
+ import { loadConfig, buildMcpRegistry, mcpTransport, Store, databasePath } from '../lib/core.js';
3
3
  import { parse } from '../lib/util.js';
4
4
  import { color } from '../ui/color.js';
5
5
 
@@ -9,6 +9,26 @@ const OPTIONS = {
9
9
  help: { type: 'boolean', short: 'h' },
10
10
  };
11
11
 
12
+ /** Servidores de `tamnora.json` + los guardados desde la consola (gana la base). */
13
+ function mergedServers(config, store) {
14
+ const fromConfig = Array.isArray(config.mcp) ? config.mcp : [];
15
+ const fromStore = store.listMcpServers().map((row) => ({
16
+ id: row.id,
17
+ name: row.name ?? row.id,
18
+ type: row.transport ?? 'stdio',
19
+ command: row.command ?? undefined,
20
+ args: Array.isArray(row.config?.args) ? row.config.args : [],
21
+ url: row.url ?? undefined,
22
+ env: row.config?.env ?? undefined,
23
+ inheritEnv: row.config?.inheritEnv === true,
24
+ enabled: row.enabled !== false,
25
+ }));
26
+ const byId = new Map();
27
+ for (const server of fromConfig) if (server?.id) byId.set(server.id, server);
28
+ for (const server of fromStore) byId.set(server.id, server);
29
+ return [...byId.values()].filter((server) => server.enabled !== false);
30
+ }
31
+
12
32
  export async function mcpCommand(argv) {
13
33
  const { values } = parse(argv, OPTIONS);
14
34
  if (values.help) {
@@ -18,11 +38,17 @@ export async function mcpCommand(argv) {
18
38
 
19
39
  const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
20
40
  const config = await loadConfig(cwd);
21
- const servers = Array.isArray(config.mcp) ? config.mcp : [];
41
+ const store = new Store(databasePath(cwd));
42
+ let servers;
43
+ try {
44
+ servers = mergedServers(config, store);
45
+ } finally {
46
+ store.close?.();
47
+ }
22
48
 
23
49
  if (!servers.length) {
24
50
  if (values.json) process.stdout.write(`${JSON.stringify({ servers: [] }, null, 2)}\n`);
25
- else process.stdout.write(color.dim('No hay servidores MCP configurados en tamnora.json.\n'));
51
+ else process.stdout.write(color.dim('No hay servidores MCP habilitados (tamnora.json + consola).\n'));
26
52
  return 0;
27
53
  }
28
54
 
@@ -93,7 +119,8 @@ function safeTransport(server) {
93
119
  function printHelp() {
94
120
  console.log(`Uso: tamnora mcp [opciones]
95
121
 
96
- Conecta a los servidores MCP de tamnora.json y lista sus herramientas.
122
+ Conecta a los servidores MCP habilitados (de tamnora.json y de la consola) y
123
+ lista sus herramientas.
97
124
 
98
125
  Opciones:
99
126
  --dir <path> Directorio del proyecto (tamnora.json)
@@ -9,7 +9,7 @@ import {
9
9
  scopeDirective,
10
10
  } from '../lib/core.js';
11
11
  import { parse, databasePath, num } from '../lib/util.js';
12
- import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
12
+ import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
13
13
  import { color } from '../ui/color.js';
14
14
 
15
15
  const OPTIONS = {
@@ -31,11 +31,12 @@ export async function replCommand(argv) {
31
31
 
32
32
  const instructions = await loadInstructions(cwd, config.instructions);
33
33
  let systemExtra = '';
34
+ let mcpBlock = '';
34
35
  const maxRounds = num(values['max-steps'], config.maxRounds);
35
36
  const store = new Store(databasePath(cwd));
36
37
  let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
37
38
  const buildPrompt = () =>
38
- buildSystemPrompt(instructions, [systemExtra, scopeDirective(strict)].filter(Boolean).join('\n\n'));
39
+ buildSystemPrompt(instructions, [systemExtra, mcpBlock, scopeDirective(strict)].filter(Boolean).join('\n\n'));
39
40
  const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
40
41
  let providerId = resolved.providerId;
41
42
  let model = resolved.model;
@@ -76,6 +77,8 @@ export async function replCommand(argv) {
76
77
  process.stdout.write(color.yellow(`MCP: ${err.message}\n`));
77
78
  }
78
79
  }
80
+ const { mode: mcpMode, pinned: mcpPinned } = mcpSettings(store, config);
81
+ mcpBlock = mcpIndexBlock(mcp, { mode: mcpMode });
79
82
  const enabled = values['no-tools']
80
83
  ? []
81
84
  : values.tools
@@ -90,6 +93,8 @@ export async function replCommand(argv) {
90
93
  yes: Boolean(values.yes),
91
94
  onAsk,
92
95
  mcp,
96
+ mcpMode,
97
+ mcpPinned,
93
98
  });
94
99
 
95
100
  process.stdout.write(color.dim(`Tamnora REPL · ${providerId}/${model}\n`));
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry, scopeDirective } from '../lib/core.js';
3
3
  import { parse, databasePath, num } from '../lib/util.js';
4
- import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
4
+ import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
5
5
  import { color } from '../ui/color.js';
6
6
 
7
7
  const OPTIONS = {
@@ -64,12 +64,15 @@ export async function runCommand(argv) {
64
64
  : Array.isArray(config.tools) && config.tools.length
65
65
  ? config.tools
66
66
  : undefined;
67
+ const { mode: mcpMode, pinned: mcpPinned } = mcpSettings(store, config);
67
68
  const { definitions: toolDefs, execute } = createTools({
68
69
  root: cwd,
69
70
  enabled,
70
71
  permissions: config.permissions,
71
72
  yes: Boolean(values.yes),
72
73
  mcp,
74
+ mcpMode,
75
+ mcpPinned,
73
76
  });
74
77
 
75
78
  let sessionId = values.session;
@@ -108,7 +111,7 @@ export async function runCommand(argv) {
108
111
  customProviders,
109
112
  systemPrompt: buildSystemPrompt(
110
113
  instructions,
111
- [values.system, scopeDirective(strict)].filter(Boolean).join('\n\n')
114
+ [values.system, mcpIndexBlock(mcp, { mode: mcpMode }), scopeDirective(strict)].filter(Boolean).join('\n\n')
112
115
  ),
113
116
  history,
114
117
  input: prompt,
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode, scopeDirective } from '../lib/core.js';
3
3
  import { parse, databasePath, num } from '../lib/util.js';
4
- import { buildSystemPrompt, resolveCliProvider } from '../lib/runtime.js';
4
+ import { buildSystemPrompt, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
5
5
  import { runTui } from '../tui/app.js';
6
6
  import { color } from '../ui/color.js';
7
7
 
@@ -55,6 +55,8 @@ export async function tuiCommand(argv) {
55
55
  process.stderr.write(color.yellow(`MCP: ${err.message}\n`));
56
56
  }
57
57
  }
58
+ const { mode: mcpMode, pinned: mcpPinned } = mcpSettings(store, config);
59
+ const mcpBlock = mcpIndexBlock(mcp, { mode: mcpMode });
58
60
 
59
61
  const baseEnabled = values['no-tools']
60
62
  ? []
@@ -69,7 +71,10 @@ export async function tuiCommand(argv) {
69
71
  const enabledFor = (m) =>
70
72
  m.tools ? (baseEnabled ? baseEnabled.filter((tool) => m.tools.includes(tool)) : m.tools) : baseEnabled;
71
73
  const promptFor = (m) =>
72
- buildSystemPrompt(instructions, [values.system, m.systemPrompt, scopeDirective(strict)].filter(Boolean).join('\n\n'));
74
+ buildSystemPrompt(
75
+ instructions,
76
+ [values.system, m.systemPrompt, mcpBlock, scopeDirective(strict)].filter(Boolean).join('\n\n')
77
+ );
73
78
  const modeContext = (id) => {
74
79
  const m = resolveMode(id);
75
80
  return { label: m.label, enabled: enabledFor(m), systemPrompt: promptFor(m) };
@@ -101,6 +106,8 @@ export async function tuiCommand(argv) {
101
106
  enabled: enabledFor(mode),
102
107
  permissions: config.permissions,
103
108
  mcp,
109
+ mcpMode,
110
+ mcpPinned,
104
111
  yes: Boolean(values.yes),
105
112
  },
106
113
  store,
@@ -1,4 +1,26 @@
1
- import { runAgent, resolveProviderRuntime, composeProviderSettings, resolveApiKey } from './core.js';
1
+ import { runAgent, resolveProviderRuntime, composeProviderSettings, resolveApiKey, useLazyMcp, buildMcpIndex, MCP_AUTO_THRESHOLD } from './core.js';
2
+
3
+ const MCP_MODES = new Set(['auto', 'eager', 'lazy']);
4
+
5
+ /** Modo y tools fijadas de MCP (setting de la base, con fallback a tamnora.json). */
6
+ export function mcpSettings(store, config = {}) {
7
+ const raw = String(store.getSetting('mcpMode') ?? config.mcpMode ?? 'auto');
8
+ const mode = MCP_MODES.has(raw) ? raw : 'auto';
9
+ const stored = store.getSetting('mcpPinned');
10
+ const pinned = Array.isArray(stored)
11
+ ? stored.map(String)
12
+ : Array.isArray(config.mcpPinned)
13
+ ? config.mcpPinned.map(String)
14
+ : [];
15
+ return { mode, pinned };
16
+ }
17
+
18
+ /** Bloque de indice de MCP para el prompt (vacio si el modo es completo). */
19
+ export function mcpIndexBlock(mcp, { mode = 'auto' } = {}) {
20
+ const total = mcp?.definitions?.length ?? 0;
21
+ if (!total || !useLazyMcp(mode, total, MCP_AUTO_THRESHOLD)) return '';
22
+ return buildMcpIndex(mcp.tools ?? []);
23
+ }
2
24
 
3
25
  /**
4
26
  * Resolve provider/model/apiKey/baseUrl for CLI commands, merging the active
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { parseTunnelmoleUrls, parseTailscaleStatus, startTunnel } from '../src/lib/tunnel.js';
3
+ import { parseTunnelmoleUrls, parseTailscaleStatus, startTunnel, tailscalePreviewUrl } from '../src/lib/tunnel.js';
4
4
  import { qrTerminal } from '../src/lib/qr.js';
5
5
 
6
6
  test('parseTunnelmoleUrls extracts the https URL', () => {
@@ -32,6 +32,18 @@ test('startTunnel rechaza el proveedor retirado (relay)', async () => {
32
32
  await assert.rejects(() => startTunnel(4517, 'relay'), /no soportado/);
33
33
  });
34
34
 
35
+ test('startTunnel con provider none no abre nada', async () => {
36
+ const tun = await startTunnel(4517, 'none', { preview: true });
37
+ assert.equal(tun.provider, 'none');
38
+ assert.equal(tun.url, '');
39
+ });
40
+
41
+ test('tailscalePreviewUrl usa el 8443 y omite el 443', () => {
42
+ assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net'), 'https://pc.tailnet.ts.net:8443');
43
+ assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net', 9000), 'https://pc.tailnet.ts.net:9000');
44
+ assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net', 443), 'https://pc.tailnet.ts.net');
45
+ });
46
+
35
47
  test('qrTerminal renders a block matrix for a URL', () => {
36
48
  const art = qrTerminal('https://abc-123.tunnelmole.net', 1);
37
49
  assert.ok(art.length > 0);
@@ -23,6 +23,9 @@ export const MODES = {
23
23
  'screenshot',
24
24
  'console_errors',
25
25
  'preview_url',
26
+ 'ask_user',
27
+ 'offer_choices',
28
+ 'show_plan',
26
29
  'git_status',
27
30
  'git_diff',
28
31
  'git_log',
@@ -21,9 +21,11 @@ export const DEFAULT_CONFIG = {
21
21
  tools: [],
22
22
  permissions: {},
23
23
  mcp: [],
24
+ mcpMode: 'auto',
25
+ mcpPinned: [],
24
26
  server: { host: '127.0.0.1', port: 4517 },
25
27
  processes: { enabled: true, allow: [] },
26
- preview: { provider: 'tunnelmole' },
28
+ preview: { provider: 'tailscale', tailscalePort: 8443, fallback: true },
27
29
  telegram: { token: '', allowedChatIds: [] },
28
30
  agents: { maxRounds: 6 },
29
31
  };
@@ -4,6 +4,8 @@ export {
4
4
  redactDeep,
5
5
  secretValues,
6
6
  isSecretName,
7
+ isSensitiveEnvName,
8
+ scrubEnv,
7
9
  maskSecret,
8
10
  maskEnv,
9
11
  REDACTED,
@@ -77,8 +79,15 @@ export {
77
79
  TOOL_NAMES,
78
80
  DEFAULT_PERMISSIONS,
79
81
  evaluatePermission,
82
+ bashGuard,
80
83
  resolveInRoot,
81
84
  toRel,
85
+ MCP_ROUTER_NAME,
86
+ MCP_AUTO_THRESHOLD,
87
+ useLazyMcp,
88
+ buildMcpIndex,
89
+ mcpRouterDefinition,
90
+ planMcpTools,
82
91
  } from './tools/index.js';
83
92
  export { Store, openDatabase } from './storage/sqlite.js';
84
93
  export { runTask } from './eval/runTask.js';
@@ -96,7 +105,8 @@ export {
96
105
  buildSkillsPrompt,
97
106
  buildSkillsIndex,
98
107
  } from './skills/index.js';
99
- export { parseSkillRef, fetchSkillFromGithub } from './skills/remote.js';
108
+ export { parseSkillRef, fetchSkillFromGithub, searchSkillsGithub } from './skills/remote.js';
109
+ export { SKILLS_CATALOG, skillCatalogEntry } from './skills/catalog.js';
100
110
  export {
101
111
  MEMORY_DIR,
102
112
  MEMORY_INDEX_NAME,
@@ -130,16 +140,20 @@ export {
130
140
  searchMcpRegistry,
131
141
  getMcpRegistryServer,
132
142
  registryServerToConfig,
143
+ registryServerToCandidate,
133
144
  normalizeMcpConfig,
134
145
  } from './mcp/registry.js';
146
+ export { MCP_CATALOG, mcpCatalogEntry } from './mcp/catalog.js';
135
147
  export {
136
148
  parseTunnelmoleUrls,
137
149
  parseTailscaleStatus,
138
150
  probeTailscale,
139
151
  resetTailscaleServe,
140
152
  startTailscaleServe,
153
+ startTailscalePreview,
141
154
  startTunnelmole,
142
155
  startTunnel,
156
+ tailscalePreviewUrl,
143
157
  tunnelState,
144
158
  tunnelActive,
145
159
  } from './tunnel.js';
@@ -157,8 +171,8 @@ export {
157
171
  export { reconocerGoogle, wavToPcm } from './stt/google.js';
158
172
  export { pidAlive, killPidTree } from './pid.js';
159
173
  export { openBrowser, openerFor } from './open.js';
160
- export { detectProcs, packageManager, DEFAULT_PROCESS_ALLOW, staticServerChip } from './procs/detect.js';
161
- export { killPort } from './procs/kill.js';
174
+ export { detectProcs, loadProjectProcs, packageManager, DEFAULT_PROCESS_ALLOW, staticServerChip, PROCS_FILE } from './procs/detect.js';
175
+ export { killPort, portInfo, pidsOnPort } from './procs/kill.js';
162
176
  export {
163
177
  ROOT_PACKAGE_NAME,
164
178
  readPackageVersion,
@@ -0,0 +1,79 @@
1
+ // Catalogo curado de servidores MCP sugeridos. No se instalan solos: la consola
2
+ // (o el agente) los agrega deshabilitados y el usuario decide habilitarlos.
3
+ // Los `env` con valor "env:VAR" se resuelven desde el entorno al arrancar el MCP.
4
+
5
+ export const MCP_CATALOG = [
6
+ {
7
+ id: 'playwright',
8
+ name: 'Playwright (navegador)',
9
+ description:
10
+ 'Automatiza un navegador real: webs con JavaScript, formularios y scraping. Complementa las tools screenshot/console_errors.',
11
+ type: 'stdio',
12
+ command: 'npx',
13
+ args: ['-y', '@playwright/mcp@latest'],
14
+ enabled: false,
15
+ },
16
+ {
17
+ id: 'fetch',
18
+ name: 'Fetch (webs con JS)',
19
+ description: 'Trae cualquier URL y la convierte a texto, incluidas las que renderizan con JavaScript. Requiere uv.',
20
+ type: 'stdio',
21
+ command: 'uvx',
22
+ args: ['mcp-server-fetch'],
23
+ enabled: false,
24
+ },
25
+ {
26
+ id: 'github',
27
+ name: 'GitHub',
28
+ description: 'Repos, issues, PRs y búsqueda en GitHub. Necesita un token personal.',
29
+ type: 'stdio',
30
+ command: 'npx',
31
+ args: ['-y', '@modelcontextprotocol/server-github'],
32
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: 'env:GITHUB_PERSONAL_ACCESS_TOKEN' },
33
+ requiredEnv: ['GITHUB_PERSONAL_ACCESS_TOKEN'],
34
+ enabled: false,
35
+ },
36
+ {
37
+ id: 'memory',
38
+ name: 'Memory (grafo)',
39
+ description: 'Memoria de conocimiento persistente: entidades y relaciones que sobreviven entre sesiones.',
40
+ type: 'stdio',
41
+ command: 'npx',
42
+ args: ['-y', '@modelcontextprotocol/server-memory'],
43
+ enabled: false,
44
+ },
45
+ {
46
+ id: 'sequential-thinking',
47
+ name: 'Sequential Thinking',
48
+ description: 'Razonamiento paso a paso para descomponer problemas complejos.',
49
+ type: 'stdio',
50
+ command: 'npx',
51
+ args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
52
+ enabled: false,
53
+ },
54
+ {
55
+ id: 'sqlite',
56
+ name: 'SQLite',
57
+ description: 'Consulta una base SQLite. Reemplazá la ruta por tu archivo .db. Requiere uv.',
58
+ type: 'stdio',
59
+ command: 'uvx',
60
+ args: ['mcp-server-sqlite', '--db-path', 'ruta/a/base.db'],
61
+ enabled: false,
62
+ },
63
+ {
64
+ id: 'time',
65
+ name: 'Time',
66
+ description: 'Hora actual y conversión de zonas horarias. Requiere uv.',
67
+ type: 'stdio',
68
+ command: 'uvx',
69
+ args: ['mcp-server-time'],
70
+ enabled: false,
71
+ },
72
+ ];
73
+
74
+ /** Entrada del catalogo por id, como copia (siempre deshabilitada). */
75
+ export function mcpCatalogEntry(id) {
76
+ const entry = MCP_CATALOG.find((item) => item.id === id);
77
+ if (!entry) return null;
78
+ return { ...entry, args: [...(entry.args ?? [])], env: entry.env ? { ...entry.env } : undefined, enabled: false };
79
+ }
@@ -70,12 +70,19 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
70
70
 
71
71
  const map = new Map();
72
72
  const definitions = [];
73
+ const tools = [];
73
74
  const sessions = [];
74
75
  const errors = [];
75
76
 
76
- const add = (name, definition, call) => {
77
+ const add = (server, name, definition, call) => {
77
78
  map.set(name, call);
78
79
  definitions.push(definition);
80
+ tools.push({
81
+ name,
82
+ server: server.id,
83
+ serverName: server.name ?? server.id,
84
+ description: definition.function?.description ?? '',
85
+ });
79
86
  };
80
87
 
81
88
  for (const r of results) {
@@ -101,19 +108,21 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
101
108
 
102
109
  for (const tool of session.tools ?? []) {
103
110
  const name = qualifiedToolName(server.id, tool.name);
104
- add(name, mcpToolToDefinition(tool, server, name), async (args) => (await reconnect()).callTool(tool.name, args));
111
+ add(server, name, mcpToolToDefinition(tool, server, name), async (args) => (await reconnect()).callTool(tool.name, args));
105
112
  }
106
113
 
107
114
  if (session.capabilities?.resources || session.resources?.length) {
108
115
  const label = server.name ?? server.id;
109
116
  const listName = qualifiedToolName(server.id, 'list_resources');
110
117
  add(
118
+ server,
111
119
  listName,
112
120
  syntheticDefinition(listName, `Lista los recursos MCP del servidor ${label}.`),
113
121
  async () => formatResources(await (await reconnect()).listResources())
114
122
  );
115
123
  const readName = qualifiedToolName(server.id, 'read_resource');
116
124
  add(
125
+ server,
117
126
  readName,
118
127
  syntheticDefinition(readName, `Lee un recurso MCP del servidor ${label} a partir de su uri.`, {
119
128
  type: 'object',
@@ -128,12 +137,14 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
128
137
  const label = server.name ?? server.id;
129
138
  const listName = qualifiedToolName(server.id, 'list_prompts');
130
139
  add(
140
+ server,
131
141
  listName,
132
142
  syntheticDefinition(listName, `Lista los prompts MCP del servidor ${label}.`),
133
143
  async () => formatPrompts(await (await reconnect()).listPrompts())
134
144
  );
135
145
  const getName = qualifiedToolName(server.id, 'get_prompt');
136
146
  add(
147
+ server,
137
148
  getName,
138
149
  syntheticDefinition(getName, `Obtiene un prompt MCP del servidor ${label}.`, {
139
150
  type: 'object',
@@ -150,6 +161,7 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
150
161
 
151
162
  return {
152
163
  definitions,
164
+ tools,
153
165
  sessions,
154
166
  errors,
155
167
  resources: sessions.flatMap((s) => s.resources ?? []),