@danieltmn/tamnora 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +13 -25
- package/SECURITY.md +3 -4
- package/harness/packages/cli/src/cli.js +0 -3
- package/harness/packages/cli/src/commands/bridge.js +0 -1
- package/harness/packages/cli/src/commands/repl.js +20 -4
- package/harness/packages/cli/src/commands/run.js +6 -2
- package/harness/packages/cli/src/commands/server.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +6 -2
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/lib/util.js +2 -2
- package/harness/packages/cli/src/tui/app.js +24 -4
- package/harness/packages/cli/src/tui/layout.js +1 -1
- package/harness/packages/cli/test/tunnel.test.js +5 -32
- package/harness/packages/core/src/agent/runAgent.js +5 -2
- package/harness/packages/core/src/agents/index.js +113 -0
- package/harness/packages/core/src/config/app.js +0 -4
- package/harness/packages/core/src/config/home.js +1 -20
- package/harness/packages/core/src/config/index.js +6 -8
- package/harness/packages/core/src/extensions/load.js +22 -0
- package/harness/packages/core/src/frontmatter.js +32 -0
- package/harness/packages/core/src/hooks/index.js +2 -16
- package/harness/packages/core/src/index.js +12 -14
- package/harness/packages/core/src/plugins/index.js +2 -16
- package/harness/packages/core/src/procs/project.js +68 -0
- package/harness/packages/core/src/procs/static.js +17 -0
- package/harness/packages/core/src/providers/index.js +3 -1
- package/harness/packages/core/src/providers/ollama.js +4 -1
- package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
- package/harness/packages/core/src/providers/openrouter.js +5 -1
- package/harness/packages/core/src/providers/presets.js +4 -3
- package/harness/packages/core/src/providers/reasoning.js +58 -0
- package/harness/packages/core/src/rules/index.js +8 -27
- package/harness/packages/core/src/skills/index.js +8 -19
- package/harness/packages/core/src/storage/sqlite.js +595 -690
- package/harness/packages/core/src/tunnel.js +9 -147
- package/harness/packages/core/test/agents.test.js +111 -0
- package/harness/packages/core/test/home.test.js +2 -35
- package/harness/packages/core/test/procs.test.js +3 -64
- package/harness/packages/core/test/reasoning.test.js +43 -0
- package/harness/packages/core/test/storage.test.js +1 -12
- package/harness/packages/server/public/app.js +39 -19
- package/harness/packages/server/public/appearance.js +2 -56
- package/harness/packages/server/public/boot.js +2 -2
- package/harness/packages/server/public/cards.js +2 -21
- package/harness/packages/server/public/chat.js +1 -16
- package/harness/packages/server/public/chatFormat.js +0 -11
- package/harness/packages/server/public/chrome.js +95 -5
- package/harness/packages/server/public/commands.js +1 -2
- package/harness/packages/server/public/core.js +1 -1
- package/harness/packages/server/public/explorer.js +7 -10
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/public/index.html +1228 -1237
- package/harness/packages/server/public/landing/boot.js +2 -3
- package/harness/packages/server/public/landing/index.html +3 -367
- package/harness/packages/server/public/sessions.js +1 -0
- package/harness/packages/server/public/settings.js +1255 -1155
- package/harness/packages/server/public/settingsNav.js +1 -1
- package/harness/packages/server/public/styles.css +2341 -2867
- package/harness/packages/server/public/system.js +356 -495
- package/harness/packages/server/public/voice.js +6 -22
- package/harness/packages/server/src/auth.js +4 -15
- package/harness/packages/server/src/cli.js +1 -1
- package/harness/packages/server/src/lib/agents.js +60 -0
- package/harness/packages/server/src/lib/appearance.js +3 -22
- package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
- package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
- package/harness/packages/server/src/lib/definitions.js +40 -0
- package/harness/packages/server/src/lib/prompt.js +11 -3
- package/harness/packages/server/src/lib/scheduler.js +4 -0
- package/harness/packages/server/src/lib/sessionView.js +1 -0
- package/harness/packages/server/src/lib/skills.js +27 -21
- package/harness/packages/server/src/lib/tasks.js +71 -23
- package/harness/packages/server/src/lib/ttsSettings.js +2 -2
- package/harness/packages/server/src/lib/workspace.js +9 -11
- package/harness/packages/server/src/routes/agents.js +88 -0
- package/harness/packages/server/src/routes/chat.js +9 -44
- package/harness/packages/server/src/routes/preview.js +5 -22
- package/harness/packages/server/src/routes/procs.js +2 -4
- package/harness/packages/server/src/routes/providers.js +7 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/routes/tts.js +1 -1
- package/harness/packages/server/src/server.js +8 -5
- package/harness/packages/server/test/agents.test.js +194 -0
- package/harness/packages/server/test/appearance-route.test.js +3 -3
- package/harness/packages/server/test/appearance.test.js +12 -38
- package/harness/packages/server/test/chatFormat.test.js +0 -8
- package/harness/packages/server/test/consoleTabs.test.js +2 -2
- package/harness/packages/server/test/ops.test.js +8 -56
- package/harness/packages/server/test/preview.test.js +2 -42
- package/harness/packages/server/test/security.test.js +194 -370
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/packages/server/test/tts.test.js +4 -4
- package/harness/tamnora.example.json +2 -1
- package/package.json +2 -2
- package/harness/packages/cli/src/commands/eval.js +0 -186
- package/harness/packages/core/src/eval/dataset.js +0 -47
- package/harness/packages/core/src/eval/judge.js +0 -39
- package/harness/packages/core/src/eval/report.js +0 -48
- package/harness/packages/core/src/eval/runTask.js +0 -62
- package/harness/packages/core/src/eval/runner.js +0 -153
- package/harness/packages/core/src/eval/scorers.js +0 -217
- package/harness/packages/core/src/eval/task.js +0 -59
- package/harness/packages/core/src/procs/detect.js +0 -149
- package/harness/packages/core/test/eval.test.js +0 -144
- package/harness/packages/server/src/lib/advisor.js +0 -198
- package/harness/packages/server/src/routes/advisor.js +0 -79
- package/harness/packages/server/src/routes/eval.js +0 -100
- package/harness/packages/server/test/advisor.test.js +0 -249
- package/harness/scripts/example-dataset.jsonl +0 -4
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
import { bridgeStatePath } from './config/home.js';
|
|
4
|
-
import { pidAlive, killPidTree } from './pid.js';
|
|
2
|
+
import { killPidTree } from './pid.js';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
|
-
* Acceso remoto
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Acceso remoto por tailscale: VPN mesh privada + `tailscale serve`, que
|
|
6
|
+
* publica HTTPS dentro del tailnet reenviando a loopback. No hay proveedor
|
|
7
|
+
* publico: lo unico que sale de la maquina es lo que ya esta en la red privada
|
|
8
|
+
* del usuario.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
function killTree(child) {
|
|
@@ -14,147 +13,11 @@ function killTree(child) {
|
|
|
14
13
|
killPidTree(child.pid);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
* Estado del bridge (escrito por `tamnora bridge`), si el supervisor sigue
|
|
19
|
-
* vivo. Lo consulta el server para saber que hay un tunel publico activo y
|
|
20
|
-
* ajustar el modelo de confianza (guards locales, cookie Secure).
|
|
21
|
-
* Cache de 1 s + invalidacion por mtime del archivo: se lee en cada request
|
|
22
|
-
* sensible, pero un bridge.json nuevo (o borrado) se ve al instante.
|
|
23
|
-
*/
|
|
24
|
-
let tunnelCache = { key: null, mtime: null, at: 0, state: null };
|
|
25
|
-
export function tunnelState() {
|
|
26
|
-
let statePath;
|
|
27
|
-
try {
|
|
28
|
-
statePath = bridgeStatePath();
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
const now = Date.now();
|
|
33
|
-
let mtime = null;
|
|
34
|
-
try {
|
|
35
|
-
mtime = statSync(statePath).mtimeMs;
|
|
36
|
-
} catch {
|
|
37
|
-
tunnelCache = { key: statePath, mtime: null, at: now, state: null };
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
if (tunnelCache.key === statePath && tunnelCache.mtime === mtime && now - tunnelCache.at < 1000) {
|
|
41
|
-
return tunnelCache.state;
|
|
42
|
-
}
|
|
43
|
-
let state = null;
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
46
|
-
// Tailscale es una red privada que entra por loopback: no es un tunel
|
|
47
|
-
// publico, asi que no debe degradar los guards locales (`localOk`).
|
|
48
|
-
if (parsed?.provider !== 'tailscale' && parsed?.pid && pidAlive(parsed.pid)) state = parsed;
|
|
49
|
-
} catch {
|
|
50
|
-
/* sin bridge */
|
|
51
|
-
}
|
|
52
|
-
tunnelCache = { key: statePath, mtime, at: now, state };
|
|
53
|
-
return state;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** True si hay un tunel publico activo hacia este server. */
|
|
57
|
-
export function tunnelActive() {
|
|
58
|
-
return Boolean(tunnelState());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function parseTunnelmoleUrls(text) {
|
|
62
|
-
const urls = [...new Set(String(text).match(/https?:\/\/[a-z0-9-]+\.tunnelmole\.net/gi) || [])];
|
|
63
|
-
return {
|
|
64
|
-
https: urls.find((u) => u.startsWith('https')) || '',
|
|
65
|
-
http: urls.find((u) => u.startsWith('http:')) || '',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function spawnTunnel(bin, args, opts = {}) {
|
|
70
|
-
const log = opts.log || (() => {});
|
|
71
|
-
const parse = opts.parse || (() => ({ https: '', http: '' }));
|
|
72
|
-
const timeoutMs = opts.timeoutMs || 90000;
|
|
73
|
-
const label = opts.label || bin;
|
|
74
|
-
const useShell = process.platform === 'win32';
|
|
75
|
-
return new Promise((resolve, reject) => {
|
|
76
|
-
let child;
|
|
77
|
-
try {
|
|
78
|
-
// En Windows `npx` es un shim .cmd y exige shell; ahi se manda un unico
|
|
79
|
-
// comando (ver shellCommandLine): con array de args Node concatena sin
|
|
80
|
-
// escapar y emite el warning DEP0190.
|
|
81
|
-
const [cmd, cmdArgs] = useShell ? [shellCommandLine(bin, args), []] : [bin, args];
|
|
82
|
-
child = (opts.spawnImpl || spawn)(cmd, cmdArgs, {
|
|
83
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
-
windowsHide: true,
|
|
85
|
-
shell: useShell,
|
|
86
|
-
detached: !useShell,
|
|
87
|
-
});
|
|
88
|
-
} catch (err) {
|
|
89
|
-
reject(new Error(`no se pudo iniciar ${label}: ${err.message}`));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
let buf = '';
|
|
93
|
-
let settled = false;
|
|
94
|
-
const timer = setTimeout(() => {
|
|
95
|
-
if (settled) return;
|
|
96
|
-
settled = true;
|
|
97
|
-
killTree(child);
|
|
98
|
-
reject(new Error(`${label} no devolvio URL (${Math.round(timeoutMs / 1000)} s).`));
|
|
99
|
-
}, timeoutMs);
|
|
100
|
-
const feed = (data) => {
|
|
101
|
-
buf += String(data);
|
|
102
|
-
if (opts.verbose) process.stdout.write(String(data));
|
|
103
|
-
const urls = parse(buf);
|
|
104
|
-
const url = urls.https || urls.http;
|
|
105
|
-
if (url) {
|
|
106
|
-
if (settled) return;
|
|
107
|
-
settled = true;
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
log(`${label}: ${url} -> localhost:${opts.port}`);
|
|
110
|
-
resolve({ url, provider: opts.provider, pid: child.pid, child, stop: () => killTree(child) });
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
child.stdout.on('data', feed);
|
|
114
|
-
child.stderr.on('data', feed);
|
|
115
|
-
child.on('error', (err) => {
|
|
116
|
-
if (settled) return;
|
|
117
|
-
settled = true;
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
reject(new Error(`${label}: ${err.message}`));
|
|
120
|
-
});
|
|
121
|
-
child.on('exit', (code) => {
|
|
122
|
-
if (settled) return;
|
|
123
|
-
settled = true;
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
reject(new Error(`${label} termino (codigo ${code})${buf ? `: ${buf.slice(0, 200).trim()}` : ''}`));
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Tunel publico para el feature `preview` (no lo usa `bridge`).
|
|
131
|
-
export function startTunnelmole(port, opts = {}) {
|
|
132
|
-
return spawnTunnel('npx', ['--yes', 'tunnelmole', String(port)], {
|
|
133
|
-
...opts,
|
|
134
|
-
port,
|
|
135
|
-
provider: 'tunnelmole',
|
|
136
|
-
label: 'tunnelmole',
|
|
137
|
-
parse: parseTunnelmoleUrls,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
16
|
/**
|
|
142
17
|
* Tailscale: VPN mesh (WireGuard). `tailscale serve` publica HTTPS dentro del
|
|
143
|
-
* tailnet reenviando a loopback, asi que
|
|
144
|
-
*
|
|
18
|
+
* tailnet reenviando a loopback, asi que las peticiones siguen siendo "locales"
|
|
19
|
+
* para el server.
|
|
145
20
|
*/
|
|
146
|
-
// Con `shell: true` Node concatena los args sin escapar (warning DEP0190): se
|
|
147
|
-
// arma un unico comando con args validados contra metacaracteres del shell.
|
|
148
|
-
const SAFE_SHELL_ARG = /^[^&|<>^%!"\r\n]+$/;
|
|
149
|
-
|
|
150
|
-
function shellCommandLine(bin, args) {
|
|
151
|
-
for (const arg of args) {
|
|
152
|
-
if (!SAFE_SHELL_ARG.test(String(arg))) {
|
|
153
|
-
throw new Error(`argumento no permitido para ${bin}: "${arg}"`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return [bin, ...args].join(' ');
|
|
157
|
-
}
|
|
158
21
|
|
|
159
22
|
function runTailscale(args, opts = {}) {
|
|
160
23
|
const timeoutMs = opts.timeoutMs || 15000;
|
|
@@ -312,12 +175,11 @@ export function stopTailscaleServe(opts = {}) {
|
|
|
312
175
|
* `opts.preview` usa el `serve` de un puerto HTTPS propio (8443) para no pisar el
|
|
313
176
|
* bridge de la consola en 443.
|
|
314
177
|
*/
|
|
315
|
-
export async function startTunnel(port, provider = '
|
|
316
|
-
const p = String(provider || '
|
|
178
|
+
export async function startTunnel(port, provider = 'tailscale', opts = {}) {
|
|
179
|
+
const p = String(provider || 'tailscale').toLowerCase();
|
|
317
180
|
if (p === 'none' || p === 'off' || p === '') return { url: '', provider: 'none', pid: 0, stop: () => {} };
|
|
318
181
|
if (p === 'tailscale' || p === 'ts') {
|
|
319
182
|
return opts.preview ? startTailscalePreview(port, opts) : startTailscaleServe(port, opts);
|
|
320
183
|
}
|
|
321
|
-
if (p === 'tunnelmole' || p === 'tmole') return startTunnelmole(port, opts);
|
|
322
184
|
throw new Error(`proveedor de tunel no soportado: ${provider}`);
|
|
323
185
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
AGENT_FILE,
|
|
9
|
+
parseAgentMarkdown,
|
|
10
|
+
loadAgents,
|
|
11
|
+
mergeAgents,
|
|
12
|
+
buildAgentsIndex,
|
|
13
|
+
buildSubagentPrompt,
|
|
14
|
+
writeAgentFile,
|
|
15
|
+
} from '../src/agents/index.js';
|
|
16
|
+
|
|
17
|
+
test('parseAgentMarkdown reads frontmatter, lists and the body', () => {
|
|
18
|
+
const parsed = parseAgentMarkdown(
|
|
19
|
+
'---\nname: Revisor\ndescription: Busca bugs\nmodel: claude\nprovider: anthropic\nmode: plan\neffort: high\nmaxRounds: 4\ntools: read, grep\nskills: [code-review, tests]\n---\n\n# Rol\nMirá el diff.',
|
|
20
|
+
'revisor'
|
|
21
|
+
);
|
|
22
|
+
assert.equal(parsed.id, 'revisor');
|
|
23
|
+
assert.equal(parsed.name, 'Revisor');
|
|
24
|
+
assert.equal(parsed.description, 'Busca bugs');
|
|
25
|
+
assert.equal(parsed.model, 'claude');
|
|
26
|
+
assert.equal(parsed.provider, 'anthropic');
|
|
27
|
+
assert.equal(parsed.mode, 'plan');
|
|
28
|
+
assert.equal(parsed.effort, 'high');
|
|
29
|
+
assert.equal(parsed.maxRounds, 4);
|
|
30
|
+
assert.deepEqual(parsed.tools, ['read', 'grep']);
|
|
31
|
+
assert.deepEqual(parsed.skills, ['code-review', 'tests']);
|
|
32
|
+
assert.match(parsed.body, /Mirá el diff/);
|
|
33
|
+
|
|
34
|
+
const plain = parseAgentMarkdown('Solo cuerpo.', 'x');
|
|
35
|
+
assert.equal(plain.name, 'x');
|
|
36
|
+
assert.equal(plain.tools, null);
|
|
37
|
+
assert.equal(plain.skills, null);
|
|
38
|
+
assert.equal(plain.body, 'Solo cuerpo.');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('loadAgents scans directories and standalone .md files', async () => {
|
|
42
|
+
const root = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-'));
|
|
43
|
+
await mkdir(path.join(root, 'revisor'));
|
|
44
|
+
await writeFile(path.join(root, 'revisor', AGENT_FILE), '---\nname: Revisor\n---\nCuerpo.');
|
|
45
|
+
await writeFile(path.join(root, 'tests.md'), '---\nname: Tests\n---\nCuerpo tests.');
|
|
46
|
+
await writeFile(path.join(root, 'ignorado.txt'), 'no es un agente');
|
|
47
|
+
|
|
48
|
+
const agents = loadAgents(root);
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
agents.map((agent) => agent.id),
|
|
51
|
+
['revisor', 'tests']
|
|
52
|
+
);
|
|
53
|
+
assert.equal(agents.find((agent) => agent.id === 'revisor').name, 'Revisor');
|
|
54
|
+
assert.equal(loadAgents(path.join(root, 'nope')).length, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('mergeAgents lets the workspace override the casa agent', async () => {
|
|
58
|
+
const home = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-home-'));
|
|
59
|
+
const workspace = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-ws-'));
|
|
60
|
+
await mkdir(path.join(home, 'demo'));
|
|
61
|
+
await writeFile(path.join(home, 'demo', AGENT_FILE), '---\nname: Demo casa\n---\nCasa.');
|
|
62
|
+
await mkdir(path.join(workspace, 'demo'));
|
|
63
|
+
await writeFile(path.join(workspace, 'demo', AGENT_FILE), '---\nname: Demo workspace\n---\nWorkspace.');
|
|
64
|
+
|
|
65
|
+
const merged = mergeAgents([loadAgents(home), loadAgents(workspace)]);
|
|
66
|
+
assert.equal(merged.length, 1);
|
|
67
|
+
assert.equal(merged[0].name, 'Demo workspace');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('buildAgentsIndex catalogs subagents and buildSubagentPrompt includes instructions', () => {
|
|
71
|
+
assert.equal(buildAgentsIndex([]), '');
|
|
72
|
+
const index = buildAgentsIndex([
|
|
73
|
+
{ id: 'revisor', name: 'Revisor', description: 'Busca bugs' },
|
|
74
|
+
{ id: 'docs', name: 'Docs' },
|
|
75
|
+
]);
|
|
76
|
+
assert.match(index, /task\(\{ agent: "<id>"/);
|
|
77
|
+
assert.match(index, /revisor: Revisor — Busca bugs/);
|
|
78
|
+
assert.match(index, /docs: Docs/);
|
|
79
|
+
|
|
80
|
+
const prompt = buildSubagentPrompt({ id: 'revisor', name: 'Revisor', description: 'Busca bugs', body: 'Mirá el diff.' }, { basePrompt: 'BASE' });
|
|
81
|
+
assert.match(prompt, /BASE/);
|
|
82
|
+
assert.match(prompt, /subagente "Revisor"/);
|
|
83
|
+
assert.match(prompt, /Mirá el diff\./);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('writeAgentFile round-trips through parseAgentMarkdown', async () => {
|
|
87
|
+
const root = await mkdtemp(path.join(tmpdir(), 'tamnora-agents-write-'));
|
|
88
|
+
await writeAgentFile(path.join(root, 'revisor'), {
|
|
89
|
+
id: 'revisor',
|
|
90
|
+
name: 'Revisor',
|
|
91
|
+
description: 'Busca bugs',
|
|
92
|
+
model: 'gpt',
|
|
93
|
+
provider: 'openai',
|
|
94
|
+
mode: 'plan',
|
|
95
|
+
effort: 'low',
|
|
96
|
+
maxRounds: 3,
|
|
97
|
+
tools: ['read'],
|
|
98
|
+
skills: null,
|
|
99
|
+
body: '# Rol\nMirá el diff.',
|
|
100
|
+
});
|
|
101
|
+
const text = await readFile(path.join(root, 'revisor', AGENT_FILE), 'utf8');
|
|
102
|
+
const parsed = parseAgentMarkdown(text, 'revisor');
|
|
103
|
+
assert.equal(parsed.name, 'Revisor');
|
|
104
|
+
assert.equal(parsed.effort, 'low');
|
|
105
|
+
assert.equal(parsed.maxRounds, 3);
|
|
106
|
+
assert.deepEqual(parsed.tools, ['read']);
|
|
107
|
+
assert.equal(parsed.skills, null);
|
|
108
|
+
assert.match(parsed.body, /Mirá el diff/);
|
|
109
|
+
|
|
110
|
+
await assert.rejects(() => writeAgentFile(path.join(root, '..'), { id: '../escape', body: 'x' }), /inválido/);
|
|
111
|
+
});
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { test, afterEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtemp,
|
|
3
|
+
import { mkdtemp, readFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
7
|
-
setHome,
|
|
8
|
-
homeDir,
|
|
9
|
-
homePath,
|
|
10
|
-
databaseFile,
|
|
11
|
-
ensureApp,
|
|
12
|
-
readApp,
|
|
13
|
-
writeApp,
|
|
14
|
-
migrateLegacyDb,
|
|
15
|
-
} from '../src/index.js';
|
|
6
|
+
import { setHome, homeDir, homePath, ensureApp, readApp, writeApp } from '../src/index.js';
|
|
16
7
|
|
|
17
8
|
const prevHome = process.env.TAMNORA_HOME;
|
|
18
9
|
|
|
@@ -47,27 +38,3 @@ test('ensureApp creates app.json with a secret and roundtrips', async () => {
|
|
|
47
38
|
writeApp(raw);
|
|
48
39
|
assert.equal(readApp().port, 4599);
|
|
49
40
|
});
|
|
50
|
-
|
|
51
|
-
test('readApp migra los proveedores retirados (tunnelmole/relay) a tailscale', async () => {
|
|
52
|
-
const dir = await mkdtemp(path.join(tmpdir(), 'tamnora-home-'));
|
|
53
|
-
setHome(dir);
|
|
54
|
-
for (const provider of ['tunnelmole', 'relay']) {
|
|
55
|
-
const app = ensureApp();
|
|
56
|
-
app.tunnel = { ...app.tunnel, provider };
|
|
57
|
-
writeApp(app);
|
|
58
|
-
assert.equal(readApp().tunnel.provider, 'tailscale', provider);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('migrateLegacyDb copies the legacy project database to the global home', async () => {
|
|
63
|
-
const home = await mkdtemp(path.join(tmpdir(), 'tamnora-home-'));
|
|
64
|
-
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-cwd-'));
|
|
65
|
-
setHome(home);
|
|
66
|
-
|
|
67
|
-
await mkdir(path.join(cwd, '.tamnora'), { recursive: true });
|
|
68
|
-
await writeFile(path.join(cwd, '.tamnora', 'tamnora.db'), 'legacy-db');
|
|
69
|
-
|
|
70
|
-
const target = migrateLegacyDb(cwd);
|
|
71
|
-
assert.equal(target, databaseFile());
|
|
72
|
-
assert.equal(await readFile(target, 'utf8'), 'legacy-db');
|
|
73
|
-
});
|
|
@@ -5,7 +5,7 @@ import net from 'node:net';
|
|
|
5
5
|
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import {
|
|
8
|
+
import { loadProjectProcs, PROCS_FILE } from '../src/procs/project.js';
|
|
9
9
|
import { killPort, portInfo } from '../src/procs/kill.js';
|
|
10
10
|
|
|
11
11
|
async function waitForPort(port, attempts = 50) {
|
|
@@ -55,49 +55,9 @@ test('killPort terminates the process listening on a port', async (t) => {
|
|
|
55
55
|
assert.ok(child.exitCode !== null || child.signalCode, 'el hijo debería haber terminado');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test('
|
|
58
|
+
test('loadProjectProcs devuelve [] si el proyecto no declara procesos', async () => {
|
|
59
59
|
const dir = await mkdtemp(path.join(tmpdir(), 'tamnora-proc-'));
|
|
60
|
-
|
|
61
|
-
await writeFile(path.join(dir, 'pnpm-lock.yaml'), '');
|
|
62
|
-
assert.equal(packageManager(dir), 'pnpm');
|
|
63
|
-
|
|
64
|
-
const chips = detectProcs(dir);
|
|
65
|
-
const dev = chips.find((c) => c.command === 'pnpm' && c.args.includes('dev'));
|
|
66
|
-
assert.ok(dev);
|
|
67
|
-
assert.equal(dev.cwd, dir);
|
|
68
|
-
assert.ok(DEFAULT_PROCESS_ALLOW.includes('pnpm'));
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('detectProcs falls back to npm and detects php entrypoints', async () => {
|
|
72
|
-
const nodeDir = await mkdtemp(path.join(tmpdir(), 'tamnora-proc-'));
|
|
73
|
-
await writeFile(path.join(nodeDir, 'package.json'), JSON.stringify({ scripts: { start: 'node app.js' } }));
|
|
74
|
-
assert.equal(packageManager(nodeDir), 'npm');
|
|
75
|
-
assert.ok(detectProcs(nodeDir).some((c) => c.command === 'npm' && c.args.includes('start')));
|
|
76
|
-
|
|
77
|
-
const phpDir = await mkdtemp(path.join(tmpdir(), 'tamnora-proc-'));
|
|
78
|
-
await mkdir(path.join(phpDir, 'public'));
|
|
79
|
-
await writeFile(path.join(phpDir, 'public', 'index.php'), '<?php');
|
|
80
|
-
const chips = detectProcs(phpDir);
|
|
81
|
-
const php = chips.find((c) => c.command === 'php');
|
|
82
|
-
assert.ok(php);
|
|
83
|
-
assert.equal(php.args[0], '-S');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('detectProcs offers a static server for plain HTML sites', async () => {
|
|
87
|
-
const dir = await mkdtemp(path.join(tmpdir(), 'tamnora-proc-'));
|
|
88
|
-
await writeFile(path.join(dir, 'index.html'), '<h1>hola</h1>');
|
|
89
|
-
const chips = detectProcs(dir);
|
|
90
|
-
const staticChip = chips.find((c) => c.port && c.args.some((a) => a.endsWith('static-server.mjs')));
|
|
91
|
-
assert.ok(staticChip);
|
|
92
|
-
assert.equal(staticChip.cwd, dir);
|
|
93
|
-
assert.ok(staticChip.args.includes(dir));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('detectProcs does not add the static chip when there is a dev script', async () => {
|
|
97
|
-
const dir = await mkdtemp(path.join(tmpdir(), 'tamnora-proc-'));
|
|
98
|
-
await writeFile(path.join(dir, 'index.html'), '<h1>hola</h1>');
|
|
99
|
-
await writeFile(path.join(dir, 'package.json'), JSON.stringify({ scripts: { dev: 'vite' } }));
|
|
100
|
-
assert.ok(!detectProcs(dir).some((c) => c.args.some((a) => String(a).endsWith('static-server.mjs'))));
|
|
60
|
+
assert.deepEqual(loadProjectProcs(dir), []);
|
|
101
61
|
});
|
|
102
62
|
|
|
103
63
|
test('loadProjectProcs reads .tamnora/procs.json and confines dir to the root', async () => {
|
|
@@ -124,27 +84,6 @@ test('loadProjectProcs reads .tamnora/procs.json and confines dir to the root',
|
|
|
124
84
|
assert.ok(chips.every((c) => c.source === 'project'));
|
|
125
85
|
});
|
|
126
86
|
|
|
127
|
-
test('detectProcs puts project chips first and dedupes against auto-detection', async () => {
|
|
128
|
-
const dir = await mkdtemp(path.join(tmpdir(), 'tamnora-proc-'));
|
|
129
|
-
await mkdir(path.join(dir, '.tamnora'), { recursive: true });
|
|
130
|
-
await writeFile(path.join(dir, 'package.json'), JSON.stringify({ scripts: { dev: 'vite' } }));
|
|
131
|
-
await writeFile(
|
|
132
|
-
path.join(dir, PROCS_FILE),
|
|
133
|
-
JSON.stringify({
|
|
134
|
-
processes: [
|
|
135
|
-
{ label: 'backend', command: 'node', args: ['server.js'], port: 3000 },
|
|
136
|
-
{ label: 'dev repetido', command: 'npm', args: ['run', 'dev'] },
|
|
137
|
-
],
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const chips = detectProcs(dir);
|
|
142
|
-
assert.equal(chips[0].source, 'project');
|
|
143
|
-
assert.equal(chips[0].command, 'node');
|
|
144
|
-
const devChips = chips.filter((c) => c.command === 'npm' && c.args.join(' ') === 'run dev');
|
|
145
|
-
assert.equal(devChips.length, 1, 'el chip manual gana al autodetectado');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
87
|
test('portInfo reports whether a port is listening', async (t) => {
|
|
149
88
|
const free = await portInfo(47599);
|
|
150
89
|
assert.equal(free.listening, false);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { normalizeEffort, applyReasoningEffort, EFFORT_LEVELS } from '../src/providers/reasoning.js';
|
|
4
|
+
|
|
5
|
+
test('normalizeEffort accepts the common vocabulary and aliases', () => {
|
|
6
|
+
for (const level of EFFORT_LEVELS) assert.equal(normalizeEffort(level), level);
|
|
7
|
+
assert.equal(normalizeEffort(' HIGH '), 'high');
|
|
8
|
+
assert.equal(normalizeEffort('none'), 'off');
|
|
9
|
+
assert.equal(normalizeEffort('minimal'), 'low');
|
|
10
|
+
assert.equal(normalizeEffort('xhigh'), 'max');
|
|
11
|
+
assert.equal(normalizeEffort('ultra'), 'max');
|
|
12
|
+
assert.equal(normalizeEffort('auto'), '');
|
|
13
|
+
assert.equal(normalizeEffort(''), '');
|
|
14
|
+
assert.equal(normalizeEffort(null), '');
|
|
15
|
+
assert.equal(normalizeEffort('inventado'), '');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('applyReasoningEffort maps the level to each provider shape', () => {
|
|
19
|
+
const deepseek = applyReasoningEffort({}, 'high', { reasoning: 'deepseek' });
|
|
20
|
+
assert.equal(deepseek.reasoning_effort, 'high');
|
|
21
|
+
assert.deepEqual(deepseek.thinking, { type: 'enabled' });
|
|
22
|
+
|
|
23
|
+
const off = applyReasoningEffort({}, 'off', { reasoning: 'deepseek' });
|
|
24
|
+
assert.equal(off.reasoning_effort, 'none');
|
|
25
|
+
assert.deepEqual(off.thinking, { type: 'disabled' });
|
|
26
|
+
|
|
27
|
+
const openai = applyReasoningEffort({}, 'max', { reasoning: 'openai' });
|
|
28
|
+
assert.equal(openai.reasoning_effort, 'high', 'OpenAI no llega a max');
|
|
29
|
+
|
|
30
|
+
const openrouter = applyReasoningEffort({}, 'max', { reasoning: 'effort' });
|
|
31
|
+
assert.equal(openrouter.reasoning_effort, 'max');
|
|
32
|
+
|
|
33
|
+
const ollama = applyReasoningEffort({}, 'low', { reasoning: 'think' });
|
|
34
|
+
assert.equal(ollama.think, 'low');
|
|
35
|
+
const ollamaOff = applyReasoningEffort({}, 'off', { reasoning: 'think' });
|
|
36
|
+
assert.equal(ollamaOff.think, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('applyReasoningEffort leaves unsupported providers untouched', () => {
|
|
40
|
+
assert.deepEqual(applyReasoningEffort({ model: 'x' }, 'high', { reasoning: 'none' }), { model: 'x' });
|
|
41
|
+
assert.deepEqual(applyReasoningEffort({ model: 'x' }, 'high', {}), { model: 'x' });
|
|
42
|
+
assert.deepEqual(applyReasoningEffort({ model: 'x' }, '', { reasoning: 'effort' }), { model: 'x' });
|
|
43
|
+
});
|
|
@@ -2,7 +2,7 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { Store } from '../src/storage/sqlite.js';
|
|
4
4
|
|
|
5
|
-
test('Store persists sessions
|
|
5
|
+
test('Store persists sessions and messages', () => {
|
|
6
6
|
const store = new Store(':memory:');
|
|
7
7
|
|
|
8
8
|
const sessionId = store.createSession({ title: 'test', provider: 'openrouter', model: 'm' });
|
|
@@ -27,17 +27,6 @@ test('Store persists sessions, messages and runs', () => {
|
|
|
27
27
|
|
|
28
28
|
assert.equal(store.latestSession().id, sessionId);
|
|
29
29
|
|
|
30
|
-
const runId = store.createRun({ dataset: 'd.jsonl', config: { models: ['m'] } });
|
|
31
|
-
store.addRunResult({ runId, taskId: 't1', model: 'm', passed: true, output: 'ok' });
|
|
32
|
-
store.finishRun(runId, { summary: { total: 1 } });
|
|
33
|
-
|
|
34
|
-
const run = store.getRun(runId);
|
|
35
|
-
assert.equal(run.status, 'done');
|
|
36
|
-
assert.equal(run.summary.total, 1);
|
|
37
|
-
const results = store.getRunResults(runId);
|
|
38
|
-
assert.equal(results.length, 1);
|
|
39
|
-
assert.equal(results[0].passed, 1);
|
|
40
|
-
|
|
41
30
|
store.close();
|
|
42
31
|
});
|
|
43
32
|
|
|
@@ -26,12 +26,16 @@ import {
|
|
|
26
26
|
agentControlsSlot,
|
|
27
27
|
buildAppearanceControls,
|
|
28
28
|
closeCreateFolder,
|
|
29
|
+
closeEffortMenu,
|
|
29
30
|
closeFolderPicker,
|
|
30
31
|
closeModeMenu,
|
|
31
32
|
closeSettingsModal,
|
|
32
33
|
createFolder,
|
|
33
34
|
cwdPill,
|
|
34
35
|
cycleTheme,
|
|
36
|
+
effortMenu,
|
|
37
|
+
effortPill,
|
|
38
|
+
effortSelect,
|
|
35
39
|
filterFolders,
|
|
36
40
|
folderGoto,
|
|
37
41
|
folderList,
|
|
@@ -60,6 +64,7 @@ import {
|
|
|
60
64
|
providerSelect,
|
|
61
65
|
renderModePill,
|
|
62
66
|
setCwd,
|
|
67
|
+
setEffort,
|
|
63
68
|
setMode,
|
|
64
69
|
settingsModal,
|
|
65
70
|
statusbarEl,
|
|
@@ -87,7 +92,7 @@ import {
|
|
|
87
92
|
PREVIEW_DEVICE_KEY,
|
|
88
93
|
PREVIEW_URL_KEY,
|
|
89
94
|
applyTreeFilter,
|
|
90
|
-
|
|
95
|
+
loadProcChips,
|
|
91
96
|
explorerFilter,
|
|
92
97
|
explorerMode,
|
|
93
98
|
filePreviewState,
|
|
@@ -114,6 +119,9 @@ import { wireModelPicker } from './models.js';
|
|
|
114
119
|
import { restoreTabs, startNewSession } from './sessions.js';
|
|
115
120
|
import {
|
|
116
121
|
addPerfilFieldRow,
|
|
122
|
+
agentForm,
|
|
123
|
+
agentModal,
|
|
124
|
+
closeAgentForm,
|
|
117
125
|
closeMcpForm,
|
|
118
126
|
closeMemoryForm,
|
|
119
127
|
closeProviderForm,
|
|
@@ -133,6 +141,7 @@ import {
|
|
|
133
141
|
mcpPinnedEl,
|
|
134
142
|
memoryForm,
|
|
135
143
|
memoryModal,
|
|
144
|
+
openAgentForm,
|
|
136
145
|
openMcpForm,
|
|
137
146
|
openMemoryForm,
|
|
138
147
|
openProviderForm,
|
|
@@ -155,7 +164,7 @@ import {
|
|
|
155
164
|
setStatus,
|
|
156
165
|
skillForm,
|
|
157
166
|
skillModal,
|
|
158
|
-
|
|
167
|
+
submitAgent,
|
|
159
168
|
submitChannel,
|
|
160
169
|
submitMcp,
|
|
161
170
|
submitMemory,
|
|
@@ -164,7 +173,6 @@ import {
|
|
|
164
173
|
submitSchedule,
|
|
165
174
|
submitSkill,
|
|
166
175
|
syncMcpFormFields,
|
|
167
|
-
testAsesor,
|
|
168
176
|
testChannel,
|
|
169
177
|
testProvider,
|
|
170
178
|
vozPitchEl,
|
|
@@ -174,15 +182,11 @@ import {
|
|
|
174
182
|
import {
|
|
175
183
|
brandVerEl,
|
|
176
184
|
dismissUpdate,
|
|
177
|
-
evalRunBtn,
|
|
178
|
-
evalTasks,
|
|
179
185
|
followEvents,
|
|
180
186
|
forceUpdateCheck,
|
|
181
187
|
reconcile,
|
|
182
|
-
refreshRuns,
|
|
183
188
|
refreshUpdate,
|
|
184
189
|
renderAlertSignals,
|
|
185
|
-
runEval,
|
|
186
190
|
runUpdate,
|
|
187
191
|
watchLifecycle,
|
|
188
192
|
} from './system.js';
|
|
@@ -335,6 +339,25 @@ document.addEventListener('keydown', (event) => {
|
|
|
335
339
|
if (event.key === 'Escape') closeModeMenu();
|
|
336
340
|
});
|
|
337
341
|
modelPill?.addEventListener('click', () => openSettingsModal());
|
|
342
|
+
effortPill?.addEventListener('click', (event) => {
|
|
343
|
+
event.stopPropagation();
|
|
344
|
+
if (!effortMenu) return;
|
|
345
|
+
effortMenu.hidden = !effortMenu.hidden;
|
|
346
|
+
effortPill.setAttribute('aria-expanded', effortMenu.hidden ? 'false' : 'true');
|
|
347
|
+
});
|
|
348
|
+
effortMenu?.addEventListener('click', async (event) => {
|
|
349
|
+
const item = event.target.closest('.ctrl-menu-item');
|
|
350
|
+
if (!item) return;
|
|
351
|
+
closeEffortMenu();
|
|
352
|
+
await setEffort(item.dataset.effort || '');
|
|
353
|
+
});
|
|
354
|
+
effortSelect?.addEventListener('change', () => setEffort(effortSelect.value));
|
|
355
|
+
document.addEventListener('click', (event) => {
|
|
356
|
+
if (effortMenu && !effortMenu.hidden && !event.target.closest('.ctrl-wrap')) closeEffortMenu();
|
|
357
|
+
});
|
|
358
|
+
document.addEventListener('keydown', (event) => {
|
|
359
|
+
if (event.key === 'Escape') closeEffortMenu();
|
|
360
|
+
});
|
|
338
361
|
yesInput?.addEventListener('change', renderModePill);
|
|
339
362
|
strictInput?.addEventListener('change', () =>
|
|
340
363
|
api('/api/settings', { method: 'PUT', body: { strictScope: strictInput.checked } }).catch(() => null)
|
|
@@ -619,8 +642,6 @@ $('#voz-save')?.addEventListener('click', submitVoz);
|
|
|
619
642
|
$('#voz-test')?.addEventListener('click', testVoz);
|
|
620
643
|
vozRateEl?.addEventListener('input', renderVozSliders);
|
|
621
644
|
vozPitchEl?.addEventListener('input', renderVozSliders);
|
|
622
|
-
$('#asesor-save')?.addEventListener('click', submitAsesor);
|
|
623
|
-
$('#asesor-test')?.addEventListener('click', testAsesor);
|
|
624
645
|
$('#mcp-close').addEventListener('click', closeMcpForm);
|
|
625
646
|
$('#mf-cancel').addEventListener('click', closeMcpForm);
|
|
626
647
|
$('#mf-transport').addEventListener('change', syncMcpFormFields);
|
|
@@ -637,6 +658,14 @@ $('#asesor-test')?.addEventListener('click', testAsesor);
|
|
|
637
658
|
if (event.target === skillModal) closeSkillForm();
|
|
638
659
|
});
|
|
639
660
|
|
|
661
|
+
$('#agent-new')?.addEventListener('click', () => openAgentForm());
|
|
662
|
+
$('#agent-close')?.addEventListener('click', closeAgentForm);
|
|
663
|
+
$('#af-cancel')?.addEventListener('click', closeAgentForm);
|
|
664
|
+
agentForm?.addEventListener('submit', submitAgent);
|
|
665
|
+
agentModal?.addEventListener('click', (event) => {
|
|
666
|
+
if (event.target === agentModal) closeAgentForm();
|
|
667
|
+
});
|
|
668
|
+
|
|
640
669
|
$('#memory-new')?.addEventListener('click', () => openMemoryForm());
|
|
641
670
|
$('#memory-close')?.addEventListener('click', closeMemoryForm);
|
|
642
671
|
$('#mem-cancel')?.addEventListener('click', closeMemoryForm);
|
|
@@ -656,15 +685,6 @@ $('#asesor-test')?.addEventListener('click', testAsesor);
|
|
|
656
685
|
});
|
|
657
686
|
}
|
|
658
687
|
|
|
659
|
-
evalTasks.value = [
|
|
660
|
-
'{"id":"math","input":"¿Cuánto es 17 * 23? Respondé solo el número.","expect":{"equals":"391"}}',
|
|
661
|
-
'{"id":"regex","input":"Escribí una oración que contenga un número.","expect":{"regex":"\\\\d"}}',
|
|
662
|
-
].join('\n');
|
|
663
|
-
|
|
664
|
-
evalRunBtn.addEventListener('click', runEval);
|
|
665
|
-
|
|
666
|
-
$('#runs-refresh').addEventListener('click', refreshRuns);
|
|
667
|
-
|
|
668
688
|
$('#file-preview-toggle')?.addEventListener('click', async () => {
|
|
669
689
|
const toggle = $('#file-preview-toggle');
|
|
670
690
|
const code = $('#file-code');
|
|
@@ -759,7 +779,7 @@ $('#preview-device')?.addEventListener('change', (event) => storage.set(PREVIEW_
|
|
|
759
779
|
syncPreviewTunnel({ load: true });
|
|
760
780
|
})();
|
|
761
781
|
|
|
762
|
-
$('#procs-detect')?.addEventListener('click',
|
|
782
|
+
$('#procs-detect')?.addEventListener('click', loadProcChips);
|
|
763
783
|
|
|
764
784
|
gitFilterEl?.addEventListener('input', () => {
|
|
765
785
|
if (explorerMode === 'git' && gitEntries.length) renderGitList();
|