@danieltmn/tamnora 0.25.1 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +13 -25
- package/SECURITY.md +3 -4
- package/harness/packages/cli/src/cli.js +0 -3
- package/harness/packages/cli/src/commands/bridge.js +0 -1
- package/harness/packages/cli/src/commands/repl.js +3 -3
- package/harness/packages/cli/src/commands/run.js +1 -1
- package/harness/packages/cli/src/commands/server.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +1 -1
- package/harness/packages/cli/src/lib/util.js +2 -2
- package/harness/packages/cli/src/tui/app.js +2 -2
- package/harness/packages/cli/test/tunnel.test.js +5 -32
- package/harness/packages/core/src/agents/index.js +13 -41
- package/harness/packages/core/src/config/app.js +0 -4
- package/harness/packages/core/src/config/home.js +1 -20
- package/harness/packages/core/src/config/index.js +5 -8
- package/harness/packages/core/src/extensions/load.js +22 -0
- package/harness/packages/core/src/frontmatter.js +32 -0
- package/harness/packages/core/src/hooks/index.js +2 -16
- package/harness/packages/core/src/index.js +2 -14
- package/harness/packages/core/src/plugins/index.js +2 -16
- package/harness/packages/core/src/procs/project.js +68 -0
- package/harness/packages/core/src/procs/static.js +17 -0
- package/harness/packages/core/src/rules/index.js +8 -27
- package/harness/packages/core/src/skills/index.js +8 -19
- package/harness/packages/core/src/storage/sqlite.js +595 -690
- package/harness/packages/core/src/tunnel.js +9 -147
- package/harness/packages/core/test/home.test.js +2 -35
- package/harness/packages/core/test/procs.test.js +3 -64
- package/harness/packages/core/test/storage.test.js +1 -12
- package/harness/packages/server/public/app.js +2 -19
- package/harness/packages/server/public/appearance.js +2 -56
- package/harness/packages/server/public/boot.js +2 -2
- package/harness/packages/server/public/cards.js +2 -21
- package/harness/packages/server/public/chat.js +0 -16
- package/harness/packages/server/public/chatFormat.js +0 -11
- package/harness/packages/server/public/chrome.js +2 -5
- package/harness/packages/server/public/commands.js +1 -2
- package/harness/packages/server/public/explorer.js +7 -10
- package/harness/packages/server/public/icons/icon-128x128.png +0 -0
- package/harness/packages/server/public/icons/icon-144x144.png +0 -0
- package/harness/packages/server/public/icons/icon-152x152.png +0 -0
- package/harness/packages/server/public/icons/icon-192x192.png +0 -0
- package/harness/packages/server/public/icons/icon-384x384.png +0 -0
- package/harness/packages/server/public/icons/icon-512x512.png +0 -0
- package/harness/packages/server/public/icons/icon-72x72.png +0 -0
- package/harness/packages/server/public/icons/icon-96x96.png +0 -0
- package/harness/packages/server/public/icons/logo.png +0 -0
- package/harness/packages/server/public/icons/logo_light.png +0 -0
- package/harness/packages/server/public/index.html +1228 -1326
- package/harness/packages/server/public/landing/boot.js +2 -3
- package/harness/packages/server/public/landing/index.html +3 -367
- package/harness/packages/server/public/logo.png +0 -0
- package/harness/packages/server/public/settings.js +1255 -1322
- package/harness/packages/server/public/settingsNav.js +0 -1
- package/harness/packages/server/public/styles.css +2341 -2867
- package/harness/packages/server/public/system.js +356 -495
- package/harness/packages/server/public/voice.js +6 -22
- package/harness/packages/server/src/auth.js +4 -15
- package/harness/packages/server/src/cli.js +1 -1
- package/harness/packages/server/src/lib/agents.js +13 -27
- package/harness/packages/server/src/lib/appearance.js +3 -22
- package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
- package/harness/packages/server/src/lib/definitions.js +40 -0
- package/harness/packages/server/src/lib/skills.js +14 -21
- package/harness/packages/server/src/lib/ttsSettings.js +2 -2
- package/harness/packages/server/src/lib/workspace.js +9 -11
- package/harness/packages/server/src/routes/chat.js +0 -43
- package/harness/packages/server/src/routes/preview.js +5 -22
- package/harness/packages/server/src/routes/procs.js +2 -4
- package/harness/packages/server/src/routes/tts.js +1 -1
- package/harness/packages/server/src/server.js +1 -5
- package/harness/packages/server/test/appearance-route.test.js +3 -3
- package/harness/packages/server/test/appearance.test.js +12 -38
- package/harness/packages/server/test/chatFormat.test.js +0 -8
- package/harness/packages/server/test/consoleTabs.test.js +4 -4
- package/harness/packages/server/test/ops.test.js +8 -56
- package/harness/packages/server/test/preview.test.js +2 -42
- package/harness/packages/server/test/security.test.js +194 -370
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/packages/server/test/tts.test.js +4 -4
- package/harness/tamnora.example.json +1 -1
- package/package.json +2 -2
- package/harness/packages/cli/src/commands/eval.js +0 -186
- package/harness/packages/core/src/eval/dataset.js +0 -47
- package/harness/packages/core/src/eval/judge.js +0 -39
- package/harness/packages/core/src/eval/report.js +0 -48
- package/harness/packages/core/src/eval/runTask.js +0 -62
- package/harness/packages/core/src/eval/runner.js +0 -153
- package/harness/packages/core/src/eval/scorers.js +0 -217
- package/harness/packages/core/src/eval/task.js +0 -59
- package/harness/packages/core/src/procs/detect.js +0 -149
- package/harness/packages/core/test/eval.test.js +0 -144
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/src/lib/advisor.js +0 -198
- package/harness/packages/server/src/routes/advisor.js +0 -79
- package/harness/packages/server/src/routes/eval.js +0 -100
- package/harness/packages/server/test/advisor.test.js +0 -249
- 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.1] - 2026-11-04
|
|
9
|
+
|
|
10
|
+
### Cambios
|
|
11
|
+
|
|
12
|
+
- chore: iconos nuevos, logo en el sitio y docs al dia
|
|
13
|
+
|
|
14
|
+
## [0.26.0] - 2026-11-04
|
|
15
|
+
|
|
16
|
+
### Cambios
|
|
17
|
+
|
|
18
|
+
- refactor: retirar el eval del proyecto
|
|
19
|
+
- refactor: retirar el asesor y la evaluacion desde el server
|
|
20
|
+
- refactor(temas): dejar 4 temas y 2 tipografias
|
|
21
|
+
- refactor: quitar la autodeteccion de procesos de desarrollo
|
|
22
|
+
- refactor: unificar registros markdown y carga de extensiones
|
|
23
|
+
- refactor: quitar alias y compatibilidad que ya no compra nada
|
|
24
|
+
- refactor: retirar el tunel publico (tunnelmole)
|
|
25
|
+
|
|
8
26
|
## [0.25.1] - 2026-11-04
|
|
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
|
|
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
|
|
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
|
|
135
|
-
respuesta final
|
|
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
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
- **
|
|
24
|
-
|
|
25
|
-
remoto
|
|
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(
|
|
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> /
|
|
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 === '
|
|
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(
|
|
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(
|
|
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(
|
|
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-
|
|
19
|
-
export {
|
|
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'
|
|
176
|
-
const strictArg = text.match(/^\/
|
|
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
|
|
50
|
-
|
|
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://
|
|
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
|
|
23
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
37
|
+
* sobrescribe.
|
|
41
38
|
*/
|
|
42
|
-
export function databasePath(
|
|
43
|
-
return process.env.TAMNORA_DB ||
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
}
|