@danieltmn/tamnora 0.25.0 → 0.26.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 (110) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +13 -25
  3. package/SECURITY.md +3 -4
  4. package/harness/packages/cli/src/cli.js +0 -3
  5. package/harness/packages/cli/src/commands/bridge.js +0 -1
  6. package/harness/packages/cli/src/commands/repl.js +20 -4
  7. package/harness/packages/cli/src/commands/run.js +6 -2
  8. package/harness/packages/cli/src/commands/server.js +1 -1
  9. package/harness/packages/cli/src/commands/tui.js +6 -2
  10. package/harness/packages/cli/src/lib/runtime.js +2 -0
  11. package/harness/packages/cli/src/lib/util.js +2 -2
  12. package/harness/packages/cli/src/tui/app.js +24 -4
  13. package/harness/packages/cli/src/tui/layout.js +1 -1
  14. package/harness/packages/cli/test/tunnel.test.js +5 -32
  15. package/harness/packages/core/src/agent/runAgent.js +5 -2
  16. package/harness/packages/core/src/agents/index.js +113 -0
  17. package/harness/packages/core/src/config/app.js +0 -4
  18. package/harness/packages/core/src/config/home.js +1 -20
  19. package/harness/packages/core/src/config/index.js +6 -8
  20. package/harness/packages/core/src/extensions/load.js +22 -0
  21. package/harness/packages/core/src/frontmatter.js +32 -0
  22. package/harness/packages/core/src/hooks/index.js +2 -16
  23. package/harness/packages/core/src/index.js +12 -14
  24. package/harness/packages/core/src/plugins/index.js +2 -16
  25. package/harness/packages/core/src/procs/project.js +68 -0
  26. package/harness/packages/core/src/procs/static.js +17 -0
  27. package/harness/packages/core/src/providers/index.js +3 -1
  28. package/harness/packages/core/src/providers/ollama.js +4 -1
  29. package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
  30. package/harness/packages/core/src/providers/openrouter.js +5 -1
  31. package/harness/packages/core/src/providers/presets.js +4 -3
  32. package/harness/packages/core/src/providers/reasoning.js +58 -0
  33. package/harness/packages/core/src/rules/index.js +8 -27
  34. package/harness/packages/core/src/skills/index.js +8 -19
  35. package/harness/packages/core/src/storage/sqlite.js +595 -690
  36. package/harness/packages/core/src/tunnel.js +9 -147
  37. package/harness/packages/core/test/agents.test.js +111 -0
  38. package/harness/packages/core/test/home.test.js +2 -35
  39. package/harness/packages/core/test/procs.test.js +3 -64
  40. package/harness/packages/core/test/reasoning.test.js +43 -0
  41. package/harness/packages/core/test/storage.test.js +1 -12
  42. package/harness/packages/server/public/app.js +39 -19
  43. package/harness/packages/server/public/appearance.js +2 -56
  44. package/harness/packages/server/public/boot.js +2 -2
  45. package/harness/packages/server/public/cards.js +2 -21
  46. package/harness/packages/server/public/chat.js +1 -16
  47. package/harness/packages/server/public/chatFormat.js +0 -11
  48. package/harness/packages/server/public/chrome.js +95 -5
  49. package/harness/packages/server/public/commands.js +1 -2
  50. package/harness/packages/server/public/core.js +1 -1
  51. package/harness/packages/server/public/explorer.js +7 -10
  52. package/harness/packages/server/public/icons (1).zip +0 -0
  53. package/harness/packages/server/public/index.html +1228 -1237
  54. package/harness/packages/server/public/landing/boot.js +2 -3
  55. package/harness/packages/server/public/landing/index.html +3 -367
  56. package/harness/packages/server/public/sessions.js +1 -0
  57. package/harness/packages/server/public/settings.js +1255 -1155
  58. package/harness/packages/server/public/settingsNav.js +1 -1
  59. package/harness/packages/server/public/styles.css +2341 -2867
  60. package/harness/packages/server/public/system.js +356 -495
  61. package/harness/packages/server/public/voice.js +6 -22
  62. package/harness/packages/server/src/auth.js +4 -15
  63. package/harness/packages/server/src/cli.js +1 -1
  64. package/harness/packages/server/src/lib/agents.js +60 -0
  65. package/harness/packages/server/src/lib/appearance.js +3 -22
  66. package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
  67. package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
  68. package/harness/packages/server/src/lib/definitions.js +40 -0
  69. package/harness/packages/server/src/lib/prompt.js +11 -3
  70. package/harness/packages/server/src/lib/scheduler.js +4 -0
  71. package/harness/packages/server/src/lib/sessionView.js +1 -0
  72. package/harness/packages/server/src/lib/skills.js +27 -21
  73. package/harness/packages/server/src/lib/tasks.js +71 -23
  74. package/harness/packages/server/src/lib/ttsSettings.js +2 -2
  75. package/harness/packages/server/src/lib/workspace.js +9 -11
  76. package/harness/packages/server/src/routes/agents.js +88 -0
  77. package/harness/packages/server/src/routes/chat.js +9 -44
  78. package/harness/packages/server/src/routes/preview.js +5 -22
  79. package/harness/packages/server/src/routes/procs.js +2 -4
  80. package/harness/packages/server/src/routes/providers.js +7 -0
  81. package/harness/packages/server/src/routes/sessions.js +3 -1
  82. package/harness/packages/server/src/routes/tts.js +1 -1
  83. package/harness/packages/server/src/server.js +8 -5
  84. package/harness/packages/server/test/agents.test.js +194 -0
  85. package/harness/packages/server/test/appearance-route.test.js +3 -3
  86. package/harness/packages/server/test/appearance.test.js +12 -38
  87. package/harness/packages/server/test/chatFormat.test.js +0 -8
  88. package/harness/packages/server/test/consoleTabs.test.js +2 -2
  89. package/harness/packages/server/test/ops.test.js +8 -56
  90. package/harness/packages/server/test/preview.test.js +2 -42
  91. package/harness/packages/server/test/security.test.js +194 -370
  92. package/harness/packages/server/test/settingsNav.test.js +2 -2
  93. package/harness/packages/server/test/tts.test.js +4 -4
  94. package/harness/tamnora.example.json +2 -1
  95. package/package.json +2 -2
  96. package/harness/packages/cli/src/commands/eval.js +0 -186
  97. package/harness/packages/core/src/eval/dataset.js +0 -47
  98. package/harness/packages/core/src/eval/judge.js +0 -39
  99. package/harness/packages/core/src/eval/report.js +0 -48
  100. package/harness/packages/core/src/eval/runTask.js +0 -62
  101. package/harness/packages/core/src/eval/runner.js +0 -153
  102. package/harness/packages/core/src/eval/scorers.js +0 -217
  103. package/harness/packages/core/src/eval/task.js +0 -59
  104. package/harness/packages/core/src/procs/detect.js +0 -149
  105. package/harness/packages/core/test/eval.test.js +0 -144
  106. package/harness/packages/server/src/lib/advisor.js +0 -198
  107. package/harness/packages/server/src/routes/advisor.js +0 -79
  108. package/harness/packages/server/src/routes/eval.js +0 -100
  109. package/harness/packages/server/test/advisor.test.js +0 -249
  110. package/harness/scripts/example-dataset.jsonl +0 -4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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.26.0] - 2026-11-04
9
+
10
+ ### Cambios
11
+
12
+ - refactor: retirar el eval del proyecto
13
+ - refactor: retirar el asesor y la evaluacion desde el server
14
+ - refactor(temas): dejar 4 temas y 2 tipografias
15
+ - refactor: quitar la autodeteccion de procesos de desarrollo
16
+ - refactor: unificar registros markdown y carga de extensiones
17
+ - refactor: quitar alias y compatibilidad que ya no compra nada
18
+ - refactor: retirar el tunel publico (tunnelmole)
19
+
20
+ ## [0.25.1] - 2026-11-04
21
+
22
+ ### Cambios
23
+
24
+ - feat: esfuerzo de razonamiento y subagentes en archivos
25
+
8
26
  ## [0.25.0] - 2026-11-01
9
27
 
10
28
  ### Cambios
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Tamnora Harness
2
2
 
3
3
  Harness local para IAs: un agente **CLI-first** (estilo opencode) en Node puro,
4
- con TUI, consola web, tools locales, MCP y evaluacion de modelos.
4
+ con TUI, consola web, tools locales y MCP.
5
5
 
6
6
  - **Sin dependencias**: solo modulos de Node (`node:sqlite`, `node:crypto`, ...).
7
7
  - **Local**: corre en tu maquina y guarda el estado en tu casa local.
@@ -14,10 +14,6 @@ con TUI, consola web, tools locales, MCP y evaluacion de modelos.
14
14
  bloques de codigo ni tablas) y mandar audio bajo pedido (`speak`,
15
15
  `/audioresumen`). El **modo Voz** habilita **manos libres**: hablás y responde en
16
16
  audio, como una conversacion (y solo escribe notas `.md`/`.json`).
17
- - **Asesor**: una capa de comentario sobre el run que interpreta lo que hizo el
18
- agente y, en **un solo mensaje** estilo Jarvis, resume lo hecho y —si aporta—
19
- avisa de un problema en el codigo, explica un proceso o da un consejo (tarjeta en
20
- el chat y voz opcional). No lee la respuesta.
21
17
  - **Actualizar desde la consola**: la consola avisa cuando hay una version nueva
22
18
  y la instala con un click (el server se reinicia solo).
23
19
  - **Memoria y reglas**: memoria durable por proyecto (`memory`) y reglas por path
@@ -48,7 +44,6 @@ Requiere **Node.js >= 22.13** (por `node:sqlite`).
48
44
  tamnora init # crea la configuracion (proveedor, modelo)
49
45
  tamnora # consola TUI full-screen (REPL si no hay TTY)
50
46
  tamnora run "hola" # tarea headless
51
- tamnora eval <dataset.jsonl> # evaluacion de modelos
52
47
  tamnora mcp [--json] # servidores MCP: tools, recursos y prompts
53
48
  tamnora serve # consola web
54
49
  ```
@@ -103,8 +98,7 @@ Tamnora (override con `--home` o `TAMNORA_HOME`):
103
98
  ### Consola web
104
99
 
105
100
  `tamnora serve` sirve la consola en `http://127.0.0.1:4517`. Las pestanas
106
- principales son `chat`, `proveedores` y `ajustes`; `eval` y `runs` viven en el
107
- menu "mas" (los tres puntitos). En el chat:
101
+ principales son `chat`, `proveedores` y `ajustes`. En el chat:
108
102
 
109
103
  - **Paneles laterales** de sesiones y archivos, colapsables desde cada panel (con
110
104
  un tirador en el borde para reabrirlos).
@@ -131,11 +125,9 @@ menu "mas" (los tres puntitos). En el chat:
131
125
  - **Solo lo pedido (estricto)**: el toggle de la pestaña Agente (o `PUT
132
126
  /api/settings` con `strictScope`) hace que el agente se limite a lo solicitado,
133
127
  sin mejoras ni cambios extra; se combina con Plan o Build.
134
- - **Audio**: un unico reproductor encola la narracion, el tool `speak`, la
135
- respuesta final y los consejos del asesor, y reproduce de a uno con pausa y
128
+ - **Audio**: un unico reproductor encola la narracion, el tool `speak` y la
129
+ respuesta final, y reproduce de a uno con pausa y
136
130
  detener (Ajustes -> Voz).
137
- - **Asesor**: en Ajustes -> Asesor se activa una capa que comenta el turno
138
- (problemas de codigo / explicaciones / consejos) con proveedor y modelo activos.
139
131
  - **Actualizar**: cuando npm tiene una version nueva aparece un banner para
140
132
  actualizar con un click; el server se reinicia solo.
141
133
  - **Comandos**: el input muestra un menu al escribir `/` (`/new`, `/init`, `/goal`,
@@ -191,7 +183,7 @@ Los tres se renderizan en vivo y se reconstruyen al recargar la consola.
191
183
 
192
184
  - **Hooks** (`hooks[]` en `tamnora.json`, p. ej. `[".tamnora/hooks/*.mjs"]`):
193
185
  observan el ciclo de vida (`run.start/end`, `tool.before/after`,
194
- `session.compacting/compacted`, `advice`). Corren aislados y con los datos
186
+ `session.compacting/compacted`). Corren aislados y con los datos
195
187
  redactados. Es **opt-in explícito**: la presencia de archivos en el proyecto no
196
188
  ejecuta nada.
197
189
  - **Plugins** (`plugins[]`, p. ej. `[".tamnora/plugins/*.mjs"]`): agregan **tools
@@ -200,15 +192,11 @@ Los tres se renderizan en vivo y se reconstruyen al recargar la consola.
200
192
 
201
193
  ### Preview
202
194
 
203
- La consola puede levantar un dev server (deteccion de scripts `dev`/`start`,
204
- Laravel, PHP) y abrir un tunel al puerto para verlo desde el celular. Para sitios
205
- **estaticos** (un `index.html` sin script de dev) ofrece un chip "sitio estatico"
206
- que los sirve con un servidor sin dependencias (`harness/scripts/static-server.mjs`).
207
-
208
- En un **monorepo** donde cada parte arranca distinto, el proyecto declara sus
209
- procesos en `.tamnora/procs.json` (relativo al proyecto seleccionado); salen
210
- primero, se suman a los detectados y **solo** se pueden lanzar los que pasan por
211
- la allowlist `processes.allow`:
195
+ La consola puede levantar un dev server declarado por el proyecto en
196
+ `.tamnora/procs.json` y abrir un tunel al puerto para verlo desde el celular.
197
+ No autodetecta nada: el proyecto declara **solo** los procesos que quiere ofrecer
198
+ (relativo al proyecto seleccionado) y solo se pueden lanzar los que pasan por la
199
+ allowlist `processes.allow`:
212
200
 
213
201
  ```jsonc
214
202
  { "processes": [
@@ -259,8 +247,8 @@ tamnora bridge --stop # tailscale serve reset
259
247
  Sigue protegido por el login del server: configura un usuario antes de exponerlo
260
248
  (ver [`SECURITY.md`](SECURITY.md)).
261
249
 
262
- Los tuneles publicos (tunnelmole, relay) fueron **retirados**; `tunnelmole` se
263
- conserva solo para el feature de *preview*.
250
+ El acceso remoto es solo por Tailscale: no hay tuneles publicos ni exposicion a
251
+ Internet.
264
252
 
265
253
  ### Telegram (canales de mensajeria)
266
254
 
@@ -337,7 +325,7 @@ tambien esta disponible en modo `plan` (exploracion delegada).
337
325
  ## Documentacion
338
326
 
339
327
  - [`CHANGELOG.md`](CHANGELOG.md) — historial de versiones.
340
- - [`AGENTS.md`](AGENTS.md) — arquitectura del harness, tools, MCP, evaluacion y
328
+ - [`AGENTS.md`](AGENTS.md) — arquitectura del harness, tools, MCP y
341
329
  convenciones.
342
330
  - [`harness/tamnora.example.json`](harness/tamnora.example.json) — plantilla de
343
331
  configuracion.
package/SECURITY.md CHANGED
@@ -20,10 +20,9 @@ asi que se lo debe tratar como codigo con acceso a tu usuario.
20
20
  **usuario** con contrasena fuerte (defensa en profundidad). Como entra por
21
21
  loopback, una vez autenticado se alcanza toda la consola y las tools
22
22
  (explorador de archivos, shell, git): tratalo como acceso completo a tu PC.
23
- - **Tuneles publicos (retirados)**: las versiones anteriores usaban tunnelmole y
24
- un relay propio para exponer la consola desde Internet. Se retiraron; el acceso
25
- remoto hoy es solo por Tailscale. `tunnelmole` sobrevive unicamente para el
26
- feature de *preview* de dev servers, que no expone la consola.
23
+ - **Sin tuneles publicos**: las versiones anteriores usaban tunnelmole y un relay
24
+ propio para exponer la consola desde Internet. Se retiraron por completo; el
25
+ acceso remoto es solo por Tailscale, que no expone nada a Internet.
27
26
  - **Credenciales**: las API keys de proveedores se guardan en la base SQLite de
28
27
  la casa local (fuera del repo) **en texto plano**; protege el archivo con los
29
28
  permisos del sistema. Para no persistirlas, usa `"apiKey": "env:VAR"` y
@@ -1,6 +1,5 @@
1
1
  import { runCommand } from './commands/run.js';
2
2
  import { replCommand } from './commands/repl.js';
3
- import { evalCommand } from './commands/eval.js';
4
3
  import { initCommand } from './commands/init.js';
5
4
  import { mcpCommand } from './commands/mcp.js';
6
5
  import { serveCommand } from './commands/serve.js';
@@ -20,7 +19,6 @@ const COMMANDS = {
20
19
  run: runCommand,
21
20
  repl: replCommand,
22
21
  tui: tuiCommand,
23
- eval: evalCommand,
24
22
  init: initCommand,
25
23
  mcp: mcpCommand,
26
24
  serve: serveCommand,
@@ -70,7 +68,6 @@ function printHelp() {
70
68
  Uso:
71
69
  tamnora Consola full-screen (REPL si no hay TTY)
72
70
  tamnora run "<prompt>" Ejecutar una tarea headless
73
- tamnora eval <dataset.jsonl> Correr un dataset de evaluación
74
71
  tamnora mcp Listar servidores y herramientas MCP
75
72
  tamnora server [--init|--start|--reload|--stop|--status|--logs] Server en segundo plano (abre el navegador)
76
73
  tamnora user Usuario local (login del server)
@@ -73,7 +73,6 @@ async function cmdStart(values) {
73
73
  }
74
74
  if (!isTailscale(provider)) {
75
75
  console.error(`Proveedor no soportado: "${provider}". El acceso remoto usa Tailscale.`);
76
- console.error('Los tuneles publicos (tunnelmole/relay) fueron retirados.');
77
76
  return 1;
78
77
  }
79
78
 
@@ -7,6 +7,7 @@ import {
7
7
  createTools,
8
8
  buildMcpRegistry,
9
9
  scopeDirective,
10
+ normalizeEffort,
10
11
  } from '../lib/core.js';
11
12
  import { parse, databasePath, num } from '../lib/util.js';
12
13
  import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
@@ -21,6 +22,7 @@ const OPTIONS = {
21
22
  'no-tools': { type: 'boolean' },
22
23
  strict: { type: 'boolean' },
23
24
  'no-strict': { type: 'boolean' },
25
+ effort: { type: 'string' },
24
26
  yes: { type: 'boolean', short: 'y' },
25
27
  };
26
28
 
@@ -33,8 +35,9 @@ export async function replCommand(argv) {
33
35
  let systemExtra = '';
34
36
  let mcpBlock = '';
35
37
  const maxRounds = num(values['max-steps'], config.maxRounds);
36
- const store = new Store(databasePath(cwd));
38
+ const store = new Store(databasePath());
37
39
  let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
40
+ let effort = normalizeEffort(values.effort ?? store.getSetting('activeEffort') ?? config.effort);
38
41
  const buildPrompt = () =>
39
42
  buildSystemPrompt(instructions, [systemExtra, mcpBlock, scopeDirective(strict)].filter(Boolean).join('\n\n'));
40
43
  const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
@@ -97,7 +100,7 @@ export async function replCommand(argv) {
97
100
  mcpPinned,
98
101
  });
99
102
 
100
- process.stdout.write(color.dim(`Tamnora REPL · ${providerId}/${model}\n`));
103
+ process.stdout.write(color.dim(`Tamnora REPL · ${providerId}/${model}${effort ? ` · esfuerzo ${effort}` : ''}\n`));
101
104
  process.stdout.write(color.dim(`workspace ${cwd}\n`));
102
105
  process.stdout.write(color.dim(`session ${sessionId} · /help para comandos · Ctrl+C para salir\n\n`));
103
106
  rl.prompt();
@@ -120,7 +123,7 @@ export async function replCommand(argv) {
120
123
  } else if (cmd === 'help') {
121
124
  process.stdout.write(
122
125
  `${color.dim(
123
- '/model <id> /provider <id> /system <texto> /strict /clear /session /exit'
126
+ '/model <id> /provider <id> /system <texto> /estricto /esfuerzo <nivel> /clear /session /exit'
124
127
  )}\n`
125
128
  );
126
129
  } else if (cmd === 'model') {
@@ -132,11 +135,23 @@ export async function replCommand(argv) {
132
135
  } else if (cmd === 'system') {
133
136
  if (arg) systemExtra = arg;
134
137
  process.stdout.write(color.dim(`system prompt (${buildPrompt().length} chars)\n`));
135
- } else if (cmd === 'strict' || cmd === 'estricto') {
138
+ } else if (cmd === 'estricto') {
136
139
  if (arg) strict = /^(on|si|sí|1|true)$/i.test(arg);
137
140
  else strict = !strict;
138
141
  store.setSetting('strictScope', strict);
139
142
  process.stdout.write(color.dim(`solo lo pedido: ${strict ? 'activado' : 'desactivado'}\n`));
143
+ } else if (cmd === 'effort' || cmd === 'esfuerzo') {
144
+ if (arg) {
145
+ const level = normalizeEffort(arg);
146
+ if (!level && !/^(auto|default)$/i.test(arg)) {
147
+ process.stdout.write(color.red('nivel inválido: off | low | medium | high | max | auto\n'));
148
+ rl.prompt();
149
+ return;
150
+ }
151
+ effort = level;
152
+ store.setSetting('activeEffort', level || null);
153
+ }
154
+ process.stdout.write(color.dim(`esfuerzo: ${effort || 'auto'}\n`));
140
155
  } else if (cmd === 'clear') {
141
156
  history = [];
142
157
  process.stdout.write(color.dim('historial limpiado\n'));
@@ -167,6 +182,7 @@ export async function replCommand(argv) {
167
182
  maxDurationMs: config.maxDurationMs,
168
183
  maxOutputTokens: config.maxOutputTokens,
169
184
  promptCaching: config.promptCaching === true,
185
+ reasoningEffort: effort,
170
186
  tools: toolDefs,
171
187
  execute,
172
188
  onEvent: printer.onEvent,
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry, scopeDirective } from '../lib/core.js';
2
+ import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry, scopeDirective, normalizeEffort } from '../lib/core.js';
3
3
  import { parse, databasePath, num } from '../lib/util.js';
4
4
  import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
5
5
  import { color } from '../ui/color.js';
@@ -17,6 +17,7 @@ const OPTIONS = {
17
17
  'no-tools': { type: 'boolean' },
18
18
  strict: { type: 'boolean' },
19
19
  'no-strict': { type: 'boolean' },
20
+ effort: { type: 'string' },
20
21
  yes: { type: 'boolean', short: 'y' },
21
22
  help: { type: 'boolean', short: 'h' },
22
23
  };
@@ -37,7 +38,7 @@ export async function runCommand(argv) {
37
38
  const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
38
39
  const config = await loadConfig(cwd);
39
40
  const instructions = await loadInstructions(cwd, config.instructions);
40
- const store = new Store(databasePath(cwd));
41
+ const store = new Store(databasePath());
41
42
  const { providerId, model, baseUrl, apiKey, customProviders } = resolveCliProvider(store, config, {
42
43
  provider: values.provider,
43
44
  model: values.model,
@@ -101,6 +102,7 @@ export async function runCommand(argv) {
101
102
  process.once('SIGINT', onSigint);
102
103
 
103
104
  const strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
105
+ const effort = normalizeEffort(values.effort ?? store.getSetting('activeEffort') ?? config.effort);
104
106
 
105
107
  try {
106
108
  const { result, newMessages } = await runTurn({
@@ -120,6 +122,7 @@ export async function runCommand(argv) {
120
122
  maxDurationMs: config.maxDurationMs,
121
123
  maxOutputTokens: config.maxOutputTokens,
122
124
  promptCaching: config.promptCaching === true,
125
+ reasoningEffort: effort,
123
126
  signal: controller.signal,
124
127
  tools: toolDefs,
125
128
  execute,
@@ -176,6 +179,7 @@ Opciones:
176
179
  --tools <lista> Tools habilitadas (coma), ej. read,list,grep
177
180
  --no-tools Deshabilitar todas las tools
178
181
  --strict Hacer solo lo pedido (estricto) --no-strict
182
+ --effort <nivel> Esfuerzo de razonamiento: off | low | medium | high | max
179
183
  -y, --yes Aprobar automáticamente los permisos "ask"
180
184
  --json Salida JSON (sin streaming)`);
181
185
  }
@@ -149,7 +149,7 @@ if (!foreground) {
149
149
  async function runForeground({ values, port, host, cwd }) {
150
150
  let instance;
151
151
  try {
152
- instance = await createTamnoraServer({ cwd, port, host, dbPath: databasePath(cwd), yes: Boolean(values.yes) });
152
+ instance = await createTamnoraServer({ cwd, port, host, dbPath: databasePath(), yes: Boolean(values.yes) });
153
153
  } catch (err) {
154
154
  log(`ERROR: ${err.message}`);
155
155
  return 1;
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode, scopeDirective } from '../lib/core.js';
2
+ import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode, scopeDirective, normalizeEffort } from '../lib/core.js';
3
3
  import { parse, databasePath, num } from '../lib/util.js';
4
4
  import { buildSystemPrompt, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
5
5
  import { runTui } from '../tui/app.js';
@@ -16,6 +16,7 @@ const OPTIONS = {
16
16
  'no-tools': { type: 'boolean' },
17
17
  strict: { type: 'boolean' },
18
18
  'no-strict': { type: 'boolean' },
19
+ effort: { type: 'string' },
19
20
  yes: { type: 'boolean', short: 'y' },
20
21
  help: { type: 'boolean', short: 'h' },
21
22
  };
@@ -29,7 +30,7 @@ export async function tuiCommand(argv) {
29
30
 
30
31
  const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
31
32
  const config = await loadConfig(cwd);
32
- const store = new Store(databasePath(cwd));
33
+ const store = new Store(databasePath());
33
34
  const { providerId, model, baseUrl, apiKey, customProviders } = resolveCliProvider(store, config, {
34
35
  provider: values.provider,
35
36
  model: values.model,
@@ -68,6 +69,7 @@ export async function tuiCommand(argv) {
68
69
 
69
70
  const mode = resolveMode(values.mode || store.getSetting('activeMode') || 'agent');
70
71
  let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
72
+ const effort = normalizeEffort(values.effort ?? store.getSetting('activeEffort') ?? config.effort);
71
73
  const enabledFor = (m) =>
72
74
  m.tools ? (baseEnabled ? baseEnabled.filter((tool) => m.tools.includes(tool)) : m.tools) : baseEnabled;
73
75
  const promptFor = (m) =>
@@ -102,6 +104,7 @@ export async function tuiCommand(argv) {
102
104
  maxDurationMs: config.maxDurationMs,
103
105
  maxOutputTokens: config.maxOutputTokens,
104
106
  promptCaching: config.promptCaching === true,
107
+ reasoningEffort: effort,
105
108
  toolOptions: {
106
109
  root: cwd,
107
110
  enabled: enabledFor(mode),
@@ -133,5 +136,6 @@ Opciones:
133
136
  --max-steps <n> Techo de rondas de herramientas (0 = sin límite, default)
134
137
  --tools <lista> Tools habilitadas (coma) --no-tools
135
138
  --strict Hacer solo lo pedido (estricto) --no-strict
139
+ --effort <nivel> Esfuerzo de razonamiento: off | low | medium | high | max
136
140
  -y, --yes Auto-aprobar tools con permiso "ask"`);
137
141
  }
@@ -78,6 +78,7 @@ export async function runTurn({
78
78
  maxDurationMs,
79
79
  maxOutputTokens,
80
80
  promptCaching,
81
+ reasoningEffort,
81
82
  stallWarnMs,
82
83
  signal,
83
84
  execute,
@@ -106,6 +107,7 @@ export async function runTurn({
106
107
  maxDurationMs,
107
108
  maxOutputTokens,
108
109
  promptCaching,
110
+ reasoningEffort,
109
111
  stallWarnMs,
110
112
  signal,
111
113
  onEvent,
@@ -15,8 +15,8 @@ export function parse(argv, options) {
15
15
  return { values, positionals };
16
16
  }
17
17
 
18
- /** Re-exportados desde el core: la base es global (casa de Tamnora). */
19
- export { STATE_DIR, databasePath } from '../../../core/src/index.js';
18
+ /** Re-exportado desde el core: la base es global (casa de Tamnora). */
19
+ export { databasePath } from '../../../core/src/index.js';
20
20
 
21
21
  export function num(value, fallback) {
22
22
  const n = Number(value);
@@ -2,7 +2,7 @@
2
2
  // Alt-screen + raw input + incremental rendering; reuses the harness engine.
3
3
 
4
4
  import { readFileSync } from 'node:fs';
5
- import { createTools, diffRows, resolveInRoot } from '../lib/core.js';
5
+ import { createTools, diffRows, resolveInRoot, normalizeEffort } from '../lib/core.js';
6
6
  import { runTurn } from '../lib/runtime.js';
7
7
  import { parseKeys } from './keys.js';
8
8
  import { render } from './layout.js';
@@ -25,6 +25,7 @@ export function runTui({
25
25
  maxDurationMs,
26
26
  maxOutputTokens,
27
27
  promptCaching,
28
+ reasoningEffort,
28
29
  toolOptions,
29
30
  store,
30
31
  sessionId,
@@ -37,8 +38,9 @@ export function runTui({
37
38
  let activeMode = readMode(mode);
38
39
  let modePrompt = activeMode.systemPrompt ?? systemPrompt;
39
40
  let modeEnabled = activeMode.enabled;
41
+ let effort = reasoningEffort || '';
40
42
  const state = {
41
- header: { provider: providerId, model, cwd: toolOptions.root },
43
+ header: { provider: providerId, model, cwd: toolOptions.root, effort },
42
44
  messages: [],
43
45
  input: '',
44
46
  inputCursor: 0,
@@ -170,12 +172,29 @@ export function runTui({
170
172
  draw();
171
173
  return;
172
174
  }
173
- if (text === '/estricto' || text === '/strict') return toggleStrict();
174
- const strictArg = text.match(/^\/(?:estricto|strict)\s+(on|off|si|sí|no)$/i);
175
+ if (text === '/estricto') return toggleStrict();
176
+ const strictArg = text.match(/^\/estricto\s+(on|off|si|sí|no)$/i);
175
177
  if (strictArg) {
176
178
  toggleStrict(/^(on|si|sí)$/i.test(strictArg[1]));
177
179
  return;
178
180
  }
181
+ const effortArg = text.match(/^\/(?:esfuerzo|effort)(?:\s+(\S+))?$/i);
182
+ if (effortArg) {
183
+ if (effortArg[1]) {
184
+ const next = normalizeEffort(effortArg[1]);
185
+ if (!next && !/^(auto|default)$/i.test(effortArg[1])) {
186
+ addMessage('assistant', 'Nivel inválido: off | low | medium | high | max | auto.');
187
+ draw();
188
+ return;
189
+ }
190
+ effort = next;
191
+ state.header.effort = effort;
192
+ store.setSetting('activeEffort', effort || null);
193
+ }
194
+ addMessage('assistant', `Esfuerzo: ${effort || 'auto'}.`);
195
+ draw();
196
+ return;
197
+ }
179
198
  if (text === '/limpiar' || text === '/clear') {
180
199
  state.input = '';
181
200
  state.messages = [];
@@ -209,6 +228,7 @@ export function runTui({
209
228
  maxDurationMs,
210
229
  maxOutputTokens,
211
230
  promptCaching,
231
+ reasoningEffort: effort,
212
232
  signal: controller.signal,
213
233
  tools: toolset.definitions,
214
234
  execute: toolset.execute,
@@ -126,7 +126,7 @@ function buildBody(state, width) {
126
126
 
127
127
  function headerText(state, width) {
128
128
  const left = ` ❯ tamnora`;
129
- const middle = ` ${state.header.provider}/${state.header.model}`;
129
+ const middle = ` ${state.header.provider}/${state.header.model}${state.header.effort ? ` · ${state.header.effort}` : ''}`;
130
130
  const tokens = state.tokens?.prompt ? ` ctx ${formatK(state.tokens.prompt)}` : '';
131
131
  const right = state.streaming ? `generando…${tokens} ` : `${state.header.cwd ?? ''}${tokens} `;
132
132
  const pad = Math.max(0, width - chars(left).length - chars(middle).length - chars(right).length);
@@ -2,10 +2,8 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import {
5
- parseTunnelmoleUrls,
6
5
  parseTailscaleStatus,
7
6
  startTunnel,
8
- startTunnelmole,
9
7
  stopTailscaleServe,
10
8
  tailscalePreviewUrl,
11
9
  } from '../src/lib/tunnel.js';
@@ -21,16 +19,6 @@ function fakeChild(exitCode = 0) {
21
19
  return child;
22
20
  }
23
21
 
24
- test('parseTunnelmoleUrls extracts the https URL', () => {
25
- const out = parseTunnelmoleUrls('Listening at https://abc-123.tunnelmole.net (forwarding to 4517)');
26
- assert.equal(out.https, 'https://abc-123.tunnelmole.net');
27
- assert.equal(out.http, '');
28
- });
29
-
30
- test('parseTunnelmoleUrls ignores unrelated output', () => {
31
- assert.deepEqual(parseTunnelmoleUrls('starting tunnelmole...'), { https: '', http: '' });
32
- });
33
-
34
22
  test('parseTailscaleStatus lee el DNSName del nodo y su IPv4', () => {
35
23
  const out = parseTailscaleStatus(
36
24
  JSON.stringify({ Self: { DNSName: 'pc.tailnet.ts.net.', TailscaleIPs: ['100.64.0.5', 'fd7a:115c:a1e0::5'] } })
@@ -46,8 +34,10 @@ test('parseTailscaleStatus marca desconectado sin DNSName', () => {
46
34
  assert.equal(out.dnsName, '');
47
35
  });
48
36
 
49
- test('startTunnel rechaza el proveedor retirado (relay)', async () => {
50
- await assert.rejects(() => startTunnel(4517, 'relay'), /no soportado/);
37
+ test('startTunnel rechaza los proveedores publicos retirados', async () => {
38
+ for (const provider of ['relay', 'tunnelmole', 'tmole']) {
39
+ await assert.rejects(() => startTunnel(4517, provider), /no soportado/, provider);
40
+ }
51
41
  });
52
42
 
53
43
  test('startTunnel con provider none no abre nada', async () => {
@@ -69,23 +59,6 @@ test('stopTailscaleServe apaga solo el handler 443 y lanza tailscale sin shell',
69
59
  assert.equal(calls[0].options.shell, undefined);
70
60
  });
71
61
 
72
- test(
73
- 'startTunnelmole con shell manda un unico comando (Node no concatena args)',
74
- { skip: process.platform !== 'win32' },
75
- async () => {
76
- const calls = [];
77
- const spawnImpl = (cmd, args, options) => {
78
- calls.push({ cmd, args, options });
79
- return fakeChild(1);
80
- };
81
- await assert.rejects(() => startTunnelmole(4517, { spawnImpl }), /termino/);
82
- assert.equal(calls.length, 1);
83
- assert.equal(calls[0].cmd, 'npx --yes tunnelmole 4517');
84
- assert.deepEqual(calls[0].args, []);
85
- assert.equal(calls[0].options.shell, true);
86
- }
87
- );
88
-
89
62
  test('tailscalePreviewUrl usa el 8443 y omite el 443', () => {
90
63
  assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net'), 'https://pc.tailnet.ts.net:8443');
91
64
  assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net', 9000), 'https://pc.tailnet.ts.net:9000');
@@ -93,7 +66,7 @@ test('tailscalePreviewUrl usa el 8443 y omite el 443', () => {
93
66
  });
94
67
 
95
68
  test('qrTerminal renders a block matrix for a URL', () => {
96
- const art = qrTerminal('https://abc-123.tunnelmole.net', 1);
69
+ const art = qrTerminal('https://pc.tailnet.ts.net', 1);
97
70
  assert.ok(art.length > 0);
98
71
  assert.match(art, /[\u2580\u2584\u2588]/);
99
72
  });
@@ -60,6 +60,7 @@ export async function runAgent({
60
60
  maxDurationMs = 0,
61
61
  maxOutputTokens = 0,
62
62
  promptCaching = false,
63
+ reasoningEffort = '',
63
64
  repeatLimit = 3,
64
65
  wrapUp = true,
65
66
  stallWarnMs = 90000,
@@ -148,6 +149,7 @@ export async function runAgent({
148
149
  signal,
149
150
  maxOutputTokens,
150
151
  promptCaching,
152
+ reasoningEffort,
151
153
  onEvent: (event) => {
152
154
  if (event && (event.type === 'reasoning' || event.type === 'tool_calls')) emitted = true;
153
155
  if (event?.type === 'text' && event.delta) emitted = true;
@@ -274,7 +276,7 @@ export async function runAgent({
274
276
 
275
277
  let wrappedUp = false;
276
278
  if (wrapUp && (stopReason === 'max_rounds' || stopReason === 'no_progress' || stopReason === 'budget')) {
277
- const wrap = await runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries: retries, retryDelayMs, maxOutputTokens, promptCaching });
279
+ const wrap = await runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries: retries, retryDelayMs, maxOutputTokens, promptCaching, reasoningEffort });
278
280
  if (wrap.usage) accumulateUsage(wrap.usage);
279
281
  if (wrap.content) {
280
282
  working.push({ role: 'assistant', content: wrap.content, tokens_in: wrap.usage?.prompt_tokens, tokens_out: wrap.usage?.completion_tokens });
@@ -297,7 +299,7 @@ export async function runAgent({
297
299
  }
298
300
 
299
301
  /** Ronda final con tools deshabilitadas para forzar un cierre con texto. */
300
- async function runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries, retryDelayMs, maxOutputTokens, promptCaching }) {
302
+ async function runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries, retryDelayMs, maxOutputTokens, promptCaching, reasoningEffort }) {
301
303
  try {
302
304
  const result = await withRetry(
303
305
  () =>
@@ -308,6 +310,7 @@ async function runFinalAnswer({ provider, config, model, working, signal, onEven
308
310
  signal,
309
311
  maxOutputTokens,
310
312
  promptCaching,
313
+ reasoningEffort,
311
314
  onEvent,
312
315
  }),
313
316
  { retries: maxRetries, baseDelayMs: retryDelayMs, signal }