@danieltmn/tamnora 0.25.1 → 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 +12 -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 +3 -3
- package/harness/packages/cli/src/commands/run.js +1 -1
- package/harness/packages/cli/src/commands/server.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +1 -1
- package/harness/packages/cli/src/lib/util.js +2 -2
- package/harness/packages/cli/src/tui/app.js +2 -2
- package/harness/packages/cli/test/tunnel.test.js +5 -32
- package/harness/packages/core/src/agents/index.js +13 -41
- 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 +5 -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 +2 -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/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/home.test.js +2 -35
- package/harness/packages/core/test/procs.test.js +3 -64
- package/harness/packages/core/test/storage.test.js +1 -12
- package/harness/packages/server/public/app.js +2 -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 +0 -16
- package/harness/packages/server/public/chatFormat.js +0 -11
- package/harness/packages/server/public/chrome.js +2 -5
- package/harness/packages/server/public/commands.js +1 -2
- package/harness/packages/server/public/explorer.js +7 -10
- package/harness/packages/server/public/index.html +1228 -1326
- 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/settings.js +1255 -1322
- package/harness/packages/server/public/settingsNav.js +0 -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 +13 -27
- 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/definitions.js +40 -0
- package/harness/packages/server/src/lib/skills.js +14 -21
- 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/chat.js +0 -43
- 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/tts.js +1 -1
- package/harness/packages/server/src/server.js +1 -5
- 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 +4 -4
- 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 +1 -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
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
setStatus,
|
|
7
7
|
trModelEl,
|
|
8
8
|
trProviderEl,
|
|
9
|
-
vozAnnAdviceEl,
|
|
10
9
|
vozAnnAnswerEl,
|
|
11
10
|
vozAnnEndEl,
|
|
12
11
|
vozAnnStartEl,
|
|
13
|
-
vozAnnSummaryEl,
|
|
14
12
|
vozAnnToolsEl,
|
|
15
13
|
vozEnabledEl,
|
|
16
14
|
vozFfmpegEl,
|
|
@@ -573,7 +571,7 @@ export const voz = {
|
|
|
573
571
|
voice: 'es-AR-ElenaNeural',
|
|
574
572
|
rate: '+0%',
|
|
575
573
|
pitch: '+0Hz',
|
|
576
|
-
announce: { start: true, tools: true, end: true,
|
|
574
|
+
announce: { start: true, tools: true, end: true, answer: false },
|
|
577
575
|
},
|
|
578
576
|
};
|
|
579
577
|
|
|
@@ -772,11 +770,9 @@ export function narrar(event, data, turn) {
|
|
|
772
770
|
const { enabled, announce } = voz.settings;
|
|
773
771
|
if (!enabled && !handsFreeOn) return;
|
|
774
772
|
// Manos libres: conversacion, sin narracion de arranque/herramientas; lee la
|
|
775
|
-
// respuesta final
|
|
773
|
+
// respuesta final aunque la narracion este apagada en Ajustes.
|
|
776
774
|
if (handsFreeOn) {
|
|
777
|
-
if (event === '
|
|
778
|
-
if (data?.say) hablar(data.say, { label: 'asesor', maxAgeMs: 0, force: true });
|
|
779
|
-
} else if (event === 'done') {
|
|
775
|
+
if (event === 'done') {
|
|
780
776
|
const spoken = speechText(data?.content);
|
|
781
777
|
if (spoken) hablar(spoken, { label: 'respuesta', maxAgeMs: 0, force: true });
|
|
782
778
|
else if (data?.say) hablar(data.say, { maxAgeMs: 0, force: true });
|
|
@@ -788,15 +784,9 @@ export function narrar(event, data, turn) {
|
|
|
788
784
|
if (!enabled) return;
|
|
789
785
|
if (event === 'start' && announce.start) hablar(data?.say);
|
|
790
786
|
else if (event === 'tool_call' && announce.tools) hablar(data?.say);
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
else if (event === '
|
|
794
|
-
if (data?.kind === 'summary') {
|
|
795
|
-
if (announce.summary) hablar(data?.say, { label: 'resumen', maxAgeMs: 0 });
|
|
796
|
-
} else if (announce.advice) {
|
|
797
|
-
hablar(data?.say, { label: 'asesor', maxAgeMs: 0 });
|
|
798
|
-
}
|
|
799
|
-
} else if (event === 'done' && announce.answer) {
|
|
787
|
+
// La respuesta final es el cierre del turno: si queda en cola detras de un
|
|
788
|
+
// audio largo, igual hay que leerla (sin vencimiento).
|
|
789
|
+
else if (event === 'done' && announce.answer) {
|
|
800
790
|
// Lee la respuesta final limpia; si era solo codigo, cae al cierre.
|
|
801
791
|
const spoken = speechText(data?.content);
|
|
802
792
|
if (spoken) hablar(spoken, { label: 'respuesta', maxAgeMs: 0 });
|
|
@@ -828,9 +818,7 @@ export async function aplicarVoz(data) {
|
|
|
828
818
|
start: data.announce?.start !== false,
|
|
829
819
|
tools: data.announce?.tools !== false,
|
|
830
820
|
end: data.announce?.end !== false,
|
|
831
|
-
advice: data.announce?.advice !== false,
|
|
832
821
|
answer: data.announce?.answer === true,
|
|
833
|
-
summary: data.announce?.summary === true,
|
|
834
822
|
},
|
|
835
823
|
};
|
|
836
824
|
if (vozEnabledEl) vozEnabledEl.checked = voz.settings.enabled;
|
|
@@ -850,8 +838,6 @@ export async function aplicarVoz(data) {
|
|
|
850
838
|
if (vozAnnStartEl) vozAnnStartEl.checked = voz.settings.announce.start;
|
|
851
839
|
if (vozAnnToolsEl) vozAnnToolsEl.checked = voz.settings.announce.tools;
|
|
852
840
|
if (vozAnnEndEl) vozAnnEndEl.checked = voz.settings.announce.end;
|
|
853
|
-
if (vozAnnAdviceEl) vozAnnAdviceEl.checked = voz.settings.announce.advice;
|
|
854
|
-
if (vozAnnSummaryEl) vozAnnSummaryEl.checked = voz.settings.announce.summary;
|
|
855
841
|
if (vozAnnAnswerEl) vozAnnAnswerEl.checked = voz.settings.announce.answer;
|
|
856
842
|
const estado = $('#voz-status');
|
|
857
843
|
if (estado) {
|
|
@@ -877,9 +863,7 @@ export async function submitVoz() {
|
|
|
877
863
|
start: vozAnnStartEl?.checked !== false,
|
|
878
864
|
tools: vozAnnToolsEl?.checked !== false,
|
|
879
865
|
end: vozAnnEndEl?.checked !== false,
|
|
880
|
-
advice: vozAnnAdviceEl?.checked !== false,
|
|
881
866
|
answer: vozAnnAnswerEl?.checked === true,
|
|
882
|
-
summary: vozAnnSummaryEl?.checked === true,
|
|
883
867
|
},
|
|
884
868
|
};
|
|
885
869
|
const data = await api('/api/tts/settings', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { sendError } from './lib/http.js';
|
|
3
|
-
import { tunnelState } from './lib/core.js';
|
|
4
3
|
import { isLoopback } from './lib/workspace.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -38,26 +37,16 @@ export function parseCookies(header) {
|
|
|
38
37
|
return out;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
// Detras del
|
|
42
|
-
// X-Forwarded-Proto para marcar la cookie Secure.
|
|
43
|
-
//
|
|
44
|
-
// local por direccion: usamos el Host (localhost/127.0.0.1 = local) y el
|
|
45
|
-
// estado del bridge para no mandar la cookie sin Secure por el tunel.
|
|
46
|
-
function isLocalHost(req) {
|
|
47
|
-
const host = String(req.headers.host || '').split(':')[0].toLowerCase();
|
|
48
|
-
return !host || host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
49
|
-
}
|
|
50
|
-
|
|
40
|
+
// Detras del bridge de tailscale el server recibe desde loopback: solo ahi
|
|
41
|
+
// confiamos en X-Forwarded-Proto para marcar la cookie Secure. La URL del
|
|
42
|
+
// tailnet es HTTPS aunque el salto interno sea http.
|
|
51
43
|
function isSecure(req) {
|
|
52
44
|
if (req.socket && req.socket.encrypted) return true;
|
|
53
45
|
const remote = req.socket && req.socket.remoteAddress;
|
|
54
46
|
const loopback = !remote || isLoopback(remote);
|
|
55
47
|
if (!loopback) return false;
|
|
56
48
|
const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
|
|
57
|
-
|
|
58
|
-
// Peticion por el tunel (host publico + bridge activo): la URL publica es
|
|
59
|
-
// HTTPS, la cookie no tiene que poder viajar por la variante http.
|
|
60
|
-
return Boolean(tunnelState()) && !isLocalHost(req);
|
|
49
|
+
return proto === 'https';
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
function serializeCookie(value, req, maxAge) {
|
|
@@ -52,7 +52,7 @@ export async function serveCommand(argv) {
|
|
|
52
52
|
function printHelp() {
|
|
53
53
|
console.log(`Uso: tamnora serve [opciones]
|
|
54
54
|
|
|
55
|
-
Levanta la consola del harness (chat
|
|
55
|
+
Levanta la consola del harness (chat) en localhost.
|
|
56
56
|
|
|
57
57
|
Opciones:
|
|
58
58
|
--port <n> Puerto (default: 4517)
|
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { homeDir, loadAgents, mergeAgents, buildAgentsIndex, writeAgentFile } from './core.js';
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './
|
|
4
|
+
DEFINITION_ID_PATTERN,
|
|
5
|
+
allDefinitions,
|
|
6
|
+
disabledDefinitionIds,
|
|
7
|
+
removeDefinition,
|
|
8
|
+
setDefinitionEnabled,
|
|
9
|
+
} from './definitions.js';
|
|
10
10
|
|
|
11
|
-
export const AGENT_ID_PATTERN =
|
|
12
|
-
|
|
13
|
-
/** Agent roots, lowest priority first: casa agents, then the workspace's. */
|
|
14
|
-
export function agentRoots(cwd) {
|
|
15
|
-
return [path.join(homeDir(), 'agents'), path.join(cwd, '.tamnora', 'agents')];
|
|
16
|
-
}
|
|
11
|
+
export const AGENT_ID_PATTERN = DEFINITION_ID_PATTERN;
|
|
17
12
|
|
|
18
13
|
export function allAgents(cwd) {
|
|
19
|
-
|
|
20
|
-
return mergeAgents(agentRoots(cwd).map((root) => loadAgents(root).map((a) => ({ ...a, root, home: root === home }))));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function disabledIds(store) {
|
|
24
|
-
const raw = store.getSetting('disabledAgents');
|
|
25
|
-
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
|
14
|
+
return allDefinitions(cwd, { kind: 'agents', load: loadAgents, merge: mergeAgents });
|
|
26
15
|
}
|
|
27
16
|
|
|
28
17
|
export function agentsView(ctx) {
|
|
29
|
-
const disabled =
|
|
18
|
+
const disabled = disabledDefinitionIds(ctx.store, 'agents');
|
|
30
19
|
return allAgents(ctx.cwd).map((agent) => ({
|
|
31
20
|
id: agent.id,
|
|
32
21
|
name: agent.name || agent.id,
|
|
@@ -48,7 +37,7 @@ export function findAgent(ctx, id) {
|
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
export function enabledAgents(ctx) {
|
|
51
|
-
const disabled =
|
|
40
|
+
const disabled = disabledDefinitionIds(ctx.store, 'agents');
|
|
52
41
|
return allAgents(ctx.cwd).filter((agent) => !disabled.has(agent.id));
|
|
53
42
|
}
|
|
54
43
|
|
|
@@ -63,12 +52,9 @@ export async function writeAgent(id, data) {
|
|
|
63
52
|
}
|
|
64
53
|
|
|
65
54
|
export async function removeAgent(id) {
|
|
66
|
-
await
|
|
55
|
+
await removeDefinition('agents', id);
|
|
67
56
|
}
|
|
68
57
|
|
|
69
58
|
export function setAgentEnabled(store, id, enabled) {
|
|
70
|
-
|
|
71
|
-
if (enabled) current.delete(id);
|
|
72
|
-
else current.add(id);
|
|
73
|
-
store.setSetting('disabledAgents', [...current]);
|
|
59
|
+
setDefinitionEnabled(store, 'agents', id, enabled);
|
|
74
60
|
}
|
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
// Apariencia de la consola (app.json -> appearance): tema, tipografía y tamaño.
|
|
2
2
|
// Los ids deben coincidir con public/appearance.js; acá solo se valida y acota.
|
|
3
3
|
|
|
4
|
-
export const THEME_IDS = [
|
|
5
|
-
'paper',
|
|
6
|
-
'dark',
|
|
7
|
-
'midnight',
|
|
8
|
-
'nord',
|
|
9
|
-
'dracula',
|
|
10
|
-
'solarized',
|
|
11
|
-
'solarized-dark',
|
|
12
|
-
'bosque',
|
|
13
|
-
'vino',
|
|
14
|
-
'arena',
|
|
15
|
-
'hielo',
|
|
16
|
-
'vercel',
|
|
17
|
-
'dark-plus',
|
|
18
|
-
'github',
|
|
19
|
-
'github-dark',
|
|
20
|
-
'mocha',
|
|
21
|
-
'ebano',
|
|
22
|
-
];
|
|
4
|
+
export const THEME_IDS = ['paper', 'github', 'dark', 'mocha'];
|
|
23
5
|
|
|
24
|
-
export const FONT_IDS = ['clasico', 'sistema'
|
|
6
|
+
export const FONT_IDS = ['clasico', 'sistema'];
|
|
25
7
|
|
|
26
8
|
export const DEFAULT_APPEARANCE = { theme: 'paper', font: 'clasico', chatFont: 14.5, scale: 1 };
|
|
27
9
|
|
|
@@ -31,8 +13,7 @@ const MIN_SCALE = 0.8;
|
|
|
31
13
|
const MAX_SCALE = 1.3;
|
|
32
14
|
|
|
33
15
|
export function resolveThemeId(id) {
|
|
34
|
-
|
|
35
|
-
return THEME_IDS.includes(legacy) ? legacy : DEFAULT_APPEARANCE.theme;
|
|
16
|
+
return THEME_IDS.includes(id) ? id : DEFAULT_APPEARANCE.theme;
|
|
36
17
|
}
|
|
37
18
|
|
|
38
19
|
export function resolveFontId(id) {
|
|
@@ -179,7 +179,7 @@ export async function handleCommand(T, chatId, text) {
|
|
|
179
179
|
T.controllers.get(String(chatId))?.abort();
|
|
180
180
|
return send(T, chatId, 'Cancelando.');
|
|
181
181
|
}
|
|
182
|
-
if (cmd === 'estricto'
|
|
182
|
+
if (cmd === 'estricto') {
|
|
183
183
|
const current = T.ctx.store.getSetting('strictScope') === true;
|
|
184
184
|
const value = arg ? /^(on|si|sí|1|true)$/i.test(arg) : !current;
|
|
185
185
|
T.ctx.store.setSetting('strictScope', value);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Registro comun de definiciones en markdown (skills y subagentes): raices
|
|
2
|
+
// casa + workspace, merge con prioridad al workspace y estado on/off en un
|
|
3
|
+
// setting (`disabledSkills` / `disabledAgents`).
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { rm } from 'node:fs/promises';
|
|
7
|
+
import { homeDir } from './core.js';
|
|
8
|
+
|
|
9
|
+
export const DEFINITION_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
|
|
10
|
+
|
|
11
|
+
/** Raices, de menor a mayor prioridad: casa y despues workspace. */
|
|
12
|
+
export function definitionRoots(cwd, kind) {
|
|
13
|
+
return [path.join(homeDir(), kind), path.join(cwd, '.tamnora', kind)];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Carga y mergea las definiciones, marcando `root` y si viven en la casa. */
|
|
17
|
+
export function allDefinitions(cwd, { kind, load, merge }) {
|
|
18
|
+
const home = path.join(homeDir(), kind);
|
|
19
|
+
return merge(definitionRoots(cwd, kind).map((root) => load(root).map((item) => ({ ...item, root, home: root === home }))));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function disabledKey(kind) {
|
|
23
|
+
return `disabled${kind[0].toUpperCase()}${kind.slice(1)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function disabledDefinitionIds(store, kind) {
|
|
27
|
+
const raw = store.getSetting(disabledKey(kind));
|
|
28
|
+
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setDefinitionEnabled(store, kind, id, enabled) {
|
|
32
|
+
const current = disabledDefinitionIds(store, kind);
|
|
33
|
+
if (enabled) current.delete(id);
|
|
34
|
+
else current.add(id);
|
|
35
|
+
store.setSetting(disabledKey(kind), [...current]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function removeDefinition(kind, id) {
|
|
39
|
+
return rm(path.join(homeDir(), kind, String(id)), { recursive: true, force: true });
|
|
40
|
+
}
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homeDir, loadSkills, mergeSkills, buildSkillsPrompt, buildSkillsIndex } from './core.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFINITION_ID_PATTERN,
|
|
6
|
+
allDefinitions,
|
|
7
|
+
disabledDefinitionIds,
|
|
8
|
+
removeDefinition,
|
|
9
|
+
setDefinitionEnabled,
|
|
10
|
+
} from './definitions.js';
|
|
4
11
|
|
|
5
|
-
export const SKILL_ID_PATTERN =
|
|
6
|
-
|
|
7
|
-
/** Skill roots, lowest priority first: casa skills, then the workspace's. */
|
|
8
|
-
export function skillRoots(cwd) {
|
|
9
|
-
return [path.join(homeDir(), 'skills'), path.join(cwd, '.tamnora', 'skills')];
|
|
10
|
-
}
|
|
12
|
+
export const SKILL_ID_PATTERN = DEFINITION_ID_PATTERN;
|
|
11
13
|
|
|
12
14
|
export function allSkills(cwd) {
|
|
13
|
-
|
|
14
|
-
return mergeSkills(skillRoots(cwd).map((root) => loadSkills(root).map((s) => ({ ...s, root, home: root === home }))));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function disabledIds(store) {
|
|
18
|
-
const raw = store.getSetting('disabledSkills');
|
|
19
|
-
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
|
15
|
+
return allDefinitions(cwd, { kind: 'skills', load: loadSkills, merge: mergeSkills });
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export function skillsView(ctx) {
|
|
23
|
-
const disabled =
|
|
19
|
+
const disabled = disabledDefinitionIds(ctx.store, 'skills');
|
|
24
20
|
return allSkills(ctx.cwd).map((s) => ({
|
|
25
21
|
id: s.id,
|
|
26
22
|
name: s.name || s.id,
|
|
@@ -36,7 +32,7 @@ export function findSkill(ctx, id) {
|
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
export function enabledSkills(ctx) {
|
|
39
|
-
const disabled =
|
|
35
|
+
const disabled = disabledDefinitionIds(ctx.store, 'skills');
|
|
40
36
|
return allSkills(ctx.cwd)
|
|
41
37
|
.filter((s) => !disabled.has(s.id))
|
|
42
38
|
.map((s) => ({ id: s.id, name: s.name || s.id, description: s.description, when: s.when, body: s.body }));
|
|
@@ -82,12 +78,9 @@ export async function writeSkill(id, { name, description, when, body }) {
|
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
export async function removeSkill(id) {
|
|
85
|
-
await
|
|
81
|
+
await removeDefinition('skills', id);
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
export function setSkillEnabled(store, id, enabled) {
|
|
89
|
-
|
|
90
|
-
if (enabled) current.delete(id);
|
|
91
|
-
else current.add(id);
|
|
92
|
-
store.setSetting('disabledSkills', [...current]);
|
|
85
|
+
setDefinitionEnabled(store, 'skills', id, enabled);
|
|
93
86
|
}
|
|
@@ -9,14 +9,14 @@ export const DEFAULT_TTS = {
|
|
|
9
9
|
voice: VOZ_POR_DEFECTO,
|
|
10
10
|
rate: '+0%',
|
|
11
11
|
pitch: '+0Hz',
|
|
12
|
-
announce: { start: true, tools: true, end: true,
|
|
12
|
+
announce: { start: true, tools: true, end: true, answer: false },
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/** Merge de defaults + lo guardado (con `announce` aplanado a booleans). */
|
|
16
16
|
export function resolveTts(app) {
|
|
17
17
|
const saved = app?.tts && typeof app.tts === 'object' ? app.tts : {};
|
|
18
18
|
const announce = { ...DEFAULT_TTS.announce, ...(saved.announce ?? {}) };
|
|
19
|
-
for (const key of ['start', 'tools', 'end', '
|
|
19
|
+
for (const key of ['start', 'tools', 'end', 'answer']) announce[key] = announce[key] === true;
|
|
20
20
|
return {
|
|
21
21
|
...DEFAULT_TTS,
|
|
22
22
|
...saved,
|
|
@@ -3,7 +3,7 @@ import net from 'node:net';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { sendError } from './http.js';
|
|
6
|
-
import {
|
|
6
|
+
import { realpathDeep } from './core.js';
|
|
7
7
|
|
|
8
8
|
/** True for 127.x / ::1 / ::ffff:127.x addresses. */
|
|
9
9
|
export function isLoopback(address) {
|
|
@@ -13,19 +13,17 @@ export function isLoopback(address) {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* "Local de verdad": el par TCP es loopback (remoteAddress, no localAddress:
|
|
16
|
-
* esa es la direccion donde bindea el server, siempre 127.0.0.1)
|
|
17
|
-
*
|
|
18
|
-
* llegan desde loopback (el cliente del tunel corre en la misma maquina), asi
|
|
19
|
-
* que la ausencia de tunel es parte del check.
|
|
16
|
+
* esa es la direccion donde bindea el server, siempre 127.0.0.1). El bridge de
|
|
17
|
+
* tailscale tambien entra por loopback, asi que sigue siendo local.
|
|
20
18
|
*/
|
|
21
19
|
export function localOk(req) {
|
|
22
|
-
return isLoopback(req?.socket?.remoteAddress)
|
|
20
|
+
return isLoopback(req?.socket?.remoteAddress);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
/** `localOk` o 403. Devuelve true si la request puede seguir. */
|
|
26
24
|
export function requireLocal(req, res, mensaje = 'Solo disponible en conexiones locales.') {
|
|
27
25
|
if (localOk(req)) return true;
|
|
28
|
-
sendError(res, 403,
|
|
26
|
+
sendError(res, 403, mensaje);
|
|
29
27
|
return false;
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -35,7 +33,7 @@ export function requireLocal(req, res, mensaje = 'Solo disponible en conexiones
|
|
|
35
33
|
// victima contra 127.0.0.1 no puede ejecutar nada.
|
|
36
34
|
// ---------------------------------------------------------------------------
|
|
37
35
|
|
|
38
|
-
const TRUSTED_HOST_SUFFIXES = ['.ts.net', '.
|
|
36
|
+
const TRUSTED_HOST_SUFFIXES = ['.ts.net', '.localhost'];
|
|
39
37
|
|
|
40
38
|
/** Hostname (sin puerto, sin brackets de IPv6) del header `Host`. */
|
|
41
39
|
export function hostNameOf(hostHeader) {
|
|
@@ -60,9 +58,9 @@ function normalizeHost(host) {
|
|
|
60
58
|
|
|
61
59
|
/**
|
|
62
60
|
* Guarda DNS-rebinding: el `Host` debe ser un nombre que controlamos — una IP
|
|
63
|
-
* literal, localhost, el hostname de la maquina, un dominio de tailscale o
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* literal, localhost, el hostname de la maquina, un dominio de tailscale, o una
|
|
62
|
+
* lista explicita (`config.server.allowedHosts`). Una pagina que rebindee su
|
|
63
|
+
* dominio hacia 127.0.0.1 llega con `Host: <su dominio>` y cae aca.
|
|
66
64
|
*/
|
|
67
65
|
export function trustedHost(req, allowed = []) {
|
|
68
66
|
const host = hostNameOf(req?.headers?.host);
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
toolResultPayload,
|
|
21
21
|
} from '../lib/agentRun.js';
|
|
22
22
|
import { fraseInicio, fraseFin, fraseError, createFiltroTools } from '../lib/announcer.js';
|
|
23
|
-
import { resolveAdvisor, advisorShouldRun, consultAdvisor } from '../lib/advisor.js';
|
|
24
23
|
import { maybeAutoCompact } from '../lib/compact.js';
|
|
25
24
|
import { createRulesInjector } from '../lib/rules.js';
|
|
26
25
|
import { createCheckpointer } from '../lib/checkpoints.js';
|
|
@@ -419,48 +418,6 @@ export function registerChat(route, ctx) {
|
|
|
419
418
|
durationMs,
|
|
420
419
|
say: fraseFin({ stopReason: result.stopReason, rounds: result.rounds, toolCalls, durationMs }),
|
|
421
420
|
});
|
|
422
|
-
|
|
423
|
-
// El asesor va despues del `done` pero antes del `finally`, asi el run
|
|
424
|
-
// sigue vivo y el evento llega/persiste. Puede quedarse callado.
|
|
425
|
-
const advisorOptions = resolveAdvisor(ctx.app);
|
|
426
|
-
if (
|
|
427
|
-
result.stopReason !== 'aborted' &&
|
|
428
|
-
advisorOptions.enabled &&
|
|
429
|
-
advisorShouldRun({ toolCalls, answer: result.content })
|
|
430
|
-
) {
|
|
431
|
-
// El asesor puede tener proveedor/modelo propios; si no, usa los activos.
|
|
432
|
-
let advisorProvider = provider;
|
|
433
|
-
let advisorConfig = config;
|
|
434
|
-
let advisorModel = model;
|
|
435
|
-
if (advisorOptions.provider || advisorOptions.model) {
|
|
436
|
-
const resolved = resolveProviderFor(ctx, advisorOptions.provider || providerId);
|
|
437
|
-
advisorProvider = resolved.provider;
|
|
438
|
-
advisorConfig = resolved.config;
|
|
439
|
-
advisorModel = advisorOptions.model || model;
|
|
440
|
-
}
|
|
441
|
-
const advice = await consultAdvisor({
|
|
442
|
-
provider: advisorProvider,
|
|
443
|
-
config: advisorConfig,
|
|
444
|
-
model: advisorModel,
|
|
445
|
-
signal: ctx.runs.signal(sessionId),
|
|
446
|
-
userInput: input,
|
|
447
|
-
answer: result.content,
|
|
448
|
-
messages: newMessages,
|
|
449
|
-
options: advisorOptions,
|
|
450
|
-
});
|
|
451
|
-
// Un solo mensaje (estilo Jarvis): el asesor arranca con el resumen y
|
|
452
|
-
// sigue con la alerta/consejo en el mismo texto. Una sola locucion.
|
|
453
|
-
if (advice?.speak) {
|
|
454
|
-
ctx.store.addMessage(sessionId, {
|
|
455
|
-
role: 'assistant',
|
|
456
|
-
name: 'advisor',
|
|
457
|
-
content: advice.text,
|
|
458
|
-
mode: advice.kind,
|
|
459
|
-
});
|
|
460
|
-
ctx.runs.emit(sessionId, 'advice', { text: advice.text, kind: advice.kind, say: advice.text });
|
|
461
|
-
ctx.hooks?.dispatch({ type: 'advice', sessionId, text: advice.text, kind: advice.kind });
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
421
|
} catch (error) {
|
|
465
422
|
partials.flush();
|
|
466
423
|
if (error?.name === 'AbortError') ctx.runs.emit(sessionId, 'aborted', { say: fraseFin({ stopReason: 'aborted' }) });
|
|
@@ -3,11 +3,10 @@ import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
|
3
3
|
import { requireLocal, localOk } from '../lib/workspace.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Preview de un dev server para acceso remoto
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* liberar el tunel al apagar.
|
|
6
|
+
* Preview de un dev server para acceso remoto: publica el puerto en el tailnet
|
|
7
|
+
* (`tailscale serve --https=8443`, privado). El estado vive en memoria y se
|
|
8
|
+
* expone en `GET /api/preview` para que el cliente lo recupere tras una recarga.
|
|
9
|
+
* Devuelve `{ close }` para liberar el tunel al apagar.
|
|
11
10
|
*/
|
|
12
11
|
export function registerPreview(route, ctx) {
|
|
13
12
|
let tunnel = null;
|
|
@@ -49,23 +48,7 @@ export function registerPreview(route, ctx) {
|
|
|
49
48
|
try {
|
|
50
49
|
started = await startTunnel(port, provider, { preview: true, httpsPort });
|
|
51
50
|
} catch (error) {
|
|
52
|
-
|
|
53
|
-
// explicito (`allowPublic` o `preview.fallback: true`). Quien pidio un
|
|
54
|
-
// tunel privado no puede quedar expuesto sin saberlo.
|
|
55
|
-
const allowPublic = body.allowPublic === true || preview.fallback === true;
|
|
56
|
-
if (provider !== 'tunnelmole' && allowPublic) {
|
|
57
|
-
try {
|
|
58
|
-
started = await startTunnel(port, 'tunnelmole');
|
|
59
|
-
} catch (fallbackError) {
|
|
60
|
-
return sendError(res, 502, `No se pudo abrir el túnel: ${fallbackError.message}`);
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
return sendError(
|
|
64
|
-
res,
|
|
65
|
-
502,
|
|
66
|
-
`No se pudo abrir el túnel: ${error.message}. Si querés exponerlo con tunnelmole (público), reintentá con ese proveedor.`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
51
|
+
return sendError(res, 502, `No se pudo abrir el túnel: ${error.message}`);
|
|
69
52
|
}
|
|
70
53
|
tunnel = { url: started.url, port, provider: started.provider, stop: started.stop };
|
|
71
54
|
sendJson(res, 200, { tunnel: view(tunnel) });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadProjectProcs, killPort, portInfo } from '../lib/core.js';
|
|
2
2
|
import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
3
3
|
import { requireLocal, resolveDirOr400 } from '../lib/workspace.js';
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ export function registerProcs(route, ctx) {
|
|
|
11
11
|
if (!ctx.procsEnabled) return sendJson(res, 200, { cwd: ctx.cwd, chips: [] });
|
|
12
12
|
const dir = resolveDirOr400(res, query.get('cwd'), ctx.cwd);
|
|
13
13
|
if (!dir) return;
|
|
14
|
-
const chips =
|
|
14
|
+
const chips = loadProjectProcs(dir).map((chip) => ({ ...chip, allowed: ctx.procs.allows(chip.command) }));
|
|
15
15
|
sendJson(res, 200, { cwd: dir, chips });
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -53,8 +53,6 @@ export function registerProcs(route, ctx) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
route('POST', '/api/procs/kill', killPortHandler);
|
|
57
|
-
|
|
58
56
|
route('POST', '/api/ports/kill', killPortHandler);
|
|
59
57
|
|
|
60
58
|
route('GET', '/api/ports/:port', async ({ req, res, params }) => {
|
|
@@ -40,7 +40,7 @@ export function registerTts(route, ctx) {
|
|
|
40
40
|
if ('pitch' in body) next.pitch = sanearPitch(body.pitch, actual.pitch);
|
|
41
41
|
if (body.announce && typeof body.announce === 'object') {
|
|
42
42
|
const announce = { ...actual.announce };
|
|
43
|
-
for (const key of ['start', 'tools', 'end', '
|
|
43
|
+
for (const key of ['start', 'tools', 'end', 'answer']) {
|
|
44
44
|
if (key in body.announce) announce[key] = body.announce[key] === true;
|
|
45
45
|
}
|
|
46
46
|
next.announce = announce;
|
|
@@ -52,7 +52,6 @@ import { registerTts } from './routes/tts.js';
|
|
|
52
52
|
import { registerTranscribe } from './routes/transcribe.js';
|
|
53
53
|
import { registerUpdate } from './routes/update.js';
|
|
54
54
|
import { registerReload } from './routes/reload.js';
|
|
55
|
-
import { registerAdvisor } from './routes/advisor.js';
|
|
56
55
|
import { registerProcs } from './routes/procs.js';
|
|
57
56
|
import { registerShell } from './routes/shell.js';
|
|
58
57
|
import { registerPreview } from './routes/preview.js';
|
|
@@ -60,7 +59,6 @@ import { registerChat } from './routes/chat.js';
|
|
|
60
59
|
import { registerEvents } from './routes/events.js';
|
|
61
60
|
import { registerState } from './routes/state.js';
|
|
62
61
|
import { registerPresence } from './routes/presence.js';
|
|
63
|
-
import { registerEval } from './routes/eval.js';
|
|
64
62
|
import { registerSchedules } from './routes/schedules.js';
|
|
65
63
|
import { createScheduler } from './lib/scheduler.js';
|
|
66
64
|
import { createHookRunner } from './lib/hooks.js';
|
|
@@ -99,7 +97,7 @@ export async function createTamnoraServer({
|
|
|
99
97
|
const config = await loadConfig(cwd);
|
|
100
98
|
const app = readApp();
|
|
101
99
|
const authEnabled = Boolean(app.user && app.user.password);
|
|
102
|
-
const store = new Store(dbPath ?? databasePath(
|
|
100
|
+
const store = new Store(dbPath ?? databasePath());
|
|
103
101
|
const approvals = createApprovalBroker({ timeoutMs: config.approvalTimeoutMs });
|
|
104
102
|
const questions = createQuestionBroker({ timeoutMs: config.questionTimeoutMs });
|
|
105
103
|
const presence = createPresence();
|
|
@@ -338,7 +336,6 @@ export async function createTamnoraServer({
|
|
|
338
336
|
registerProfile(route, ctx);
|
|
339
337
|
registerTts(route, ctx);
|
|
340
338
|
registerTranscribe(route, ctx);
|
|
341
|
-
registerAdvisor(route, ctx);
|
|
342
339
|
registerUpdate(route, ctx);
|
|
343
340
|
registerReload(route, ctx);
|
|
344
341
|
registerProcs(route, ctx);
|
|
@@ -348,7 +345,6 @@ export async function createTamnoraServer({
|
|
|
348
345
|
registerEvents(route, ctx);
|
|
349
346
|
registerState(route, ctx);
|
|
350
347
|
registerPresence(route, ctx);
|
|
351
|
-
registerEval(route, ctx);
|
|
352
348
|
registerSchedules(route, ctx);
|
|
353
349
|
|
|
354
350
|
route('GET', '/api/hooks', ({ res }) => {
|
|
@@ -44,12 +44,12 @@ test('la apariencia se guarda en app.json y se valida', async () => {
|
|
|
44
44
|
const put = await fetch(`${base}/api/settings`, {
|
|
45
45
|
method: 'PUT',
|
|
46
46
|
headers: { 'Content-Type': 'application/json' },
|
|
47
|
-
body: JSON.stringify({ appearance: { theme: '
|
|
47
|
+
body: JSON.stringify({ appearance: { theme: 'github', font: 'sistema', chatFont: 18, scale: 1.1 } }),
|
|
48
48
|
});
|
|
49
49
|
assert.equal(put.status, 200);
|
|
50
50
|
const saved = (await put.json()).settings.appearance;
|
|
51
|
-
assert.deepEqual(saved, { theme: '
|
|
52
|
-
assert.equal(readApp().appearance.theme, '
|
|
51
|
+
assert.deepEqual(saved, { theme: 'github', font: 'sistema', chatFont: 18, scale: 1.1 });
|
|
52
|
+
assert.equal(readApp().appearance.theme, 'github');
|
|
53
53
|
|
|
54
54
|
const reloaded = (await (await fetch(`${base}/api/settings`)).json()).settings.appearance;
|
|
55
55
|
assert.deepEqual(reloaded, saved);
|