@danieltmn/tamnora 0.25.1 → 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 (87) hide show
  1. package/CHANGELOG.md +12 -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 +3 -3
  7. package/harness/packages/cli/src/commands/run.js +1 -1
  8. package/harness/packages/cli/src/commands/server.js +1 -1
  9. package/harness/packages/cli/src/commands/tui.js +1 -1
  10. package/harness/packages/cli/src/lib/util.js +2 -2
  11. package/harness/packages/cli/src/tui/app.js +2 -2
  12. package/harness/packages/cli/test/tunnel.test.js +5 -32
  13. package/harness/packages/core/src/agents/index.js +13 -41
  14. package/harness/packages/core/src/config/app.js +0 -4
  15. package/harness/packages/core/src/config/home.js +1 -20
  16. package/harness/packages/core/src/config/index.js +5 -8
  17. package/harness/packages/core/src/extensions/load.js +22 -0
  18. package/harness/packages/core/src/frontmatter.js +32 -0
  19. package/harness/packages/core/src/hooks/index.js +2 -16
  20. package/harness/packages/core/src/index.js +2 -14
  21. package/harness/packages/core/src/plugins/index.js +2 -16
  22. package/harness/packages/core/src/procs/project.js +68 -0
  23. package/harness/packages/core/src/procs/static.js +17 -0
  24. package/harness/packages/core/src/rules/index.js +8 -27
  25. package/harness/packages/core/src/skills/index.js +8 -19
  26. package/harness/packages/core/src/storage/sqlite.js +595 -690
  27. package/harness/packages/core/src/tunnel.js +9 -147
  28. package/harness/packages/core/test/home.test.js +2 -35
  29. package/harness/packages/core/test/procs.test.js +3 -64
  30. package/harness/packages/core/test/storage.test.js +1 -12
  31. package/harness/packages/server/public/app.js +2 -19
  32. package/harness/packages/server/public/appearance.js +2 -56
  33. package/harness/packages/server/public/boot.js +2 -2
  34. package/harness/packages/server/public/cards.js +2 -21
  35. package/harness/packages/server/public/chat.js +0 -16
  36. package/harness/packages/server/public/chatFormat.js +0 -11
  37. package/harness/packages/server/public/chrome.js +2 -5
  38. package/harness/packages/server/public/commands.js +1 -2
  39. package/harness/packages/server/public/explorer.js +7 -10
  40. package/harness/packages/server/public/index.html +1228 -1326
  41. package/harness/packages/server/public/landing/boot.js +2 -3
  42. package/harness/packages/server/public/landing/index.html +3 -367
  43. package/harness/packages/server/public/settings.js +1255 -1322
  44. package/harness/packages/server/public/settingsNav.js +0 -1
  45. package/harness/packages/server/public/styles.css +2341 -2867
  46. package/harness/packages/server/public/system.js +356 -495
  47. package/harness/packages/server/public/voice.js +6 -22
  48. package/harness/packages/server/src/auth.js +4 -15
  49. package/harness/packages/server/src/cli.js +1 -1
  50. package/harness/packages/server/src/lib/agents.js +13 -27
  51. package/harness/packages/server/src/lib/appearance.js +3 -22
  52. package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
  53. package/harness/packages/server/src/lib/definitions.js +40 -0
  54. package/harness/packages/server/src/lib/skills.js +14 -21
  55. package/harness/packages/server/src/lib/ttsSettings.js +2 -2
  56. package/harness/packages/server/src/lib/workspace.js +9 -11
  57. package/harness/packages/server/src/routes/chat.js +0 -43
  58. package/harness/packages/server/src/routes/preview.js +5 -22
  59. package/harness/packages/server/src/routes/procs.js +2 -4
  60. package/harness/packages/server/src/routes/tts.js +1 -1
  61. package/harness/packages/server/src/server.js +1 -5
  62. package/harness/packages/server/test/appearance-route.test.js +3 -3
  63. package/harness/packages/server/test/appearance.test.js +12 -38
  64. package/harness/packages/server/test/chatFormat.test.js +0 -8
  65. package/harness/packages/server/test/consoleTabs.test.js +4 -4
  66. package/harness/packages/server/test/ops.test.js +8 -56
  67. package/harness/packages/server/test/preview.test.js +2 -42
  68. package/harness/packages/server/test/security.test.js +194 -370
  69. package/harness/packages/server/test/settingsNav.test.js +2 -2
  70. package/harness/packages/server/test/tts.test.js +4 -4
  71. package/harness/tamnora.example.json +1 -1
  72. package/package.json +2 -2
  73. package/harness/packages/cli/src/commands/eval.js +0 -186
  74. package/harness/packages/core/src/eval/dataset.js +0 -47
  75. package/harness/packages/core/src/eval/judge.js +0 -39
  76. package/harness/packages/core/src/eval/report.js +0 -48
  77. package/harness/packages/core/src/eval/runTask.js +0 -62
  78. package/harness/packages/core/src/eval/runner.js +0 -153
  79. package/harness/packages/core/src/eval/scorers.js +0 -217
  80. package/harness/packages/core/src/eval/task.js +0 -59
  81. package/harness/packages/core/src/procs/detect.js +0 -149
  82. package/harness/packages/core/test/eval.test.js +0 -144
  83. package/harness/packages/server/src/lib/advisor.js +0 -198
  84. package/harness/packages/server/src/routes/advisor.js +0 -79
  85. package/harness/packages/server/src/routes/eval.js +0 -100
  86. package/harness/packages/server/test/advisor.test.js +0 -249
  87. package/harness/scripts/example-dataset.jsonl +0 -4
package/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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
+
8
20
  ## [0.25.1] - 2026-11-04
9
21
 
10
22
  ### 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
 
@@ -35,7 +35,7 @@ export async function replCommand(argv) {
35
35
  let systemExtra = '';
36
36
  let mcpBlock = '';
37
37
  const maxRounds = num(values['max-steps'], config.maxRounds);
38
- const store = new Store(databasePath(cwd));
38
+ const store = new Store(databasePath());
39
39
  let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
40
40
  let effort = normalizeEffort(values.effort ?? store.getSetting('activeEffort') ?? config.effort);
41
41
  const buildPrompt = () =>
@@ -123,7 +123,7 @@ export async function replCommand(argv) {
123
123
  } else if (cmd === 'help') {
124
124
  process.stdout.write(
125
125
  `${color.dim(
126
- '/model <id> /provider <id> /system <texto> /strict /esfuerzo <nivel> /clear /session /exit'
126
+ '/model <id> /provider <id> /system <texto> /estricto /esfuerzo <nivel> /clear /session /exit'
127
127
  )}\n`
128
128
  );
129
129
  } else if (cmd === 'model') {
@@ -135,7 +135,7 @@ export async function replCommand(argv) {
135
135
  } else if (cmd === 'system') {
136
136
  if (arg) systemExtra = arg;
137
137
  process.stdout.write(color.dim(`system prompt (${buildPrompt().length} chars)\n`));
138
- } else if (cmd === 'strict' || cmd === 'estricto') {
138
+ } else if (cmd === 'estricto') {
139
139
  if (arg) strict = /^(on|si|sí|1|true)$/i.test(arg);
140
140
  else strict = !strict;
141
141
  store.setSetting('strictScope', strict);
@@ -38,7 +38,7 @@ export async function runCommand(argv) {
38
38
  const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
39
39
  const config = await loadConfig(cwd);
40
40
  const instructions = await loadInstructions(cwd, config.instructions);
41
- const store = new Store(databasePath(cwd));
41
+ const store = new Store(databasePath());
42
42
  const { providerId, model, baseUrl, apiKey, customProviders } = resolveCliProvider(store, config, {
43
43
  provider: values.provider,
44
44
  model: values.model,
@@ -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;
@@ -30,7 +30,7 @@ export async function tuiCommand(argv) {
30
30
 
31
31
  const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
32
32
  const config = await loadConfig(cwd);
33
- const store = new Store(databasePath(cwd));
33
+ const store = new Store(databasePath());
34
34
  const { providerId, model, baseUrl, apiKey, customProviders } = resolveCliProvider(store, config, {
35
35
  provider: values.provider,
36
36
  model: values.model,
@@ -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);
@@ -172,8 +172,8 @@ export function runTui({
172
172
  draw();
173
173
  return;
174
174
  }
175
- if (text === '/estricto' || text === '/strict') return toggleStrict();
176
- 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);
177
177
  if (strictArg) {
178
178
  toggleStrict(/^(on|si|sí)$/i.test(strictArg[1]));
179
179
  return;
@@ -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
  });
@@ -5,54 +5,26 @@
5
5
  import { readdirSync, readFileSync, statSync } from 'node:fs';
6
6
  import { mkdir, writeFile } from 'node:fs/promises';
7
7
  import path from 'node:path';
8
+ import { parseFrontmatter, parseListValue } from '../frontmatter.js';
8
9
 
9
10
  export const AGENT_FILE = 'AGENT.md';
10
11
 
11
- function parseList(value) {
12
- if (value === undefined || value === null) return null;
13
- const cleaned = String(value).trim().replace(/^\[|\]$/g, '');
14
- return cleaned
15
- .split(',')
16
- .map((item) => item.trim().replace(/^["']|["']$/g, ''))
17
- .filter(Boolean);
18
- }
19
-
20
12
  /** Parse an AGENT.md: frontmatter `--- key: value ---` plus the body prompt. */
21
13
  export function parseAgentMarkdown(text, fallbackId = '') {
22
- const source = String(text ?? '');
23
- const agent = {
14
+ const { fields, body } = parseFrontmatter(text);
15
+ return {
24
16
  id: fallbackId,
25
- name: fallbackId,
26
- description: '',
27
- model: '',
28
- provider: '',
29
- mode: '',
30
- effort: '',
31
- maxRounds: 0,
32
- tools: null,
33
- skills: null,
34
- body: source.trim(),
17
+ name: fields.name || fallbackId,
18
+ description: fields.description || '',
19
+ model: fields.model || '',
20
+ provider: fields.provider || '',
21
+ mode: fields.mode || '',
22
+ effort: fields.effort || fields.reasoning || fields.reasoning_effort || '',
23
+ maxRounds: Number(fields.maxrounds || fields.max_rounds) || 0,
24
+ tools: fields.tools === undefined ? null : parseListValue(fields.tools),
25
+ skills: fields.skills === undefined ? null : parseListValue(fields.skills),
26
+ body,
35
27
  };
36
- const match = source.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?([\s\S]*)$/);
37
- if (!match) return agent;
38
-
39
- agent.body = match[2].trim();
40
- for (const line of match[1].split(/\r?\n/)) {
41
- const kv = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
42
- if (!kv) continue;
43
- const key = kv[1].toLowerCase();
44
- const value = kv[2].trim().replace(/^["']|["']$/g, '');
45
- if (key === 'name') agent.name = value || fallbackId;
46
- else if (key === 'description') agent.description = value;
47
- else if (key === 'model') agent.model = value;
48
- else if (key === 'provider') agent.provider = value;
49
- else if (key === 'mode') agent.mode = value;
50
- else if (key === 'effort' || key === 'reasoning' || key === 'reasoning_effort') agent.effort = value;
51
- else if (key === 'maxrounds' || key === 'max_rounds') agent.maxRounds = Number(value) || 0;
52
- else if (key === 'tools') agent.tools = parseList(value);
53
- else if (key === 'skills') agent.skills = parseList(value);
54
- }
55
- return agent;
56
28
  }
57
29
 
58
30
  /** List agents under a root (missing root -> []). Directory form wins. */
@@ -29,10 +29,6 @@ export function readApp() {
29
29
  }
30
30
  const app = { ...DEFAULT_APP, ...(data && typeof data === 'object' ? data : {}) };
31
31
  app.tunnel = { ...DEFAULT_APP.tunnel, ...(app.tunnel || {}) };
32
- // `tunnelmole` fue el default viejo y `relay` fue un proveedor retirado: lo
33
- // persistido en instalaciones previas se migra a tailscale.
34
- const legacy = ['tunnelmole', 'tmole', 'relay'];
35
- if (legacy.includes(app.tunnel.provider)) app.tunnel.provider = 'tailscale';
36
32
  return app;
37
33
  }
38
34
 
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
@@ -82,22 +82,3 @@ export function ensureHome() {
82
82
  export function homeExists() {
83
83
  return existsSync(appConfigPath());
84
84
  }
85
-
86
- /**
87
- * Migra una base local legacy (`<cwd>/.tamnora/tamnora.db`) a la casa global,
88
- * una sola vez. Devuelve la ruta del archivo global.
89
- */
90
- export function migrateLegacyDb(cwd = process.cwd()) {
91
- const target = databaseFile();
92
- if (existsSync(target)) return target;
93
- const legacy = path.join(cwd, DIR_NAME, 'tamnora.db');
94
- if (existsSync(legacy)) {
95
- try {
96
- ensureHome();
97
- copyFileSync(legacy, target);
98
- } catch {
99
- /* si falla, se arranca con una base nueva */
100
- }
101
- }
102
- return target;
103
- }
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { migrateLegacyDb } from './home.js';
4
+ import { databaseFile } from './home.js';
5
5
 
6
6
  export const DEFAULT_CONFIG = {
7
7
  provider: 'openrouter',
@@ -26,21 +26,18 @@ export const DEFAULT_CONFIG = {
26
26
  mcpPinned: [],
27
27
  server: { host: '127.0.0.1', port: 4517 },
28
28
  processes: { enabled: true, allow: [] },
29
- preview: { provider: 'tailscale', tailscalePort: 8443, fallback: true },
29
+ preview: { provider: 'tailscale', tailscalePort: 8443 },
30
30
  telegram: { token: '', allowedChatIds: [] },
31
31
  agents: { maxRounds: 6 },
32
32
  };
33
33
 
34
- /** Directory (relative to the project) where harness state lived historically. */
35
- export const STATE_DIR = '.tamnora';
36
-
37
34
  /**
38
35
  * Absolute path to the SQLite database. Es **global** (casa de Tamnora) para
39
36
  * que proveedores y claves no se dupliquen entre proyectos. `TAMNORA_DB` lo
40
- * sobrescribe; si no existe la base global se migra la legacy del `cwd`.
37
+ * sobrescribe.
41
38
  */
42
- export function databasePath(cwd = process.cwd()) {
43
- return process.env.TAMNORA_DB || migrateLegacyDb(cwd);
39
+ export function databasePath() {
40
+ return process.env.TAMNORA_DB || databaseFile();
44
41
  }
45
42
 
46
43
  /**
@@ -0,0 +1,22 @@
1
+ // Carga de modulos de extension locales (hooks/plugins): solo los patrones
2
+ // explicitos, deduplicados; un modulo roto se ignora.
3
+
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { globFiles } from '../tools/glob.js';
7
+
8
+ export async function loadExtensionModules(cwd, patterns, normalize) {
9
+ const list = [...new Set(patterns ?? [])];
10
+ if (!list.length) return [];
11
+ const items = [];
12
+ for (const rel of await globFiles(cwd, list)) {
13
+ try {
14
+ const mod = await import(pathToFileURL(path.join(cwd, rel)).href);
15
+ const item = normalize(mod, rel);
16
+ if (item) items.push({ ...item, path: rel });
17
+ } catch {
18
+ /* modulo roto: se ignora */
19
+ }
20
+ }
21
+ return items;
22
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Frontmatter simple compartido por skills, subagentes y reglas: un bloque
3
+ * `--- clave: valor ---` (una clave por linea) y el resto del documento.
4
+ */
5
+
6
+ const FRONTMATTER = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?([\s\S]*)$/;
7
+
8
+ export function parseFrontmatter(text) {
9
+ const source = String(text ?? '');
10
+ const fields = {};
11
+ let body = source.trim();
12
+ const match = source.match(FRONTMATTER);
13
+ if (!match) return { fields, body };
14
+ body = match[2].trim();
15
+ for (const line of match[1].split(/\r?\n/)) {
16
+ const kv = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
17
+ if (!kv) continue;
18
+ fields[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
19
+ }
20
+ return { fields, body };
21
+ }
22
+
23
+ /** Lista por comas: `tools: read, grep` o `paths: [a, b]`. */
24
+ export function parseListValue(value) {
25
+ const raw = String(value ?? '').trim();
26
+ if (!raw) return [];
27
+ const inner = raw.startsWith('[') && raw.endsWith(']') ? raw.slice(1, -1) : raw;
28
+ return inner
29
+ .split(',')
30
+ .map((item) => item.trim().replace(/^["']|["']$/g, ''))
31
+ .filter(Boolean);
32
+ }
@@ -5,9 +5,7 @@
5
5
  // `{ name?, events: { '<tipo>': fn } }` o una funcion (se llama para todos los
6
6
  // eventos).
7
7
 
8
- import path from 'node:path';
9
- import { pathToFileURL } from 'node:url';
10
- import { globFiles } from '../tools/glob.js';
8
+ import { loadExtensionModules } from '../extensions/load.js';
11
9
 
12
10
  export const DEFAULT_HOOK_PATTERNS = ['.tamnora/hooks/*.mjs'];
13
11
 
@@ -30,17 +28,5 @@ export function normalizeHookModule(mod, id = '') {
30
28
  * Un modulo roto se ignora.
31
29
  */
32
30
  export async function loadHooks(cwd, patterns = []) {
33
- const list = [...new Set(patterns ?? [])];
34
- if (!list.length) return [];
35
- const hooks = [];
36
- for (const rel of await globFiles(cwd, list)) {
37
- try {
38
- const mod = await import(pathToFileURL(path.join(cwd, rel)).href);
39
- const hook = normalizeHookModule(mod, rel);
40
- if (hook) hooks.push({ ...hook, path: rel });
41
- } catch {
42
- /* modulo roto: se ignora */
43
- }
44
- }
45
- return hooks;
31
+ return loadExtensionModules(cwd, patterns, normalizeHookModule);
46
32
  }
@@ -50,7 +50,6 @@ export {
50
50
  resolveModel,
51
51
  resolveProvider,
52
52
  databasePath,
53
- STATE_DIR,
54
53
  DEFAULT_CONFIG,
55
54
  } from './config/index.js';
56
55
  export {
@@ -69,7 +68,6 @@ export {
69
68
  bridgeStatePath,
70
69
  ensureHome,
71
70
  homeExists,
72
- migrateLegacyDb,
73
71
  } from './config/home.js';
74
72
  export { DEFAULT_APP, readApp, writeApp, ensureApp, randomToken } from './config/app.js';
75
73
  export { exportConfig, importConfig, CONFIG_KIND, CONFIG_VERSION, MASKED } from './config/portable.js';
@@ -94,13 +92,6 @@ export {
94
92
  planMcpTools,
95
93
  } from './tools/index.js';
96
94
  export { Store, openDatabase } from './storage/sqlite.js';
97
- export { runTask } from './eval/runTask.js';
98
- export { loadDataset } from './eval/dataset.js';
99
- export { runDataset, extractToolCalls } from './eval/runner.js';
100
- export { summarize } from './eval/report.js';
101
- export { runScorers, validateSchema, extractJson, SCORERS } from './eval/scorers.js';
102
- export { createJudge } from './eval/judge.js';
103
- export { createTaskWorkspace, normalizeExpect } from './eval/task.js';
104
95
  export {
105
96
  parseSkillMarkdown,
106
97
  loadSkills,
@@ -158,17 +149,13 @@ export {
158
149
  } from './mcp/registry.js';
159
150
  export { MCP_CATALOG, mcpCatalogEntry } from './mcp/catalog.js';
160
151
  export {
161
- parseTunnelmoleUrls,
162
152
  parseTailscaleStatus,
163
153
  probeTailscale,
164
154
  stopTailscaleServe,
165
155
  startTailscaleServe,
166
156
  startTailscalePreview,
167
- startTunnelmole,
168
157
  startTunnel,
169
158
  tailscalePreviewUrl,
170
- tunnelState,
171
- tunnelActive,
172
159
  } from './tunnel.js';
173
160
  export {
174
161
  TRUSTED_CLIENT_TOKEN,
@@ -184,7 +171,8 @@ export {
184
171
  export { reconocerGoogle, wavToPcm } from './stt/google.js';
185
172
  export { pidAlive, killPidTree } from './pid.js';
186
173
  export { openBrowser, openerFor } from './open.js';
187
- export { detectProcs, loadProjectProcs, packageManager, DEFAULT_PROCESS_ALLOW, staticServerChip, PROCS_FILE } from './procs/detect.js';
174
+ export { loadProjectProcs, DEFAULT_PROCESS_ALLOW, PROCS_FILE } from './procs/project.js';
175
+ export { staticServerChip } from './procs/static.js';
188
176
  export { killPort, portInfo, pidsOnPort } from './procs/kill.js';
189
177
  export {
190
178
  ROOT_PACKAGE_NAME,
@@ -4,9 +4,7 @@
4
4
  // presencia de archivos en el cwd no ejecuta nada. Cada tool:
5
5
  // `{ name, description, parameters?, required?, handler(args, ctx) }`.
6
6
 
7
- import path from 'node:path';
8
- import { pathToFileURL } from 'node:url';
9
- import { globFiles } from '../tools/glob.js';
7
+ import { loadExtensionModules } from '../extensions/load.js';
10
8
 
11
9
  export const DEFAULT_PLUGIN_PATTERNS = ['.tamnora/plugins/*.mjs'];
12
10
 
@@ -22,17 +20,5 @@ export function normalizePlugin(mod, id = '') {
22
20
 
23
21
  /** Carga los plugins de los `patterns` explicitos (relativos a `cwd`). */
24
22
  export async function loadPlugins(cwd, patterns = []) {
25
- const list = [...new Set(patterns ?? [])];
26
- if (!list.length) return [];
27
- const plugins = [];
28
- for (const rel of await globFiles(cwd, list)) {
29
- try {
30
- const mod = await import(pathToFileURL(path.join(cwd, rel)).href);
31
- const plugin = normalizePlugin(mod, rel);
32
- if (plugin) plugins.push({ ...plugin, path: rel });
33
- } catch {
34
- /* plugin roto: se ignora */
35
- }
36
- }
37
- return plugins;
23
+ return loadExtensionModules(cwd, patterns, normalizePlugin);
38
24
  }