Files
everything-claude-code/scripts/dashboard-web.js
T
Md Ayan d24c7185fc feat: add web capabilities dashboard (#2100)
* 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
2026-06-15 14:01:16 -04:00

776 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// 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 };