@danieltmn/tamnora 0.25.1 → 0.26.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 (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +13 -25
  3. package/SECURITY.md +3 -4
  4. package/harness/packages/cli/src/cli.js +0 -3
  5. package/harness/packages/cli/src/commands/bridge.js +0 -1
  6. package/harness/packages/cli/src/commands/repl.js +3 -3
  7. package/harness/packages/cli/src/commands/run.js +1 -1
  8. package/harness/packages/cli/src/commands/server.js +1 -1
  9. package/harness/packages/cli/src/commands/tui.js +1 -1
  10. package/harness/packages/cli/src/lib/util.js +2 -2
  11. package/harness/packages/cli/src/tui/app.js +2 -2
  12. package/harness/packages/cli/test/tunnel.test.js +5 -32
  13. package/harness/packages/core/src/agents/index.js +13 -41
  14. package/harness/packages/core/src/config/app.js +0 -4
  15. package/harness/packages/core/src/config/home.js +1 -20
  16. package/harness/packages/core/src/config/index.js +5 -8
  17. package/harness/packages/core/src/extensions/load.js +22 -0
  18. package/harness/packages/core/src/frontmatter.js +32 -0
  19. package/harness/packages/core/src/hooks/index.js +2 -16
  20. package/harness/packages/core/src/index.js +2 -14
  21. package/harness/packages/core/src/plugins/index.js +2 -16
  22. package/harness/packages/core/src/procs/project.js +68 -0
  23. package/harness/packages/core/src/procs/static.js +17 -0
  24. package/harness/packages/core/src/rules/index.js +8 -27
  25. package/harness/packages/core/src/skills/index.js +8 -19
  26. package/harness/packages/core/src/storage/sqlite.js +595 -690
  27. package/harness/packages/core/src/tunnel.js +9 -147
  28. package/harness/packages/core/test/home.test.js +2 -35
  29. package/harness/packages/core/test/procs.test.js +3 -64
  30. package/harness/packages/core/test/storage.test.js +1 -12
  31. package/harness/packages/server/public/app.js +2 -19
  32. package/harness/packages/server/public/appearance.js +2 -56
  33. package/harness/packages/server/public/boot.js +2 -2
  34. package/harness/packages/server/public/cards.js +2 -21
  35. package/harness/packages/server/public/chat.js +0 -16
  36. package/harness/packages/server/public/chatFormat.js +0 -11
  37. package/harness/packages/server/public/chrome.js +2 -5
  38. package/harness/packages/server/public/commands.js +1 -2
  39. package/harness/packages/server/public/explorer.js +7 -10
  40. package/harness/packages/server/public/icons/icon-128x128.png +0 -0
  41. package/harness/packages/server/public/icons/icon-144x144.png +0 -0
  42. package/harness/packages/server/public/icons/icon-152x152.png +0 -0
  43. package/harness/packages/server/public/icons/icon-192x192.png +0 -0
  44. package/harness/packages/server/public/icons/icon-384x384.png +0 -0
  45. package/harness/packages/server/public/icons/icon-512x512.png +0 -0
  46. package/harness/packages/server/public/icons/icon-72x72.png +0 -0
  47. package/harness/packages/server/public/icons/icon-96x96.png +0 -0
  48. package/harness/packages/server/public/icons/logo.png +0 -0
  49. package/harness/packages/server/public/icons/logo_light.png +0 -0
  50. package/harness/packages/server/public/index.html +1228 -1326
  51. package/harness/packages/server/public/landing/boot.js +2 -3
  52. package/harness/packages/server/public/landing/index.html +3 -367
  53. package/harness/packages/server/public/logo.png +0 -0
  54. package/harness/packages/server/public/settings.js +1255 -1322
  55. package/harness/packages/server/public/settingsNav.js +0 -1
  56. package/harness/packages/server/public/styles.css +2341 -2867
  57. package/harness/packages/server/public/system.js +356 -495
  58. package/harness/packages/server/public/voice.js +6 -22
  59. package/harness/packages/server/src/auth.js +4 -15
  60. package/harness/packages/server/src/cli.js +1 -1
  61. package/harness/packages/server/src/lib/agents.js +13 -27
  62. package/harness/packages/server/src/lib/appearance.js +3 -22
  63. package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
  64. package/harness/packages/server/src/lib/definitions.js +40 -0
  65. package/harness/packages/server/src/lib/skills.js +14 -21
  66. package/harness/packages/server/src/lib/ttsSettings.js +2 -2
  67. package/harness/packages/server/src/lib/workspace.js +9 -11
  68. package/harness/packages/server/src/routes/chat.js +0 -43
  69. package/harness/packages/server/src/routes/preview.js +5 -22
  70. package/harness/packages/server/src/routes/procs.js +2 -4
  71. package/harness/packages/server/src/routes/tts.js +1 -1
  72. package/harness/packages/server/src/server.js +1 -5
  73. package/harness/packages/server/test/appearance-route.test.js +3 -3
  74. package/harness/packages/server/test/appearance.test.js +12 -38
  75. package/harness/packages/server/test/chatFormat.test.js +0 -8
  76. package/harness/packages/server/test/consoleTabs.test.js +4 -4
  77. package/harness/packages/server/test/ops.test.js +8 -56
  78. package/harness/packages/server/test/preview.test.js +2 -42
  79. package/harness/packages/server/test/security.test.js +194 -370
  80. package/harness/packages/server/test/settingsNav.test.js +2 -2
  81. package/harness/packages/server/test/tts.test.js +4 -4
  82. package/harness/tamnora.example.json +1 -1
  83. package/package.json +2 -2
  84. package/harness/packages/cli/src/commands/eval.js +0 -186
  85. package/harness/packages/core/src/eval/dataset.js +0 -47
  86. package/harness/packages/core/src/eval/judge.js +0 -39
  87. package/harness/packages/core/src/eval/report.js +0 -48
  88. package/harness/packages/core/src/eval/runTask.js +0 -62
  89. package/harness/packages/core/src/eval/runner.js +0 -153
  90. package/harness/packages/core/src/eval/scorers.js +0 -217
  91. package/harness/packages/core/src/eval/task.js +0 -59
  92. package/harness/packages/core/src/procs/detect.js +0 -149
  93. package/harness/packages/core/test/eval.test.js +0 -144
  94. package/harness/packages/server/public/icons (1).zip +0 -0
  95. package/harness/packages/server/src/lib/advisor.js +0 -198
  96. package/harness/packages/server/src/routes/advisor.js +0 -79
  97. package/harness/packages/server/src/routes/eval.js +0 -100
  98. package/harness/packages/server/test/advisor.test.js +0 -249
  99. 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, advice: true, answer: false, summary: false },
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 (y el asesor) aunque la narracion este apagada en Ajustes.
773
+ // respuesta final aunque la narracion este apagada en Ajustes.
776
774
  if (handsFreeOn) {
777
- if (event === 'advice') {
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
- // El asesor y la respuesta final son el cierre del turno: si quedan en cola
792
- // detras de un audio largo, igual hay que leerlos (sin vencimiento).
793
- else if (event === 'advice') {
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 tunel el server recibe desde loopback: solo ahi confiamos en
42
- // X-Forwarded-Proto para marcar la cookie Secure. Como el cliente del tunel
43
- // corre en la misma maquina, un request por el tunel es indistinguible de uno
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
- if (proto === 'https') return true;
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 + evaluación) en localhost.
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 { rm } from 'node:fs/promises';
2
+ import { homeDir, loadAgents, mergeAgents, buildAgentsIndex, writeAgentFile } from './core.js';
3
3
  import {
4
- homeDir,
5
- loadAgents,
6
- mergeAgents,
7
- buildAgentsIndex,
8
- writeAgentFile,
9
- } from './core.js';
4
+ DEFINITION_ID_PATTERN,
5
+ allDefinitions,
6
+ disabledDefinitionIds,
7
+ removeDefinition,
8
+ setDefinitionEnabled,
9
+ } from './definitions.js';
10
10
 
11
- export const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
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
- const home = path.join(homeDir(), 'agents');
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 = disabledIds(ctx.store);
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 = disabledIds(ctx.store);
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 rm(path.join(homeDir(), 'agents', String(id)), { recursive: true, force: true });
55
+ await removeDefinition('agents', id);
67
56
  }
68
57
 
69
58
  export function setAgentEnabled(store, id, enabled) {
70
- const current = new Set(Array.isArray(store.getSetting('disabledAgents')) ? store.getSetting('disabledAgents') : []);
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', 'plex', 'inter', 'jetbrains', 'fira', 'source'];
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
- const legacy = id === 'light' ? 'paper' : id;
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' || cmd === 'strict') {
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, rm, writeFile } from 'node:fs/promises';
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 = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
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
- const home = path.join(homeDir(), 'skills');
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 = disabledIds(ctx.store);
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 = disabledIds(ctx.store);
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 rm(path.join(homeDir(), 'skills', id), { recursive: true, force: true });
81
+ await removeDefinition('skills', id);
86
82
  }
87
83
 
88
84
  export function setSkillEnabled(store, id, enabled) {
89
- const current = new Set(Array.isArray(store.getSetting('disabledSkills')) ? store.getSetting('disabledSkills') : []);
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, advice: true, answer: false, summary: false },
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', 'advice', 'answer', 'summary']) announce[key] = announce[key] === true;
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 { tunnelState, realpathDeep } from './core.js';
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) y no hay un
17
- * tunel publico activo. Con el tunel abierto las peticiones remotas TAMBIEN
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) && !tunnelState();
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, tunnelState() ? `${mensaje} Hay un túnel público activo.` : mensaje);
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', '.tunnelmole.net', '.localhost'];
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 del
64
- * tunel, o una lista explicita (`config.server.allowedHosts`). Una pagina que
65
- * rebindee su dominio hacia 127.0.0.1 llega con `Host: <su dominio>` y cae aca.
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. Con `tailscale` publica el puerto
7
- * en el tailnet (`tailscale serve --https=8443`, privado); con `tunnelmole` abre
8
- * un tunel publico. El estado vive en memoria y se expone en `GET /api/preview`
9
- * para que el cliente lo recupere tras una recarga. Devuelve `{ close }` para
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
- // El fallback a tunnelmole es un tunel PUBLICO: solo con consentimiento
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 { detectProcs, killPort, portInfo } from '../lib/core.js';
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 = detectProcs(dir).map((chip) => ({ ...chip, allowed: ctx.procs.allows(chip.command) }));
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', 'advice', 'answer', 'summary']) {
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(cwd));
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: 'nord', font: 'plex', chatFont: 18, scale: 1.1 } }),
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: 'nord', font: 'plex', chatFont: 18, scale: 1.1 });
52
- assert.equal(readApp().appearance.theme, 'nord');
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);