@danieltmn/tamnora 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +37 -3
- package/SECURITY.md +8 -0
- package/harness/packages/cli/src/commands/mcp.js +31 -4
- package/harness/packages/cli/src/commands/repl.js +7 -2
- package/harness/packages/cli/src/commands/run.js +5 -2
- package/harness/packages/cli/src/commands/tui.js +9 -2
- package/harness/packages/cli/src/lib/runtime.js +23 -1
- package/harness/packages/cli/test/tunnel.test.js +13 -1
- package/harness/packages/core/src/agent/modes.js +3 -0
- package/harness/packages/core/src/config/index.js +3 -1
- package/harness/packages/core/src/index.js +15 -1
- package/harness/packages/core/src/mcp/catalog.js +79 -0
- package/harness/packages/core/src/mcp/index.js +14 -2
- package/harness/packages/core/src/mcp/registry.js +29 -1
- package/harness/packages/core/src/redact.js +25 -0
- package/harness/packages/core/src/skills/catalog.js +78 -0
- package/harness/packages/core/src/skills/remote.js +51 -0
- package/harness/packages/core/src/tools/executors.js +93 -2
- package/harness/packages/core/src/tools/index.js +27 -6
- package/harness/packages/core/src/tools/labels.js +2 -0
- package/harness/packages/core/src/tools/mcp.js +91 -0
- package/harness/packages/core/src/tools/permissions.js +53 -8
- package/harness/packages/core/src/tools/registry.js +14 -7
- package/harness/packages/core/src/tools/schema.js +86 -0
- package/harness/packages/core/src/tts/edge.js +28 -7
- package/harness/packages/core/src/tunnel.js +51 -5
- package/harness/packages/core/test/compact.test.js +2 -0
- package/harness/packages/core/test/labels.test.js +1 -0
- package/harness/packages/core/test/mcp-registry.test.js +37 -1
- package/harness/packages/core/test/mcp-routing.test.js +115 -0
- package/harness/packages/core/test/redact.test.js +22 -0
- package/harness/packages/core/test/skills.test.js +11 -0
- package/harness/packages/core/test/tools.test.js +97 -3
- package/harness/packages/server/public/app.js +501 -23
- package/harness/packages/server/public/index.html +41 -2
- package/harness/packages/server/public/styles.css +99 -3
- package/harness/packages/server/src/lib/agentTools.js +45 -3
- package/harness/packages/server/src/lib/channels/telegram.js +2 -0
- package/harness/packages/server/src/lib/prompt.js +4 -1
- package/harness/packages/server/src/lib/questions.js +72 -0
- package/harness/packages/server/src/lib/scheduler.js +12 -1
- package/harness/packages/server/src/lib/tasks.js +3 -1
- package/harness/packages/server/src/routes/chat.js +31 -2
- package/harness/packages/server/src/routes/mcp.js +54 -2
- package/harness/packages/server/src/routes/preview.js +49 -28
- package/harness/packages/server/src/routes/providers.js +12 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/routes/skills.js +48 -1
- package/harness/packages/server/src/routes/state.js +8 -1
- package/harness/packages/server/src/server.js +37 -1
- package/harness/packages/server/test/preview.test.js +111 -0
- package/harness/packages/server/test/server.test.js +186 -19
- package/harness/packages/server/test/telegram-features.test.js +2 -0
- package/harness/packages/server/test/telegram-voz.test.js +2 -0
- package/harness/packages/server/test/telegram.test.js +2 -0
- package/harness/tamnora.example.json +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
|
|
|
5
5
|
El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
|
|
6
6
|
y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
|
|
7
7
|
|
|
8
|
+
## [0.18.0] - 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Agregado
|
|
11
|
+
|
|
12
|
+
- **UI interactiva**: el agente pide un **formulario** (`ask_user`), ofrece
|
|
13
|
+
**botones** (`offer_choices`) o muestra un **checklist** (`show_plan`); la
|
|
14
|
+
consola los renderiza (el formulario pausa el turno, los botones se mandan como
|
|
15
|
+
mensaje, el checklist tiene estado) y los reconstruye al recargar la sesión.
|
|
16
|
+
- **MCP con carga diferida**: cuando hay muchas tools, se expone un índice y un
|
|
17
|
+
router `mcp_call` en vez de inyectar todos los esquemas (`mcpMode` `auto` /
|
|
18
|
+
`eager` / `lazy`; `mcpPinned` fija tools o servidores). Ajustes → MCP muestra
|
|
19
|
+
modo, fijas y conteo en contexto.
|
|
20
|
+
- **Catálogo y búsqueda de MCP y skills**: catálogo curado en "sugeridos" (se
|
|
21
|
+
agregan deshabilitados) y búsqueda en el registro oficial de MCP y en GitHub. El
|
|
22
|
+
agente suma `search_mcp` / `install_mcp` y `search_skills` / `install_skill`.
|
|
23
|
+
- **Preview remoto**: la pestaña preview publica el dev server por Tailscale
|
|
24
|
+
(`tailscale serve --https=8443`) para verlo desde otro dispositivo.
|
|
25
|
+
|
|
26
|
+
### Cambiado
|
|
27
|
+
|
|
28
|
+
- **Voz (Edge)**: la síntesis usa el host nuevo
|
|
29
|
+
(`api.msedgeservices.com/.../websocket/v1`) con el token como
|
|
30
|
+
`Ocp-Apim-Subscription-Key`; el host viejo respondía 403.
|
|
31
|
+
- **Asesor**: la tarjeta cambia de color y etiqueta según el tipo
|
|
32
|
+
(código / explicación / consejo / resumen).
|
|
33
|
+
|
|
34
|
+
### Seguridad
|
|
35
|
+
|
|
36
|
+
- **Tools MCP por permisos**: ahora pasan por el mismo control que las locales
|
|
37
|
+
(default `ask`); antes se ejecutaban sin consultar. Se habilita por tool con
|
|
38
|
+
`permissions: { "mcp__...": "allow" }`.
|
|
39
|
+
- **Endurecimiento de `bash`**: piso de seguridad que **deniega** los comandos
|
|
40
|
+
catastróficos aunque la política sea `allow` o `--yes` (`rm -rf /`/`~`, fork
|
|
41
|
+
bomb, `mkfs`, `dd of=/dev/`, formatear/borrar una unidad) y marca como `ask` los
|
|
42
|
+
irreversibles (`git push --force`, `git reset --hard`, `git clean -f`).
|
|
43
|
+
- **Scrub de entorno**: `bash` y los `git` locales ya no heredan las variables con
|
|
44
|
+
nombre de credencial (`*_API_KEY`, `*_TOKEN`, `*PASSWORD`, ...).
|
|
45
|
+
|
|
8
46
|
## [0.17.0] - 2026-10-11
|
|
9
47
|
|
|
10
48
|
### Cambios
|
package/README.md
CHANGED
|
@@ -22,8 +22,11 @@ con TUI, consola web, tools locales, MCP y evaluacion de modelos.
|
|
|
22
22
|
que se inyectan solo cuando el agente toca el archivo.
|
|
23
23
|
- **Automatizacion**: tareas programadas (`every`, `daily`, cron) que corren solas
|
|
24
24
|
y **deshacer** un turno (restaura los archivos a como estaban).
|
|
25
|
-
- **Contexto y costo**: skills on-demand,
|
|
26
|
-
y prompt caching.
|
|
25
|
+
- **Contexto y costo**: skills on-demand, MCP con **carga diferida** (índice +
|
|
26
|
+
router `mcp_call`), auto-compactacion, `max_tokens` por ronda y prompt caching.
|
|
27
|
+
- **UI interactiva**: el agente pide un **formulario** (`ask_user`), ofrece
|
|
28
|
+
**botones** (`offer_choices`) o muestra un **checklist** (`show_plan`); la
|
|
29
|
+
consola los renderiza.
|
|
27
30
|
- **Plataforma**: hooks y plugins locales (`.tamnora/hooks`, `.tamnora/plugins`).
|
|
28
31
|
|
|
29
32
|
## Instalacion
|
|
@@ -154,6 +157,20 @@ menu "mas" (los tres puntitos). En el chat:
|
|
|
154
157
|
archivo; con **deshacer cambios** (menú `⋮` de cada respuesta) se restauran los
|
|
155
158
|
archivos y se borra el turno del historial.
|
|
156
159
|
|
|
160
|
+
### UI interactiva
|
|
161
|
+
|
|
162
|
+
El agente no numera preguntas en el texto: cuando necesita decidir algo, usa un
|
|
163
|
+
componente que la consola dibuja.
|
|
164
|
+
|
|
165
|
+
- **Formulario** (`ask_user`): pausa el turno y devuelve las respuestas (texto,
|
|
166
|
+
textarea, select o confirmar). En Telegram/CLI cae a una pregunta en prosa.
|
|
167
|
+
- **Botones** (`offer_choices`): opciones tappables; al tocar una se manda como tu
|
|
168
|
+
mensaje (bifurcaciones A/B/C). No pausa el turno.
|
|
169
|
+
- **Checklist** (`show_plan`): pasos con estado (`pending`/`active`/`done`); es de
|
|
170
|
+
solo lectura y el agente lo reemite para actualizarlo.
|
|
171
|
+
|
|
172
|
+
Los tres se renderizan en vivo y se reconstruyen al recargar la consola.
|
|
173
|
+
|
|
157
174
|
### Contexto y costo
|
|
158
175
|
|
|
159
176
|
- **Skills on-demand**: en vez de inyectar todas las skills, el prompt lleva un
|
|
@@ -269,14 +286,31 @@ Los MCP stdio **no heredan `process.env`**: reciben un baseline minimo mas el
|
|
|
269
286
|
(ver [`SECURITY.md`](SECURITY.md)). Para un servidor de confianza que necesite el
|
|
270
287
|
entorno completo, `"inheritEnv": true`.
|
|
271
288
|
|
|
289
|
+
Cuando hay **muchas** tools MCP, el harness no inyecta todos los esquemas en cada
|
|
290
|
+
turno: expone un **indice** y una tool router `mcp_call({ tool, args })`, y deja
|
|
291
|
+
solo las "calientes" con esquema completo (`mcpMode`: `auto` | `eager` | `lazy`;
|
|
292
|
+
`mcpPinned` fija tools o servidores enteros). Ademas, las tools MCP pasan por los
|
|
293
|
+
**mismos permisos** que las locales (default `ask`): se habilita por tool con
|
|
294
|
+
`permissions: { "mcp__postgres__query": "allow" }`. Se ajustan en Ajustes → MCP.
|
|
295
|
+
|
|
296
|
+
Desde Ajustes → MCP tambien hay un **catalogo de sugeridos** y busqueda en el
|
|
297
|
+
registro oficial; se agregan deshabilitados y los prendes cuando quieras.
|
|
298
|
+
|
|
272
299
|
## Tools
|
|
273
300
|
|
|
274
301
|
Tools locales confinadas al workspace (`read`, `list`, `glob`, `grep`, `write`,
|
|
275
302
|
`edit`, `bash`, `fetch_url`), de navegador (`screenshot`, `console_errors`,
|
|
276
|
-
`preview_url`), de voz (`speak`), de
|
|
303
|
+
`preview_url`), de voz (`speak`), de interfaz interactiva (`ask_user`,
|
|
304
|
+
`offer_choices`, `show_plan`), de Git (`git_status`, `git_diff`, `git_log`,
|
|
277
305
|
`git_commit`) y de conocimiento (`memory`, `skill`), con permisos
|
|
278
306
|
`allow`/`ask`/`deny` por tool o por patron.
|
|
279
307
|
|
|
308
|
+
`bash` esta **endurecido**: un piso de seguridad **deniega** los comandos
|
|
309
|
+
catastroficos (`rm -rf /`/`~`, fork bomb, `mkfs`, `dd of=/dev/`, formatear/borrar
|
|
310
|
+
una unidad) aunque la politica sea `allow`, marca como `ask` los irreversibles
|
|
311
|
+
(`git push --force`, `git reset --hard`) y el shell **no hereda** las variables con
|
|
312
|
+
nombre de credencial (`scrubEnv`), asi que no filtra tus API keys.
|
|
313
|
+
|
|
280
314
|
`fetch_url` lee URLs (HTML a texto) y avisa cuando un sitio exige JavaScript o
|
|
281
315
|
esta detras de Cloudflare; para esos casos las tools `screenshot` / `console_errors`
|
|
282
316
|
usan el **Chrome/Edge/Chromium del sistema** como navegador headless (sin
|
package/SECURITY.md
CHANGED
|
@@ -35,6 +35,14 @@ asi que se lo debe tratar como codigo con acceso a tu usuario.
|
|
|
35
35
|
servidores de confianza. Un tool tipo `get-env` no puede leer tus secretos del
|
|
36
36
|
shell, y los resultados MCP se **redactan** (`redactSecrets`) por si devuelven un
|
|
37
37
|
token. No pases credenciales en los argumentos de las tools: van por `env`.
|
|
38
|
+
- **Tools locales y `bash`**: `bash` corre en tu maquina como tu usuario, asi que
|
|
39
|
+
hay un piso de seguridad que **no se puede saltear** ni con `bash: "allow"` ni
|
|
40
|
+
con `--yes`: los comandos catastroficos (`rm -rf /`/`~`, fork bomb, `mkfs`,
|
|
41
|
+
`dd of=/dev/`, formatear/borrar una unidad) quedan **denegados**, y los
|
|
42
|
+
irreversibles (`rm -rf *`/`.`, `git push --force`, `git reset --hard`,
|
|
43
|
+
`git clean -f`) al menos `ask`. Ademas el shell y los `git` locales **no heredan**
|
|
44
|
+
las variables con nombre de credencial (`scrubEnv` quita `*_API_KEY`, `*_TOKEN`,
|
|
45
|
+
`*PASSWORD`, ...), asi que `env`/`printenv` no filtran tus claves del proceso.
|
|
38
46
|
|
|
39
47
|
## Buenas practicas
|
|
40
48
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { loadConfig, buildMcpRegistry, mcpTransport } from '../lib/core.js';
|
|
2
|
+
import { loadConfig, buildMcpRegistry, mcpTransport, Store, databasePath } from '../lib/core.js';
|
|
3
3
|
import { parse } from '../lib/util.js';
|
|
4
4
|
import { color } from '../ui/color.js';
|
|
5
5
|
|
|
@@ -9,6 +9,26 @@ const OPTIONS = {
|
|
|
9
9
|
help: { type: 'boolean', short: 'h' },
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/** Servidores de `tamnora.json` + los guardados desde la consola (gana la base). */
|
|
13
|
+
function mergedServers(config, store) {
|
|
14
|
+
const fromConfig = Array.isArray(config.mcp) ? config.mcp : [];
|
|
15
|
+
const fromStore = store.listMcpServers().map((row) => ({
|
|
16
|
+
id: row.id,
|
|
17
|
+
name: row.name ?? row.id,
|
|
18
|
+
type: row.transport ?? 'stdio',
|
|
19
|
+
command: row.command ?? undefined,
|
|
20
|
+
args: Array.isArray(row.config?.args) ? row.config.args : [],
|
|
21
|
+
url: row.url ?? undefined,
|
|
22
|
+
env: row.config?.env ?? undefined,
|
|
23
|
+
inheritEnv: row.config?.inheritEnv === true,
|
|
24
|
+
enabled: row.enabled !== false,
|
|
25
|
+
}));
|
|
26
|
+
const byId = new Map();
|
|
27
|
+
for (const server of fromConfig) if (server?.id) byId.set(server.id, server);
|
|
28
|
+
for (const server of fromStore) byId.set(server.id, server);
|
|
29
|
+
return [...byId.values()].filter((server) => server.enabled !== false);
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
export async function mcpCommand(argv) {
|
|
13
33
|
const { values } = parse(argv, OPTIONS);
|
|
14
34
|
if (values.help) {
|
|
@@ -18,11 +38,17 @@ export async function mcpCommand(argv) {
|
|
|
18
38
|
|
|
19
39
|
const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
|
|
20
40
|
const config = await loadConfig(cwd);
|
|
21
|
-
const
|
|
41
|
+
const store = new Store(databasePath(cwd));
|
|
42
|
+
let servers;
|
|
43
|
+
try {
|
|
44
|
+
servers = mergedServers(config, store);
|
|
45
|
+
} finally {
|
|
46
|
+
store.close?.();
|
|
47
|
+
}
|
|
22
48
|
|
|
23
49
|
if (!servers.length) {
|
|
24
50
|
if (values.json) process.stdout.write(`${JSON.stringify({ servers: [] }, null, 2)}\n`);
|
|
25
|
-
else process.stdout.write(color.dim('No hay servidores MCP
|
|
51
|
+
else process.stdout.write(color.dim('No hay servidores MCP habilitados (tamnora.json + consola).\n'));
|
|
26
52
|
return 0;
|
|
27
53
|
}
|
|
28
54
|
|
|
@@ -93,7 +119,8 @@ function safeTransport(server) {
|
|
|
93
119
|
function printHelp() {
|
|
94
120
|
console.log(`Uso: tamnora mcp [opciones]
|
|
95
121
|
|
|
96
|
-
Conecta a los servidores MCP de tamnora.json y
|
|
122
|
+
Conecta a los servidores MCP habilitados (de tamnora.json y de la consola) y
|
|
123
|
+
lista sus herramientas.
|
|
97
124
|
|
|
98
125
|
Opciones:
|
|
99
126
|
--dir <path> Directorio del proyecto (tamnora.json)
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
scopeDirective,
|
|
10
10
|
} from '../lib/core.js';
|
|
11
11
|
import { parse, databasePath, num } from '../lib/util.js';
|
|
12
|
-
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
|
|
12
|
+
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
|
|
13
13
|
import { color } from '../ui/color.js';
|
|
14
14
|
|
|
15
15
|
const OPTIONS = {
|
|
@@ -31,11 +31,12 @@ export async function replCommand(argv) {
|
|
|
31
31
|
|
|
32
32
|
const instructions = await loadInstructions(cwd, config.instructions);
|
|
33
33
|
let systemExtra = '';
|
|
34
|
+
let mcpBlock = '';
|
|
34
35
|
const maxRounds = num(values['max-steps'], config.maxRounds);
|
|
35
36
|
const store = new Store(databasePath(cwd));
|
|
36
37
|
let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
|
|
37
38
|
const buildPrompt = () =>
|
|
38
|
-
buildSystemPrompt(instructions, [systemExtra, scopeDirective(strict)].filter(Boolean).join('\n\n'));
|
|
39
|
+
buildSystemPrompt(instructions, [systemExtra, mcpBlock, scopeDirective(strict)].filter(Boolean).join('\n\n'));
|
|
39
40
|
const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
|
|
40
41
|
let providerId = resolved.providerId;
|
|
41
42
|
let model = resolved.model;
|
|
@@ -76,6 +77,8 @@ export async function replCommand(argv) {
|
|
|
76
77
|
process.stdout.write(color.yellow(`MCP: ${err.message}\n`));
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
const { mode: mcpMode, pinned: mcpPinned } = mcpSettings(store, config);
|
|
81
|
+
mcpBlock = mcpIndexBlock(mcp, { mode: mcpMode });
|
|
79
82
|
const enabled = values['no-tools']
|
|
80
83
|
? []
|
|
81
84
|
: values.tools
|
|
@@ -90,6 +93,8 @@ export async function replCommand(argv) {
|
|
|
90
93
|
yes: Boolean(values.yes),
|
|
91
94
|
onAsk,
|
|
92
95
|
mcp,
|
|
96
|
+
mcpMode,
|
|
97
|
+
mcpPinned,
|
|
93
98
|
});
|
|
94
99
|
|
|
95
100
|
process.stdout.write(color.dim(`Tamnora REPL · ${providerId}/${model}\n`));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry, scopeDirective } from '../lib/core.js';
|
|
3
3
|
import { parse, databasePath, num } from '../lib/util.js';
|
|
4
|
-
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
|
|
4
|
+
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
|
|
5
5
|
import { color } from '../ui/color.js';
|
|
6
6
|
|
|
7
7
|
const OPTIONS = {
|
|
@@ -64,12 +64,15 @@ export async function runCommand(argv) {
|
|
|
64
64
|
: Array.isArray(config.tools) && config.tools.length
|
|
65
65
|
? config.tools
|
|
66
66
|
: undefined;
|
|
67
|
+
const { mode: mcpMode, pinned: mcpPinned } = mcpSettings(store, config);
|
|
67
68
|
const { definitions: toolDefs, execute } = createTools({
|
|
68
69
|
root: cwd,
|
|
69
70
|
enabled,
|
|
70
71
|
permissions: config.permissions,
|
|
71
72
|
yes: Boolean(values.yes),
|
|
72
73
|
mcp,
|
|
74
|
+
mcpMode,
|
|
75
|
+
mcpPinned,
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
let sessionId = values.session;
|
|
@@ -108,7 +111,7 @@ export async function runCommand(argv) {
|
|
|
108
111
|
customProviders,
|
|
109
112
|
systemPrompt: buildSystemPrompt(
|
|
110
113
|
instructions,
|
|
111
|
-
[values.system, scopeDirective(strict)].filter(Boolean).join('\n\n')
|
|
114
|
+
[values.system, mcpIndexBlock(mcp, { mode: mcpMode }), scopeDirective(strict)].filter(Boolean).join('\n\n')
|
|
112
115
|
),
|
|
113
116
|
history,
|
|
114
117
|
input: prompt,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode, scopeDirective } from '../lib/core.js';
|
|
3
3
|
import { parse, databasePath, num } from '../lib/util.js';
|
|
4
|
-
import { buildSystemPrompt, resolveCliProvider } from '../lib/runtime.js';
|
|
4
|
+
import { buildSystemPrompt, resolveCliProvider, mcpSettings, mcpIndexBlock } from '../lib/runtime.js';
|
|
5
5
|
import { runTui } from '../tui/app.js';
|
|
6
6
|
import { color } from '../ui/color.js';
|
|
7
7
|
|
|
@@ -55,6 +55,8 @@ export async function tuiCommand(argv) {
|
|
|
55
55
|
process.stderr.write(color.yellow(`MCP: ${err.message}\n`));
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
const { mode: mcpMode, pinned: mcpPinned } = mcpSettings(store, config);
|
|
59
|
+
const mcpBlock = mcpIndexBlock(mcp, { mode: mcpMode });
|
|
58
60
|
|
|
59
61
|
const baseEnabled = values['no-tools']
|
|
60
62
|
? []
|
|
@@ -69,7 +71,10 @@ export async function tuiCommand(argv) {
|
|
|
69
71
|
const enabledFor = (m) =>
|
|
70
72
|
m.tools ? (baseEnabled ? baseEnabled.filter((tool) => m.tools.includes(tool)) : m.tools) : baseEnabled;
|
|
71
73
|
const promptFor = (m) =>
|
|
72
|
-
buildSystemPrompt(
|
|
74
|
+
buildSystemPrompt(
|
|
75
|
+
instructions,
|
|
76
|
+
[values.system, m.systemPrompt, mcpBlock, scopeDirective(strict)].filter(Boolean).join('\n\n')
|
|
77
|
+
);
|
|
73
78
|
const modeContext = (id) => {
|
|
74
79
|
const m = resolveMode(id);
|
|
75
80
|
return { label: m.label, enabled: enabledFor(m), systemPrompt: promptFor(m) };
|
|
@@ -101,6 +106,8 @@ export async function tuiCommand(argv) {
|
|
|
101
106
|
enabled: enabledFor(mode),
|
|
102
107
|
permissions: config.permissions,
|
|
103
108
|
mcp,
|
|
109
|
+
mcpMode,
|
|
110
|
+
mcpPinned,
|
|
104
111
|
yes: Boolean(values.yes),
|
|
105
112
|
},
|
|
106
113
|
store,
|
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
import { runAgent, resolveProviderRuntime, composeProviderSettings, resolveApiKey } from './core.js';
|
|
1
|
+
import { runAgent, resolveProviderRuntime, composeProviderSettings, resolveApiKey, useLazyMcp, buildMcpIndex, MCP_AUTO_THRESHOLD } from './core.js';
|
|
2
|
+
|
|
3
|
+
const MCP_MODES = new Set(['auto', 'eager', 'lazy']);
|
|
4
|
+
|
|
5
|
+
/** Modo y tools fijadas de MCP (setting de la base, con fallback a tamnora.json). */
|
|
6
|
+
export function mcpSettings(store, config = {}) {
|
|
7
|
+
const raw = String(store.getSetting('mcpMode') ?? config.mcpMode ?? 'auto');
|
|
8
|
+
const mode = MCP_MODES.has(raw) ? raw : 'auto';
|
|
9
|
+
const stored = store.getSetting('mcpPinned');
|
|
10
|
+
const pinned = Array.isArray(stored)
|
|
11
|
+
? stored.map(String)
|
|
12
|
+
: Array.isArray(config.mcpPinned)
|
|
13
|
+
? config.mcpPinned.map(String)
|
|
14
|
+
: [];
|
|
15
|
+
return { mode, pinned };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Bloque de indice de MCP para el prompt (vacio si el modo es completo). */
|
|
19
|
+
export function mcpIndexBlock(mcp, { mode = 'auto' } = {}) {
|
|
20
|
+
const total = mcp?.definitions?.length ?? 0;
|
|
21
|
+
if (!total || !useLazyMcp(mode, total, MCP_AUTO_THRESHOLD)) return '';
|
|
22
|
+
return buildMcpIndex(mcp.tools ?? []);
|
|
23
|
+
}
|
|
2
24
|
|
|
3
25
|
/**
|
|
4
26
|
* Resolve provider/model/apiKey/baseUrl for CLI commands, merging the active
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { parseTunnelmoleUrls, parseTailscaleStatus, startTunnel } from '../src/lib/tunnel.js';
|
|
3
|
+
import { parseTunnelmoleUrls, parseTailscaleStatus, startTunnel, tailscalePreviewUrl } from '../src/lib/tunnel.js';
|
|
4
4
|
import { qrTerminal } from '../src/lib/qr.js';
|
|
5
5
|
|
|
6
6
|
test('parseTunnelmoleUrls extracts the https URL', () => {
|
|
@@ -32,6 +32,18 @@ test('startTunnel rechaza el proveedor retirado (relay)', async () => {
|
|
|
32
32
|
await assert.rejects(() => startTunnel(4517, 'relay'), /no soportado/);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
test('startTunnel con provider none no abre nada', async () => {
|
|
36
|
+
const tun = await startTunnel(4517, 'none', { preview: true });
|
|
37
|
+
assert.equal(tun.provider, 'none');
|
|
38
|
+
assert.equal(tun.url, '');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('tailscalePreviewUrl usa el 8443 y omite el 443', () => {
|
|
42
|
+
assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net'), 'https://pc.tailnet.ts.net:8443');
|
|
43
|
+
assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net', 9000), 'https://pc.tailnet.ts.net:9000');
|
|
44
|
+
assert.equal(tailscalePreviewUrl('pc.tailnet.ts.net', 443), 'https://pc.tailnet.ts.net');
|
|
45
|
+
});
|
|
46
|
+
|
|
35
47
|
test('qrTerminal renders a block matrix for a URL', () => {
|
|
36
48
|
const art = qrTerminal('https://abc-123.tunnelmole.net', 1);
|
|
37
49
|
assert.ok(art.length > 0);
|
|
@@ -21,9 +21,11 @@ export const DEFAULT_CONFIG = {
|
|
|
21
21
|
tools: [],
|
|
22
22
|
permissions: {},
|
|
23
23
|
mcp: [],
|
|
24
|
+
mcpMode: 'auto',
|
|
25
|
+
mcpPinned: [],
|
|
24
26
|
server: { host: '127.0.0.1', port: 4517 },
|
|
25
27
|
processes: { enabled: true, allow: [] },
|
|
26
|
-
preview: { provider: '
|
|
28
|
+
preview: { provider: 'tailscale', tailscalePort: 8443, fallback: true },
|
|
27
29
|
telegram: { token: '', allowedChatIds: [] },
|
|
28
30
|
agents: { maxRounds: 6 },
|
|
29
31
|
};
|
|
@@ -4,6 +4,8 @@ export {
|
|
|
4
4
|
redactDeep,
|
|
5
5
|
secretValues,
|
|
6
6
|
isSecretName,
|
|
7
|
+
isSensitiveEnvName,
|
|
8
|
+
scrubEnv,
|
|
7
9
|
maskSecret,
|
|
8
10
|
maskEnv,
|
|
9
11
|
REDACTED,
|
|
@@ -77,8 +79,15 @@ export {
|
|
|
77
79
|
TOOL_NAMES,
|
|
78
80
|
DEFAULT_PERMISSIONS,
|
|
79
81
|
evaluatePermission,
|
|
82
|
+
bashGuard,
|
|
80
83
|
resolveInRoot,
|
|
81
84
|
toRel,
|
|
85
|
+
MCP_ROUTER_NAME,
|
|
86
|
+
MCP_AUTO_THRESHOLD,
|
|
87
|
+
useLazyMcp,
|
|
88
|
+
buildMcpIndex,
|
|
89
|
+
mcpRouterDefinition,
|
|
90
|
+
planMcpTools,
|
|
82
91
|
} from './tools/index.js';
|
|
83
92
|
export { Store, openDatabase } from './storage/sqlite.js';
|
|
84
93
|
export { runTask } from './eval/runTask.js';
|
|
@@ -96,7 +105,8 @@ export {
|
|
|
96
105
|
buildSkillsPrompt,
|
|
97
106
|
buildSkillsIndex,
|
|
98
107
|
} from './skills/index.js';
|
|
99
|
-
export { parseSkillRef, fetchSkillFromGithub } from './skills/remote.js';
|
|
108
|
+
export { parseSkillRef, fetchSkillFromGithub, searchSkillsGithub } from './skills/remote.js';
|
|
109
|
+
export { SKILLS_CATALOG, skillCatalogEntry } from './skills/catalog.js';
|
|
100
110
|
export {
|
|
101
111
|
MEMORY_DIR,
|
|
102
112
|
MEMORY_INDEX_NAME,
|
|
@@ -130,16 +140,20 @@ export {
|
|
|
130
140
|
searchMcpRegistry,
|
|
131
141
|
getMcpRegistryServer,
|
|
132
142
|
registryServerToConfig,
|
|
143
|
+
registryServerToCandidate,
|
|
133
144
|
normalizeMcpConfig,
|
|
134
145
|
} from './mcp/registry.js';
|
|
146
|
+
export { MCP_CATALOG, mcpCatalogEntry } from './mcp/catalog.js';
|
|
135
147
|
export {
|
|
136
148
|
parseTunnelmoleUrls,
|
|
137
149
|
parseTailscaleStatus,
|
|
138
150
|
probeTailscale,
|
|
139
151
|
resetTailscaleServe,
|
|
140
152
|
startTailscaleServe,
|
|
153
|
+
startTailscalePreview,
|
|
141
154
|
startTunnelmole,
|
|
142
155
|
startTunnel,
|
|
156
|
+
tailscalePreviewUrl,
|
|
143
157
|
tunnelState,
|
|
144
158
|
tunnelActive,
|
|
145
159
|
} from './tunnel.js';
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Catalogo curado de servidores MCP sugeridos. No se instalan solos: la consola
|
|
2
|
+
// (o el agente) los agrega deshabilitados y el usuario decide habilitarlos.
|
|
3
|
+
// Los `env` con valor "env:VAR" se resuelven desde el entorno al arrancar el MCP.
|
|
4
|
+
|
|
5
|
+
export const MCP_CATALOG = [
|
|
6
|
+
{
|
|
7
|
+
id: 'playwright',
|
|
8
|
+
name: 'Playwright (navegador)',
|
|
9
|
+
description:
|
|
10
|
+
'Automatiza un navegador real: webs con JavaScript, formularios y scraping. Complementa las tools screenshot/console_errors.',
|
|
11
|
+
type: 'stdio',
|
|
12
|
+
command: 'npx',
|
|
13
|
+
args: ['-y', '@playwright/mcp@latest'],
|
|
14
|
+
enabled: false,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'fetch',
|
|
18
|
+
name: 'Fetch (webs con JS)',
|
|
19
|
+
description: 'Trae cualquier URL y la convierte a texto, incluidas las que renderizan con JavaScript. Requiere uv.',
|
|
20
|
+
type: 'stdio',
|
|
21
|
+
command: 'uvx',
|
|
22
|
+
args: ['mcp-server-fetch'],
|
|
23
|
+
enabled: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'github',
|
|
27
|
+
name: 'GitHub',
|
|
28
|
+
description: 'Repos, issues, PRs y búsqueda en GitHub. Necesita un token personal.',
|
|
29
|
+
type: 'stdio',
|
|
30
|
+
command: 'npx',
|
|
31
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
32
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: 'env:GITHUB_PERSONAL_ACCESS_TOKEN' },
|
|
33
|
+
requiredEnv: ['GITHUB_PERSONAL_ACCESS_TOKEN'],
|
|
34
|
+
enabled: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'memory',
|
|
38
|
+
name: 'Memory (grafo)',
|
|
39
|
+
description: 'Memoria de conocimiento persistente: entidades y relaciones que sobreviven entre sesiones.',
|
|
40
|
+
type: 'stdio',
|
|
41
|
+
command: 'npx',
|
|
42
|
+
args: ['-y', '@modelcontextprotocol/server-memory'],
|
|
43
|
+
enabled: false,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'sequential-thinking',
|
|
47
|
+
name: 'Sequential Thinking',
|
|
48
|
+
description: 'Razonamiento paso a paso para descomponer problemas complejos.',
|
|
49
|
+
type: 'stdio',
|
|
50
|
+
command: 'npx',
|
|
51
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
|
52
|
+
enabled: false,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'sqlite',
|
|
56
|
+
name: 'SQLite',
|
|
57
|
+
description: 'Consulta una base SQLite. Reemplazá la ruta por tu archivo .db. Requiere uv.',
|
|
58
|
+
type: 'stdio',
|
|
59
|
+
command: 'uvx',
|
|
60
|
+
args: ['mcp-server-sqlite', '--db-path', 'ruta/a/base.db'],
|
|
61
|
+
enabled: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'time',
|
|
65
|
+
name: 'Time',
|
|
66
|
+
description: 'Hora actual y conversión de zonas horarias. Requiere uv.',
|
|
67
|
+
type: 'stdio',
|
|
68
|
+
command: 'uvx',
|
|
69
|
+
args: ['mcp-server-time'],
|
|
70
|
+
enabled: false,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/** Entrada del catalogo por id, como copia (siempre deshabilitada). */
|
|
75
|
+
export function mcpCatalogEntry(id) {
|
|
76
|
+
const entry = MCP_CATALOG.find((item) => item.id === id);
|
|
77
|
+
if (!entry) return null;
|
|
78
|
+
return { ...entry, args: [...(entry.args ?? [])], env: entry.env ? { ...entry.env } : undefined, enabled: false };
|
|
79
|
+
}
|
|
@@ -70,12 +70,19 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
|
|
|
70
70
|
|
|
71
71
|
const map = new Map();
|
|
72
72
|
const definitions = [];
|
|
73
|
+
const tools = [];
|
|
73
74
|
const sessions = [];
|
|
74
75
|
const errors = [];
|
|
75
76
|
|
|
76
|
-
const add = (name, definition, call) => {
|
|
77
|
+
const add = (server, name, definition, call) => {
|
|
77
78
|
map.set(name, call);
|
|
78
79
|
definitions.push(definition);
|
|
80
|
+
tools.push({
|
|
81
|
+
name,
|
|
82
|
+
server: server.id,
|
|
83
|
+
serverName: server.name ?? server.id,
|
|
84
|
+
description: definition.function?.description ?? '',
|
|
85
|
+
});
|
|
79
86
|
};
|
|
80
87
|
|
|
81
88
|
for (const r of results) {
|
|
@@ -101,19 +108,21 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
|
|
|
101
108
|
|
|
102
109
|
for (const tool of session.tools ?? []) {
|
|
103
110
|
const name = qualifiedToolName(server.id, tool.name);
|
|
104
|
-
add(name, mcpToolToDefinition(tool, server, name), async (args) => (await reconnect()).callTool(tool.name, args));
|
|
111
|
+
add(server, name, mcpToolToDefinition(tool, server, name), async (args) => (await reconnect()).callTool(tool.name, args));
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
if (session.capabilities?.resources || session.resources?.length) {
|
|
108
115
|
const label = server.name ?? server.id;
|
|
109
116
|
const listName = qualifiedToolName(server.id, 'list_resources');
|
|
110
117
|
add(
|
|
118
|
+
server,
|
|
111
119
|
listName,
|
|
112
120
|
syntheticDefinition(listName, `Lista los recursos MCP del servidor ${label}.`),
|
|
113
121
|
async () => formatResources(await (await reconnect()).listResources())
|
|
114
122
|
);
|
|
115
123
|
const readName = qualifiedToolName(server.id, 'read_resource');
|
|
116
124
|
add(
|
|
125
|
+
server,
|
|
117
126
|
readName,
|
|
118
127
|
syntheticDefinition(readName, `Lee un recurso MCP del servidor ${label} a partir de su uri.`, {
|
|
119
128
|
type: 'object',
|
|
@@ -128,12 +137,14 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
|
|
|
128
137
|
const label = server.name ?? server.id;
|
|
129
138
|
const listName = qualifiedToolName(server.id, 'list_prompts');
|
|
130
139
|
add(
|
|
140
|
+
server,
|
|
131
141
|
listName,
|
|
132
142
|
syntheticDefinition(listName, `Lista los prompts MCP del servidor ${label}.`),
|
|
133
143
|
async () => formatPrompts(await (await reconnect()).listPrompts())
|
|
134
144
|
);
|
|
135
145
|
const getName = qualifiedToolName(server.id, 'get_prompt');
|
|
136
146
|
add(
|
|
147
|
+
server,
|
|
137
148
|
getName,
|
|
138
149
|
syntheticDefinition(getName, `Obtiene un prompt MCP del servidor ${label}.`, {
|
|
139
150
|
type: 'object',
|
|
@@ -150,6 +161,7 @@ export async function buildMcpRegistry(servers = [], { signal } = {}) {
|
|
|
150
161
|
|
|
151
162
|
return {
|
|
152
163
|
definitions,
|
|
164
|
+
tools,
|
|
153
165
|
sessions,
|
|
154
166
|
errors,
|
|
155
167
|
resources: sessions.flatMap((s) => s.resources ?? []),
|
|
@@ -54,7 +54,9 @@ export function registryServerToConfig(server) {
|
|
|
54
54
|
if (runtime === 'npx' && !args.includes('-y')) args.unshift('-y');
|
|
55
55
|
args.push(`${pkg.identifier}${pkg.version ? `@${pkg.version}` : ''}`);
|
|
56
56
|
const env = {};
|
|
57
|
-
for (const variable of pkg.environmentVariables ?? [])
|
|
57
|
+
for (const variable of pkg.environmentVariables ?? []) {
|
|
58
|
+
env[variable.name] = variable.isRequired ? `env:${variable.name}` : '';
|
|
59
|
+
}
|
|
58
60
|
const requiredEnv = (pkg.environmentVariables ?? []).filter((v) => v.isRequired).map((v) => v.name);
|
|
59
61
|
return {
|
|
60
62
|
id,
|
|
@@ -70,6 +72,32 @@ export function registryServerToConfig(server) {
|
|
|
70
72
|
throw new Error('El servidor del registro no tiene remotes ni packages compatibles.');
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/** Vista liviana de un server del registro, para mostrar candidatos sin instalar. */
|
|
76
|
+
export function registryServerToCandidate(server) {
|
|
77
|
+
const id = slugifyId(server?.name) || 'mcp';
|
|
78
|
+
const remote = (server?.remotes ?? [])[0];
|
|
79
|
+
const pkg = (server?.packages ?? [])[0];
|
|
80
|
+
const runtime = pkg ? pkg.runtimeHint || (pkg.registryType === 'npm' ? 'npx' : 'uvx') : '';
|
|
81
|
+
const requiredEnv = (pkg?.environmentVariables ?? []).filter((v) => v.isRequired).map((v) => v.name);
|
|
82
|
+
const args = pkg
|
|
83
|
+
? [
|
|
84
|
+
...(pkg.runtimeArguments ?? []).filter((a) => a.type === 'positional' && a.value).map((a) => a.value),
|
|
85
|
+
`${pkg.identifier}${pkg.version ? `@${pkg.version}` : ''}`,
|
|
86
|
+
]
|
|
87
|
+
: [];
|
|
88
|
+
return {
|
|
89
|
+
id,
|
|
90
|
+
name: server?.title || server?.name || id,
|
|
91
|
+
description: server?.description ?? '',
|
|
92
|
+
registry: server?.name ?? null,
|
|
93
|
+
transport: remote?.url ? 'http' : 'stdio',
|
|
94
|
+
url: remote?.url ?? null,
|
|
95
|
+
command: runtime || null,
|
|
96
|
+
args,
|
|
97
|
+
requiredEnv,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
/**
|
|
74
102
|
* Normaliza un snippet JSON a nuestra config. Acepta:
|
|
75
103
|
* - `{ command, args, env }` o `{ url }`
|
|
@@ -27,6 +27,31 @@ export function isSecretName(name) {
|
|
|
27
27
|
return SECRET_NAME.test(String(name ?? ''));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Nombre sensible con criterio mas estricto (para el scrub de entorno): solo
|
|
32
|
+
* credenciales reales, para no romper variables legitimas como `SSH_AUTH_SOCK`
|
|
33
|
+
* o `DBUS_SESSION_BUS_ADDRESS` que matchean el patron amplio de `isSecretName`.
|
|
34
|
+
*/
|
|
35
|
+
const SENSITIVE_ENV_NAME = /(API[_-]?KEY|ACCESS[_-]?KEY|SECRET|PASSWORD|PASSWD|TOKEN|CREDENTIAL|PRIVATE[_-]?KEY|OAUTH)/i;
|
|
36
|
+
|
|
37
|
+
export function isSensitiveEnvName(name) {
|
|
38
|
+
return SENSITIVE_ENV_NAME.test(String(name ?? ''));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Copia el entorno sin las variables con nombre de credencial, para no filtrar
|
|
43
|
+
* secretos a un proceso hijo (`bash`). Conserva PATH/HOME/etc. Defensa en
|
|
44
|
+
* profundidad: el comando del modelo no puede leer `env` y ver las claves.
|
|
45
|
+
*/
|
|
46
|
+
export function scrubEnv(env = getEnv()) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [name, value] of Object.entries(env ?? {})) {
|
|
49
|
+
if (isSensitiveEnvName(name)) continue;
|
|
50
|
+
out[name] = value;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
/**
|
|
31
56
|
* Valores de entorno que hay que redactar: los de variables con nombre
|
|
32
57
|
* sensible y al menos 8 caracteres (evita tapar valores triviales).
|