diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55abe8da..5a8d41c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,18 @@ jobs: # Package manager setup - name: Setup pnpm - if: matrix.pm == 'pnpm' - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 + if: matrix.pm == 'pnpm' && matrix.node != '18.x' + uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 with: - version: latest + # Keep an explicit pnpm major because this repo's packageManager is Yarn. + version: 10 + + - name: Setup pnpm (via Corepack) + if: matrix.pm == 'pnpm' && matrix.node == '18.x' + shell: bash + run: | + corepack enable + corepack prepare pnpm@9 --activate - name: Setup Yarn (via Corepack) if: matrix.pm == 'yarn' @@ -79,6 +87,8 @@ jobs: if: matrix.pm == 'pnpm' id: pnpm-cache-dir shell: bash + env: + COREPACK_ENABLE_STRICT: '0' run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Cache pnpm diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index e30ae1bc..3d650434 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -35,10 +35,18 @@ jobs: node-version: ${{ inputs.node-version }} - name: Setup pnpm - if: inputs.package-manager == 'pnpm' - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 + if: inputs.package-manager == 'pnpm' && inputs.node-version != '18.x' + uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0 with: - version: latest + # Keep an explicit pnpm major because this repo's packageManager is Yarn. + version: 10 + + - name: Setup pnpm (via Corepack) + if: inputs.package-manager == 'pnpm' && inputs.node-version == '18.x' + shell: bash + run: | + corepack enable + corepack prepare pnpm@9 --activate - name: Setup Yarn (via Corepack) if: inputs.package-manager == 'yarn' @@ -70,6 +78,8 @@ jobs: if: inputs.package-manager == 'pnpm' id: pnpm-cache-dir shell: bash + env: + COREPACK_ENABLE_STRICT: '0' run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Cache pnpm diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json index 7c6c07c5..2998e1bc 100644 --- a/.opencode/package-lock.json +++ b/.opencode/package-lock.json @@ -9,7 +9,7 @@ "version": "1.10.0", "license": "MIT", "devDependencies": { - "@opencode-ai/plugin": "^1.0.0", + "@opencode-ai/plugin": "^1.4.3", "@types/node": "^20.0.0", "typescript": "^5.3.0" }, @@ -21,22 +21,37 @@ } }, "node_modules/@opencode-ai/plugin": { - "version": "1.1.53", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.1.53.tgz", - "integrity": "sha512-9ye7Wz2kESgt02AUDaMea4hXxj6XhWwKAG8NwFhrw09Ux54bGaMJFt1eIS8QQGIMaD+Lp11X4QdyEg96etEBJw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.3.tgz", + "integrity": "sha512-Ob/3tVSIeuMRJBr2O23RtrnC5djRe01Lglx+TwGEmjrH9yDBJ2tftegYLnNEjRoMuzITgq9LD8168p4pzv+U/A==", "dev": true, "license": "MIT", "dependencies": { - "@opencode-ai/sdk": "1.1.53", + "@opencode-ai/sdk": "1.4.3", "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.97", + "@opentui/solid": ">=0.1.97" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } } }, "node_modules/@opencode-ai/sdk": { - "version": "1.1.53", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.53.tgz", - "integrity": "sha512-RUIVnPOP1CyyU32FrOOYuE7Ge51lOBuhaFp2NSX98ncApT7ffoNetmwzqrhOiJQgZB1KrbCHLYOCK6AZfacxag==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.3.tgz", + "integrity": "sha512-X0CAVbwoGAjTY2iecpWkx2B+GAa2jSaQKYpJ+xILopeF/OGKZUN15mjqci+L7cEuwLHV5wk3x2TStUOVCa5p0A==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } }, "node_modules/@types/node": { "version": "20.19.33", @@ -48,6 +63,61 @@ "undici-types": "~6.21.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -69,6 +139,22 @@ "dev": true, "license": "MIT" }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/zod": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", diff --git a/.opencode/package.json b/.opencode/package.json index 92736bcd..729524c9 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -60,7 +60,7 @@ "@opencode-ai/plugin": ">=1.0.0" }, "devDependencies": { - "@opencode-ai/plugin": "^1.0.0", + "@opencode-ai/plugin": "^1.4.3", "@types/node": "^20.0.0", "typescript": "^5.3.0" }, diff --git a/.opencode/plugins/ecc-hooks.ts b/.opencode/plugins/ecc-hooks.ts index 9e4ab3fc..51bde010 100644 --- a/.opencode/plugins/ecc-hooks.ts +++ b/.opencode/plugins/ecc-hooks.ts @@ -456,7 +456,7 @@ export const ECCHooksPlugin: ECCHooksPluginFn = async ({ const contextBlock = [ "# ECC Context (preserve across compaction)", "", - "## Active Plugin: Everything Claude Code v1.8.0", + "## Active Plugin: Everything Claude Code v1.10.0", "- Hooks: file.edited, tool.execute.before/after, session.created/idle/deleted, shell.env, compacting, permission.ask", "- Tools: run-tests, check-coverage, security-audit, format-code, lint-check, git-summary, changed-files", "- Agents: 13 specialized (planner, architect, tdd-guide, code-reviewer, security-reviewer, build-error-resolver, e2e-runner, refactor-cleaner, doc-updater, go-reviewer, go-build-resolver, database-reviewer, python-reviewer)", diff --git a/README.md b/README.md index b1b31e50..28251aa2 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ This repo is the raw code only. The guides explain everything. ### v1.10.0 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026) +- **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar. - **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 38 agents, 156 skills, and 72 legacy command shims. - **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane. - **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system. @@ -240,6 +241,23 @@ For manual install instructions see the README in the `rules/` folder. When copy **That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims. +### Dashboard GUI + +Launch the desktop dashboard to visually explore ECC components: + +```bash +npm run dashboard +# or +python3 ./ecc_dashboard.py +``` + +**Features:** +- Tabbed interface: Agents, Skills, Commands, Rules, Settings +- Dark/Light theme toggle +- Font customization (family & size) +- Project logo in header and taskbar +- Search and filter across all components + ### Multi-model commands require additional setup > WARNING: `multi-*` commands are **not** covered by the base plugin/rules install above. @@ -500,6 +518,12 @@ everything-claude-code/ |-- mcp-configs/ # MCP server configurations | |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc. | +|-- ecc_dashboard.py # Desktop GUI dashboard (Tkinter) +| +|-- assets/ # Assets for dashboard +| |-- images/ +| |-- ecc-logo.png +| |-- marketplace.json # Self-hosted marketplace config (for /plugin marketplace add) ``` @@ -989,6 +1013,14 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - Testing strategies (different frameworks, visual regression) - Domain-specific knowledge (ML, data engineering, mobile) +### Community Ecosystem Notes + +These are not bundled with ECC and are not audited by this repo, but they are worth knowing about if you are exploring the broader Claude Code skills ecosystem: + +- [claude-seo](https://github.com/AgriciDaniel/claude-seo) — SEO-focused skill and agent collection +- [claude-ads](https://github.com/AgriciDaniel/claude-ads) — Ad-audit and paid-growth workflow collection +- [claude-cybersecurity](https://github.com/AgriciDaniel/claude-cybersecurity) — Security-oriented skill and agent collection + --- ## Cursor IDE Support diff --git a/agents/a11y-architect.md b/agents/a11y-architect.md new file mode 100644 index 00000000..7ef2e517 --- /dev/null +++ b/agents/a11y-architect.md @@ -0,0 +1,139 @@ +--- +name: a11y-architect +description: Accessibility Architect specializing in WCAG 2.2 compliance for Web and Native platforms. Use PROACTIVELY when designing UI components, establishing design systems, or auditing code for inclusive user experiences. +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +--- + +You are a Senior Accessibility Architect. Your goal is to ensure that every digital product is Perceivable, Operable, Understandable, and Robust (POUR) for all users, including those with visual, auditory, motor, or cognitive disabilities. + +## Your Role + +- **Architecting Inclusivity**: Design UI systems that natively support assistive technologies (Screen Readers, Voice Control, Switch Access). +- **WCAG 2.2 Enforcement**: Apply the latest success criteria, focusing on new standards like Focus Appearance, Target Size, and Redundant Entry. +- **Platform Strategy**: Bridge the gap between Web standards (WAI-ARIA) and Native frameworks (SwiftUI/Jetpack Compose). +- **Technical Specifications**: Provide developers with precise attributes (roles, labels, hints, and traits) required for compliance. + +## Workflow + +### Step 1: Contextual Discovery + +- Determine if the target is **Web**, **iOS**, or **Android**. +- Analyze the user interaction (e.g., Is this a simple button or a complex data grid?). +- Identify potential accessibility "blockers" (e.g., color-only indicators, missing focus containment in modals). + +### Step 2: Strategic Implementation + +- **Apply the Accessibility Skill**: Invoke specific logic to generate semantic code. +- **Define Focus Flow**: Map out how a keyboard or screen reader user will move through the interface. +- **Optimize Touch/Pointer**: Ensure all interactive elements meet the minimum **24x24 pixel** spacing or **44x44 pixel** target size requirements. + +### Step 3: Validation & Documentation + +- Review the output against the WCAG 2.2 Level AA checklist. +- Provide a brief "Implementation Note" explaining _why_ certain attributes (like `aria-live` or `accessibilityHint`) were used. + +## Output Format + +For every component or page request, provide: + +1. **The Code**: Semantic HTML/ARIA or Native code. +2. **The Accessibility Tree**: A description of what a screen reader will announce. +3. **Compliance Mapping**: A list of specific WCAG 2.2 criteria addressed. + +## Examples + +### Example: Accessible Search Component + +**Input**: "Create a search bar with a submit icon." +**Action**: Ensuring the icon-only button has a visible label and the input is correctly labeled. +**Output**: + +```html +
+ + + +
+``` + +## WCAG 2.2 Core Compliance Checklist + +### 1. Perceivable (Information must be presentable) + +- [ ] **Text Alternatives**: All non-text content has a text alternative (Alt text or labels). +- [ ] **Contrast**: Text meets 4.5:1; UI components/graphics meet 3:1 contrast ratios. +- [ ] **Adaptable**: Content reflows and remains functional when resized up to 400%. + +### 2. Operable (Interface components must be usable) + +- [ ] **Keyboard Accessible**: Every interactive element is reachable via keyboard/switch control. +- [ ] **Navigable**: Focus order is logical, and focus indicators are high-contrast (SC 2.4.11). +- [ ] **Pointer Gestures**: Single-pointer alternatives exist for all dragging or multipoint gestures. +- [ ] **Target Size**: Interactive elements are at least 24x24 CSS pixels (SC 2.5.8). + +### 3. Understandable (Information must be clear) + +- [ ] **Predictable**: Navigation and identification of elements are consistent across the app. +- [ ] **Input Assistance**: Forms provide clear error identification and suggestions for fix. +- [ ] **Redundant Entry**: Avoid asking for the same info twice in a single process (SC 3.3.7). + +### 4. Robust (Content must be compatible) + +- [ ] **Compatibility**: Maximize compatibility with assistive tech using valid Name, Role, and Value. +- [ ] **Status Messages**: Screen readers are notified of dynamic changes via ARIA live regions. + +--- + +## Anti-Patterns + +| Issue | Why it fails | +| :------------------------- | :------------------------------------------------------------------------------------------------- | +| **"Click Here" Links** | Non-descriptive; screen reader users navigating by links won't know the destination. | +| **Fixed-Sized Containers** | Prevents content reflow and breaks the layout at higher zoom levels. | +| **Keyboard Traps** | Prevents users from navigating the rest of the page once they enter a component. | +| **Auto-Playing Media** | Distracting for users with cognitive disabilities; interferes with screen reader audio. | +| **Empty Buttons** | Icon-only buttons without an `aria-label` or `accessibilityLabel` are invisible to screen readers. | + +## Accessibility Decision Record Template + +For major UI decisions, use this format: + +````markdown +# ADR-ACC-[000]: [Title of the Accessibility Decision] + +## Status + +Proposed | **Accepted** | Deprecated | Superseded by [ADR-XXX] + +## Context + +_Describe the UI component or workflow being addressed._ + +- **Platform**: [Web | iOS | Android | Cross-platform] +- **WCAG 2.2 Success Criterion**: [e.g., 2.5.8 Target Size (Minimum)] +- **Problem**: What is the current accessibility barrier? (e.g., "The 'Close' button in the modal is too small for users with motor impairments.") + +## Decision + +_Detail the specific implementation choice._ +"We will implement a touch target of at least 44x44 points for all mobile navigation elements and 24x24 CSS pixels for web, ensuring a minimum 4px spacing between adjacent targets." + +## Implementation Details + +### Code/Spec + +```[language] +// Example: SwiftUI +Button(action: close) { + Image(systemName: "xmark") + .frame(width: 44, height: 44) // Standardizing hit area +} +.accessibilityLabel("Close modal") +``` +```` + +## Reference + +- See skill `accessibility` to transform raw UI requirements into platform-specific accessible code (WAI-ARIA, SwiftUI, or Jetpack Compose) based on WCAG 2.2 criteria. diff --git a/assets/images/ecc-logo.png b/assets/images/ecc-logo.png new file mode 100644 index 00000000..ef6e8ac8 Binary files /dev/null and b/assets/images/ecc-logo.png differ diff --git a/ecc2/Cargo.lock b/ecc2/Cargo.lock index ff240c32..9d0e631f 100644 --- a/ecc2/Cargo.lock +++ b/ecc2/Cargo.lock @@ -1286,6 +1286,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.112" @@ -1294,6 +1303,7 @@ checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/ecc2/Cargo.toml b/ecc2/Cargo.toml index ea8d9733..5ba65e66 100644 --- a/ecc2/Cargo.toml +++ b/ecc2/Cargo.toml @@ -7,6 +7,10 @@ license = "MIT" authors = ["Affaan Mustafa "] repository = "https://github.com/affaan-m/everything-claude-code" +[features] +default = ["vendored-openssl"] +vendored-openssl = ["git2/vendored-openssl"] + [dependencies] # TUI ratatui = { version = "0.30", features = ["crossterm_0_28"] } @@ -19,7 +23,7 @@ tokio = { version = "1", features = ["full"] } rusqlite = { version = "0.32", features = ["bundled"] } # Git integration -git2 = "0.20" +git2 = { version = "0.20", features = ["ssh"] } # Serialization serde = { version = "1", features = ["derive"] } diff --git a/ecc_dashboard.py b/ecc_dashboard.py new file mode 100644 index 00000000..b2ea49d3 --- /dev/null +++ b/ecc_dashboard.py @@ -0,0 +1,914 @@ +#!/usr/bin/env python3 +""" +ECC Dashboard - Everything Claude Code GUI +Cross-platform TkInter application for managing ECC components +""" + +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox +import os +import json +from typing import Dict, List, Optional + +# ============================================================================ +# DATA LOADERS - Load ECC data from the project +# ============================================================================ + +def get_project_path() -> str: + """Get the ECC project path - assumes this script is run from the project dir""" + return os.path.dirname(os.path.abspath(__file__)) + +def load_agents(project_path: str) -> List[Dict]: + """Load agents from AGENTS.md""" + agents_file = os.path.join(project_path, "AGENTS.md") + agents = [] + + if os.path.exists(agents_file): + with open(agents_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Parse agent table from AGENTS.md + lines = content.split('\n') + in_table = False + for line in lines: + if '| Agent | Purpose | When to Use |' in line: + in_table = True + continue + if in_table and line.startswith('|'): + parts = [p.strip() for p in line.split('|')] + if len(parts) >= 4 and parts[1] and parts[1] != 'Agent': + agents.append({ + 'name': parts[1], + 'purpose': parts[2], + 'when_to_use': parts[3] + }) + + # Fallback default agents if file not found + if not agents: + agents = [ + {'name': 'planner', 'purpose': 'Implementation planning', 'when_to_use': 'Complex features, refactoring'}, + {'name': 'architect', 'purpose': 'System design and scalability', 'when_to_use': 'Architectural decisions'}, + {'name': 'tdd-guide', 'purpose': 'Test-driven development', 'when_to_use': 'New features, bug fixes'}, + {'name': 'code-reviewer', 'purpose': 'Code quality and maintainability', 'when_to_use': 'After writing/modifying code'}, + {'name': 'security-reviewer', 'purpose': 'Vulnerability detection', 'when_to_use': 'Before commits, sensitive code'}, + {'name': 'build-error-resolver', 'purpose': 'Fix build/type errors', 'when_to_use': 'When build fails'}, + {'name': 'e2e-runner', 'purpose': 'End-to-end Playwright testing', 'when_to_use': 'Critical user flows'}, + {'name': 'refactor-cleaner', 'purpose': 'Dead code cleanup', 'when_to_use': 'Code maintenance'}, + {'name': 'doc-updater', 'purpose': 'Documentation and codemaps', 'when_to_use': 'Updating docs'}, + {'name': 'go-reviewer', 'purpose': 'Go code review', 'when_to_use': 'Go projects'}, + {'name': 'python-reviewer', 'purpose': 'Python code review', 'when_to_use': 'Python projects'}, + {'name': 'typescript-reviewer', 'purpose': 'TypeScript/JavaScript code review', 'when_to_use': 'TypeScript projects'}, + {'name': 'rust-reviewer', 'purpose': 'Rust code review', 'when_to_use': 'Rust projects'}, + {'name': 'java-reviewer', 'purpose': 'Java and Spring Boot code review', 'when_to_use': 'Java projects'}, + {'name': 'kotlin-reviewer', 'purpose': 'Kotlin code review', 'when_to_use': 'Kotlin projects'}, + {'name': 'cpp-reviewer', 'purpose': 'C/C++ code review', 'when_to_use': 'C/C++ projects'}, + {'name': 'database-reviewer', 'purpose': 'PostgreSQL/Supabase specialist', 'when_to_use': 'Database work'}, + {'name': 'loop-operator', 'purpose': 'Autonomous loop execution', 'when_to_use': 'Run loops safely'}, + {'name': 'harness-optimizer', 'purpose': 'Harness config tuning', 'when_to_use': 'Reliability, cost, throughput'}, + ] + + return agents + +def load_skills(project_path: str) -> List[Dict]: + """Load skills from skills directory""" + skills_dir = os.path.join(project_path, "skills") + skills = [] + + if os.path.exists(skills_dir): + for item in os.listdir(skills_dir): + skill_path = os.path.join(skills_dir, item) + if os.path.isdir(skill_path): + skill_file = os.path.join(skill_path, "SKILL.md") + description = item.replace('-', ' ').title() + + if os.path.exists(skill_file): + try: + with open(skill_file, 'r', encoding='utf-8') as f: + content = f.read() + # Extract description from first lines + lines = content.split('\n') + for line in lines: + if line.strip() and not line.startswith('#'): + description = line.strip()[:100] + break + if line.startswith('# '): + description = line[2:].strip()[:100] + break + except: + pass + + # Determine category + category = "General" + item_lower = item.lower() + if 'python' in item_lower or 'django' in item_lower: + category = "Python" + elif 'golang' in item_lower or 'go-' in item_lower: + category = "Go" + elif 'frontend' in item_lower or 'react' in item_lower: + category = "Frontend" + elif 'backend' in item_lower or 'api' in item_lower: + category = "Backend" + elif 'security' in item_lower: + category = "Security" + elif 'testing' in item_lower or 'tdd' in item_lower: + category = "Testing" + elif 'docker' in item_lower or 'deployment' in item_lower: + category = "DevOps" + elif 'swift' in item_lower or 'ios' in item_lower: + category = "iOS" + elif 'java' in item_lower or 'spring' in item_lower: + category = "Java" + elif 'rust' in item_lower: + category = "Rust" + + skills.append({ + 'name': item, + 'description': description, + 'category': category, + 'path': skill_path + }) + + # Fallback if directory doesn't exist + if not skills: + skills = [ + {'name': 'tdd-workflow', 'description': 'Test-driven development workflow', 'category': 'Testing'}, + {'name': 'coding-standards', 'description': 'Baseline coding conventions', 'category': 'General'}, + {'name': 'security-review', 'description': 'Security checklist and patterns', 'category': 'Security'}, + {'name': 'frontend-patterns', 'description': 'React and Next.js patterns', 'category': 'Frontend'}, + {'name': 'backend-patterns', 'description': 'API and database patterns', 'category': 'Backend'}, + {'name': 'api-design', 'description': 'REST API design patterns', 'category': 'Backend'}, + {'name': 'docker-patterns', 'description': 'Docker and container patterns', 'category': 'DevOps'}, + {'name': 'e2e-testing', 'description': 'Playwright E2E testing patterns', 'category': 'Testing'}, + {'name': 'verification-loop', 'description': 'Build, test, lint verification', 'category': 'General'}, + {'name': 'python-patterns', 'description': 'Python idioms and best practices', 'category': 'Python'}, + {'name': 'golang-patterns', 'description': 'Go idioms and best practices', 'category': 'Go'}, + {'name': 'django-patterns', 'description': 'Django patterns and best practices', 'category': 'Python'}, + {'name': 'springboot-patterns', 'description': 'Java Spring Boot patterns', 'category': 'Java'}, + {'name': 'laravel-patterns', 'description': 'Laravel architecture patterns', 'category': 'PHP'}, + ] + + return skills + +def load_commands(project_path: str) -> List[Dict]: + """Load commands from commands directory""" + commands_dir = os.path.join(project_path, "commands") + commands = [] + + if os.path.exists(commands_dir): + for item in os.listdir(commands_dir): + if item.endswith('.md'): + cmd_name = item[:-3] + description = "" + + try: + with open(os.path.join(commands_dir, item), 'r', encoding='utf-8') as f: + content = f.read() + lines = content.split('\n') + for line in lines: + if line.startswith('# '): + description = line[2:].strip() + break + except: + pass + + commands.append({ + 'name': cmd_name, + 'description': description or cmd_name.replace('-', ' ').title() + }) + + # Fallback commands + if not commands: + commands = [ + {'name': 'plan', 'description': 'Create implementation plan'}, + {'name': 'tdd', 'description': 'Test-driven development workflow'}, + {'name': 'code-review', 'description': 'Review code for quality and security'}, + {'name': 'build-fix', 'description': 'Fix build and TypeScript errors'}, + {'name': 'e2e', 'description': 'Generate and run E2E tests'}, + {'name': 'refactor-clean', 'description': 'Remove dead code'}, + {'name': 'verify', 'description': 'Run verification loop'}, + {'name': 'eval', 'description': 'Run evaluation against criteria'}, + {'name': 'security', 'description': 'Run comprehensive security review'}, + {'name': 'test-coverage', 'description': 'Analyze test coverage'}, + {'name': 'update-docs', 'description': 'Update documentation'}, + {'name': 'setup-pm', 'description': 'Configure package manager'}, + {'name': 'go-review', 'description': 'Go code review'}, + {'name': 'go-test', 'description': 'Go TDD workflow'}, + {'name': 'python-review', 'description': 'Python code review'}, + ] + + return commands + +def load_rules(project_path: str) -> List[Dict]: + """Load rules from rules directory""" + rules_dir = os.path.join(project_path, "rules") + rules = [] + + if os.path.exists(rules_dir): + for item in os.listdir(rules_dir): + item_path = os.path.join(rules_dir, item) + if os.path.isdir(item_path): + # Common rules + if item == "common": + for file in os.listdir(item_path): + if file.endswith('.md'): + rules.append({ + 'name': file[:-3], + 'language': 'Common', + 'path': os.path.join(item_path, file) + }) + else: + # Language-specific rules + for file in os.listdir(item_path): + if file.endswith('.md'): + rules.append({ + 'name': file[:-3], + 'language': item.title(), + 'path': os.path.join(item_path, file) + }) + + # Fallback rules + if not rules: + rules = [ + {'name': 'coding-style', 'language': 'Common', 'path': ''}, + {'name': 'git-workflow', 'language': 'Common', 'path': ''}, + {'name': 'testing', 'language': 'Common', 'path': ''}, + {'name': 'performance', 'language': 'Common', 'path': ''}, + {'name': 'patterns', 'language': 'Common', 'path': ''}, + {'name': 'security', 'language': 'Common', 'path': ''}, + {'name': 'typescript', 'language': 'TypeScript', 'path': ''}, + {'name': 'python', 'language': 'Python', 'path': ''}, + {'name': 'golang', 'language': 'Go', 'path': ''}, + {'name': 'swift', 'language': 'Swift', 'path': ''}, + {'name': 'php', 'language': 'PHP', 'path': ''}, + ] + + return rules + +# ============================================================================ +# MAIN APPLICATION +# ============================================================================ + +class ECCDashboard(tk.Tk): + """Main ECC Dashboard Application""" + + def __init__(self): + super().__init__() + + self.project_path = get_project_path() + self.title("ECC Dashboard - Everything Claude Code") + + self.state('zoomed') + + try: + self.icon_image = tk.PhotoImage(file='assets/images/ecc-logo.png') + self.iconphoto(True, self.icon_image) + except: + pass + + self.minsize(800, 600) + + # Load data + self.agents = load_agents(self.project_path) + self.skills = load_skills(self.project_path) + self.commands = load_commands(self.project_path) + self.rules = load_rules(self.project_path) + + # Settings + self.settings = { + 'project_path': self.project_path, + 'theme': 'light' + } + + # Setup UI + self.setup_styles() + self.create_widgets() + + # Center window + self.center_window() + + def setup_styles(self): + """Setup ttk styles for modern look""" + style = ttk.Style() + style.theme_use('clam') + + # Configure tab style + style.configure('TNotebook', background='#f0f0f0') + style.configure('TNotebook.Tab', padding=[10, 5], font=('Arial', 10)) + style.map('TNotebook.Tab', background=[('selected', '#ffffff')]) + + # Configure Treeview + style.configure('Treeview', font=('Arial', 10), rowheight=25) + style.configure('Treeview.Heading', font=('Arial', 10, 'bold')) + + # Configure buttons + style.configure('TButton', font=('Arial', 10), padding=5) + + def center_window(self): + """Center the window on screen""" + self.update_idletasks() + width = self.winfo_width() + height = self.winfo_height() + x = (self.winfo_screenwidth() // 2) - (width // 2) + y = (self.winfo_screenheight() // 2) - (height // 2) + self.geometry(f'{width}x{height}+{x}+{y}') + + def create_widgets(self): + """Create all UI widgets""" + # Main container + main_frame = ttk.Frame(self) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Header + header_frame = ttk.Frame(main_frame) + header_frame.pack(fill=tk.X, pady=(0, 10)) + + try: + self.logo_image = tk.PhotoImage(file='assets/images/ecc-logo.png') + self.logo_image = self.logo_image.subsample(2, 2) + ttk.Label(header_frame, image=self.logo_image).pack(side=tk.LEFT, padx=(0, 10)) + except: + pass + + self.title_label = ttk.Label(header_frame, text="ECC Dashboard", font=('Open Sans', 18, 'bold')) + self.title_label.pack(side=tk.LEFT) + self.version_label = ttk.Label(header_frame, text="v1.10.0", font=('Open Sans', 10), foreground='gray') + self.version_label.pack(side=tk.LEFT, padx=(10, 0)) + + # Notebook (tabs) + self.notebook = ttk.Notebook(main_frame) + self.notebook.pack(fill=tk.BOTH, expand=True) + + # Create tabs + self.create_agents_tab() + self.create_skills_tab() + self.create_commands_tab() + self.create_rules_tab() + self.create_settings_tab() + + # Status bar + status_frame = ttk.Frame(main_frame) + status_frame.pack(fill=tk.X, pady=(10, 0)) + + self.status_label = ttk.Label(status_frame, + text=f"Ready | Agents: {len(self.agents)} | Skills: {len(self.skills)} | Commands: {len(self.commands)}", + font=('Arial', 9), foreground='gray') + self.status_label.pack(side=tk.LEFT) + + # ========================================================================= + # AGENTS TAB + # ========================================================================= + + def create_agents_tab(self): + """Create Agents tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Agents ({len(self.agents)})") + + # Search bar + search_frame = ttk.Frame(frame) + search_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT) + self.agent_search = ttk.Entry(search_frame, width=30) + self.agent_search.pack(side=tk.LEFT, padx=5) + self.agent_search.bind('', self.filter_agents) + + ttk.Label(search_frame, text="Count:").pack(side=tk.LEFT, padx=(20, 0)) + self.agent_count_label = ttk.Label(search_frame, text=str(len(self.agents))) + self.agent_count_label.pack(side=tk.LEFT) + + # Split pane: list + details + paned = ttk.PanedWindow(frame, orient=tk.HORIZONTAL) + paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + # Agent list + list_frame = ttk.Frame(paned) + paned.add(list_frame, weight=2) + + columns = ('name', 'purpose') + self.agent_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.agent_tree.heading('#0', text='#') + self.agent_tree.heading('name', text='Agent Name') + self.agent_tree.heading('purpose', text='Purpose') + self.agent_tree.column('#0', width=40) + self.agent_tree.column('name', width=180) + self.agent_tree.column('purpose', width=250) + + self.agent_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Scrollbar + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.agent_tree.yview) + self.agent_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Details panel + details_frame = ttk.Frame(paned) + paned.add(details_frame, weight=1) + + ttk.Label(details_frame, text="Details", font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=5) + + self.agent_details = scrolledtext.ScrolledText(details_frame, wrap=tk.WORD, height=15) + self.agent_details.pack(fill=tk.BOTH, expand=True) + + # Bind selection + self.agent_tree.bind('<>', self.on_agent_select) + + # Populate list + self.populate_agents(self.agents) + + def populate_agents(self, agents: List[Dict]): + """Populate agents list""" + for item in self.agent_tree.get_children(): + self.agent_tree.delete(item) + + for i, agent in enumerate(agents, 1): + self.agent_tree.insert('', tk.END, text=str(i), values=(agent['name'], agent['purpose'])) + + def filter_agents(self, event=None): + """Filter agents based on search""" + query = self.agent_search.get().lower() + + if not query: + filtered = self.agents + else: + filtered = [a for a in self.agents + if query in a['name'].lower() or query in a['purpose'].lower()] + + self.populate_agents(filtered) + self.agent_count_label.config(text=str(len(filtered))) + + def on_agent_select(self, event): + """Handle agent selection""" + selection = self.agent_tree.selection() + if not selection: + return + + item = self.agent_tree.item(selection[0]) + agent_name = item['values'][0] + + agent = next((a for a in self.agents if a['name'] == agent_name), None) + if agent: + details = f"""Agent: {agent['name']} + +Purpose: {agent['purpose']} + +When to Use: {agent['when_to_use']} + +--- +Usage in Claude Code: +Use the /{agent['name']} command or invoke via agent delegation.""" + self.agent_details.delete('1.0', tk.END) + self.agent_details.insert('1.0', details) + + # ========================================================================= + # SKILLS TAB + # ========================================================================= + + def create_skills_tab(self): + """Create Skills tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Skills ({len(self.skills)})") + + # Search and filter + filter_frame = ttk.Frame(frame) + filter_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(filter_frame, text="Search:").pack(side=tk.LEFT) + self.skill_search = ttk.Entry(filter_frame, width=25) + self.skill_search.pack(side=tk.LEFT, padx=5) + self.skill_search.bind('', self.filter_skills) + + ttk.Label(filter_frame, text="Category:").pack(side=tk.LEFT, padx=(20, 0)) + self.skill_category = ttk.Combobox(filter_frame, values=['All'] + self.get_categories(), width=15) + self.skill_category.set('All') + self.skill_category.pack(side=tk.LEFT, padx=5) + self.skill_category.bind('<>', self.filter_skills) + + ttk.Label(filter_frame, text="Count:").pack(side=tk.LEFT, padx=(20, 0)) + self.skill_count_label = ttk.Label(filter_frame, text=str(len(self.skills))) + self.skill_count_label.pack(side=tk.LEFT) + + # Split pane + paned = ttk.PanedWindow(frame, orient=tk.HORIZONTAL) + paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + # Skill list + list_frame = ttk.Frame(paned) + paned.add(list_frame, weight=1) + + columns = ('name', 'category', 'description') + self.skill_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.skill_tree.heading('#0', text='#') + self.skill_tree.heading('name', text='Skill Name') + self.skill_tree.heading('category', text='Category') + self.skill_tree.heading('description', text='Description') + + self.skill_tree.column('#0', width=40) + self.skill_tree.column('name', width=180) + self.skill_tree.column('category', width=100) + self.skill_tree.column('description', width=300) + + self.skill_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.skill_tree.yview) + self.skill_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Details + details_frame = ttk.Frame(paned) + paned.add(details_frame, weight=1) + + ttk.Label(details_frame, text="Description", font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=5) + + self.skill_details = scrolledtext.ScrolledText(details_frame, wrap=tk.WORD, height=15) + self.skill_details.pack(fill=tk.BOTH, expand=True) + + self.skill_tree.bind('<>', self.on_skill_select) + + self.populate_skills(self.skills) + + def get_categories(self) -> List[str]: + """Get unique categories from skills""" + categories = set(s['category'] for s in self.skills) + return sorted(categories) + + def populate_skills(self, skills: List[Dict]): + """Populate skills list""" + for item in self.skill_tree.get_children(): + self.skill_tree.delete(item) + + for i, skill in enumerate(skills, 1): + self.skill_tree.insert('', tk.END, text=str(i), + values=(skill['name'], skill['category'], skill['description'])) + + def filter_skills(self, event=None): + """Filter skills based on search and category""" + search = self.skill_search.get().lower() + category = self.skill_category.get() + + filtered = self.skills + + if category != 'All': + filtered = [s for s in filtered if s['category'] == category] + + if search: + filtered = [s for s in filtered + if search in s['name'].lower() or search in s['description'].lower()] + + self.populate_skills(filtered) + self.skill_count_label.config(text=str(len(filtered))) + + def on_skill_select(self, event): + """Handle skill selection""" + selection = self.skill_tree.selection() + if not selection: + return + + item = self.skill_tree.item(selection[0]) + skill_name = item['values'][0] + + skill = next((s for s in self.skills if s['name'] == skill_name), None) + if skill: + details = f"""Skill: {skill['name']} + +Category: {skill['category']} + +Description: {skill['description']} + +Path: {skill['path']} + +--- +Usage: This skill is automatically activated when working with related technologies.""" + self.skill_details.delete('1.0', tk.END) + self.skill_details.insert('1.0', details) + + # ========================================================================= + # COMMANDS TAB + # ========================================================================= + + def create_commands_tab(self): + """Create Commands tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Commands ({len(self.commands)})") + + # Info + info_frame = ttk.Frame(frame) + info_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(info_frame, text="Slash Commands for Claude Code:", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(info_frame, text="Use these commands in Claude Code by typing /command_name", + foreground='gray').pack(anchor=tk.W) + + # Commands list + list_frame = ttk.Frame(frame) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + columns = ('name', 'description') + self.command_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.command_tree.heading('#0', text='#') + self.command_tree.heading('name', text='Command') + self.command_tree.heading('description', text='Description') + + self.command_tree.column('#0', width=40) + self.command_tree.column('name', width=150) + self.command_tree.column('description', width=400) + + self.command_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.command_tree.yview) + self.command_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Populate + for i, cmd in enumerate(self.commands, 1): + self.command_tree.insert('', tk.END, text=str(i), + values=('/' + cmd['name'], cmd['description'])) + + # ========================================================================= + # RULES TAB + # ========================================================================= + + def create_rules_tab(self): + """Create Rules tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text=f"Rules ({len(self.rules)})") + + # Info + info_frame = ttk.Frame(frame) + info_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(info_frame, text="Coding Rules by Language:", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(info_frame, text="These rules are automatically applied in Claude Code", + foreground='gray').pack(anchor=tk.W) + + # Filter + filter_frame = ttk.Frame(frame) + filter_frame.pack(fill=tk.X, padx=10, pady=5) + + ttk.Label(filter_frame, text="Language:").pack(side=tk.LEFT) + self.rules_language = ttk.Combobox(filter_frame, + values=['All'] + self.get_rule_languages(), + width=15) + self.rules_language.set('All') + self.rules_language.pack(side=tk.LEFT, padx=5) + self.rules_language.bind('<>', self.filter_rules) + + # Rules list + list_frame = ttk.Frame(frame) + list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) + + columns = ('name', 'language') + self.rules_tree = ttk.Treeview(list_frame, columns=columns, show='tree headings') + self.rules_tree.heading('#0', text='#') + self.rules_tree.heading('name', text='Rule Name') + self.rules_tree.heading('language', text='Language') + + self.rules_tree.column('#0', width=40) + self.rules_tree.column('name', width=250) + self.rules_tree.column('language', width=100) + + self.rules_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.rules_tree.yview) + self.rules_tree.configure(yscrollcommand=scrollbar.set) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + self.populate_rules(self.rules) + + def get_rule_languages(self) -> List[str]: + """Get unique languages from rules""" + languages = set(r['language'] for r in self.rules) + return sorted(languages) + + def populate_rules(self, rules: List[Dict]): + """Populate rules list""" + for item in self.rules_tree.get_children(): + self.rules_tree.delete(item) + + for i, rule in enumerate(rules, 1): + self.rules_tree.insert('', tk.END, text=str(i), + values=(rule['name'], rule['language'])) + + def filter_rules(self, event=None): + """Filter rules by language""" + language = self.rules_language.get() + + if language == 'All': + filtered = self.rules + else: + filtered = [r for r in self.rules if r['language'] == language] + + self.populate_rules(filtered) + + # ========================================================================= + # SETTINGS TAB + # ========================================================================= + + def create_settings_tab(self): + """Create Settings tab""" + frame = ttk.Frame(self.notebook) + self.notebook.add(frame, text="Settings") + + # Project path + path_frame = ttk.LabelFrame(frame, text="Project Path", padding=10) + path_frame.pack(fill=tk.X, padx=10, pady=10) + + self.path_entry = ttk.Entry(path_frame, width=60) + self.path_entry.insert(0, self.project_path) + self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + ttk.Button(path_frame, text="Browse...", command=self.browse_path).pack(side=tk.LEFT, padx=5) + + # Theme + theme_frame = ttk.LabelFrame(frame, text="Appearance", padding=10) + theme_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(theme_frame, text="Theme:").pack(anchor=tk.W) + self.theme_var = tk.StringVar(value='light') + light_rb = ttk.Radiobutton(theme_frame, text="Light", variable=self.theme_var, + value='light', command=self.apply_theme) + light_rb.pack(anchor=tk.W) + dark_rb = ttk.Radiobutton(theme_frame, text="Dark", variable=self.theme_var, + value='dark', command=self.apply_theme) + dark_rb.pack(anchor=tk.W) + + font_frame = ttk.LabelFrame(frame, text="Font", padding=10) + font_frame.pack(fill=tk.X, padx=10, pady=10) + + ttk.Label(font_frame, text="Font Family:").pack(anchor=tk.W) + self.font_var = tk.StringVar(value='Open Sans') + + fonts = ['Open Sans', 'Arial', 'Helvetica', 'Times New Roman', 'Courier New', 'Verdana', 'Georgia', 'Tahoma', 'Trebuchet MS'] + self.font_combo = ttk.Combobox(font_frame, textvariable=self.font_var, values=fonts, state='readonly') + self.font_combo.pack(anchor=tk.W, fill=tk.X, pady=(5, 0)) + self.font_combo.bind('<>', lambda e: self.apply_theme()) + + ttk.Label(font_frame, text="Font Size:").pack(anchor=tk.W, pady=(10, 0)) + self.size_var = tk.StringVar(value='10') + sizes = ['8', '9', '10', '11', '12', '14', '16', '18', '20'] + self.size_combo = ttk.Combobox(font_frame, textvariable=self.size_var, values=sizes, state='readonly', width=10) + self.size_combo.pack(anchor=tk.W, pady=(5, 0)) + self.size_combo.bind('<>', lambda e: self.apply_theme()) + + # Quick Actions + actions_frame = ttk.LabelFrame(frame, text="Quick Actions", padding=10) + actions_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + ttk.Button(actions_frame, text="Open Project in Terminal", + command=self.open_terminal).pack(fill=tk.X, pady=2) + ttk.Button(actions_frame, text="Open README", + command=self.open_readme).pack(fill=tk.X, pady=2) + ttk.Button(actions_frame, text="Open AGENTS.md", + command=self.open_agents).pack(fill=tk.X, pady=2) + ttk.Button(actions_frame, text="Refresh Data", + command=self.refresh_data).pack(fill=tk.X, pady=2) + + # About + about_frame = ttk.LabelFrame(frame, text="About", padding=10) + about_frame.pack(fill=tk.X, padx=10, pady=10) + + about_text = """ECC Dashboard v1.0.0 +Everything Claude Code GUI + +A cross-platform desktop application for +managing and exploring ECC components. + +Version: 1.10.0 +Project: github.com/affaan-m/everything-claude-code""" + + ttk.Label(about_frame, text=about_text, justify=tk.LEFT).pack(anchor=tk.W) + + def browse_path(self): + """Browse for project path""" + from tkinter import filedialog + path = filedialog.askdirectory(initialdir=self.project_path) + if path: + self.path_entry.delete(0, tk.END) + self.path_entry.insert(0, path) + + def open_terminal(self): + """Open terminal at project path""" + import subprocess + path = self.path_entry.get() + if os.name == 'nt': # Windows + subprocess.Popen(['cmd', '/c', 'start', 'cmd', '/k', f'cd /d "{path}"']) + elif os.uname().sysname == 'Darwin': # macOS + subprocess.Popen(['open', '-a', 'Terminal', path]) + else: # Linux + subprocess.Popen(['x-terminal-emulator', '-e', f'cd {path}']) + + def open_readme(self): + """Open README in default browser/reader""" + import subprocess + path = os.path.join(self.path_entry.get(), 'README.md') + if os.path.exists(path): + subprocess.Popen(['xdg-open' if os.name != 'nt' else 'start', path]) + else: + messagebox.showerror("Error", "README.md not found") + + def open_agents(self): + """Open AGENTS.md""" + import subprocess + path = os.path.join(self.path_entry.get(), 'AGENTS.md') + if os.path.exists(path): + subprocess.Popen(['xdg-open' if os.name != 'nt' else 'start', path]) + else: + messagebox.showerror("Error", "AGENTS.md not found") + + def refresh_data(self): + """Refresh all data""" + self.project_path = self.path_entry.get() + self.agents = load_agents(self.project_path) + self.skills = load_skills(self.project_path) + self.commands = load_commands(self.project_path) + self.rules = load_rules(self.project_path) + + # Update tabs + self.notebook.tab(0, text=f"Agents ({len(self.agents)})") + self.notebook.tab(1, text=f"Skills ({len(self.skills)})") + self.notebook.tab(2, text=f"Commands ({len(self.commands)})") + self.notebook.tab(3, text=f"Rules ({len(self.rules)})") + + # Repopulate + self.populate_agents(self.agents) + self.populate_skills(self.skills) + + # Update status + self.status_label.config( + text=f"Ready | Agents: {len(self.agents)} | Skills: {len(self.skills)} | Commands: {len(self.commands)}" + ) + + messagebox.showinfo("Success", "Data refreshed successfully!") + + def apply_theme(self): + theme = self.theme_var.get() + font_family = self.font_var.get() + font_size = int(self.size_var.get()) + font_tuple = (font_family, font_size) + + if theme == 'dark': + bg_color = '#2b2b2b' + fg_color = '#ffffff' + entry_bg = '#3c3c3c' + frame_bg = '#2b2b2b' + select_bg = '#0f5a9e' + else: + bg_color = '#f0f0f0' + fg_color = '#000000' + entry_bg = '#ffffff' + frame_bg = '#f0f0f0' + select_bg = '#e0e0e0' + + self.configure(background=bg_color) + + style = ttk.Style() + style.configure('.', background=bg_color, foreground=fg_color, font=font_tuple) + style.configure('TFrame', background=bg_color, font=font_tuple) + style.configure('TLabel', background=bg_color, foreground=fg_color, font=font_tuple) + style.configure('TNotebook', background=bg_color, font=font_tuple) + style.configure('TNotebook.Tab', background=frame_bg, foreground=fg_color, font=font_tuple) + style.map('TNotebook.Tab', background=[('selected', select_bg)]) + style.configure('Treeview', background=entry_bg, foreground=fg_color, fieldbackground=entry_bg, font=font_tuple) + style.configure('Treeview.Heading', background=frame_bg, foreground=fg_color, font=font_tuple) + style.configure('TEntry', fieldbackground=entry_bg, foreground=fg_color, font=font_tuple) + style.configure('TButton', background=frame_bg, foreground=fg_color, font=font_tuple) + + self.title_label.configure(font=(font_family, 18, 'bold')) + self.version_label.configure(font=(font_family, 10)) + + def update_widget_colors(widget): + try: + widget.configure(background=bg_color) + except: + pass + for child in widget.winfo_children(): + try: + child.configure(background=bg_color) + except: + pass + try: + update_widget_colors(child) + except: + pass + + try: + update_widget_colors(self) + except: + pass + + self.update() + + +# ============================================================================ +# MAIN +# ============================================================================ + +def main(): + """Main entry point""" + app = ECCDashboard() + app.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/hooks/hooks.json b/hooks/hooks.json index cdd2584d..22799c08 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -126,6 +126,30 @@ ], "description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls", "id": "pre:mcp-health-check" + }, + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"pre:edit-write:gateguard-fact-force\" \"scripts/hooks/gateguard-fact-force.js\" \"standard,strict\"", + "timeout": 5 + } + ], + "description": "Fact-forcing gate: block first Edit/Write/MultiEdit per file and demand investigation (importers, data schemas, user instruction) before allowing", + "id": "pre:edit-write:gateguard-fact-force" + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"pre:bash:gateguard-fact-force\" \"scripts/hooks/gateguard-fact-force.js\" \"standard,strict\"", + "timeout": 5 + } + ], + "description": "Fact-forcing gate: block destructive Bash commands and demand rollback plan; quote user instruction on first Bash per session", + "id": "pre:bash:gateguard-fact-force" } ], "PreCompact": [ diff --git a/package.json b/package.json index 4266e884..8baa18d5 100644 --- a/package.json +++ b/package.json @@ -39,69 +39,198 @@ }, "files": [ ".agents/", + ".claude-plugin/", ".codex/", + ".codex-plugin/", ".cursor/", - ".opencode/commands/", - ".opencode/dist/", - ".opencode/instructions/", - ".opencode/plugins/", - ".opencode/prompts/", - ".opencode/tools/", - ".opencode/index.ts", - ".opencode/opencode.json", - ".opencode/package.json", - ".opencode/package-lock.json", - ".opencode/tsconfig.json", - ".opencode/MIGRATION.md", - ".opencode/README.md", + ".gemini/", + ".opencode/", + ".mcp.json", + "AGENTS.md", + "VERSION", + "agent.yaml", "agents/", "commands/", - "contexts/", - "examples/CLAUDE.md", - "examples/user-CLAUDE.md", - "examples/statusline.json", "hooks/", + "install.ps1", + "install.sh", "manifests/", "mcp-configs/", - "plugins/", "rules/", "schemas/", - "scripts/ci/", + "scripts/catalog.js", + "scripts/claw.js", + "scripts/codex/merge-codex-config.js", + "scripts/codex/merge-mcp-config.js", + "scripts/doctor.js", "scripts/ecc.js", "scripts/gemini-adapt-agents.js", + "scripts/harness-audit.js", "scripts/hooks/", - "scripts/lib/", - "scripts/claw.js", - "scripts/doctor.js", - "scripts/status.js", - "scripts/sessions-cli.js", "scripts/install-apply.js", "scripts/install-plan.js", + "scripts/lib/", "scripts/list-installed.js", "scripts/orchestration-status.js", "scripts/orchestrate-codex-worker.sh", "scripts/orchestrate-worktrees.js", + "scripts/repair.js", + "scripts/session-inspect.js", + "scripts/sessions-cli.js", "scripts/setup-package-manager.js", "scripts/skill-create-output.js", - "scripts/codex/merge-codex-config.js", - "scripts/codex/merge-mcp-config.js", - "scripts/repair.js", - "scripts/harness-audit.js", - "scripts/session-inspect.js", + "scripts/status.js", "scripts/uninstall.js", - "skills/", - "AGENTS.md", - "agent.yaml", - ".claude-plugin/plugin.json", - ".claude-plugin/marketplace.json", - ".claude-plugin/README.md", - ".codex-plugin/plugin.json", - ".codex-plugin/README.md", - ".mcp.json", - "install.sh", - "install.ps1", - "llms.txt", - "VERSION" + "skills/agent-harness-construction/", + "skills/agent-introspection-debugging/", + "skills/agent-sort/", + "skills/agentic-engineering/", + "skills/ai-first-engineering/", + "skills/ai-regression-testing/", + "skills/android-clean-architecture/", + "skills/api-connector-builder/", + "skills/api-design/", + "skills/article-writing/", + "skills/automation-audit-ops/", + "skills/autonomous-loops/", + "skills/backend-patterns/", + "skills/blueprint/", + "skills/brand-voice/", + "skills/carrier-relationship-management/", + "skills/claude-api/", + "skills/claude-devfleet/", + "skills/clickhouse-io/", + "skills/code-tour/", + "skills/coding-standards/", + "skills/compose-multiplatform-patterns/", + "skills/configure-ecc/", + "skills/connections-optimizer/", + "skills/content-engine/", + "skills/content-hash-cache-pattern/", + "skills/continuous-agent-loop/", + "skills/continuous-learning/", + "skills/continuous-learning-v2/", + "skills/cost-aware-llm-pipeline/", + "skills/council/", + "skills/cpp-coding-standards/", + "skills/cpp-testing/", + "skills/crosspost/", + "skills/csharp-testing/", + "skills/customer-billing-ops/", + "skills/customs-trade-compliance/", + "skills/dart-flutter-patterns/", + "skills/dashboard-builder/", + "skills/data-scraper-agent/", + "skills/database-migrations/", + "skills/deep-research/", + "skills/defi-amm-security/", + "skills/deployment-patterns/", + "skills/django-patterns/", + "skills/django-security/", + "skills/django-tdd/", + "skills/django-verification/", + "skills/dmux-workflows/", + "skills/docker-patterns/", + "skills/dotnet-patterns/", + "skills/e2e-testing/", + "skills/ecc-tools-cost-audit/", + "skills/email-ops/", + "skills/energy-procurement/", + "skills/enterprise-agent-ops/", + "skills/eval-harness/", + "skills/evm-token-decimals/", + "skills/exa-search/", + "skills/fal-ai-media/", + "skills/finance-billing-ops/", + "skills/foundation-models-on-device/", + "skills/frontend-design/", + "skills/frontend-patterns/", + "skills/frontend-slides/", + "skills/github-ops/", + "skills/golang-patterns/", + "skills/golang-testing/", + "skills/google-workspace-ops/", + "skills/healthcare-phi-compliance/", + "skills/hipaa-compliance/", + "skills/hookify-rules/", + "skills/inventory-demand-planning/", + "skills/investor-materials/", + "skills/investor-outreach/", + "skills/iterative-retrieval/", + "skills/java-coding-standards/", + "skills/jira-integration/", + "skills/jpa-patterns/", + "skills/knowledge-ops/", + "skills/kotlin-coroutines-flows/", + "skills/kotlin-exposed-patterns/", + "skills/kotlin-ktor-patterns/", + "skills/kotlin-patterns/", + "skills/kotlin-testing/", + "skills/laravel-patterns/", + "skills/laravel-plugin-discovery/", + "skills/laravel-security/", + "skills/laravel-tdd/", + "skills/laravel-verification/", + "skills/lead-intelligence/", + "skills/liquid-glass-design/", + "skills/llm-trading-agent-security/", + "skills/logistics-exception-management/", + "skills/manim-video/", + "skills/market-research/", + "skills/mcp-server-patterns/", + "skills/messages-ops/", + "skills/nanoclaw-repl/", + "skills/nestjs-patterns/", + "skills/nodejs-keccak256/", + "skills/nutrient-document-processing/", + "skills/perl-patterns/", + "skills/perl-security/", + "skills/perl-testing/", + "skills/plankton-code-quality/", + "skills/postgres-patterns/", + "skills/product-capability/", + "skills/production-scheduling/", + "skills/project-flow-ops/", + "skills/prompt-optimizer/", + "skills/python-patterns/", + "skills/python-testing/", + "skills/quality-nonconformance/", + "skills/ralphinho-rfc-pipeline/", + "skills/regex-vs-llm-structured-text/", + "skills/remotion-video-creation/", + "skills/research-ops/", + "skills/returns-reverse-logistics/", + "skills/rust-patterns/", + "skills/rust-testing/", + "skills/search-first/", + "skills/security-bounty-hunter/", + "skills/security-review/", + "skills/security-scan/", + "skills/seo/", + "skills/skill-stocktake/", + "skills/social-graph-ranker/", + "skills/springboot-patterns/", + "skills/springboot-security/", + "skills/springboot-tdd/", + "skills/springboot-verification/", + "skills/strategic-compact/", + "skills/swift-actor-persistence/", + "skills/swift-concurrency-6-2/", + "skills/swift-protocol-di-testing/", + "skills/swiftui-patterns/", + "skills/tdd-workflow/", + "skills/team-builder/", + "skills/terminal-ops/", + "skills/token-budget-advisor/", + "skills/ui-demo/", + "skills/unified-notifications-ops/", + "skills/verification-loop/", + "skills/video-editing/", + "skills/videodb/", + "skills/visa-doc-translate/", + "skills/workspace-surface-audit/", + "skills/x-api/", + "the-security-guide.md" ], "bin": { "ecc": "scripts/ecc.js", @@ -120,7 +249,8 @@ "test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && node tests/run-all.js", "coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js", "build:opencode": "node scripts/build-opencode.js", - "prepack": "npm run build:opencode" + "prepack": "npm run build:opencode", + "dashboard": "python3 ./ecc_dashboard.py" }, "dependencies": { "@iarna/toml": "^2.2.5", @@ -141,4 +271,4 @@ "node": ">=18" }, "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c" -} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ee13baa0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,78 @@ +[project] +name = "llm-abstraction" +version = "0.1.0" +description = "Provider-agnostic LLM abstraction layer" +readme = "README.md" +requires-python = ">=3.11" +license = {text = "MIT"} +authors = [ + {name = "Affaan Mustafa", email = "affaan@example.com"} +] +keywords = ["llm", "openai", "anthropic", "ollama", "ai"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "anthropic>=0.25.0", + "openai>=1.30.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "pytest-cov>=4.1", + "pytest-mock>=3.12", + "ruff>=0.4", + "mypy>=1.10", +] + +[project.urls] +Homepage = "https://github.com/affaan-m/everything-claude-code" +Repository = "https://github.com/affaan-m/everything-claude-code" + +[project.scripts] +llm-select = "llm.cli.selector:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/llm"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" +filterwarnings = ["ignore::DeprecationWarning"] + +[tool.coverage.run] +source = ["src/llm"] +branch = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "raise NotImplementedError", +] + +[tool.ruff] +src-path = ["src"] +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "UP"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.11" +src_paths = ["src"] +warn_return_any = true +warn_unused_ignores = true diff --git a/scripts/harness-audit.js b/scripts/harness-audit.js index 6180eb48..4eb6b7b2 100644 --- a/scripts/harness-audit.js +++ b/scripts/harness-audit.js @@ -196,7 +196,9 @@ function findPluginInstall(rootDir) { ]; const candidateRoots = [ path.join(rootDir, '.claude', 'plugins'), + path.join(rootDir, '.claude', 'plugins', 'marketplaces'), homeDir && path.join(homeDir, '.claude', 'plugins'), + homeDir && path.join(homeDir, '.claude', 'plugins', 'marketplaces'), ].filter(Boolean); const candidates = candidateRoots.flatMap((pluginsDir) => pluginDirs.flatMap((pluginDir) => [ diff --git a/scripts/hooks/gateguard-fact-force.js b/scripts/hooks/gateguard-fact-force.js new file mode 100644 index 00000000..b8a0fcc8 --- /dev/null +++ b/scripts/hooks/gateguard-fact-force.js @@ -0,0 +1,265 @@ +#!/usr/bin/env node +/** + * PreToolUse Hook: GateGuard Fact-Forcing Gate + * + * Forces Claude to investigate before editing files or running commands. + * Instead of asking "are you sure?" (which LLMs always answer "yes"), + * this hook demands concrete facts: importers, public API, data schemas. + * + * The act of investigation creates awareness that self-evaluation never did. + * + * Gates: + * - Edit/Write: list importers, affected API, verify data schemas, quote instruction + * - Bash (destructive): list targets, rollback plan, quote instruction + * - Bash (routine): quote current instruction (once per session) + * + * Compatible with run-with-flags.js via module.exports.run(). + * Cross-platform (Windows, macOS, Linux). + * + * Full package with config support: pip install gateguard-ai + * Repo: https://github.com/zunoworks/gateguard + */ + +'use strict'; + +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +// Session state — scoped per session to avoid cross-session races. +// Uses CLAUDE_SESSION_ID (set by Claude Code) or falls back to PID-based isolation. +const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard'); +const SESSION_ID = process.env.CLAUDE_SESSION_ID || process.env.ECC_SESSION_ID || `pid-${process.ppid || process.pid}`; +const STATE_FILE = path.join(STATE_DIR, `state-${SESSION_ID.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`); + +// State expires after 30 minutes of inactivity +const SESSION_TIMEOUT_MS = 30 * 60 * 1000; + +// Maximum checked entries to prevent unbounded growth +const MAX_CHECKED_ENTRIES = 500; +const MAX_SESSION_KEYS = 50; +const ROUTINE_BASH_SESSION_KEY = '__bash_session__'; + +const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|git\s+clean\s+-f|drop\s+table|delete\s+from|truncate|git\s+push\s+--force|dd\s+if=)\b/i; + +// --- State management (per-session, atomic writes, bounded) --- + +function loadState() { + try { + if (fs.existsSync(STATE_FILE)) { + const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); + const lastActive = state.last_active || 0; + if (Date.now() - lastActive > SESSION_TIMEOUT_MS) { + try { fs.unlinkSync(STATE_FILE); } catch (_) { /* ignore */ } + return { checked: [], last_active: Date.now() }; + } + return state; + } + } catch (_) { /* ignore */ } + return { checked: [], last_active: Date.now() }; +} + +function pruneCheckedEntries(checked) { + if (checked.length <= MAX_CHECKED_ENTRIES) { + return checked; + } + + const preserved = checked.includes(ROUTINE_BASH_SESSION_KEY) ? [ROUTINE_BASH_SESSION_KEY] : []; + const sessionKeys = checked.filter(k => k.startsWith('__') && k !== ROUTINE_BASH_SESSION_KEY); + const fileKeys = checked.filter(k => !k.startsWith('__')); + const remainingSessionSlots = Math.max(MAX_SESSION_KEYS - preserved.length, 0); + const cappedSession = sessionKeys.slice(-remainingSessionSlots); + const remainingFileSlots = Math.max(MAX_CHECKED_ENTRIES - preserved.length - cappedSession.length, 0); + const cappedFiles = fileKeys.slice(-remainingFileSlots); + return [...preserved, ...cappedSession, ...cappedFiles]; +} + +function saveState(state) { + try { + state.last_active = Date.now(); + state.checked = pruneCheckedEntries(state.checked); + fs.mkdirSync(STATE_DIR, { recursive: true }); + // Atomic write: temp file + rename prevents partial reads + const tmpFile = STATE_FILE + '.tmp.' + process.pid; + fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), 'utf8'); + fs.renameSync(tmpFile, STATE_FILE); + } catch (_) { /* ignore */ } +} + +function markChecked(key) { + const state = loadState(); + if (!state.checked.includes(key)) { + state.checked.push(key); + saveState(state); + } +} + +function isChecked(key) { + const state = loadState(); + const found = state.checked.includes(key); + saveState(state); + return found; +} + +// Prune stale session files older than 1 hour +(function pruneStaleFiles() { + try { + const files = fs.readdirSync(STATE_DIR); + const now = Date.now(); + for (const f of files) { + if (!f.startsWith('state-') || !f.endsWith('.json')) continue; + const fp = path.join(STATE_DIR, f); + const stat = fs.statSync(fp); + if (now - stat.mtimeMs > SESSION_TIMEOUT_MS * 2) { + fs.unlinkSync(fp); + } + } + } catch (_) { /* ignore */ } +})(); + +// --- Sanitize file path against injection --- + +function sanitizePath(filePath) { + // Strip control chars (including null), bidi overrides, and newlines + return filePath.replace(/[\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, ' ').trim().slice(0, 500); +} + +// --- Gate messages --- + +function editGateMsg(filePath) { + const safe = sanitizePath(filePath); + return [ + '[Fact-Forcing Gate]', + '', + `Before editing ${safe}, present these facts:`, + '', + '1. List ALL files that import/require this file (use Grep)', + '2. List the public functions/classes affected by this change', + '3. If this file reads/writes data files, show field names, structure, and date format (use redacted or synthetic values, not raw production data)', + '4. Quote the user\'s current instruction verbatim', + '', + 'Present the facts, then retry the same operation.' + ].join('\n'); +} + +function writeGateMsg(filePath) { + const safe = sanitizePath(filePath); + return [ + '[Fact-Forcing Gate]', + '', + `Before creating ${safe}, present these facts:`, + '', + '1. Name the file(s) and line(s) that will call this new file', + '2. Confirm no existing file serves the same purpose (use Glob)', + '3. If this file reads/writes data files, show field names, structure, and date format (use redacted or synthetic values, not raw production data)', + '4. Quote the user\'s current instruction verbatim', + '', + 'Present the facts, then retry the same operation.' + ].join('\n'); +} + +function destructiveBashMsg() { + return [ + '[Fact-Forcing Gate]', + '', + 'Destructive command detected. Before running, present:', + '', + '1. List all files/data this command will modify or delete', + '2. Write a one-line rollback procedure', + '3. Quote the user\'s current instruction verbatim', + '', + 'Present the facts, then retry the same operation.' + ].join('\n'); +} + +function routineBashMsg() { + return [ + '[Fact-Forcing Gate]', + '', + 'Quote the user\'s current instruction verbatim.', + 'Then retry the same operation.' + ].join('\n'); +} + +// --- Deny helper --- + +function denyResult(reason) { + return { + stdout: JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: reason + } + }), + exitCode: 0 + }; +} + +// --- Core logic (exported for run-with-flags.js) --- + +function run(rawInput) { + let data; + try { + data = typeof rawInput === 'string' ? JSON.parse(rawInput) : rawInput; + } catch (_) { + return rawInput; // allow on parse error + } + + const rawToolName = data.tool_name || ''; + const toolInput = data.tool_input || {}; + // Normalize: case-insensitive matching via lookup map + const TOOL_MAP = { 'edit': 'Edit', 'write': 'Write', 'multiedit': 'MultiEdit', 'bash': 'Bash' }; + const toolName = TOOL_MAP[rawToolName.toLowerCase()] || rawToolName; + + if (toolName === 'Edit' || toolName === 'Write') { + const filePath = toolInput.file_path || ''; + if (!filePath) { + return rawInput; // allow + } + + if (!isChecked(filePath)) { + markChecked(filePath); + return denyResult(toolName === 'Edit' ? editGateMsg(filePath) : writeGateMsg(filePath)); + } + + return rawInput; // allow + } + + if (toolName === 'MultiEdit') { + const edits = toolInput.edits || []; + for (const edit of edits) { + const filePath = edit.file_path || ''; + if (filePath && !isChecked(filePath)) { + markChecked(filePath); + return denyResult(editGateMsg(filePath)); + } + } + return rawInput; // allow + } + + if (toolName === 'Bash') { + const command = toolInput.command || ''; + + if (DESTRUCTIVE_BASH.test(command)) { + // Gate destructive commands on first attempt; allow retry after facts presented + const key = '__destructive__' + crypto.createHash('sha256').update(command).digest('hex').slice(0, 16); + if (!isChecked(key)) { + markChecked(key); + return denyResult(destructiveBashMsg()); + } + return rawInput; // allow retry after facts presented + } + + if (!isChecked(ROUTINE_BASH_SESSION_KEY)) { + markChecked(ROUTINE_BASH_SESSION_KEY); + return denyResult(routineBashMsg()); + } + + return rawInput; // allow + } + + return rawInput; // allow +} + +module.exports = { run }; diff --git a/scripts/lib/install-targets/cursor-project.js b/scripts/lib/install-targets/cursor-project.js index 527ba2a6..03d19b1d 100644 --- a/scripts/lib/install-targets/cursor-project.js +++ b/scripts/lib/install-targets/cursor-project.js @@ -1,11 +1,23 @@ +const fs = require('fs'); const path = require('path'); const { createFlatRuleOperations, createInstallTargetAdapter, + createManagedOperation, isForeignPlatformPath, } = require('./helpers'); +function toCursorRuleFileName(fileName, sourceRelativeFile) { + if (path.basename(sourceRelativeFile).toLowerCase() === 'readme.md') { + return null; + } + + return fileName.endsWith('.md') + ? `${fileName.slice(0, -3)}.mdc` + : fileName; +} + module.exports = createInstallTargetAdapter({ id: 'cursor-project', target: 'cursor', @@ -17,6 +29,7 @@ module.exports = createInstallTargetAdapter({ const modules = Array.isArray(input.modules) ? input.modules : (input.module ? [input.module] : []); + const seenDestinationPaths = new Set(); const { repoRoot, projectRoot, @@ -28,23 +41,98 @@ module.exports = createInstallTargetAdapter({ homeDir, }; const targetRoot = adapter.resolveRoot(planningInput); - - return modules.flatMap(module => { + const entries = modules.flatMap((module, moduleIndex) => { const paths = Array.isArray(module.paths) ? module.paths : []; return paths .filter(p => !isForeignPlatformPath(p, adapter.target)) - .flatMap(sourceRelativePath => { - if (sourceRelativePath === 'rules') { - return createFlatRuleOperations({ - moduleId: module.id, - repoRoot, - sourceRelativePath, - destinationDir: path.join(targetRoot, 'rules'), - }); - } + .map((sourceRelativePath, pathIndex) => ({ + module, + sourceRelativePath, + moduleIndex, + pathIndex, + })); + }).sort((left, right) => { + const getPriority = value => { + if (value === 'rules') { + return 0; + } - return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; + if (value === '.cursor') { + return 1; + } + + return 2; + }; + + const leftPriority = getPriority(left.sourceRelativePath); + const rightPriority = getPriority(right.sourceRelativePath); + if (leftPriority !== rightPriority) { + return leftPriority - rightPriority; + } + + if (left.moduleIndex !== right.moduleIndex) { + return left.moduleIndex - right.moduleIndex; + } + + return left.pathIndex - right.pathIndex; + }); + + function takeUniqueOperations(operations) { + return operations.filter(operation => { + if (!operation || !operation.destinationPath) { + return false; + } + + if (seenDestinationPaths.has(operation.destinationPath)) { + return false; + } + + seenDestinationPaths.add(operation.destinationPath); + return true; + }); + } + + return entries.flatMap(({ module, sourceRelativePath }) => { + if (sourceRelativePath === 'rules') { + return takeUniqueOperations(createFlatRuleOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath, + destinationDir: path.join(targetRoot, 'rules'), + destinationNameTransform: toCursorRuleFileName, + })); + } + + if (sourceRelativePath === '.cursor') { + const cursorRoot = path.join(repoRoot, '.cursor'); + if (!fs.existsSync(cursorRoot) || !fs.statSync(cursorRoot).isDirectory()) { + return []; + } + + const childOperations = fs.readdirSync(cursorRoot, { withFileTypes: true }) + .sort((left, right) => left.name.localeCompare(right.name)) + .filter(entry => entry.name !== 'rules') + .map(entry => createManagedOperation({ + moduleId: module.id, + sourceRelativePath: path.join('.cursor', entry.name), + destinationPath: path.join(targetRoot, entry.name), + strategy: 'preserve-relative-path', + })); + + const ruleOperations = createFlatRuleOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath: '.cursor/rules', + destinationDir: path.join(targetRoot, 'rules'), + destinationNameTransform: toCursorRuleFileName, }); + + return takeUniqueOperations([...childOperations, ...ruleOperations]); + } + + return takeUniqueOperations([ + adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput), + ]); }); }, }); diff --git a/scripts/lib/install-targets/helpers.js b/scripts/lib/install-targets/helpers.js index fd959aa7..a39506e1 100644 --- a/scripts/lib/install-targets/helpers.js +++ b/scripts/lib/install-targets/helpers.js @@ -181,7 +181,13 @@ function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePat return operations; } -function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, destinationDir }) { +function createFlatRuleOperations({ + moduleId, + repoRoot, + sourceRelativePath, + destinationDir, + destinationNameTransform, +}) { const normalizedSourcePath = normalizeRelativePath(sourceRelativePath); const sourceRoot = path.join(repoRoot || '', normalizedSourcePath); @@ -201,19 +207,33 @@ function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, dest if (entry.isDirectory()) { const relativeFiles = listRelativeFiles(entryPath); for (const relativeFile of relativeFiles) { - const flattenedFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`; + const defaultFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`; + const sourceRelativeFile = path.join(normalizedSourcePath, namespace, relativeFile); + const flattenedFileName = typeof destinationNameTransform === 'function' + ? destinationNameTransform(defaultFileName, sourceRelativeFile) + : defaultFileName; + if (!flattenedFileName) { + continue; + } operations.push(createManagedOperation({ moduleId, - sourceRelativePath: path.join(normalizedSourcePath, namespace, relativeFile), + sourceRelativePath: sourceRelativeFile, destinationPath: path.join(destinationDir, flattenedFileName), strategy: 'flatten-copy', })); } } else if (entry.isFile()) { + const sourceRelativeFile = path.join(normalizedSourcePath, entry.name); + const destinationFileName = typeof destinationNameTransform === 'function' + ? destinationNameTransform(entry.name, sourceRelativeFile) + : entry.name; + if (!destinationFileName) { + continue; + } operations.push(createManagedOperation({ moduleId, - sourceRelativePath: path.join(normalizedSourcePath, entry.name), - destinationPath: path.join(destinationDir, entry.name), + sourceRelativePath: sourceRelativeFile, + destinationPath: path.join(destinationDir, destinationFileName), strategy: 'flatten-copy', })); } diff --git a/scripts/release.sh b/scripts/release.sh index d2694623..7fdbf082 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -18,6 +18,7 @@ CODEX_MARKETPLACE_JSON=".agents/plugins/marketplace.json" CODEX_PLUGIN_JSON=".codex-plugin/plugin.json" OPENCODE_PACKAGE_JSON=".opencode/package.json" OPENCODE_PACKAGE_LOCK_JSON=".opencode/package-lock.json" +OPENCODE_ECC_HOOKS_PLUGIN=".opencode/plugins/ecc-hooks.ts" README_FILE="README.md" ZH_CN_README_FILE="docs/zh-CN/README.md" SELECTIVE_INSTALL_ARCHITECTURE_DOC="docs/SELECTIVE-INSTALL-ARCHITECTURE.md" @@ -55,7 +56,7 @@ if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then fi # Verify versioned manifests exist -for FILE in "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"; do +for FILE in "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$OPENCODE_ECC_HOOKS_PLUGIN" "$README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"; do if [[ ! -f "$FILE" ]]; then echo "Error: $FILE not found" exit 1 @@ -217,6 +218,24 @@ update_codex_marketplace_version() { ' "$CODEX_MARKETPLACE_JSON" "$VERSION" } +update_opencode_hook_banner_version() { + node -e ' + const fs = require("fs"); + const file = process.argv[1]; + const version = process.argv[2]; + const current = fs.readFileSync(file, "utf8"); + const updated = current.replace( + /(## Active Plugin: Everything Claude Code v)[0-9]+\.[0-9]+\.[0-9]+/, + `$1${version}` + ); + if (updated === current) { + console.error(`Error: could not update OpenCode hook banner version in ${file}`); + process.exit(1); + } + fs.writeFileSync(file, updated); + ' "$OPENCODE_ECC_HOOKS_PLUGIN" "$VERSION" +} + # Update all shipped package/plugin manifests update_version "$ROOT_PACKAGE_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" update_package_lock_version "$PACKAGE_LOCK_JSON" @@ -231,6 +250,7 @@ update_codex_marketplace_version update_version "$CODEX_PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" update_version "$OPENCODE_PACKAGE_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" update_package_lock_version "$OPENCODE_PACKAGE_LOCK_JSON" +update_opencode_hook_banner_version update_readme_version_row "$README_FILE" "Version" "Plugin" "Plugin" "Reference config" update_readme_version_row "$ZH_CN_README_FILE" "版本" "插件" "插件" "参考配置" update_selective_install_repo_version "$SELECTIVE_INSTALL_ARCHITECTURE_DOC" @@ -243,7 +263,7 @@ node tests/scripts/build-opencode.test.js node tests/plugin-manifest.test.js # Stage, commit, tag, and push -git add "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC" +git add "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$OPENCODE_ECC_HOOKS_PLUGIN" "$README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC" git commit -m "chore: bump plugin version to $VERSION" git tag "v$VERSION" git push origin main "v$VERSION" diff --git a/skills/accessibility/SKILL.md b/skills/accessibility/SKILL.md new file mode 100644 index 00000000..c9021041 --- /dev/null +++ b/skills/accessibility/SKILL.md @@ -0,0 +1,146 @@ +--- +name: accessibility +description: Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA + standards. Use this skill to generate semantic ARIA for Web and accessibility traits for Web and Native platforms (iOS/Android). +origin: ECC +--- + +# Accessibility (WCAG 2.2) + +This skill ensures that digital interfaces are Perceivable, Operable, Understandable, and Robust (POUR) for all users, including those using screen readers, switch controls, or keyboard navigation. It focuses on the technical implementation of WCAG 2.2 success criteria. + +## When to Use + +- Defining UI component specifications for Web, iOS, or Android. +- Auditing existing code for accessibility barriers or compliance gaps. +- Implementing new WCAG 2.2 standards like Target Size (Minimum) and Focus Appearance. +- Mapping high-level design requirements to technical attributes (ARIA roles, traits, hints). + +## Core Concepts + +- **POUR Principles**: The foundation of WCAG (Perceivable, Operable, Understandable, Robust). +- **Semantic Mapping**: Using native elements over generic containers to provide built-in accessibility. +- **Accessibility Tree**: The representation of the UI that assistive technologies actually "read." +- **Focus Management**: Controlling the order and visibility of the keyboard/screen reader cursor. +- **Labeling & Hints**: Providing context through `aria-label`, `accessibilityLabel`, and `contentDescription`. + +## How It Works + +### Step 1: Identify the Component Role + +Determine the functional purpose (e.g., Is this a button, a link, or a tab?). Use the most semantic native element available before resorting to custom roles. + +### Step 2: Define Perceivable Attributes + +- Ensure text contrast meets **4.5:1** (normal) or **3:1** (large/UI). +- Add text alternatives for non-text content (images, icons). +- Implement responsive reflow (up to 400% zoom without loss of function). + +### Step 3: Implement Operable Controls + +- Ensure a minimum **24x24 CSS pixel** target size (WCAG 2.2 SC 2.5.8). +- Verify all interactive elements are reachable via keyboard and have a visible focus indicator (SC 2.4.11). +- Provide single-pointer alternatives for dragging movements. + +### Step 4: Ensure Understandable Logic + +- Use consistent navigation patterns. +- Provide descriptive error messages and suggestions for correction (SC 3.3.3). +- Implement "Redundant Entry" (SC 3.3.7) to prevent asking for the same data twice. + +### Step 5: Verify Robust Compatibility + +- Use correct `Name, Role, Value` patterns. +- Implement `aria-live` or live regions for dynamic status updates. + +## Accessibility Architecture Diagram + +```mermaid +flowchart TD + UI["UI Component"] --> Platform{Platform?} + Platform -->|Web| ARIA["WAI-ARIA + HTML5"] + Platform -->|iOS| SwiftUI["Accessibility Traits + Labels"] + Platform -->|Android| Compose["Semantics + ContentDesc"] + + ARIA --> AT["Assistive Technology (Screen Readers, Switches)"] + SwiftUI --> AT + Compose --> AT +``` + +## Cross-Platform Mapping + +| Feature | Web (HTML/ARIA) | iOS (SwiftUI) | Android (Compose) | +| :----------------- | :----------------------- | :----------------------------------- | :---------------------------------------------------------- | +| **Primary Label** | `aria-label` / `