@danieltmn/tamnora 0.16.1 → 0.18.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 +58 -4
- 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 +17 -3
- 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/procs/detect.js +59 -8
- package/harness/packages/core/src/procs/kill.js +26 -5
- 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 +93 -2
- 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 +86 -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/procs.test.js +70 -2
- 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 +629 -28
- package/harness/packages/server/public/index.html +44 -2
- package/harness/packages/server/public/styles.css +106 -7
- package/harness/packages/server/src/lib/agentTools.js +45 -3
- package/harness/packages/server/src/lib/channels/telegram.js +2 -0
- package/harness/packages/server/src/lib/procs.js +9 -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/runManager.js +29 -2
- 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/routes/chat.js +40 -3
- 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/procs.js +20 -3
- 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/server.js +37 -1
- package/harness/packages/server/test/preview.test.js +111 -0
- package/harness/packages/server/test/server.test.js +285 -34
- 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/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.18.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>
|
|
@@ -209,6 +209,9 @@
|
|
|
209
209
|
<button class="send-btn" type="submit" aria-label="enviar" title="enviar">
|
|
210
210
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></svg>
|
|
211
211
|
</button>
|
|
212
|
+
<button class="add-btn interrupt-btn" type="button" id="interrupt-btn" title="Interrumpir al agente y enviar este mensaje (Ctrl+Enter)" aria-label="interrumpir y enviar" hidden>
|
|
213
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" /></svg>
|
|
214
|
+
</button>
|
|
212
215
|
<button class="add-btn" type="button" id="stop-btn" title="Detener la respuesta" aria-label="detener" hidden>
|
|
213
216
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>
|
|
214
217
|
</button>
|
|
@@ -267,7 +270,12 @@
|
|
|
267
270
|
<div class="xp-bar">
|
|
268
271
|
<input id="preview-url" placeholder="http://127.0.0.1:5173" spellcheck="false" />
|
|
269
272
|
<button id="preview-open" class="btn" type="button">abrir</button>
|
|
270
|
-
<
|
|
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>
|
|
271
279
|
</div>
|
|
272
280
|
<div class="preview-tools">
|
|
273
281
|
<select id="preview-device" title="tamaño de pantalla" aria-label="tamaño de pantalla">
|
|
@@ -561,12 +569,36 @@
|
|
|
561
569
|
<p class="muted verify-line">
|
|
562
570
|
Se combinan con <code>mcp[]</code> de <code>tamnora.json</code>; los guardados acá viven en la base.
|
|
563
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>
|
|
564
586
|
<div class="import-bar">
|
|
565
587
|
<input id="mcp-import" placeholder="nombre del registro oficial, URL o JSON (bloque mcpServers)" spellcheck="false" />
|
|
566
588
|
<button id="mcp-import-btn" class="btn" type="button">importar</button>
|
|
567
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>
|
|
568
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>
|
|
569
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>
|
|
570
602
|
</div>
|
|
571
603
|
|
|
572
604
|
<div class="panel ajustes-panel" data-panel="skills">
|
|
@@ -581,8 +613,18 @@
|
|
|
581
613
|
<input id="skill-import" placeholder="https://skills.sh/owner/repo/skill" spellcheck="false" />
|
|
582
614
|
<button id="skill-import-btn" class="btn" type="button">importar</button>
|
|
583
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>
|
|
584
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>
|
|
585
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>
|
|
586
628
|
</div>
|
|
587
629
|
|
|
588
630
|
<div class="panel ajustes-panel" data-panel="memoria">
|
|
@@ -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;
|
|
@@ -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); }
|
|
@@ -1018,6 +1084,10 @@ details.reasoning .body { color: var(--muted); white-space: pre-wrap; margin-top
|
|
|
1018
1084
|
.send-btn:hover { filter: brightness(1.08); }
|
|
1019
1085
|
.send-btn:disabled { opacity: 0.5; cursor: default; }
|
|
1020
1086
|
.send-btn[data-working="1"] { background: var(--theirs); color: var(--accent); border-color: var(--accent); }
|
|
1087
|
+
/* Interrumpir: corta el turno en curso y manda el mensaje ya. */
|
|
1088
|
+
.interrupt-btn { color: var(--accent); border-color: var(--accent); }
|
|
1089
|
+
.interrupt-btn:hover { background: var(--accent); color: var(--primary-fg); }
|
|
1090
|
+
.interrupt-btn:disabled { opacity: 0.45; cursor: default; color: var(--muted); border-color: var(--border); }
|
|
1021
1091
|
/* Dictado activo: verde titilando (cuenta regresiva y grabación). */
|
|
1022
1092
|
.add-btn.recording { color: #2eb872; border-color: currentColor; animation: live-pulse 1s ease-in-out infinite; }
|
|
1023
1093
|
.add-btn.busy { opacity: 0.55; }
|
|
@@ -1407,8 +1477,8 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1407
1477
|
.explorer-head { display: flex; align-items: center; gap: 6px; padding: 9px 12px; border-bottom: 1px solid var(--border); min-width: 0; }
|
|
1408
1478
|
.explorer-title { font: 700 14px var(--serif); letter-spacing: -0.02em; }
|
|
1409
1479
|
.explorer-head .spacer { flex: 1; }
|
|
1410
|
-
/* Los botones de accion (refrescar/cerrar) nunca se recortan: las pestanas
|
|
1411
|
-
|
|
1480
|
+
/* Los botones de accion (refrescar/cerrar) nunca se recortan: las pestanas
|
|
1481
|
+
bajan a una segunda fila (wrap) en vez de empujarlos fuera o desaparecer. */
|
|
1412
1482
|
.explorer-head .btn.icon { flex: 0 0 auto; }
|
|
1413
1483
|
.explorer-cwd { padding: 9px 12px 0; color: var(--muted); font: 500 11px var(--mono); word-break: break-all; }
|
|
1414
1484
|
.explorer-cwd:empty { display: none; }
|
|
@@ -1430,13 +1500,12 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1430
1500
|
.explorer-status { padding: 6px 12px; border-top: 1px solid var(--border); color: var(--muted); font: 500 11px var(--mono); min-height: 27px; }
|
|
1431
1501
|
.explorer-status.err { color: var(--danger); }
|
|
1432
1502
|
|
|
1433
|
-
.xtabs { display: flex; gap: 4px; min-width: 0; flex: 0 1 auto;
|
|
1434
|
-
.xtabs::-webkit-scrollbar { display: none; }
|
|
1503
|
+
.xtabs { display: flex; flex-wrap: wrap; gap: 4px; min-width: 0; flex: 0 1 auto; }
|
|
1435
1504
|
.xtab { flex: 0 0 auto; border: 0; background: none; color: var(--muted); cursor: pointer; font: 600 12px var(--sans); padding: 4px 7px; border-radius: 7px; }
|
|
1436
1505
|
.xtab:hover { background: var(--theirs); color: var(--text); }
|
|
1437
1506
|
.xtab.is-active { background: var(--theirs); color: var(--text); }
|
|
1438
1507
|
.xp-pane { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
1439
|
-
.xp-bar { display: flex; gap: 6px; align-items: center; padding: 9px 12px; }
|
|
1508
|
+
.xp-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 9px 12px; }
|
|
1440
1509
|
.xp-bar input { flex: 1; min-width: 0; }
|
|
1441
1510
|
.xp-bar .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1442
1511
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px 8px; }
|
|
@@ -1495,6 +1564,16 @@ pre.code code { background: none; border: 0; padding: 0; font-size: 0.92em; }
|
|
|
1495
1564
|
.import-bar { display: flex; gap: 6px; margin: 4px 0 10px; }
|
|
1496
1565
|
.import-bar input { flex: 1; min-width: 0; }
|
|
1497
1566
|
|
|
1567
|
+
details.catalog { margin: 10px 0; }
|
|
1568
|
+
details.catalog > summary {
|
|
1569
|
+
cursor: pointer;
|
|
1570
|
+
font: 600 11px var(--mono);
|
|
1571
|
+
letter-spacing: 0.06em;
|
|
1572
|
+
text-transform: uppercase;
|
|
1573
|
+
color: var(--muted);
|
|
1574
|
+
}
|
|
1575
|
+
details.catalog[open] > summary { margin-bottom: 6px; }
|
|
1576
|
+
|
|
1498
1577
|
.file-actions { display: flex; align-items: center; gap: 10px; }
|
|
1499
1578
|
.file-modal { width: min(880px, calc(100vw - 40px)); }
|
|
1500
1579
|
.file-preview {
|
|
@@ -1947,9 +2026,29 @@ dialog.modal::backdrop { background: rgba(24, 22, 16, 0.55); backdrop-filter: bl
|
|
|
1947
2026
|
.advisor-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
1948
2027
|
.advisor-glyph { color: var(--accent); font-size: 13px; line-height: 1; }
|
|
1949
2028
|
.advisor-title { font: 600 10px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
2029
|
+
.advisor-badge {
|
|
2030
|
+
font: 600 10px var(--mono);
|
|
2031
|
+
letter-spacing: 0.04em;
|
|
2032
|
+
text-transform: uppercase;
|
|
2033
|
+
padding: 1px 7px;
|
|
2034
|
+
border-radius: 999px;
|
|
2035
|
+
color: var(--accent);
|
|
2036
|
+
background: var(--accent-soft);
|
|
2037
|
+
border: 1px solid currentColor;
|
|
2038
|
+
}
|
|
1950
2039
|
.advisor-text { color: var(--text); line-height: 1.5; }
|
|
1951
|
-
.advisor-code { border-left-color: var(--
|
|
1952
|
-
.advisor-code .advisor-glyph
|
|
2040
|
+
.advisor-code { border-left-color: var(--danger); background: var(--danger-soft); }
|
|
2041
|
+
.advisor-code .advisor-glyph,
|
|
2042
|
+
.advisor-code .advisor-badge { color: var(--danger); }
|
|
2043
|
+
.advisor-explain { border-left-color: var(--info); background: var(--info-soft); }
|
|
2044
|
+
.advisor-explain .advisor-glyph,
|
|
2045
|
+
.advisor-explain .advisor-badge { color: var(--info); }
|
|
2046
|
+
.advisor-advice { border-left-color: var(--ok); background: var(--ok-soft); }
|
|
2047
|
+
.advisor-advice .advisor-glyph,
|
|
2048
|
+
.advisor-advice .advisor-badge { color: var(--ok); }
|
|
2049
|
+
.advisor-summary { border-left-color: var(--muted); background: var(--muted-soft); }
|
|
2050
|
+
.advisor-summary .advisor-glyph,
|
|
2051
|
+
.advisor-summary .advisor-badge { color: var(--muted); }
|
|
1953
2052
|
|
|
1954
2053
|
.brand .ver.ver-action { cursor: pointer; }
|
|
1955
2054
|
.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
|
}
|
|
@@ -36,11 +36,17 @@ export function createProcManager({ allow } = {}) {
|
|
|
36
36
|
);
|
|
37
37
|
const procs = new Map();
|
|
38
38
|
|
|
39
|
+
function allows(bin) {
|
|
40
|
+
const value = String(bin ?? '').trim();
|
|
41
|
+
if (!value) return false;
|
|
42
|
+
const base = path.basename(value).replace(/\.(exe|cmd|bat)$/i, '').toLowerCase();
|
|
43
|
+
return allowed.has(base) || allowed.has(value.toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
function start({ cwd, command, args = [], port } = {}) {
|
|
40
47
|
const bin = String(command ?? '').trim();
|
|
41
48
|
if (!bin) throw new Error('Falta el comando.');
|
|
42
|
-
|
|
43
|
-
if (!allowed.has(base) && !allowed.has(bin.toLowerCase())) {
|
|
49
|
+
if (!allows(bin)) {
|
|
44
50
|
throw new Error(`Comando no permitido: ${bin}. Agregalo a processes.allow en tamnora.json.`);
|
|
45
51
|
}
|
|
46
52
|
const child = spawn(bin, args.map(String), {
|
|
@@ -106,5 +112,5 @@ export function createProcManager({ allow } = {}) {
|
|
|
106
112
|
procs.clear();
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
return { start, stop, list, logs, close, allowed: [...allowed] };
|
|
115
|
+
return { start, stop, list, logs, close, allows, allowed: [...allowed] };
|
|
110
116
|
}
|
|
@@ -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
|
+
}
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
export function createRunManager({ store }) {
|
|
11
11
|
const runs = new Map();
|
|
12
12
|
const listeners = new Set();
|
|
13
|
+
// Resolvers esperando a que el run de una sesion termine (`waitForIdle`).
|
|
14
|
+
const idleWaiters = new Map();
|
|
15
|
+
|
|
16
|
+
function settleIdle(sessionId) {
|
|
17
|
+
const waiters = idleWaiters.get(sessionId);
|
|
18
|
+
if (!waiters) return;
|
|
19
|
+
idleWaiters.delete(sessionId);
|
|
20
|
+
for (const resolve of waiters) resolve();
|
|
21
|
+
}
|
|
13
22
|
|
|
14
23
|
function persist(run, sessionId, type, data, agentId) {
|
|
15
24
|
const coalesce =
|
|
@@ -82,6 +91,7 @@ export function createRunManager({ store }) {
|
|
|
82
91
|
run.subscribers.clear();
|
|
83
92
|
runs.delete(sessionId);
|
|
84
93
|
announce(sessionId, 'end', { durationMs: Date.now() - run.startedAt });
|
|
94
|
+
settleIdle(sessionId);
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
function cancel(sessionId) {
|
|
@@ -92,7 +102,24 @@ export function createRunManager({ store }) {
|
|
|
92
102
|
const active = [...runs.values()];
|
|
93
103
|
for (const run of active) run.controller.abort();
|
|
94
104
|
runs.clear();
|
|
95
|
-
for (const run of active)
|
|
105
|
+
for (const run of active) {
|
|
106
|
+
announce(run.sessionId, 'end', { durationMs: Date.now() - run.startedAt, reason: 'shutdown' });
|
|
107
|
+
settleIdle(run.sessionId);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Promesa que resuelve cuando el run de la sesion ya no esta activo. Sirve
|
|
113
|
+
* para "interrumpir": cancelar el run actual y esperar a que se desenrolle
|
|
114
|
+
* antes de arrancar el siguiente (si no, `start` devuelve null y se pierde).
|
|
115
|
+
*/
|
|
116
|
+
function waitForIdle(sessionId) {
|
|
117
|
+
if (!runs.has(sessionId)) return Promise.resolve();
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
const waiters = idleWaiters.get(sessionId) ?? [];
|
|
120
|
+
waiters.push(resolve);
|
|
121
|
+
idleWaiters.set(sessionId, waiters);
|
|
122
|
+
});
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
function isActive(sessionId) {
|
|
@@ -120,5 +147,5 @@ export function createRunManager({ store }) {
|
|
|
120
147
|
return () => listeners.delete(fn);
|
|
121
148
|
}
|
|
122
149
|
|
|
123
|
-
return { emit, start, finish, cancel, cancelAll, isActive, signal, subscribe, subscribeAll, activeSessions };
|
|
150
|
+
return { emit, start, finish, cancel, cancelAll, isActive, waitForIdle, signal, subscribe, subscribeAll, activeSessions };
|
|
124
151
|
}
|
|
@@ -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}` },
|