@danieltmn/tamnora 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +12 -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/index.html +1228 -1326
  41. package/harness/packages/server/public/landing/boot.js +2 -3
  42. package/harness/packages/server/public/landing/index.html +3 -367
  43. package/harness/packages/server/public/settings.js +1255 -1322
  44. package/harness/packages/server/public/settingsNav.js +0 -1
  45. package/harness/packages/server/public/styles.css +2341 -2867
  46. package/harness/packages/server/public/system.js +356 -495
  47. package/harness/packages/server/public/voice.js +6 -22
  48. package/harness/packages/server/src/auth.js +4 -15
  49. package/harness/packages/server/src/cli.js +1 -1
  50. package/harness/packages/server/src/lib/agents.js +13 -27
  51. package/harness/packages/server/src/lib/appearance.js +3 -22
  52. package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
  53. package/harness/packages/server/src/lib/definitions.js +40 -0
  54. package/harness/packages/server/src/lib/skills.js +14 -21
  55. package/harness/packages/server/src/lib/ttsSettings.js +2 -2
  56. package/harness/packages/server/src/lib/workspace.js +9 -11
  57. package/harness/packages/server/src/routes/chat.js +0 -43
  58. package/harness/packages/server/src/routes/preview.js +5 -22
  59. package/harness/packages/server/src/routes/procs.js +2 -4
  60. package/harness/packages/server/src/routes/tts.js +1 -1
  61. package/harness/packages/server/src/server.js +1 -5
  62. package/harness/packages/server/test/appearance-route.test.js +3 -3
  63. package/harness/packages/server/test/appearance.test.js +12 -38
  64. package/harness/packages/server/test/chatFormat.test.js +0 -8
  65. package/harness/packages/server/test/consoleTabs.test.js +4 -4
  66. package/harness/packages/server/test/ops.test.js +8 -56
  67. package/harness/packages/server/test/preview.test.js +2 -42
  68. package/harness/packages/server/test/security.test.js +194 -370
  69. package/harness/packages/server/test/settingsNav.test.js +2 -2
  70. package/harness/packages/server/test/tts.test.js +4 -4
  71. package/harness/tamnora.example.json +1 -1
  72. package/package.json +2 -2
  73. package/harness/packages/cli/src/commands/eval.js +0 -186
  74. package/harness/packages/core/src/eval/dataset.js +0 -47
  75. package/harness/packages/core/src/eval/judge.js +0 -39
  76. package/harness/packages/core/src/eval/report.js +0 -48
  77. package/harness/packages/core/src/eval/runTask.js +0 -62
  78. package/harness/packages/core/src/eval/runner.js +0 -153
  79. package/harness/packages/core/src/eval/scorers.js +0 -217
  80. package/harness/packages/core/src/eval/task.js +0 -59
  81. package/harness/packages/core/src/procs/detect.js +0 -149
  82. package/harness/packages/core/test/eval.test.js +0 -144
  83. package/harness/packages/server/src/lib/advisor.js +0 -198
  84. package/harness/packages/server/src/routes/advisor.js +0 -79
  85. package/harness/packages/server/src/routes/eval.js +0 -100
  86. package/harness/packages/server/test/advisor.test.js +0 -249
  87. package/harness/scripts/example-dataset.jsonl +0 -4
@@ -1,1322 +1,1255 @@
1
- import { deleteEntity, importEntity, toggleEntity } from './chat.js';
2
- import {
3
- agentControlsSlot,
4
- applySessionAgent,
5
- loadModels,
6
- modelSelect,
7
- moveAgentControls,
8
- persistAgent,
9
- providerSelect,
10
- setSummaryText,
11
- updateStatus,
12
- } from './chrome.js';
13
- import { $, api, button, clear, el, emptyState, icon, state } from './core.js';
14
- import { filterSettings } from './settingsNav.js';
15
-
16
- /* --- providers ----------------------------------------------------------- */
17
-
18
- export const providerListEl = $('#provider-list');
19
- export const providerForm = $('#provider-form');
20
- export const providerModal = $('#provider-modal');
21
-
22
- export async function refreshProviders() {
23
- const data = await api('/api/providers').catch(() => null);
24
- if (!data) return;
25
- state.providers = data.providers;
26
- state.active = data.active;
27
- syncTopbar();
28
- renderProviderList();
29
- const record = state.panes.get(state.sessionId);
30
- if (record) await applySessionAgent(record);
31
- }
32
-
33
- export function isConfigured(p) {
34
- return Boolean(p.hasKey) || !p.requiresKey;
35
- }
36
-
37
- export function syncTopbar() {
38
- clear(providerSelect);
39
- const configured = state.providers.filter(isConfigured);
40
- for (const p of configured) {
41
- const option = document.createElement('option');
42
- option.value = p.id;
43
- option.textContent = p.label && p.label !== p.id ? `${p.label} (${p.id})` : p.id;
44
- providerSelect.appendChild(option);
45
- }
46
- const record = state.panes.get(state.sessionId);
47
- const active = record?.provider || state.active.provider;
48
- if (active && !configured.some((p) => p.id === active)) {
49
- const current = state.providers.find((p) => p.id === active);
50
- const option = document.createElement('option');
51
- option.value = active;
52
- option.textContent = `${current?.label ?? active} (sin configurar)`;
53
- providerSelect.appendChild(option);
54
- }
55
- if (!providerSelect.options.length) {
56
- const option = document.createElement('option');
57
- option.value = '';
58
- option.textContent = 'sin proveedores configurados';
59
- providerSelect.appendChild(option);
60
- providerSelect.disabled = true;
61
- } else {
62
- providerSelect.disabled = false;
63
- if (active) providerSelect.value = active;
64
- }
65
- updateStatus();
66
- }
67
-
68
- export function renderProviderList() {
69
- clear(providerListEl);
70
- for (const p of state.providers) {
71
- const li = el('li', p.id === state.active.provider ? 'active' : '');
72
- li.appendChild(el('span', `pdot${p.hasKey || !p.requiresKey ? ' on' : ''}`));
73
- const main = el('div', 'grow');
74
- const title = el('div');
75
- title.append(el('span', 'pid', p.id), document.createTextNode(' '), el('span', 'pmeta', p.label));
76
- main.append(
77
- title,
78
- el(
79
- 'div',
80
- 'pmeta',
81
- `${p.baseUrl || p.defaultBaseUrl || 'sin base URL'} · ${p.models.length} modelos · ${p.apiKeyHint || (p.requiresKey ? 'sin key' : 'sin key requerida')}`
82
- )
83
- );
84
- li.appendChild(main);
85
- const actions = el('div', 'pactions');
86
- actions.appendChild(button('usar', (event) => { event.stopPropagation(); useProvider(p.id); }));
87
- actions.appendChild(button('editar', (event) => { event.stopPropagation(); openProviderForm(p); }));
88
- if (p.stored) actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeProvider(p.id); }, 'btn danger'));
89
- li.appendChild(actions);
90
- li.addEventListener('click', () => openProviderForm(p));
91
- providerListEl.appendChild(li);
92
- }
93
- }
94
-
95
- export async function useProvider(id) {
96
- providerSelect.value = id;
97
- await loadModels({ save: false, force: true });
98
- await persistAgent({ provider: id, model: modelSelect.value || undefined });
99
- await refreshProviders();
100
- }
101
-
102
- export function openProviderForm(provider = null) {
103
- $('#provider-title').textContent = provider ? `editar ${provider.id}` : 'nuevo proveedor';
104
- $('#pf-id').value = provider?.id ?? '';
105
- $('#pf-id').readOnly = Boolean(provider);
106
- $('#pf-label').value = provider?.label ?? '';
107
- $('#pf-baseurl').value = provider?.baseUrl ?? '';
108
- $('#pf-key').value = '';
109
- $('#pf-key').placeholder = provider?.apiKeyHint ? `guardada (${provider.apiKeyHint})` : 'dejar vacío para no cambiar';
110
- $('#pf-models').value = (provider?.models ?? []).join(', ');
111
- setProviderStatus('');
112
- providerModal.showModal();
113
- if (!provider) $('#pf-id').focus();
114
- }
115
-
116
- export function closeProviderForm() {
117
- if (providerModal.open) providerModal.close();
118
- }
119
-
120
- export function setProviderStatus(text, cls = '') {
121
- setSummaryText($('#pf-status'), text, cls);
122
- }
123
-
124
- export async function submitProvider(event) {
125
- event.preventDefault();
126
- const id = $('#pf-id').value.trim();
127
- if (!id) return setProviderStatus('Falta el id.', 'err');
128
- const body = {
129
- label: $('#pf-label').value.trim() || id,
130
- baseUrl: $('#pf-baseurl').value.trim(),
131
- models: $('#pf-models').value.split(',').map((s) => s.trim()).filter(Boolean),
132
- };
133
- const key = $('#pf-key').value;
134
- if (key) body.apiKey = key;
135
- try {
136
- await api(`/api/providers/${encodeURIComponent(id)}`, { method: 'PUT', body });
137
- closeProviderForm();
138
- await refreshProviders();
139
- await loadModels({ force: true });
140
- } catch (error) {
141
- setProviderStatus(error.message, 'err');
142
- }
143
- }
144
-
145
- export async function removeProvider(id) {
146
- await deleteEntity(`/api/providers/${encodeURIComponent(id)}`, {
147
- confirmText: `¿Borrar la configuración de "${id}"?`,
148
- after: () => loadModels({ force: true }),
149
- refresh: refreshProviders,
150
- });
151
- }
152
-
153
- export async function testProvider() {
154
- const id = $('#pf-id').value.trim();
155
- if (!id) return setProviderStatus('Guardá primero el proveedor para probarlo.', 'err');
156
- setProviderStatus('probando…');
157
- const out = await api(`/api/providers/${encodeURIComponent(id)}/test`, { method: 'POST' }).catch((error) => ({
158
- ok: false,
159
- error: error.message,
160
- }));
161
- if (!out.ok) return setProviderStatus(`error: ${out.error}`, 'err');
162
-
163
- const ids = (out.models ?? []).map((m) => m.id ?? m).filter(Boolean);
164
- let note = '';
165
- if (out.source === 'api' && ids.length) {
166
- $('#pf-models').value = ids.join(', ');
167
- note = ' · modelos autocompletados';
168
- }
169
- if (id === state.active.provider) await loadModels({ force: true });
170
- setProviderStatus(`ok · ${out.count} modelos (${out.source})${note}`, 'ok');
171
- }
172
-
173
- /* --- ajustes: MCP + Skills ----------------------------------------------- */
174
-
175
- export const mcpListEl = $('#mcp-list');
176
- export const mcpSearchResultsEl = $('#mcp-search-results');
177
- export const mcpCatalogEl = $('#mcp-catalog');
178
- export const mcpModal = $('#mcp-modal');
179
- export const mcpForm = $('#mcp-form');
180
- export const mcpModeEl = $('#mcp-mode');
181
- export const mcpCountEl = $('#mcp-count');
182
- export const mcpPinnedEl = $('#mcp-pinned');
183
- export const skillListEl = $('#skill-list');
184
- export const skillSearchResultsEl = $('#skill-search-results');
185
- export const skillCatalogEl = $('#skill-catalog');
186
- export const skillModal = $('#skill-modal');
187
- export const skillForm = $('#skill-form');
188
- export const agentListEl = $('#agent-list');
189
- export const agentModal = $('#agent-modal');
190
- export const agentForm = $('#agent-form');
191
- export const memoryListEl = $('#memory-list');
192
- export const memoryIndexEl = $('#memory-index');
193
- export const memoryModal = $('#memory-modal');
194
- export const memoryForm = $('#memory-form');
195
- export const scheduleListEl = $('#schedule-list');
196
- export const scheduleModal = $('#schedule-modal');
197
- export const scheduleForm = $('#schedule-form');
198
- export const tgTokenEl = $('#tg-token');
199
- export const tgChatsEl = $('#tg-chats');
200
- export const tgTranscribeEl = $('#tg-transcribe');
201
- export const tgTranscribeProviderEl = $('#tg-transcribe-provider');
202
- export const tgNotifyEl = $('#tg-notify');
203
- export const tgNotifyDoneEl = $('#tg-notify-done');
204
- export const tgNotifyErrorEl = $('#tg-notify-error');
205
- export const tgNotifyPermissionEl = $('#tg-notify-permission');
206
- export const tgNotifyAgentEl = $('#tg-notify-agent');
207
- export const tgStatusEl = $('#tg-status');
208
- export const vozEnabledEl = $('#voz-enabled');
209
- export const vozVoiceEl = $('#voz-voice');
210
- export const vozRateEl = $('#voz-rate');
211
- export const vozRateValEl = $('#voz-rate-val');
212
- export const vozPitchEl = $('#voz-pitch');
213
- export const vozPitchValEl = $('#voz-pitch-val');
214
- export const vozAnnStartEl = $('#voz-ann-start');
215
- export const vozAnnToolsEl = $('#voz-ann-tools');
216
- export const vozAnnEndEl = $('#voz-ann-end');
217
- export const vozAnnAdviceEl = $('#voz-ann-advice');
218
- export const vozAnnSummaryEl = $('#voz-ann-summary');
219
- export const vozAnnAnswerEl = $('#voz-ann-answer');
220
- export const vozFfmpegEl = $('#voz-ffmpeg');
221
- export const asesorEnabledEl = $('#asesor-enabled');
222
- export const asesorCodeEl = $('#asesor-code');
223
- export const asesorExplainEl = $('#asesor-explain');
224
- export const asesorAdviceEl = $('#asesor-advice');
225
- export const asesorSummaryEl = $('#asesor-summary');
226
- export const asesorWordsEl = $('#asesor-words');
227
- export const asesorNotesEl = $('#asesor-notes');
228
- export const asesorProviderEl = $('#asesor-provider');
229
- export const asesorModelEl = $('#asesor-model');
230
- export const trProviderEl = $('#tr-provider');
231
- export const trModelEl = $('#tr-model');
232
-
233
- /* --- ajustes: pestañas internas + límites del agente --------------------- */
234
-
235
- export const ajustesTabs = document.querySelectorAll('.ajustes-tab');
236
- export const ajustesWrap = document.querySelector('.ajustes-wrap');
237
- export const ajustesSearchEl = $('#ajustes-search');
238
-
239
- /** En móvil los ajustes son dos pantallas (lista y detalle); en escritorio el nivel no importa. */
240
- export function setAjustesLevel(level) {
241
- if (ajustesWrap) ajustesWrap.dataset.level = level === 'panel' ? 'panel' : 'list';
242
- }
243
-
244
- export function activateAjustesPanel(name) {
245
- ajustesTabs.forEach((tab) => {
246
- const on = tab.dataset.panel === name;
247
- tab.classList.toggle('active', on);
248
- tab.setAttribute('aria-selected', on ? 'true' : 'false');
249
- tab.tabIndex = on ? 0 : -1;
250
- });
251
- document
252
- .querySelectorAll('.ajustes-panel')
253
- .forEach((panel) => panel.classList.toggle('active', panel.dataset.panel === name));
254
- setAjustesLevel('panel');
255
- if (name === 'agente') moveAgentControls(agentControlsSlot);
256
- }
257
-
258
- /** Muestra solo las secciones que coinciden con la búsqueda y oculta los grupos vacíos. */
259
- export function filterAjustesNav(query) {
260
- const hits = new Set(filterSettings(query).map((section) => section.id));
261
- ajustesTabs.forEach((tab) => {
262
- tab.hidden = !hits.has(tab.dataset.panel);
263
- });
264
- document.querySelectorAll('.ajustes-group').forEach((group) => {
265
- group.hidden = !group.querySelector('.ajustes-tab:not([hidden])');
266
- });
267
- const empty = $('#ajustes-empty');
268
- if (empty) empty.hidden = hits.size > 0;
269
- return [...ajustesTabs].filter((tab) => !tab.hidden);
270
- }
271
-
272
- function focusAjustesTab(tab) {
273
- if (!tab) return;
274
- ajustesTabs.forEach((other) => {
275
- other.tabIndex = other === tab ? 0 : -1;
276
- });
277
- tab.focus();
278
- }
279
-
280
- export function wireAjustesNav() {
281
- ajustesTabs.forEach((tab) => tab.addEventListener('click', () => activateAjustesPanel(tab.dataset.panel)));
282
-
283
- $('#ajustes-back')?.addEventListener('click', () => {
284
- setAjustesLevel('list');
285
- focusAjustesTab(document.querySelector('.ajustes-tab.active'));
286
- });
287
-
288
- $('.ajustes-tabs')?.addEventListener('keydown', (event) => {
289
- const visible = [...ajustesTabs].filter((tab) => !tab.hidden);
290
- const index = visible.indexOf(document.activeElement);
291
- if (index < 0) return;
292
- const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
293
- let next = null;
294
- if (step) next = visible[(index + step + visible.length) % visible.length];
295
- else if (event.key === 'Home') next = visible[0];
296
- else if (event.key === 'End') next = visible[visible.length - 1];
297
- if (!next) return;
298
- event.preventDefault();
299
- focusAjustesTab(next);
300
- });
301
-
302
- ajustesSearchEl?.addEventListener('input', () => filterAjustesNav(ajustesSearchEl.value));
303
- ajustesSearchEl?.addEventListener('keydown', (event) => {
304
- if (event.key === 'Escape' && ajustesSearchEl.value) {
305
- event.preventDefault();
306
- ajustesSearchEl.value = '';
307
- filterAjustesNav('');
308
- } else if (event.key === 'Enter') {
309
- event.preventDefault();
310
- const first = [...ajustesTabs].find((tab) => !tab.hidden);
311
- if (first) activateAjustesPanel(first.dataset.panel);
312
- } else if (event.key === 'ArrowDown') {
313
- event.preventDefault();
314
- focusAjustesTab([...ajustesTabs].find((tab) => !tab.hidden));
315
- }
316
- });
317
- }
318
-
319
- export const limitsRoundsEl = $('#limits-rounds');
320
- export const limitsTokensEl = $('#limits-tokens');
321
- export const limitsDurationEl = $('#limits-duration');
322
- export const limitsOutputEl = $('#limits-output');
323
- export const limitsCachingEl = $('#limits-caching');
324
-
325
- export async function refreshLimits() {
326
- const data = await api('/api/settings').catch(() => null);
327
- const settings = data?.settings;
328
- if (!settings) return;
329
- if (limitsRoundsEl) limitsRoundsEl.value = settings.maxRounds ?? 0;
330
- if (limitsTokensEl) limitsTokensEl.value = settings.maxTokens ?? 0;
331
- if (limitsDurationEl) limitsDurationEl.value = Math.round((settings.maxDurationMs ?? 0) / 1000);
332
- if (limitsOutputEl) limitsOutputEl.value = settings.maxOutputTokens ?? 0;
333
- if (limitsCachingEl) limitsCachingEl.checked = settings.promptCaching === true;
334
- }
335
-
336
- export function setStatus(node, text, cls = '') {
337
- const elNode = $(node);
338
- if (!elNode) return;
339
- setSummaryText(elNode, text, cls);
340
- }
341
-
342
- export async function refreshMcp({ connect = false } = {}) {
343
- const data = await api(`/api/mcp${connect ? '?connect=1' : ''}`).catch(() => null);
344
- if (!data) return;
345
- if (mcpModeEl && data.mode) mcpModeEl.value = data.mode;
346
- if (mcpPinnedEl && Array.isArray(data.pinned) && document.activeElement !== mcpPinnedEl) {
347
- mcpPinnedEl.value = data.pinned.join(', ');
348
- }
349
- if (mcpCountEl) {
350
- mcpCountEl.textContent =
351
- data.toolCount == null
352
- ? ''
353
- : `${data.toolCount} herramientas · ${data.lazy ? 'diferidas (mcp_call)' : 'completas en contexto'}`;
354
- }
355
- clear(mcpListEl);
356
- if (!data.servers.length) {
357
- mcpListEl.appendChild(emptyState('Sin servidores MCP', 'Tocá «+ nuevo» o prendé uno de los sugeridos de abajo.'));
358
- return;
359
- }
360
- for (const s of data.servers) {
361
- const li = el('li', s.enabled ? 'active' : '');
362
- li.appendChild(el('span', `pdot${s.ok !== false ? ' on' : ''}`));
363
- const main = el('div', 'grow');
364
- const title = el('div');
365
- title.append(el('span', 'pid', s.id), document.createTextNode(' '), el('span', 'pmeta', s.name || ''));
366
- const detail = s.transport === 'http' ? s.url || '' : `${s.command || ''} ${(s.args || []).join(' ')}`.trim();
367
- const parts = [s.transport, detail || '(sin destino)', s.source === 'config' ? 'tamnora.json' : 'guardado'];
368
- if (connect) {
369
- if (s.ok === false) parts.push(`error: ${s.error}`);
370
- else parts.push(`${(s.tools || []).length} tools`);
371
- }
372
- main.append(title, el('div', 'pmeta', parts.join(' · ')));
373
- li.appendChild(main);
374
- const actions = el('div', 'pactions');
375
- actions.appendChild(button(s.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleMcp(s); }));
376
- actions.appendChild(button('editar', (event) => { event.stopPropagation(); openMcpForm(s); }));
377
- if (s.source !== 'config') actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeMcp(s.id); }, 'btn danger'));
378
- li.appendChild(actions);
379
- li.addEventListener('click', () => openMcpForm(s));
380
- mcpListEl.appendChild(li);
381
- }
382
- }
383
-
384
- export function syncMcpFormFields() {
385
- const http = $('#mf-transport').value === 'http';
386
- $('#mf-command-field').hidden = http;
387
- $('#mf-args-field').hidden = http;
388
- $('#mf-env-field').hidden = http;
389
- $('#mf-inherit-field').hidden = http;
390
- $('#mf-url-field').hidden = !http;
391
- }
392
-
393
- export function openMcpForm(server = null) {
394
- $('#mcp-title').textContent = server ? `editar ${server.id}` : 'nuevo servidor MCP';
395
- $('#mf-id').value = server?.id ?? '';
396
- $('#mf-id').readOnly = Boolean(server);
397
- $('#mf-name').value = server?.name ?? '';
398
- $('#mf-transport').value = server?.transport === 'http' ? 'http' : 'stdio';
399
- $('#mf-command').value = server?.command ?? '';
400
- $('#mf-args').value = (server?.args ?? []).join(' ');
401
- $('#mf-url').value = server?.url ?? '';
402
- $('#mf-env').value = Object.entries(server?.env ?? {})
403
- .map(([key, value]) => `${key}=${value ?? ''}`)
404
- .join('\n');
405
- $('#mf-inherit-env').checked = server?.inheritEnv === true;
406
- $('#mf-enabled').checked = server ? server.enabled !== false : true;
407
- syncMcpFormFields();
408
- setStatus('#mf-status', '');
409
- mcpModal.showModal();
410
- if (!server) $('#mf-id').focus();
411
- }
412
-
413
- export function closeMcpForm() {
414
- if (mcpModal.open) mcpModal.close();
415
- }
416
-
417
- export async function toggleMcp(server) {
418
- await toggleEntity(`/api/mcp/servers/${encodeURIComponent(server.id)}`, {
419
- body: {
420
- name: server.name,
421
- transport: server.transport,
422
- command: server.command,
423
- args: server.args,
424
- url: server.url,
425
- env: server.env,
426
- enabled: !server.enabled,
427
- },
428
- errorLabel: 'actualizar MCP',
429
- refresh: refreshMcp,
430
- });
431
- }
432
-
433
- export async function removeMcp(id) {
434
- await deleteEntity(`/api/mcp/servers/${encodeURIComponent(id)}`, {
435
- confirmText: `¿Eliminar el servidor MCP "${id}"?`,
436
- refresh: refreshMcp,
437
- });
438
- }
439
-
440
- export function parseEnv(text) {
441
- const out = {};
442
- for (const line of String(text ?? '').split('\n')) {
443
- const trimmed = line.trim();
444
- if (!trimmed || trimmed.startsWith('#')) continue;
445
- const eq = trimmed.indexOf('=');
446
- if (eq === -1) continue;
447
- out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1);
448
- }
449
- return out;
450
- }
451
-
452
- export async function submitMcp(event) {
453
- event.preventDefault();
454
- const id = $('#mf-id').value.trim();
455
- if (!id) return setStatus('#mf-status', 'Falta el id.', 'err');
456
- const body = {
457
- name: $('#mf-name').value.trim() || id,
458
- transport: $('#mf-transport').value,
459
- command: $('#mf-command').value.trim(),
460
- args: $('#mf-args').value.trim(),
461
- url: $('#mf-url').value.trim(),
462
- env: parseEnv($('#mf-env').value),
463
- inheritEnv: $('#mf-inherit-env').checked,
464
- enabled: $('#mf-enabled').checked,
465
- };
466
- try {
467
- await api(`/api/mcp/servers/${encodeURIComponent(id)}`, { method: 'PUT', body });
468
- } catch (error) {
469
- return setStatus('#mf-status', error.message, 'err');
470
- }
471
- closeMcpForm();
472
- refreshMcp();
473
- }
474
-
475
- export async function refreshSkills() {
476
- const data = await api('/api/skills').catch(() => null);
477
- if (!data) return;
478
- clear(skillListEl);
479
- if (!data.skills.length) {
480
- skillListEl.appendChild(emptyState('Sin skills todavía', 'Tocá «+ nueva» o instalá una de las sugeridas del catálogo.'));
481
- return;
482
- }
483
- for (const s of data.skills) {
484
- const li = el('li', s.enabled ? 'active' : '');
485
- li.appendChild(el('span', `pdot${s.enabled ? ' on' : ''}`));
486
- const main = el('div', 'grow');
487
- main.append(
488
- el('div', '', s.name || s.id),
489
- el('div', 'pmeta', s.description || s.when || '(sin descripción)')
490
- );
491
- li.appendChild(main);
492
- const actions = el('div', 'pactions');
493
- actions.appendChild(button(s.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleSkill(s.id); }));
494
- actions.appendChild(button('editar', (event) => { event.stopPropagation(); openSkillForm(s.id); }));
495
- if (s.removable) actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeSkill(s.id); }, 'btn danger'));
496
- li.appendChild(actions);
497
- li.addEventListener('click', () => openSkillForm(s.id));
498
- skillListEl.appendChild(li);
499
- }
500
- }
501
-
502
- export async function openSkillForm(id = null) {
503
- $('#skill-title').textContent = id ? `editar ${id}` : 'nueva skill';
504
- $('#sf-id').value = id ?? '';
505
- $('#sf-id').readOnly = Boolean(id);
506
- $('#sf-name').value = '';
507
- $('#sf-description').value = '';
508
- $('#sf-when').value = '';
509
- $('#sf-body').value = '';
510
- setStatus('#sf-status', '');
511
- if (id) {
512
- const data = await api(`/api/skills/${encodeURIComponent(id)}`).catch(() => null);
513
- if (data?.skill) {
514
- $('#sf-name').value = data.skill.name ?? '';
515
- $('#sf-description').value = data.skill.description ?? '';
516
- $('#sf-when').value = data.skill.when ?? '';
517
- $('#sf-body').value = data.skill.body ?? '';
518
- }
519
- }
520
- skillModal.showModal();
521
- if (!id) $('#sf-id').focus();
522
- }
523
-
524
- export function closeSkillForm() {
525
- if (skillModal.open) skillModal.close();
526
- }
527
-
528
- export async function toggleSkill(id) {
529
- await toggleEntity(`/api/skills/${encodeURIComponent(id)}/toggle`, {
530
- method: 'POST',
531
- errorLabel: 'cambiar la skill',
532
- refresh: refreshSkills,
533
- });
534
- }
535
-
536
- export async function removeSkill(id) {
537
- await deleteEntity(`/api/skills/${encodeURIComponent(id)}`, {
538
- confirmText: `¿Eliminar la skill "${id}"?`,
539
- refresh: refreshSkills,
540
- });
541
- }
542
-
543
- export async function submitSkill(event) {
544
- event.preventDefault();
545
- const id = $('#sf-id').value.trim();
546
- if (!id) return setStatus('#sf-status', 'Falta el id.', 'err');
547
- const body = {
548
- name: $('#sf-name').value.trim() || id,
549
- description: $('#sf-description').value.trim(),
550
- when: $('#sf-when').value.trim(),
551
- body: $('#sf-body').value,
552
- };
553
- try {
554
- await api(`/api/skills/${encodeURIComponent(id)}`, { method: 'PUT', body });
555
- } catch (error) {
556
- return setStatus('#sf-status', error.message, 'err');
557
- }
558
- closeSkillForm();
559
- refreshSkills();
560
- }
561
-
562
- /* --- ajustes: Subagentes ------------------------------------------------- */
563
-
564
- export async function refreshAgents() {
565
- if (!agentListEl) return;
566
- const data = await api('/api/agents').catch(() => null);
567
- if (!data) return;
568
- clear(agentListEl);
569
- if (!data.agents.length) {
570
- agentListEl.appendChild(
571
- emptyState(
572
- 'Sin subagentes todavía',
573
- 'Tocá «+ nuevo» para crear uno: el agente principal va a verlo en su catálogo y podrá delegarle tareas con task.'
574
- )
575
- );
576
- return;
577
- }
578
- for (const agent of data.agents) {
579
- const li = el('li', agent.enabled ? 'active' : '');
580
- li.appendChild(el('span', `pdot${agent.enabled ? ' on' : ''}`));
581
- const main = el('div', 'grow');
582
- const details = [
583
- agent.provider || 'proveedor heredado',
584
- agent.model || 'modelo heredado',
585
- agent.mode || 'modo heredado',
586
- agent.effort ? `esfuerzo ${agent.effort}` : '',
587
- Array.isArray(agent.skills) ? `${agent.skills.length} skill(s)` : 'todas las skills',
588
- ].filter(Boolean);
589
- main.append(
590
- el('div', '', `${agent.name || agent.id} · ${agent.id}`),
591
- el('div', 'pmeta', agent.description || details.join(' · '))
592
- );
593
- li.appendChild(main);
594
- const actions = el('div', 'pactions');
595
- actions.appendChild(button(agent.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleAgent(agent.id); }));
596
- actions.appendChild(button('editar', (event) => { event.stopPropagation(); openAgentForm(agent.id); }));
597
- if (agent.removable) {
598
- actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeAgent(agent.id); }, 'btn danger'));
599
- }
600
- li.appendChild(actions);
601
- li.addEventListener('click', () => openAgentForm(agent.id));
602
- agentListEl.appendChild(li);
603
- }
604
- }
605
-
606
- function fillAgentProviders() {
607
- const select = $('#af-provider');
608
- if (!select) return;
609
- const current = select.value;
610
- clear(select);
611
- const inherit = document.createElement('option');
612
- inherit.value = '';
613
- inherit.textContent = 'heredar';
614
- select.appendChild(inherit);
615
- for (const provider of state.providers ?? []) {
616
- const option = document.createElement('option');
617
- option.value = provider.id;
618
- option.textContent = provider.label && provider.label !== provider.id ? `${provider.label} (${provider.id})` : provider.id;
619
- select.appendChild(option);
620
- }
621
- select.value = current;
622
- }
623
-
624
- function fillAgentSkills(selected = null) {
625
- const select = $('#af-skills');
626
- if (!select) return;
627
- clear(select);
628
- for (const skill of state.skills ?? []) {
629
- const option = document.createElement('option');
630
- option.value = skill.id;
631
- option.textContent = `${skill.name || skill.id}${skill.enabled ? '' : ' (deshabilitada)'}`;
632
- option.disabled = !skill.enabled;
633
- select.appendChild(option);
634
- }
635
- const wanted = new Set(Array.isArray(selected) ? selected : []);
636
- for (const option of select.options) option.selected = wanted.has(option.value);
637
- }
638
-
639
- export async function openAgentForm(id = null) {
640
- $('#agent-title').textContent = id ? `editar ${id}` : 'nuevo subagente';
641
- $('#af-id').value = id ?? '';
642
- $('#af-id').readOnly = Boolean(id);
643
- $('#af-name').value = '';
644
- $('#af-description').value = '';
645
- $('#af-model').value = '';
646
- $('#af-rounds').value = '';
647
- $('#af-tools').value = '';
648
- $('#af-body').value = '';
649
- setStatus('#af-status', '');
650
- const providers = await api('/api/providers').catch(() => null);
651
- if (providers?.providers) state.providers = providers.providers;
652
- const skills = await api('/api/skills').catch(() => null);
653
- state.skills = skills?.skills ?? [];
654
- const effort = $('#af-effort');
655
- if (effort) effort.value = '';
656
- fillAgentProviders();
657
- $('#af-provider').value = '';
658
- $('#af-mode').value = '';
659
- fillAgentSkills([]);
660
- if (id) {
661
- const data = await api(`/api/agents/${encodeURIComponent(id)}`).catch(() => null);
662
- if (data?.agent) {
663
- const agent = data.agent;
664
- $('#af-name').value = agent.name ?? '';
665
- $('#af-description').value = agent.description ?? '';
666
- $('#af-model').value = agent.model ?? '';
667
- $('#af-provider').value = agent.provider ?? '';
668
- $('#af-mode').value = agent.mode ?? '';
669
- $('#af-effort').value = agent.effort ?? '';
670
- $('#af-rounds').value = agent.maxRounds || '';
671
- $('#af-tools').value = (agent.tools ?? []).join(', ');
672
- fillAgentSkills(agent.skills ?? null);
673
- $('#af-body').value = agent.body ?? '';
674
- }
675
- }
676
- agentModal.showModal();
677
- if (!id) $('#af-id').focus();
678
- }
679
-
680
- export function closeAgentForm() {
681
- if (agentModal?.open) agentModal.close();
682
- }
683
-
684
- export async function toggleAgent(id) {
685
- await toggleEntity(`/api/agents/${encodeURIComponent(id)}/toggle`, {
686
- method: 'POST',
687
- errorLabel: 'cambiar el subagente',
688
- refresh: refreshAgents,
689
- });
690
- }
691
-
692
- export async function removeAgent(id) {
693
- await deleteEntity(`/api/agents/${encodeURIComponent(id)}`, {
694
- confirmText: `¿Eliminar el subagente "${id}"?`,
695
- refresh: refreshAgents,
696
- });
697
- }
698
-
699
- export async function submitAgent(event) {
700
- event.preventDefault();
701
- const id = $('#af-id').value.trim();
702
- if (!id) return setStatus('#af-status', 'Falta el id.', 'err');
703
- const skillsSelect = $('#af-skills');
704
- const selectedSkills = skillsSelect ? [...skillsSelect.selectedOptions].map((option) => option.value) : [];
705
- const body = {
706
- name: $('#af-name').value.trim() || id,
707
- description: $('#af-description').value.trim(),
708
- provider: $('#af-provider').value.trim(),
709
- model: $('#af-model').value.trim(),
710
- mode: $('#af-mode').value,
711
- effort: $('#af-effort').value,
712
- maxRounds: Number($('#af-rounds').value) || 0,
713
- tools: $('#af-tools').value.split(',').map((item) => item.trim()).filter(Boolean),
714
- skills: selectedSkills.length ? selectedSkills : null,
715
- body: $('#af-body').value,
716
- };
717
- try {
718
- await api(`/api/agents/${encodeURIComponent(id)}`, { method: 'PUT', body });
719
- } catch (error) {
720
- return setStatus('#af-status', error.message, 'err');
721
- }
722
- closeAgentForm();
723
- refreshAgents();
724
- }
725
-
726
- /* --- ajustes: Memoria ---------------------------------------------------- */
727
-
728
- export async function refreshMemory() {
729
- if (!memoryListEl) return;
730
- const data = await api('/api/memory').catch(() => null);
731
- if (!data) return;
732
- if (memoryIndexEl && document.activeElement !== memoryIndexEl) memoryIndexEl.value = data.index ?? '';
733
- clear(memoryListEl);
734
- if (!data.topics?.length) {
735
- memoryListEl.appendChild(emptyState('Sin memoria todavía', 'El agente guarda acá lo que conviene recordar del proyecto; también podés crear un tema con «+ tema».'));
736
- return;
737
- }
738
- for (const t of data.topics) {
739
- const li = el('li', 'active');
740
- const main = el('div', 'grow');
741
- main.append(
742
- el('div', '', t.title || t.name),
743
- el('div', 'pmeta', `${t.name}${t.bytes ? ` · ${Math.max(1, Math.round(t.bytes / 1024))} KB` : ''}`)
744
- );
745
- li.appendChild(main);
746
- const actions = el('div', 'pactions');
747
- actions.appendChild(button('editar', (event) => { event.stopPropagation(); openMemoryForm(t.name); }));
748
- actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeMemory(t.name); }, 'btn danger'));
749
- li.appendChild(actions);
750
- li.addEventListener('click', () => openMemoryForm(t.name));
751
- memoryListEl.appendChild(li);
752
- }
753
- }
754
-
755
- export async function openMemoryForm(name = null) {
756
- $('#memory-title').textContent = name ? `editar ${name}` : 'nuevo tema';
757
- $('#mem-name').value = name ? name.replace(/\.md$/i, '') : '';
758
- $('#mem-name').readOnly = Boolean(name);
759
- $('#mem-title').value = '';
760
- $('#mem-body').value = '';
761
- setStatus('#mem-status', '');
762
- if (name) {
763
- const data = await api(`/api/memory/${encodeURIComponent(name)}`).catch(() => null);
764
- if (data?.topic) {
765
- $('#mem-title').value = data.topic.title ?? '';
766
- $('#mem-body').value = data.topic.body ?? '';
767
- }
768
- }
769
- memoryModal.showModal();
770
- if (!name) $('#mem-name').focus();
771
- }
772
-
773
- export function closeMemoryForm() {
774
- if (memoryModal.open) memoryModal.close();
775
- }
776
-
777
- export async function removeMemory(name) {
778
- await deleteEntity(`/api/memory/${encodeURIComponent(name)}`, {
779
- confirmText: `¿Borrar la memoria "${name}"?`,
780
- refresh: refreshMemory,
781
- });
782
- }
783
-
784
- export async function submitMemory(event) {
785
- event.preventDefault();
786
- const name = $('#mem-name').value.trim();
787
- if (!name) return setStatus('#mem-status', 'Falta el tema.', 'err');
788
- const body = { title: $('#mem-title').value.trim(), body: $('#mem-body').value };
789
- try {
790
- await api(`/api/memory/${encodeURIComponent(name)}`, { method: 'PUT', body });
791
- } catch (error) {
792
- return setStatus('#mem-status', error.message, 'err');
793
- }
794
- closeMemoryForm();
795
- refreshMemory();
796
- }
797
-
798
- export async function saveMemoryIndex() {
799
- try {
800
- await api('/api/memory', { method: 'PUT', body: { index: memoryIndexEl.value } });
801
- } catch (error) {
802
- return setStatus('#memory-status', error.message, 'err');
803
- }
804
- setStatus('#memory-status', 'guardado', 'ok');
805
- refreshMemory();
806
- }
807
-
808
- /* --- ajustes: Tareas programadas ----------------------------------------- */
809
-
810
- export function fmtWhen(ms) {
811
- try {
812
- return new Date(ms).toLocaleString();
813
- } catch {
814
- return '';
815
- }
816
- }
817
-
818
- export function scheduleTypeFields() {
819
- const type = $('#sc-type').value;
820
- const agent = type === 'agent';
821
- $('#sc-agent-prompt-row').hidden = !agent;
822
- $('#sc-agent-mode-row').hidden = !agent;
823
- $('#sc-shell-cmd-row').hidden = agent;
824
- $('#sc-shell-args-row').hidden = agent;
825
- }
826
-
827
- export async function refreshSchedules() {
828
- if (!scheduleListEl) return;
829
- const data = await api('/api/schedules').catch(() => null);
830
- if (!data) return;
831
- clear(scheduleListEl);
832
- if (!data.schedules.length) {
833
- scheduleListEl.appendChild(emptyState('Sin tareas programadas', 'Con «+ tarea» programás un comando o un turno del agente: cada tanto, a diario o con cron.'));
834
- return;
835
- }
836
- for (const s of data.schedules) {
837
- const li = el('li', s.enabled ? 'active' : '');
838
- li.appendChild(el('span', `pdot${s.enabled ? ' on' : ''}`));
839
- const main = el('div', 'grow');
840
- const meta = [s.type, s.spec];
841
- if (s.nextRun) meta.push(`próxima ${fmtWhen(s.nextRun)}`);
842
- if (s.lastStatus) meta.push(s.lastStatus);
843
- main.append(el('div', '', s.name || s.id), el('div', 'pmeta', meta.join(' · ')));
844
- li.appendChild(main);
845
- const actions = el('div', 'pactions');
846
- actions.appendChild(button('correr', (event) => { event.stopPropagation(); runSchedule(s.id); }));
847
- actions.appendChild(button(s.enabled ? 'pausar' : 'activar', (event) => { event.stopPropagation(); toggleSchedule(s.id); }));
848
- actions.appendChild(button('editar', (event) => { event.stopPropagation(); openScheduleForm(s.id); }));
849
- actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeSchedule(s.id); }, 'btn danger'));
850
- li.appendChild(actions);
851
- li.addEventListener('click', () => openScheduleForm(s.id));
852
- scheduleListEl.appendChild(li);
853
- }
854
- }
855
-
856
- export async function openScheduleForm(id = null) {
857
- $('#schedule-title').textContent = id ? `editar ${id}` : 'nueva tarea';
858
- $('#sc-id').value = id ?? '';
859
- $('#sc-id').readOnly = Boolean(id);
860
- $('#sc-name').value = '';
861
- $('#sc-type').value = 'agent';
862
- $('#sc-spec').value = '';
863
- $('#sc-prompt').value = '';
864
- $('#sc-mode').value = 'agent';
865
- $('#sc-command').value = '';
866
- $('#sc-args').value = '';
867
- $('#sc-cwd').value = '';
868
- $('#sc-enabled').checked = true;
869
- setStatus('#sc-status', '');
870
- if (id) {
871
- const data = await api('/api/schedules').catch(() => null);
872
- const s = data?.schedules?.find((item) => item.id === id);
873
- if (s) {
874
- $('#sc-name').value = s.name ?? '';
875
- $('#sc-type').value = s.type ?? 'agent';
876
- $('#sc-spec').value = s.spec ?? '';
877
- $('#sc-cwd').value = s.cwd ?? '';
878
- $('#sc-enabled').checked = s.enabled !== false;
879
- const payload = s.payload ?? {};
880
- if (s.type === 'shell') {
881
- $('#sc-command').value = payload.command ?? '';
882
- $('#sc-args').value = Array.isArray(payload.args) ? payload.args.join(' ') : payload.args ?? '';
883
- } else {
884
- $('#sc-prompt').value = payload.prompt ?? '';
885
- $('#sc-mode').value = payload.mode ?? 'agent';
886
- }
887
- }
888
- }
889
- scheduleTypeFields();
890
- scheduleModal.showModal();
891
- if (!id) $('#sc-id').focus();
892
- }
893
-
894
- export function closeScheduleForm() {
895
- if (scheduleModal.open) scheduleModal.close();
896
- }
897
-
898
- export async function submitSchedule(event) {
899
- event.preventDefault();
900
- const id = $('#sc-id').value.trim();
901
- if (!id) return setStatus('#sc-status', 'Falta el id.', 'err');
902
- const type = $('#sc-type').value;
903
- const body = {
904
- id,
905
- name: $('#sc-name').value.trim() || id,
906
- type,
907
- spec: $('#sc-spec').value.trim(),
908
- cwd: $('#sc-cwd').value.trim() || undefined,
909
- enabled: $('#sc-enabled').checked,
910
- };
911
- if (type === 'shell') {
912
- body.command = $('#sc-command').value.trim();
913
- body.args = $('#sc-args').value.trim();
914
- } else {
915
- body.prompt = $('#sc-prompt').value.trim();
916
- body.mode = $('#sc-mode').value;
917
- }
918
- try {
919
- const exists = $('#sc-id').readOnly;
920
- await api(exists ? `/api/schedules/${encodeURIComponent(id)}` : '/api/schedules', {
921
- method: exists ? 'PUT' : 'POST',
922
- body,
923
- });
924
- } catch (error) {
925
- return setStatus('#sc-status', error.message, 'err');
926
- }
927
- closeScheduleForm();
928
- refreshSchedules();
929
- }
930
-
931
- export async function toggleSchedule(id) {
932
- await toggleEntity(`/api/schedules/${encodeURIComponent(id)}/toggle`, {
933
- method: 'POST',
934
- errorLabel: 'cambiar la tarea',
935
- refresh: refreshSchedules,
936
- });
937
- }
938
-
939
- export async function removeSchedule(id) {
940
- await deleteEntity(`/api/schedules/${encodeURIComponent(id)}`, {
941
- confirmText: `¿Eliminar la tarea "${id}"?`,
942
- refresh: refreshSchedules,
943
- });
944
- }
945
-
946
- export async function runSchedule(id) {
947
- setStatus('#schedule-status', `corriendo ${id}…`);
948
- try {
949
- await api(`/api/schedules/${encodeURIComponent(id)}/run`, { method: 'POST' });
950
- } catch (error) {
951
- setStatus('#schedule-status', error.message, 'err');
952
- refreshSchedules();
953
- return;
954
- }
955
- setStatus('#schedule-status', `${id}: listo`, 'ok');
956
- refreshSchedules();
957
- }
958
-
959
- export async function refreshChannels() {
960
- if (!tgStatusEl) return;
961
- const data = await api('/api/channels').catch(() => null);
962
- if (!data?.telegram) return;
963
- const tg = data.telegram;
964
- tgStatusEl.className = `summary${tg.running ? ' ok' : ''}`;
965
- tgStatusEl.textContent = tg.running
966
- ? `activo${tg.tokenHint ? ` · ${tg.tokenHint}` : ''}`
967
- : tg.tokenSet
968
- ? 'token guardado (bot detenido)'
969
- : 'sin configurar';
970
- if (tgChatsEl && document.activeElement !== tgChatsEl) {
971
- tgChatsEl.value = (tg.allowedChatIds ?? []).join(', ');
972
- }
973
- if (tgTranscribeEl && document.activeElement !== tgTranscribeEl) {
974
- tgTranscribeEl.value = tg.transcribeModel ?? 'whisper-1';
975
- }
976
- if (tgTranscribeProviderEl && document.activeElement !== tgTranscribeProviderEl) {
977
- tgTranscribeProviderEl.value = tg.transcribeProvider ?? '';
978
- }
979
- const notify = tg.notify ?? {};
980
- if (tgNotifyEl) tgNotifyEl.checked = notify.enabled === true;
981
- if (tgNotifyDoneEl) tgNotifyDoneEl.checked = notify.done !== false;
982
- if (tgNotifyErrorEl) tgNotifyErrorEl.checked = notify.error !== false;
983
- if (tgNotifyPermissionEl) tgNotifyPermissionEl.checked = notify.permission !== false;
984
- if (tgNotifyAgentEl) tgNotifyAgentEl.checked = notify.agent !== false;
985
- if (tgTokenEl && document.activeElement !== tgTokenEl) {
986
- tgTokenEl.value = '';
987
- tgTokenEl.placeholder = tg.tokenHint ? `${tg.tokenHint} (dejar vacío para no cambiar)` : '123456:ABC…';
988
- }
989
- }
990
-
991
- export async function submitChannel() {
992
- if (!tgTokenEl) return;
993
- setStatus('#tg-msg', 'guardando…');
994
- const body = {
995
- allowedChatIds: tgChatsEl?.value ?? '',
996
- transcribeModel: tgTranscribeEl?.value.trim() || 'whisper-1',
997
- transcribeProvider: tgTranscribeProviderEl?.value.trim() || '',
998
- notify: {
999
- enabled: tgNotifyEl?.checked === true,
1000
- done: tgNotifyDoneEl?.checked !== false,
1001
- error: tgNotifyErrorEl?.checked !== false,
1002
- permission: tgNotifyPermissionEl?.checked !== false,
1003
- agent: tgNotifyAgentEl?.checked !== false,
1004
- },
1005
- };
1006
- const token = tgTokenEl.value.trim();
1007
- if (token) body.token = token;
1008
- const data = await api('/api/channels/telegram', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
1009
- if (data?.error) return setStatus('#tg-msg', data.error, 'err');
1010
- setStatus('#tg-msg', data.telegram?.running ? 'guardado · bot activo' : 'guardado', 'ok');
1011
- refreshChannels();
1012
- }
1013
-
1014
- export async function testChannel() {
1015
- if (!tgTokenEl) return;
1016
- setStatus('#tg-msg', 'probando…');
1017
- const data = await api('/api/channels/telegram/test', {
1018
- method: 'POST',
1019
- body: { token: tgTokenEl.value.trim() },
1020
- }).catch((e) => ({ ok: false, error: e.message }));
1021
- if (data?.ok) setStatus('#tg-msg', `token OK${data.username ? ` · @${data.username}` : ''}`, 'ok');
1022
- else setStatus('#tg-msg', data?.error || 'token inválido', 'err');
1023
- }
1024
-
1025
- /* --- ajustes: asesor ------------------------------------------------------- */
1026
-
1027
- export function aplicarAsesor(data) {
1028
- if (!data) return;
1029
- if (asesorEnabledEl) asesorEnabledEl.checked = data.enabled === true;
1030
- if (asesorCodeEl) asesorCodeEl.checked = data.code !== false;
1031
- if (asesorExplainEl) asesorExplainEl.checked = data.explain !== false;
1032
- if (asesorAdviceEl) asesorAdviceEl.checked = data.advice !== false;
1033
- if (asesorSummaryEl) asesorSummaryEl.checked = data.summary === true;
1034
- if (asesorWordsEl) asesorWordsEl.value = data.maxWords ?? 40;
1035
- if (asesorNotesEl) asesorNotesEl.value = data.notes ?? '';
1036
- if (asesorProviderEl) asesorProviderEl.value = data.provider ?? '';
1037
- if (asesorModelEl) asesorModelEl.value = data.model ?? '';
1038
- const estado = $('#asesor-status');
1039
- if (estado) {
1040
- estado.className = `summary${data.enabled ? ' ok' : ''}`;
1041
- estado.textContent = data.enabled ? 'activo' : 'apagado';
1042
- }
1043
- }
1044
-
1045
- export async function refreshAsesor() {
1046
- if (!asesorEnabledEl) return;
1047
- const data = await api('/api/advisor/settings').catch(() => null);
1048
- if (data) aplicarAsesor(data);
1049
- }
1050
-
1051
- export function asesorBody() {
1052
- return {
1053
- enabled: asesorEnabledEl?.checked === true,
1054
- code: asesorCodeEl?.checked !== false,
1055
- explain: asesorExplainEl?.checked !== false,
1056
- advice: asesorAdviceEl?.checked !== false,
1057
- summary: asesorSummaryEl?.checked === true,
1058
- maxWords: Number(asesorWordsEl?.value) || 40,
1059
- notes: asesorNotesEl?.value ?? '',
1060
- provider: asesorProviderEl?.value ?? '',
1061
- model: asesorModelEl?.value ?? '',
1062
- };
1063
- }
1064
-
1065
- export async function submitAsesor() {
1066
- setStatus('#asesor-msg', 'guardando…');
1067
- const data = await api('/api/advisor/settings', { method: 'PUT', body: asesorBody() }).catch((e) => ({
1068
- error: e.message,
1069
- }));
1070
- if (data?.error) return setStatus('#asesor-msg', data.error, 'err');
1071
- aplicarAsesor(data);
1072
- setStatus('#asesor-msg', 'guardado', 'ok');
1073
- }
1074
-
1075
- export async function testAsesor() {
1076
- setStatus('#asesor-msg', 'consultando…');
1077
- const data = await api('/api/advisor/test', { method: 'POST' }).catch((e) => ({ error: e.message }));
1078
- if (data?.error) return setStatus('#asesor-msg', data.error, 'err');
1079
- setStatus('#asesor-msg', data?.speak && data.text ? data.text : '(no diría nada en este caso)', 'ok');
1080
- }
1081
-
1082
- /* --- ajustes: perfil del usuario ------------------------------------------ */
1083
-
1084
- export const perfil = {
1085
- address: $('#perfil-address'),
1086
- name: $('#perfil-name'),
1087
- language: $('#perfil-language'),
1088
- notes: $('#perfil-notes'),
1089
- fields: $('#perfil-fields'),
1090
- add: $('#perfil-add-field'),
1091
- save: $('#perfil-save'),
1092
- };
1093
-
1094
- export function addPerfilFieldRow(key = '', value = '') {
1095
- if (!perfil.fields) return;
1096
- const row = el('div', 'perfil-field');
1097
- const keyInput = el('input', 'pf-key');
1098
- keyInput.placeholder = 'clave';
1099
- keyInput.value = key;
1100
- keyInput.spellcheck = false;
1101
- const valueInput = el('input', 'pf-value');
1102
- valueInput.placeholder = 'valor';
1103
- valueInput.value = value;
1104
- valueInput.spellcheck = false;
1105
- const remove = button('', () => row.remove(), 'btn icon');
1106
- remove.appendChild(icon('x', 12));
1107
- row.append(keyInput, valueInput, remove);
1108
- perfil.fields.appendChild(row);
1109
- }
1110
-
1111
- export function renderPerfilFields(fields = {}) {
1112
- if (!perfil.fields) return;
1113
- clear(perfil.fields);
1114
- for (const [key, value] of Object.entries(fields)) addPerfilFieldRow(key, value);
1115
- }
1116
-
1117
- export function collectPerfilFields() {
1118
- const out = {};
1119
- for (const row of perfil.fields?.querySelectorAll('.perfil-field') ?? []) {
1120
- const key = row.querySelector('.pf-key').value.trim();
1121
- const value = row.querySelector('.pf-value').value.trim();
1122
- if (key && value) out[key] = value;
1123
- }
1124
- return out;
1125
- }
1126
-
1127
- export async function refreshProfile() {
1128
- if (!perfil.address) return;
1129
- const data = await api('/api/profile').catch(() => null);
1130
- if (!data?.profile) return;
1131
- const p = data.profile;
1132
- perfil.address.value = p.address ?? '';
1133
- perfil.name.value = p.name ?? '';
1134
- perfil.language.value = p.language ?? '';
1135
- perfil.notes.value = p.notes ?? '';
1136
- renderPerfilFields(p.fields ?? {});
1137
- setStatus('#perfil-status', p.address || p.name ? `trato: ${p.address || p.name}` : 'sin datos', '');
1138
- }
1139
-
1140
- export async function submitProfile() {
1141
- if (!perfil.address) return;
1142
- setStatus('#perfil-msg', 'guardando…');
1143
- const body = {
1144
- address: perfil.address.value.trim(),
1145
- name: perfil.name.value.trim(),
1146
- language: perfil.language.value.trim(),
1147
- notes: perfil.notes.value.trim(),
1148
- fields: collectPerfilFields(),
1149
- };
1150
- const data = await api('/api/profile', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
1151
- if (data?.error) return setStatus('#perfil-msg', data.error, 'err');
1152
- setStatus('#perfil-msg', 'guardado', 'ok');
1153
- refreshProfile();
1154
- }
1155
-
1156
- export async function importMcp() {
1157
- const input = $('#mcp-import');
1158
- const value = input.value.trim();
1159
- if (!value) return;
1160
- const payload = value.startsWith('{')
1161
- ? { json: value }
1162
- : /^https?:\/\//i.test(value)
1163
- ? { url: value }
1164
- : { name: value };
1165
- await importEntity({
1166
- url: '/api/mcp/import',
1167
- body: payload,
1168
- input,
1169
- statusSel: '#mcp-import-status',
1170
- refresh: refreshMcp,
1171
- okMessage: (data) => {
1172
- const required = (data.requiredEnv ?? []).length ? ` · requiere env: ${data.requiredEnv.join(', ')}` : '';
1173
- return `Importado ${data.imported}${required}`;
1174
- },
1175
- });
1176
- }
1177
-
1178
- export async function importSkill() {
1179
- const input = $('#skill-import');
1180
- const ref = input.value.trim();
1181
- if (!ref) return;
1182
- await importEntity({
1183
- url: '/api/skills/import',
1184
- body: { ref },
1185
- input,
1186
- statusSel: '#skill-import-status',
1187
- refresh: refreshSkills,
1188
- okMessage: (data) => `Importada ${data.installed} (${data.source})`,
1189
- });
1190
- }
1191
-
1192
- /* --- ajustes: biblioteca y buscador de MCP y skills ----------------------- */
1193
-
1194
- export async function refreshMcpCatalog() {
1195
- if (!mcpCatalogEl) return;
1196
- const data = await api('/api/mcp/catalog').catch(() => null);
1197
- if (!data) return;
1198
- clear(mcpCatalogEl);
1199
- for (const entry of data.catalog) {
1200
- const li = el('li', entry.installed ? 'active' : '');
1201
- const main = el('div', 'grow');
1202
- main.append(el('div', '', entry.name), el('div', 'pmeta', entry.description || ''));
1203
- li.appendChild(main);
1204
- const actions = el('div', 'pactions');
1205
- if (entry.requiredEnv?.length) actions.appendChild(el('span', 'pmeta', `env: ${entry.requiredEnv.join(', ')}`));
1206
- if (entry.installed) actions.appendChild(el('span', 'pmeta', 'instalado'));
1207
- else actions.appendChild(button('agregar', (event) => { event.stopPropagation(); installCatalogMcp(entry.id); }));
1208
- li.appendChild(actions);
1209
- mcpCatalogEl.appendChild(li);
1210
- }
1211
- }
1212
-
1213
- export async function installCatalogMcp(id) {
1214
- setStatus('#mcp-import-status', `agregando ${id}…`);
1215
- const data = await api('/api/mcp/import', { method: 'POST', body: { catalog: id } }).catch((e) => ({ error: e.message }));
1216
- if (data?.error) return setStatus('#mcp-import-status', data.error, 'err');
1217
- setStatus('#mcp-import-status', `agregado ${id} (deshabilitado)`, 'ok');
1218
- await refreshMcp();
1219
- await refreshMcpCatalog();
1220
- }
1221
-
1222
- export async function searchMcp() {
1223
- const input = $('#mcp-search');
1224
- const q = input?.value.trim();
1225
- if (!q) return;
1226
- setStatus('#mcp-search-status', 'buscando…');
1227
- const data = await api(`/api/mcp/search?q=${encodeURIComponent(q)}`).catch((e) => ({ error: e.message }));
1228
- if (data?.error) return setStatus('#mcp-search-status', data.error, 'err');
1229
- clear(mcpSearchResultsEl);
1230
- if (!data.results.length) {
1231
- setStatus('#mcp-search-status', 'sin resultados');
1232
- return;
1233
- }
1234
- setStatus('#mcp-search-status', `${data.results.length} resultado(s)`);
1235
- for (const r of data.results) {
1236
- const li = el('li', r.installed ? 'active' : '');
1237
- const main = el('div', 'grow');
1238
- main.append(el('div', '', r.name || r.id), el('div', 'pmeta', r.description || r.id));
1239
- li.appendChild(main);
1240
- const actions = el('div', 'pactions');
1241
- if (r.requiredEnv?.length) actions.appendChild(el('span', 'pmeta', `env: ${r.requiredEnv.join(', ')}`));
1242
- if (r.installed) actions.appendChild(el('span', 'pmeta', 'instalado'));
1243
- else actions.appendChild(button('instalar', (event) => { event.stopPropagation(); installMcpFromRegistry(r.registry || r.name); }));
1244
- li.appendChild(actions);
1245
- mcpSearchResultsEl.appendChild(li);
1246
- }
1247
- }
1248
-
1249
- export async function installMcpFromRegistry(name) {
1250
- setStatus('#mcp-search-status', `instalando ${name}…`);
1251
- const data = await api('/api/mcp/import', { method: 'POST', body: { name } }).catch((e) => ({ error: e.message }));
1252
- if (data?.error) return setStatus('#mcp-search-status', data.error, 'err');
1253
- const required = (data.requiredEnv ?? []).length ? ` · requiere env: ${data.requiredEnv.join(', ')}` : '';
1254
- setStatus('#mcp-search-status', `instalado ${data.imported}${required}`, 'ok');
1255
- await refreshMcp();
1256
- await refreshMcpCatalog();
1257
- await searchMcp();
1258
- }
1259
-
1260
- export async function refreshSkillCatalog() {
1261
- if (!skillCatalogEl) return;
1262
- const data = await api('/api/skills/catalog').catch(() => null);
1263
- if (!data) return;
1264
- clear(skillCatalogEl);
1265
- for (const entry of data.catalog) {
1266
- const li = el('li', entry.installed ? 'active' : '');
1267
- const main = el('div', 'grow');
1268
- main.append(el('div', '', entry.name), el('div', 'pmeta', entry.description || ''));
1269
- li.appendChild(main);
1270
- const actions = el('div', 'pactions');
1271
- if (entry.installed) actions.appendChild(el('span', 'pmeta', 'instalada'));
1272
- else actions.appendChild(button('instalar', (event) => { event.stopPropagation(); installCatalogSkill(entry.id); }));
1273
- li.appendChild(actions);
1274
- skillCatalogEl.appendChild(li);
1275
- }
1276
- }
1277
-
1278
- export async function installCatalogSkill(id) {
1279
- setStatus('#skill-import-status', `instalando ${id}…`);
1280
- const data = await api('/api/skills', { method: 'POST', body: { id } }).catch((e) => ({ error: e.message }));
1281
- if (data?.error) return setStatus('#skill-import-status', data.error, 'err');
1282
- setStatus('#skill-import-status', `instalada ${id} (deshabilitada)`, 'ok');
1283
- await refreshSkills();
1284
- await refreshSkillCatalog();
1285
- }
1286
-
1287
- export async function searchSkills() {
1288
- const input = $('#skill-search');
1289
- const q = input?.value.trim();
1290
- if (!q) return;
1291
- setStatus('#skill-search-status', 'buscando…');
1292
- const data = await api(`/api/skills/search?q=${encodeURIComponent(q)}`).catch((e) => ({ error: e.message }));
1293
- if (data?.error) return setStatus('#skill-search-status', data.error, 'err');
1294
- clear(skillSearchResultsEl);
1295
- if (!data.results.length) {
1296
- setStatus('#skill-search-status', 'sin resultados');
1297
- return;
1298
- }
1299
- setStatus('#skill-search-status', `${data.results.length} resultado(s)`);
1300
- for (const r of data.results) {
1301
- const li = el('li', r.installed ? 'active' : '');
1302
- const main = el('div', 'grow');
1303
- main.append(el('div', '', r.source), el('div', 'pmeta', r.description || r.ref));
1304
- li.appendChild(main);
1305
- const actions = el('div', 'pactions');
1306
- if (r.installed) actions.appendChild(el('span', 'pmeta', 'instalada'));
1307
- else actions.appendChild(button('instalar', (event) => { event.stopPropagation(); installSkillRef(r.ref); }));
1308
- li.appendChild(actions);
1309
- skillSearchResultsEl.appendChild(li);
1310
- }
1311
- }
1312
-
1313
- export async function installSkillRef(ref) {
1314
- setStatus('#skill-search-status', `instalando ${ref}…`);
1315
- const data = await api('/api/skills/import', { method: 'POST', body: { ref } }).catch((e) => ({ error: e.message }));
1316
- if (data?.error) return setStatus('#skill-search-status', data.error, 'err');
1317
- setStatus('#skill-search-status', `instalada ${data.installed}`, 'ok');
1318
- await refreshSkills();
1319
- await refreshSkillCatalog();
1320
- await searchSkills();
1321
- }
1322
-
1
+ import { deleteEntity, importEntity, toggleEntity } from './chat.js';
2
+ import {
3
+ agentControlsSlot,
4
+ applySessionAgent,
5
+ loadModels,
6
+ modelSelect,
7
+ moveAgentControls,
8
+ persistAgent,
9
+ providerSelect,
10
+ setSummaryText,
11
+ updateStatus,
12
+ } from './chrome.js';
13
+ import { $, api, button, clear, el, emptyState, icon, state } from './core.js';
14
+ import { filterSettings } from './settingsNav.js';
15
+
16
+ /* --- providers ----------------------------------------------------------- */
17
+
18
+ export const providerListEl = $('#provider-list');
19
+ export const providerForm = $('#provider-form');
20
+ export const providerModal = $('#provider-modal');
21
+
22
+ export async function refreshProviders() {
23
+ const data = await api('/api/providers').catch(() => null);
24
+ if (!data) return;
25
+ state.providers = data.providers;
26
+ state.active = data.active;
27
+ syncTopbar();
28
+ renderProviderList();
29
+ const record = state.panes.get(state.sessionId);
30
+ if (record) await applySessionAgent(record);
31
+ }
32
+
33
+ export function isConfigured(p) {
34
+ return Boolean(p.hasKey) || !p.requiresKey;
35
+ }
36
+
37
+ export function syncTopbar() {
38
+ clear(providerSelect);
39
+ const configured = state.providers.filter(isConfigured);
40
+ for (const p of configured) {
41
+ const option = document.createElement('option');
42
+ option.value = p.id;
43
+ option.textContent = p.label && p.label !== p.id ? `${p.label} (${p.id})` : p.id;
44
+ providerSelect.appendChild(option);
45
+ }
46
+ const record = state.panes.get(state.sessionId);
47
+ const active = record?.provider || state.active.provider;
48
+ if (active && !configured.some((p) => p.id === active)) {
49
+ const current = state.providers.find((p) => p.id === active);
50
+ const option = document.createElement('option');
51
+ option.value = active;
52
+ option.textContent = `${current?.label ?? active} (sin configurar)`;
53
+ providerSelect.appendChild(option);
54
+ }
55
+ if (!providerSelect.options.length) {
56
+ const option = document.createElement('option');
57
+ option.value = '';
58
+ option.textContent = 'sin proveedores configurados';
59
+ providerSelect.appendChild(option);
60
+ providerSelect.disabled = true;
61
+ } else {
62
+ providerSelect.disabled = false;
63
+ if (active) providerSelect.value = active;
64
+ }
65
+ updateStatus();
66
+ }
67
+
68
+ export function renderProviderList() {
69
+ clear(providerListEl);
70
+ for (const p of state.providers) {
71
+ const li = el('li', p.id === state.active.provider ? 'active' : '');
72
+ li.appendChild(el('span', `pdot${p.hasKey || !p.requiresKey ? ' on' : ''}`));
73
+ const main = el('div', 'grow');
74
+ const title = el('div');
75
+ title.append(el('span', 'pid', p.id), document.createTextNode(' '), el('span', 'pmeta', p.label));
76
+ main.append(
77
+ title,
78
+ el(
79
+ 'div',
80
+ 'pmeta',
81
+ `${p.baseUrl || p.defaultBaseUrl || 'sin base URL'} · ${p.models.length} modelos · ${p.apiKeyHint || (p.requiresKey ? 'sin key' : 'sin key requerida')}`
82
+ )
83
+ );
84
+ li.appendChild(main);
85
+ const actions = el('div', 'pactions');
86
+ actions.appendChild(button('usar', (event) => { event.stopPropagation(); useProvider(p.id); }));
87
+ actions.appendChild(button('editar', (event) => { event.stopPropagation(); openProviderForm(p); }));
88
+ if (p.stored) actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeProvider(p.id); }, 'btn danger'));
89
+ li.appendChild(actions);
90
+ li.addEventListener('click', () => openProviderForm(p));
91
+ providerListEl.appendChild(li);
92
+ }
93
+ }
94
+
95
+ export async function useProvider(id) {
96
+ providerSelect.value = id;
97
+ await loadModels({ save: false, force: true });
98
+ await persistAgent({ provider: id, model: modelSelect.value || undefined });
99
+ await refreshProviders();
100
+ }
101
+
102
+ export function openProviderForm(provider = null) {
103
+ $('#provider-title').textContent = provider ? `editar ${provider.id}` : 'nuevo proveedor';
104
+ $('#pf-id').value = provider?.id ?? '';
105
+ $('#pf-id').readOnly = Boolean(provider);
106
+ $('#pf-label').value = provider?.label ?? '';
107
+ $('#pf-baseurl').value = provider?.baseUrl ?? '';
108
+ $('#pf-key').value = '';
109
+ $('#pf-key').placeholder = provider?.apiKeyHint ? `guardada (${provider.apiKeyHint})` : 'dejar vacío para no cambiar';
110
+ $('#pf-models').value = (provider?.models ?? []).join(', ');
111
+ setProviderStatus('');
112
+ providerModal.showModal();
113
+ if (!provider) $('#pf-id').focus();
114
+ }
115
+
116
+ export function closeProviderForm() {
117
+ if (providerModal.open) providerModal.close();
118
+ }
119
+
120
+ export function setProviderStatus(text, cls = '') {
121
+ setSummaryText($('#pf-status'), text, cls);
122
+ }
123
+
124
+ export async function submitProvider(event) {
125
+ event.preventDefault();
126
+ const id = $('#pf-id').value.trim();
127
+ if (!id) return setProviderStatus('Falta el id.', 'err');
128
+ const body = {
129
+ label: $('#pf-label').value.trim() || id,
130
+ baseUrl: $('#pf-baseurl').value.trim(),
131
+ models: $('#pf-models').value.split(',').map((s) => s.trim()).filter(Boolean),
132
+ };
133
+ const key = $('#pf-key').value;
134
+ if (key) body.apiKey = key;
135
+ try {
136
+ await api(`/api/providers/${encodeURIComponent(id)}`, { method: 'PUT', body });
137
+ closeProviderForm();
138
+ await refreshProviders();
139
+ await loadModels({ force: true });
140
+ } catch (error) {
141
+ setProviderStatus(error.message, 'err');
142
+ }
143
+ }
144
+
145
+ export async function removeProvider(id) {
146
+ await deleteEntity(`/api/providers/${encodeURIComponent(id)}`, {
147
+ confirmText: `¿Borrar la configuración de "${id}"?`,
148
+ after: () => loadModels({ force: true }),
149
+ refresh: refreshProviders,
150
+ });
151
+ }
152
+
153
+ export async function testProvider() {
154
+ const id = $('#pf-id').value.trim();
155
+ if (!id) return setProviderStatus('Guardá primero el proveedor para probarlo.', 'err');
156
+ setProviderStatus('probando…');
157
+ const out = await api(`/api/providers/${encodeURIComponent(id)}/test`, { method: 'POST' }).catch((error) => ({
158
+ ok: false,
159
+ error: error.message,
160
+ }));
161
+ if (!out.ok) return setProviderStatus(`error: ${out.error}`, 'err');
162
+
163
+ const ids = (out.models ?? []).map((m) => m.id ?? m).filter(Boolean);
164
+ let note = '';
165
+ if (out.source === 'api' && ids.length) {
166
+ $('#pf-models').value = ids.join(', ');
167
+ note = ' · modelos autocompletados';
168
+ }
169
+ if (id === state.active.provider) await loadModels({ force: true });
170
+ setProviderStatus(`ok · ${out.count} modelos (${out.source})${note}`, 'ok');
171
+ }
172
+
173
+ /* --- ajustes: MCP + Skills ----------------------------------------------- */
174
+
175
+ export const mcpListEl = $('#mcp-list');
176
+ export const mcpSearchResultsEl = $('#mcp-search-results');
177
+ export const mcpCatalogEl = $('#mcp-catalog');
178
+ export const mcpModal = $('#mcp-modal');
179
+ export const mcpForm = $('#mcp-form');
180
+ export const mcpModeEl = $('#mcp-mode');
181
+ export const mcpCountEl = $('#mcp-count');
182
+ export const mcpPinnedEl = $('#mcp-pinned');
183
+ export const skillListEl = $('#skill-list');
184
+ export const skillSearchResultsEl = $('#skill-search-results');
185
+ export const skillCatalogEl = $('#skill-catalog');
186
+ export const skillModal = $('#skill-modal');
187
+ export const skillForm = $('#skill-form');
188
+ export const agentListEl = $('#agent-list');
189
+ export const agentModal = $('#agent-modal');
190
+ export const agentForm = $('#agent-form');
191
+ export const memoryListEl = $('#memory-list');
192
+ export const memoryIndexEl = $('#memory-index');
193
+ export const memoryModal = $('#memory-modal');
194
+ export const memoryForm = $('#memory-form');
195
+ export const scheduleListEl = $('#schedule-list');
196
+ export const scheduleModal = $('#schedule-modal');
197
+ export const scheduleForm = $('#schedule-form');
198
+ export const tgTokenEl = $('#tg-token');
199
+ export const tgChatsEl = $('#tg-chats');
200
+ export const tgTranscribeEl = $('#tg-transcribe');
201
+ export const tgTranscribeProviderEl = $('#tg-transcribe-provider');
202
+ export const tgNotifyEl = $('#tg-notify');
203
+ export const tgNotifyDoneEl = $('#tg-notify-done');
204
+ export const tgNotifyErrorEl = $('#tg-notify-error');
205
+ export const tgNotifyPermissionEl = $('#tg-notify-permission');
206
+ export const tgNotifyAgentEl = $('#tg-notify-agent');
207
+ export const tgStatusEl = $('#tg-status');
208
+ export const vozEnabledEl = $('#voz-enabled');
209
+ export const vozVoiceEl = $('#voz-voice');
210
+ export const vozRateEl = $('#voz-rate');
211
+ export const vozRateValEl = $('#voz-rate-val');
212
+ export const vozPitchEl = $('#voz-pitch');
213
+ export const vozPitchValEl = $('#voz-pitch-val');
214
+ export const vozAnnStartEl = $('#voz-ann-start');
215
+ export const vozAnnToolsEl = $('#voz-ann-tools');
216
+ export const vozAnnEndEl = $('#voz-ann-end');
217
+ export const vozAnnAnswerEl = $('#voz-ann-answer');
218
+ export const vozFfmpegEl = $('#voz-ffmpeg');
219
+ export const trProviderEl = $('#tr-provider');
220
+ export const trModelEl = $('#tr-model');
221
+
222
+ /* --- ajustes: pestañas internas + límites del agente --------------------- */
223
+
224
+ export const ajustesTabs = document.querySelectorAll('.ajustes-tab');
225
+ export const ajustesWrap = document.querySelector('.ajustes-wrap');
226
+ export const ajustesSearchEl = $('#ajustes-search');
227
+
228
+ /** En móvil los ajustes son dos pantallas (lista y detalle); en escritorio el nivel no importa. */
229
+ export function setAjustesLevel(level) {
230
+ if (ajustesWrap) ajustesWrap.dataset.level = level === 'panel' ? 'panel' : 'list';
231
+ }
232
+
233
+ export function activateAjustesPanel(name) {
234
+ ajustesTabs.forEach((tab) => {
235
+ const on = tab.dataset.panel === name;
236
+ tab.classList.toggle('active', on);
237
+ tab.setAttribute('aria-selected', on ? 'true' : 'false');
238
+ tab.tabIndex = on ? 0 : -1;
239
+ });
240
+ document
241
+ .querySelectorAll('.ajustes-panel')
242
+ .forEach((panel) => panel.classList.toggle('active', panel.dataset.panel === name));
243
+ setAjustesLevel('panel');
244
+ if (name === 'agente') moveAgentControls(agentControlsSlot);
245
+ }
246
+
247
+ /** Muestra solo las secciones que coinciden con la búsqueda y oculta los grupos vacíos. */
248
+ export function filterAjustesNav(query) {
249
+ const hits = new Set(filterSettings(query).map((section) => section.id));
250
+ ajustesTabs.forEach((tab) => {
251
+ tab.hidden = !hits.has(tab.dataset.panel);
252
+ });
253
+ document.querySelectorAll('.ajustes-group').forEach((group) => {
254
+ group.hidden = !group.querySelector('.ajustes-tab:not([hidden])');
255
+ });
256
+ const empty = $('#ajustes-empty');
257
+ if (empty) empty.hidden = hits.size > 0;
258
+ return [...ajustesTabs].filter((tab) => !tab.hidden);
259
+ }
260
+
261
+ function focusAjustesTab(tab) {
262
+ if (!tab) return;
263
+ ajustesTabs.forEach((other) => {
264
+ other.tabIndex = other === tab ? 0 : -1;
265
+ });
266
+ tab.focus();
267
+ }
268
+
269
+ export function wireAjustesNav() {
270
+ ajustesTabs.forEach((tab) => tab.addEventListener('click', () => activateAjustesPanel(tab.dataset.panel)));
271
+
272
+ $('#ajustes-back')?.addEventListener('click', () => {
273
+ setAjustesLevel('list');
274
+ focusAjustesTab(document.querySelector('.ajustes-tab.active'));
275
+ });
276
+
277
+ $('.ajustes-tabs')?.addEventListener('keydown', (event) => {
278
+ const visible = [...ajustesTabs].filter((tab) => !tab.hidden);
279
+ const index = visible.indexOf(document.activeElement);
280
+ if (index < 0) return;
281
+ const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
282
+ let next = null;
283
+ if (step) next = visible[(index + step + visible.length) % visible.length];
284
+ else if (event.key === 'Home') next = visible[0];
285
+ else if (event.key === 'End') next = visible[visible.length - 1];
286
+ if (!next) return;
287
+ event.preventDefault();
288
+ focusAjustesTab(next);
289
+ });
290
+
291
+ ajustesSearchEl?.addEventListener('input', () => filterAjustesNav(ajustesSearchEl.value));
292
+ ajustesSearchEl?.addEventListener('keydown', (event) => {
293
+ if (event.key === 'Escape' && ajustesSearchEl.value) {
294
+ event.preventDefault();
295
+ ajustesSearchEl.value = '';
296
+ filterAjustesNav('');
297
+ } else if (event.key === 'Enter') {
298
+ event.preventDefault();
299
+ const first = [...ajustesTabs].find((tab) => !tab.hidden);
300
+ if (first) activateAjustesPanel(first.dataset.panel);
301
+ } else if (event.key === 'ArrowDown') {
302
+ event.preventDefault();
303
+ focusAjustesTab([...ajustesTabs].find((tab) => !tab.hidden));
304
+ }
305
+ });
306
+ }
307
+
308
+ export const limitsRoundsEl = $('#limits-rounds');
309
+ export const limitsTokensEl = $('#limits-tokens');
310
+ export const limitsDurationEl = $('#limits-duration');
311
+ export const limitsOutputEl = $('#limits-output');
312
+ export const limitsCachingEl = $('#limits-caching');
313
+
314
+ export async function refreshLimits() {
315
+ const data = await api('/api/settings').catch(() => null);
316
+ const settings = data?.settings;
317
+ if (!settings) return;
318
+ if (limitsRoundsEl) limitsRoundsEl.value = settings.maxRounds ?? 0;
319
+ if (limitsTokensEl) limitsTokensEl.value = settings.maxTokens ?? 0;
320
+ if (limitsDurationEl) limitsDurationEl.value = Math.round((settings.maxDurationMs ?? 0) / 1000);
321
+ if (limitsOutputEl) limitsOutputEl.value = settings.maxOutputTokens ?? 0;
322
+ if (limitsCachingEl) limitsCachingEl.checked = settings.promptCaching === true;
323
+ }
324
+
325
+ export function setStatus(node, text, cls = '') {
326
+ const elNode = $(node);
327
+ if (!elNode) return;
328
+ setSummaryText(elNode, text, cls);
329
+ }
330
+
331
+ export async function refreshMcp({ connect = false } = {}) {
332
+ const data = await api(`/api/mcp${connect ? '?connect=1' : ''}`).catch(() => null);
333
+ if (!data) return;
334
+ if (mcpModeEl && data.mode) mcpModeEl.value = data.mode;
335
+ if (mcpPinnedEl && Array.isArray(data.pinned) && document.activeElement !== mcpPinnedEl) {
336
+ mcpPinnedEl.value = data.pinned.join(', ');
337
+ }
338
+ if (mcpCountEl) {
339
+ mcpCountEl.textContent =
340
+ data.toolCount == null
341
+ ? ''
342
+ : `${data.toolCount} herramientas · ${data.lazy ? 'diferidas (mcp_call)' : 'completas en contexto'}`;
343
+ }
344
+ clear(mcpListEl);
345
+ if (!data.servers.length) {
346
+ mcpListEl.appendChild(emptyState('Sin servidores MCP', 'Tocá «+ nuevo» o prendé uno de los sugeridos de abajo.'));
347
+ return;
348
+ }
349
+ for (const s of data.servers) {
350
+ const li = el('li', s.enabled ? 'active' : '');
351
+ li.appendChild(el('span', `pdot${s.ok !== false ? ' on' : ''}`));
352
+ const main = el('div', 'grow');
353
+ const title = el('div');
354
+ title.append(el('span', 'pid', s.id), document.createTextNode(' '), el('span', 'pmeta', s.name || ''));
355
+ const detail = s.transport === 'http' ? s.url || '' : `${s.command || ''} ${(s.args || []).join(' ')}`.trim();
356
+ const parts = [s.transport, detail || '(sin destino)', s.source === 'config' ? 'tamnora.json' : 'guardado'];
357
+ if (connect) {
358
+ if (s.ok === false) parts.push(`error: ${s.error}`);
359
+ else parts.push(`${(s.tools || []).length} tools`);
360
+ }
361
+ main.append(title, el('div', 'pmeta', parts.join(' · ')));
362
+ li.appendChild(main);
363
+ const actions = el('div', 'pactions');
364
+ actions.appendChild(button(s.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleMcp(s); }));
365
+ actions.appendChild(button('editar', (event) => { event.stopPropagation(); openMcpForm(s); }));
366
+ if (s.source !== 'config') actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeMcp(s.id); }, 'btn danger'));
367
+ li.appendChild(actions);
368
+ li.addEventListener('click', () => openMcpForm(s));
369
+ mcpListEl.appendChild(li);
370
+ }
371
+ }
372
+
373
+ export function syncMcpFormFields() {
374
+ const http = $('#mf-transport').value === 'http';
375
+ $('#mf-command-field').hidden = http;
376
+ $('#mf-args-field').hidden = http;
377
+ $('#mf-env-field').hidden = http;
378
+ $('#mf-inherit-field').hidden = http;
379
+ $('#mf-url-field').hidden = !http;
380
+ }
381
+
382
+ export function openMcpForm(server = null) {
383
+ $('#mcp-title').textContent = server ? `editar ${server.id}` : 'nuevo servidor MCP';
384
+ $('#mf-id').value = server?.id ?? '';
385
+ $('#mf-id').readOnly = Boolean(server);
386
+ $('#mf-name').value = server?.name ?? '';
387
+ $('#mf-transport').value = server?.transport === 'http' ? 'http' : 'stdio';
388
+ $('#mf-command').value = server?.command ?? '';
389
+ $('#mf-args').value = (server?.args ?? []).join(' ');
390
+ $('#mf-url').value = server?.url ?? '';
391
+ $('#mf-env').value = Object.entries(server?.env ?? {})
392
+ .map(([key, value]) => `${key}=${value ?? ''}`)
393
+ .join('\n');
394
+ $('#mf-inherit-env').checked = server?.inheritEnv === true;
395
+ $('#mf-enabled').checked = server ? server.enabled !== false : true;
396
+ syncMcpFormFields();
397
+ setStatus('#mf-status', '');
398
+ mcpModal.showModal();
399
+ if (!server) $('#mf-id').focus();
400
+ }
401
+
402
+ export function closeMcpForm() {
403
+ if (mcpModal.open) mcpModal.close();
404
+ }
405
+
406
+ export async function toggleMcp(server) {
407
+ await toggleEntity(`/api/mcp/servers/${encodeURIComponent(server.id)}`, {
408
+ body: {
409
+ name: server.name,
410
+ transport: server.transport,
411
+ command: server.command,
412
+ args: server.args,
413
+ url: server.url,
414
+ env: server.env,
415
+ enabled: !server.enabled,
416
+ },
417
+ errorLabel: 'actualizar MCP',
418
+ refresh: refreshMcp,
419
+ });
420
+ }
421
+
422
+ export async function removeMcp(id) {
423
+ await deleteEntity(`/api/mcp/servers/${encodeURIComponent(id)}`, {
424
+ confirmText: `¿Eliminar el servidor MCP "${id}"?`,
425
+ refresh: refreshMcp,
426
+ });
427
+ }
428
+
429
+ export function parseEnv(text) {
430
+ const out = {};
431
+ for (const line of String(text ?? '').split('\n')) {
432
+ const trimmed = line.trim();
433
+ if (!trimmed || trimmed.startsWith('#')) continue;
434
+ const eq = trimmed.indexOf('=');
435
+ if (eq === -1) continue;
436
+ out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1);
437
+ }
438
+ return out;
439
+ }
440
+
441
+ export async function submitMcp(event) {
442
+ event.preventDefault();
443
+ const id = $('#mf-id').value.trim();
444
+ if (!id) return setStatus('#mf-status', 'Falta el id.', 'err');
445
+ const body = {
446
+ name: $('#mf-name').value.trim() || id,
447
+ transport: $('#mf-transport').value,
448
+ command: $('#mf-command').value.trim(),
449
+ args: $('#mf-args').value.trim(),
450
+ url: $('#mf-url').value.trim(),
451
+ env: parseEnv($('#mf-env').value),
452
+ inheritEnv: $('#mf-inherit-env').checked,
453
+ enabled: $('#mf-enabled').checked,
454
+ };
455
+ try {
456
+ await api(`/api/mcp/servers/${encodeURIComponent(id)}`, { method: 'PUT', body });
457
+ } catch (error) {
458
+ return setStatus('#mf-status', error.message, 'err');
459
+ }
460
+ closeMcpForm();
461
+ refreshMcp();
462
+ }
463
+
464
+ export async function refreshSkills() {
465
+ const data = await api('/api/skills').catch(() => null);
466
+ if (!data) return;
467
+ clear(skillListEl);
468
+ if (!data.skills.length) {
469
+ skillListEl.appendChild(emptyState('Sin skills todavía', 'Tocá «+ nueva» o instalá una de las sugeridas del catálogo.'));
470
+ return;
471
+ }
472
+ for (const s of data.skills) {
473
+ const li = el('li', s.enabled ? 'active' : '');
474
+ li.appendChild(el('span', `pdot${s.enabled ? ' on' : ''}`));
475
+ const main = el('div', 'grow');
476
+ main.append(
477
+ el('div', '', s.name || s.id),
478
+ el('div', 'pmeta', s.description || s.when || '(sin descripción)')
479
+ );
480
+ li.appendChild(main);
481
+ const actions = el('div', 'pactions');
482
+ actions.appendChild(button(s.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleSkill(s.id); }));
483
+ actions.appendChild(button('editar', (event) => { event.stopPropagation(); openSkillForm(s.id); }));
484
+ if (s.removable) actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeSkill(s.id); }, 'btn danger'));
485
+ li.appendChild(actions);
486
+ li.addEventListener('click', () => openSkillForm(s.id));
487
+ skillListEl.appendChild(li);
488
+ }
489
+ }
490
+
491
+ export async function openSkillForm(id = null) {
492
+ $('#skill-title').textContent = id ? `editar ${id}` : 'nueva skill';
493
+ $('#sf-id').value = id ?? '';
494
+ $('#sf-id').readOnly = Boolean(id);
495
+ $('#sf-name').value = '';
496
+ $('#sf-description').value = '';
497
+ $('#sf-when').value = '';
498
+ $('#sf-body').value = '';
499
+ setStatus('#sf-status', '');
500
+ if (id) {
501
+ const data = await api(`/api/skills/${encodeURIComponent(id)}`).catch(() => null);
502
+ if (data?.skill) {
503
+ $('#sf-name').value = data.skill.name ?? '';
504
+ $('#sf-description').value = data.skill.description ?? '';
505
+ $('#sf-when').value = data.skill.when ?? '';
506
+ $('#sf-body').value = data.skill.body ?? '';
507
+ }
508
+ }
509
+ skillModal.showModal();
510
+ if (!id) $('#sf-id').focus();
511
+ }
512
+
513
+ export function closeSkillForm() {
514
+ if (skillModal.open) skillModal.close();
515
+ }
516
+
517
+ export async function toggleSkill(id) {
518
+ await toggleEntity(`/api/skills/${encodeURIComponent(id)}/toggle`, {
519
+ method: 'POST',
520
+ errorLabel: 'cambiar la skill',
521
+ refresh: refreshSkills,
522
+ });
523
+ }
524
+
525
+ export async function removeSkill(id) {
526
+ await deleteEntity(`/api/skills/${encodeURIComponent(id)}`, {
527
+ confirmText: `¿Eliminar la skill "${id}"?`,
528
+ refresh: refreshSkills,
529
+ });
530
+ }
531
+
532
+ export async function submitSkill(event) {
533
+ event.preventDefault();
534
+ const id = $('#sf-id').value.trim();
535
+ if (!id) return setStatus('#sf-status', 'Falta el id.', 'err');
536
+ const body = {
537
+ name: $('#sf-name').value.trim() || id,
538
+ description: $('#sf-description').value.trim(),
539
+ when: $('#sf-when').value.trim(),
540
+ body: $('#sf-body').value,
541
+ };
542
+ try {
543
+ await api(`/api/skills/${encodeURIComponent(id)}`, { method: 'PUT', body });
544
+ } catch (error) {
545
+ return setStatus('#sf-status', error.message, 'err');
546
+ }
547
+ closeSkillForm();
548
+ refreshSkills();
549
+ }
550
+
551
+ /* --- ajustes: Subagentes ------------------------------------------------- */
552
+
553
+ export async function refreshAgents() {
554
+ if (!agentListEl) return;
555
+ const data = await api('/api/agents').catch(() => null);
556
+ if (!data) return;
557
+ clear(agentListEl);
558
+ if (!data.agents.length) {
559
+ agentListEl.appendChild(
560
+ emptyState(
561
+ 'Sin subagentes todavía',
562
+ 'Tocá «+ nuevo» para crear uno: el agente principal va a verlo en su catálogo y podrá delegarle tareas con task.'
563
+ )
564
+ );
565
+ return;
566
+ }
567
+ for (const agent of data.agents) {
568
+ const li = el('li', agent.enabled ? 'active' : '');
569
+ li.appendChild(el('span', `pdot${agent.enabled ? ' on' : ''}`));
570
+ const main = el('div', 'grow');
571
+ const details = [
572
+ agent.provider || 'proveedor heredado',
573
+ agent.model || 'modelo heredado',
574
+ agent.mode || 'modo heredado',
575
+ agent.effort ? `esfuerzo ${agent.effort}` : '',
576
+ Array.isArray(agent.skills) ? `${agent.skills.length} skill(s)` : 'todas las skills',
577
+ ].filter(Boolean);
578
+ main.append(
579
+ el('div', '', `${agent.name || agent.id} · ${agent.id}`),
580
+ el('div', 'pmeta', agent.description || details.join(' · '))
581
+ );
582
+ li.appendChild(main);
583
+ const actions = el('div', 'pactions');
584
+ actions.appendChild(button(agent.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleAgent(agent.id); }));
585
+ actions.appendChild(button('editar', (event) => { event.stopPropagation(); openAgentForm(agent.id); }));
586
+ if (agent.removable) {
587
+ actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeAgent(agent.id); }, 'btn danger'));
588
+ }
589
+ li.appendChild(actions);
590
+ li.addEventListener('click', () => openAgentForm(agent.id));
591
+ agentListEl.appendChild(li);
592
+ }
593
+ }
594
+
595
+ function fillAgentProviders() {
596
+ const select = $('#af-provider');
597
+ if (!select) return;
598
+ const current = select.value;
599
+ clear(select);
600
+ const inherit = document.createElement('option');
601
+ inherit.value = '';
602
+ inherit.textContent = 'heredar';
603
+ select.appendChild(inherit);
604
+ for (const provider of state.providers ?? []) {
605
+ const option = document.createElement('option');
606
+ option.value = provider.id;
607
+ option.textContent = provider.label && provider.label !== provider.id ? `${provider.label} (${provider.id})` : provider.id;
608
+ select.appendChild(option);
609
+ }
610
+ select.value = current;
611
+ }
612
+
613
+ function fillAgentSkills(selected = null) {
614
+ const select = $('#af-skills');
615
+ if (!select) return;
616
+ clear(select);
617
+ for (const skill of state.skills ?? []) {
618
+ const option = document.createElement('option');
619
+ option.value = skill.id;
620
+ option.textContent = `${skill.name || skill.id}${skill.enabled ? '' : ' (deshabilitada)'}`;
621
+ option.disabled = !skill.enabled;
622
+ select.appendChild(option);
623
+ }
624
+ const wanted = new Set(Array.isArray(selected) ? selected : []);
625
+ for (const option of select.options) option.selected = wanted.has(option.value);
626
+ }
627
+
628
+ export async function openAgentForm(id = null) {
629
+ $('#agent-title').textContent = id ? `editar ${id}` : 'nuevo subagente';
630
+ $('#af-id').value = id ?? '';
631
+ $('#af-id').readOnly = Boolean(id);
632
+ $('#af-name').value = '';
633
+ $('#af-description').value = '';
634
+ $('#af-model').value = '';
635
+ $('#af-rounds').value = '';
636
+ $('#af-tools').value = '';
637
+ $('#af-body').value = '';
638
+ setStatus('#af-status', '');
639
+ const providers = await api('/api/providers').catch(() => null);
640
+ if (providers?.providers) state.providers = providers.providers;
641
+ const skills = await api('/api/skills').catch(() => null);
642
+ state.skills = skills?.skills ?? [];
643
+ const effort = $('#af-effort');
644
+ if (effort) effort.value = '';
645
+ fillAgentProviders();
646
+ $('#af-provider').value = '';
647
+ $('#af-mode').value = '';
648
+ fillAgentSkills([]);
649
+ if (id) {
650
+ const data = await api(`/api/agents/${encodeURIComponent(id)}`).catch(() => null);
651
+ if (data?.agent) {
652
+ const agent = data.agent;
653
+ $('#af-name').value = agent.name ?? '';
654
+ $('#af-description').value = agent.description ?? '';
655
+ $('#af-model').value = agent.model ?? '';
656
+ $('#af-provider').value = agent.provider ?? '';
657
+ $('#af-mode').value = agent.mode ?? '';
658
+ $('#af-effort').value = agent.effort ?? '';
659
+ $('#af-rounds').value = agent.maxRounds || '';
660
+ $('#af-tools').value = (agent.tools ?? []).join(', ');
661
+ fillAgentSkills(agent.skills ?? null);
662
+ $('#af-body').value = agent.body ?? '';
663
+ }
664
+ }
665
+ agentModal.showModal();
666
+ if (!id) $('#af-id').focus();
667
+ }
668
+
669
+ export function closeAgentForm() {
670
+ if (agentModal?.open) agentModal.close();
671
+ }
672
+
673
+ export async function toggleAgent(id) {
674
+ await toggleEntity(`/api/agents/${encodeURIComponent(id)}/toggle`, {
675
+ method: 'POST',
676
+ errorLabel: 'cambiar el subagente',
677
+ refresh: refreshAgents,
678
+ });
679
+ }
680
+
681
+ export async function removeAgent(id) {
682
+ await deleteEntity(`/api/agents/${encodeURIComponent(id)}`, {
683
+ confirmText: `¿Eliminar el subagente "${id}"?`,
684
+ refresh: refreshAgents,
685
+ });
686
+ }
687
+
688
+ export async function submitAgent(event) {
689
+ event.preventDefault();
690
+ const id = $('#af-id').value.trim();
691
+ if (!id) return setStatus('#af-status', 'Falta el id.', 'err');
692
+ const skillsSelect = $('#af-skills');
693
+ const selectedSkills = skillsSelect ? [...skillsSelect.selectedOptions].map((option) => option.value) : [];
694
+ const body = {
695
+ name: $('#af-name').value.trim() || id,
696
+ description: $('#af-description').value.trim(),
697
+ provider: $('#af-provider').value.trim(),
698
+ model: $('#af-model').value.trim(),
699
+ mode: $('#af-mode').value,
700
+ effort: $('#af-effort').value,
701
+ maxRounds: Number($('#af-rounds').value) || 0,
702
+ tools: $('#af-tools').value.split(',').map((item) => item.trim()).filter(Boolean),
703
+ skills: selectedSkills.length ? selectedSkills : null,
704
+ body: $('#af-body').value,
705
+ };
706
+ try {
707
+ await api(`/api/agents/${encodeURIComponent(id)}`, { method: 'PUT', body });
708
+ } catch (error) {
709
+ return setStatus('#af-status', error.message, 'err');
710
+ }
711
+ closeAgentForm();
712
+ refreshAgents();
713
+ }
714
+
715
+ /* --- ajustes: Memoria ---------------------------------------------------- */
716
+
717
+ export async function refreshMemory() {
718
+ if (!memoryListEl) return;
719
+ const data = await api('/api/memory').catch(() => null);
720
+ if (!data) return;
721
+ if (memoryIndexEl && document.activeElement !== memoryIndexEl) memoryIndexEl.value = data.index ?? '';
722
+ clear(memoryListEl);
723
+ if (!data.topics?.length) {
724
+ memoryListEl.appendChild(emptyState('Sin memoria todavía', 'El agente guarda acá lo que conviene recordar del proyecto; también podés crear un tema con «+ tema».'));
725
+ return;
726
+ }
727
+ for (const t of data.topics) {
728
+ const li = el('li', 'active');
729
+ const main = el('div', 'grow');
730
+ main.append(
731
+ el('div', '', t.title || t.name),
732
+ el('div', 'pmeta', `${t.name}${t.bytes ? ` · ${Math.max(1, Math.round(t.bytes / 1024))} KB` : ''}`)
733
+ );
734
+ li.appendChild(main);
735
+ const actions = el('div', 'pactions');
736
+ actions.appendChild(button('editar', (event) => { event.stopPropagation(); openMemoryForm(t.name); }));
737
+ actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeMemory(t.name); }, 'btn danger'));
738
+ li.appendChild(actions);
739
+ li.addEventListener('click', () => openMemoryForm(t.name));
740
+ memoryListEl.appendChild(li);
741
+ }
742
+ }
743
+
744
+ export async function openMemoryForm(name = null) {
745
+ $('#memory-title').textContent = name ? `editar ${name}` : 'nuevo tema';
746
+ $('#mem-name').value = name ? name.replace(/\.md$/i, '') : '';
747
+ $('#mem-name').readOnly = Boolean(name);
748
+ $('#mem-title').value = '';
749
+ $('#mem-body').value = '';
750
+ setStatus('#mem-status', '');
751
+ if (name) {
752
+ const data = await api(`/api/memory/${encodeURIComponent(name)}`).catch(() => null);
753
+ if (data?.topic) {
754
+ $('#mem-title').value = data.topic.title ?? '';
755
+ $('#mem-body').value = data.topic.body ?? '';
756
+ }
757
+ }
758
+ memoryModal.showModal();
759
+ if (!name) $('#mem-name').focus();
760
+ }
761
+
762
+ export function closeMemoryForm() {
763
+ if (memoryModal.open) memoryModal.close();
764
+ }
765
+
766
+ export async function removeMemory(name) {
767
+ await deleteEntity(`/api/memory/${encodeURIComponent(name)}`, {
768
+ confirmText: `¿Borrar la memoria "${name}"?`,
769
+ refresh: refreshMemory,
770
+ });
771
+ }
772
+
773
+ export async function submitMemory(event) {
774
+ event.preventDefault();
775
+ const name = $('#mem-name').value.trim();
776
+ if (!name) return setStatus('#mem-status', 'Falta el tema.', 'err');
777
+ const body = { title: $('#mem-title').value.trim(), body: $('#mem-body').value };
778
+ try {
779
+ await api(`/api/memory/${encodeURIComponent(name)}`, { method: 'PUT', body });
780
+ } catch (error) {
781
+ return setStatus('#mem-status', error.message, 'err');
782
+ }
783
+ closeMemoryForm();
784
+ refreshMemory();
785
+ }
786
+
787
+ export async function saveMemoryIndex() {
788
+ try {
789
+ await api('/api/memory', { method: 'PUT', body: { index: memoryIndexEl.value } });
790
+ } catch (error) {
791
+ return setStatus('#memory-status', error.message, 'err');
792
+ }
793
+ setStatus('#memory-status', 'guardado', 'ok');
794
+ refreshMemory();
795
+ }
796
+
797
+ /* --- ajustes: Tareas programadas ----------------------------------------- */
798
+
799
+ export function fmtWhen(ms) {
800
+ try {
801
+ return new Date(ms).toLocaleString();
802
+ } catch {
803
+ return '';
804
+ }
805
+ }
806
+
807
+ export function scheduleTypeFields() {
808
+ const type = $('#sc-type').value;
809
+ const agent = type === 'agent';
810
+ $('#sc-agent-prompt-row').hidden = !agent;
811
+ $('#sc-agent-mode-row').hidden = !agent;
812
+ $('#sc-shell-cmd-row').hidden = agent;
813
+ $('#sc-shell-args-row').hidden = agent;
814
+ }
815
+
816
+ export async function refreshSchedules() {
817
+ if (!scheduleListEl) return;
818
+ const data = await api('/api/schedules').catch(() => null);
819
+ if (!data) return;
820
+ clear(scheduleListEl);
821
+ if (!data.schedules.length) {
822
+ scheduleListEl.appendChild(emptyState('Sin tareas programadas', 'Con «+ tarea» programás un comando o un turno del agente: cada tanto, a diario o con cron.'));
823
+ return;
824
+ }
825
+ for (const s of data.schedules) {
826
+ const li = el('li', s.enabled ? 'active' : '');
827
+ li.appendChild(el('span', `pdot${s.enabled ? ' on' : ''}`));
828
+ const main = el('div', 'grow');
829
+ const meta = [s.type, s.spec];
830
+ if (s.nextRun) meta.push(`próxima ${fmtWhen(s.nextRun)}`);
831
+ if (s.lastStatus) meta.push(s.lastStatus);
832
+ main.append(el('div', '', s.name || s.id), el('div', 'pmeta', meta.join(' · ')));
833
+ li.appendChild(main);
834
+ const actions = el('div', 'pactions');
835
+ actions.appendChild(button('correr', (event) => { event.stopPropagation(); runSchedule(s.id); }));
836
+ actions.appendChild(button(s.enabled ? 'pausar' : 'activar', (event) => { event.stopPropagation(); toggleSchedule(s.id); }));
837
+ actions.appendChild(button('editar', (event) => { event.stopPropagation(); openScheduleForm(s.id); }));
838
+ actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeSchedule(s.id); }, 'btn danger'));
839
+ li.appendChild(actions);
840
+ li.addEventListener('click', () => openScheduleForm(s.id));
841
+ scheduleListEl.appendChild(li);
842
+ }
843
+ }
844
+
845
+ export async function openScheduleForm(id = null) {
846
+ $('#schedule-title').textContent = id ? `editar ${id}` : 'nueva tarea';
847
+ $('#sc-id').value = id ?? '';
848
+ $('#sc-id').readOnly = Boolean(id);
849
+ $('#sc-name').value = '';
850
+ $('#sc-type').value = 'agent';
851
+ $('#sc-spec').value = '';
852
+ $('#sc-prompt').value = '';
853
+ $('#sc-mode').value = 'agent';
854
+ $('#sc-command').value = '';
855
+ $('#sc-args').value = '';
856
+ $('#sc-cwd').value = '';
857
+ $('#sc-enabled').checked = true;
858
+ setStatus('#sc-status', '');
859
+ if (id) {
860
+ const data = await api('/api/schedules').catch(() => null);
861
+ const s = data?.schedules?.find((item) => item.id === id);
862
+ if (s) {
863
+ $('#sc-name').value = s.name ?? '';
864
+ $('#sc-type').value = s.type ?? 'agent';
865
+ $('#sc-spec').value = s.spec ?? '';
866
+ $('#sc-cwd').value = s.cwd ?? '';
867
+ $('#sc-enabled').checked = s.enabled !== false;
868
+ const payload = s.payload ?? {};
869
+ if (s.type === 'shell') {
870
+ $('#sc-command').value = payload.command ?? '';
871
+ $('#sc-args').value = Array.isArray(payload.args) ? payload.args.join(' ') : payload.args ?? '';
872
+ } else {
873
+ $('#sc-prompt').value = payload.prompt ?? '';
874
+ $('#sc-mode').value = payload.mode ?? 'agent';
875
+ }
876
+ }
877
+ }
878
+ scheduleTypeFields();
879
+ scheduleModal.showModal();
880
+ if (!id) $('#sc-id').focus();
881
+ }
882
+
883
+ export function closeScheduleForm() {
884
+ if (scheduleModal.open) scheduleModal.close();
885
+ }
886
+
887
+ export async function submitSchedule(event) {
888
+ event.preventDefault();
889
+ const id = $('#sc-id').value.trim();
890
+ if (!id) return setStatus('#sc-status', 'Falta el id.', 'err');
891
+ const type = $('#sc-type').value;
892
+ const body = {
893
+ id,
894
+ name: $('#sc-name').value.trim() || id,
895
+ type,
896
+ spec: $('#sc-spec').value.trim(),
897
+ cwd: $('#sc-cwd').value.trim() || undefined,
898
+ enabled: $('#sc-enabled').checked,
899
+ };
900
+ if (type === 'shell') {
901
+ body.command = $('#sc-command').value.trim();
902
+ body.args = $('#sc-args').value.trim();
903
+ } else {
904
+ body.prompt = $('#sc-prompt').value.trim();
905
+ body.mode = $('#sc-mode').value;
906
+ }
907
+ try {
908
+ const exists = $('#sc-id').readOnly;
909
+ await api(exists ? `/api/schedules/${encodeURIComponent(id)}` : '/api/schedules', {
910
+ method: exists ? 'PUT' : 'POST',
911
+ body,
912
+ });
913
+ } catch (error) {
914
+ return setStatus('#sc-status', error.message, 'err');
915
+ }
916
+ closeScheduleForm();
917
+ refreshSchedules();
918
+ }
919
+
920
+ export async function toggleSchedule(id) {
921
+ await toggleEntity(`/api/schedules/${encodeURIComponent(id)}/toggle`, {
922
+ method: 'POST',
923
+ errorLabel: 'cambiar la tarea',
924
+ refresh: refreshSchedules,
925
+ });
926
+ }
927
+
928
+ export async function removeSchedule(id) {
929
+ await deleteEntity(`/api/schedules/${encodeURIComponent(id)}`, {
930
+ confirmText: `¿Eliminar la tarea "${id}"?`,
931
+ refresh: refreshSchedules,
932
+ });
933
+ }
934
+
935
+ export async function runSchedule(id) {
936
+ setStatus('#schedule-status', `corriendo ${id}…`);
937
+ try {
938
+ await api(`/api/schedules/${encodeURIComponent(id)}/run`, { method: 'POST' });
939
+ } catch (error) {
940
+ setStatus('#schedule-status', error.message, 'err');
941
+ refreshSchedules();
942
+ return;
943
+ }
944
+ setStatus('#schedule-status', `${id}: listo`, 'ok');
945
+ refreshSchedules();
946
+ }
947
+
948
+ export async function refreshChannels() {
949
+ if (!tgStatusEl) return;
950
+ const data = await api('/api/channels').catch(() => null);
951
+ if (!data?.telegram) return;
952
+ const tg = data.telegram;
953
+ tgStatusEl.className = `summary${tg.running ? ' ok' : ''}`;
954
+ tgStatusEl.textContent = tg.running
955
+ ? `activo${tg.tokenHint ? ` · ${tg.tokenHint}` : ''}`
956
+ : tg.tokenSet
957
+ ? 'token guardado (bot detenido)'
958
+ : 'sin configurar';
959
+ if (tgChatsEl && document.activeElement !== tgChatsEl) {
960
+ tgChatsEl.value = (tg.allowedChatIds ?? []).join(', ');
961
+ }
962
+ if (tgTranscribeEl && document.activeElement !== tgTranscribeEl) {
963
+ tgTranscribeEl.value = tg.transcribeModel ?? 'whisper-1';
964
+ }
965
+ if (tgTranscribeProviderEl && document.activeElement !== tgTranscribeProviderEl) {
966
+ tgTranscribeProviderEl.value = tg.transcribeProvider ?? '';
967
+ }
968
+ const notify = tg.notify ?? {};
969
+ if (tgNotifyEl) tgNotifyEl.checked = notify.enabled === true;
970
+ if (tgNotifyDoneEl) tgNotifyDoneEl.checked = notify.done !== false;
971
+ if (tgNotifyErrorEl) tgNotifyErrorEl.checked = notify.error !== false;
972
+ if (tgNotifyPermissionEl) tgNotifyPermissionEl.checked = notify.permission !== false;
973
+ if (tgNotifyAgentEl) tgNotifyAgentEl.checked = notify.agent !== false;
974
+ if (tgTokenEl && document.activeElement !== tgTokenEl) {
975
+ tgTokenEl.value = '';
976
+ tgTokenEl.placeholder = tg.tokenHint ? `${tg.tokenHint} (dejar vacío para no cambiar)` : '123456:ABC…';
977
+ }
978
+ }
979
+
980
+ export async function submitChannel() {
981
+ if (!tgTokenEl) return;
982
+ setStatus('#tg-msg', 'guardando…');
983
+ const body = {
984
+ allowedChatIds: tgChatsEl?.value ?? '',
985
+ transcribeModel: tgTranscribeEl?.value.trim() || 'whisper-1',
986
+ transcribeProvider: tgTranscribeProviderEl?.value.trim() || '',
987
+ notify: {
988
+ enabled: tgNotifyEl?.checked === true,
989
+ done: tgNotifyDoneEl?.checked !== false,
990
+ error: tgNotifyErrorEl?.checked !== false,
991
+ permission: tgNotifyPermissionEl?.checked !== false,
992
+ agent: tgNotifyAgentEl?.checked !== false,
993
+ },
994
+ };
995
+ const token = tgTokenEl.value.trim();
996
+ if (token) body.token = token;
997
+ const data = await api('/api/channels/telegram', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
998
+ if (data?.error) return setStatus('#tg-msg', data.error, 'err');
999
+ setStatus('#tg-msg', data.telegram?.running ? 'guardado · bot activo' : 'guardado', 'ok');
1000
+ refreshChannels();
1001
+ }
1002
+
1003
+ export async function testChannel() {
1004
+ if (!tgTokenEl) return;
1005
+ setStatus('#tg-msg', 'probando…');
1006
+ const data = await api('/api/channels/telegram/test', {
1007
+ method: 'POST',
1008
+ body: { token: tgTokenEl.value.trim() },
1009
+ }).catch((e) => ({ ok: false, error: e.message }));
1010
+ if (data?.ok) setStatus('#tg-msg', `token OK${data.username ? ` · @${data.username}` : ''}`, 'ok');
1011
+ else setStatus('#tg-msg', data?.error || 'token inválido', 'err');
1012
+ }
1013
+
1014
+
1015
+ /* --- ajustes: perfil del usuario ------------------------------------------ */
1016
+
1017
+ export const perfil = {
1018
+ address: $('#perfil-address'),
1019
+ name: $('#perfil-name'),
1020
+ language: $('#perfil-language'),
1021
+ notes: $('#perfil-notes'),
1022
+ fields: $('#perfil-fields'),
1023
+ add: $('#perfil-add-field'),
1024
+ save: $('#perfil-save'),
1025
+ };
1026
+
1027
+ export function addPerfilFieldRow(key = '', value = '') {
1028
+ if (!perfil.fields) return;
1029
+ const row = el('div', 'perfil-field');
1030
+ const keyInput = el('input', 'pf-key');
1031
+ keyInput.placeholder = 'clave';
1032
+ keyInput.value = key;
1033
+ keyInput.spellcheck = false;
1034
+ const valueInput = el('input', 'pf-value');
1035
+ valueInput.placeholder = 'valor';
1036
+ valueInput.value = value;
1037
+ valueInput.spellcheck = false;
1038
+ const remove = button('', () => row.remove(), 'btn icon');
1039
+ remove.appendChild(icon('x', 12));
1040
+ row.append(keyInput, valueInput, remove);
1041
+ perfil.fields.appendChild(row);
1042
+ }
1043
+
1044
+ export function renderPerfilFields(fields = {}) {
1045
+ if (!perfil.fields) return;
1046
+ clear(perfil.fields);
1047
+ for (const [key, value] of Object.entries(fields)) addPerfilFieldRow(key, value);
1048
+ }
1049
+
1050
+ export function collectPerfilFields() {
1051
+ const out = {};
1052
+ for (const row of perfil.fields?.querySelectorAll('.perfil-field') ?? []) {
1053
+ const key = row.querySelector('.pf-key').value.trim();
1054
+ const value = row.querySelector('.pf-value').value.trim();
1055
+ if (key && value) out[key] = value;
1056
+ }
1057
+ return out;
1058
+ }
1059
+
1060
+ export async function refreshProfile() {
1061
+ if (!perfil.address) return;
1062
+ const data = await api('/api/profile').catch(() => null);
1063
+ if (!data?.profile) return;
1064
+ const p = data.profile;
1065
+ perfil.address.value = p.address ?? '';
1066
+ perfil.name.value = p.name ?? '';
1067
+ perfil.language.value = p.language ?? '';
1068
+ perfil.notes.value = p.notes ?? '';
1069
+ renderPerfilFields(p.fields ?? {});
1070
+ setStatus('#perfil-status', p.address || p.name ? `trato: ${p.address || p.name}` : 'sin datos', '');
1071
+ }
1072
+
1073
+ export async function submitProfile() {
1074
+ if (!perfil.address) return;
1075
+ setStatus('#perfil-msg', 'guardando…');
1076
+ const body = {
1077
+ address: perfil.address.value.trim(),
1078
+ name: perfil.name.value.trim(),
1079
+ language: perfil.language.value.trim(),
1080
+ notes: perfil.notes.value.trim(),
1081
+ fields: collectPerfilFields(),
1082
+ };
1083
+ const data = await api('/api/profile', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
1084
+ if (data?.error) return setStatus('#perfil-msg', data.error, 'err');
1085
+ setStatus('#perfil-msg', 'guardado', 'ok');
1086
+ refreshProfile();
1087
+ }
1088
+
1089
+ export async function importMcp() {
1090
+ const input = $('#mcp-import');
1091
+ const value = input.value.trim();
1092
+ if (!value) return;
1093
+ const payload = value.startsWith('{')
1094
+ ? { json: value }
1095
+ : /^https?:\/\//i.test(value)
1096
+ ? { url: value }
1097
+ : { name: value };
1098
+ await importEntity({
1099
+ url: '/api/mcp/import',
1100
+ body: payload,
1101
+ input,
1102
+ statusSel: '#mcp-import-status',
1103
+ refresh: refreshMcp,
1104
+ okMessage: (data) => {
1105
+ const required = (data.requiredEnv ?? []).length ? ` · requiere env: ${data.requiredEnv.join(', ')}` : '';
1106
+ return `Importado ${data.imported}${required}`;
1107
+ },
1108
+ });
1109
+ }
1110
+
1111
+ export async function importSkill() {
1112
+ const input = $('#skill-import');
1113
+ const ref = input.value.trim();
1114
+ if (!ref) return;
1115
+ await importEntity({
1116
+ url: '/api/skills/import',
1117
+ body: { ref },
1118
+ input,
1119
+ statusSel: '#skill-import-status',
1120
+ refresh: refreshSkills,
1121
+ okMessage: (data) => `Importada ${data.installed} (${data.source})`,
1122
+ });
1123
+ }
1124
+
1125
+ /* --- ajustes: biblioteca y buscador de MCP y skills ----------------------- */
1126
+
1127
+ export async function refreshMcpCatalog() {
1128
+ if (!mcpCatalogEl) return;
1129
+ const data = await api('/api/mcp/catalog').catch(() => null);
1130
+ if (!data) return;
1131
+ clear(mcpCatalogEl);
1132
+ for (const entry of data.catalog) {
1133
+ const li = el('li', entry.installed ? 'active' : '');
1134
+ const main = el('div', 'grow');
1135
+ main.append(el('div', '', entry.name), el('div', 'pmeta', entry.description || ''));
1136
+ li.appendChild(main);
1137
+ const actions = el('div', 'pactions');
1138
+ if (entry.requiredEnv?.length) actions.appendChild(el('span', 'pmeta', `env: ${entry.requiredEnv.join(', ')}`));
1139
+ if (entry.installed) actions.appendChild(el('span', 'pmeta', 'instalado'));
1140
+ else actions.appendChild(button('agregar', (event) => { event.stopPropagation(); installCatalogMcp(entry.id); }));
1141
+ li.appendChild(actions);
1142
+ mcpCatalogEl.appendChild(li);
1143
+ }
1144
+ }
1145
+
1146
+ export async function installCatalogMcp(id) {
1147
+ setStatus('#mcp-import-status', `agregando ${id}…`);
1148
+ const data = await api('/api/mcp/import', { method: 'POST', body: { catalog: id } }).catch((e) => ({ error: e.message }));
1149
+ if (data?.error) return setStatus('#mcp-import-status', data.error, 'err');
1150
+ setStatus('#mcp-import-status', `agregado ${id} (deshabilitado)`, 'ok');
1151
+ await refreshMcp();
1152
+ await refreshMcpCatalog();
1153
+ }
1154
+
1155
+ export async function searchMcp() {
1156
+ const input = $('#mcp-search');
1157
+ const q = input?.value.trim();
1158
+ if (!q) return;
1159
+ setStatus('#mcp-search-status', 'buscando…');
1160
+ const data = await api(`/api/mcp/search?q=${encodeURIComponent(q)}`).catch((e) => ({ error: e.message }));
1161
+ if (data?.error) return setStatus('#mcp-search-status', data.error, 'err');
1162
+ clear(mcpSearchResultsEl);
1163
+ if (!data.results.length) {
1164
+ setStatus('#mcp-search-status', 'sin resultados');
1165
+ return;
1166
+ }
1167
+ setStatus('#mcp-search-status', `${data.results.length} resultado(s)`);
1168
+ for (const r of data.results) {
1169
+ const li = el('li', r.installed ? 'active' : '');
1170
+ const main = el('div', 'grow');
1171
+ main.append(el('div', '', r.name || r.id), el('div', 'pmeta', r.description || r.id));
1172
+ li.appendChild(main);
1173
+ const actions = el('div', 'pactions');
1174
+ if (r.requiredEnv?.length) actions.appendChild(el('span', 'pmeta', `env: ${r.requiredEnv.join(', ')}`));
1175
+ if (r.installed) actions.appendChild(el('span', 'pmeta', 'instalado'));
1176
+ else actions.appendChild(button('instalar', (event) => { event.stopPropagation(); installMcpFromRegistry(r.registry || r.name); }));
1177
+ li.appendChild(actions);
1178
+ mcpSearchResultsEl.appendChild(li);
1179
+ }
1180
+ }
1181
+
1182
+ export async function installMcpFromRegistry(name) {
1183
+ setStatus('#mcp-search-status', `instalando ${name}…`);
1184
+ const data = await api('/api/mcp/import', { method: 'POST', body: { name } }).catch((e) => ({ error: e.message }));
1185
+ if (data?.error) return setStatus('#mcp-search-status', data.error, 'err');
1186
+ const required = (data.requiredEnv ?? []).length ? ` · requiere env: ${data.requiredEnv.join(', ')}` : '';
1187
+ setStatus('#mcp-search-status', `instalado ${data.imported}${required}`, 'ok');
1188
+ await refreshMcp();
1189
+ await refreshMcpCatalog();
1190
+ await searchMcp();
1191
+ }
1192
+
1193
+ export async function refreshSkillCatalog() {
1194
+ if (!skillCatalogEl) return;
1195
+ const data = await api('/api/skills/catalog').catch(() => null);
1196
+ if (!data) return;
1197
+ clear(skillCatalogEl);
1198
+ for (const entry of data.catalog) {
1199
+ const li = el('li', entry.installed ? 'active' : '');
1200
+ const main = el('div', 'grow');
1201
+ main.append(el('div', '', entry.name), el('div', 'pmeta', entry.description || ''));
1202
+ li.appendChild(main);
1203
+ const actions = el('div', 'pactions');
1204
+ if (entry.installed) actions.appendChild(el('span', 'pmeta', 'instalada'));
1205
+ else actions.appendChild(button('instalar', (event) => { event.stopPropagation(); installCatalogSkill(entry.id); }));
1206
+ li.appendChild(actions);
1207
+ skillCatalogEl.appendChild(li);
1208
+ }
1209
+ }
1210
+
1211
+ export async function installCatalogSkill(id) {
1212
+ setStatus('#skill-import-status', `instalando ${id}…`);
1213
+ const data = await api('/api/skills', { method: 'POST', body: { id } }).catch((e) => ({ error: e.message }));
1214
+ if (data?.error) return setStatus('#skill-import-status', data.error, 'err');
1215
+ setStatus('#skill-import-status', `instalada ${id} (deshabilitada)`, 'ok');
1216
+ await refreshSkills();
1217
+ await refreshSkillCatalog();
1218
+ }
1219
+
1220
+ export async function searchSkills() {
1221
+ const input = $('#skill-search');
1222
+ const q = input?.value.trim();
1223
+ if (!q) return;
1224
+ setStatus('#skill-search-status', 'buscando…');
1225
+ const data = await api(`/api/skills/search?q=${encodeURIComponent(q)}`).catch((e) => ({ error: e.message }));
1226
+ if (data?.error) return setStatus('#skill-search-status', data.error, 'err');
1227
+ clear(skillSearchResultsEl);
1228
+ if (!data.results.length) {
1229
+ setStatus('#skill-search-status', 'sin resultados');
1230
+ return;
1231
+ }
1232
+ setStatus('#skill-search-status', `${data.results.length} resultado(s)`);
1233
+ for (const r of data.results) {
1234
+ const li = el('li', r.installed ? 'active' : '');
1235
+ const main = el('div', 'grow');
1236
+ main.append(el('div', '', r.source), el('div', 'pmeta', r.description || r.ref));
1237
+ li.appendChild(main);
1238
+ const actions = el('div', 'pactions');
1239
+ if (r.installed) actions.appendChild(el('span', 'pmeta', 'instalada'));
1240
+ else actions.appendChild(button('instalar', (event) => { event.stopPropagation(); installSkillRef(r.ref); }));
1241
+ li.appendChild(actions);
1242
+ skillSearchResultsEl.appendChild(li);
1243
+ }
1244
+ }
1245
+
1246
+ export async function installSkillRef(ref) {
1247
+ setStatus('#skill-search-status', `instalando ${ref}…`);
1248
+ const data = await api('/api/skills/import', { method: 'POST', body: { ref } }).catch((e) => ({ error: e.message }));
1249
+ if (data?.error) return setStatus('#skill-search-status', data.error, 'err');
1250
+ setStatus('#skill-search-status', `instalada ${data.installed}`, 'ok');
1251
+ await refreshSkills();
1252
+ await refreshSkillCatalog();
1253
+ await searchSkills();
1254
+ }
1255
+