@danieltmn/tamnora 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/harness/packages/cli/src/cli.js +2 -0
- package/harness/packages/cli/src/commands/eval.js +3 -0
- package/harness/packages/cli/src/commands/repl.js +2 -0
- package/harness/packages/cli/src/commands/run.js +2 -1
- package/harness/packages/cli/src/commands/tui.js +18 -11
- package/harness/packages/cli/src/lib/runtime.js +3 -1
- package/harness/packages/cli/src/lib/tunnel.js +2 -113
- package/harness/packages/cli/src/lib/util.js +2 -0
- package/harness/packages/cli/src/tui/app.js +45 -4
- package/harness/packages/cli/src/tui/keys.js +3 -0
- package/harness/packages/cli/test/tui.test.js +5 -0
- package/harness/packages/core/src/agent/modes.js +1 -1
- package/harness/packages/core/src/config/index.js +3 -0
- package/harness/packages/core/src/console.js +18 -0
- package/harness/packages/core/src/eval/judge.js +3 -3
- package/harness/packages/core/src/eval/runner.js +3 -0
- package/harness/packages/core/src/index.js +4 -0
- package/harness/packages/core/src/procs/detect.js +69 -0
- package/harness/packages/core/src/providers/ollama.js +3 -3
- package/harness/packages/core/src/providers/openaiCompatible.js +3 -3
- package/harness/packages/core/src/providers/openrouter.js +2 -2
- package/harness/packages/core/src/providers/resilience.js +1 -1
- package/harness/packages/core/src/providers/utils.js +43 -0
- package/harness/packages/core/src/skills/index.js +72 -0
- package/harness/packages/core/src/storage/sqlite.js +21 -0
- package/harness/packages/core/src/tunnel.js +113 -0
- package/harness/packages/core/test/config.test.js +15 -0
- package/harness/packages/core/test/procs.test.js +34 -0
- package/harness/packages/core/test/providers.test.js +20 -0
- package/harness/packages/core/test/resilience.test.js +1 -0
- package/harness/packages/core/test/skills.test.js +49 -0
- package/harness/packages/server/public/app.js +440 -6
- package/harness/packages/server/public/index.html +124 -6
- package/harness/packages/server/public/styles.css +43 -1
- package/harness/packages/server/src/cli.js +2 -0
- package/harness/packages/server/src/lib/agentTools.js +159 -0
- package/harness/packages/server/src/lib/procs.js +125 -0
- package/harness/packages/server/src/lib/prompt.js +6 -3
- package/harness/packages/server/src/lib/skills.js +69 -0
- package/harness/packages/server/src/lib/telegram.js +267 -0
- package/harness/packages/server/src/routes/chat.js +9 -1
- package/harness/packages/server/src/routes/mcp.js +71 -8
- package/harness/packages/server/src/routes/preview.js +58 -0
- package/harness/packages/server/src/routes/procs.js +47 -0
- package/harness/packages/server/src/routes/sessions.js +10 -0
- package/harness/packages/server/src/routes/skills.js +61 -0
- package/harness/packages/server/src/server.js +79 -6
- package/harness/packages/server/test/server.test.js +227 -0
- package/harness/tamnora.example.json +7 -1
- package/package.json +1 -1
|
@@ -57,6 +57,7 @@ const ICON_PATHS = {
|
|
|
57
57
|
'folder-open': ['m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2'],
|
|
58
58
|
file: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4'],
|
|
59
59
|
'refresh-cw': ['M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8', 'M21 3v5h-5', 'M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16', 'M8 16H3v5'],
|
|
60
|
+
trash: ['M3 6h18', 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6', 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', 'M10 11v6', 'M14 11v6'],
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
function icon(name, size = 16, className = '') {
|
|
@@ -166,6 +167,7 @@ function activateTab(name) {
|
|
|
166
167
|
hideMoreMenu();
|
|
167
168
|
if (name === 'runs') refreshRuns();
|
|
168
169
|
if (name === 'proveedores') refreshProviders();
|
|
170
|
+
if (name === 'ajustes') { refreshMcp(); refreshSkills(); }
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
@@ -210,8 +212,10 @@ const PALETTE_COMMANDS = [
|
|
|
210
212
|
{ id: 'init', label: 'Analizar el proyecto y crear AGENTS.md', hint: '/init', run: () => runSlash('init') },
|
|
211
213
|
{ id: 'compact', label: 'Compactar la conversación', hint: '/compact', run: () => runSlash('compact') },
|
|
212
214
|
{ id: 'clear', label: 'Limpiar la conversación', hint: '/clear', run: () => runSlash('clear') },
|
|
215
|
+
{ id: 'mode', label: 'Cambiar Plan / Build', hint: 'Ctrl+.', run: () => toggleMode() },
|
|
213
216
|
{ id: 'settings', label: 'Ajustes del agente (modelo, modo, auto)', run: () => settingsModal?.showModal() },
|
|
214
217
|
{ id: 'providers', label: 'Gestionar proveedores', run: () => activateTab('proveedores') },
|
|
218
|
+
{ id: 'ajustes', label: 'MCP y Skills (ajustes)', run: () => activateTab('ajustes') },
|
|
215
219
|
{ id: 'explorer', label: 'Mostrar u ocultar archivos', run: () => setExplorerOpen(!state.explorerOpen) },
|
|
216
220
|
{ id: 'sidebar', label: 'Mostrar u ocultar sesiones', run: () => setSidebarOpen(!state.sidebarOpen) },
|
|
217
221
|
{ id: 'theme', label: 'Cambiar tema', run: () => applyTheme(document.documentElement.dataset.theme === 'paper' ? 'dark' : 'paper') },
|
|
@@ -369,14 +373,36 @@ async function loadModes() {
|
|
|
369
373
|
updateStatus();
|
|
370
374
|
}
|
|
371
375
|
|
|
372
|
-
|
|
373
|
-
|
|
376
|
+
function modeLabel(id) {
|
|
377
|
+
const option = [...modeSelect.options].find((o) => o.value === id);
|
|
378
|
+
return option ? option.textContent : id;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function setMode(id, { persist = true } = {}) {
|
|
382
|
+
if (![...modeSelect.options].some((o) => o.value === id)) return;
|
|
383
|
+
modeSelect.value = id;
|
|
384
|
+
if (persist) await api('/api/settings', { method: 'PUT', body: { mode: id } }).catch(() => null);
|
|
374
385
|
updateStatus();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function toggleMode() {
|
|
389
|
+
const next = modeSelect.value === 'plan' ? 'agent' : 'plan';
|
|
390
|
+
await setMode(next);
|
|
391
|
+
notify(`Modo **${modeLabel(next)}**${next === 'plan' ? ' (solo lectura)' : ''}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
modeSelect.addEventListener('change', () => setMode(modeSelect.value));
|
|
395
|
+
|
|
396
|
+
document.addEventListener('keydown', (event) => {
|
|
397
|
+
if ((event.ctrlKey || event.metaKey) && event.key === '.') {
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
toggleMode();
|
|
400
|
+
}
|
|
375
401
|
});
|
|
376
402
|
|
|
377
403
|
function updateStatus(extra = {}) {
|
|
378
404
|
const p = state.providers.find((x) => x.id === state.active.provider);
|
|
379
|
-
$('#sb-mode').textContent = modeSelect.value || '
|
|
405
|
+
$('#sb-mode').textContent = modeLabel(modeSelect.value) || 'Build';
|
|
380
406
|
$('#sb-provider').textContent = p ? `${p.id}` : state.active.provider || '—';
|
|
381
407
|
$('#sb-model').textContent = state.active.model || '—';
|
|
382
408
|
if (extra.rounds !== undefined) $('#sb-rounds').textContent = `${extra.rounds} rondas`;
|
|
@@ -948,6 +974,16 @@ async function refreshSessions() {
|
|
|
948
974
|
twisty.appendChild(icon(collapsed ? 'chevron-right' : 'chevron-down', 14));
|
|
949
975
|
head.append(twisty, icon('folder', 15, 'fi'), el('span', 'gname', key ? basename(key) : 'sin carpeta'), el('span', 'gcount', String(items.length)));
|
|
950
976
|
head.title = key || 'sesiones sin carpeta';
|
|
977
|
+
const delGroup = el('button', 'group-del');
|
|
978
|
+
delGroup.type = 'button';
|
|
979
|
+
delGroup.title = 'eliminar proyecto (borra sus sesiones, no los archivos)';
|
|
980
|
+
delGroup.setAttribute('aria-label', 'eliminar proyecto');
|
|
981
|
+
delGroup.appendChild(icon('trash', 13));
|
|
982
|
+
delGroup.addEventListener('click', (event) => {
|
|
983
|
+
event.stopPropagation();
|
|
984
|
+
removeProject(key, items.length);
|
|
985
|
+
});
|
|
986
|
+
head.appendChild(delGroup);
|
|
951
987
|
const list = el('ul', 'group-sessions');
|
|
952
988
|
list.hidden = collapsed;
|
|
953
989
|
head.addEventListener('click', () => {
|
|
@@ -961,8 +997,18 @@ async function refreshSessions() {
|
|
|
961
997
|
});
|
|
962
998
|
for (const session of items) {
|
|
963
999
|
const li = el('li', `session-item${session.id === state.sessionId ? ' active' : ''}`);
|
|
964
|
-
li.appendChild(el('
|
|
1000
|
+
li.appendChild(el('span', 'session-title', session.title || '(sin título)'));
|
|
965
1001
|
li.title = `${session.provider ?? ''}/${session.model ?? ''}${session.cwd ? ` · ${session.cwd}` : ''}`;
|
|
1002
|
+
const del = el('button', 'session-del');
|
|
1003
|
+
del.type = 'button';
|
|
1004
|
+
del.title = 'eliminar sesión';
|
|
1005
|
+
del.setAttribute('aria-label', `eliminar sesión ${session.title || ''}`);
|
|
1006
|
+
del.appendChild(icon('trash', 13));
|
|
1007
|
+
del.addEventListener('click', (event) => {
|
|
1008
|
+
event.stopPropagation();
|
|
1009
|
+
removeSession(session);
|
|
1010
|
+
});
|
|
1011
|
+
li.appendChild(del);
|
|
966
1012
|
li.addEventListener('click', () => {
|
|
967
1013
|
if (isMobile()) setSidebarOpen(false);
|
|
968
1014
|
openTab(session.id);
|
|
@@ -975,6 +1021,32 @@ async function refreshSessions() {
|
|
|
975
1021
|
if (!data.sessions.length) sessionsEl.appendChild(el('li', 'muted', 'sin sesiones'));
|
|
976
1022
|
}
|
|
977
1023
|
|
|
1024
|
+
function closeSessionsOf(cwd) {
|
|
1025
|
+
for (const [id, record] of [...state.panes]) {
|
|
1026
|
+
if ((record.cwd || '') === (cwd || '')) closeTab(id);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
async function removeSession(session) {
|
|
1031
|
+
const name = session.title || '(sin título)';
|
|
1032
|
+
if (!window.confirm(`¿Eliminar la sesión "${name}"? No se puede deshacer.`)) return;
|
|
1033
|
+
const error = await api(`/api/sessions/${session.id}`, { method: 'DELETE' }).then(() => null).catch((e) => e);
|
|
1034
|
+
if (error) return notify(`No se pudo eliminar: ${error.message}`, 'notice err');
|
|
1035
|
+
if (state.panes.has(session.id)) closeTab(session.id);
|
|
1036
|
+
refreshSessions();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function removeProject(cwd, count) {
|
|
1040
|
+
const label = cwd ? basename(cwd) : 'sin carpeta';
|
|
1041
|
+
if (!window.confirm(`¿Eliminar ${count} sesión(es) del proyecto "${label}"? No se tocan los archivos del disco.`)) return;
|
|
1042
|
+
const error = await api(`/api/projects?cwd=${encodeURIComponent(cwd ?? '')}`, { method: 'DELETE' })
|
|
1043
|
+
.then(() => null)
|
|
1044
|
+
.catch((e) => e);
|
|
1045
|
+
if (error) return notify(`No se pudo eliminar el proyecto: ${error.message}`, 'notice err');
|
|
1046
|
+
closeSessionsOf(cwd);
|
|
1047
|
+
refreshSessions();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
978
1050
|
/* --- session tabs --------------------------------------------------------- */
|
|
979
1051
|
|
|
980
1052
|
function registerSession(session, messages = [], tokens = { in: 0, out: 0 }) {
|
|
@@ -1847,6 +1919,231 @@ providerModal.addEventListener('click', (event) => {
|
|
|
1847
1919
|
if (event.target === providerModal) closeProviderForm();
|
|
1848
1920
|
});
|
|
1849
1921
|
|
|
1922
|
+
/* --- ajustes: MCP + Skills ----------------------------------------------- */
|
|
1923
|
+
|
|
1924
|
+
const mcpListEl = $('#mcp-list');
|
|
1925
|
+
const mcpModal = $('#mcp-modal');
|
|
1926
|
+
const mcpForm = $('#mcp-form');
|
|
1927
|
+
const skillListEl = $('#skill-list');
|
|
1928
|
+
const skillModal = $('#skill-modal');
|
|
1929
|
+
const skillForm = $('#skill-form');
|
|
1930
|
+
|
|
1931
|
+
function setStatus(node, text, cls = '') {
|
|
1932
|
+
const elNode = $(node);
|
|
1933
|
+
if (!elNode) return;
|
|
1934
|
+
elNode.className = `summary ${cls}`.trim();
|
|
1935
|
+
elNode.textContent = text;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
async function refreshMcp({ connect = false } = {}) {
|
|
1939
|
+
const data = await api(`/api/mcp${connect ? '?connect=1' : ''}`).catch(() => null);
|
|
1940
|
+
if (!data) return;
|
|
1941
|
+
clear(mcpListEl);
|
|
1942
|
+
if (!data.servers.length) {
|
|
1943
|
+
mcpListEl.appendChild(el('li', 'muted', 'sin servidores MCP'));
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
for (const s of data.servers) {
|
|
1947
|
+
const li = el('li', s.enabled ? 'active' : '');
|
|
1948
|
+
li.appendChild(el('span', `pdot${s.ok !== false ? ' on' : ''}`));
|
|
1949
|
+
const main = el('div', 'grow');
|
|
1950
|
+
const title = el('div');
|
|
1951
|
+
title.append(el('span', 'pid', s.id), document.createTextNode(' '), el('span', 'pmeta', s.name || ''));
|
|
1952
|
+
const detail = s.transport === 'http' ? s.url || '' : `${s.command || ''} ${(s.args || []).join(' ')}`.trim();
|
|
1953
|
+
const parts = [s.transport, detail || '(sin destino)', s.source === 'config' ? 'tamnora.json' : 'guardado'];
|
|
1954
|
+
if (connect) {
|
|
1955
|
+
if (s.ok === false) parts.push(`error: ${s.error}`);
|
|
1956
|
+
else parts.push(`${(s.tools || []).length} tools`);
|
|
1957
|
+
}
|
|
1958
|
+
main.append(title, el('div', 'pmeta', parts.join(' · ')));
|
|
1959
|
+
li.appendChild(main);
|
|
1960
|
+
li.appendChild(button(s.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleMcp(s); }));
|
|
1961
|
+
li.appendChild(button('editar', (event) => { event.stopPropagation(); openMcpForm(s); }));
|
|
1962
|
+
if (s.source !== 'config') li.appendChild(button('borrar', (event) => { event.stopPropagation(); removeMcp(s.id); }, 'btn danger'));
|
|
1963
|
+
li.addEventListener('click', () => openMcpForm(s));
|
|
1964
|
+
mcpListEl.appendChild(li);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function syncMcpFormFields() {
|
|
1969
|
+
const http = $('#mf-transport').value === 'http';
|
|
1970
|
+
$('#mf-command-field').hidden = http;
|
|
1971
|
+
$('#mf-args-field').hidden = http;
|
|
1972
|
+
$('#mf-url-field').hidden = !http;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function openMcpForm(server = null) {
|
|
1976
|
+
$('#mcp-title').textContent = server ? `editar ${server.id}` : 'nuevo servidor MCP';
|
|
1977
|
+
$('#mf-id').value = server?.id ?? '';
|
|
1978
|
+
$('#mf-id').readOnly = Boolean(server);
|
|
1979
|
+
$('#mf-name').value = server?.name ?? '';
|
|
1980
|
+
$('#mf-transport').value = server?.transport === 'http' ? 'http' : 'stdio';
|
|
1981
|
+
$('#mf-command').value = server?.command ?? '';
|
|
1982
|
+
$('#mf-args').value = (server?.args ?? []).join(' ');
|
|
1983
|
+
$('#mf-url').value = server?.url ?? '';
|
|
1984
|
+
$('#mf-enabled').checked = server ? server.enabled !== false : true;
|
|
1985
|
+
syncMcpFormFields();
|
|
1986
|
+
setStatus('#mf-status', '');
|
|
1987
|
+
mcpModal.showModal();
|
|
1988
|
+
if (!server) $('#mf-id').focus();
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function closeMcpForm() {
|
|
1992
|
+
if (mcpModal.open) mcpModal.close();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
async function toggleMcp(server) {
|
|
1996
|
+
await api(`/api/mcp/servers/${encodeURIComponent(server.id)}`, {
|
|
1997
|
+
method: 'PUT',
|
|
1998
|
+
body: {
|
|
1999
|
+
name: server.name,
|
|
2000
|
+
transport: server.transport,
|
|
2001
|
+
command: server.command,
|
|
2002
|
+
args: server.args,
|
|
2003
|
+
url: server.url,
|
|
2004
|
+
enabled: !server.enabled,
|
|
2005
|
+
},
|
|
2006
|
+
}).catch((error) => notify(`No se pudo actualizar MCP: ${error.message}`, 'notice err'));
|
|
2007
|
+
refreshMcp();
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
async function removeMcp(id) {
|
|
2011
|
+
if (!window.confirm(`¿Eliminar el servidor MCP "${id}"?`)) return;
|
|
2012
|
+
const error = await api(`/api/mcp/servers/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
2013
|
+
.then(() => null)
|
|
2014
|
+
.catch((e) => e);
|
|
2015
|
+
if (error) return notify(`No se pudo eliminar: ${error.message}`, 'notice err');
|
|
2016
|
+
refreshMcp();
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
async function submitMcp(event) {
|
|
2020
|
+
event.preventDefault();
|
|
2021
|
+
const id = $('#mf-id').value.trim();
|
|
2022
|
+
if (!id) return setStatus('#mf-status', 'Falta el id.', 'err');
|
|
2023
|
+
const body = {
|
|
2024
|
+
name: $('#mf-name').value.trim() || id,
|
|
2025
|
+
transport: $('#mf-transport').value,
|
|
2026
|
+
command: $('#mf-command').value.trim(),
|
|
2027
|
+
args: $('#mf-args').value.trim(),
|
|
2028
|
+
url: $('#mf-url').value.trim(),
|
|
2029
|
+
enabled: $('#mf-enabled').checked,
|
|
2030
|
+
};
|
|
2031
|
+
try {
|
|
2032
|
+
await api(`/api/mcp/servers/${encodeURIComponent(id)}`, { method: 'PUT', body });
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
return setStatus('#mf-status', error.message, 'err');
|
|
2035
|
+
}
|
|
2036
|
+
closeMcpForm();
|
|
2037
|
+
refreshMcp();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
async function refreshSkills() {
|
|
2041
|
+
const data = await api('/api/skills').catch(() => null);
|
|
2042
|
+
if (!data) return;
|
|
2043
|
+
clear(skillListEl);
|
|
2044
|
+
if (!data.skills.length) {
|
|
2045
|
+
skillListEl.appendChild(el('li', 'muted', 'sin skills todavía'));
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
for (const s of data.skills) {
|
|
2049
|
+
const li = el('li', s.enabled ? 'active' : '');
|
|
2050
|
+
li.appendChild(el('span', `pdot${s.enabled ? ' on' : ''}`));
|
|
2051
|
+
const main = el('div', 'grow');
|
|
2052
|
+
main.append(
|
|
2053
|
+
el('div', '', s.name || s.id),
|
|
2054
|
+
el('div', 'pmeta', s.description || s.when || '(sin descripción)')
|
|
2055
|
+
);
|
|
2056
|
+
li.appendChild(main);
|
|
2057
|
+
li.appendChild(button(s.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleSkill(s.id); }));
|
|
2058
|
+
li.appendChild(button('editar', (event) => { event.stopPropagation(); openSkillForm(s.id); }));
|
|
2059
|
+
if (s.removable) li.appendChild(button('borrar', (event) => { event.stopPropagation(); removeSkill(s.id); }, 'btn danger'));
|
|
2060
|
+
li.addEventListener('click', () => openSkillForm(s.id));
|
|
2061
|
+
skillListEl.appendChild(li);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async function openSkillForm(id = null) {
|
|
2066
|
+
$('#skill-title').textContent = id ? `editar ${id}` : 'nueva skill';
|
|
2067
|
+
$('#sf-id').value = id ?? '';
|
|
2068
|
+
$('#sf-id').readOnly = Boolean(id);
|
|
2069
|
+
$('#sf-name').value = '';
|
|
2070
|
+
$('#sf-description').value = '';
|
|
2071
|
+
$('#sf-when').value = '';
|
|
2072
|
+
$('#sf-body').value = '';
|
|
2073
|
+
setStatus('#sf-status', '');
|
|
2074
|
+
if (id) {
|
|
2075
|
+
const data = await api(`/api/skills/${encodeURIComponent(id)}`).catch(() => null);
|
|
2076
|
+
if (data?.skill) {
|
|
2077
|
+
$('#sf-name').value = data.skill.name ?? '';
|
|
2078
|
+
$('#sf-description').value = data.skill.description ?? '';
|
|
2079
|
+
$('#sf-when').value = data.skill.when ?? '';
|
|
2080
|
+
$('#sf-body').value = data.skill.body ?? '';
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
skillModal.showModal();
|
|
2084
|
+
if (!id) $('#sf-id').focus();
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
function closeSkillForm() {
|
|
2088
|
+
if (skillModal.open) skillModal.close();
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
async function toggleSkill(id) {
|
|
2092
|
+
const error = await api(`/api/skills/${encodeURIComponent(id)}/toggle`, { method: 'POST' })
|
|
2093
|
+
.then(() => null)
|
|
2094
|
+
.catch((e) => e);
|
|
2095
|
+
if (error) return notify(`No se pudo cambiar la skill: ${error.message}`, 'notice err');
|
|
2096
|
+
refreshSkills();
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
async function removeSkill(id) {
|
|
2100
|
+
if (!window.confirm(`¿Eliminar la skill "${id}"?`)) return;
|
|
2101
|
+
const error = await api(`/api/skills/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
|
2102
|
+
.then(() => null)
|
|
2103
|
+
.catch((e) => e);
|
|
2104
|
+
if (error) return notify(`No se pudo eliminar: ${error.message}`, 'notice err');
|
|
2105
|
+
refreshSkills();
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
async function submitSkill(event) {
|
|
2109
|
+
event.preventDefault();
|
|
2110
|
+
const id = $('#sf-id').value.trim();
|
|
2111
|
+
if (!id) return setStatus('#sf-status', 'Falta el id.', 'err');
|
|
2112
|
+
const body = {
|
|
2113
|
+
name: $('#sf-name').value.trim() || id,
|
|
2114
|
+
description: $('#sf-description').value.trim(),
|
|
2115
|
+
when: $('#sf-when').value.trim(),
|
|
2116
|
+
body: $('#sf-body').value,
|
|
2117
|
+
};
|
|
2118
|
+
try {
|
|
2119
|
+
await api(`/api/skills/${encodeURIComponent(id)}`, { method: 'PUT', body });
|
|
2120
|
+
} catch (error) {
|
|
2121
|
+
return setStatus('#sf-status', error.message, 'err');
|
|
2122
|
+
}
|
|
2123
|
+
closeSkillForm();
|
|
2124
|
+
refreshSkills();
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
if (mcpListEl) {
|
|
2128
|
+
$('#mcp-new').addEventListener('click', () => openMcpForm());
|
|
2129
|
+
$('#mcp-reload').addEventListener('click', () => refreshMcp({ connect: true }));
|
|
2130
|
+
$('#mcp-close').addEventListener('click', closeMcpForm);
|
|
2131
|
+
$('#mf-cancel').addEventListener('click', closeMcpForm);
|
|
2132
|
+
$('#mf-transport').addEventListener('change', syncMcpFormFields);
|
|
2133
|
+
mcpForm.addEventListener('submit', submitMcp);
|
|
2134
|
+
mcpModal.addEventListener('click', (event) => {
|
|
2135
|
+
if (event.target === mcpModal) closeMcpForm();
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
$('#skill-new').addEventListener('click', () => openSkillForm());
|
|
2139
|
+
$('#skill-close').addEventListener('click', closeSkillForm);
|
|
2140
|
+
$('#sf-cancel').addEventListener('click', closeSkillForm);
|
|
2141
|
+
skillForm.addEventListener('submit', submitSkill);
|
|
2142
|
+
skillModal.addEventListener('click', (event) => {
|
|
2143
|
+
if (event.target === skillModal) closeSkillForm();
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
|
|
1850
2147
|
/* --- eval ---------------------------------------------------------------- */
|
|
1851
2148
|
|
|
1852
2149
|
const evalTasks = $('#eval-tasks');
|
|
@@ -2018,7 +2315,7 @@ function setExplorerOpen(open) {
|
|
|
2018
2315
|
} catch {
|
|
2019
2316
|
// storage unavailable
|
|
2020
2317
|
}
|
|
2021
|
-
if (open)
|
|
2318
|
+
if (open) refreshExplorerMode();
|
|
2022
2319
|
}
|
|
2023
2320
|
|
|
2024
2321
|
function fileListUrl(relPath) {
|
|
@@ -2147,9 +2444,146 @@ async function openFilePreview(relPath, name) {
|
|
|
2147
2444
|
$('#file-modal').showModal();
|
|
2148
2445
|
}
|
|
2149
2446
|
|
|
2447
|
+
/* --- explorer panes: estructura / preview / procs ------------------------- */
|
|
2448
|
+
|
|
2449
|
+
const xpPanes = { files: $('#xp-files'), preview: $('#xp-preview'), procs: $('#xp-procs') };
|
|
2450
|
+
let explorerMode = 'files';
|
|
2451
|
+
|
|
2452
|
+
function setExplorerMode(mode) {
|
|
2453
|
+
explorerMode = xpPanes[mode] ? mode : 'files';
|
|
2454
|
+
for (const [name, pane] of Object.entries(xpPanes)) pane.hidden = name !== explorerMode;
|
|
2455
|
+
document.querySelectorAll('.xtab').forEach((b) => b.classList.toggle('is-active', b.dataset.xp === explorerMode));
|
|
2456
|
+
refreshExplorerMode();
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function refreshExplorerMode() {
|
|
2460
|
+
if (!state.explorerOpen) return;
|
|
2461
|
+
if (explorerMode === 'files') refreshExplorer();
|
|
2462
|
+
else if (explorerMode === 'procs') detectProcs();
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
document.querySelectorAll('.xtab').forEach((tab) => tab.addEventListener('click', () => setExplorerMode(tab.dataset.xp)));
|
|
2466
|
+
|
|
2467
|
+
/* preview: iframe del dev server (local o por tunel) */
|
|
2468
|
+
|
|
2469
|
+
const previewFrame = $('#preview-frame');
|
|
2470
|
+
const previewUrlInput = $('#preview-url');
|
|
2471
|
+
const previewStatusEl = $('#preview-status');
|
|
2472
|
+
|
|
2473
|
+
function setPreviewStatus(text, isError = false) {
|
|
2474
|
+
previewStatusEl.textContent = text;
|
|
2475
|
+
previewStatusEl.classList.toggle('err', isError);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
function loadPreview(url) {
|
|
2479
|
+
const value = String(url ?? '').trim();
|
|
2480
|
+
if (!value) return setPreviewStatus('Poné una URL (p. ej. http://127.0.0.1:5173).', true);
|
|
2481
|
+
previewUrlInput.value = value;
|
|
2482
|
+
previewFrame.src = value;
|
|
2483
|
+
setPreviewStatus(`cargando ${value} …`);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
$('#preview-open')?.addEventListener('click', () => loadPreview(previewUrlInput.value));
|
|
2487
|
+
previewUrlInput?.addEventListener('keydown', (event) => {
|
|
2488
|
+
if (event.key === 'Enter') {
|
|
2489
|
+
event.preventDefault();
|
|
2490
|
+
loadPreview(previewUrlInput.value);
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2493
|
+
previewFrame?.addEventListener('load', () => {
|
|
2494
|
+
if (previewFrame.src) setPreviewStatus(previewFrame.src);
|
|
2495
|
+
});
|
|
2496
|
+
$('#preview-tunnel')?.addEventListener('click', async () => {
|
|
2497
|
+
const match = String(previewUrlInput.value).match(/:(\d+)/);
|
|
2498
|
+
const port = match ? Number(match[1]) : 0;
|
|
2499
|
+
if (!port) return setPreviewStatus('Poné una URL con puerto (p. ej. http://127.0.0.1:5173).', true);
|
|
2500
|
+
setPreviewStatus('abriendo túnel público…');
|
|
2501
|
+
const data = await api('/api/preview/tunnel', { method: 'POST', body: { port } }).catch((e) => ({ error: e.message }));
|
|
2502
|
+
if (data?.error) return setPreviewStatus(`error: ${data.error}`, true);
|
|
2503
|
+
loadPreview(data.tunnel.url);
|
|
2504
|
+
setPreviewStatus(`túnel público: ${data.tunnel.url}`);
|
|
2505
|
+
});
|
|
2506
|
+
|
|
2507
|
+
/* procs: deteccion y dev servers */
|
|
2508
|
+
|
|
2509
|
+
const procsListEl = $('#procs-list');
|
|
2510
|
+
const procsChipsEl = $('#procs-chips');
|
|
2511
|
+
const procsCwdEl = $('#procs-cwd');
|
|
2512
|
+
const procsLogEl = $('#procs-log');
|
|
2513
|
+
let selectedProc = null;
|
|
2514
|
+
|
|
2515
|
+
async function detectProcs() {
|
|
2516
|
+
clear(procsChipsEl);
|
|
2517
|
+
const cwd = state.cwd || '';
|
|
2518
|
+
procsCwdEl.textContent = cwd ? `cwd: ${cwd}` : 'sin carpeta';
|
|
2519
|
+
const data = await api(`/api/procs/detect?cwd=${encodeURIComponent(cwd)}`).catch(() => null);
|
|
2520
|
+
if (!data) return;
|
|
2521
|
+
for (const chip of data.chips) procsChipsEl.appendChild(button(chip.label, () => startProc(chip), 'xchip'));
|
|
2522
|
+
if (!data.chips.length) procsChipsEl.appendChild(el('span', 'muted', 'no detecté comandos de dev'));
|
|
2523
|
+
await refreshProcs();
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
async function refreshProcs() {
|
|
2527
|
+
const data = await api('/api/procs').catch(() => null);
|
|
2528
|
+
if (!data) return;
|
|
2529
|
+
clear(procsListEl);
|
|
2530
|
+
if (!data.enabled) {
|
|
2531
|
+
procsListEl.appendChild(el('li', 'muted', 'procesos deshabilitados (processes.enabled = false)'));
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
if (!data.procs.length) {
|
|
2535
|
+
procsListEl.appendChild(el('li', 'muted', 'sin procesos corriendo'));
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
for (const proc of data.procs) {
|
|
2539
|
+
const li = el('li', proc.running ? 'active' : '');
|
|
2540
|
+
const main = el('div', 'grow');
|
|
2541
|
+
main.append(
|
|
2542
|
+
el('div', '', `${proc.command} ${(proc.args || []).join(' ')}`),
|
|
2543
|
+
el('div', 'pmeta', `${proc.status}${proc.port ? ` · puerto ${proc.port}` : ''}`)
|
|
2544
|
+
);
|
|
2545
|
+
li.appendChild(main);
|
|
2546
|
+
li.appendChild(button('logs', () => { selectedProc = proc.id; refreshProcLog(); }));
|
|
2547
|
+
if (proc.port) li.appendChild(button('preview', () => { setExplorerMode('preview'); loadPreview(`http://127.0.0.1:${proc.port}`); }));
|
|
2548
|
+
if (proc.running) li.appendChild(button('detener', () => stopProc(proc.id), 'btn danger'));
|
|
2549
|
+
procsListEl.appendChild(li);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
async function startProc(chip) {
|
|
2554
|
+
const data = await api('/api/procs/start', {
|
|
2555
|
+
method: 'POST',
|
|
2556
|
+
body: { cwd: chip.cwd || state.cwd, command: chip.command, args: chip.args, port: chip.port },
|
|
2557
|
+
}).catch((e) => ({ error: e.message }));
|
|
2558
|
+
if (data?.error) {
|
|
2559
|
+
procsCwdEl.classList.add('err');
|
|
2560
|
+
procsCwdEl.textContent = `error: ${data.error}`;
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
procsCwdEl.classList.remove('err');
|
|
2564
|
+
selectedProc = data.proc.id;
|
|
2565
|
+
await refreshProcs();
|
|
2566
|
+
refreshProcLog();
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
async function stopProc(id) {
|
|
2570
|
+
await api(`/api/procs/${id}/stop`, { method: 'POST' }).catch(() => null);
|
|
2571
|
+
await refreshProcs();
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
async function refreshProcLog() {
|
|
2575
|
+
if (!selectedProc) return;
|
|
2576
|
+
const data = await api(`/api/procs/${selectedProc}/logs`).catch(() => null);
|
|
2577
|
+
if (!data) return;
|
|
2578
|
+
procsLogEl.textContent = (data.logs || []).map((l) => l.text).join('').slice(-4000) || '(sin salida)';
|
|
2579
|
+
procsLogEl.scrollTop = procsLogEl.scrollHeight;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
$('#procs-detect')?.addEventListener('click', detectProcs);
|
|
2583
|
+
|
|
2150
2584
|
explorerToggle.addEventListener('click', () => setExplorerOpen(!state.explorerOpen));
|
|
2151
2585
|
$('#explorer-close').addEventListener('click', () => setExplorerOpen(false));
|
|
2152
|
-
$('#explorer-refresh').addEventListener('click',
|
|
2586
|
+
$('#explorer-refresh').addEventListener('click', refreshExplorerMode);
|
|
2153
2587
|
explorerFilter.addEventListener('input', applyTreeFilter);
|
|
2154
2588
|
$('#file-close').addEventListener('click', () => $('#file-modal').close());
|
|
2155
2589
|
$('#file-modal').addEventListener('click', (event) => {
|