mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-17 00:46:53 +08:00
* feat: add web capabilities dashboard with agents, skills, commands, MCPs, rules, and hooks * fix: address code review - XSS, env exposure, port validation, error handling, packaging * add tests for dashboard
776 lines
63 KiB
JavaScript
776 lines
63 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* ECC Capabilities Dashboard — agents, skills, commands, MCPs, rules & hooks
|
||
* With multi-language, routing, search suggestions, recently viewed, fine UI
|
||
*
|
||
* Usage: node scripts/dashboard-web.js [port]
|
||
* Open http://localhost:3456
|
||
*
|
||
* Contribution: https://github.com/affaan-m/ECC
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const http = require('http');
|
||
|
||
function parsePort(v) {
|
||
const n = parseInt(String(v), 10);
|
||
if (isNaN(n) || n < 1 || n > 65535) { console.error('[ECC] Invalid port: ' + v + ' — using 3456'); return 3456; }
|
||
return n;
|
||
}
|
||
const PORT = parsePort(process.argv[2] || process.env.ECC_DASHBOARD_PORT || '3456');
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
|
||
function readFrontmatter(p) {
|
||
try {
|
||
const c = fs.readFileSync(p, 'utf8');
|
||
const m = c.match(/^---\n([\s\S]*?)\n---/);
|
||
if (!m) return {};
|
||
const fm = {};
|
||
for (const l of m[1].split('\n')) {
|
||
const s = l.indexOf(':'); if (s <= 0) continue;
|
||
let k = l.slice(0, s).trim(), v = l.slice(s + 1).trim();
|
||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
||
if (v.startsWith('[') && v.endsWith(']')) { try { v = JSON.parse(v); } catch { v = v.slice(1, -1).split(',').map(x => x.trim().replace(/["']/g, '')); } }
|
||
fm[k] = v;
|
||
}
|
||
fm._body = c.replace(/^---[\s\S]*?---\n*/, '').trim();
|
||
return fm;
|
||
} catch { return {}; }
|
||
}
|
||
function readSkill(p) { try { const c = fs.readFileSync(p, 'utf8'); const fm = readFrontmatter(p); return { d: fm.description || '', b: c.replace(/^---[\s\S]*?---\n*/, '').trim() }; } catch { return { d: '', b: '' }; } }
|
||
|
||
function loadAgents(_root) {
|
||
const root = _root || ROOT;
|
||
const dir = path.join(root, 'agents'); if (!fs.existsSync(dir)) return [];
|
||
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort().map(f => {
|
||
const fm = readFrontmatter(path.join(dir, f));
|
||
return { n: fm.name || f.replace('.md', ''), d: fm.description || '', m: fm.model || 'default', t: Array.isArray(fm.tools) ? fm.tools : [], b: (fm._body || '').slice(0, 1200), f };
|
||
});
|
||
}
|
||
function loadSkills(_root) {
|
||
const root = _root || ROOT;
|
||
const dir = path.join(root, 'skills'); if (!fs.existsSync(dir)) return [];
|
||
return fs.readdirSync(dir).filter(d => { try { return fs.statSync(path.join(dir, d)).isDirectory(); } catch { return false; } }).sort().map(d => {
|
||
const r = readSkill(path.join(dir, d, 'SKILL.md')); return { n: d, d: r.d, b: r.b.slice(0, 1000) };
|
||
});
|
||
}
|
||
function loadCommands(_root) {
|
||
const root = _root || ROOT;
|
||
const dir = path.join(root, 'commands'); if (!fs.existsSync(dir)) return [];
|
||
const cm = { plan: 'Planning', 'plan-': 'Planning', 'prp-': 'Git & PR', pr: 'Git & PR', 'review-': 'Review', 'code-': 'Review', build: 'Build', fix: 'Build', test: 'Testing', 'e2e': 'Testing', coverage: 'Testing', quality: 'Testing', session: 'Session', save: 'Session', resume: 'Session', skill: 'Knowledge', learn: 'Knowledge', instinct: 'Knowledge', evolve: 'Knowledge', ecc: 'System', hookify: 'System', model: 'System', setup: 'System', multi: 'Multi-Agent', security: 'Security', harness: 'Security', 'go-': 'Languages', 'rust-': 'Languages', 'cpp-': 'Languages', 'kotlin-': 'Languages', 'flutter-': 'Languages', 'react-': 'Languages', 'python-': 'Languages', 'fastapi-': 'Languages', 'gradle-': 'Languages', gan: 'GAN', marketing: 'Marketing', jira: 'Project', pm2: 'Process', cost: 'Analytics', promote: 'Project', aside: 'Other', santa: 'Fun' };
|
||
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort().map(f => {
|
||
const fm = readFrontmatter(path.join(dir, f));
|
||
const n = '/' + f.replace('.md', ''); let c = 'Other';
|
||
for (const [p, cat] of Object.entries(cm)) if (f.startsWith(p)) { c = cat; break; }
|
||
return { n, f, d: fm.description || fm['argument-hint'] || '', c, b: (fm._body || '').slice(0, 600) };
|
||
});
|
||
}
|
||
function loadRules(_root) {
|
||
const root = _root || ROOT;
|
||
const dir = path.join(root, 'rules'); if (!fs.existsSync(dir)) return [];
|
||
return fs.readdirSync(dir).filter(d => { try { return fs.statSync(path.join(dir, d)).isDirectory(); } catch { return false; } }).sort().map(l => ({ l, f: fs.readdirSync(path.join(dir, l)).filter(f => f.endsWith('.md')).sort().map(f => f.replace('.md', '')) }));
|
||
}
|
||
function loadMcps(_root) {
|
||
const root = _root || ROOT;
|
||
const r = [];
|
||
const m = path.join(root, '.mcp.json');
|
||
if (fs.existsSync(m)) { try { const d = JSON.parse(fs.readFileSync(m, 'utf8')); r.push({ f: '.mcp.json', s: Object.entries(d.mcpServers || {}).map(([k, v]) => ({ n: k, cmd: typeof v === 'object' ? (v.command || v.url || '') : String(v), args: v.args || [], env: v.env ? Object.keys(v.env).reduce((a,k)=>{a[k]='••••••'; return a;}, {}) : {}, type: v.type || 'stdio' })) }); } catch (e) { console.error('[ECC] Failed to parse .mcp.json:', e.message); } }
|
||
const dir = path.join(root, 'mcp-configs');
|
||
if (fs.existsSync(dir)) { for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.json'))) { try { const d = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); r.push({ f, s: Object.entries(d.mcpServers || {}).map(([k, v]) => ({ n: k, cmd: typeof v === 'object' ? (v.command || v.url || '') : String(v), args: v.args || [], env: v.env ? Object.keys(v.env).reduce((a,k)=>{a[k]='••••••'; return a;}, {}) : {}, type: v.type || 'stdio' })) }); } catch (e) { console.error('[ECC] Failed to parse mcp-configs/' + f + ':', e.message); } } }
|
||
return r;
|
||
}
|
||
function loadHooks(_root) {
|
||
const root = _root || ROOT;
|
||
const p = path.join(root, 'hooks', 'hooks.json'); if (!fs.existsSync(p)) return [];
|
||
try { const d = JSON.parse(fs.readFileSync(p, 'utf8')); const h = []; for (const [ev, es] of Object.entries(d.hooks || {})) for (const e of es || []) h.push({ ev, m: e.matcher || '*', id: e.id || '', d: e.description || '' }); return h; } catch (e) { console.error('[ECC] Failed to parse hooks/hooks.json:', e.message); return []; }
|
||
}
|
||
|
||
const LANG = {
|
||
en: { name:'English', title:'ECC Capabilities', search:'Search agents, skills, commands...', agents:'Agents', skills:'Skills', commands:'Commands', rules:'Rules', mcps:'MCPs', hooks:'Hooks', ruleSets:'Rule Sets', mcpConfigs:'MCP Configs', all:'All', reviewers:'Reviewers', buildResolvers:'Build Resolvers', architects:'Architects', security:'Security', testing:'Testing', patterns:'Patterns', design:'Design', research:'Research', data:'Data', agent:'Agent', devops:'DevOps', description:'Description', details:'Details', tools:'Tools', copied:'Copied', noMcps:'No MCP configs found', checkMcps:'Check mcp-configs/ directory', noHooks:'No hooks configured', recentlyViewed:'Recently Viewed', clearHistory:'Clear', ruleFiles:'rule files', more:'more', servers:'servers', skill:'Skill', workflow:'workflow', event:'Event', matcher:'Matcher', id:'ID', contribution:'Contribution to ECC' },
|
||
pt: { name:'Português', title:'Recursos do ECC', search:'Pesquisar agentes, skills, comandos...', agents:'Agentes', skills:'Skills', commands:'Comandos', rules:'Regras', mcps:'MCPs', hooks:'Hooks', ruleSets:'Conjuntos de Regras', mcpConfigs:'Configs MCP', all:'Todos', reviewers:'Revisores', buildResolvers:'Resolvedores', architects:'Arquitetos', security:'Segurança', testing:'Testes', patterns:'Padrões', design:'Design', research:'Pesquisa', data:'Dados', agent:'Agente', devops:'DevOps', description:'Descrição', details:'Detalhes', tools:'Ferramentas', copied:'Copiado', noMcps:'Nenhuma config MCP encontrada', checkMcps:'Verifique mcp-configs/', noHooks:'Nenhum hook configurado', recentlyViewed:'Vistos Recentemente', clearHistory:'Limpar', ruleFiles:'arquivos de regras', more:'mais', servers:'servidores', skill:'Skill', workflow:'workflow', event:'Evento', matcher:'Corresp.', id:'ID', contribution:'Contribuição ao ECC' },
|
||
zh: { name:'简体中文', title:'ECC 能力', search:'搜索代理、技能、命令...', agents:'代理', skills:'技能', commands:'命令', rules:'规则', mcps:'MCP', hooks:'钩子', ruleSets:'规则集', mcpConfigs:'MCP 配置', all:'全部', reviewers:'审查者', buildResolvers:'构建解析器', architects:'架构师', security:'安全', testing:'测试', patterns:'模式', design:'设计', research:'研究', data:'数据', agent:'代理', devops:'运维', description:'描述', details:'详情', tools:'工具', copied:'已复制', noMcps:'未找到 MCP 配置', checkMcps:'检查 mcp-configs/ 目录', noHooks:'未配置钩子', recentlyViewed:'最近查看', clearHistory:'清除', ruleFiles:'规则文件', more:'更多', servers:'服务器', skill:'技能', workflow:'工作流', event:'事件', matcher:'匹配器', id:'ID', contribution:'对 ECC 的贡献' },
|
||
zht: { name:'繁體中文', title:'ECC 能力', search:'搜索代理、技能、命令...', agents:'代理', skills:'技能', commands:'命令', rules:'規則', mcps:'MCP', hooks:'鉤子', ruleSets:'規則集', mcpConfigs:'MCP 配置', all:'全部', reviewers:'審查者', buildResolvers:'構建解析器', architects:'架構師', security:'安全', testing:'測試', patterns:'模式', design:'設計', research:'研究', data:'數據', agent:'代理', devops:'運維', description:'描述', details:'詳情', tools:'工具', copied:'已複製', noMcps:'未找到 MCP 配置', checkMcps:'檢查 mcp-configs/ 目錄', noHooks:'未配置鉤子', recentlyViewed:'最近查看', clearHistory:'清除', ruleFiles:'規則文件', more:'更多', servers:'服務器', skill:'技能', workflow:'工作流', event:'事件', matcher:'匹配器', id:'ID', contribution:'對 ECC 的貢獻' },
|
||
ja: { name:'日本語', title:'ECC 機能一覧', search:'エージェント、スキル、コマンドを検索...', agents:'エージェント', skills:'スキル', commands:'コマンド', rules:'ルール', mcps:'MCP', hooks:'フック', ruleSets:'ルールセット', mcpConfigs:'MCP設定', all:'すべて', reviewers:'レビュアー', buildResolvers:'ビルド解決', architects:'アーキテクト', security:'セキュリティ', testing:'テスト', patterns:'パターン', design:'デザイン', research:'研究', data:'データ', agent:'エージェント', devops:'DevOps', description:'説明', details:'詳細', tools:'ツール', copied:'コピーしました', noMcps:'MCP設定が見つかりません', checkMcps:'mcp-configs/を確認', noHooks:'フックが設定されていません', recentlyViewed:'最近見た項目', clearHistory:'クリア', ruleFiles:'ルールファイル', more:'もっと見る', servers:'サーバー', skill:'スキル', workflow:'ワークフロー', event:'イベント', matcher:'マッチャー', id:'ID', contribution:'ECCへの貢献' },
|
||
ko: { name:'한국어', title:'ECC 기능', search:'에이전트, 스킬, 명령어 검색...', agents:'에이전트', skills:'스킬', commands:'명령어', rules:'규칙', mcps:'MCP', hooks:'훅', ruleSets:'규칙 세트', mcpConfigs:'MCP 설정', all:'전체', reviewers:'리뷰어', buildResolvers:'빌드 해결사', architects:'아키텍트', security:'보안', testing:'테스트', patterns:'패턴', design:'디자인', research:'연구', data:'데이터', agent:'에이전트', devops:'DevOps', description:'설명', details:'세부정보', tools:'도구', copied:'복사됨', noMcps:'MCP 설정을 찾을 수 없음', checkMcps:'mcp-configs/ 확인', noHooks:'훅이 설정되지 않음', recentlyViewed:'최근 본 항목', clearHistory:'지우기', ruleFiles:'규칙 파일', more:'더보기', servers:'서버', skill:'스킬', workflow:'워크플로우', event:'이벤트', matcher:'매처', id:'ID', contribution:'ECC에 기여' },
|
||
tr: { name:'Türkçe', title:'ECC Yetenekleri', search:'Ajan, beceri, komut ara...', agents:'Ajanlar', skills:'Beceriler', commands:'Komutlar', rules:'Kurallar', mcps:'MCP\'ler', hooks:'Kancalar', ruleSets:'Kural Setleri', mcpConfigs:'MCP Yapılandırmaları', all:'Tümü', reviewers:'İnceleyenler', buildResolvers:'Derleme Çözücüler', architects:'Mimarlar', security:'Güvenlik', testing:'Test', patterns:'Desenler', design:'Tasarım', research:'Araştırma', data:'Veri', agent:'Ajan', devops:'DevOps', description:'Açıklama', details:'Detaylar', tools:'Araçlar', copied:'Kopyalandı', noMcps:'MCP yapılandırması bulunamadı', checkMcps:'mcp-configs/ dizinini kontrol edin', noHooks:'Kanca yapılandırılmamış', recentlyViewed:'Son Görüntülenenler', clearHistory:'Temizle', ruleFiles:'kural dosyası', more:'daha fazla', servers:'sunucu', skill:'Beceri', workflow:'iş akışı', event:'Olay', matcher:'Eşleştirici', id:'ID', contribution:'ECC\'ye Katkı' },
|
||
ru: { name:'Русский', title:'Возможности ECC', search:'Поиск агентов, навыков, команд...', agents:'Агенты', skills:'Навыки', commands:'Команды', rules:'Правила', mcps:'MCP', hooks:'Хуки', ruleSets:'Наборы правил', mcpConfigs:'MCP конфиги', all:'Все', reviewers:'Ревьюеры', buildResolvers:'Сборщики', architects:'Архитекторы', security:'Безопасность', testing:'Тестирование', patterns:'Паттерны', design:'Дизайн', research:'Исследования', data:'Данные', agent:'Агент', devops:'DevOps', description:'Описание', details:'Детали', tools:'Инструменты', copied:'Скопировано', noMcps:'MCP конфиги не найдены', checkMcps:'Проверьте mcp-configs/', noHooks:'Хуки не настроены', recentlyViewed:'Недавние', clearHistory:'Очистить', ruleFiles:'файлов правил', more:'ещё', servers:'серверов', skill:'Навык', workflow:'воркфлоу', event:'Событие', matcher:'Матчер', id:'ID', contribution:'Вклад в ECC' },
|
||
vi: { name:'Tiếng Việt', title:'Năng lực ECC', search:'Tìm kiếm agent, kỹ năng, lệnh...', agents:'Agent', skills:'Kỹ năng', commands:'Lệnh', rules:'Luật', mcps:'MCP', hooks:'Hook', ruleSets:'Bộ luật', mcpConfigs:'Cấu hình MCP', all:'Tất cả', reviewers:'Người đánh giá', buildResolvers:'Trình giải quyết build', architects:'Kiến trúc sư', security:'Bảo mật', testing:'Kiểm thử', patterns:'Mẫu', design:'Thiết kế', research:'Nghiên cứu', data:'Dữ liệu', agent:'Agent', devops:'DevOps', description:'Mô tả', details:'Chi tiết', tools:'Công cụ', copied:'Đã sao chép', noMcps:'Không tìm thấy cấu hình MCP', checkMcps:'Kiểm tra mcp-configs/', noHooks:'Chưa có hook nào', recentlyViewed:'Đã xem gần đây', clearHistory:'Xóa', ruleFiles:'tệp luật', more:'thêm', servers:'máy chủ', skill:'Kỹ năng', workflow:'quy trình', event:'Sự kiện', matcher:'Bộ so khớp', id:'ID', contribution:'Đóng góp cho ECC' },
|
||
th: { name:'ไทย', title:'ความสามารถ ECC', search:'ค้นหาเอเจนต์ ทักษะ คำสั่ง...', agents:'เอเจนต์', skills:'ทักษะ', commands:'คำสั่ง', rules:'กฎ', mcps:'MCP', hooks:'ฮุค', ruleSets:'ชุดกฎ', mcpConfigs:'การตั้งค่า MCP', all:'ทั้งหมด', reviewers:'ผู้ตรวจสอบ', buildResolvers:'ตัวแก้ไขบิลด์', architects:'สถาปนิก', security:'ความปลอดภัย', testing:'การทดสอบ', patterns:'รูปแบบ', design:'ออกแบบ', research:'วิจัย', data:'ข้อมูล', agent:'เอเจนต์', devops:'DevOps', description:'คำอธิบาย', details:'รายละเอียด', tools:'เครื่องมือ', copied:'คัดลอกแล้ว', noMcps:'ไม่พบการตั้งค่า MCP', checkMcps:'ตรวจสอบ mcp-configs/', noHooks:'ไม่มีการตั้งค่าฮุค', recentlyViewed:'ที่ดูล่าสุด', clearHistory:'ล้าง', ruleFiles:'ไฟล์กฎ', more:'เพิ่มเติม', servers:'เซิร์ฟเวอร์', skill:'ทักษะ', workflow:'เวิร์กโฟลว์', event:'เหตุการณ์', matcher:'ตัวจับคู่', id:'ID', contribution:'มีส่วนร่วมกับ ECC' },
|
||
de: { name:'Deutsch', title:'ECC-Funktionen', search:'Agenten, Fähigkeiten, Befehle suchen...', agents:'Agenten', skills:'Fähigkeiten', commands:'Befehle', rules:'Regeln', mcps:'MCPs', hooks:'Hooks', ruleSets:'Regelsätze', mcpConfigs:'MCP-Konfigurationen', all:'Alle', reviewers:'Prüfer', buildResolvers:'Build-Resolver', architects:'Architekten', security:'Sicherheit', testing:'Tests', patterns:'Muster', design:'Design', research:'Forschung', data:'Daten', agent:'Agent', devops:'DevOps', description:'Beschreibung', details:'Details', tools:'Werkzeuge', copied:'Kopiert', noMcps:'Keine MCP-Konfigurationen gefunden', checkMcps:'Prüfen Sie mcp-configs/', noHooks:'Keine Hooks konfiguriert', recentlyViewed:'Zuletzt angesehen', clearHistory:'Löschen', ruleFiles:'Regeldateien', more:'mehr', servers:'Server', skill:'Fähigkeit', workflow:'Workflow', event:'Ereignis', matcher:'Matcher', id:'ID', contribution:'Beitrag zu ECC' },
|
||
};
|
||
const LANG_KEYS = Object.keys(LANG);
|
||
|
||
function renderHTML(data) {
|
||
// data passed from Node.js - use for static template values
|
||
const ag = JSON.stringify(data.agents).replace(/</g, '\\u003c');
|
||
const sk = JSON.stringify(data.skills).replace(/</g, '\\u003c');
|
||
const co = JSON.stringify(data.commands).replace(/</g, '\\u003c');
|
||
const ru = JSON.stringify(data.rules).replace(/</g, '\\u003c');
|
||
const mc = JSON.stringify(data.mcps).replace(/</g, '\\u003c');
|
||
const ho = JSON.stringify(data.hooks).replace(/</g, '\\u003c');
|
||
const ll = JSON.stringify(LANG).replace(/</g, '\\u003c');
|
||
const lc = JSON.stringify(LANG_KEYS);
|
||
|
||
/* eslint-disable no-useless-escape */
|
||
return `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0">
|
||
<title>ECC Capabilities</title>
|
||
<style>
|
||
:root {
|
||
--bg: #080a0e; --bg2: #0d0f14; --bg3: #13161e; --bg4: #191d2a;
|
||
--surface: #101218; --surface-hover: #171a24; --border: #1d2130; --border-light: #272c3e;
|
||
--text: #dfe2e9; --text2: #80859a; --text3: #4c5168;
|
||
--accent: #6885e8; --accent-glow: rgba(104,133,232,0.15); --accent-dim: #3d5ab8;
|
||
--green: #4acb8a; --green-glow: rgba(74,203,138,0.15);
|
||
--orange: #eca85a; --orange-glow: rgba(236,168,90,0.15);
|
||
--pink: #e26a9e; --pink-glow: rgba(226,106,158,0.15);
|
||
--red: #e86060; --red-glow: rgba(232,96,96,0.15);
|
||
--teal: #4acbbe; --teal-glow: rgba(74,203,190,0.15);
|
||
--radius: 8px; --radius-sm: 5px;
|
||
--font: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Inter', 'Segoe UI', Roboto, sans-serif;
|
||
--mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace;
|
||
--shadow: 0 1px 2px rgba(0,0,0,0.4);
|
||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.6);
|
||
}
|
||
[data-theme="light"] {
|
||
--bg: #f4f5f7; --bg2: #ffffff; --bg3: #eaecef; --bg4: #dfe2e6;
|
||
--surface: #ffffff; --surface-hover: #f4f5f7; --border: #cdd1d9; --border-light: #dde1e8;
|
||
--text: #181b23; --text2: #585e6e; --text3: #9197a8;
|
||
--accent: #4560d0; --accent-glow: rgba(69,96,208,0.08); --accent-dim: #2f44a0;
|
||
--green: #16a34a; --green-glow: rgba(22,163,74,0.08);
|
||
--orange: #d97706; --orange-glow: rgba(217,119,6,0.08);
|
||
--pink: #c73877; --pink-glow: rgba(199,56,119,0.08);
|
||
--red: #dc2626; --red-glow: rgba(220,38,38,0.08);
|
||
--teal: #0d9488; --teal-glow: rgba(13,148,136,0.08);
|
||
--shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.08);
|
||
}
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.4}
|
||
::selection{background:var(--accent);color:#fff}
|
||
::-webkit-scrollbar{width:4px;height:4px}
|
||
::-webkit-scrollbar-track{background:transparent}
|
||
::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||
a{color:var(--accent);text-decoration:none}
|
||
a:hover{text-decoration:underline}
|
||
|
||
.header{background:color-mix(in srgb,var(--bg2) 85%,transparent);border-bottom:1px solid var(--border);padding:0 28px;display:flex;align-items:center;height:54px;gap:12px;position:sticky;top:0;z-index:100;backdrop-filter:blur(16px)}
|
||
.brand{display:flex;align-items:center;gap:9px;cursor:pointer;user-select:none;flex-shrink:0}
|
||
.brand .logo{width:26px;height:26px;background:linear-gradient(135deg,var(--accent),var(--pink));border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff;transition:transform .15s}
|
||
.brand:hover .logo{transform:scale(1.05)}
|
||
.brand h1{font-size:14px;font-weight:600;letter-spacing:-.01em}
|
||
.brand .ver{font-size:9px;font-weight:500;color:var(--text3);background:var(--bg3);padding:1px 6px;border-radius:3px;margin-left:2px;letter-spacing:0}
|
||
.header-center{flex:1;min-width:0}
|
||
.header-right{display:flex;align-items:center;gap:6px;flex-shrink:0}
|
||
|
||
.search{position:relative;width:260px}
|
||
.search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;color:var(--text3);pointer-events:none}
|
||
.search input{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:6px 10px 6px 30px;color:var(--text);font-size:12.5px;outline:none;transition:all .2s;font-family:var(--font)}
|
||
.search input:focus{border-color:var(--accent);background:var(--bg2);box-shadow:0 0 0 3px var(--accent-glow)}
|
||
.search input::placeholder{color:var(--text3)}
|
||
.search .hint{position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:9px;color:var(--text3);background:var(--bg4);padding:1px 4px;border-radius:3px;pointer-events:none;line-height:1.4}
|
||
|
||
.suggest{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--bg2);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-lg);max-height:360px;overflow-y:auto;display:none;z-index:200}
|
||
.suggest.show{display:block}
|
||
.suggest .sg{padding:4px 0}
|
||
.suggest .sg-label{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);padding:5px 10px 2px}
|
||
.suggest .si{display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;transition:background .1s;font-size:12px;color:var(--text)}
|
||
.suggest .si:hover,.suggest .si.active{background:var(--surface-hover)}
|
||
.suggest .si .ic{width:18px;height:18px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0}
|
||
.suggest .si .ic.a{background:var(--accent-glow);color:var(--accent)}
|
||
.suggest .si .ic.s{background:var(--green-glow);color:var(--green)}
|
||
.suggest .si .ic.c{background:var(--orange-glow);color:var(--orange)}
|
||
.suggest .si .sn{font-weight:500}
|
||
.suggest .si .sd{font-size:10px;color:var(--text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
||
.suggest .si .stg{font-size:9px;color:var(--text3);background:var(--bg3);padding:0 5px;border-radius:3px}
|
||
|
||
.icon-btn{width:28px;height:28px;border-radius:6px;border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s;font-size:13px}
|
||
.icon-btn:hover{border-color:var(--border-light);color:var(--text);background:var(--bg4)}
|
||
.icon-btn:active{transform:scale(.93)}
|
||
|
||
.lang-wrap{position:relative}
|
||
.lang-btn{font-size:11px;padding:3px 8px;border-radius:5px;border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all .12s;display:flex;align-items:center;gap:4px;font-family:var(--font)}
|
||
.lang-btn:hover{border-color:var(--border-light);color:var(--text)}
|
||
.lang-drop{position:absolute;top:calc(100% + 4px);right:0;background:var(--bg2);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-lg);min-width:180px;display:none;z-index:200;max-height:280px;overflow-y:auto}
|
||
.lang-drop.show{display:block}
|
||
.lang-drop .li{padding:6px 12px;cursor:pointer;font-size:12px;color:var(--text2);transition:background .1s}
|
||
.lang-drop .li:hover{background:var(--surface-hover);color:var(--text)}
|
||
.lang-drop .li.active{color:var(--accent);background:var(--accent-glow)}
|
||
|
||
.nav{display:flex;background:color-mix(in srgb,var(--bg2) 80%,transparent);border-bottom:1px solid var(--border);padding:0 28px;gap:2px;position:sticky;top:54px;z-index:99;overflow-x:auto;backdrop-filter:blur(12px)}
|
||
.nav-it{padding:10px 16px;cursor:pointer;font-size:12.5px;font-weight:500;color:var(--text2);border-bottom:2px solid transparent;transition:all .12s;white-space:nowrap;background:none;border-top:none;border-left:none;border-right:none;display:flex;align-items:center;gap:5px;font-family:var(--font)}
|
||
.nav-it:hover{color:var(--text);background:var(--accent-glow)}
|
||
.nav-it.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||
.nav-it .ct{font-size:9px;font-weight:500;padding:0 5px;border-radius:3px;background:var(--bg3);color:var(--text3);line-height:1.5}
|
||
.nav-it.active .ct{background:var(--accent-glow);color:var(--accent)}
|
||
|
||
.out{max-width:1280px;margin:0 auto;padding:18px 28px;min-height:calc(100vh - 110px)}
|
||
|
||
.stats{display:grid;grid-template-columns:repeat(6,1fr);gap:6px;margin-bottom:16px}
|
||
.stat{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 8px;text-align:center;cursor:pointer;transition:all .12s}
|
||
.stat:hover{border-color:var(--border-light);transform:translateY(-1px);box-shadow:var(--shadow)}
|
||
.stat:active{transform:translateY(0)}
|
||
.stat .num{font-size:20px;font-weight:700;line-height:1.2}
|
||
.stat .lbl{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-top:1px;font-weight:500}
|
||
.stat.c0 .num{color:var(--accent)}.stat.c1 .num{color:var(--green)}.stat.c2 .num{color:var(--orange)}
|
||
.stat.c3 .num{color:var(--pink)}.stat.c4 .num{color:var(--teal)}.stat.c5 .num{color:var(--red)}
|
||
|
||
.panel{display:none;animation:fadeIn .12s ease}
|
||
.panel.active{display:block}
|
||
@keyframes fadeIn{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}
|
||
|
||
.recent-bar{display:flex;align-items:center;gap:8px;margin-bottom:14px;padding:8px 12px;background:var(--accent-glow);border:1px solid rgba(104,133,232,0.3);border-radius:var(--radius);font-size:12px;flex-wrap:wrap}
|
||
.recent-bar .rb-lbl{font-weight:600;color:var(--accent);font-size:11px;text-transform:uppercase;letter-spacing:.04em}
|
||
.recent-bar .rb-items{display:flex;gap:4px;flex-wrap:wrap;flex:1}
|
||
.recent-bar .rb-item{font-size:11px;padding:2px 8px;border-radius:4px;background:var(--bg3);cursor:pointer;transition:all .12s;color:var(--text2)}
|
||
.recent-bar .rb-item:hover{background:var(--accent-glow);color:var(--accent)}
|
||
.recent-bar .rb-clear{font-size:10px;color:var(--text3);cursor:pointer;padding:2px 6px;border-radius:3px;transition:all .12s;flex-shrink:0}
|
||
.recent-bar .rb-clear:hover{color:var(--red);background:var(--red-glow)}
|
||
|
||
.filters{display:flex;gap:3px;flex-wrap:wrap;margin-bottom:12px}
|
||
.filters button{font-size:10.5px;font-weight:500;padding:3px 10px;border-radius:5px;border:1px solid var(--border);background:transparent;color:var(--text2);cursor:pointer;transition:all .12s;font-family:var(--font)}
|
||
.filters button:hover{border-color:var(--border-light);color:var(--text)}
|
||
.filters button.active{background:var(--accent-glow);border-color:var(--accent);color:var(--accent)}
|
||
|
||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:6px}
|
||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px;cursor:pointer;transition:all .12s;position:relative}
|
||
.card:hover{border-color:var(--border-light);background:var(--surface-hover);box-shadow:var(--shadow)}
|
||
.card:active{transform:scale(.995)}
|
||
.card .top{display:flex;align-items:flex-start;justify-content:space-between;gap:6px}
|
||
.card .top .il{display:flex;align-items:center;gap:6px;min-width:0}
|
||
.card .top .il .ic{width:20px;height:20px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0}
|
||
.card .top .il .ic.i0{background:var(--accent-glow);color:var(--accent)}
|
||
.card .top .il .ic.i1{background:var(--green-glow);color:var(--green)}
|
||
.card .top .il .ic.i2{background:var(--orange-glow);color:var(--orange)}
|
||
.card .top .il .ic.i3{background:var(--pink-glow);color:var(--pink)}
|
||
.card .top .il .ic.i4{background:var(--teal-glow);color:var(--teal)}
|
||
.card .top .il .ic.i5{background:var(--red-glow);color:var(--red)}
|
||
.card .top .il .nm{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.card .top .bd{font-size:9px;font-weight:600;padding:1px 5px;border-radius:3px;text-transform:uppercase;letter-spacing:.03em;flex-shrink:0}
|
||
.card .top .bd.opus{background:var(--pink-glow);color:var(--pink)}
|
||
.card .top .bd.sonnet{background:var(--accent-glow);color:var(--accent)}
|
||
.card .top .bd.haiku{background:var(--green-glow);color:var(--green)}
|
||
.card .desc{font-size:11.5px;color:var(--text2);margin-top:4px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.card .tags{margin-top:6px;display:flex;flex-wrap:wrap;gap:2px}
|
||
.card .tags .t{font-size:9px;font-weight:500;padding:1px 5px;border-radius:3px;background:var(--bg3);color:var(--text3)}
|
||
.card .ar{position:absolute;bottom:10px;right:12px;font-size:9px;color:var(--text3);opacity:0;transition:opacity .12s}
|
||
.card:hover .ar{opacity:1}
|
||
|
||
.cmd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:4px}
|
||
.cmd-it{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:7px 10px;display:flex;align-items:center;gap:8px;transition:all .12s;cursor:pointer}
|
||
.cmd-it:hover{border-color:var(--border-light);background:var(--surface-hover)}
|
||
.cmd-it .cl{flex:1;min-width:0}
|
||
.cmd-it .cn{font-family:var(--mono);font-size:11.5px;font-weight:600;color:var(--accent)}
|
||
.cmd-it .cd{font-size:10.5px;color:var(--text2);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.cmd-it .cc{font-size:9px;color:var(--text3);font-weight:500}
|
||
.cmd-it .cpy{flex-shrink:0;width:24px;height:24px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--text3);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s;font-size:11px}
|
||
.cmd-it .cpy:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-glow)}
|
||
.cmd-it .cpy.done{border-color:var(--green);color:var(--green);background:var(--green-glow)}
|
||
|
||
.rules-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:6px}
|
||
.rule-cd{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;cursor:pointer;transition:all .12s}
|
||
.rule-cd:hover{border-color:var(--border-light);box-shadow:var(--shadow)}
|
||
.rule-cd h3{font-size:12.5px;font-weight:600;color:var(--accent);text-transform:capitalize;margin-bottom:5px;display:flex;align-items:center;gap:5px}
|
||
.rule-cd .rf{font-size:10.5px;color:var(--text2);padding:1.5px 0;display:flex;align-items:center;gap:4px}
|
||
.rule-cd .rf::before{content:'';width:2.5px;height:2.5px;border-radius:50%;background:var(--text3);flex-shrink:0}
|
||
|
||
.mcp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:6px}
|
||
.mcp-cd{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;cursor:pointer;transition:all .12s}
|
||
.mcp-cd:hover{border-color:var(--border-light);box-shadow:var(--shadow)}
|
||
.mcp-cd h3{font-size:11.5px;font-weight:600;margin-bottom:5px;color:var(--text);display:flex;align-items:center;gap:5px}
|
||
.mcp-cd .st{display:inline-block;font-size:10px;font-weight:500;padding:1px 6px;border-radius:3px;background:var(--bg3);color:var(--text2);margin:1.5px;font-family:var(--mono);max-width:100%;overflow:hidden;text-overflow:ellipsis}
|
||
.mcp-cd .st small{color:var(--text3);font-weight:400;margin-left:3px;font-family:var(--font)}
|
||
|
||
.hw{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}
|
||
.ht{width:100%;border-collapse:collapse;font-size:11.5px}
|
||
.ht th{text-align:left;font-weight:600;color:var(--text3);padding:8px 10px;border-bottom:1px solid var(--border);font-size:9.5px;text-transform:uppercase;letter-spacing:.04em;background:var(--bg2)}
|
||
.ht td{padding:6px 10px;border-bottom:1px solid var(--border);cursor:pointer}
|
||
.ht tr:last-child td{border-bottom:none}
|
||
.ht tr:hover td{background:var(--surface-hover)}
|
||
.ht .ev{color:var(--accent);font-weight:500;font-size:10.5px}
|
||
.ht .mt{font-family:var(--mono);font-size:9.5px;background:var(--bg3);padding:1px 4px;border-radius:2px;color:var(--text2)}
|
||
|
||
.page{max-width:800px;margin:0 auto;padding:24px 28px 60px;animation:fadeIn .15s ease}
|
||
.page .back{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:5px;border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;font-size:11px;transition:all .12s;margin-bottom:16px;font-family:var(--font)}
|
||
.page .back:hover{border-color:var(--border-light);color:var(--text)}
|
||
.page h2{font-size:20px;font-weight:700;letter-spacing:-.01em;margin-bottom:2px}
|
||
.page .sub{font-size:12px;color:var(--text3);margin-bottom:16px}
|
||
.page .sec{margin-top:16px}
|
||
.page .sec h3{font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text3);margin-bottom:5px}
|
||
.page .sec p,.page .sec .tx{font-size:13px;color:var(--text2);line-height:1.55}
|
||
.page .sec .tt{display:inline-block;font-size:10px;font-weight:500;padding:2px 7px;border-radius:3px;background:var(--bg3);color:var(--accent);margin:1.5px;font-family:var(--mono)}
|
||
.page .sec pre.pb{background:var(--bg3);padding:10px 12px;border-radius:6px;font-family:var(--font);font-size:12px;line-height:1.5;color:var(--text2);max-height:300px;overflow-y:auto;white-space:pre-wrap}
|
||
.page .copy-btn{display:inline-flex;align-items:center;gap:5px;padding:5px 12px;border-radius:5px;border:1px solid var(--accent);background:var(--accent-glow);color:var(--accent);cursor:pointer;font-size:11.5px;font-weight:500;transition:all .12s;font-family:var(--mono);margin-top:6px}
|
||
.page .copy-btn:hover{background:var(--accent);color:#fff}
|
||
.page .copy-btn.done{border-color:var(--green);background:var(--green-glow);color:var(--green)}
|
||
|
||
.svr-list{margin-top:8px}
|
||
.svr-it{padding:10px 0;border-bottom:1px solid var(--border)}
|
||
.svr-it:last-child{border-bottom:none}
|
||
.svr-it .svr-n{font-size:13px;font-weight:600;display:flex;align-items:center;gap:5px}
|
||
.svr-it .svr-cmd{font-size:10.5px;color:var(--text3);font-family:var(--mono);margin-top:2px;word-break:break-all}
|
||
.svr-it .svr-tags{margin-top:4px;display:flex;gap:3px;flex-wrap:wrap}
|
||
.svr-it .svr-tags .stg{font-size:9px;padding:1px 5px;border-radius:3px;background:var(--bg3);color:var(--text3)}
|
||
|
||
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%) translateY(70px);background:var(--bg2);border:1px solid var(--border);border-radius:7px;padding:8px 16px;font-size:12.5px;color:var(--text);box-shadow:var(--shadow-lg);z-index:300;opacity:0;transition:all .25s ease;pointer-events:none;display:flex;align-items:center;gap:7px}
|
||
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
||
.toast .ck{width:16px;height:16px;border-radius:50%;background:var(--green-glow);color:var(--green);display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0}
|
||
|
||
.empty{text-align:center;padding:50px 20px}
|
||
.empty .eic{font-size:32px;margin-bottom:10px;opacity:.25}
|
||
.empty h3{font-size:14px;color:var(--text2);margin-bottom:3px;font-weight:500}
|
||
.empty p{font-size:11px;color:var(--text3)}
|
||
|
||
.footer{text-align:center;padding:16px;color:var(--text3);font-size:10.5px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap}
|
||
.footer a{color:var(--accent)}
|
||
.footer .dt{width:2.5px;height:2.5px;border-radius:50%;background:var(--text3);flex-shrink:0}
|
||
|
||
@media(max-width:768px){
|
||
.header{padding:0 14px;gap:8px}
|
||
.brand .ver{display:none}
|
||
.search{width:160px}
|
||
.search .hint{display:none}
|
||
.nav{padding:0 14px}
|
||
.nav-it{padding:8px 10px;font-size:11.5px}
|
||
.out{padding:10px 14px}
|
||
.stats{grid-template-columns:repeat(3,1fr)}
|
||
.grid{grid-template-columns:1fr}
|
||
.cmd-grid{grid-template-columns:1fr}
|
||
.page{padding:14px 16px 40px}
|
||
}
|
||
@media(max-width:480px){
|
||
.stats{grid-template-columns:repeat(2,1fr)}
|
||
.search{width:120px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div class="brand" id="brand-link">
|
||
<div class="logo">E</div>
|
||
<h1><span id="t-title">ECC Capabilities</span> <span class="ver">v2.0.0-rc.1</span></h1>
|
||
</div>
|
||
<div class="header-center"></div>
|
||
<div class="header-right">
|
||
<div class="search">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
|
||
<input type="text" id="search" placeholder="" oninput="onSearchInput(this.value)" onclick="showSuggestions()" onkeydown="onSearchKey(event)" autocomplete="off" spellcheck="false">
|
||
<span class="hint">⌘K</span>
|
||
<div class="suggest" id="suggest"></div>
|
||
</div>
|
||
<div class="lang-wrap">
|
||
<button class="lang-btn" onclick="toggleLang()">🌐 <span id="lang-label">EN</span></button>
|
||
<div class="lang-drop" id="lang-drop"></div>
|
||
</div>
|
||
<button class="icon-btn" onclick="toggleTheme()" title="Toggle theme">☀</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav" id="nav">
|
||
<button class="nav-it active" data-tab="agents" onclick="showTab('agents',this)"><span id="nav-agents">🤖 Agents</span> <span class="ct" id="nav-ct-agents"></span></button>
|
||
<button class="nav-it" data-tab="skills" onclick="showTab('skills',this)"><span id="nav-skills">📚 Skills</span> <span class="ct" id="nav-ct-skills"></span></button>
|
||
<button class="nav-it" data-tab="commands" onclick="showTab('commands',this)"><span id="nav-commands">⚡ Commands</span> <span class="ct" id="nav-ct-commands"></span></button>
|
||
<button class="nav-it" data-tab="rules" onclick="showTab('rules',this)"><span id="nav-rules">📏 Rules</span> <span class="ct" id="nav-ct-rules"></span></button>
|
||
<button class="nav-it" data-tab="mcps" onclick="showTab('mcps',this)"><span id="nav-mcps">🔌 MCPs</span> <span class="ct" id="nav-ct-mcps"></span></button>
|
||
<button class="nav-it" data-tab="hooks" onclick="showTab('hooks',this)"><span id="nav-hooks">🪝 Hooks</span> <span class="ct" id="nav-ct-hooks"></span></button>
|
||
</div>
|
||
|
||
<div class="out" id="app"></div>
|
||
|
||
<div class="toast" id="toast"><span class="ck">✓</span> <span id="toast-msg"></span></div>
|
||
<div class="footer">
|
||
<a href="https://github.com/affaan-m/ECC" target="_blank">github.com/affaan-m/ECC</a>
|
||
<span class="dt"></span>
|
||
<span>ECC v2.0.0-rc.1</span>
|
||
<span class="dt"></span>
|
||
<span id="t-contribution">Contribution to ECC</span>
|
||
<span class="dt"></span>
|
||
<span>Dashboard :${PORT}</span>
|
||
</div>
|
||
|
||
<script>
|
||
const AGENTS = ${ag};
|
||
const SKILLS = ${sk};
|
||
const COMMANDS = ${co};
|
||
const RULES = ${ru};
|
||
const MCPS = ${mc};
|
||
const HOOKS = ${ho};
|
||
const L = ${ll};
|
||
const LANG_KEYS = ${lc};
|
||
|
||
let lang = localStorage.getItem('ecc-lang') || 'en';
|
||
let suggIdx = -1;
|
||
|
||
function t(key) { return (L[lang] && L[lang][key]) || L.en[key] || key; }
|
||
|
||
function toast(msg) {
|
||
const el = document.getElementById('toast');
|
||
document.getElementById('toast-msg').textContent = msg;
|
||
el.classList.add('show');
|
||
clearTimeout(el._t);
|
||
el._t = setTimeout(() => el.classList.remove('show'), 1600);
|
||
}
|
||
function copy(text, btn) {
|
||
if (navigator.clipboard) navigator.clipboard.writeText(text).then(() => {});
|
||
else { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }
|
||
toast(t('copied') + ' ' + text);
|
||
if (btn) { btn.classList.add('done'); setTimeout(() => btn.classList.remove('done'), 1000); }
|
||
}
|
||
|
||
// Recently viewed
|
||
function recents() { try { return JSON.parse(localStorage.getItem('ecc-recent') || '[]'); } catch { return []; } }
|
||
function addRecent(type, name) {
|
||
if (!name || !/^[\\w\\-./@]+$/.test(name)) return;
|
||
let r = recents().filter(x => !(x.t === type && x.n === name));
|
||
r.unshift({ t: type, n: name, at: Date.now() });
|
||
if (r.length > 8) r = r.slice(0, 8);
|
||
localStorage.setItem('ecc-recent', JSON.stringify(r));
|
||
}
|
||
function clearRecents() { localStorage.removeItem('ecc-recent'); location.hash=''; location.reload(); }
|
||
|
||
function aType(name) {
|
||
if (name.includes('reviewer')||name.includes('-review')) return 'reviewer';
|
||
if (name.includes('build')||name.includes('resolver')) return 'builder';
|
||
if (name.includes('architect')) return 'architect';
|
||
if (name.includes('security')) return 'security';
|
||
return 'other';
|
||
}
|
||
|
||
// Language
|
||
function setLang(l) {
|
||
lang = l; localStorage.setItem('ecc-lang', l);
|
||
document.querySelectorAll('.lang-drop .li').forEach(el => el.classList.toggle('active', el.dataset.lang === l));
|
||
document.getElementById('lang-label').textContent = (L[l]||L.en).name.split(' ')[0].slice(0,2).toUpperCase();
|
||
document.getElementById('lang-drop').classList.remove('show');
|
||
applyLang();
|
||
if (!location.hash || location.hash==='#/') renderMain();
|
||
else handleRoute();
|
||
}
|
||
function toggleLang() { document.getElementById('lang-drop').classList.toggle('show'); }
|
||
function applyLang() {
|
||
document.getElementById('t-title').textContent = t('title');
|
||
document.getElementById('t-contribution').textContent = t('contribution');
|
||
document.getElementById('search').placeholder = t('search');
|
||
// Update label text only — counter spans are separate siblings
|
||
document.getElementById('nav-agents').childNodes[0].textContent = '🤖 ' + t('agents');
|
||
document.getElementById('nav-skills').childNodes[0].textContent = '📚 ' + t('skills');
|
||
document.getElementById('nav-commands').childNodes[0].textContent = '⚡ ' + t('commands');
|
||
document.getElementById('nav-rules').childNodes[0].textContent = '📏 ' + t('rules');
|
||
document.getElementById('nav-mcps').childNodes[0].textContent = '🔌 ' + t('mcps');
|
||
document.getElementById('nav-hooks').childNodes[0].textContent = '🪝 ' + t('hooks');
|
||
// Update counter spans by their own IDs (avoids duplicate IDs in DOM)
|
||
document.getElementById('nav-ct-agents').textContent = AGENTS.length;
|
||
document.getElementById('nav-ct-skills').textContent = SKILLS.length;
|
||
document.getElementById('nav-ct-commands').textContent = COMMANDS.length;
|
||
document.getElementById('nav-ct-rules').textContent = RULES.length;
|
||
document.getElementById('nav-ct-mcps').textContent = MCPS.length;
|
||
document.getElementById('nav-ct-hooks').textContent = HOOKS.length;
|
||
}
|
||
// Build lang dropdown
|
||
(function(){
|
||
const dd = document.getElementById('lang-drop');
|
||
dd.innerHTML = LANG_KEYS.map(c => '<div class="li'+(c==='en'?' active':'')+'" data-lang="'+c+'" onclick="setLang(\\''+c+'\\')">'+L[c].name+'</div>').join('');
|
||
})();
|
||
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.lang-wrap')) document.getElementById('lang-drop').classList.remove('show');
|
||
if (!e.target.closest('.search')) document.getElementById('suggest').classList.remove('show');
|
||
});
|
||
|
||
// Theme
|
||
function toggleTheme() {
|
||
const h = document.documentElement;
|
||
h.dataset.theme = h.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
localStorage.setItem('ecc-theme', h.dataset.theme);
|
||
}
|
||
if (localStorage.getItem('ecc-theme')) document.documentElement.dataset.theme = localStorage.getItem('ecc-theme');
|
||
|
||
// Routing
|
||
function handleRoute() {
|
||
const hash = location.hash.slice(1);
|
||
if (!hash || hash === '/') { renderMain(); return; }
|
||
const parts = hash.split('/').filter(Boolean);
|
||
if (parts.length < 2) { renderMain(); return; }
|
||
renderPage(parts[0], decodeURIComponent(parts.slice(1).join('/')));
|
||
}
|
||
window.addEventListener('hashchange', handleRoute);
|
||
|
||
// Render Main Dashboard
|
||
function renderMain() {
|
||
const app = document.getElementById('app');
|
||
app.innerHTML = '<div class="stats" id="stats-bar"></div><div class="panel active" id="panel-agents"></div><div class="panel" id="panel-skills"></div><div class="panel" id="panel-commands"></div><div class="panel" id="panel-rules"></div><div class="panel" id="panel-mcps"></div><div class="panel" id="panel-hooks"></div>';
|
||
|
||
document.getElementById('stats-bar').innerHTML =
|
||
'<div class="stat c0" onclick="showTab(\\'agents\\',document.querySelector(\\'.nav-it[data-tab=\\\\"agents\\\"]\\'))"><div class="num">'+AGENTS.length+'</div><div class="lbl">'+t('agents')+'</div></div>' +
|
||
'<div class="stat c1" onclick="showTab(\\'skills\\',document.querySelector(\\'.nav-it[data-tab=\\\\"skills\\\"]\\'))"><div class="num">'+SKILLS.length+'</div><div class="lbl">'+t('skills')+'</div></div>' +
|
||
'<div class="stat c2" onclick="showTab(\\'commands\\',document.querySelector(\\'.nav-it[data-tab=\\\\"commands\\\"]\\'))"><div class="num">'+COMMANDS.length+'</div><div class="lbl">'+t('commands')+'</div></div>' +
|
||
'<div class="stat c3" onclick="showTab(\\'rules\\',document.querySelector(\\'.nav-it[data-tab=\\\\"rules\\\"]\\'))"><div class="num">'+RULES.length+'</div><div class="lbl">'+t('ruleSets')+'</div></div>' +
|
||
'<div class="stat c4" onclick="showTab(\\'mcps\\',document.querySelector(\\'.nav-it[data-tab=\\\\"mcps\\\"]\\'))"><div class="num">'+MCPS.length+'</div><div class="lbl">'+t('mcpConfigs')+'</div></div>' +
|
||
'<div class="stat c5" onclick="showTab(\\'hooks\\',document.querySelector(\\'.nav-it[data-tab=\\\\"hooks\\\"]\\'))"><div class="num">'+HOOKS.length+'</div><div class="lbl">'+t('hooks')+'</div></div>';
|
||
|
||
const recent = recents().filter(r => r.n && /^[\\w\\-./@]+$/.test(r.n));
|
||
if (recent.length) {
|
||
const icons = {agents:'🤖',skills:'📚',commands:'⚡',rules:'📏',mcps:'🔌',hooks:'🪝'};
|
||
const rb = document.createElement('div');
|
||
rb.className = 'recent-bar';
|
||
rb.innerHTML = '<span class="rb-lbl">'+t('recentlyViewed')+'</span><span class="rb-items"></span><span class="rb-clear" onclick="clearRecents()">✕ '+t('clearHistory')+'</span>';
|
||
const items = rb.querySelector('.rb-items');
|
||
recent.forEach(r => {
|
||
const el = document.createElement('span');
|
||
el.className = 'rb-item';
|
||
el.textContent = (icons[r.t]||'•')+' '+r.n;
|
||
el.onclick = () => { location.hash = '#/'+r.t+'/'+encodeURIComponent(r.n); };
|
||
items.appendChild(el);
|
||
});
|
||
document.getElementById('stats-bar').after(rb);
|
||
}
|
||
|
||
document.querySelectorAll('.nav-it').forEach(n => n.classList.toggle('active', n.dataset.tab === 'agents'));
|
||
renderAgents(AGENTS); renderSkills(SKILLS); renderCommands(COMMANDS);
|
||
renderRules(RULES); renderMcps(MCPS); renderHooks(HOOKS);
|
||
}
|
||
|
||
function showTab(name, btn) {
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
document.querySelectorAll('.nav-it').forEach(n => n.classList.remove('active'));
|
||
const p = document.getElementById('panel-'+name);
|
||
if (p) p.classList.add('active');
|
||
if (btn) btn.classList.add('active');
|
||
location.hash = '';
|
||
}
|
||
|
||
// Render functions
|
||
const ICONS = ['⊙','⊡','⊞','⊕','⊠','⊟'];
|
||
function iBg(i) { return 'i' + (i % 6); }
|
||
|
||
function renderAgents(list) {
|
||
const el = document.getElementById('panel-agents');
|
||
if (!el) return;
|
||
const cats = ['all','reviewer','builder','architect','security'];
|
||
const lbls = [t('all'),'👁️ '+t('reviewers'),'🔧 '+t('buildResolvers'),'🏗️ '+t('architects'),'🔒 '+t('security')];
|
||
el.innerHTML = '<div class="filters" id="af">'+cats.map((c,i)=>'<button'+(i===0?' class="active"':'')+' onclick="filterAgents(\\''+c+'\\',this)">'+lbls[i]+'</button>').join('')+
|
||
'</div><div class="grid" id="ag">'+
|
||
list.map((a,i)=>{const m=(a.m||'').toLowerCase(),bd=m.includes('opus')?'opus':m.includes('sonnet')?'sonnet':m.includes('haiku')?'haiku':'';const tag=aType(a.n),ic=tag==='reviewer'?0:tag==='builder'?1:tag==='architect'?2:tag==='security'?3:4;
|
||
return '<div class="card" data-tag="'+tag+'" data-model="'+a.m+'" onclick="location.hash=\\'#/agents/'+encodeURIComponent(a.n)+'\\'">'+
|
||
'<div class="top"><div class="il"><div class="ic '+iBg(ic)+'">'+ICONS[ic]+'</div><span class="nm">'+esc(a.n)+'</span></div>'+(bd?'<span class="bd '+bd+'">'+a.m+'</span>':'')+'</div>'+
|
||
'<div class="desc">'+esc(a.d.slice(0,150))+'</div>'+
|
||
'<div class="tags">'+a.t.slice(0,5).map(t=>'<span class="t">'+esc(t)+'</span>').join('')+'</div><span class="ar">↗</span></div>';}).join('')+'</div>';
|
||
}
|
||
function renderSkills(list) {
|
||
const el = document.getElementById('panel-skills'); if (!el) return;
|
||
el.innerHTML = '<div class="filters" id="sf">'+['all','sec','test','pattern','design','research','data','agent','devops'].map((c,i)=>'<button'+(i===0?' class="active"':'')+' onclick="filterSkills(\\''+c+'\\',this)">'+[t('all'),'🔒 '+t('security'),'🧪 '+t('testing'),'📐 '+t('patterns'),'🎨 '+t('design'),'🔬 '+t('research'),'🗄️ '+t('data'),'🤖 '+t('agent'),'⚙️ '+t('devops')][i]+'</button>').join('')+
|
||
'</div><div class="grid" id="sg">'+list.map((s,i)=>'<div class="card" onclick="location.hash=\\'#/skills/'+encodeURIComponent(s.n)+'\\'">'+
|
||
'<div class="top"><div class="il"><div class="ic '+iBg(i%6)+'">'+ICONS[i%6]+'</div><span class="nm">'+esc(s.n)+'</span></div></div>'+
|
||
'<div class="desc">'+esc(s.d||'—')+'</div><span class="ar">↗</span></div>').join('')+'</div>';
|
||
}
|
||
function renderCommands(list) {
|
||
const el = document.getElementById('panel-commands'); if (!el) return;
|
||
const cats = [...new Set(list.map(c=>c.c))];
|
||
const filters = document.createElement('div');
|
||
filters.className = 'filters'; filters.id = 'cf';
|
||
const allBtn = document.createElement('button');
|
||
allBtn.className = 'active'; allBtn.textContent = t('all');
|
||
allBtn.onclick = () => filterCommands('all', allBtn);
|
||
filters.appendChild(allBtn);
|
||
cats.forEach(cat => {
|
||
const btn = document.createElement('button');
|
||
btn.textContent = cat;
|
||
btn.onclick = () => filterCommands(cat, btn);
|
||
filters.appendChild(btn);
|
||
});
|
||
el.innerHTML = '';
|
||
el.appendChild(filters);
|
||
const grid = document.createElement('div');
|
||
grid.className = 'cmd-grid'; grid.id = 'cg';
|
||
list.forEach(c => {
|
||
const div = document.createElement('div');
|
||
div.className = 'cmd-it';
|
||
div.onclick = () => { location.hash = '#/commands/'+encodeURIComponent(c.n.replace('/','')); };
|
||
div.innerHTML = '<div class="cl"><div class="cn">'+esc(c.n)+'</div><div class="cd">'+esc(c.d||'—')+'</div><div class="cc">'+esc(c.c)+'</div></div>'+
|
||
'<button class="cpy" title="Copy">⊡</button>';
|
||
div.querySelector('.cpy').onclick = (e) => { e.stopPropagation(); copy(c.n, e.target); };
|
||
grid.appendChild(div);
|
||
});
|
||
el.appendChild(grid);
|
||
}
|
||
function renderRules(list) {
|
||
const el = document.getElementById('panel-rules'); if (!el) return;
|
||
const grid = document.createElement('div');
|
||
grid.className = 'rules-grid';
|
||
list.forEach(r => {
|
||
const div = document.createElement('div');
|
||
div.className = 'rule-cd';
|
||
div.onclick = () => { location.hash = '#/rules/'+encodeURIComponent(r.l); };
|
||
let html = '<h3>'+esc(r.l)+'</h3>';
|
||
r.f.slice(0,8).forEach(f => { html += '<div class="rf">'+esc(f)+'</div>'; });
|
||
if (r.f.length > 8) html += '<div class="rf" style="color:var(--text3);font-size:9.5px;margin-top:3px">+ '+(r.f.length-8)+' '+t('more')+'</div>';
|
||
div.innerHTML = html;
|
||
grid.appendChild(div);
|
||
});
|
||
el.innerHTML = '';
|
||
el.appendChild(grid);
|
||
}
|
||
function renderMcps(list) {
|
||
const el = document.getElementById('panel-mcps'); if (!el) return;
|
||
if (!list.length) { el.innerHTML = '<div class="empty"><div class="eic">🔌</div><h3>'+esc(t('noMcps'))+'</h3><p>'+esc(t('checkMcps'))+'</p></div>'; return; }
|
||
const grid = document.createElement('div'); grid.className = 'mcp-grid';
|
||
list.forEach(m => {
|
||
const div = document.createElement('div'); div.className = 'mcp-cd';
|
||
div.onclick = () => { location.hash = '#/mcps/'+encodeURIComponent(m.f); };
|
||
div.innerHTML = '<h3>📄 '+esc(m.f)+'</h3>'+m.s.map(s => '<span class="st">'+esc(s.n)+' <small>'+esc((s.cmd||'').slice(0,40))+'</small></span>').join('');
|
||
grid.appendChild(div);
|
||
});
|
||
el.innerHTML = ''; el.appendChild(grid);
|
||
}
|
||
function renderHooks(list) {
|
||
const el = document.getElementById('panel-hooks'); if (!el) return;
|
||
if (!list.length) { el.innerHTML = '<div class="empty"><div class="eic">🪝</div><h3>'+esc(t('noHooks'))+'</h3></div>'; return; }
|
||
const wrap = document.createElement('div'); wrap.className = 'hw';
|
||
const tbl = document.createElement('table'); tbl.className = 'ht';
|
||
tbl.innerHTML = '<thead><tr><th>'+esc(t('event'))+'</th><th>'+esc(t('matcher'))+'</th><th>'+esc(t('description'))+'</th><th>'+esc(t('id'))+'</th></tr></thead><tbody></tbody>';
|
||
const tbody = tbl.querySelector('tbody');
|
||
list.forEach(h => {
|
||
const tr = document.createElement('tr');
|
||
tr.onclick = () => { location.hash = '#/hooks/'+encodeURIComponent(h.id); };
|
||
tr.innerHTML = '<td class="ev">'+esc(h.ev)+'</td><td><span class="mt">'+esc(h.m)+'</span></td><td>'+esc(h.d)+'</td><td style="color:var(--text3);font-size:9.5px">'+esc(h.id)+'</td>';
|
||
tbody.appendChild(tr);
|
||
});
|
||
wrap.appendChild(tbl);
|
||
el.innerHTML = '';
|
||
el.appendChild(wrap);
|
||
}
|
||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
// Detail Pages
|
||
function renderPage(type, name) {
|
||
addRecent(type, name);
|
||
const app = document.getElementById('app');
|
||
let html = '';
|
||
if (type === 'agents') {
|
||
const a = AGENTS.find(x=>x.n===name); if (!a) { app.innerHTML='<div class="empty"><h3>Agent not found</h3></div>'; return; }
|
||
const m=(a.m||'').toLowerCase(),bd=m.includes('opus')?'opus':m.includes('sonnet')?'sonnet':m.includes('haiku')?'haiku':'';
|
||
html='<div class="page"><button class="back" onclick="location.hash=\\'\\'">← '+t('agents')+'</button><h2>'+esc(a.n)+'</h2><div class="sub">'+(bd?'<span class="bd '+bd+'" style="display:inline-block;margin-right:6px">'+a.m+'</span>':'')+a.t.length+' tools</div>'+
|
||
'<div class="sec"><h3>'+t('description')+'</h3><p>'+esc(a.d)+'</p></div>'+(a.t.length?'<div class="sec"><h3>'+t('tools')+'</h3>'+a.t.map(t=>'<span class="tt">'+esc(t)+'</span>').join('')+'</div>':'')+
|
||
(a.b?'<div class="sec"><h3>'+t('details')+'</h3><pre class="pb">'+esc(a.b)+'</pre></div>':'')+'</div>';
|
||
} else if (type === 'skills') {
|
||
const s=SKILLS.find(x=>x.n===name); if(!s){app.innerHTML='<div class="empty"><h3>Skill not found</h3></div>';return;}
|
||
html='<div class="page"><button class="back" onclick="location.hash=\\'\\'">← '+t('skills')+'</button><h2>'+esc(s.n)+'</h2><div class="sub">'+t('skill')+'</div>'+
|
||
'<div class="sec"><h3>'+t('description')+'</h3><p>'+esc(s.d||'—')+'</p></div>'+(s.b?'<div class="sec"><h3>'+t('details')+'</h3><pre class="pb">'+esc(s.b)+'</pre></div>':'')+'</div>';
|
||
} else if (type === 'commands') {
|
||
const c=COMMANDS.find(x=>x.n==='/'+name); if(!c){app.innerHTML='<div class="empty"><h3>Command not found</h3></div>';return;}
|
||
html='<div class="page"><button class="back" onclick="location.hash=\\'\\'">← '+t('commands')+'</button><h2>'+esc(c.n)+'</h2><div class="sub">'+esc(c.c)+'</div>'+
|
||
'<div class="sec"><h3>'+t('description')+'</h3><p>'+esc(c.d||'—')+'</p></div>'+
|
||
'<div class="sec"><button class="copy-btn" data-cmd="'+esc(c.n)+'">⊡ Copy '+esc(c.n)+'</button></div>'+(c.b?'<div class="sec"><h3>'+t('details')+'</h3><pre class="pb">'+esc(c.b)+'</pre></div>':'')+'</div>';
|
||
} else if (type === 'rules') {
|
||
const r=RULES.find(x=>x.l===name); if(!r){app.innerHTML='<div class="empty"><h3>Rules not found</h3></div>';return;}
|
||
html='<div class="page"><button class="back" onclick="location.hash=\\'\\'">← '+t('rules')+'</button><h2>'+esc(r.l)+'</h2><div class="sub">'+r.f.length+' '+t('ruleFiles')+'</div>'+
|
||
'<div class="sec">'+r.f.map(f=>'<div style="padding:3px 0;font-size:13px;color:var(--text2);display:flex;align-items:center;gap:6px"><span style="color:var(--text3)">—</span>'+esc(f)+'</div>').join('')+'</div></div>';
|
||
} else if (type === 'mcps') {
|
||
const m=MCPS.find(x=>x.f===name); if(!m){app.innerHTML='<div class="empty"><h3>MCP config not found</h3></div>';return;}
|
||
html='<div class="page"><button class="back" onclick="location.hash=\\'\\'">← '+t('mcps')+'</button><h2>'+esc(m.f)+'</h2><div class="sub">'+m.s.length+' '+t('servers')+'</div>'+
|
||
'<div class="svr-list">'+m.s.map(s=>'<div class="svr-it"><div class="svr-n">'+esc(s.n)+'</div><div class="svr-cmd">'+esc(s.cmd||'')+(s.args&&s.args.length?' '+s.args.join(' '):'')+'</div>'+
|
||
'<div class="svr-tags">'+(s.type?'<span class="stg">'+esc(s.type)+'</span>':'')+(s.env&&Object.keys(s.env).length?Object.entries(s.env).map(([k,v])=>'<span class="stg">'+esc(k)+'='+esc(v)+'</span>').join(''):'')+'</div></div>').join('')+'</div></div>';
|
||
} else if (type === 'hooks') {
|
||
const h=HOOKS.find(x=>x.id===name); if(!h){app.innerHTML='<div class="empty"><h3>Hook not found</h3></div>';return;}
|
||
html='<div class="page"><button class="back" onclick="location.hash=\\'\\'">← '+t('hooks')+'</button><h2 style="font-family:var(--mono);font-size:15px">'+esc(h.id)+'</h2><div class="sub">'+esc(h.ev)+' · <span class="mt" style="font-size:11px;background:var(--bg3);padding:1px 5px;border-radius:3px">'+esc(h.m)+'</span></div>'+
|
||
'<div class="sec"><p>'+esc(h.d)+'</p></div></div>';
|
||
}
|
||
app.innerHTML = html;
|
||
// Attach copy handlers for detail page copy buttons
|
||
app.querySelectorAll('.copy-btn[data-cmd]').forEach(btn => {
|
||
const cmd = btn.getAttribute('data-cmd');
|
||
btn.onclick = () => copy(cmd, btn);
|
||
});
|
||
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
|
||
document.querySelectorAll('.nav-it').forEach(n=>n.classList.remove('active'));
|
||
}
|
||
|
||
// Filters
|
||
function filterAgents(tag, btn) {
|
||
document.querySelectorAll('#af .active').forEach(b=>b.classList.remove('active')); btn.classList.add('active');
|
||
document.querySelectorAll('#ag .card').forEach(c=>{if(tag==='all'){c.style.display='';return}
|
||
if(['opus','sonnet','haiku'].includes(tag)){c.style.display=c.dataset.model.toLowerCase().includes(tag)?'':'none';return}
|
||
c.style.display=c.dataset.tag===tag?'':'none';});
|
||
}
|
||
function filterSkills(tag, btn) {
|
||
document.querySelectorAll('#sf .active').forEach(b=>b.classList.remove('active')); btn.classList.add('active');
|
||
document.querySelectorAll('#sg .card').forEach(c=>{if(tag==='all'){c.style.display='';return}
|
||
const nm=c.querySelector('.nm').textContent.toLowerCase(),dc=(c.querySelector('.desc')?.textContent||'').toLowerCase();
|
||
c.style.display=(nm.includes(tag)||dc.includes(tag))?'':'none';});
|
||
}
|
||
function filterCommands(cat, btn) {
|
||
document.querySelectorAll('#cf .active').forEach(b=>b.classList.remove('active')); btn.classList.add('active');
|
||
document.querySelectorAll('#cg .cmd-it').forEach(c=>{c.style.display=(cat==='all'||c.querySelector('.cc').textContent===cat)?'':'none';});
|
||
}
|
||
|
||
// Search
|
||
function onSearchInput(q) {
|
||
q = q.toLowerCase().trim();
|
||
const fa=q?AGENTS.filter(a=>a.n.toLowerCase().includes(q)||a.d.toLowerCase().includes(q)||(a.t||[]).some(t=>t.toLowerCase().includes(q))):AGENTS;
|
||
const fs=q?SKILLS.filter(s=>s.n.toLowerCase().includes(q)||s.d.toLowerCase().includes(q)):SKILLS;
|
||
const fc=q?COMMANDS.filter(c=>c.n.toLowerCase().includes(q)||c.d.toLowerCase().includes(q)||c.c.toLowerCase().includes(q)):COMMANDS;
|
||
renderAgents(fa); renderSkills(fs); renderCommands(fc);
|
||
document.querySelectorAll('#af .active, #sf .active, #cf .active').forEach(b=>b.classList.remove('active'));
|
||
['#af button','#sf button','#cf button'].forEach(s=>{const b=document.querySelector(s);if(b)b.classList.add('active')});
|
||
showSuggestions();
|
||
}
|
||
function showSuggestions() {
|
||
const q = document.getElementById('search').value.toLowerCase().trim();
|
||
const sug = document.getElementById('suggest');
|
||
if (!q) { sug.classList.remove('show'); return; }
|
||
const results = [];
|
||
AGENTS.filter(a=>a.n.toLowerCase().includes(q)||a.d.toLowerCase().includes(q)).slice(0,4).forEach(a=>results.push({t:'agents',n:a.n,d:a.d.slice(0,60),ic:'a',e:'⊙'}));
|
||
SKILLS.filter(s=>s.n.toLowerCase().includes(q)||s.d.toLowerCase().includes(q)).slice(0,4).forEach(s=>results.push({t:'skills',n:s.n,d:s.d.slice(0,60),ic:'s',e:'⊞'}));
|
||
COMMANDS.filter(c=>c.n.toLowerCase().includes(q)||c.d.toLowerCase().includes(q)).slice(0,4).forEach(c=>results.push({t:'commands',n:c.n,d:c.d.slice(0,60),ic:'c',e:'⊡'}));
|
||
if (!results.length) { sug.classList.remove('show'); return; }
|
||
const groups = {};
|
||
results.forEach(r=>{if(!groups[r.t])groups[r.t]=[];groups[r.t].push(r);});
|
||
sug.innerHTML = Object.entries(groups).map(([type,items]) =>
|
||
'<div class="sg"><div class="sg-label">'+(type==='agents'?'🤖 '+t('agents'):type==='skills'?'📚 '+t('skills'):'⚡ '+t('commands'))+'</div>'+
|
||
items.map(r=>'<div class="si" onclick="location.hash=\\'#/'+r.t+'/'+encodeURIComponent(r.n)+'\\';document.getElementById(\\'suggest\\').classList.remove(\\'show\\');document.getElementById(\\'search\\').blur()">'+
|
||
'<span class="ic '+r.ic+'">'+r.e+'</span><span class="sn">'+esc(r.n)+'</span><span class="sd">'+esc(r.d)+'</span></div>').join('')+'</div>'
|
||
).join('');
|
||
sug.classList.add('show'); suggIdx = -1;
|
||
}
|
||
function onSearchKey(e) {
|
||
const items = document.querySelectorAll('#suggest .si');
|
||
if (e.key==='ArrowDown'){e.preventDefault();suggIdx=Math.min(suggIdx+1,items.length-1);items.forEach((el,i)=>el.classList.toggle('active',i===suggIdx));}
|
||
else if(e.key==='ArrowUp'){e.preventDefault();suggIdx=Math.max(suggIdx-1,-1);items.forEach((el,i)=>el.classList.toggle('active',i===suggIdx));}
|
||
else if(e.key==='Enter'&&suggIdx>=0&&items[suggIdx])items[suggIdx].click();
|
||
else if(e.key==='Escape'){document.getElementById('suggest').classList.remove('show');document.getElementById('search').blur();}
|
||
}
|
||
|
||
// Keyboard
|
||
document.addEventListener('keydown', e => { if((e.metaKey||e.ctrlKey)&&e.key==='k'){e.preventDefault();document.getElementById('search').focus();} });
|
||
|
||
// Init
|
||
setLang(lang);
|
||
handleRoute();
|
||
</script>
|
||
</body></html>`;
|
||
/* eslint-enable no-useless-escape */
|
||
}
|
||
|
||
const server = http.createServer((req, res) => {
|
||
const url = new URL(req.url, 'http://localhost');
|
||
if (url.pathname === '/api/data') {
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
return res.end(JSON.stringify({ agents: loadAgents(), skills: loadSkills(), commands: loadCommands(), rules: loadRules(), mcps: loadMcps(), hooks: loadHooks() }));
|
||
}
|
||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||
res.end(renderHTML({ agents: loadAgents(), skills: loadSkills(), commands: loadCommands(), rules: loadRules(), mcps: loadMcps(), hooks: loadHooks() }));
|
||
});
|
||
|
||
if (require.main === module) {
|
||
server.listen(PORT, () => {
|
||
console.log(`\n 🧩 ECC Capabilities → http://localhost:${PORT}\n`);
|
||
try { const { spawn } = require('child_process'); const p = process.platform; const c = p === 'darwin' ? 'open' : p === 'win32' ? 'start' : 'xdg-open'; if (c === 'start') spawn('cmd', ['/c', 'start', `http://localhost:${PORT}`], { stdio: 'ignore' }); else spawn(c, [`http://localhost:${PORT}`], { stdio: 'ignore' }); } catch { /* best-effort auto-open */ }
|
||
});
|
||
}
|
||
|
||
module.exports = { parsePort, readFrontmatter, readSkill, loadAgents, loadSkills, loadCommands, loadRules, loadMcps, loadHooks, renderHTML, LANG, LANG_KEYS, server };
|