@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.
- package/CHANGELOG.md +63 -0
- package/harness/packages/cli/src/cli.js +2 -0
- package/harness/packages/cli/src/commands/eval.js +3 -0
- package/harness/packages/cli/src/commands/repl.js +2 -0
- package/harness/packages/cli/src/commands/run.js +2 -1
- package/harness/packages/cli/src/commands/tui.js +18 -11
- package/harness/packages/cli/src/lib/runtime.js +3 -1
- package/harness/packages/cli/src/lib/tunnel.js +2 -113
- package/harness/packages/cli/src/lib/util.js +2 -0
- package/harness/packages/cli/src/tui/app.js +45 -4
- package/harness/packages/cli/src/tui/keys.js +3 -0
- package/harness/packages/cli/test/tui.test.js +5 -0
- package/harness/packages/core/src/agent/modes.js +2 -2
- package/harness/packages/core/src/config/index.js +3 -0
- package/harness/packages/core/src/console.js +18 -0
- package/harness/packages/core/src/eval/judge.js +3 -3
- package/harness/packages/core/src/eval/runner.js +3 -0
- package/harness/packages/core/src/index.js +11 -0
- package/harness/packages/core/src/mcp/registry.js +117 -0
- package/harness/packages/core/src/procs/detect.js +69 -0
- package/harness/packages/core/src/providers/ollama.js +3 -3
- package/harness/packages/core/src/providers/openaiCompatible.js +3 -3
- package/harness/packages/core/src/providers/openrouter.js +2 -2
- package/harness/packages/core/src/providers/resilience.js +1 -1
- package/harness/packages/core/src/providers/utils.js +43 -0
- package/harness/packages/core/src/skills/index.js +90 -0
- package/harness/packages/core/src/skills/remote.js +80 -0
- package/harness/packages/core/src/storage/sqlite.js +21 -0
- package/harness/packages/core/src/tools/executors.js +55 -0
- package/harness/packages/core/src/tools/permissions.js +2 -0
- package/harness/packages/core/src/tools/schema.js +16 -0
- package/harness/packages/core/src/tunnel.js +113 -0
- package/harness/packages/core/test/config.test.js +15 -0
- package/harness/packages/core/test/mcp-registry.test.js +49 -0
- package/harness/packages/core/test/procs.test.js +34 -0
- package/harness/packages/core/test/providers.test.js +20 -0
- package/harness/packages/core/test/resilience.test.js +1 -0
- package/harness/packages/core/test/skills.test.js +69 -0
- package/harness/packages/core/test/tools.test.js +10 -0
- package/harness/packages/server/public/app.js +484 -6
- package/harness/packages/server/public/index.html +134 -6
- package/harness/packages/server/public/styles.css +45 -1
- package/harness/packages/server/src/cli.js +2 -0
- package/harness/packages/server/src/lib/agentTools.js +238 -0
- package/harness/packages/server/src/lib/procs.js +125 -0
- package/harness/packages/server/src/lib/prompt.js +6 -3
- package/harness/packages/server/src/lib/skills.js +69 -0
- package/harness/packages/server/src/lib/telegram.js +267 -0
- package/harness/packages/server/src/routes/chat.js +9 -1
- package/harness/packages/server/src/routes/mcp.js +128 -9
- package/harness/packages/server/src/routes/preview.js +58 -0
- package/harness/packages/server/src/routes/procs.js +47 -0
- package/harness/packages/server/src/routes/sessions.js +10 -0
- package/harness/packages/server/src/routes/skills.js +94 -0
- package/harness/packages/server/src/server.js +79 -6
- package/harness/packages/server/test/server.test.js +268 -1
- package/harness/tamnora.example.json +7 -1
- 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
|
|
67
|
-
? baseEnabled
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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';
|
|
@@ -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
|
|
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
|
-
|
|
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: '
|
|
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';
|