// settings-agents.js — 설정: AI 에이전트 관리(AGENTS.md 에디터), 스킬 설치/토글/편집. // extracted from app.js (v4.3.19-3 split). function closeSettings() { document.getElementById('settings-modal').style.display = 'none'; _searchSettingsLoaded = false; _modelSettingsLoaded = false; } function ensureAgentMdEditor() { if (agentMdEditor || typeof CodeMirror === 'undefined') return; const wrap = document.getElementById('agent-md-editor-wrap'); if (!wrap) return; agentMdEditor = CodeMirror(wrap, { value: '', mode: 'markdown', lineNumbers: true, lineWrapping: true, tabSize: 2, indentWithTabs: false, theme: 'default', }); agentMdEditor.setSize('100%', 240); } function getAgentFromForm() { const profile = document.getElementById('agent-edit-profile').value.trim(); const maxStepsRaw = document.getElementById('agent-edit-max-steps').value; const maxSteps = Number(maxStepsRaw); const spawnAllowlist = (document.getElementById('agent-edit-spawn-allowlist').value || '') .split(',') .map(s => s.trim()) .filter(Boolean); const agent = { id: document.getElementById('agent-edit-id').value.trim(), name: document.getElementById('agent-edit-name').value.trim(), emoji: document.getElementById('agent-edit-emoji').value.trim(), description: document.getElementById('agent-edit-description').value.trim(), workspace: document.getElementById('agent-edit-workspace').value.trim(), model: document.getElementById('agent-edit-model').value.trim(), cronSchedule: document.getElementById('agent-edit-cron').value.trim(), minimalPrompt: document.getElementById('agent-edit-minimal').checked, default: document.getElementById('agent-edit-default').checked, canSpawn: document.getElementById('agent-edit-can-spawn').checked, spawnAllowlist, tools: {}, }; if (profile) agent.tools.profile = profile; if (Number.isFinite(maxSteps) && maxSteps > 0) agent.maxSteps = Math.floor(maxSteps); if (!agent.tools.profile) delete agent.tools; if (!agent.spawnAllowlist.length) delete agent.spawnAllowlist; return agent; } function setAgentForm(agent) { const a = agent || {}; document.getElementById('agent-edit-id').value = a.id || ''; document.getElementById('agent-edit-name').value = a.name || ''; document.getElementById('agent-edit-emoji').value = a.emoji || ''; document.getElementById('agent-edit-description').value = a.description || ''; document.getElementById('agent-edit-workspace').value = a.workspace || ''; document.getElementById('agent-edit-model').value = a.model || ''; document.getElementById('agent-edit-cron').value = a.cronSchedule || ''; document.getElementById('agent-edit-max-steps').value = (a.maxSteps || '') + ''; document.getElementById('agent-edit-default').checked = a.default === true; document.getElementById('agent-edit-minimal').checked = a.minimalPrompt === true; document.getElementById('agent-edit-can-spawn').checked = a.canSpawn === true; document.getElementById('agent-edit-spawn-allowlist').value = Array.isArray(a.spawnAllowlist) ? a.spawnAllowlist.join(',') : ''; document.getElementById('agent-edit-profile').value = a.tools?.profile || ''; } function renderAgentsList() { const el = document.getElementById('settings-agents-list'); if (!el) return; if (!agentsConfigList.length) { el.innerHTML = '
No agents found.
'; return; } el.innerHTML = agentsConfigList.map((a) => { const selected = a.id === selectedAgentId; const lastRun = a.lastRun?.finishedAt ? new Date(a.lastRun.finishedAt).toLocaleString() : 'never'; const heartbeat = a.lastHeartbeatAt ? new Date(a.lastHeartbeatAt).toLocaleString() : 'never'; const badge = a.default ? 'default' : ''; return ` `; }).join(''); } function findSelectedAgent() { return agentsConfigList.find(a => a.id === selectedAgentId) || null; } function agentFormNew() { selectedAgentId = ''; setAgentForm({ id: '', name: '', emoji: '', description: '', workspace: '', model: '', cronSchedule: '', maxSteps: '', default: false, minimalPrompt: true, canSpawn: false, spawnAllowlist: [], tools: {}, }); if (agentMdEditor) agentMdEditor.setValue(''); const mdPath = document.getElementById('agent-md-path'); if (mdPath) mdPath.textContent = '에이전트를 선택하거나 저장하면 AGENTS.md를 불러옵니다'; const resultEl = document.getElementById('agent-spawn-result'); if (resultEl) resultEl.textContent = ''; renderAgentsList(); } async function selectAgent(id) { selectedAgentId = id; const selected = findSelectedAgent(); setAgentForm(selected); renderAgentsList(); await loadSelectedAgentMd(); await loadAgentRunHistory(); } async function loadAgentsTab() { ensureAgentMdEditor(); try { const data = await api('/api/agents'); agentsConfigList = Array.isArray(data?.agents) ? data.agents : []; if (!selectedAgentId || !agentsConfigList.some(a => a.id === selectedAgentId)) { selectedAgentId = data?.defaultAgentId || agentsConfigList[0]?.id || ''; } renderAgentsList(); const selected = findSelectedAgent(); setAgentForm(selected); if (selected) { await loadSelectedAgentMd(); await loadAgentRunHistory(); } else { agentFormNew(); } } catch (err) { addProcessEntry('error', `Failed to load agents: ${err.message}`); } } async function saveAgentFromForm() { const agent = getAgentFromForm(); if (!agent.id) { alert('Agent ID is required'); return; } if (!agent.name) { alert('Agent name is required'); return; } try { const exists = agentsConfigList.some(a => a.id === agent.id); const endpoint = exists ? `/api/agents/${encodeURIComponent(agent.id)}` : '/api/agents'; const method = exists ? 'PUT' : 'POST'; const data = await api(endpoint, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agent }), }); if (!data?.success) throw new Error(data?.error || 'save failed'); selectedAgentId = data?.agent?.id || agent.id; addProcessEntry('info', `Agent "${selectedAgentId}" saved.`); await loadAgentsTab(); } catch (err) { alert(`Failed to save agent: ${err.message}`); } } async function deleteSelectedAgent() { if (!selectedAgentId) return; if (!confirm(`Delete agent "${selectedAgentId}"?`)) return; try { await api(`/api/agents/${encodeURIComponent(selectedAgentId)}`, { method: 'DELETE' }); addProcessEntry('info', `Agent "${selectedAgentId}" deleted.`); selectedAgentId = ''; await loadAgentsTab(); } catch (err) { alert(`Failed to delete agent: ${err.message}`); } } async function loadSelectedAgentMd() { ensureAgentMdEditor(); if (!selectedAgentId || !agentMdEditor) return; try { const data = await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/agents-md`); agentMdEditor.setValue(data?.content || ''); const pathEl = document.getElementById('agent-md-path'); if (pathEl) pathEl.textContent = data?.path || ''; } catch (err) { addProcessEntry('error', `Failed to load AGENTS.md: ${err.message}`); } } async function saveSelectedAgentMd() { if (!selectedAgentId || !agentMdEditor) return; try { const content = agentMdEditor.getValue(); await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/agents-md`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }), }); addProcessEntry('final', `Saved AGENTS.md for "${selectedAgentId}".`); } catch (err) { alert(`Failed to save AGENTS.md: ${err.message}`); } } async function runSelectedAgentOnce() { if (!selectedAgentId) return; const task = (document.getElementById('agent-spawn-task').value || '').trim(); if (!task) { alert('Provide a task first.'); return; } const out = document.getElementById('agent-spawn-result'); if (out) out.textContent = '실행 중...'; try { const data = await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/spawn`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task }), }); const result = data?.result || {}; if (out) { const head = result.success ? `Success (${result.durationMs || 0}ms)` : `Failed: ${result.error || 'unknown error'}`; out.textContent = `${head}\n\n${String(result.result || '').slice(0, 2500)}`; } await loadAgentsTab(); } catch (err) { if (out) out.textContent = `Run failed: ${err.message}`; } } async function loadAgentRunHistory() { const el = document.getElementById('agent-run-history'); if (!el) return; if (!selectedAgentId) { el.innerHTML = ''; return; } try { const data = await api(`/api/agents/history?agentId=${encodeURIComponent(selectedAgentId)}&limit=12`); const rows = Array.isArray(data?.history) ? data.history : []; if (!rows.length) { el.innerHTML = '
No runs yet.
'; return; } el.innerHTML = rows.map((r) => { const color = r.success ? '#1a6e35' : '#9c1a1a'; const when = new Date(r.finishedAt || r.startedAt || Date.now()).toLocaleString(); const label = `${r.trigger || 'manual'} • ${r.durationMs || 0}ms`; const preview = String(r.resultPreview || r.error || '').slice(0, 140); return `
${r.success ? 'success' : 'failed'} ${escHtml(when)}
${escHtml(label)}
${escHtml(preview)}
`; }).join(''); } catch (err) { el.innerHTML = `
Failed to load history: ${escHtml(err.message)}
`; } } const logLines = []; function log(text, type = 'log') { const ts = new Date().toLocaleTimeString(); logLines.push({ text: `[${ts}] ${text}`, type }); if (logLines.length > 100) logLines.shift(); const lp = document.getElementById('log-panel'); if (lp) { lp.innerHTML = logLines.map(l => `
${l.text}
`).join(''); lp.scrollTop = lp.scrollHeight; } } // ---- Skills management (v2 - local skills system) ---- let skillsCache = []; let editingSkillId = null; let skillsTargetUser = ''; // '' = global (admin) or own username (regular user) async function initSkillsUserSelector() { const selector = document.getElementById('skills-user-selector'); if (!selector) return; if (!currentUser) { selector.style.display = 'none'; return; } if (currentUser.role !== 'admin') { selector.style.display = 'none'; skillsTargetUser = currentUser.username || ''; return; } selector.style.display = 'block'; try { const data = await api('/api/users'); const sel = document.getElementById('skills-target-user'); if (!sel || !data.users) return; const opts = ['']; for (const u of data.users) { const selected = u.username === skillsTargetUser ? ' selected' : ''; opts.push(``); } sel.innerHTML = opts.join(''); } catch {} } function onSkillsUserChange() { const sel = document.getElementById('skills-target-user'); skillsTargetUser = sel ? sel.value : ''; loadInstalledSkills(); } async function loadInstalledSkills() { const el = document.getElementById('skills-list'); el.innerHTML = '
Loading...
'; try { const effectiveUser = skillsTargetUser || currentUser?.username || null; const url = effectiveUser ? `/api/skills?username=${encodeURIComponent(effectiveUser)}` : '/api/skills'; const data = await api(url); const skills = data.skills || []; skillsCache = skills; if (skills.length === 0) { el.innerHTML = '
No skills yet.
Click "+ Create" to make one!
'; } else { el.innerHTML = skills.map(s => { const skillEmoji = (s.emoji && s.emoji !== '🦞' && s.emoji !== '⚠️') ? s.emoji : '🧩'; const globalBadge = s.globalOnly ? '[전체]' : ''; return `
${escHtml(skillEmoji)}
${escHtml(s.name)}${globalBadge}
${escHtml(s.description || 'No description')}
`; }).join(''); renderSkillEligibility(skills); } } catch (err) { el.innerHTML = `
Error: ${err.message}
`; } } async function toggleSkill(id) { const skillMeta = (skillsCache || []).find(s => s.id === id); if (skillMeta && skillMeta.eligible === false && !skillMeta.enabled) { addProcessEntry('error', skillMeta.eligibleReason || 'This skill is currently unavailable.'); return; } // globalOnly 스킬은 per-user 모드에서 토글 불가 (서버가 전역으로 처리) if (skillMeta?.globalOnly && skillsTargetUser) { addProcessEntry('error', `"${skillMeta.name}" 스킬은 글로벌 전용입니다.`); return; } try { const effectiveUser = skillsTargetUser || currentUser?.username || null; const body = effectiveUser ? JSON.stringify({ username: effectiveUser }) : undefined; const res = await api(`/api/skills/${encodeURIComponent(id)}/toggle`, { method: 'POST', body }); if (!res.success) { addProcessEntry('error', res.error || 'Failed to toggle skill.'); return; } const label = res.targetUser ? ` (${res.targetUser})` : ''; addProcessEntry('info', `Skill "${res.skill.name}"${label}: ${res.skill.enabled ? 'ENABLED' : 'DISABLED'}`); loadInstalledSkills(); } catch (err) { addProcessEntry('error', `Failed to toggle skill: ${err.message}`); } } async function deleteSkill(id) { if (!confirm(`Delete skill "${id}"?`)) return; try { const res = await api(`/api/skills/${encodeURIComponent(id)}`, { method: 'DELETE' }); if (res.success) { addProcessEntry('info', `Skill "${id}" deleted.`); loadInstalledSkills(); } else { alert(`Delete failed: ${res.error}`); } } catch (err) { alert(`Error: ${err.message}`); } } function openCreateSkillModal() { editingSkillId = null; document.getElementById('skill-modal-title').textContent = '스킬 생성'; document.getElementById('skill-modal-save-btn').textContent = '스킬 생성'; document.getElementById('skill-modal-name').value = ''; document.getElementById('skill-modal-emoji').value = ''; document.getElementById('skill-modal-desc').value = ''; document.getElementById('skill-modal-instructions').value = ''; document.getElementById('skill-modal').style.display = 'flex'; document.getElementById('skill-modal-name').focus(); } async function editSkill(id) { try { const res = await api(`/api/skills/${encodeURIComponent(id)}`); if (!res.success) { alert('Skill not found'); return; } const s = res.skill; editingSkillId = id; document.getElementById('skill-modal-title').textContent = '스킬 편집'; document.getElementById('skill-modal-save-btn').textContent = '변경 저장'; document.getElementById('skill-modal-name').value = s.name || ''; document.getElementById('skill-modal-emoji').value = s.emoji || ''; document.getElementById('skill-modal-desc').value = s.description || ''; document.getElementById('skill-modal-instructions').value = s.instructions || ''; document.getElementById('skill-modal').style.display = 'flex'; document.getElementById('skill-modal-name').focus(); } catch (err) { alert(`Error loading skill: ${err.message}`); } } function closeSkillModal() { document.getElementById('skill-modal').style.display = 'none'; editingSkillId = null; } async function saveSkillFromModal() { const name = document.getElementById('skill-modal-name').value.trim(); const rawEmoji = document.getElementById('skill-modal-emoji').value.trim(); const emoji = (rawEmoji === '🧩' || rawEmoji === '🦞' || rawEmoji === '⚠️') ? rawEmoji : '🧩'; const description = document.getElementById('skill-modal-desc').value.trim(); const instructions = document.getElementById('skill-modal-instructions').value.trim(); if (!name) { alert('Name is required'); return; } if (!instructions) { alert('Instructions are required'); return; } const btn = document.getElementById('skill-modal-save-btn'); btn.disabled = true; btn.textContent = '저장 중...'; try { let res; if (editingSkillId) { // Update existing res = await api(`/api/skills/${encodeURIComponent(editingSkillId)}`, { method: 'PUT', body: JSON.stringify({ name, emoji, description, instructions }), }); } else { // Create new res = await api('/api/skills', { method: 'POST', body: JSON.stringify({ name, emoji, description, instructions }), }); } if (res.success) { addProcessEntry('info', `Skill "${name}" ${editingSkillId ? 'updated' : 'created'}!`); closeSkillModal(); loadInstalledSkills(); } else { alert(`Failed: ${res.error}`); } } catch (err) { alert(`Error: ${err.message}`); } finally { btn.disabled = false; btn.textContent = editingSkillId ? '변경 저장' : '스킬 생성'; } }