@danieltmn/tamnora 0.1.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 (123) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/SECURITY.md +31 -0
  5. package/harness/packages/cli/bin/tamnora.mjs +11 -0
  6. package/harness/packages/cli/package.json +10 -0
  7. package/harness/packages/cli/src/cli.js +90 -0
  8. package/harness/packages/cli/src/commands/autostart.js +121 -0
  9. package/harness/packages/cli/src/commands/bridge.js +274 -0
  10. package/harness/packages/cli/src/commands/doctor.js +73 -0
  11. package/harness/packages/cli/src/commands/eval.js +183 -0
  12. package/harness/packages/cli/src/commands/init.js +31 -0
  13. package/harness/packages/cli/src/commands/mcp.js +101 -0
  14. package/harness/packages/cli/src/commands/repl.js +172 -0
  15. package/harness/packages/cli/src/commands/run.js +165 -0
  16. package/harness/packages/cli/src/commands/serve.js +1 -0
  17. package/harness/packages/cli/src/commands/server.js +345 -0
  18. package/harness/packages/cli/src/commands/tui.js +110 -0
  19. package/harness/packages/cli/src/commands/update.js +76 -0
  20. package/harness/packages/cli/src/commands/user.js +135 -0
  21. package/harness/packages/cli/src/lib/core.js +2 -0
  22. package/harness/packages/cli/src/lib/daemon.js +153 -0
  23. package/harness/packages/cli/src/lib/qr.js +203 -0
  24. package/harness/packages/cli/src/lib/runtime.js +110 -0
  25. package/harness/packages/cli/src/lib/tunnel.js +113 -0
  26. package/harness/packages/cli/src/lib/util.js +24 -0
  27. package/harness/packages/cli/src/tui/app.js +302 -0
  28. package/harness/packages/cli/src/tui/keys.js +78 -0
  29. package/harness/packages/cli/src/tui/layout.js +130 -0
  30. package/harness/packages/cli/src/tui/screen.js +13 -0
  31. package/harness/packages/cli/src/ui/color.js +15 -0
  32. package/harness/packages/cli/test/release.test.js +34 -0
  33. package/harness/packages/cli/test/tui.test.js +99 -0
  34. package/harness/packages/cli/test/tunnel.test.js +20 -0
  35. package/harness/packages/core/package.json +14 -0
  36. package/harness/packages/core/src/agent/context.js +66 -0
  37. package/harness/packages/core/src/agent/modes.js +26 -0
  38. package/harness/packages/core/src/agent/runAgent.js +257 -0
  39. package/harness/packages/core/src/config/app.js +49 -0
  40. package/harness/packages/core/src/config/home.js +98 -0
  41. package/harness/packages/core/src/config/identity.js +41 -0
  42. package/harness/packages/core/src/config/index.js +69 -0
  43. package/harness/packages/core/src/config/providers.js +24 -0
  44. package/harness/packages/core/src/env.js +19 -0
  45. package/harness/packages/core/src/eval/dataset.js +47 -0
  46. package/harness/packages/core/src/eval/judge.js +39 -0
  47. package/harness/packages/core/src/eval/report.js +48 -0
  48. package/harness/packages/core/src/eval/runTask.js +71 -0
  49. package/harness/packages/core/src/eval/runner.js +150 -0
  50. package/harness/packages/core/src/eval/scorers.js +217 -0
  51. package/harness/packages/core/src/eval/task.js +59 -0
  52. package/harness/packages/core/src/index.js +72 -0
  53. package/harness/packages/core/src/mcp/common.js +82 -0
  54. package/harness/packages/core/src/mcp/http.js +149 -0
  55. package/harness/packages/core/src/mcp/index.js +167 -0
  56. package/harness/packages/core/src/mcp/stdio.js +218 -0
  57. package/harness/packages/core/src/providers/index.js +119 -0
  58. package/harness/packages/core/src/providers/ollama.js +76 -0
  59. package/harness/packages/core/src/providers/openaiCompatible.js +57 -0
  60. package/harness/packages/core/src/providers/openrouter.js +57 -0
  61. package/harness/packages/core/src/providers/presets.js +19 -0
  62. package/harness/packages/core/src/providers/resilience.js +74 -0
  63. package/harness/packages/core/src/providers/stream.js +200 -0
  64. package/harness/packages/core/src/providers/utils.js +66 -0
  65. package/harness/packages/core/src/storage/sqlite.js +453 -0
  66. package/harness/packages/core/src/tools/diff.js +90 -0
  67. package/harness/packages/core/src/tools/executors.js +277 -0
  68. package/harness/packages/core/src/tools/glob.js +67 -0
  69. package/harness/packages/core/src/tools/index.js +28 -0
  70. package/harness/packages/core/src/tools/paths.js +29 -0
  71. package/harness/packages/core/src/tools/permissions.js +69 -0
  72. package/harness/packages/core/src/tools/registry.js +44 -0
  73. package/harness/packages/core/src/tools/schema.js +177 -0
  74. package/harness/packages/core/test/agent.test.js +237 -0
  75. package/harness/packages/core/test/config.test.js +33 -0
  76. package/harness/packages/core/test/context.test.js +44 -0
  77. package/harness/packages/core/test/diff.test.js +20 -0
  78. package/harness/packages/core/test/eval.test.js +144 -0
  79. package/harness/packages/core/test/git-tools.test.js +44 -0
  80. package/harness/packages/core/test/home.test.js +62 -0
  81. package/harness/packages/core/test/mcp.test.js +158 -0
  82. package/harness/packages/core/test/providers.test.js +80 -0
  83. package/harness/packages/core/test/resilience.test.js +61 -0
  84. package/harness/packages/core/test/storage.test.js +55 -0
  85. package/harness/packages/core/test/stream.test.js +91 -0
  86. package/harness/packages/core/test/tools.test.js +96 -0
  87. package/harness/packages/server/bin/tamnora-server.mjs +11 -0
  88. package/harness/packages/server/package.json +10 -0
  89. package/harness/packages/server/public/app.js +2188 -0
  90. package/harness/packages/server/public/img/fondo-agentes.webp +0 -0
  91. package/harness/packages/server/public/index.html +368 -0
  92. package/harness/packages/server/public/landing/index.html +523 -0
  93. package/harness/packages/server/public/landing/landing.js +89 -0
  94. package/harness/packages/server/public/markdown.js +174 -0
  95. package/harness/packages/server/public/styles.css +923 -0
  96. package/harness/packages/server/src/auth.js +146 -0
  97. package/harness/packages/server/src/cli.js +61 -0
  98. package/harness/packages/server/src/index.js +1 -0
  99. package/harness/packages/server/src/lib/approvals.js +51 -0
  100. package/harness/packages/server/src/lib/core.js +2 -0
  101. package/harness/packages/server/src/lib/http.js +74 -0
  102. package/harness/packages/server/src/lib/modelCache.js +32 -0
  103. package/harness/packages/server/src/lib/prompt.js +48 -0
  104. package/harness/packages/server/src/lib/workspace.js +74 -0
  105. package/harness/packages/server/src/routes/auth.js +56 -0
  106. package/harness/packages/server/src/routes/chat.js +222 -0
  107. package/harness/packages/server/src/routes/eval.js +100 -0
  108. package/harness/packages/server/src/routes/files.js +117 -0
  109. package/harness/packages/server/src/routes/folders.js +80 -0
  110. package/harness/packages/server/src/routes/mcp.js +50 -0
  111. package/harness/packages/server/src/routes/models.js +26 -0
  112. package/harness/packages/server/src/routes/providers.js +135 -0
  113. package/harness/packages/server/src/routes/sessions.js +157 -0
  114. package/harness/packages/server/src/server.js +249 -0
  115. package/harness/packages/server/src/static.js +67 -0
  116. package/harness/packages/server/test/markdown.test.js +36 -0
  117. package/harness/packages/server/test/modelCache.test.js +72 -0
  118. package/harness/packages/server/test/server.test.js +809 -0
  119. package/harness/scripts/example-dataset.jsonl +4 -0
  120. package/harness/scripts/mcp-echo-server.mjs +108 -0
  121. package/harness/scripts/mock-provider.mjs +134 -0
  122. package/harness/tamnora.example.json +18 -0
  123. package/package.json +70 -0
@@ -0,0 +1,274 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFileSync, writeFileSync, unlinkSync, appendFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import {
5
+ setHome,
6
+ homeDir,
7
+ ensureHome,
8
+ ensureApp,
9
+ bridgeStatePath,
10
+ bridgeLogPath,
11
+ } from '../../../core/src/index.js';
12
+ import { parse, num } from '../lib/util.js';
13
+ import { killPid, pidAlive } from '../lib/daemon.js';
14
+ import { startTunnel } from '../lib/tunnel.js';
15
+ import { qrTerminal } from '../lib/qr.js';
16
+
17
+ const OPTIONS = {
18
+ start: { type: 'boolean' },
19
+ reload: { type: 'boolean' },
20
+ stop: { type: 'boolean' },
21
+ status: { type: 'boolean' },
22
+ foreground: { type: 'boolean' },
23
+ stream: { type: 'boolean' },
24
+ tunnel: { type: 'string' },
25
+ port: { type: 'string' },
26
+ home: { type: 'string' },
27
+ verbose: { type: 'boolean' },
28
+ help: { type: 'boolean', short: 'h' },
29
+ };
30
+
31
+ function readBridgeState() {
32
+ try {
33
+ return JSON.parse(readFileSync(bridgeStatePath(), 'utf8'));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function writeBridgeState(data) {
40
+ ensureHome();
41
+ writeFileSync(bridgeStatePath(), `${JSON.stringify(data, null, 2)}\n`);
42
+ }
43
+
44
+ function clearBridgeState() {
45
+ try {
46
+ unlinkSync(bridgeStatePath());
47
+ } catch {
48
+ /* nada */
49
+ }
50
+ }
51
+
52
+ export async function bridgeCommand(argv) {
53
+ const { values } = parse(argv, OPTIONS);
54
+ if (values.help) {
55
+ printHelp();
56
+ return 0;
57
+ }
58
+ if (values.home) {
59
+ setHome(values.home);
60
+ process.env.TAMNORA_HOME = path.resolve(values.home);
61
+ }
62
+
63
+ if (values.stop) return cmdStop();
64
+ if (values.status) return cmdStatus();
65
+
66
+ const isChild = Boolean(process.env.TAMNORA_BRIDGE_CHILD);
67
+ if (isChild || values.foreground || values.stream) return runForeground(values);
68
+ if (values.reload) return cmdReload(values);
69
+ return cmdStart(values);
70
+ }
71
+
72
+ function resolve(values) {
73
+ ensureHome();
74
+ const app = ensureApp();
75
+ return {
76
+ port: num(values.port, app.port || 4517),
77
+ provider: String(values.tunnel || app.tunnel?.provider || 'tunnelmole').toLowerCase(),
78
+ };
79
+ }
80
+
81
+ async function cmdStart(values) {
82
+ const { port, provider } = resolve(values);
83
+ if (provider === 'none') {
84
+ console.error('El tunel esta desactivado (provider "none"). Usa --tunnel tunnelmole.');
85
+ return 1;
86
+ }
87
+
88
+ const current = readBridgeState();
89
+ if (current && pidAlive(current.pid)) {
90
+ console.log(`Bridge ya esta corriendo (pid ${current.pid}).`);
91
+ printStatus(current);
92
+ return 0;
93
+ }
94
+
95
+ const bin = process.argv[1];
96
+ const args = [bin, 'bridge', '--foreground', '--port', String(port), '--tunnel', provider];
97
+ if (process.env.TAMNORA_HOME) args.push('--home', process.env.TAMNORA_HOME);
98
+ if (values.verbose) args.push('--verbose');
99
+ const child = spawn(process.execPath, args, {
100
+ detached: true,
101
+ stdio: 'ignore',
102
+ windowsHide: true,
103
+ env: { ...process.env, TAMNORA_BRIDGE_CHILD: '1' },
104
+ });
105
+ child.unref();
106
+ let dead = false;
107
+ child.on('exit', () => {
108
+ dead = true;
109
+ });
110
+ console.log(`Abriendo tunel (${provider}) en el puerto ${port}...`);
111
+
112
+ const state = await waitForBridge(child.pid, () => dead, 90000);
113
+ if (!state) {
114
+ const tail = tailBridgeLog(8);
115
+ if (tail) console.log(tail);
116
+ console.log('No pude abrir el tunel. Revisa: tamnora bridge --status');
117
+ return 1;
118
+ }
119
+ printStatus(state, true);
120
+ return 0;
121
+ }
122
+
123
+ async function runForeground(values) {
124
+ const { port, provider } = resolve(values);
125
+ const log = (line) => {
126
+ const text = `[${new Date().toISOString()}] ${line}`;
127
+ try {
128
+ ensureHome();
129
+ appendFileSync(bridgeLogPath(), `${text}\n`);
130
+ } catch {
131
+ /* nada */
132
+ }
133
+ console.log(text);
134
+ };
135
+
136
+ let tun;
137
+ try {
138
+ tun = await startTunnel(port, provider, { log, verbose: values.verbose });
139
+ } catch (err) {
140
+ log(`ERROR: ${err.message}`);
141
+ return 1;
142
+ }
143
+ if (!tun.url) {
144
+ log('ERROR: el tunel no devolvio URL.');
145
+ return 1;
146
+ }
147
+
148
+ writeBridgeState({
149
+ pid: process.pid,
150
+ url: tun.url,
151
+ provider: tun.provider,
152
+ port,
153
+ childPid: tun.pid,
154
+ startedAt: new Date().toISOString(),
155
+ });
156
+ printStatus(readBridgeState(), true);
157
+ log(`Tunel publico: ${tun.url}`);
158
+
159
+ return new Promise((resolve) => {
160
+ let closing = false;
161
+ const shutdown = () => {
162
+ if (closing) return;
163
+ closing = true;
164
+ try {
165
+ tun.stop();
166
+ } catch {
167
+ /* nada */
168
+ }
169
+ clearBridgeState();
170
+ resolve(0);
171
+ };
172
+ process.once('SIGINT', shutdown);
173
+ process.once('SIGTERM', shutdown);
174
+ });
175
+ }
176
+
177
+ async function cmdReload(values) {
178
+ await cmdStop();
179
+ const current = readBridgeState();
180
+ for (let i = 0; i < 25 && current && pidAlive(current.pid); i++) {
181
+ await new Promise((r) => setTimeout(r, 200));
182
+ }
183
+ return cmdStart(values);
184
+ }
185
+
186
+ async function cmdStop() {
187
+ const state = readBridgeState();
188
+ const stopped = [];
189
+ // Mata el supervisor y tambien el proceso del tunel (aunque el supervisor
190
+ // haya muerto y haya quedado huerfano).
191
+ if (state?.pid && pidAlive(state.pid)) {
192
+ killPid(state.pid);
193
+ stopped.push(`bridge ${state.pid}`);
194
+ }
195
+ if (state?.childPid && pidAlive(state.childPid)) {
196
+ killPid(state.childPid);
197
+ stopped.push(`tunel ${state.childPid}`);
198
+ }
199
+ clearBridgeState();
200
+ if (!stopped.length) {
201
+ console.log('El bridge no esta corriendo.');
202
+ return 0;
203
+ }
204
+ console.log(`Bridge detenido (${stopped.join(', ')}).`);
205
+ return 0;
206
+ }
207
+
208
+ async function cmdStatus() {
209
+ const state = readBridgeState();
210
+ if (!state || !pidAlive(state.pid)) {
211
+ console.log('Bridge detenido.');
212
+ return 0;
213
+ }
214
+ printStatus(state);
215
+ return 0;
216
+ }
217
+
218
+ function printStatus(state, withQr = false) {
219
+ console.log(`Bridge corriendo (pid ${state.pid})`);
220
+ console.log(` url : ${state.url}`);
221
+ console.log(` proveedor: ${state.provider} puerto local: ${state.port}`);
222
+ console.log(` casa : ${homeDir()}`);
223
+ if (withQr && state.url) {
224
+ const art = qrTerminal(state.url, 2);
225
+ if (art) {
226
+ console.log('');
227
+ console.log(art);
228
+ console.log(` escanea para abrir ${state.url}`);
229
+ }
230
+ }
231
+ }
232
+
233
+ function tailBridgeLog(n) {
234
+ try {
235
+ const lines = readFileSync(bridgeLogPath(), 'utf8')
236
+ .split(/\r?\n/)
237
+ .filter((l) => l.trim() !== '');
238
+ return lines.slice(-n).join('\n');
239
+ } catch {
240
+ return '';
241
+ }
242
+ }
243
+
244
+ function waitForBridge(childPid, isDead, timeoutMs) {
245
+ return new Promise((resolve) => {
246
+ const start = Date.now();
247
+ const tick = () => {
248
+ if (isDead()) return resolve(null);
249
+ const state = readBridgeState();
250
+ if (state && state.pid && pidAlive(state.pid) && (!childPid || state.pid === childPid)) return resolve(state);
251
+ if (Date.now() - start > timeoutMs) return resolve(null);
252
+ setTimeout(tick, 500);
253
+ };
254
+ tick();
255
+ });
256
+ }
257
+
258
+ function printHelp() {
259
+ console.log(`Uso: tamnora bridge [accion] [opciones]
260
+
261
+ Abre un tunel publico (tunnelmole) hacia el server local para trabajar remoto.
262
+
263
+ Acciones:
264
+ --start Abre el tunel en segundo plano + QR (default)
265
+ --reload Reinicia el tunel
266
+ --stop Cierra el tunel
267
+ --status Estado y URL publica
268
+ --foreground Corre en primer plano (debug)
269
+
270
+ Opciones:
271
+ --tunnel <prov> Proveedor (default: el de app.json; hoy: tunnelmole)
272
+ --port <n> Puerto local a exponer (default: el del server)
273
+ --home <path> Casa de Tamnora (default: TAMNORA_HOME o datos del sistema)`);
274
+ }
@@ -0,0 +1,73 @@
1
+ import { existsSync } from 'node:fs';
2
+ import net from 'node:net';
3
+ import {
4
+ homeDir,
5
+ homeExists,
6
+ readApp,
7
+ databasePath,
8
+ serverLogPath,
9
+ Store,
10
+ } from '../../../core/src/index.js';
11
+ import { parse } from '../lib/util.js';
12
+ import { pidAlive, readRuntime } from '../lib/daemon.js';
13
+
14
+ const OPTIONS = { home: { type: 'string' }, help: { type: 'boolean', short: 'h' } };
15
+
16
+ export async function doctorCommand(argv) {
17
+ const { values } = parse(argv, OPTIONS);
18
+ if (values.help) {
19
+ console.log('Uso: tamnora doctor [--home <path>]\n\nVerifica Node, la casa, el puerto y la base.');
20
+ return 0;
21
+ }
22
+
23
+ const [maj, min] = process.versions.node.split('.').map((n) => parseInt(n, 10));
24
+ const nodeOk = maj > 22 || (maj === 22 && min >= 5);
25
+ const checks = [
26
+ [`Node ${process.versions.node}`, nodeOk, nodeOk ? '' : 'requiere Node >= 22.5 (node:sqlite)'],
27
+ ];
28
+
29
+ const cfgOk = homeExists();
30
+ checks.push([`casa (${homeDir()})`, cfgOk, cfgOk ? '' : 'corre: tamnora server --init']);
31
+
32
+ const app = readApp();
33
+ const rt = readRuntime();
34
+ const serverLive = Boolean(rt && pidAlive(rt.pid));
35
+ const port = serverLive && rt.port ? rt.port : app.port || 4517;
36
+ const host = serverLive && rt.host ? rt.host : app.host || '127.0.0.1';
37
+ const portFree = await isPortFree(port, host);
38
+ checks.push([`puerto ${port}`, portFree || serverLive, portFree ? '' : 'ocupado (¿ya corre Tamnora?)']);
39
+
40
+ const db = databasePath();
41
+ const dbOk = existsSync(db);
42
+ let provider = '';
43
+ if (dbOk) {
44
+ try {
45
+ const store = new Store(db);
46
+ provider = store.getSetting('activeProvider') || '';
47
+ store.close();
48
+ } catch {
49
+ /* base ilegible */
50
+ }
51
+ }
52
+ checks.push(['base de datos', dbOk, dbOk ? '' : 'se creara al primer uso']);
53
+ checks.push(['proveedor activo', Boolean(provider), provider || 'elegi uno en la consola (npm: tamnora serve)']);
54
+
55
+ if (serverLive) checks.push([`server (pid ${rt.pid})`, true, rt.url || '']);
56
+ checks.push([`logs`, true, serverLogPath()]);
57
+
58
+ let ok = true;
59
+ for (const [name, pass, hint] of checks) {
60
+ console.log(`${pass ? ' OK ' : ' FALTA'} ${name}${hint ? ` -> ${hint}` : ''}`);
61
+ if (!pass && !name.startsWith('proveedor')) ok = false;
62
+ }
63
+ return ok ? 0 : 1;
64
+ }
65
+
66
+ function isPortFree(port, host) {
67
+ return new Promise((resolve) => {
68
+ const server = net.createServer();
69
+ server.once('error', () => resolve(false));
70
+ server.once('listening', () => server.close(() => resolve(true)));
71
+ server.listen(port, host);
72
+ });
73
+ }
@@ -0,0 +1,183 @@
1
+ import path from 'node:path';
2
+ import {
3
+ loadConfig,
4
+ loadInstructions,
5
+ loadDataset,
6
+ runDataset,
7
+ summarize,
8
+ createJudge,
9
+ Store,
10
+ } from '../lib/core.js';
11
+ import { parse, databasePath, num } from '../lib/util.js';
12
+ import { buildSystemPrompt, resolveCliProvider } from '../lib/runtime.js';
13
+ import { color } from '../ui/color.js';
14
+
15
+ const OPTIONS = {
16
+ provider: { type: 'string' },
17
+ model: { type: 'string', multiple: true },
18
+ dir: { type: 'string' },
19
+ trials: { type: 'string' },
20
+ concurrency: { type: 'string' },
21
+ tools: { type: 'string' },
22
+ 'no-tools': { type: 'boolean' },
23
+ 'judge-model': { type: 'string' },
24
+ yes: { type: 'boolean', short: 'y' },
25
+ json: { type: 'boolean' },
26
+ help: { type: 'boolean', short: 'h' },
27
+ };
28
+
29
+ function pct(value) {
30
+ return `${Math.round(value * 100)}%`;
31
+ }
32
+
33
+ export async function evalCommand(argv) {
34
+ const { values, positionals } = parse(argv, OPTIONS);
35
+ if (values.help) {
36
+ printHelp();
37
+ return 0;
38
+ }
39
+
40
+ const datasetFile = positionals[0];
41
+ if (!datasetFile) {
42
+ console.error(color.red('Falta el dataset. Uso: tamnora eval <dataset.jsonl> [--model <id>]'));
43
+ return 1;
44
+ }
45
+
46
+ const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
47
+ const config = await loadConfig(cwd);
48
+
49
+ const tasks = await loadDataset(path.resolve(cwd, datasetFile));
50
+ if (!tasks.length) {
51
+ console.error(color.red('El dataset está vacío.'));
52
+ return 1;
53
+ }
54
+
55
+ const instructions = await loadInstructions(cwd, config.instructions);
56
+ const systemPrompt = buildSystemPrompt(instructions);
57
+ const trials = num(values.trials, 1);
58
+ const concurrency = num(values.concurrency, 1);
59
+
60
+ const store = new Store(databasePath(cwd));
61
+ const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
62
+ const providerId = resolved.providerId;
63
+ const baseUrl = resolved.baseUrl;
64
+ const apiKey = resolved.apiKey;
65
+ const models = (values.model?.length ? values.model : [resolved.model]).filter(Boolean);
66
+ if (!models.length) {
67
+ console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
68
+ store.close();
69
+ return 1;
70
+ }
71
+
72
+ const judge = createJudge({
73
+ providerId,
74
+ model: values['judge-model'] || config.judgeModel,
75
+ baseUrl,
76
+ apiKey,
77
+ });
78
+
79
+ const enabledTools = values['no-tools']
80
+ ? []
81
+ : values.tools
82
+ ? String(values.tools).split(',').map((s) => s.trim()).filter(Boolean)
83
+ : Array.isArray(config.tools) && config.tools.length
84
+ ? config.tools
85
+ : undefined;
86
+
87
+ const runId = store.createRun({
88
+ dataset: datasetFile,
89
+ config: { providerId, models, trials, concurrency, tools: enabledTools ?? 'all' },
90
+ });
91
+ const startedAt = Date.now();
92
+
93
+ if (!values.json) {
94
+ process.stderr.write(
95
+ color.dim(
96
+ `run ${runId} · ${tasks.length} tareas × ${models.length} modelos × ${trials} trials · concurrencia ${concurrency}\n`
97
+ )
98
+ );
99
+ }
100
+
101
+ const results = await runDataset({
102
+ tasks,
103
+ models,
104
+ providerId,
105
+ baseUrl,
106
+ apiKey,
107
+ systemPrompt,
108
+ rootDir: cwd,
109
+ trials,
110
+ concurrency,
111
+ enabledTools,
112
+ permissions: config.permissions,
113
+ yes: Boolean(values.yes),
114
+ judge,
115
+ store,
116
+ runId,
117
+ onProgress(entry) {
118
+ if (values.json) return;
119
+ const mark = entry.error ? color.red('✗') : entry.passed ? color.green('✓') : color.yellow('·');
120
+ const scorers = (entry.trace?.scorers ?? []).map((s) => `${s.scorer}:${s.passed ? 'ok' : 'no'}`).join(' ');
121
+ const suffix = entry.error ? ` — ${entry.error}` : scorers ? ` [${scorers}]` : '';
122
+ process.stdout.write(`${mark} ${entry.taskId} · ${entry.model}${suffix}\n`);
123
+ },
124
+ });
125
+
126
+ const summary = summarize(results);
127
+ summary.durationMs = Date.now() - startedAt;
128
+ const status = summary.errors && summary.passed === 0 ? 'failed' : summary.errors ? 'done_with_errors' : 'done';
129
+ store.finishRun(runId, { status, summary });
130
+ store.close();
131
+
132
+ if (values.json) {
133
+ process.stdout.write(`${JSON.stringify({ runId, summary }, null, 2)}\n`);
134
+ } else {
135
+ printReport(summary, runId);
136
+ }
137
+
138
+ return summary.passed === 0 && summary.errors > 0 ? 1 : summary.passed < summary.total ? 1 : 0;
139
+ }
140
+
141
+ function printReport(summary, runId) {
142
+ const lines = [];
143
+ lines.push('');
144
+ lines.push(color.bold(`Resultados (run ${runId})`));
145
+ lines.push(color.dim('modelo'.padEnd(34) + 'pasó total pass errores'));
146
+ for (const m of summary.models) {
147
+ lines.push(
148
+ `${String(m.model).slice(0, 33).padEnd(34)}${String(m.passed).padStart(4)}${String(m.total).padStart(7)}${pct(m.passRate).padStart(6)}${String(m.errors).padStart(9)}`
149
+ );
150
+ }
151
+ if (summary.scorers.length) {
152
+ lines.push('');
153
+ lines.push(color.dim('scorer'.padEnd(34) + 'pasó total pass'));
154
+ for (const s of summary.scorers) {
155
+ lines.push(
156
+ `${String(s.scorer).slice(0, 33).padEnd(34)}${String(s.passed).padStart(4)}${String(s.total).padStart(7)}${pct(s.passRate).padStart(6)}`
157
+ );
158
+ }
159
+ }
160
+ lines.push('');
161
+ lines.push(
162
+ color.dim(
163
+ `total ${summary.total} · ${summary.passed} ok · ${summary.errors} errores · ${(summary.durationMs / 1000).toFixed(1)}s`
164
+ )
165
+ );
166
+ process.stderr.write(`${lines.join('\n')}\n`);
167
+ }
168
+
169
+ function printHelp() {
170
+ console.log(`Uso: tamnora eval <dataset.jsonl> [opciones]
171
+
172
+ Opciones:
173
+ --provider <id> Proveedor
174
+ --model <id> Modelo (repetible para comparar varios)
175
+ --trials <n> Repeticiones por tarea/modelo (default: 1)
176
+ --concurrency <n> Tareas en paralelo (default: 1)
177
+ --judge-model <id> Modelo para el scorer llmJudge
178
+ --dir <path> Directorio del proyecto
179
+ --tools <lista> Tools habilitadas (coma)
180
+ --no-tools Deshabilitar tools
181
+ -y, --yes Aprobar automáticamente permisos "ask"
182
+ --json Salida JSON`);
183
+ }
@@ -0,0 +1,31 @@
1
+ import { writeFile, access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { DEFAULT_CONFIG } from '../lib/core.js';
4
+ import { parse } from '../lib/util.js';
5
+ import { color } from '../ui/color.js';
6
+
7
+ const OPTIONS = {
8
+ dir: { type: 'string' },
9
+ help: { type: 'boolean', short: 'h' },
10
+ };
11
+
12
+ export async function initCommand(argv) {
13
+ const { values } = parse(argv, OPTIONS);
14
+ if (values.help) {
15
+ console.log('Uso: tamnora init [--dir <path>]');
16
+ return 0;
17
+ }
18
+
19
+ const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
20
+ const file = path.join(cwd, 'tamnora.json');
21
+ try {
22
+ await access(file);
23
+ console.log(color.yellow(`Ya existe ${file}`));
24
+ return 0;
25
+ } catch {
26
+ // create it
27
+ }
28
+ await writeFile(file, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, 'utf8');
29
+ console.log(color.green(`Creado ${file}`));
30
+ return 0;
31
+ }
@@ -0,0 +1,101 @@
1
+ import path from 'node:path';
2
+ import { loadConfig, buildMcpRegistry, mcpTransport } from '../lib/core.js';
3
+ import { parse } from '../lib/util.js';
4
+ import { color } from '../ui/color.js';
5
+
6
+ const OPTIONS = {
7
+ dir: { type: 'string' },
8
+ json: { type: 'boolean' },
9
+ help: { type: 'boolean', short: 'h' },
10
+ };
11
+
12
+ export async function mcpCommand(argv) {
13
+ const { values } = parse(argv, OPTIONS);
14
+ if (values.help) {
15
+ printHelp();
16
+ return 0;
17
+ }
18
+
19
+ const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
20
+ const config = await loadConfig(cwd);
21
+ const servers = Array.isArray(config.mcp) ? config.mcp : [];
22
+
23
+ if (!servers.length) {
24
+ if (values.json) process.stdout.write(`${JSON.stringify({ servers: [] }, null, 2)}\n`);
25
+ else process.stdout.write(color.dim('No hay servidores MCP configurados en tamnora.json.\n'));
26
+ return 0;
27
+ }
28
+
29
+ const registry = await buildMcpRegistry(servers);
30
+
31
+ if (values.json) {
32
+ process.stdout.write(
33
+ `${JSON.stringify(
34
+ {
35
+ servers: servers.map((server) => {
36
+ const session = registry.sessions.find((s) => s.server.id === server.id);
37
+ return {
38
+ id: server.id,
39
+ name: server.name,
40
+ transport: safeTransport(server),
41
+ tools: (session?.tools ?? []).map((t) => t.name),
42
+ resources: (session?.resources ?? []).map((r) => r.uri),
43
+ prompts: (session?.prompts ?? []).map((p) => p.name),
44
+ error: registry.errors.find((e) => e.server.id === server.id)?.error,
45
+ };
46
+ }),
47
+ },
48
+ null,
49
+ 2
50
+ )}\n`
51
+ );
52
+ registry.close();
53
+ return 0;
54
+ }
55
+
56
+ let failed = false;
57
+ for (const server of servers) {
58
+ const transport = safeTransport(server);
59
+ const session = registry.sessions.find((s) => s.server.id === server.id);
60
+ const error = registry.errors.find((e) => e.server.id === server.id)?.error;
61
+ const label = `${server.id}${server.name ? ` (${server.name})` : ''}`;
62
+
63
+ if (error) {
64
+ failed = true;
65
+ process.stdout.write(`${color.red('✗')} ${label} ${color.dim(`[${transport}]`)}\n`);
66
+ process.stdout.write(` ${color.red(error)}\n`);
67
+ continue;
68
+ }
69
+ process.stdout.write(`${color.green('✓')} ${label} ${color.dim(`[${transport}]`)}\n`);
70
+ for (const tool of session?.tools ?? []) {
71
+ process.stdout.write(` ${color.cyan(tool.name)}${tool.description ? color.dim(` — ${tool.description}`) : ''}\n`);
72
+ }
73
+ for (const resource of session?.resources ?? []) {
74
+ process.stdout.write(` ${color.dim('resource')} ${resource.uri}${resource.name ? color.dim(` (${resource.name})`) : ''}\n`);
75
+ }
76
+ for (const prompt of session?.prompts ?? []) {
77
+ process.stdout.write(` ${color.dim('prompt')} ${prompt.name}\n`);
78
+ }
79
+ }
80
+
81
+ registry.close();
82
+ return failed ? 1 : 0;
83
+ }
84
+
85
+ function safeTransport(server) {
86
+ try {
87
+ return mcpTransport(server);
88
+ } catch {
89
+ return '?';
90
+ }
91
+ }
92
+
93
+ function printHelp() {
94
+ console.log(`Uso: tamnora mcp [opciones]
95
+
96
+ Conecta a los servidores MCP de tamnora.json y lista sus herramientas.
97
+
98
+ Opciones:
99
+ --dir <path> Directorio del proyecto (tamnora.json)
100
+ --json Salida JSON`);
101
+ }