@danieltmn/tamnora 0.1.0 → 0.3.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 (58) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/harness/packages/cli/src/cli.js +2 -0
  3. package/harness/packages/cli/src/commands/eval.js +3 -0
  4. package/harness/packages/cli/src/commands/repl.js +2 -0
  5. package/harness/packages/cli/src/commands/run.js +2 -1
  6. package/harness/packages/cli/src/commands/tui.js +18 -11
  7. package/harness/packages/cli/src/lib/runtime.js +3 -1
  8. package/harness/packages/cli/src/lib/tunnel.js +2 -113
  9. package/harness/packages/cli/src/lib/util.js +2 -0
  10. package/harness/packages/cli/src/tui/app.js +45 -4
  11. package/harness/packages/cli/src/tui/keys.js +3 -0
  12. package/harness/packages/cli/test/tui.test.js +5 -0
  13. package/harness/packages/core/src/agent/modes.js +2 -2
  14. package/harness/packages/core/src/config/index.js +3 -0
  15. package/harness/packages/core/src/console.js +18 -0
  16. package/harness/packages/core/src/eval/judge.js +3 -3
  17. package/harness/packages/core/src/eval/runner.js +3 -0
  18. package/harness/packages/core/src/index.js +11 -0
  19. package/harness/packages/core/src/mcp/registry.js +117 -0
  20. package/harness/packages/core/src/procs/detect.js +69 -0
  21. package/harness/packages/core/src/providers/ollama.js +3 -3
  22. package/harness/packages/core/src/providers/openaiCompatible.js +3 -3
  23. package/harness/packages/core/src/providers/openrouter.js +2 -2
  24. package/harness/packages/core/src/providers/resilience.js +1 -1
  25. package/harness/packages/core/src/providers/utils.js +43 -0
  26. package/harness/packages/core/src/skills/index.js +90 -0
  27. package/harness/packages/core/src/skills/remote.js +80 -0
  28. package/harness/packages/core/src/storage/sqlite.js +21 -0
  29. package/harness/packages/core/src/tools/executors.js +55 -0
  30. package/harness/packages/core/src/tools/permissions.js +2 -0
  31. package/harness/packages/core/src/tools/schema.js +16 -0
  32. package/harness/packages/core/src/tunnel.js +113 -0
  33. package/harness/packages/core/test/config.test.js +15 -0
  34. package/harness/packages/core/test/mcp-registry.test.js +49 -0
  35. package/harness/packages/core/test/procs.test.js +34 -0
  36. package/harness/packages/core/test/providers.test.js +20 -0
  37. package/harness/packages/core/test/resilience.test.js +1 -0
  38. package/harness/packages/core/test/skills.test.js +69 -0
  39. package/harness/packages/core/test/tools.test.js +10 -0
  40. package/harness/packages/server/public/app.js +484 -6
  41. package/harness/packages/server/public/index.html +134 -6
  42. package/harness/packages/server/public/styles.css +45 -1
  43. package/harness/packages/server/src/cli.js +2 -0
  44. package/harness/packages/server/src/lib/agentTools.js +238 -0
  45. package/harness/packages/server/src/lib/procs.js +125 -0
  46. package/harness/packages/server/src/lib/prompt.js +6 -3
  47. package/harness/packages/server/src/lib/skills.js +69 -0
  48. package/harness/packages/server/src/lib/telegram.js +267 -0
  49. package/harness/packages/server/src/routes/chat.js +9 -1
  50. package/harness/packages/server/src/routes/mcp.js +128 -9
  51. package/harness/packages/server/src/routes/preview.js +58 -0
  52. package/harness/packages/server/src/routes/procs.js +47 -0
  53. package/harness/packages/server/src/routes/sessions.js +10 -0
  54. package/harness/packages/server/src/routes/skills.js +94 -0
  55. package/harness/packages/server/src/server.js +79 -6
  56. package/harness/packages/server/test/server.test.js +268 -1
  57. package/harness/tamnora.example.json +7 -1
  58. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,69 @@ 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.3.0] - 2026-10-02
9
+
10
+ ### Agregado
11
+
12
+ - Tool local `fetch_url` (http/https) para que el agente pueda leer docs, APIs y
13
+ archivos remotos; bloquea hosts privados/loopback y tiene timeout y tope de
14
+ tamano. Disponible en CLI/TUI/consola/Telegram y en modo plan.
15
+ - Importar **skills desde skills.sh**: `POST /api/skills/import` y la tool
16
+ `install_skill` aceptan una URL de skills.sh o un id `owner/repo/skill`, bajan
17
+ el `SKILL.md` (y archivos de apoyo) desde GitHub y lo instalan en la casa.
18
+ (La API de skills.sh pide token OIDC de Vercel; resolvemos por GitHub.)
19
+ - Importar **servidores MCP**: `POST /api/mcp/import` y la tool `install_mcp`
20
+ aceptan el nombre del registro oficial de MCP, un snippet JSON (bloque
21
+ `mcpServers`) o una URL del registro. `mcpservers.org` esta detras de
22
+ Cloudflare y no se puede leer desde el server; se puede pegar su JSON.
23
+ - Consola (ajustes): barras para importar skills y MCP, con estado y aviso de
24
+ variables de entorno requeridas.
25
+
26
+ ### Pendiente
27
+
28
+ - Confirmar el proveedor Xiaomi (`mimo-v2.6-flash`): respondio `429 Too Many
29
+ Requests` (cuota/rate limit del endpoint), no del harness.
30
+ - Telegram: audio y voz (transcribir notas de voz y responder con audio).
31
+ - Plugins: cargador de plugins locales (fase 2 del apartado ajustes).
32
+
33
+ ## [0.2.0] - 2026-10-02
34
+
35
+ ### Agregado
36
+
37
+ - Consola: eliminar sesiones (menu por sesion) y proyectos (borra las sesiones
38
+ de esa carpeta, nunca los archivos). `GET/DELETE /api/projects` y
39
+ `deleteSessionsByCwd`/`listProjects` en el Store.
40
+ - Atajo `Ctrl+.` para alternar **Plan / Build** en la consola y la TUI (el modo
41
+ `agent` pasa a mostrarse como "Build"); en la TUI tambien `/modo`.
42
+ - Apartado **ajustes** en la consola: alta/baja/edicion de **servidores MCP**
43
+ (se combinan con `mcp[]` de tamnora.json y se recargan al guardar) y de
44
+ **skills** (`<casa>/skills/<slug>/SKILL.md`), con habilitar/deshabilitar. Las
45
+ skills habilitadas se inyectan al system prompt.
46
+ - Tools de agente para el chat: `list_mcp`, `install_mcp`, `remove_mcp`,
47
+ `list_skills` e `install_skill` (contenido o URL). Instalar/eliminar piden
48
+ aprobacion (permiso `ask`); en modo plan no se exponen.
49
+ - **Preview** de la app en desarrollo y **procs**: deteccion de comandos de dev
50
+ (`package.json`, composer/artisan, PHP), arranque/parada de procesos con lista
51
+ blanca (`processes.allow`) y su log. Preview local (`127.0.0.1:<puerto>`) o
52
+ remoto con un **tunel** aparte (`POST /api/preview/tunnel`).
53
+ - **Telegram**: bot por long polling (`telegram.token`/`allowedChatIds` o
54
+ `TAMNORA_TELEGRAM_TOKEN`) que mapea cada chat a una sesion y reenvia al agente
55
+ con streaming; comandos `/new`, `/status`, `/model`, `/provider`, `/mode`,
56
+ `/stop`, `/help`.
57
+ - `startTunnel`/`parseTunnelmoleUrls` movidos al core (los usa el server y el CLI).
58
+
59
+ ### Arreglado
60
+
61
+ - La consola en Windows fuerza UTF-8 (`chcp 65001`) al arrancar, para que no se
62
+ pierdan acentos ni la ñ. Nuevo `document.md` con la referencia completa.
63
+ - Los proveedores custom ya se resuelven bien en la CLI, la TUI y la evaluacion:
64
+ antes `runTurn`/`judge`/`runDataset` no pasaban `customProviders`, el adapter
65
+ caia al de OpenRouter e ignoraba el `baseUrl` (un provider propio terminaba
66
+ pegandole a `openrouter.ai`).
67
+ - Los fallos de red al llamar a un proveedor muestran
68
+ `No se pudo conectar con <host>...` en vez del crudo `fetch failed` (el error
69
+ conserva el `code` para seguir siendo reintentable).
70
+
8
71
  ## [0.1.0] - 2026-10-01
9
72
 
10
73
  ### Agregado
@@ -14,6 +14,7 @@ import { updateCommand } from './commands/update.js';
14
14
  import { autostartCommand } from './commands/autostart.js';
15
15
  import { doctorCommand } from './commands/doctor.js';
16
16
  import { tuiCommand } from './commands/tui.js';
17
+ import { ensureUtf8Console } from './lib/util.js';
17
18
  import { color } from './ui/color.js';
18
19
 
19
20
  const COMMANDS = {
@@ -43,6 +44,7 @@ function readVersion() {
43
44
  }
44
45
 
45
46
  export async function runCli(argv) {
47
+ ensureUtf8Console();
46
48
  const [command, ...rest] = argv;
47
49
 
48
50
  if (command === '-h' || command === '--help' || command === 'help') {
@@ -62,6 +62,7 @@ export async function evalCommand(argv) {
62
62
  const providerId = resolved.providerId;
63
63
  const baseUrl = resolved.baseUrl;
64
64
  const apiKey = resolved.apiKey;
65
+ const customProviders = resolved.customProviders;
65
66
  const models = (values.model?.length ? values.model : [resolved.model]).filter(Boolean);
66
67
  if (!models.length) {
67
68
  console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
@@ -74,6 +75,7 @@ export async function evalCommand(argv) {
74
75
  model: values['judge-model'] || config.judgeModel,
75
76
  baseUrl,
76
77
  apiKey,
78
+ customProviders,
77
79
  });
78
80
 
79
81
  const enabledTools = values['no-tools']
@@ -104,6 +106,7 @@ export async function evalCommand(argv) {
104
106
  providerId,
105
107
  baseUrl,
106
108
  apiKey,
109
+ customProviders,
107
110
  systemPrompt,
108
111
  rootDir: cwd,
109
112
  trials,
@@ -34,6 +34,7 @@ export async function replCommand(argv) {
34
34
  let model = resolved.model;
35
35
  const baseUrl = resolved.baseUrl;
36
36
  const apiKey = resolved.apiKey;
37
+ const customProviders = resolved.customProviders;
37
38
  if (!model) {
38
39
  console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
39
40
  store.close();
@@ -140,6 +141,7 @@ export async function replCommand(argv) {
140
141
  model,
141
142
  baseUrl,
142
143
  apiKey,
144
+ customProviders,
143
145
  systemPrompt,
144
146
  history,
145
147
  input: text,
@@ -36,7 +36,7 @@ export async function runCommand(argv) {
36
36
  const config = await loadConfig(cwd);
37
37
  const instructions = await loadInstructions(cwd, config.instructions);
38
38
  const store = new Store(databasePath(cwd));
39
- const { providerId, model, baseUrl, apiKey } = resolveCliProvider(store, config, {
39
+ const { providerId, model, baseUrl, apiKey, customProviders } = resolveCliProvider(store, config, {
40
40
  provider: values.provider,
41
41
  model: values.model,
42
42
  });
@@ -101,6 +101,7 @@ export async function runCommand(argv) {
101
101
  model,
102
102
  baseUrl,
103
103
  apiKey,
104
+ customProviders,
104
105
  systemPrompt: buildSystemPrompt(instructions, values.system),
105
106
  history,
106
107
  input: prompt,
@@ -28,7 +28,7 @@ export async function tuiCommand(argv) {
28
28
  const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
29
29
  const config = await loadConfig(cwd);
30
30
  const store = new Store(databasePath(cwd));
31
- const { providerId, model, baseUrl, apiKey } = resolveCliProvider(store, config, {
31
+ const { providerId, model, baseUrl, apiKey, customProviders } = resolveCliProvider(store, config, {
32
32
  provider: values.provider,
33
33
  model: values.model,
34
34
  });
@@ -63,11 +63,14 @@ export async function tuiCommand(argv) {
63
63
  : undefined;
64
64
 
65
65
  const mode = resolveMode(values.mode || store.getSetting('activeMode') || 'agent');
66
- const enabled = mode.tools
67
- ? baseEnabled
68
- ? baseEnabled.filter((tool) => mode.tools.includes(tool))
69
- : mode.tools
70
- : baseEnabled;
66
+ const enabledFor = (m) =>
67
+ m.tools ? (baseEnabled ? baseEnabled.filter((tool) => m.tools.includes(tool)) : m.tools) : baseEnabled;
68
+ const promptFor = (m) =>
69
+ buildSystemPrompt(instructions, [values.system, m.systemPrompt].filter(Boolean).join('\n\n'));
70
+ const modeContext = (id) => {
71
+ const m = resolveMode(id);
72
+ return { label: m.label, enabled: enabledFor(m), systemPrompt: promptFor(m) };
73
+ };
71
74
 
72
75
  const sessionId = store.createSession({ title: 'Consola', provider: providerId, model, cwd });
73
76
 
@@ -76,11 +79,15 @@ export async function tuiCommand(argv) {
76
79
  model,
77
80
  baseUrl,
78
81
  apiKey,
79
- systemPrompt: buildSystemPrompt(instructions, [values.system, mode.systemPrompt].filter(Boolean).join('\n\n')),
82
+ customProviders,
83
+ modeId: mode.id,
84
+ modeContext,
85
+ onModeChange: (id) => store.setSetting('activeMode', id),
86
+ systemPrompt: promptFor(mode),
80
87
  maxRounds: num(values['max-steps'], config.maxRounds),
81
88
  toolOptions: {
82
89
  root: cwd,
83
- enabled,
90
+ enabled: enabledFor(mode),
84
91
  permissions: config.permissions,
85
92
  mcp,
86
93
  yes: Boolean(values.yes),
@@ -95,9 +102,9 @@ function printHelp() {
95
102
 
96
103
  Consola full-screen (estilo opencode/codex). Atajos:
97
104
  enter enviar ↑ / ↓ historial
98
- ← → mover cursor home/end inicio / fin
99
- pgup/pgdn scroll ctrl+l limpiar
100
- ctrl+c cancelar / salir /salir /limpiar
105
+ ← → mover cursor home/end inicio / fin
106
+ pgup/pgdn scroll ctrl+l limpiar
107
+ ctrl+. alternar Plan / Build /modo /salir /limpiar
101
108
 
102
109
  Opciones:
103
110
  --provider <id> Proveedor (por defecto: el activo)
@@ -22,6 +22,7 @@ export function resolveCliProvider(store, config, { provider, model } = {}) {
22
22
  model: activeModel,
23
23
  baseUrl: settings.baseUrl,
24
24
  apiKey: resolveApiKey(settings.apiKey, process.env),
25
+ customProviders,
25
26
  settings,
26
27
  };
27
28
  }
@@ -46,6 +47,7 @@ export async function runTurn({
46
47
  model,
47
48
  baseUrl,
48
49
  apiKey,
50
+ customProviders = [],
49
51
  systemPrompt,
50
52
  history = [],
51
53
  input,
@@ -57,7 +59,7 @@ export async function runTurn({
57
59
  toolContext = {},
58
60
  onEvent,
59
61
  }) {
60
- const { provider, config } = resolveProviderRuntime({ providerId, apiKey, baseUrl });
62
+ const { provider, config } = resolveProviderRuntime({ providerId, apiKey, baseUrl, customProviders });
61
63
 
62
64
  const messages = [
63
65
  { role: 'system', content: systemPrompt },
@@ -1,113 +1,2 @@
1
- import { spawn } from 'node:child_process';
2
-
3
- /**
4
- * Tuneles publicos. v1 soporta tunnelmole (gratis, sin cuenta; URL aleatoria)
5
- * via `npx --yes tunnelmole <puerto>`.
6
- */
7
-
8
- function killTree(child) {
9
- if (!child || child.killed) return;
10
- const pid = child.pid;
11
- try {
12
- if (process.platform === 'win32' && pid) {
13
- spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
14
- } else {
15
- if (pid) {
16
- try {
17
- process.kill(-pid, 'SIGTERM');
18
- } catch {
19
- /* sin grupo */
20
- }
21
- }
22
- try {
23
- child.kill('SIGTERM');
24
- } catch {
25
- /* ya murio */
26
- }
27
- }
28
- } catch {
29
- /* ya murio */
30
- }
31
- }
32
-
33
- export function parseTunnelmoleUrls(text) {
34
- const urls = [...new Set(String(text).match(/https?:\/\/[a-z0-9-]+\.tunnelmole\.net/gi) || [])];
35
- return {
36
- https: urls.find((u) => u.startsWith('https')) || '',
37
- http: urls.find((u) => u.startsWith('http:')) || '',
38
- };
39
- }
40
-
41
- function spawnTunnel(bin, args, opts = {}) {
42
- const log = opts.log || (() => {});
43
- const parse = opts.parse || (() => ({ https: '', http: '' }));
44
- const timeoutMs = opts.timeoutMs || 90000;
45
- const label = opts.label || bin;
46
- return new Promise((resolve, reject) => {
47
- let child;
48
- try {
49
- child = spawn(bin, args, {
50
- stdio: ['ignore', 'pipe', 'pipe'],
51
- windowsHide: true,
52
- shell: process.platform === 'win32',
53
- detached: process.platform !== 'win32',
54
- });
55
- } catch (err) {
56
- reject(new Error(`no se pudo iniciar ${label}: ${err.message}`));
57
- return;
58
- }
59
- let buf = '';
60
- let settled = false;
61
- const timer = setTimeout(() => {
62
- if (settled) return;
63
- settled = true;
64
- killTree(child);
65
- reject(new Error(`${label} no devolvio URL (${Math.round(timeoutMs / 1000)} s).`));
66
- }, timeoutMs);
67
- const feed = (data) => {
68
- buf += String(data);
69
- if (opts.verbose) process.stdout.write(String(data));
70
- const urls = parse(buf);
71
- const url = urls.https || urls.http;
72
- if (url) {
73
- if (settled) return;
74
- settled = true;
75
- clearTimeout(timer);
76
- log(`${label}: ${url} -> localhost:${opts.port}`);
77
- resolve({ url, provider: opts.provider, pid: child.pid, stop: () => killTree(child) });
78
- }
79
- };
80
- child.stdout.on('data', feed);
81
- child.stderr.on('data', feed);
82
- child.on('error', (err) => {
83
- if (settled) return;
84
- settled = true;
85
- clearTimeout(timer);
86
- reject(new Error(`${label}: ${err.message}`));
87
- });
88
- child.on('exit', (code) => {
89
- if (settled) return;
90
- settled = true;
91
- clearTimeout(timer);
92
- reject(new Error(`${label} termino (codigo ${code})${buf ? `: ${buf.slice(0, 200).trim()}` : ''}`));
93
- });
94
- });
95
- }
96
-
97
- export function startTunnelmole(port, opts = {}) {
98
- return spawnTunnel('npx', ['--yes', 'tunnelmole', String(port)], {
99
- ...opts,
100
- port,
101
- provider: 'tunnelmole',
102
- label: 'tunnelmole',
103
- parse: parseTunnelmoleUrls,
104
- });
105
- }
106
-
107
- /** Devuelve { url, provider, pid, stop }. provider 'none' no abre nada. */
108
- export async function startTunnel(port, provider = 'tunnelmole', opts = {}) {
109
- const p = String(provider || 'tunnelmole').toLowerCase();
110
- if (p === 'none' || p === 'off' || p === '') return { url: '', provider: 'none', pid: 0, stop: () => {} };
111
- if (p === 'tunnelmole' || p === 'tmole') return startTunnelmole(port, opts);
112
- throw new Error(`proveedor de tunel no soportado: ${provider}`);
113
- }
1
+ // Movido al core para que tambien lo use el server; se mantiene el import.
2
+ export * from '../../../core/src/tunnel.js';
@@ -22,3 +22,5 @@ export function num(value, fallback) {
22
22
  const n = Number(value);
23
23
  return Number.isFinite(n) ? n : fallback;
24
24
  }
25
+
26
+ export { ensureUtf8Console } from '../../../core/src/index.js';
@@ -12,12 +12,24 @@ export function runTui({
12
12
  model,
13
13
  baseUrl,
14
14
  apiKey,
15
+ customProviders = [],
16
+ modeId = 'agent',
17
+ modeContext,
18
+ onModeChange,
15
19
  systemPrompt,
16
20
  maxRounds,
17
21
  toolOptions,
18
22
  store,
19
23
  sessionId,
20
24
  }) {
25
+ let mode = modeId;
26
+ const readMode = (id) =>
27
+ modeContext
28
+ ? modeContext(id)
29
+ : { systemPrompt, enabled: toolOptions.enabled, label: id };
30
+ let activeMode = readMode(mode);
31
+ let modePrompt = activeMode.systemPrompt ?? systemPrompt;
32
+ let modeEnabled = activeMode.enabled;
21
33
  const state = {
22
34
  header: { provider: providerId, model, cwd: toolOptions.root },
23
35
  messages: [],
@@ -59,7 +71,23 @@ export function runTui({
59
71
  draw();
60
72
  });
61
73
 
62
- const { definitions, execute } = createTools({ ...toolOptions, onAsk });
74
+ const buildTools = () => createTools({ ...toolOptions, enabled: modeEnabled, onAsk });
75
+ let toolset = buildTools();
76
+
77
+ function setMode(id) {
78
+ mode = id;
79
+ activeMode = readMode(id);
80
+ modePrompt = activeMode.systemPrompt ?? systemPrompt;
81
+ modeEnabled = activeMode.enabled;
82
+ toolset = buildTools();
83
+ onModeChange?.(id);
84
+ }
85
+
86
+ function toggleMode() {
87
+ setMode(mode === 'plan' ? 'agent' : 'plan');
88
+ addMessage('assistant', `Modo ${activeMode.label ?? mode}${mode === 'plan' ? ' (solo lectura)' : ''}.`);
89
+ draw();
90
+ }
63
91
 
64
92
  function draw() {
65
93
  if (closed) return;
@@ -104,6 +132,15 @@ export function runTui({
104
132
  const text = state.input.trim();
105
133
  if (!text) return;
106
134
  if (text === '/salir' || text === '/exit' || text === '/quit') return shutdown(0);
135
+ if (text === '/modo' || text === '/mode') return toggleMode();
136
+ const modeArg = text.match(/^\/(?:modo|mode)\s+(plan|agent|build)$/i);
137
+ if (modeArg) {
138
+ const id = modeArg[1].toLowerCase() === 'build' ? 'agent' : modeArg[1].toLowerCase();
139
+ setMode(id);
140
+ addMessage('assistant', `Modo ${activeMode.label ?? id}.`);
141
+ draw();
142
+ return;
143
+ }
107
144
  if (text === '/limpiar' || text === '/clear') {
108
145
  state.input = '';
109
146
  state.messages = [];
@@ -128,13 +165,14 @@ export function runTui({
128
165
  model,
129
166
  baseUrl,
130
167
  apiKey,
131
- systemPrompt,
168
+ customProviders,
169
+ systemPrompt: modePrompt,
132
170
  history: conv,
133
171
  input: text,
134
172
  maxRounds,
135
173
  signal: controller.signal,
136
- tools: definitions,
137
- execute,
174
+ tools: toolset.definitions,
175
+ execute: toolset.execute,
138
176
  onEvent(event) {
139
177
  if (event.type === 'text') {
140
178
  sawText = true;
@@ -243,6 +281,9 @@ export function runTui({
243
281
  case 'clear':
244
282
  state.messages = [];
245
283
  break;
284
+ case 'toggle-mode':
285
+ toggleMode();
286
+ return;
246
287
  case 'enter':
247
288
  submit();
248
289
  return;
@@ -18,6 +18,9 @@ const ESCAPE_ACTIONS = {
18
18
  '\x1b[3~': 'delete',
19
19
  '\x1b[5~': 'pageup',
20
20
  '\x1b[6~': 'pagedown',
21
+ // Ctrl+. (Kitty keyboard protocol / xterm modifyOtherKeys)
22
+ '\x1b[46;5u': 'toggle-mode',
23
+ '\x1b[27;5;46~': 'toggle-mode',
21
24
  };
22
25
 
23
26
  const FINAL_BYTE = /[@-~]/;
@@ -30,6 +30,11 @@ test('parseKeys handles editing and navigation keys', () => {
30
30
  assert.deepEqual(parseKeys('\x05'), [{ type: 'end' }]);
31
31
  });
32
32
 
33
+ test('parseKeys maps Ctrl+. (Plan/Build) from kitty and xterm sequences', () => {
34
+ assert.deepEqual(parseKeys('\x1b[46;5u'), [{ type: 'toggle-mode' }]);
35
+ assert.deepEqual(parseKeys('\x1b[27;5;46~'), [{ type: 'toggle-mode' }]);
36
+ });
37
+
33
38
  test('parseKeys reads bracketed paste as a single action', () => {
34
39
  assert.deepEqual(parseKeys('\x1b[200~hola mundo\x1b[201~'), [{ type: 'paste', value: 'hola mundo' }]);
35
40
  assert.deepEqual(parseKeys('\x1b[200~a\nb\x1b[201~'), [{ type: 'paste', value: 'a\nb' }]);
@@ -4,7 +4,7 @@
4
4
  export const MODES = {
5
5
  agent: {
6
6
  id: 'agent',
7
- label: 'Agent',
7
+ label: 'Build',
8
8
  description: 'Lee, escribe y ejecuta comandos en el workspace.',
9
9
  tools: null,
10
10
  systemPrompt: '',
@@ -13,7 +13,7 @@ export const MODES = {
13
13
  id: 'plan',
14
14
  label: 'Plan',
15
15
  description: 'Solo lectura: investiga y propone un plan, sin modificar nada.',
16
- tools: ['read', 'list', 'glob', 'grep', 'git_status', 'git_diff', 'git_log'],
16
+ tools: ['read', 'list', 'glob', 'grep', 'fetch_url', 'git_status', 'git_diff', 'git_log'],
17
17
  systemPrompt:
18
18
  'Estás en modo PLAN: no modifiques archivos ni ejecutes comandos. Investigá el proyecto con las herramientas de solo lectura y respondé con un plan concreto y accionable.',
19
19
  },
@@ -12,6 +12,9 @@ export const DEFAULT_CONFIG = {
12
12
  permissions: {},
13
13
  mcp: [],
14
14
  server: { host: '127.0.0.1', port: 4517 },
15
+ processes: { enabled: true, allow: [] },
16
+ preview: { provider: 'tunnelmole' },
17
+ telegram: { token: '', allowedChatIds: [] },
15
18
  };
16
19
 
17
20
  /** Directory (relative to the project) where harness state lived historically. */
@@ -0,0 +1,18 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ let utf8Ready = false;
4
+
5
+ /**
6
+ * En Windows la consola suele arrancar en cp850/cp437 y los acentos y la ñ se
7
+ * pierden. Cambia el code page a UTF-8 (65001) una sola vez, solo en TTY.
8
+ */
9
+ export function ensureUtf8Console() {
10
+ if (utf8Ready || process.platform !== 'win32') return;
11
+ utf8Ready = true;
12
+ if (!process.stdout.isTTY) return;
13
+ try {
14
+ spawnSync('chcp.com', ['65001'], { stdio: 'ignore', windowsHide: true });
15
+ } catch {
16
+ /* no pasa nada si no esta disponible */
17
+ }
18
+ }
@@ -6,11 +6,11 @@ import { extractJson } from './scorers.js';
6
6
  * The judge model receives the criteria and the candidate output and must
7
7
  * reply with JSON: { "passed": boolean, "reason": string }.
8
8
  *
9
- * @param {{providerId?:string, model?:string, baseUrl?:string, apiKey?:string}} opts
9
+ * @param {{providerId?:string, model?:string, baseUrl?:string, apiKey?:string, customProviders?:object[]}} opts
10
10
  */
11
- export function createJudge({ providerId, model, baseUrl, apiKey } = {}) {
11
+ export function createJudge({ providerId, model, baseUrl, apiKey, customProviders = [] } = {}) {
12
12
  if (!model) return null;
13
- const { provider, config } = resolveProviderRuntime({ providerId, apiKey, baseUrl });
13
+ const { provider, config } = resolveProviderRuntime({ providerId, apiKey, baseUrl, customProviders });
14
14
 
15
15
  return async ({ criteria, output, input }) => {
16
16
  const prompt =
@@ -53,6 +53,7 @@ async function pool(items, limit, worker) {
53
53
  * @param {string} opts.providerId
54
54
  * @param {string} [opts.baseUrl]
55
55
  * @param {string} [opts.apiKey]
56
+ * @param {object[]} [opts.customProviders]
56
57
  * @param {string} [opts.systemPrompt]
57
58
  * @param {string} [opts.rootDir] - base dir for task fixtures
58
59
  * @param {number} [opts.trials]
@@ -72,6 +73,7 @@ export async function runDataset({
72
73
  providerId,
73
74
  baseUrl,
74
75
  apiKey,
76
+ customProviders = [],
75
77
  systemPrompt,
76
78
  rootDir = process.cwd(),
77
79
  trials = 1,
@@ -107,6 +109,7 @@ export async function runDataset({
107
109
  model,
108
110
  baseUrl: task.baseUrl ?? baseUrl,
109
111
  apiKey: task.apiKey ?? apiKey,
112
+ customProviders: task.customProviders ?? customProviders,
110
113
  systemPrompt: task.systemPrompt || systemPrompt,
111
114
  tools: tools.definitions.length ? tools.definitions : undefined,
112
115
  },
@@ -1,4 +1,5 @@
1
1
  export { getEnv, envValue } from './env.js';
2
+ export { ensureUtf8Console } from './console.js';
2
3
  export { MAX_TOOL_ROUNDS, runAgent, parseToolArguments } from './agent/runAgent.js';
3
4
  export { estimateTokens, trimContext } from './agent/context.js';
4
5
  export { diffLines } from './tools/diff.js';
@@ -70,3 +71,13 @@ export { summarize } from './eval/report.js';
70
71
  export { runScorers, validateSchema, extractJson, SCORERS } from './eval/scorers.js';
71
72
  export { createJudge } from './eval/judge.js';
72
73
  export { createTaskWorkspace, normalizeExpect } from './eval/task.js';
74
+ export { parseSkillMarkdown, loadSkills, mergeSkills, writeSkillFiles, buildSkillsPrompt } from './skills/index.js';
75
+ export { parseSkillRef, fetchSkillFromGithub } from './skills/remote.js';
76
+ export {
77
+ searchMcpRegistry,
78
+ getMcpRegistryServer,
79
+ registryServerToConfig,
80
+ normalizeMcpConfig,
81
+ } from './mcp/registry.js';
82
+ export { parseTunnelmoleUrls, startTunnelmole, startTunnel } from './tunnel.js';
83
+ export { detectProcs, packageManager, DEFAULT_PROCESS_ALLOW } from './procs/detect.js';