@danieltmn/tamnora 0.17.0 → 0.19.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 +45 -0
- package/README.md +37 -3
- package/SECURITY.md +8 -0
- package/harness/packages/cli/src/commands/mcp.js +31 -4
- package/harness/packages/cli/src/commands/repl.js +7 -2
- package/harness/packages/cli/src/commands/run.js +5 -2
- package/harness/packages/cli/src/commands/tui.js +9 -2
- package/harness/packages/cli/src/lib/runtime.js +23 -1
- package/harness/packages/cli/test/tunnel.test.js +13 -1
- package/harness/packages/core/src/agent/modes.js +3 -0
- package/harness/packages/core/src/config/index.js +3 -1
- package/harness/packages/core/src/index.js +15 -1
- package/harness/packages/core/src/mcp/catalog.js +79 -0
- package/harness/packages/core/src/mcp/index.js +14 -2
- package/harness/packages/core/src/mcp/registry.js +29 -1
- package/harness/packages/core/src/redact.js +25 -0
- package/harness/packages/core/src/skills/catalog.js +78 -0
- package/harness/packages/core/src/skills/remote.js +51 -0
- package/harness/packages/core/src/tools/executors.js +95 -4
- package/harness/packages/core/src/tools/index.js +27 -6
- package/harness/packages/core/src/tools/labels.js +2 -0
- package/harness/packages/core/src/tools/mcp.js +91 -0
- package/harness/packages/core/src/tools/permissions.js +53 -8
- package/harness/packages/core/src/tools/registry.js +14 -7
- package/harness/packages/core/src/tools/schema.js +87 -0
- package/harness/packages/core/src/tts/edge.js +28 -7
- package/harness/packages/core/src/tunnel.js +51 -5
- package/harness/packages/core/test/compact.test.js +2 -0
- package/harness/packages/core/test/labels.test.js +1 -0
- package/harness/packages/core/test/mcp-registry.test.js +37 -1
- package/harness/packages/core/test/mcp-routing.test.js +115 -0
- package/harness/packages/core/test/redact.test.js +22 -0
- package/harness/packages/core/test/skills.test.js +11 -0
- package/harness/packages/core/test/tools.test.js +97 -3
- package/harness/packages/server/public/app.js +567 -33
- package/harness/packages/server/public/index.html +51 -7
- package/harness/packages/server/public/styles.css +124 -4
- package/harness/packages/server/src/lib/agentTools.js +45 -3
- package/harness/packages/server/src/lib/channels/telegram.js +10 -3
- package/harness/packages/server/src/lib/prompt.js +4 -1
- package/harness/packages/server/src/lib/questions.js +72 -0
- package/harness/packages/server/src/lib/scheduler.js +12 -1
- package/harness/packages/server/src/lib/tasks.js +3 -1
- package/harness/packages/server/src/lib/ttsSettings.js +11 -0
- package/harness/packages/server/src/routes/chat.js +31 -2
- package/harness/packages/server/src/routes/mcp.js +54 -2
- package/harness/packages/server/src/routes/preview.js +49 -28
- package/harness/packages/server/src/routes/providers.js +12 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/routes/skills.js +48 -1
- package/harness/packages/server/src/routes/state.js +8 -1
- package/harness/packages/server/src/routes/tts.js +4 -2
- package/harness/packages/server/src/server.js +37 -1
- package/harness/packages/server/test/preview.test.js +111 -0
- package/harness/packages/server/test/server.test.js +186 -19
- package/harness/packages/server/test/telegram-features.test.js +2 -0
- package/harness/packages/server/test/telegram-voz.test.js +2 -0
- package/harness/packages/server/test/telegram.test.js +2 -0
- package/harness/packages/server/test/tts.test.js +5 -1
- package/harness/tamnora.example.json +3 -1
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</head>
|
|
38
38
|
<body>
|
|
39
39
|
<header class="topbar">
|
|
40
|
-
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.
|
|
40
|
+
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.19.0</span></div>
|
|
41
41
|
<nav class="tabs">
|
|
42
42
|
<button class="tab active" data-tab="chat">chat</button>
|
|
43
43
|
<button class="tab" data-tab="providers">proveedores</button>
|
|
@@ -270,7 +270,12 @@
|
|
|
270
270
|
<div class="xp-bar">
|
|
271
271
|
<input id="preview-url" placeholder="http://127.0.0.1:5173" spellcheck="false" />
|
|
272
272
|
<button id="preview-open" class="btn" type="button">abrir</button>
|
|
273
|
-
<
|
|
273
|
+
<select id="preview-provider" title="cómo exponer el puerto para acceso remoto" aria-label="proveedor de acceso remoto">
|
|
274
|
+
<option value="tailscale">tailscale</option>
|
|
275
|
+
<option value="tunnelmole">público</option>
|
|
276
|
+
</select>
|
|
277
|
+
<button id="preview-tunnel" class="btn" type="button" title="Exponer el puerto para acceso remoto (tailscale privado o túnel público)">túnel</button>
|
|
278
|
+
<button id="preview-close" class="btn danger" type="button" hidden title="Cerrar el túnel/exposición remota">cerrar</button>
|
|
274
279
|
</div>
|
|
275
280
|
<div class="preview-tools">
|
|
276
281
|
<select id="preview-device" title="tamaño de pantalla" aria-label="tamaño de pantalla">
|
|
@@ -564,12 +569,36 @@
|
|
|
564
569
|
<p class="muted verify-line">
|
|
565
570
|
Se combinan con <code>mcp[]</code> de <code>tamnora.json</code>; los guardados acá viven en la base.
|
|
566
571
|
</p>
|
|
572
|
+
<div class="import-bar">
|
|
573
|
+
<label class="field grow"><span>carga de herramientas MCP</span>
|
|
574
|
+
<select id="mcp-mode">
|
|
575
|
+
<option value="auto">auto (diferir si hay muchas)</option>
|
|
576
|
+
<option value="lazy">diferido (índice + mcp_call)</option>
|
|
577
|
+
<option value="eager">completas (todas en cada turno)</option>
|
|
578
|
+
</select>
|
|
579
|
+
</label>
|
|
580
|
+
<span id="mcp-count" class="muted"></span>
|
|
581
|
+
</div>
|
|
582
|
+
<div class="import-bar">
|
|
583
|
+
<input id="mcp-pinned" placeholder="tools/servidores fijos (coma), ej. mcp__postgres__query" spellcheck="false" />
|
|
584
|
+
<button id="mcp-pinned-save" class="btn" type="button">fijar</button>
|
|
585
|
+
</div>
|
|
567
586
|
<div class="import-bar">
|
|
568
587
|
<input id="mcp-import" placeholder="nombre del registro oficial, URL o JSON (bloque mcpServers)" spellcheck="false" />
|
|
569
588
|
<button id="mcp-import-btn" class="btn" type="button">importar</button>
|
|
570
589
|
</div>
|
|
590
|
+
<div class="import-bar">
|
|
591
|
+
<input id="mcp-search" placeholder="buscar en el registro oficial (ej. github, postgres)" spellcheck="false" />
|
|
592
|
+
<button id="mcp-search-btn" class="btn" type="button">buscar</button>
|
|
593
|
+
</div>
|
|
571
594
|
<div id="mcp-import-status" class="summary"></div>
|
|
595
|
+
<div id="mcp-search-status" class="summary"></div>
|
|
596
|
+
<ul id="mcp-search-results" class="providers"></ul>
|
|
572
597
|
<ul id="mcp-list" class="providers"></ul>
|
|
598
|
+
<details class="catalog">
|
|
599
|
+
<summary>sugeridos</summary>
|
|
600
|
+
<ul id="mcp-catalog" class="providers"></ul>
|
|
601
|
+
</details>
|
|
573
602
|
</div>
|
|
574
603
|
|
|
575
604
|
<div class="panel ajustes-panel" data-panel="skills">
|
|
@@ -584,8 +613,18 @@
|
|
|
584
613
|
<input id="skill-import" placeholder="https://skills.sh/owner/repo/skill" spellcheck="false" />
|
|
585
614
|
<button id="skill-import-btn" class="btn" type="button">importar</button>
|
|
586
615
|
</div>
|
|
616
|
+
<div class="import-bar">
|
|
617
|
+
<input id="skill-search" placeholder="buscar skills en GitHub (ej. testing, react)" spellcheck="false" />
|
|
618
|
+
<button id="skill-search-btn" class="btn" type="button">buscar</button>
|
|
619
|
+
</div>
|
|
587
620
|
<div id="skill-import-status" class="summary"></div>
|
|
621
|
+
<div id="skill-search-status" class="summary"></div>
|
|
622
|
+
<ul id="skill-search-results" class="providers"></ul>
|
|
588
623
|
<ul id="skill-list" class="providers"></ul>
|
|
624
|
+
<details class="catalog">
|
|
625
|
+
<summary>sugeridas</summary>
|
|
626
|
+
<ul id="skill-catalog" class="providers"></ul>
|
|
627
|
+
</details>
|
|
589
628
|
</div>
|
|
590
629
|
|
|
591
630
|
<div class="panel ajustes-panel" data-panel="memoria">
|
|
@@ -697,11 +736,16 @@
|
|
|
697
736
|
<select id="voz-voice" title="voz del catálogo de Edge"></select>
|
|
698
737
|
</label>
|
|
699
738
|
<label class="field"><span>velocidad</span>
|
|
700
|
-
<
|
|
701
|
-
<
|
|
702
|
-
<
|
|
703
|
-
|
|
704
|
-
|
|
739
|
+
<div class="voz-range">
|
|
740
|
+
<input id="voz-rate" type="range" min="-100" max="100" step="5" value="0" title="velocidad del habla (%)" />
|
|
741
|
+
<output id="voz-rate-val" for="voz-rate">normal</output>
|
|
742
|
+
</div>
|
|
743
|
+
</label>
|
|
744
|
+
<label class="field"><span>tono</span>
|
|
745
|
+
<div class="voz-range">
|
|
746
|
+
<input id="voz-pitch" type="range" min="-50" max="50" step="5" value="0" title="tono de la voz (Hz)" />
|
|
747
|
+
<output id="voz-pitch-val" for="voz-pitch">normal</output>
|
|
748
|
+
</div>
|
|
705
749
|
</label>
|
|
706
750
|
<div class="voz-announce">
|
|
707
751
|
<span class="muted">qué cuenta:</span>
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
--accent-soft: rgba(217, 112, 74, 0.18);
|
|
16
16
|
--ok: #8fae7f;
|
|
17
17
|
--warn: #d9a441;
|
|
18
|
+
--info: #6f9fd8;
|
|
19
|
+
--danger-soft: rgba(224, 112, 92, 0.16);
|
|
20
|
+
--ok-soft: rgba(143, 174, 127, 0.16);
|
|
21
|
+
--warn-soft: rgba(217, 164, 65, 0.16);
|
|
22
|
+
--info-soft: rgba(111, 159, 216, 0.16);
|
|
23
|
+
--muted-soft: rgba(156, 151, 137, 0.14);
|
|
18
24
|
--solid: #12130b;
|
|
19
25
|
--primary-fg: #fff7ef;
|
|
20
26
|
--sans: Manrope, system-ui, "Segoe UI", Arial, sans-serif;
|
|
@@ -46,6 +52,12 @@ html[data-theme='paper'] {
|
|
|
46
52
|
--accent-soft: rgba(189, 84, 47, 0.1);
|
|
47
53
|
--ok: #66865d;
|
|
48
54
|
--warn: #8a5a00;
|
|
55
|
+
--info: #3f6ea6;
|
|
56
|
+
--danger-soft: rgba(147, 64, 34, 0.1);
|
|
57
|
+
--ok-soft: rgba(102, 134, 93, 0.14);
|
|
58
|
+
--warn-soft: rgba(138, 90, 0, 0.12);
|
|
59
|
+
--info-soft: rgba(63, 110, 166, 0.12);
|
|
60
|
+
--muted-soft: rgba(104, 104, 93, 0.12);
|
|
49
61
|
--solid: #f7f2e6;
|
|
50
62
|
--primary-fg: #fbf6ec;
|
|
51
63
|
--page-bg: #f5efe2;
|
|
@@ -810,7 +822,7 @@ body.resizing * { cursor: col-resize !important; }
|
|
|
810
822
|
}
|
|
811
823
|
.perm-card .perm-head { display: flex; gap: 7px; align-items: center; color: var(--warn); font-weight: 700; }
|
|
812
824
|
.perm-card .perm-args { color: var(--muted); margin: 5px 0 9px; white-space: pre-wrap; word-break: break-word; max-height: 7.5em; overflow: auto; }
|
|
813
|
-
.perm-card .perm-actions { display: flex; align-items: center; gap: 8px; }
|
|
825
|
+
.perm-card .perm-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
814
826
|
.perm-card .spacer { flex: 1; }
|
|
815
827
|
.perm-card.approved { border-color: var(--ok); background: rgba(63, 185, 80, 0.06); }
|
|
816
828
|
.perm-card.approved .perm-head { color: var(--ok); }
|
|
@@ -820,6 +832,60 @@ body.resizing * { cursor: col-resize !important; }
|
|
|
820
832
|
.perm-card.collapsed .perm-args,
|
|
821
833
|
.perm-card.collapsed .perm-actions { display: none; }
|
|
822
834
|
|
|
835
|
+
/* question card (ask_user) ------------------------------------------------- */
|
|
836
|
+
.question-card {
|
|
837
|
+
border: 1px solid var(--accent);
|
|
838
|
+
border-radius: 8px;
|
|
839
|
+
padding: 10px 12px;
|
|
840
|
+
background: var(--accent-soft);
|
|
841
|
+
}
|
|
842
|
+
.question-title { font-weight: 700; margin-bottom: 8px; }
|
|
843
|
+
.question-form { display: flex; flex-direction: column; gap: 9px; }
|
|
844
|
+
.question-field { display: flex; flex-direction: column; gap: 4px; }
|
|
845
|
+
.question-label { font-size: 12.5px; color: var(--muted); }
|
|
846
|
+
.question-input {
|
|
847
|
+
font: inherit;
|
|
848
|
+
color: var(--text);
|
|
849
|
+
background: var(--theirs);
|
|
850
|
+
border: 1px solid var(--border);
|
|
851
|
+
border-radius: 6px;
|
|
852
|
+
padding: 6px 8px;
|
|
853
|
+
}
|
|
854
|
+
.question-input:focus { outline: none; border-color: var(--accent); }
|
|
855
|
+
.question-actions { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
|
|
856
|
+
.question-actions .spacer { flex: 1; }
|
|
857
|
+
.question-card.answered { border-color: var(--ok); background: var(--ok-soft); }
|
|
858
|
+
.question-card.answered .question-title { color: var(--ok); }
|
|
859
|
+
|
|
860
|
+
/* choice card (offer_choices) ---------------------------------------------- */
|
|
861
|
+
.choice-card {
|
|
862
|
+
border: 1px solid var(--border);
|
|
863
|
+
border-radius: 8px;
|
|
864
|
+
padding: 10px 12px;
|
|
865
|
+
background: var(--theirs);
|
|
866
|
+
margin-top: 6px;
|
|
867
|
+
}
|
|
868
|
+
.choice-question { margin-bottom: 8px; }
|
|
869
|
+
.choice-options { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
870
|
+
.choice-btn { border-radius: 999px; padding: 5px 12px; }
|
|
871
|
+
|
|
872
|
+
/* plan card (show_plan) ---------------------------------------------------- */
|
|
873
|
+
.plan-card {
|
|
874
|
+
border: 1px solid var(--border);
|
|
875
|
+
border-radius: 8px;
|
|
876
|
+
padding: 10px 12px;
|
|
877
|
+
background: var(--theirs);
|
|
878
|
+
margin-top: 6px;
|
|
879
|
+
}
|
|
880
|
+
.plan-title { font-weight: 700; margin-bottom: 6px; }
|
|
881
|
+
.plan-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
882
|
+
.plan-step { display: flex; align-items: baseline; gap: 8px; line-height: 1.4; }
|
|
883
|
+
.plan-mark { flex: none; width: 14px; font-family: var(--mono); color: var(--muted); }
|
|
884
|
+
.plan-step.done .plan-mark { color: var(--ok); }
|
|
885
|
+
.plan-step.done .plan-text { color: var(--muted); text-decoration: line-through; }
|
|
886
|
+
.plan-step.active .plan-mark { color: var(--accent); }
|
|
887
|
+
.plan-step.active .plan-text { color: var(--text); font-weight: 600; }
|
|
888
|
+
|
|
823
889
|
/* activity indicators ------------------------------------------------------ */
|
|
824
890
|
.activity { display: flex; align-items: center; gap: 9px; margin-top: 10px; color: var(--muted); font-size: 12.5px; }
|
|
825
891
|
.activity .loader { color: var(--accent); }
|
|
@@ -1254,6 +1320,9 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1254
1320
|
|
|
1255
1321
|
/* ajustes: voz (TTS) -------------------------------------------------------- */
|
|
1256
1322
|
.voz-announce { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); padding: 8px 0; }
|
|
1323
|
+
.voz-range { display: flex; align-items: center; gap: 12px; }
|
|
1324
|
+
.voz-range input[type="range"] { flex: 1; max-width: 360px; accent-color: var(--accent); cursor: pointer; }
|
|
1325
|
+
.voz-range output { min-width: 62px; text-align: right; font: 600 12px var(--serif); color: var(--fg); }
|
|
1257
1326
|
|
|
1258
1327
|
/* ajustes: pestañas internas ---------------------------------------------- */
|
|
1259
1328
|
#view-ajustes.active { display: block; overflow-y: auto; }
|
|
@@ -1439,7 +1508,7 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1439
1508
|
.xtab:hover { background: var(--theirs); color: var(--text); }
|
|
1440
1509
|
.xtab.is-active { background: var(--theirs); color: var(--text); }
|
|
1441
1510
|
.xp-pane { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
1442
|
-
.xp-bar { display: flex; gap: 6px; align-items: center; padding: 9px 12px; }
|
|
1511
|
+
.xp-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 9px 12px; }
|
|
1443
1512
|
.xp-bar input { flex: 1; min-width: 0; }
|
|
1444
1513
|
.xp-bar .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1445
1514
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px 8px; }
|
|
@@ -1498,6 +1567,16 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1498
1567
|
.import-bar { display: flex; gap: 6px; margin: 4px 0 10px; }
|
|
1499
1568
|
.import-bar input { flex: 1; min-width: 0; }
|
|
1500
1569
|
|
|
1570
|
+
details.catalog { margin: 10px 0; }
|
|
1571
|
+
details.catalog > summary {
|
|
1572
|
+
cursor: pointer;
|
|
1573
|
+
font: 600 11px var(--mono);
|
|
1574
|
+
letter-spacing: 0.06em;
|
|
1575
|
+
text-transform: uppercase;
|
|
1576
|
+
color: var(--muted);
|
|
1577
|
+
}
|
|
1578
|
+
details.catalog[open] > summary { margin-bottom: 6px; }
|
|
1579
|
+
|
|
1501
1580
|
.file-actions { display: flex; align-items: center; gap: 10px; }
|
|
1502
1581
|
.file-modal { width: min(880px, calc(100vw - 40px)); }
|
|
1503
1582
|
.file-preview {
|
|
@@ -1812,6 +1891,27 @@ dialog.file-modal.preview-maximized {
|
|
|
1812
1891
|
.session-tab {
|
|
1813
1892
|
max-width: 160px;
|
|
1814
1893
|
}
|
|
1894
|
+
/* Permiso: en un movil la fila de acciones aplastaba "requiere aprobación"
|
|
1895
|
+
a una letra por linea y los botones se salian. El veredicto pasa a su
|
|
1896
|
+
propia linea y los botones se reparten el ancho. */
|
|
1897
|
+
.perm-card .perm-head {
|
|
1898
|
+
flex-wrap: wrap;
|
|
1899
|
+
}
|
|
1900
|
+
.perm-card .perm-actions .muted {
|
|
1901
|
+
flex: 1 0 100%;
|
|
1902
|
+
}
|
|
1903
|
+
.perm-card .perm-actions .spacer {
|
|
1904
|
+
display: none;
|
|
1905
|
+
}
|
|
1906
|
+
.perm-card .perm-actions .btn {
|
|
1907
|
+
flex: 1 1 auto;
|
|
1908
|
+
min-height: 40px;
|
|
1909
|
+
}
|
|
1910
|
+
/* Preview: el input de la URL se aplastaba contra los botones y no se leia.
|
|
1911
|
+
Le damos la fila entera y los controles caen abajo. */
|
|
1912
|
+
.xp-bar #preview-url {
|
|
1913
|
+
flex: 1 1 100%;
|
|
1914
|
+
}
|
|
1815
1915
|
}
|
|
1816
1916
|
|
|
1817
1917
|
@media (max-width: 480px) {
|
|
@@ -1950,9 +2050,29 @@ dialog.modal::backdrop { background: rgba(24, 22, 16, 0.55); backdrop-filter: bl
|
|
|
1950
2050
|
.advisor-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
1951
2051
|
.advisor-glyph { color: var(--accent); font-size: 13px; line-height: 1; }
|
|
1952
2052
|
.advisor-title { font: 600 10px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
2053
|
+
.advisor-badge {
|
|
2054
|
+
font: 600 10px var(--mono);
|
|
2055
|
+
letter-spacing: 0.04em;
|
|
2056
|
+
text-transform: uppercase;
|
|
2057
|
+
padding: 1px 7px;
|
|
2058
|
+
border-radius: 999px;
|
|
2059
|
+
color: var(--accent);
|
|
2060
|
+
background: var(--accent-soft);
|
|
2061
|
+
border: 1px solid currentColor;
|
|
2062
|
+
}
|
|
1953
2063
|
.advisor-text { color: var(--text); line-height: 1.5; }
|
|
1954
|
-
.advisor-code { border-left-color: var(--
|
|
1955
|
-
.advisor-code .advisor-glyph
|
|
2064
|
+
.advisor-code { border-left-color: var(--danger); background: var(--danger-soft); }
|
|
2065
|
+
.advisor-code .advisor-glyph,
|
|
2066
|
+
.advisor-code .advisor-badge { color: var(--danger); }
|
|
2067
|
+
.advisor-explain { border-left-color: var(--info); background: var(--info-soft); }
|
|
2068
|
+
.advisor-explain .advisor-glyph,
|
|
2069
|
+
.advisor-explain .advisor-badge { color: var(--info); }
|
|
2070
|
+
.advisor-advice { border-left-color: var(--ok); background: var(--ok-soft); }
|
|
2071
|
+
.advisor-advice .advisor-glyph,
|
|
2072
|
+
.advisor-advice .advisor-badge { color: var(--ok); }
|
|
2073
|
+
.advisor-summary { border-left-color: var(--muted); background: var(--muted-soft); }
|
|
2074
|
+
.advisor-summary .advisor-glyph,
|
|
2075
|
+
.advisor-summary .advisor-badge { color: var(--muted); }
|
|
1956
2076
|
|
|
1957
2077
|
.brand .ver.ver-action { cursor: pointer; }
|
|
1958
2078
|
.brand .ver.ver-action:hover { color: var(--accent); }
|
|
@@ -7,11 +7,15 @@ import {
|
|
|
7
7
|
parseSkillMarkdown,
|
|
8
8
|
parseSkillRef,
|
|
9
9
|
fetchSkillFromGithub,
|
|
10
|
+
searchSkillsGithub,
|
|
10
11
|
writeSkillFiles,
|
|
11
12
|
getMcpRegistryServer,
|
|
12
13
|
registryServerToConfig,
|
|
14
|
+
registryServerToCandidate,
|
|
15
|
+
searchMcpRegistry,
|
|
13
16
|
normalizeMcpConfig,
|
|
14
17
|
homeDir,
|
|
18
|
+
envValue,
|
|
15
19
|
memorySlug,
|
|
16
20
|
listMemoryTopics,
|
|
17
21
|
readMemoryIndex,
|
|
@@ -37,6 +41,12 @@ function definition(name, description, parameters, required = []) {
|
|
|
37
41
|
|
|
38
42
|
const DEFINITIONS = [
|
|
39
43
|
definition('list_mcp', 'Lista los servidores MCP configurados (de tamnora.json y de la base).', {}),
|
|
44
|
+
definition(
|
|
45
|
+
'search_mcp',
|
|
46
|
+
'Busca servidores en el registro oficial de MCP y devuelve candidatos (id, nombre, descripción y env requerido) sin instalarlos. Para registrar uno, usá install_mcp con el nombre del registro.',
|
|
47
|
+
{ query: { type: 'string', description: 'Texto a buscar, ej. "github" o "postgres"' } },
|
|
48
|
+
['query']
|
|
49
|
+
),
|
|
40
50
|
definition(
|
|
41
51
|
'install_mcp',
|
|
42
52
|
'Registra un servidor MCP. stdio: pasá command (y args). http: pasá url. Para webs con JavaScript/Cloudflare (que fetch_url no lee) instalá un MCP de navegador, ej. command "npx" args ["-y","@playwright/mcp@latest"].',
|
|
@@ -57,6 +67,12 @@ const DEFINITIONS = [
|
|
|
57
67
|
),
|
|
58
68
|
definition('remove_mcp', 'Elimina un servidor MCP guardado desde la consola.', { id: { type: 'string' } }, ['id']),
|
|
59
69
|
definition('list_skills', 'Lista las skills instaladas y si están habilitadas.', {}),
|
|
70
|
+
definition(
|
|
71
|
+
'search_skills',
|
|
72
|
+
'Busca skills en GitHub y devuelve refs instalables (owner/repo/slug). Para instalarlas, usá install_skill con "ref".',
|
|
73
|
+
{ query: { type: 'string', description: 'Texto a buscar, ej. "testing" o "react"' } },
|
|
74
|
+
['query']
|
|
75
|
+
),
|
|
60
76
|
definition(
|
|
61
77
|
'skill',
|
|
62
78
|
'Carga el contenido completo de una skill por su id. Usala cuando una skill del índice aplique a la tarea.',
|
|
@@ -113,7 +129,7 @@ function decisionFor(ctx, name, args) {
|
|
|
113
129
|
const value = perms[name];
|
|
114
130
|
if (value === 'allow' || value === 'ask' || value === 'deny') return value;
|
|
115
131
|
if (name === 'memory') return String(args?.action ?? 'read').toLowerCase() === 'write' ? 'ask' : 'allow';
|
|
116
|
-
return name.startsWith('list_') || name === 'skill' ? 'allow' : 'ask';
|
|
132
|
+
return name.startsWith('list_') || name.startsWith('search_') || name === 'skill' ? 'allow' : 'ask';
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
/**
|
|
@@ -144,6 +160,19 @@ export function createAgentTools(ctx) {
|
|
|
144
160
|
source: s.source ?? 'config',
|
|
145
161
|
})), null, 2);
|
|
146
162
|
|
|
163
|
+
case 'search_mcp': {
|
|
164
|
+
const query = String(args.query ?? '').trim();
|
|
165
|
+
if (!query) return 'Pasá "query" para buscar en el registro MCP.';
|
|
166
|
+
let servers;
|
|
167
|
+
try {
|
|
168
|
+
servers = await searchMcpRegistry(query, { limit: 10 });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return `No se pudo consultar el registro MCP: ${error.message}`;
|
|
171
|
+
}
|
|
172
|
+
if (!servers.length) return `Sin resultados para "${query}".`;
|
|
173
|
+
return JSON.stringify(servers.map(registryServerToCandidate), null, 2);
|
|
174
|
+
}
|
|
175
|
+
|
|
147
176
|
case 'install_mcp': {
|
|
148
177
|
if (args.registry || args.name) {
|
|
149
178
|
let server;
|
|
@@ -223,6 +252,19 @@ export function createAgentTools(ctx) {
|
|
|
223
252
|
case 'list_skills':
|
|
224
253
|
return JSON.stringify(skillsView(ctx).map(({ id, name, enabled, description }) => ({ id, name, enabled, description })), null, 2);
|
|
225
254
|
|
|
255
|
+
case 'search_skills': {
|
|
256
|
+
const query = String(args.query ?? '').trim();
|
|
257
|
+
if (!query) return 'Pasá "query" para buscar skills.';
|
|
258
|
+
let results;
|
|
259
|
+
try {
|
|
260
|
+
results = await searchSkillsGithub(query, { token: envValue(['GITHUB_TOKEN', 'GH_TOKEN']) });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return `No se pudo buscar en GitHub: ${error.message}`;
|
|
263
|
+
}
|
|
264
|
+
if (!results.length) return `Sin resultados para "${query}".`;
|
|
265
|
+
return JSON.stringify(results, null, 2);
|
|
266
|
+
}
|
|
267
|
+
|
|
226
268
|
case 'skill': {
|
|
227
269
|
const id = String(args.name ?? args.id ?? '').trim();
|
|
228
270
|
const skill = findSkill(ctx, id);
|
|
@@ -308,7 +350,7 @@ export function createAgentTools(ctx) {
|
|
|
308
350
|
}
|
|
309
351
|
}
|
|
310
352
|
|
|
311
|
-
// Tools de admin que no modifican nada (lístar/leer): disponibles en todo modo.
|
|
312
|
-
const readOnly = new Set(['list_mcp', 'list_skills', 'skill']);
|
|
353
|
+
// Tools de admin que no modifican nada (lístar/leer/buscar): disponibles en todo modo.
|
|
354
|
+
const readOnly = new Set(['list_mcp', 'search_mcp', 'list_skills', 'search_skills', 'skill']);
|
|
313
355
|
return { definitions: DEFINITIONS, handles, execute, readOnly };
|
|
314
356
|
}
|
|
@@ -252,12 +252,16 @@ export function startTelegramChannel(
|
|
|
252
252
|
* (`sendVoice`, aparece como nota de voz); sin ffmpeg cae a audio MP3.
|
|
253
253
|
* Un fallo del TTS no debe romper el run: se loguea y sigue.
|
|
254
254
|
*/
|
|
255
|
-
async function enviarNotaDeVoz(chatId, texto, { voice, rate } = {}) {
|
|
255
|
+
async function enviarNotaDeVoz(chatId, texto, { voice, rate, pitch } = {}) {
|
|
256
256
|
const tts = resolveTts(ctx.app);
|
|
257
257
|
const frase = String(texto ?? '').trim().slice(0, MAX_TEXTO);
|
|
258
258
|
if (!frase) return;
|
|
259
259
|
try {
|
|
260
|
-
const mp3 = await sintetizar(frase, {
|
|
260
|
+
const mp3 = await sintetizar(frase, {
|
|
261
|
+
voice: voice || tts.voice,
|
|
262
|
+
rate: rate || tts.rate,
|
|
263
|
+
pitch: pitch || tts.pitch,
|
|
264
|
+
});
|
|
261
265
|
const ogg = await mp3AOgg(mp3);
|
|
262
266
|
const form = new FormData();
|
|
263
267
|
form.append('chat_id', chatId);
|
|
@@ -474,6 +478,8 @@ export function startTelegramChannel(
|
|
|
474
478
|
yes: false,
|
|
475
479
|
onAsk,
|
|
476
480
|
mcp,
|
|
481
|
+
mcpMode: ctx.effectiveMcpMode(),
|
|
482
|
+
mcpPinned: ctx.effectiveMcpPinned(),
|
|
477
483
|
});
|
|
478
484
|
const exposeAdmin = !mode.tools;
|
|
479
485
|
const admin = ctx.agentTools;
|
|
@@ -525,7 +531,7 @@ export function startTelegramChannel(
|
|
|
525
531
|
} else if (event.type === 'tool_call') {
|
|
526
532
|
activity = `${event.name} ${toolCallPayload(event).summary}`.trim();
|
|
527
533
|
if (event.name === 'speak') {
|
|
528
|
-
enviarNotaDeVoz(chatId, event.args?.texto, { voice: event.args?.voice, rate: event.args?.rate });
|
|
534
|
+
enviarNotaDeVoz(chatId, event.args?.texto, { voice: event.args?.voice, rate: event.args?.rate, pitch: event.args?.pitch });
|
|
529
535
|
}
|
|
530
536
|
} else if (event.type === 'tool_result') {
|
|
531
537
|
activity = `${event.name} ✓`;
|
|
@@ -886,6 +892,7 @@ export function startTelegramChannel(
|
|
|
886
892
|
enviarNotaDeVoz(linked, event.data?.args?.texto, {
|
|
887
893
|
voice: event.data?.args?.voice,
|
|
888
894
|
rate: event.data?.args?.rate,
|
|
895
|
+
pitch: event.data?.args?.pitch,
|
|
889
896
|
});
|
|
890
897
|
}
|
|
891
898
|
return;
|
|
@@ -33,11 +33,12 @@ function workspacePreamble(cwd) {
|
|
|
33
33
|
* @param {string[]} opts.files - instruction file names (e.g. ['AGENTS.md'])
|
|
34
34
|
* @param {() => string} [opts.profileText] - user profile block, re-read on every call
|
|
35
35
|
* @param {() => string} [opts.skillsText] - skills block, re-read on every call
|
|
36
|
+
* @param {() => string} [opts.mcpText] - MCP tool index block, re-read on every call
|
|
36
37
|
* @param {() => string} [opts.scopeText] - strict-scope block, re-read on every call
|
|
37
38
|
* @param {string[]} [opts.ruleFiles] - glob patterns of project rule files
|
|
38
39
|
* @param {boolean} [opts.memory] - inject the project memory block
|
|
39
40
|
*/
|
|
40
|
-
export function createPromptBuilder({ cwd, files, profileText, skillsText, scopeText, ruleFiles = [], memory = false }) {
|
|
41
|
+
export function createPromptBuilder({ cwd, files, profileText, skillsText, mcpText, scopeText, ruleFiles = [], memory = false }) {
|
|
41
42
|
const cache = new Map();
|
|
42
43
|
|
|
43
44
|
async function buildFor(workspace) {
|
|
@@ -60,6 +61,8 @@ export function createPromptBuilder({ cwd, files, profileText, skillsText, scope
|
|
|
60
61
|
if (profile) parts.push(profile);
|
|
61
62
|
const extra = typeof skillsText === 'function' ? String(skillsText() ?? '').trim() : '';
|
|
62
63
|
if (extra) parts.push(extra);
|
|
64
|
+
const mcp = typeof mcpText === 'function' ? String(mcpText() ?? '').trim() : '';
|
|
65
|
+
if (mcp) parts.push(mcp);
|
|
63
66
|
if (memory) {
|
|
64
67
|
const mem = memoryPrompt(key);
|
|
65
68
|
if (mem) parts.push(mem);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker para formularios interactivos (`ask_user`). El chat registra un pedido
|
|
3
|
+
* pendiente (`ask`) y lo espera mientras el SSE emite un evento `question`; la
|
|
4
|
+
* consola responde con `POST /api/questions/:askId`, que llama a `answer`. Los
|
|
5
|
+
* pedidos que nadie contesta expiran como cancelados.
|
|
6
|
+
*
|
|
7
|
+
* Igual que `approvals`, guarda el `meta` (sessionId, title, questions) para que
|
|
8
|
+
* un cliente **sin** stream abierto sepa que hay un formulario esperando.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} [opts]
|
|
11
|
+
* @param {number} [opts.timeoutMs] - how long to wait before cancelling
|
|
12
|
+
*/
|
|
13
|
+
export function createQuestionBroker({ timeoutMs = 900000 } = {}) {
|
|
14
|
+
const pending = new Map();
|
|
15
|
+
|
|
16
|
+
function settle(id, answers, ok) {
|
|
17
|
+
const entry = pending.get(id);
|
|
18
|
+
if (!entry) return false;
|
|
19
|
+
clearTimeout(entry.timer);
|
|
20
|
+
pending.delete(id);
|
|
21
|
+
entry.resolve({ ok, answers: answers ?? null });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
/** Register a pending form; resolves to `{ ok, answers }`. */
|
|
27
|
+
ask(id, meta = {}) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const timer = setTimeout(() => settle(id, null, false), timeoutMs);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
pending.set(id, { resolve, timer, requestedAt: Date.now(), ...meta });
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/** Answer a pending form. Returns false if it expired or is unknown. */
|
|
36
|
+
answer(id, answers) {
|
|
37
|
+
return settle(id, answers, true);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/** Cancel every pending form of a session (used when the run stops). */
|
|
41
|
+
cancelForSession(sessionId) {
|
|
42
|
+
for (const [id, entry] of pending) {
|
|
43
|
+
if (entry.sessionId === sessionId) settle(id, null, false);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/** Cancel everything still pending (used on shutdown). */
|
|
48
|
+
cancelAll() {
|
|
49
|
+
for (const id of [...pending.keys()]) settle(id, null, false);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/** Lo que sigue esperando, para que cualquier cliente pueda mostrarlo. */
|
|
53
|
+
list({ sessionId } = {}) {
|
|
54
|
+
const items = [];
|
|
55
|
+
for (const [askId, entry] of pending) {
|
|
56
|
+
if (sessionId !== undefined && entry.sessionId !== sessionId) continue;
|
|
57
|
+
items.push({
|
|
58
|
+
askId,
|
|
59
|
+
sessionId: entry.sessionId ?? null,
|
|
60
|
+
title: entry.title ?? '',
|
|
61
|
+
questions: entry.questions ?? [],
|
|
62
|
+
requestedAt: entry.requestedAt,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return items.sort((a, b) => a.requestedAt - b.requestedAt);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
get pending() {
|
|
69
|
+
return pending.size;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -53,7 +53,16 @@ async function runAgentSchedule(ctx, schedule) {
|
|
|
53
53
|
ctx.store.addMessage(sessionId, userMessage);
|
|
54
54
|
const prefixLength = messages.length;
|
|
55
55
|
|
|
56
|
-
const local = createTools({
|
|
56
|
+
const local = createTools({
|
|
57
|
+
root: cwd,
|
|
58
|
+
enabled: mode.tools ?? undefined,
|
|
59
|
+
permissions: ctx.config.permissions,
|
|
60
|
+
yes: true,
|
|
61
|
+
mcp,
|
|
62
|
+
mcpMode: ctx.effectiveMcpMode(),
|
|
63
|
+
mcpPinned: ctx.effectiveMcpPinned(),
|
|
64
|
+
signal,
|
|
65
|
+
});
|
|
57
66
|
const exposeAdmin = !mode.tools;
|
|
58
67
|
const admin = ctx.agentTools;
|
|
59
68
|
const emit = (type, data, agentId = null) => ctx.runs.emit(sessionId, type, data, { agentId });
|
|
@@ -64,6 +73,8 @@ async function runAgentSchedule(ctx, schedule) {
|
|
|
64
73
|
model,
|
|
65
74
|
permissions: ctx.config.permissions,
|
|
66
75
|
mcp,
|
|
76
|
+
mcpMode: ctx.effectiveMcpMode(),
|
|
77
|
+
mcpPinned: ctx.effectiveMcpPinned(),
|
|
67
78
|
signal,
|
|
68
79
|
exclude: admin.definitions.map((d) => d.function.name),
|
|
69
80
|
emit,
|
|
@@ -32,6 +32,8 @@ export function createSubagentTool(ctx, options) {
|
|
|
32
32
|
model,
|
|
33
33
|
permissions = {},
|
|
34
34
|
mcp = null,
|
|
35
|
+
mcpMode = 'auto',
|
|
36
|
+
mcpPinned = [],
|
|
35
37
|
signal,
|
|
36
38
|
exclude = [],
|
|
37
39
|
emit,
|
|
@@ -67,7 +69,7 @@ export function createSubagentTool(ctx, options) {
|
|
|
67
69
|
Array.isArray(args.tools) && args.tools.length
|
|
68
70
|
? args.tools.filter((tool) => !exclude.includes(tool))
|
|
69
71
|
: mode.tools ?? undefined;
|
|
70
|
-
const local = createTools({ root: cwd, enabled, permissions, yes: false, mcp, signal });
|
|
72
|
+
const local = createTools({ root: cwd, enabled, permissions, yes: false, mcp, mcpMode, mcpPinned, signal });
|
|
71
73
|
const basePrompt = await ctx.systemPromptFor(cwd);
|
|
72
74
|
const messages = [
|
|
73
75
|
{ role: 'system', content: `${basePrompt}\n\n${SUBAGENT_PROMPT}\n\nTarea: ${description}` },
|
|
@@ -8,6 +8,7 @@ export const DEFAULT_TTS = {
|
|
|
8
8
|
enabled: false,
|
|
9
9
|
voice: VOZ_POR_DEFECTO,
|
|
10
10
|
rate: '+0%',
|
|
11
|
+
pitch: '+0Hz',
|
|
11
12
|
announce: { start: true, tools: true, end: true, advice: true, answer: false, summary: false },
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -23,6 +24,7 @@ export function resolveTts(app) {
|
|
|
23
24
|
enabled: saved.enabled === true,
|
|
24
25
|
voice: typeof saved.voice === 'string' && saved.voice ? saved.voice : DEFAULT_TTS.voice,
|
|
25
26
|
rate: typeof saved.rate === 'string' && saved.rate ? saved.rate : DEFAULT_TTS.rate,
|
|
27
|
+
pitch: typeof saved.pitch === 'string' && saved.pitch ? saved.pitch : DEFAULT_TTS.pitch,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -40,3 +42,12 @@ export function sanearRate(value, fallback = '+0%') {
|
|
|
40
42
|
if (numero === 0) return '+0%';
|
|
41
43
|
return `${match[1] === '-' ? '-' : '+'}${numero}%`;
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
/** Sanitiza un tono: "+10Hz", "-20Hz", "30Hz" -> "+30Hz". */
|
|
47
|
+
export function sanearPitch(value, fallback = '+0Hz') {
|
|
48
|
+
const match = /^([+-]?)(\d+)Hz$/.exec(String(value ?? '').trim());
|
|
49
|
+
if (!match) return fallback;
|
|
50
|
+
const numero = Math.min(50, Number(match[2]));
|
|
51
|
+
if (numero === 0) return '+0Hz';
|
|
52
|
+
return `${match[1] === '-' ? '-' : '+'}${numero}Hz`;
|
|
53
|
+
}
|