@danieltmn/tamnora 0.24.2 → 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.
- package/CHANGELOG.md +20 -0
- package/harness/packages/cli/src/commands/repl.js +18 -2
- package/harness/packages/cli/src/commands/run.js +5 -1
- package/harness/packages/cli/src/commands/tui.js +5 -1
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/tui/app.js +22 -2
- package/harness/packages/cli/src/tui/layout.js +1 -1
- package/harness/packages/core/src/agent/runAgent.js +5 -2
- package/harness/packages/core/src/agents/index.js +141 -0
- package/harness/packages/core/src/config/index.js +1 -0
- package/harness/packages/core/src/index.js +10 -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/test/agents.test.js +111 -0
- package/harness/packages/core/test/reasoning.test.js +43 -0
- package/harness/packages/server/public/app.js +868 -834
- package/harness/packages/server/public/appearance.js +4 -0
- package/harness/packages/server/public/boot.js +49 -0
- package/harness/packages/server/public/chat.js +1 -0
- package/harness/packages/server/public/chrome.js +946 -852
- package/harness/packages/server/public/commands.js +448 -405
- package/harness/packages/server/public/core.js +229 -216
- package/harness/packages/server/public/favicon.ico +0 -0
- package/harness/packages/server/public/icons/icon-128x128.png +0 -0
- package/harness/packages/server/public/icons/icon-144x144.png +0 -0
- package/harness/packages/server/public/icons/icon-152x152.png +0 -0
- package/harness/packages/server/public/icons/icon-192x192.png +0 -0
- package/harness/packages/server/public/icons/icon-384x384.png +0 -0
- package/harness/packages/server/public/icons/icon-512x512.png +0 -0
- package/harness/packages/server/public/icons/icon-72x72.png +0 -0
- package/harness/packages/server/public/icons/icon-96x96.png +0 -0
- package/harness/packages/server/public/icons/logo.png +0 -0
- package/harness/packages/server/public/icons/logo_dark.png +0 -0
- package/harness/packages/server/public/icons/logo_light.png +0 -0
- package/harness/packages/server/public/icons/tamnora.ico +0 -0
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/public/index.html +161 -101
- package/harness/packages/server/public/landing/boot.js +16 -0
- package/harness/packages/server/public/landing/index.html +123 -59
- package/harness/packages/server/public/landing/landing.js +11 -9
- package/harness/packages/server/public/logo.png +0 -0
- package/harness/packages/server/public/markdown.js +1 -1
- package/harness/packages/server/public/sessions.js +1 -0
- package/harness/packages/server/public/settings.js +1322 -1080
- package/harness/packages/server/public/settingsNav.js +82 -0
- package/harness/packages/server/public/styles.css +2807 -2525
- package/harness/packages/server/public/system.js +2 -2
- package/harness/packages/server/src/lib/agents.js +74 -0
- package/harness/packages/server/src/lib/appearance.js +4 -0
- package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
- package/harness/packages/server/src/lib/prompt.js +11 -3
- package/harness/packages/server/src/lib/restart.js +20 -0
- package/harness/packages/server/src/lib/scheduler.js +4 -0
- package/harness/packages/server/src/lib/securityHeaders.js +53 -0
- package/harness/packages/server/src/lib/sessionView.js +1 -0
- package/harness/packages/server/src/lib/skills.js +13 -0
- package/harness/packages/server/src/lib/tasks.js +71 -23
- package/harness/packages/server/src/routes/agents.js +88 -0
- package/harness/packages/server/src/routes/chat.js +9 -1
- package/harness/packages/server/src/routes/providers.js +7 -0
- package/harness/packages/server/src/routes/reload.js +46 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/server.js +13 -1
- package/harness/packages/server/test/accessibility.test.js +72 -0
- package/harness/packages/server/test/agents.test.js +194 -0
- package/harness/packages/server/test/appearance.test.js +4 -0
- package/harness/packages/server/test/consoleModules.test.js +133 -57
- package/harness/packages/server/test/consoleTabs.test.js +228 -0
- package/harness/packages/server/test/docsSync.test.js +73 -0
- package/harness/packages/server/test/landingLogin.test.js +89 -0
- package/harness/packages/server/test/markdownSecurity.test.js +80 -0
- package/harness/packages/server/test/reload.test.js +95 -0
- package/harness/packages/server/test/securityHeaders.test.js +109 -0
- package/harness/packages/server/test/settingsNav.test.js +90 -0
- package/harness/packages/server/test/themeCss.js +110 -0
- package/harness/packages/server/test/themes.test.js +72 -0
- package/harness/scripts/restart.mjs +101 -0
- package/harness/tamnora.example.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Esfuerzo de razonamiento: un vocabulario comun que cada adaptador traduce al
|
|
2
|
+
// parametro de su proveedor. `''` significa "sin override" (default del modelo).
|
|
3
|
+
|
|
4
|
+
export const EFFORT_LEVELS = ['off', 'low', 'medium', 'high', 'max'];
|
|
5
|
+
|
|
6
|
+
export const EFFORT_LABELS = {
|
|
7
|
+
off: 'Sin razonamiento',
|
|
8
|
+
low: 'Bajo',
|
|
9
|
+
medium: 'Medio',
|
|
10
|
+
high: 'Alto',
|
|
11
|
+
max: 'Máximo',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ALIASES = {
|
|
15
|
+
none: 'off',
|
|
16
|
+
minimal: 'low',
|
|
17
|
+
medium: 'medium',
|
|
18
|
+
xhigh: 'max',
|
|
19
|
+
ultra: 'max',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Normaliza lo que manda el usuario/proveedor al vocabulario comun. */
|
|
23
|
+
export function normalizeEffort(value) {
|
|
24
|
+
if (value === null || value === undefined) return '';
|
|
25
|
+
const raw = String(value).trim().toLowerCase();
|
|
26
|
+
if (!raw || raw === 'auto' || raw === 'default') return '';
|
|
27
|
+
const level = ALIASES[raw] ?? raw;
|
|
28
|
+
return EFFORT_LEVELS.includes(level) ? level : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Agrega al body del request el parametro de esfuerzo que entiende el
|
|
33
|
+
* proveedor. Los adaptadores que no lo soportan (`reasoning: 'none'`) no
|
|
34
|
+
* reciben nada, asi un modelo sin razonamiento no rompe con un 400.
|
|
35
|
+
*/
|
|
36
|
+
export function applyReasoningEffort(body, effort, provider = {}) {
|
|
37
|
+
const level = normalizeEffort(effort);
|
|
38
|
+
if (!level) return body;
|
|
39
|
+
const mode = provider?.reasoning ?? 'none';
|
|
40
|
+
|
|
41
|
+
if (mode === 'think') {
|
|
42
|
+
// Ollama: `think` acepta boolean o (en versiones nuevas) un nivel.
|
|
43
|
+
body.think = level === 'off' ? false : level;
|
|
44
|
+
return body;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (mode === 'effort' || mode === 'openai' || mode === 'deepseek') {
|
|
48
|
+
// OpenAI solo llega hasta high; DeepSeek y OpenRouter aceptan max.
|
|
49
|
+
const effortValue = level === 'max' && mode === 'openai' ? 'high' : level === 'off' ? 'none' : level;
|
|
50
|
+
body.reasoning_effort = effortValue;
|
|
51
|
+
if (mode === 'deepseek') {
|
|
52
|
+
body.thinking = { type: level === 'off' ? 'disabled' : 'enabled' };
|
|
53
|
+
}
|
|
54
|
+
return body;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return body;
|
|
58
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|