@danieltmn/tamnora 0.25.0 → 0.25.1

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 (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/harness/packages/cli/src/commands/repl.js +18 -2
  3. package/harness/packages/cli/src/commands/run.js +5 -1
  4. package/harness/packages/cli/src/commands/tui.js +5 -1
  5. package/harness/packages/cli/src/lib/runtime.js +2 -0
  6. package/harness/packages/cli/src/tui/app.js +22 -2
  7. package/harness/packages/cli/src/tui/layout.js +1 -1
  8. package/harness/packages/core/src/agent/runAgent.js +5 -2
  9. package/harness/packages/core/src/agents/index.js +141 -0
  10. package/harness/packages/core/src/config/index.js +1 -0
  11. package/harness/packages/core/src/index.js +10 -0
  12. package/harness/packages/core/src/providers/index.js +3 -1
  13. package/harness/packages/core/src/providers/ollama.js +4 -1
  14. package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
  15. package/harness/packages/core/src/providers/openrouter.js +5 -1
  16. package/harness/packages/core/src/providers/presets.js +4 -3
  17. package/harness/packages/core/src/providers/reasoning.js +58 -0
  18. package/harness/packages/core/test/agents.test.js +111 -0
  19. package/harness/packages/core/test/reasoning.test.js +43 -0
  20. package/harness/packages/server/public/app.js +37 -0
  21. package/harness/packages/server/public/chat.js +1 -0
  22. package/harness/packages/server/public/chrome.js +94 -1
  23. package/harness/packages/server/public/core.js +1 -1
  24. package/harness/packages/server/public/icons (1).zip +0 -0
  25. package/harness/packages/server/public/index.html +90 -1
  26. package/harness/packages/server/public/sessions.js +1 -0
  27. package/harness/packages/server/public/settings.js +167 -0
  28. package/harness/packages/server/public/settingsNav.js +1 -0
  29. package/harness/packages/server/src/lib/agents.js +74 -0
  30. package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
  31. package/harness/packages/server/src/lib/prompt.js +11 -3
  32. package/harness/packages/server/src/lib/scheduler.js +4 -0
  33. package/harness/packages/server/src/lib/sessionView.js +1 -0
  34. package/harness/packages/server/src/lib/skills.js +13 -0
  35. package/harness/packages/server/src/lib/tasks.js +71 -23
  36. package/harness/packages/server/src/routes/agents.js +88 -0
  37. package/harness/packages/server/src/routes/chat.js +9 -1
  38. package/harness/packages/server/src/routes/providers.js +7 -0
  39. package/harness/packages/server/src/routes/sessions.js +3 -1
  40. package/harness/packages/server/src/server.js +7 -0
  41. package/harness/packages/server/test/agents.test.js +194 -0
  42. package/harness/packages/server/test/consoleTabs.test.js +2 -2
  43. package/harness/packages/server/test/settingsNav.test.js +2 -2
  44. package/harness/tamnora.example.json +1 -0
  45. package/package.json +1 -1
@@ -3,11 +3,13 @@ import {
3
3
  runAgent,
4
4
  createTools,
5
5
  resolveMode,
6
+ normalizeEffort,
6
7
  MAX_TOOL_ROUNDS,
7
8
  } from '../lib/core.js';
8
9
  import { openSse, readJson, sendJson, sendError } from '../lib/http.js';
9
10
  import { workspaceForRequest, resolveExistingDir, localOk } from '../lib/workspace.js';
10
11
  import { createSubagentTool } from '../lib/tasks.js';
12
+ import { enabledAgents } from '../lib/agents.js';
11
13
  import {
12
14
  combineToolset,
13
15
  combineLifecycle,
@@ -162,6 +164,8 @@ export function registerChat(route, ctx) {
162
164
  const model = body.model || session?.model || ctx.effectiveModel();
163
165
  if (!model) return sendError(res, 400, 'Falta "model" (en el cuerpo, en Ajustes o en tamnora.json).');
164
166
  const mode = resolveMode(body.mode ?? session?.meta?.mode ?? ctx.store.getSetting('activeMode'));
167
+ // Esfuerzo de razonamiento: cuerpo > sesión > default global/config.
168
+ const effort = normalizeEffort(body.effort ?? session?.meta?.effort ?? ctx.effectiveEffort?.());
165
169
 
166
170
  if (!sessionId) {
167
171
  try {
@@ -174,7 +178,7 @@ export function registerChat(route, ctx) {
174
178
  provider: providerId,
175
179
  model,
176
180
  cwd: sessionCwd,
177
- meta: { mode: mode.id },
181
+ meta: { mode: mode.id, effort },
178
182
  });
179
183
  }
180
184
 
@@ -209,6 +213,7 @@ export function registerChat(route, ctx) {
209
213
  provider: providerId,
210
214
  model,
211
215
  mode: mode.id,
216
+ effort,
212
217
  cwd: sessionCwd,
213
218
  say: fraseInicio(),
214
219
  });
@@ -271,6 +276,8 @@ export function registerChat(route, ctx) {
271
276
  providerId,
272
277
  model,
273
278
  parentMode: mode,
279
+ effort,
280
+ agents: enabledAgents(ctx),
274
281
  permissions: ctx.config.permissions,
275
282
  mcp,
276
283
  mcpMode: ctx.effectiveMcpMode(),
@@ -347,6 +354,7 @@ export function registerChat(route, ctx) {
347
354
  maxDurationMs: Number(body.maxDurationMs) || ctx.effectiveMaxDurationMs?.() || 0,
348
355
  maxOutputTokens: Number(body.maxOutputTokens) || ctx.effectiveMaxOutputTokens?.() || 0,
349
356
  promptCaching: ctx.effectivePromptCaching?.() ?? ctx.config.promptCaching === true,
357
+ reasoningEffort: effort,
350
358
  stallWarnMs: body.stallWarnMs ?? ctx.config.stallWarnMs,
351
359
  contextTokens,
352
360
  onEvent(event) {
@@ -7,6 +7,7 @@ import {
7
7
  readApp,
8
8
  writeApp,
9
9
  maskSecret,
10
+ normalizeEffort,
10
11
  } from '../lib/core.js';
11
12
  import { resolveAppearance, sanitizeAppearance } from '../lib/appearance.js';
12
13
  import { readJson, sendJson, sendError } from '../lib/http.js';
@@ -26,6 +27,7 @@ export function providerList(ctx) {
26
27
  baseUrl: row?.baseUrl || '',
27
28
  defaultBaseUrl: meta.defaultBaseUrl,
28
29
  requiresKey: row?.requiresKey ?? meta.requiresKey,
30
+ reasoning: meta.reasoning ?? 'none',
29
31
  custom: false,
30
32
  builtin: true,
31
33
  stored: Boolean(row),
@@ -44,6 +46,7 @@ export function providerList(ctx) {
44
46
  baseUrl: row.baseUrl,
45
47
  defaultBaseUrl: '',
46
48
  requiresKey: row.requiresKey ?? true,
49
+ reasoning: 'none',
47
50
  custom: true,
48
51
  builtin: false,
49
52
  stored: true,
@@ -83,6 +86,7 @@ export function registerProviders(route, ctx) {
83
86
  maxDurationMs: ctx.effectiveMaxDurationMs(),
84
87
  maxOutputTokens: ctx.effectiveMaxOutputTokens(),
85
88
  promptCaching: ctx.effectivePromptCaching(),
89
+ effort: ctx.effectiveEffort(),
86
90
  defaultCwd: ctx.store.getSetting('defaultCwd') || '',
87
91
  strictScope: ctx.effectiveStrictScope(),
88
92
  mcpMode: ctx.effectiveMcpMode(),
@@ -101,6 +105,9 @@ export function registerProviders(route, ctx) {
101
105
  if (body.mode !== undefined) ctx.store.setSetting('activeMode', resolveMode(String(body.mode)).id);
102
106
  if (body.defaultCwd !== undefined) ctx.store.setSetting('defaultCwd', String(body.defaultCwd ?? '').trim() || null);
103
107
  if (body.strictScope !== undefined) ctx.store.setSetting('strictScope', Boolean(body.strictScope));
108
+ if (body.effort !== undefined) {
109
+ ctx.store.setSetting('activeEffort', normalizeEffort(body.effort) || null);
110
+ }
104
111
  if (body.promptCaching !== undefined) ctx.store.setSetting('promptCaching', Boolean(body.promptCaching));
105
112
  if (body.mcpMode !== undefined) {
106
113
  const mode = String(body.mcpMode);
@@ -1,5 +1,5 @@
1
1
  import { readJson, sendJson, sendError } from '../lib/http.js';
2
- import { resolveMode } from '../lib/core.js';
2
+ import { resolveMode, normalizeEffort } from '../lib/core.js';
3
3
  import { workspaceForRequest } from '../lib/workspace.js';
4
4
  import { sessionView, sessionState, awaitingBySession } from '../lib/sessionView.js';
5
5
  import { summarizeSession, compactSession, SUMMARY_INSTRUCTION } from '../lib/compact.js';
@@ -49,6 +49,7 @@ export function registerSessions(route, ctx) {
49
49
  const meta = {};
50
50
  if (body.goal) meta.goal = String(body.goal);
51
51
  if (body.mode) meta.mode = resolveMode(String(body.mode)).id;
52
+ if ('effort' in body) meta.effort = normalizeEffort(body.effort);
52
53
  const id = ctx.store.createSession({
53
54
  title: String(body.title || 'Nueva sesión').slice(0, 80),
54
55
  provider: body.provider || ctx.effectiveProviderId(),
@@ -105,6 +106,7 @@ export function registerSessions(route, ctx) {
105
106
  const meta = {};
106
107
  if ('goal' in body) meta.goal = body.goal ? String(body.goal) : undefined;
107
108
  if ('mode' in body) meta.mode = resolveMode(String(body.mode)).id;
109
+ if ('effort' in body) meta.effort = normalizeEffort(body.effort);
108
110
  if (Object.keys(meta).length) patch.meta = meta;
109
111
  ctx.store.touchSession(params.id, patch);
110
112
  sendJson(res, 200, { session: sessionView(ctx.store.getSession(params.id)) });
@@ -17,6 +17,7 @@ import {
17
17
  MCP_AUTO_THRESHOLD,
18
18
  readPackageVersion,
19
19
  scopeDirective,
20
+ normalizeEffort,
20
21
  } from './lib/core.js';
21
22
  import { sendJson, sendError } from './lib/http.js';
22
23
  import { requireTrustedHost, requireSameOrigin } from './lib/workspace.js';
@@ -24,6 +25,7 @@ import { applySecurityHeaders, cspMode } from './lib/securityHeaders.js';
24
25
  import { createPromptBuilder, DEFAULT_SYSTEM_PROMPT } from './lib/prompt.js';
25
26
  import { profilePrompt, resolveProfile } from './lib/profile.js';
26
27
  import { skillsBlock } from './lib/skills.js';
28
+ import { agentsIndexPrompt } from './lib/agents.js';
27
29
  import { createApprovalBroker } from './lib/approvals.js';
28
30
  import { createQuestionBroker } from './lib/questions.js';
29
31
  import { createPresence } from './lib/presence.js';
@@ -42,6 +44,7 @@ import { registerModels } from './routes/models.js';
42
44
  import { registerProviders } from './routes/providers.js';
43
45
  import { registerMcp } from './routes/mcp.js';
44
46
  import { registerSkills } from './routes/skills.js';
47
+ import { registerAgents } from './routes/agents.js';
45
48
  import { registerMemory } from './routes/memory.js';
46
49
  import { registerChannels } from './routes/channels.js';
47
50
  import { registerProfile } from './routes/profile.js';
@@ -183,6 +186,7 @@ export async function createTamnoraServer({
183
186
  const effectiveMaxDurationMs = () => settingNumber('maxDurationMs', config.maxDurationMs);
184
187
  const effectiveMaxOutputTokens = () => settingNumber('maxOutputTokens', config.maxOutputTokens);
185
188
  const effectiveStrictScope = () => store.getSetting('strictScope') === true;
189
+ const effectiveEffort = () => normalizeEffort(store.getSetting('activeEffort') ?? config.effort);
186
190
  const effectivePromptCaching = () => {
187
191
  const stored = store.getSetting('promptCaching');
188
192
  if (stored === null || stored === undefined) return config.promptCaching === true;
@@ -228,6 +232,7 @@ export async function createTamnoraServer({
228
232
  effectiveMaxDurationMs,
229
233
  effectiveMaxOutputTokens,
230
234
  effectiveStrictScope,
235
+ effectiveEffort,
231
236
  effectivePromptCaching,
232
237
  effectiveMcpMode,
233
238
  effectiveMcpPinned,
@@ -269,6 +274,7 @@ export async function createTamnoraServer({
269
274
  files: config.instructions,
270
275
  profileText: () => profilePrompt(resolveProfile(ctx.app)),
271
276
  skillsText: () => skillsBlock(ctx),
277
+ agentsText: () => agentsIndexPrompt(ctx),
272
278
  mcpText: () => ctx.mcpText(),
273
279
  scopeText: () => scopeDirective(ctx.effectiveStrictScope()),
274
280
  ruleFiles: config.rules ?? [],
@@ -326,6 +332,7 @@ export async function createTamnoraServer({
326
332
  registerProviders(route, ctx);
327
333
  registerMcp(route, ctx);
328
334
  registerSkills(route, ctx);
335
+ registerAgents(route, ctx);
329
336
  registerMemory(route, ctx);
330
337
  registerChannels(route, ctx);
331
338
  registerProfile(route, ctx);
@@ -0,0 +1,194 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { startServer } from './helpers.js';
7
+
8
+ const json = { 'Content-Type': 'application/json' };
9
+
10
+ test('manages subagents from the console (CRUD + toggle)', async () => {
11
+ const { instance, base } = await startServer();
12
+ try {
13
+ let data = await (await fetch(`${base}/api/agents`)).json();
14
+ assert.deepEqual(data.agents, []);
15
+
16
+ let res = await fetch(`${base}/api/agents/revisor`, {
17
+ method: 'PUT',
18
+ headers: json,
19
+ body: JSON.stringify({
20
+ name: 'Revisor',
21
+ description: 'Busca bugs',
22
+ provider: 'openai',
23
+ model: 'gpt',
24
+ mode: 'plan',
25
+ effort: 'high',
26
+ maxRounds: 4,
27
+ tools: ['read', 'grep'],
28
+ skills: ['code-review'],
29
+ body: '# Rol\nMirá el diff.',
30
+ }),
31
+ });
32
+ assert.equal(res.status, 200);
33
+ data = await res.json();
34
+ const agent = data.agents.find((item) => item.id === 'revisor');
35
+ assert.equal(agent.name, 'Revisor');
36
+ assert.equal(agent.effort, 'high');
37
+ assert.equal(agent.mode, 'plan');
38
+ assert.equal(agent.maxRounds, 4);
39
+ assert.deepEqual(agent.tools, ['read', 'grep']);
40
+ assert.deepEqual(agent.skills, ['code-review']);
41
+ assert.equal(agent.enabled, true);
42
+
43
+ const detail = await (await fetch(`${base}/api/agents/revisor`)).json();
44
+ assert.match(detail.agent.body, /Mirá el diff/);
45
+
46
+ res = await fetch(`${base}/api/agents/revisor/toggle`, { method: 'POST' });
47
+ data = await res.json();
48
+ assert.equal(data.enabled, false);
49
+ data = await (await fetch(`${base}/api/agents`)).json();
50
+ assert.equal(data.agents[0].enabled, false);
51
+
52
+ res = await fetch(`${base}/api/agents/revisor/toggle`, { method: 'POST' });
53
+ assert.equal((await res.json()).enabled, true);
54
+
55
+ res = await fetch(`${base}/api/agents/revisor`, { method: 'DELETE' });
56
+ assert.equal(res.status, 200);
57
+ data = await res.json();
58
+ assert.deepEqual(data.agents, []);
59
+ } finally {
60
+ await instance.close();
61
+ }
62
+ });
63
+
64
+ test('rejects invalid subagent payloads', async () => {
65
+ const { instance, base } = await startServer();
66
+ try {
67
+ let res = await fetch(`${base}/api/agents/${encodeURIComponent('../escape')}`, {
68
+ method: 'PUT',
69
+ headers: json,
70
+ body: JSON.stringify({ body: 'x' }),
71
+ });
72
+ assert.equal(res.status, 400);
73
+
74
+ res = await fetch(`${base}/api/agents/vacio`, {
75
+ method: 'PUT',
76
+ headers: json,
77
+ body: JSON.stringify({ mode: 'plan' }),
78
+ });
79
+ assert.equal(res.status, 400);
80
+ assert.match((await res.json()).error, /cuerpo o una descripción/);
81
+ } finally {
82
+ await instance.close();
83
+ }
84
+ });
85
+
86
+ test('effort persists globally and per session', async () => {
87
+ const { instance, base } = await startServer();
88
+ try {
89
+ let data = await (await fetch(`${base}/api/settings`)).json();
90
+ assert.equal(data.settings.effort, '');
91
+
92
+ let res = await fetch(`${base}/api/settings`, { method: 'PUT', headers: json, body: JSON.stringify({ effort: 'max' }) });
93
+ data = await res.json();
94
+ assert.equal(data.settings.effort, 'max');
95
+
96
+ res = await fetch(`${base}/api/settings`, { method: 'PUT', headers: json, body: JSON.stringify({ effort: 'auto' }) });
97
+ assert.equal((await res.json()).settings.effort, '');
98
+
99
+ res = await fetch(`${base}/api/sessions`, {
100
+ method: 'POST',
101
+ headers: json,
102
+ body: JSON.stringify({ title: 'con esfuerzo', model: 'm', effort: 'low' }),
103
+ });
104
+ const created = await res.json();
105
+ assert.equal(created.session.effort, 'low');
106
+
107
+ res = await fetch(`${base}/api/sessions/${created.session.id}`, {
108
+ method: 'PUT',
109
+ headers: json,
110
+ body: JSON.stringify({ effort: 'high' }),
111
+ });
112
+ assert.equal((await res.json()).session.effort, 'high');
113
+ } finally {
114
+ await instance.close();
115
+ }
116
+ });
117
+
118
+ test('a named subagent runs with its own model, effort and instructions', async () => {
119
+ const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-subagent-'));
120
+ const calls = { parent: 0 };
121
+ const sub = {};
122
+ const parent = {
123
+ id: 'parent',
124
+ async streamChat() {
125
+ calls.parent += 1;
126
+ if (calls.parent === 1) {
127
+ return {
128
+ content: '',
129
+ reasoning: '',
130
+ toolCalls: [
131
+ {
132
+ id: 't1',
133
+ type: 'function',
134
+ function: {
135
+ name: 'task',
136
+ arguments: JSON.stringify({ agent: 'revisor', description: 'revisión', prompt: 'revisá el cambio' }),
137
+ },
138
+ },
139
+ ],
140
+ };
141
+ }
142
+ return { content: 'listo', reasoning: '', toolCalls: [] };
143
+ },
144
+ };
145
+ const revisor = {
146
+ id: 'revisor',
147
+ async streamChat(_config, { model, messages, reasoningEffort }) {
148
+ Object.assign(sub, { model, reasoningEffort, system: messages?.[0]?.content ?? '' });
149
+ return { content: 'resumen del revisor', reasoning: '', toolCalls: [] };
150
+ },
151
+ };
152
+ const providers = { parent, revisor };
153
+ const { instance, base } = await startServer({
154
+ cwd,
155
+ resolveProvider: ({ providerId }) => ({ provider: providers[providerId] ?? parent, config: {} }),
156
+ });
157
+ try {
158
+ await fetch(`${base}/api/agents/revisor`, {
159
+ method: 'PUT',
160
+ headers: json,
161
+ body: JSON.stringify({
162
+ name: 'Revisor',
163
+ description: 'Busca bugs',
164
+ provider: 'revisor',
165
+ model: 'revisor-model',
166
+ mode: 'plan',
167
+ effort: 'high',
168
+ body: 'MARCADOR_REVISOR',
169
+ }),
170
+ });
171
+
172
+ const created = await (
173
+ await fetch(`${base}/api/sessions`, {
174
+ method: 'POST',
175
+ headers: json,
176
+ body: JSON.stringify({ title: 'delegar', model: 'parent-model', provider: 'parent' }),
177
+ })
178
+ ).json();
179
+ await (
180
+ await fetch(`${base}/api/chat`, {
181
+ method: 'POST',
182
+ headers: json,
183
+ body: JSON.stringify({ sessionId: created.session.id, input: 'delegá al revisor', model: 'parent-model', provider: 'parent' }),
184
+ })
185
+ ).text();
186
+
187
+ assert.equal(sub.model, 'revisor-model');
188
+ assert.equal(sub.reasoningEffort, 'high');
189
+ assert.match(sub.system, /MARCADOR_REVISOR/);
190
+ assert.equal(calls.parent >= 2, true);
191
+ } finally {
192
+ await instance.close();
193
+ }
194
+ });
@@ -159,13 +159,13 @@ for (const [label, width, height] of [['escritorio', 1280, 800], ['movil', 390,
159
159
  const r = shot.preScriptResult;
160
160
  assert.ok(r, 'el script no devolvio resultado');
161
161
 
162
- assert.equal(r.todas, 13, 'sin busqueda se ven las 13 secciones');
162
+ assert.equal(r.todas, 14, 'sin busqueda se ven las 14 secciones');
163
163
  assert.deepEqual(r.filtradoVoz, ['voz']);
164
164
  assert.deepEqual(r.gruposVoz, ['canales'], 'los grupos sin resultados se ocultan');
165
165
  assert.ok(r.filtradoTema.includes('apariencia'), 'busca por palabra clave');
166
166
  assert.deepEqual(r.sinResultados, { tabs: 0, mensaje: true, grupos: 0 });
167
167
  assert.deepEqual(r.enter, { activo: 'limites', selected: 'limites', nivel: 'panel' });
168
- assert.deepEqual(r.escape, { valor: '', tabs: 13 }, 'Escape limpia la busqueda y vuelve a mostrar todo');
168
+ assert.deepEqual(r.escape, { valor: '', tabs: 14 }, 'Escape limpia la busqueda y vuelve a mostrar todo');
169
169
  assert.ok(r.palette.some((label) => label === 'Ajustes · Apariencia'), `la palette no encontro Apariencia: ${r.palette}`);
170
170
  assert.deepEqual(r.paletteSalto, { vista: true, panel: 'apariencia', nivel: 'panel' });
171
171
 
@@ -16,7 +16,7 @@ test('foldText ignora mayusculas y acentos', () => {
16
16
  test('sin consulta devuelve todas las secciones, en el orden de los grupos', () => {
17
17
  assert.deepEqual(ids(filterSettings('')), ids(SETTINGS_SECTIONS));
18
18
  assert.deepEqual(ids(filterSettings(' ')), ids(SETTINGS_SECTIONS));
19
- assert.equal(SETTINGS_SECTIONS.length, 13);
19
+ assert.equal(SETTINGS_SECTIONS.length, 14);
20
20
  });
21
21
 
22
22
  test('busca por nombre sin importar acentos ni mayusculas', () => {
@@ -34,7 +34,7 @@ test('busca por palabra clave y por grupo', () => {
34
34
  assert.ok(ids(filterSettings('tema')).includes('apariencia'));
35
35
  assert.ok(ids(filterSettings('dictado')).includes('transcripcion'));
36
36
  assert.ok(ids(filterSettings('cron')).includes('tareas'));
37
- assert.deepEqual(ids(filterSettings('contexto')), ['perfil', 'memoria', 'skills', 'mcp']);
37
+ assert.deepEqual(ids(filterSettings('contexto')), ['perfil', 'memoria', 'skills', 'agentes', 'mcp']);
38
38
  });
39
39
 
40
40
  test('todas las palabras de la consulta tienen que coincidir', () => {
@@ -7,6 +7,7 @@
7
7
  "maxDurationMs": 0,
8
8
  "maxOutputTokens": 0,
9
9
  "promptCaching": false,
10
+ "effort": "",
10
11
  "skills": { "mode": "lazy" },
11
12
  "compact": { "enabled": true, "threshold": 0.75, "keepLast": 8 },
12
13
  "rules": [".tamnora/rules/*.md"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/tamnora",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Harness local para IAs: agente CLI-first con TUI, consola web y evaluacion. Node puro, sin dependencias.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.25.0",