2026-06-15 23:31:16 +05:30
#!/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' );
2026-07-27 11:11:29 -07:00
const {
LOOPBACK_HOSTNAMES ,
buildAllowedHostnames ,
isAllowedHostHeader ,
isAllowedOrigin ,
} = require ( './lib/loopback-guard' );
2026-07-26 03:20:15 -07:00
const { normalizeAgentTools } = require ( './lib/agent-tools' );
2026-06-15 23:31:16 +05:30
2026-07-27 11:11:29 -07:00
const DEFAULT_HOST = '127.0.0.1' ;
function resolveDashboardHost ( env = process . env ) {
const configured = String ( env . ECC_DASHBOARD_HOST || '' ). trim (). toLowerCase ();
if ( ! configured ) return DEFAULT_HOST ;
if ( ! LOOPBACK_HOSTNAMES . has ( configured )) {
throw new Error (
'[ECC] ECC_DASHBOARD_HOST must be loopback-only ' +
'(127.0.0.1, localhost, or ::1).'
);
}
return configured === '[::1]' ? '::1' : configured ;
}
2026-06-15 23:31:16 +05:30
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' );
2026-07-27 11:11:29 -07:00
const HOST = resolveDashboardHost ();
2026-06-15 23:31:16 +05:30
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 );
2026-07-26 03:20:15 -07:00
if ( k === 'tools' ) {
v = normalizeAgentTools ( v );
} else if ( v . startsWith ( '[' ) && v . endsWith ( ']' )) {
try { v = JSON . parse ( v ); } catch { v = v . slice ( 1 , - 1 ). split ( ',' ). map ( x => x . trim (). replace ( /["']/g , '' )); }
}
2026-06-15 23:31:16 +05:30
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 ;
}
2026-07-20 04:47:10 +09:00
function loadPostToolUseChildren ( root ) {
if ( path . resolve ( root ) !== ROOT ) return [];
try {
const dispatcher = require ( path . join ( root , 'scripts' , 'hooks' , 'posttooluse-dispatcher.js' ));
return [
... dispatcher . SYNC_HOOKS . map ( hook => ({ ... hook , mode : 'sync' })),
... dispatcher . ASYNC_HOOKS . map ( hook => ({ ... hook , mode : 'async' })),
]. map ( hook => ({
ev : 'PostToolUse' ,
m : hook . matcher ,
id : hook . id ,
d : `Managed by the consolidated PostToolUse ${ hook . mode } dispatcher` ,
}));
} catch ( error ) {
console . error ( '[ECC] Failed to load PostToolUse dispatcher registry:' , error . message );
return [];
}
}
2026-06-15 23:31:16 +05:30
function loadHooks ( _root ) {
const root = _root || ROOT ;
2026-07-20 04:47:10 +09:00
const hooksPath = path . join ( root , 'hooks' , 'hooks.json' );
if ( ! fs . existsSync ( hooksPath )) return [];
try {
const data = JSON . parse ( fs . readFileSync ( hooksPath , 'utf8' ));
const hooks = [];
for ( const [ eventName , entries ] of Object . entries ( data . hooks || {})) {
for ( const entry of entries || []) {
hooks . push ({ ev : eventName , m : entry . matcher || '*' , id : entry . id || '' , d : entry . description || '' });
}
}
return [... hooks , ... loadPostToolUseChildren ( root )];
} catch ( error ) {
console . error ( '[ECC] Failed to parse hooks/hooks.json:' , error . message );
return [];
}
2026-06-15 23:31:16 +05:30
}
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">
2026-06-15 14:21:28 -04:00
<button class="lang-btn" onclick="toggleLang()"> <span id="lang-label">EN</span></button>
2026-06-15 23:31:16 +05:30
<div class="lang-drop" id="lang-drop"></div>
</div>
2026-06-15 14:21:28 -04:00
<button class="icon-btn" onclick="toggleTheme()" title="Toggle theme"></button>
2026-06-15 23:31:16 +05:30
</div>
</div>
<div class="nav" id="nav">
2026-06-15 14:21:28 -04:00
<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>
2026-06-15 23:31:16 +05:30
</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
2026-06-15 14:21:28 -04:00
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');
2026-06-15 23:31:16 +05:30
// 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) {
2026-06-15 14:21:28 -04:00
const icons = {agents:'',skills:'',commands:'',rules:'',mcps:'',hooks:''};
2026-06-15 23:31:16 +05:30
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'];
2026-06-15 14:21:28 -04:00
const lbls = [t('all'),' '+t('reviewers'),' '+t('buildResolvers'),' '+t('architects'),' '+t('security')];
2026-06-15 23:31:16 +05:30
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>'+
2026-06-15 14:21:28 -04:00
'<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>';
2026-06-15 23:31:16 +05:30
}
function renderSkills(list) {
const el = document.getElementById('panel-skills'); if (!el) return;
2026-06-15 14:21:28 -04:00
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('')+
2026-06-15 23:31:16 +05:30
'</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>'+
2026-06-15 14:21:28 -04:00
'<div class="desc">'+esc(s.d||'—')+'</div><span class="ar"></span></div>').join('')+'</div>';
2026-06-15 23:31:16 +05:30
}
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;
2026-06-15 14:21:28 -04:00
if (!list.length) { el.innerHTML = '<div class="empty"><div class="eic"></div><h3>'+esc(t('noMcps'))+'</h3><p>'+esc(t('checkMcps'))+'</p></div>'; return; }
2026-06-15 23:31:16 +05:30
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); };
2026-06-15 14:21:28 -04:00
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('');
2026-06-15 23:31:16 +05:30
grid.appendChild(div);
});
el.innerHTML = ''; el.appendChild(grid);
}
function renderHooks(list) {
const el = document.getElementById('panel-hooks'); if (!el) return;
2026-06-15 14:21:28 -04:00
if (!list.length) { el.innerHTML = '<div class="empty"><div class="eic"></div><h3>'+esc(t('noHooks'))+'</h3></div>'; return; }
2026-06-15 23:31:16 +05:30
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]) =>
2026-06-15 14:21:28 -04:00
'<div class="sg"><div class="sg-label">'+(type==='agents'?' '+t('agents'):type==='skills'?' '+t('skills'):' '+t('commands'))+'</div>'+
2026-06-15 23:31:16 +05:30
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 */
}
2026-07-27 11:11:29 -07:00
function sendJson ( res , statusCode , payload ) {
res . writeHead ( statusCode , {
'Content-Type' : 'application/json' ,
'Cache-Control' : 'no-store' ,
});
res . end ( JSON . stringify ( payload ));
}
2026-06-15 23:31:16 +05:30
2026-07-27 11:11:29 -07:00
function sendHtml ( res , statusCode , html ) {
res . writeHead ( statusCode , {
'Content-Type' : 'text/html; charset=utf-8' ,
'Cache-Control' : 'no-store' ,
});
res . end ( html );
}
function loadDashboardData ( root ) {
return {
agents : loadAgents ( root ),
skills : loadSkills ( root ),
commands : loadCommands ( root ),
rules : loadRules ( root ),
mcps : loadMcps ( root ),
hooks : loadHooks ( root ),
};
}
function defaultReportError ( message , error ) {
console . error ( message , error );
}
function reportDashboardFailure ( reportError , message , error ) {
try {
reportError ( message , error );
} catch {
// Error reporting must never prevent the generic HTTP response.
}
}
function createDashboardServer ({
root = ROOT ,
host = HOST ,
loadData = loadDashboardData ,
render = renderHTML ,
reportError = defaultReportError ,
} = {}) {
const resolvedHost = resolveDashboardHost ({ ECC_DASHBOARD_HOST : host });
const allowedHostnames = buildAllowedHostnames ( resolvedHost );
return http . createServer (( req , res ) => {
if ( ! isAllowedHostHeader ( req . headers . host , allowedHostnames )) {
return sendJson ( res , 421 , { error : 'Misdirected request' });
}
if ( ! isAllowedOrigin ( req . headers . origin , allowedHostnames )) {
return sendJson ( res , 403 , { error : 'Forbidden origin' });
}
let url ;
try {
url = new URL ( req . url , `http:// ${ DEFAULT_HOST } ` );
} catch {
return sendJson ( res , 400 , { error : 'Bad request' });
}
if ( url . pathname === '/api/data' ) {
let data ;
try {
data = loadData ( root );
} catch ( error ) {
reportDashboardFailure (
reportError ,
'[ECC] Failed to load dashboard data:' ,
error
);
return sendJson ( res , 500 , { error : 'Internal server error' });
}
return sendJson ( res , 200 , data );
}
let html ;
try {
html = render ( loadData ( root ));
} catch ( error ) {
reportDashboardFailure (
reportError ,
'[ECC] Failed to render dashboard:' ,
error
);
return sendHtml (
res ,
500 ,
'<!DOCTYPE html><p>Dashboard unavailable.</p>'
);
}
return sendHtml ( res , 200 , html );
2026-06-15 23:31:16 +05:30
});
}
2026-07-27 11:11:29 -07:00
function listenDashboardServer (
dashboardServer ,
{ port = PORT , host = HOST , onListening } = {}
) {
const resolvedHost = resolveDashboardHost ({ ECC_DASHBOARD_HOST : host });
return dashboardServer . listen ( port , resolvedHost , onListening );
}
const server = createDashboardServer ();
if ( require . main === module ) {
listenDashboardServer ( server , { port : PORT , host : HOST , onListening : () => {
const displayHost = HOST . includes ( ':' ) ? `[ ${ HOST } ]` : HOST ;
const dashboardUrl = `http:// ${ displayHost } : ${ PORT } ` ;
console . log ( `\n ECC Capabilities → ${ dashboardUrl } \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' , dashboardUrl ], { stdio : 'ignore' }); else spawn ( c , [ dashboardUrl ], { stdio : 'ignore' }); } catch { /* best-effort auto-open */ }
} });
}
module . exports = {
DEFAULT_HOST ,
HOST ,
LANG ,
LANG_KEYS ,
createDashboardServer ,
listenDashboardServer ,
loadAgents ,
loadCommands ,
loadHooks ,
loadMcps ,
loadRules ,
loadSkills ,
parsePort ,
readFrontmatter ,
readSkill ,
renderHTML ,
resolveDashboardHost ,
server ,
};