@danieltmn/tamnora 0.9.0 → 0.9.2

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 (27) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/harness/packages/core/src/config/app.js +2 -0
  3. package/harness/packages/core/src/index.js +1 -0
  4. package/harness/packages/core/src/stt/google.js +91 -0
  5. package/harness/packages/server/public/app.js +659 -66
  6. package/harness/packages/server/public/index.html +120 -2
  7. package/harness/packages/server/public/styles.css +19 -0
  8. package/harness/packages/server/src/lib/channels/index.js +23 -3
  9. package/harness/packages/server/src/lib/channels/telegram.js +301 -93
  10. package/harness/packages/server/src/lib/presence.js +51 -0
  11. package/harness/packages/server/src/lib/profile.js +63 -0
  12. package/harness/packages/server/src/lib/prompt.js +7 -2
  13. package/harness/packages/server/src/lib/transcribe.js +51 -0
  14. package/harness/packages/server/src/routes/channels.js +2 -0
  15. package/harness/packages/server/src/routes/presence.js +17 -0
  16. package/harness/packages/server/src/routes/profile.js +22 -0
  17. package/harness/packages/server/src/routes/providers.js +2 -0
  18. package/harness/packages/server/src/routes/transcribe.js +115 -0
  19. package/harness/packages/server/src/routes/tts.js +2 -1
  20. package/harness/packages/server/src/server.js +11 -0
  21. package/harness/packages/server/test/profile.test.js +85 -0
  22. package/harness/packages/server/test/telegram-features.test.js +241 -0
  23. package/harness/packages/server/test/telegram-voz.test.js +9 -0
  24. package/harness/packages/server/test/telegram.test.js +37 -2
  25. package/harness/packages/server/test/transcribe.test.js +215 -0
  26. package/harness/packages/server/test/tts.test.js +24 -0
  27. package/package.json +1 -1
@@ -34,7 +34,7 @@
34
34
  </head>
35
35
  <body>
36
36
  <header class="topbar">
37
- <div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.9.0</span></div>
37
+ <div class="brand"><span class="logo"><span class="logo-t">T</span><span class="logo-h">H</span></span> <span class="ver">v0.9.2</span></div>
38
38
  <nav class="tabs">
39
39
  <button class="tab active" data-tab="chat">chat</button>
40
40
  <button class="tab" data-tab="providers">proveedores</button>
@@ -139,7 +139,6 @@
139
139
  <form id="chat-form" class="composer">
140
140
  <div id="slash-menu" class="slash-menu" hidden></div>
141
141
  <div id="toasts" class="toasts" aria-live="polite"></div>
142
- <div id="perm-banner" class="perm-banner" role="alert" aria-live="assertive" hidden></div>
143
142
  <div id="attach-preview" class="attach-preview" hidden></div>
144
143
  <div id="chat-queue" class="chat-queue" hidden></div>
145
144
  <div class="composer-row">
@@ -151,6 +150,14 @@
151
150
  <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
152
151
  </svg>
153
152
  </button>
153
+ <button type="button" id="mic-btn" class="btn icon" title="Dictar por voz (tocá para grabar y parar)" aria-label="dictar por voz">
154
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
155
+ <path d="M12 19v3" />
156
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
157
+ <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z" />
158
+ </svg>
159
+ <span class="mic-count" hidden></span>
160
+ </button>
154
161
  <input id="image-input" type="file" accept="image/*" multiple hidden />
155
162
  <textarea id="chat-input" rows="1" placeholder="escribí un mensaje…" title="Enter envía · Shift+Enter salto" spellcheck="false"></textarea>
156
163
  <button class="btn primary" type="submit">enviar</button>
@@ -402,11 +409,13 @@
402
409
  <div class="ajustes-wrap">
403
410
  <nav class="ajustes-tabs" role="tablist" aria-label="secciones de ajustes">
404
411
  <button type="button" class="ajustes-tab active" data-panel="agente" role="tab">Agente</button>
412
+ <button type="button" class="ajustes-tab" data-panel="perfil" role="tab">Perfil</button>
405
413
  <button type="button" class="ajustes-tab" data-panel="sesiones" role="tab">Sesiones</button>
406
414
  <button type="button" class="ajustes-tab" data-panel="mcp" role="tab">MCP</button>
407
415
  <button type="button" class="ajustes-tab" data-panel="skills" role="tab">Skills</button>
408
416
  <button type="button" class="ajustes-tab" data-panel="telegram" role="tab">Telegram</button>
409
417
  <button type="button" class="ajustes-tab" data-panel="voz" role="tab">Voz</button>
418
+ <button type="button" class="ajustes-tab" data-panel="transcripcion" role="tab">Transcripción</button>
410
419
  <button type="button" class="ajustes-tab" data-panel="limites" role="tab">Límites</button>
411
420
  </nav>
412
421
 
@@ -436,6 +445,38 @@
436
445
  </div>
437
446
  </div>
438
447
 
448
+ <div class="panel ajustes-panel" data-panel="perfil">
449
+ <div class="row between">
450
+ <h2>Perfil</h2>
451
+ <span id="perfil-status" class="summary">cargando…</span>
452
+ </div>
453
+ <p class="muted verify-line">
454
+ Datos que el agente conoce de vos. Viven en <code>app.json</code> (la casa) y se
455
+ inyectan en el prompt de todas las corridas.
456
+ </p>
457
+ <label class="field"><span>cómo querés que te llame</span>
458
+ <input id="perfil-address" spellcheck="false" placeholder="Señor · Amo · Amigo · Compañero · Daniel…" />
459
+ </label>
460
+ <label class="field"><span>nombre</span>
461
+ <input id="perfil-name" spellcheck="false" placeholder="Daniel" />
462
+ </label>
463
+ <label class="field"><span>idioma preferido</span>
464
+ <input id="perfil-language" spellcheck="false" placeholder="es" />
465
+ </label>
466
+ <label class="field"><span>notas</span>
467
+ <textarea id="perfil-notes" rows="3" placeholder="Preferencias, contexto, cómo te gusta trabajar…"></textarea>
468
+ </label>
469
+ <div class="row between">
470
+ <span class="muted">datos extra</span>
471
+ <button type="button" id="perfil-add-field" class="btn">+ agregar</button>
472
+ </div>
473
+ <div id="perfil-fields" class="perfil-fields"></div>
474
+ <div class="row">
475
+ <button id="perfil-save" class="btn primary" type="button">guardar</button>
476
+ <span id="perfil-msg" class="summary"></span>
477
+ </div>
478
+ </div>
479
+
439
480
  <div class="panel ajustes-panel" data-panel="sesiones">
440
481
  <div class="row between">
441
482
  <h2>Sesiones</h2>
@@ -513,6 +554,27 @@
513
554
  <label class="field"><span>modelo de transcripción (voz)</span>
514
555
  <input id="tg-transcribe" spellcheck="false" placeholder="whisper-1" />
515
556
  </label>
557
+ <label class="check" title="avisar y pedir permisos por Telegram de los proyectos abiertos en la consola">
558
+ <input id="tg-notify" type="checkbox" /> Avisar por Telegram (proyectos abiertos en la consola)
559
+ </label>
560
+ <div class="voz-announce">
561
+ <span class="muted">qué avisa (siempre con el nombre del proyecto):</span>
562
+ <label class="check" title="resumen al terminar un turno">
563
+ <input id="tg-notify-done" type="checkbox" /> el fin de turno
564
+ </label>
565
+ <label class="check" title="cuando un run falla">
566
+ <input id="tg-notify-error" type="checkbox" /> los errores
567
+ </label>
568
+ <label class="check" title="cuando el agente pide una aprobación">
569
+ <input id="tg-notify-permission" type="checkbox" /> los permisos
570
+ </label>
571
+ <label class="check" title="resultado de subagentes (tool task)">
572
+ <input id="tg-notify-agent" type="checkbox" /> los subagentes
573
+ </label>
574
+ </div>
575
+ <p class="muted verify-line">
576
+ Solo avisa de los proyectos que tengas <b>abiertos como pestañas</b> en la consola.
577
+ </p>
516
578
  <details class="tg-help">
517
579
  <summary>¿Cómo habilito mi chat?</summary>
518
580
  <ol>
@@ -575,6 +637,62 @@
575
637
  </div>
576
638
  </div>
577
639
 
640
+ <div class="panel ajustes-panel" data-panel="transcripcion">
641
+ <div class="row between">
642
+ <h2>Transcripción</h2>
643
+ <span id="tr-status" class="summary">cargando…</span>
644
+ </div>
645
+ <p class="muted verify-line">
646
+ Audio → texto para el <b>dictado del chat</b> (micrófono del input) y las notas de voz
647
+ de Telegram.
648
+ </p>
649
+ <label class="field"><span>motor</span>
650
+ <select id="tr-mode" title="cómo se transcribe el dictado del chat">
651
+ <option value="server">Servidor · proveedor de transcripción</option>
652
+ <option value="google">Servidor · Google (sin API key)</option>
653
+ <option value="browser">Navegador · nativo (Web Speech API)</option>
654
+ </select>
655
+ </label>
656
+ <label class="field"><span>idioma del dictado (se le indica al motor para no confundirse)</span>
657
+ <input id="tr-language" spellcheck="false" placeholder="es-AR" />
658
+ </label>
659
+ <div id="tr-server-fields">
660
+ <label class="field"><span>proveedor de transcripción (vacío = proveedor activo)</span>
661
+ <input id="tr-provider" spellcheck="false" placeholder="groq · openai" />
662
+ </label>
663
+ <label class="field"><span>modelo</span>
664
+ <input id="tr-model" spellcheck="false" placeholder="whisper-large-v3-turbo" />
665
+ </label>
666
+ <p class="muted verify-line">
667
+ El proveedor activo no siempre sabe transcribir (p. ej. DeepSeek), así que conviene
668
+ apuntar a uno con <code>/audio/transcriptions</code> (Groq, OpenAI…). Si lo dejás
669
+ vacío se usa lo configurado en <b>Telegram</b> y, si tampoco hay, el proveedor activo
670
+ con <code>whisper-1</code>.
671
+ </p>
672
+ </div>
673
+ <p class="muted verify-line" id="tr-google-note" hidden>
674
+ Usa el servicio de voz de Chromium/Google <b>desde el servidor</b>, sin API key ni
675
+ depender del navegador (servicio no oficial, como el TTS de Edge). Configurá el idioma.
676
+ </p>
677
+ <p class="muted verify-line" id="tr-browser-note" hidden>
678
+ El navegador transcribe en vivo (Chrome/Edge) sin usar el servidor. Necesita el
679
+ servicio de voz de Google; en Brave o sin internet suele fallar (error «network»).
680
+ </p>
681
+ <div class="row">
682
+ <button id="tr-save" class="btn primary" type="button">guardar</button>
683
+ <button id="tr-test" class="btn icon" type="button" title="grabar y ver qué devuelve la transcripción" aria-label="probar transcripción">
684
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
685
+ <path d="M12 19v3" />
686
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
687
+ <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z" />
688
+ </svg>
689
+ <span class="mic-count" hidden></span>
690
+ </button>
691
+ <span id="tr-msg" class="summary"></span>
692
+ </div>
693
+ <p id="tr-test-result" class="summary"></p>
694
+ </div>
695
+
578
696
  <div class="panel ajustes-panel" data-panel="limites">
579
697
  <div class="row between">
580
698
  <h2>Límites del agente</h2>
@@ -762,9 +762,28 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
762
762
  place-items: center;
763
763
  flex: 0 0 auto;
764
764
  line-height: 1;
765
+ position: relative;
765
766
  }
766
767
  .btn.icon svg { width: 18px; height: 18px; display: block; }
767
768
  .btn.icon:hover { color: var(--accent); }
769
+ /* Dictado activo: verde titilando (cuenta regresiva y grabación). */
770
+ .btn.icon.recording { color: #2eb872; border-color: currentColor; animation: live-pulse 1s ease-in-out infinite; }
771
+ .btn.icon.busy { opacity: 0.55; }
772
+ .btn.icon.mic-counting > svg { visibility: hidden; }
773
+ .mic-count {
774
+ position: absolute;
775
+ inset: 0;
776
+ display: grid;
777
+ place-items: center;
778
+ font: 700 13px var(--mono);
779
+ color: #2eb872;
780
+ }
781
+ .mic-count[hidden] { display: none; }
782
+
783
+ .perfil-fields { display: flex; flex-direction: column; gap: 6px; }
784
+ .perfil-field { display: flex; gap: 6px; align-items: center; }
785
+ .perfil-field .pf-key { flex: 0 0 34%; }
786
+ .perfil-field .pf-value { flex: 1; }
768
787
 
769
788
  .chat-queue { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; border: 1px dashed var(--border); border-radius: 9px; background: var(--theirs); }
770
789
  .chat-queue[hidden] { display: none; }
@@ -19,6 +19,16 @@ function parseAllowed(raw) {
19
19
  .filter(Boolean);
20
20
  }
21
21
 
22
+ const NOTIFY_EVENTS = ['done', 'error', 'permission', 'agent'];
23
+
24
+ /** Avisos por Telegram: interruptor global + que eventos llegan. */
25
+ export function sanitizeNotify(raw) {
26
+ const value = raw && typeof raw === 'object' ? raw : {};
27
+ const notify = { enabled: value.enabled === true };
28
+ for (const key of NOTIFY_EVENTS) notify[key] = value[key] !== false;
29
+ return notify;
30
+ }
31
+
22
32
  /**
23
33
  * Crea el manager de canales. `load()` arranca (o reinicia) los canales segun
24
34
  * la config efectiva (app.json + tamnora.json + entorno), `status()` la resume
@@ -37,6 +47,7 @@ export function createChannelsManager(ctx, { log = () => {}, enabled = true } =
37
47
  if (Array.isArray(cfg.allowedChatIds) && cfg.allowedChatIds.length) tg.allowedChatIds = cfg.allowedChatIds;
38
48
  if (cfg.transcribeModel) tg.transcribeModel = cfg.transcribeModel;
39
49
  if (cfg.transcribeProvider) tg.transcribeProvider = cfg.transcribeProvider;
50
+ if (cfg.notify) tg.notify = cfg.notify;
40
51
  return tg;
41
52
  };
42
53
 
@@ -49,6 +60,7 @@ export function createChannelsManager(ctx, { log = () => {}, enabled = true } =
49
60
  allowedChatIds,
50
61
  transcribeModel: tg.transcribeModel,
51
62
  transcribeProvider: tg.transcribeProvider,
63
+ notify: sanitizeNotify(tg.notify),
52
64
  };
53
65
  }
54
66
 
@@ -62,9 +74,16 @@ export function createChannelsManager(ctx, { log = () => {}, enabled = true } =
62
74
  function load() {
63
75
  stop();
64
76
  if (!enabled) return { enabled: false, channels: [] };
65
- const { token, allowedChatIds, transcribeModel, transcribeProvider } = effective();
77
+ const { token, allowedChatIds, transcribeModel, transcribeProvider, notify } = effective();
66
78
  const channels = [];
67
- const telegram = startTelegramChannel(ctx, { token, allowedChatIds, transcribeModel, transcribeProvider, log });
79
+ const telegram = startTelegramChannel(ctx, {
80
+ token,
81
+ allowedChatIds,
82
+ transcribeModel,
83
+ transcribeProvider,
84
+ notify,
85
+ log,
86
+ });
68
87
  if (telegram) channels.push(telegram);
69
88
  const unsubscribe = ctx.runs?.subscribeAll?.((sessionId, event) => {
70
89
  for (const channel of channels) {
@@ -81,13 +100,14 @@ export function createChannelsManager(ctx, { log = () => {}, enabled = true } =
81
100
  }
82
101
 
83
102
  function status() {
84
- const { token, allowedChatIds, transcribeModel, transcribeProvider } = effective();
103
+ const { token, allowedChatIds, transcribeModel, transcribeProvider, notify } = effective();
85
104
  return {
86
105
  tokenSet: Boolean(token),
87
106
  tokenHint: tokenHint(token),
88
107
  allowedChatIds,
89
108
  transcribeModel: transcribeModel || 'whisper-1',
90
109
  transcribeProvider: transcribeProvider || '',
110
+ notify,
91
111
  running: Boolean(active && active.channels.length),
92
112
  };
93
113
  }