diff --git a/.agents/README.md b/.agents/README.md index cfec76c9e..c346d121d 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -2,12 +2,18 @@ https://agentskills.io/ -Common skills share for various platforms. Symlink used to link skills for `.claude/skills`. The symlinks are source controlled by default. +Common skills and hooks shared across agent platforms. Symlinks are source controlled by default. ```shell # symlink skills ln -sfn ../.agents/skills .claude/skills +# hooks +# .claude/settings.json references .agents/hooks directly +# .codex/config.toml enables hooks and .codex/hooks.json references .agents/hooks +# Hooks resolve the project root from AGENTS_PROJECT_DIR, agent-specific env vars, +# hook JSON cwd fields, the hook script location, or git. + # symlink context ln -s AGENTS.md CLAUDE.md ``` diff --git a/.agents/hooks/lib/project-root.sh b/.agents/hooks/lib/project-root.sh new file mode 100644 index 000000000..bdcc00ef7 --- /dev/null +++ b/.agents/hooks/lib/project-root.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash + +normalize_dir() { + local dir="$1" + + if [[ -z "$dir" || ! -d "$dir" ]]; then + return 1 + fi + + (cd "$dir" 2>/dev/null && pwd -P) +} + +looks_like_project_root() { + local dir="$1" + + [[ -d "$dir/.git" || -f "$dir/.git" || -f "$dir/AGENTS.md" || -f "$dir/pnpm-workspace.yaml" || -d "$dir/.agents" ]] +} + +candidate_from_json() { + local input="$1" + + if [[ -z "$input" ]] || ! command -v jq >/dev/null 2>&1; then + return 1 + fi + + jq -r '.cwd // .workspace_root // .workspaceRoot // .project_dir // .projectDir // empty' <<<"$input" 2>/dev/null +} + +resolve_project_root() { + local input="${1:-}" + local hooks_dir="${2:-}" + local candidate normalized json_candidate + + json_candidate=$(candidate_from_json "$input" || true) + + for candidate in \ + "${AGENTS_PROJECT_DIR:-}" \ + "${CLAUDE_PROJECT_DIR:-}" \ + "${CODEX_PROJECT_DIR:-}" \ + "${CODEX_WORKSPACE_ROOT:-}" \ + "${WORKSPACE_ROOT:-}" \ + "${PROJECT_DIR:-}" \ + "$json_candidate" \ + "${PWD:-}"; do + normalized=$(normalize_dir "$candidate") || continue + if looks_like_project_root "$normalized"; then + printf '%s\n' "$normalized" + return 0 + fi + done + + if [[ -n "$hooks_dir" ]]; then + normalized=$(normalize_dir "$hooks_dir/../..") || true + if [[ -n "${normalized:-}" ]] && looks_like_project_root "$normalized"; then + printf '%s\n' "$normalized" + return 0 + fi + fi + + candidate=$(git rev-parse --show-toplevel 2>/dev/null || true) + normalized=$(normalize_dir "$candidate") || true + if [[ -n "${normalized:-}" ]]; then + printf '%s\n' "$normalized" + return 0 + fi + + return 1 +} + +resolve_hook_path() { + local project_root="$1" + local path="$2" + + if [[ -z "$path" ]]; then + return 1 + fi + + case "$path" in + /*) printf '%s\n' "$path" ;; + *) printf '%s/%s\n' "$project_root" "$path" ;; + esac +} + +hook_relative_path() { + local project_root="$1" + local path="$2" + + case "$path" in + "$project_root"/*) printf '%s\n' "${path#"$project_root"/}" ;; + *) printf '%s\n' "$path" ;; + esac +} + +hook_command_from_input() { + local input="$1" + + if [[ -z "$input" ]] || ! command -v jq >/dev/null 2>&1; then + return 1 + fi + + jq -r '.tool_input.command // .command // empty' <<<"$input" 2>/dev/null +} + +hook_file_paths_from_input() { + local input="$1" + local command + + if [[ -n "$input" ]] && command -v jq >/dev/null 2>&1; then + jq -r ' + [ + .tool_input.file_path?, + .tool_input.filePath?, + .tool_input.path?, + .file_path?, + .filePath?, + .path? + ] + | .[] + | select(type == "string" and length > 0) + ' <<<"$input" 2>/dev/null || true + fi + + command=$(hook_command_from_input "$input" || true) + if [[ -n "$command" ]]; then + awk ' + /^\*\*\* (Add|Update|Delete) File: / { + sub(/^\*\*\* (Add|Update|Delete) File: /, "") + print + } + /^\*\*\* Move to: / { + sub(/^\*\*\* Move to: /, "") + print + } + ' <<<"$command" + fi +} diff --git a/.agents/hooks/notification.sh b/.agents/hooks/notification.sh new file mode 100755 index 000000000..467305cdd --- /dev/null +++ b/.agents/hooks/notification.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$(uname -s)" == "Darwin" ]] && command -v osascript >/dev/null 2>&1; then + osascript -e 'display notification "Agent needs your attention" with title "Agent"' +fi diff --git a/.agents/hooks/post-tool-use-edit-write.sh b/.agents/hooks/post-tool-use-edit-write.sh new file mode 100755 index 000000000..123d4da6a --- /dev/null +++ b/.agents/hooks/post-tool-use-edit-write.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +set -euo pipefail + +INPUT=$(cat) +HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$HOOK_DIR/lib/project-root.sh" +PROJECT_ROOT=$(resolve_project_root "$INPUT" "$HOOK_DIR") || exit 0 +FILE_PATHS=$(hook_file_paths_from_input "$INPUT") + +if [[ -z "$FILE_PATHS" ]]; then + exit 0 +fi + +FAILED=0 + +mark_failed() { + FAILED=1 +} + +run_prettier() { + local output exit_code + + output=$(cd "$PROJECT_ROOT" && pnpm exec prettier --write --ignore-unknown --no-error-on-unmatched-pattern "$FILE_PATH" 2>&1) || exit_code=$? + + if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then + echo "$output" >&2 + mark_failed + fi +} + +run_eslint() { + case "$FILE_PATH" in + *.ts|*.js|*.css) ;; + *) return 0 ;; + esac + + case "$FILE_PATH" in + */dist/*|*/node_modules/*|*/__screenshots__/*|*/generated/*) return 0 ;; + esac + + local dir project_dir rel_path json_output hard_errors total_errors readable + + dir=$(dirname "$FILE_PATH") + project_dir="" + while [[ "$dir" != "/" && "$dir" != "." ]]; do + if [[ -f "$dir/eslint.config.js" ]]; then + project_dir="$dir" + break + fi + dir=$(dirname "$dir") + done + + if [[ -z "$project_dir" ]]; then + return 0 + fi + + rel_path=$(hook_relative_path "$project_dir" "$FILE_PATH") + + local soft_rules="no-unused-vars|@typescript-eslint/no-unused-vars" + + json_output=$(cd "$project_dir" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --cache --cache-location .eslintcache/ --format json "$rel_path" 2>/dev/null) || true + + hard_errors=$(echo "$json_output" | jq -r --arg soft "$soft_rules" ' + [.[].messages[] | select(.severity == 2) | select(.ruleId | test($soft) | not)] | length + ') 2>/dev/null || hard_errors="0" + + total_errors=$(echo "$json_output" | jq -r ' + [.[].messages[] | select(.severity == 2)] | length + ') 2>/dev/null || total_errors="0" + + if [[ "$total_errors" == "0" ]]; then + return 0 + fi + + readable=$(cd "$project_dir" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --color --cache --cache-location .eslintcache/ "$rel_path" 2>&1) || true + + if [[ "$hard_errors" != "0" ]]; then + echo "$readable" >&2 + mark_failed + else + echo "$readable" >&2 + fi +} + +run_vale() { + case "$FILE_PATH" in + *.md|*.ts) ;; + *) return 0 ;; + esac + + case "$FILE_PATH" in + *.test.*|*/starters/*|*/404/*|*/vendor/*|*/changelog/*|*/icons/*|*/generated/*|*/dist/*|*/LICENSE*|*/CHANGELOG*|*/NOTICE*) return 0 ;; + esac + + case "$FILE_PATH" in + */.claude/plans/*|*/.claude/projects/*) return 0 ;; + esac + + local output exit_code + + output=$(cd "$PROJECT_ROOT" && config/vale/bin/vale --config .vale.ini "$FILE_PATH" 2>&1) || exit_code=$? + + if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then + echo "$output" >&2 + mark_failed + fi +} + +run_stylelint() { + case "$FILE_PATH" in + *.css) ;; + *) return 0 ;; + esac + + case "$FILE_PATH" in + */dist/*|*/node_modules/*|*/vendor/*) return 0 ;; + esac + + local repo_root dir project_dir rel_path output exit_code + + repo_root="$PROJECT_ROOT" + dir=$(dirname "$FILE_PATH") + project_dir="" + while [[ "$dir" != "/" && "$dir" != "." ]]; do + if [[ -f "$dir/package.json" ]] && jq -e '.wireit["lint:style"]' "$dir/package.json" >/dev/null 2>&1; then + project_dir="$dir" + break + fi + dir=$(dirname "$dir") + done + + if [[ -z "$project_dir" ]]; then + return 0 + fi + + rel_path=$(hook_relative_path "$project_dir" "$FILE_PATH") + + output=$(cd "$project_dir" && pnpm exec stylelint --config="$repo_root/stylelint.config.mjs" --color "$rel_path" 2>&1) || exit_code=$? + + if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then + echo "$output" >&2 + mark_failed + fi +} + +while IFS= read -r FILE_PATH; do + if [[ -z "$FILE_PATH" ]]; then + continue + fi + + FILE_PATH=$(resolve_hook_path "$PROJECT_ROOT" "$FILE_PATH") + + if [[ ! -e "$FILE_PATH" || -d "$FILE_PATH" ]]; then + continue + fi + + run_prettier + run_eslint + run_vale + run_stylelint +done <<<"$FILE_PATHS" + +if [[ "$FAILED" -ne 0 ]]; then + exit 2 +fi + +exit 0 diff --git a/.claude/hooks/guard-destructive.sh b/.agents/hooks/pre-tool-use-bash.sh similarity index 89% rename from .claude/hooks/guard-destructive.sh rename to .agents/hooks/pre-tool-use-bash.sh index 43082bf7a..6e6440847 100755 --- a/.claude/hooks/guard-destructive.sh +++ b/.agents/hooks/pre-tool-use-bash.sh @@ -2,7 +2,9 @@ set -euo pipefail INPUT=$(cat) -COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') +HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$HOOK_DIR/lib/project-root.sh" +COMMAND=$(hook_command_from_input "$INPUT" || true) # Exit early if not a git command if [[ -z "$COMMAND" ]] || ! echo "$COMMAND" | grep -qE '^\s*git\s'; then diff --git a/.agents/hooks/pre-tool-use-edit-write.sh b/.agents/hooks/pre-tool-use-edit-write.sh new file mode 100755 index 000000000..38e289308 --- /dev/null +++ b/.agents/hooks/pre-tool-use-edit-write.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +INPUT=$(cat) +HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$HOOK_DIR/lib/project-root.sh" +PROJECT_ROOT=$(resolve_project_root "$INPUT" "$HOOK_DIR") || exit 0 +FILE_PATHS=$(hook_file_paths_from_input "$INPUT") + +# Exit early if no file path +if [[ -z "$FILE_PATHS" ]]; then + exit 0 +fi + +# Protected slow-layer infrastructure files +PROTECTED_FILES=( + "pnpm-workspace.yaml" + "commitlint.config.js" + "release.config.cjs" + "package.json" + "pnpm-lock.yaml" + ".nvmrc" + ".husky" + "config" +) + +while IFS= read -r FILE_PATH; do + if [[ -z "$FILE_PATH" ]]; then + continue + fi + + FILE_PATH=$(resolve_hook_path "$PROJECT_ROOT" "$FILE_PATH") + + # Resolve to a path relative to the project directory for consistent matching + REL_PATH=$(hook_relative_path "$PROJECT_ROOT" "$FILE_PATH") + + # Check if the file is a root-level protected file + BASENAME=$(basename "$REL_PATH") + DIRNAME=$(dirname "$REL_PATH") + + for PROTECTED in "${PROTECTED_FILES[@]}"; do + # Only protect root-level files (dirname is . or matches project dir) + if [[ "$BASENAME" == "$PROTECTED" && ("$DIRNAME" == "." || "$FILE_PATH" == "$PROJECT_ROOT/$PROTECTED") ]]; then + echo "BLOCKED: '$PROTECTED' is a critical infrastructure file (slow-layer)." >&2 + echo "These files affect the entire monorepo and should only be modified when the user explicitly requests it." >&2 + echo "If the user has asked for this change, re-run the command to confirm." >&2 + exit 2 + fi + + if [[ -d "$PROJECT_ROOT/$PROTECTED" && "$REL_PATH" == "$PROTECTED"/* ]]; then + echo "BLOCKED: '$PROTECTED' is a critical infrastructure file (slow-layer)." >&2 + echo "These files affect the entire monorepo and should only be modified when the user explicitly requests it." >&2 + echo "If the user has asked for this change, re-run the command to confirm." >&2 + exit 2 + fi + done +done <<<"$FILE_PATHS" + +exit 0 diff --git a/.claude/hooks/session-start.sh b/.agents/hooks/session-start.sh similarity index 71% rename from .claude/hooks/session-start.sh rename to .agents/hooks/session-start.sh index 7a6a3c5fa..b28e70d9d 100755 --- a/.claude/hooks/session-start.sh +++ b/.agents/hooks/session-start.sh @@ -2,6 +2,12 @@ set -euo pipefail INPUT=$(cat) +HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$HOOK_DIR/lib/project-root.sh" +PROJECT_ROOT=$(resolve_project_root "$INPUT" "$HOOK_DIR") || { + echo "Could not resolve project root." >&2 + exit 0 +} NVM_DIR="${NVM_DIR:-$HOME/.nvm}" if [[ ! -s "$NVM_DIR/nvm.sh" ]]; then @@ -11,7 +17,7 @@ fi source "$NVM_DIR/nvm.sh" -cd "$CLAUDE_PROJECT_DIR" +cd "$PROJECT_ROOT" nvm install 2>&1 >/dev/null corepack enable 2>&1 >/dev/null diff --git a/.claude/hooks/stop.sh b/.agents/hooks/stop.sh similarity index 56% rename from .claude/hooks/stop.sh rename to .agents/hooks/stop.sh index c5615a70c..d2e8955e0 100755 --- a/.claude/hooks/stop.sh +++ b/.agents/hooks/stop.sh @@ -1,10 +1,25 @@ #!/usr/bin/env bash set -euo pipefail -cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || cd "$(git rev-parse --show-toplevel)" 2>/dev/null || exit 0 +INPUT=$(cat) +HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +source "$HOOK_DIR/lib/project-root.sh" +PROJECT_ROOT=$(resolve_project_root "$INPUT" "$HOOK_DIR") || { + echo "Could not resolve project root." >&2 + exit 1 +} +IS_CODEX_STOP=$(jq -r 'if (.hook_event_name == "Stop" and (has("turn_id") or has("stop_hook_active") or has("last_assistant_message"))) then "1" else "0" end' <<<"$INPUT" 2>/dev/null || echo "0") + +emit_success() { + if [[ "$IS_CODEX_STOP" != "1" ]]; then + echo "$1" + fi +} + +cd "$PROJECT_ROOT" CHANGED=$(git diff --name-only HEAD 2>/dev/null || true) if [[ -z "$CHANGED" ]]; then - echo "No changed files. Skipping tests." + emit_success "No changed files. Skipping tests." exit 0 fi @@ -33,5 +48,5 @@ if [[ ${#FAILED[@]} -gt 0 ]]; then echo "Tests failed in: ${FAILED[*]}" >&2 exit 2 else - echo "All checks passed." + emit_success "All checks passed." fi diff --git a/.agents/skills/accessibility/SKILL.md b/.agents/skills/accessibility/SKILL.md index 3e3547d35..a954ae8e2 100644 --- a/.agents/skills/accessibility/SKILL.md +++ b/.agents/skills/accessibility/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-accessibility +name: accessibility description: Unified accessibility auditing workflow across static analysis, runtime testing, ARIA patterns, keyboard navigation, and color contrast. Use this skill whenever the user mentions accessibility, a11y, WCAG, ARIA roles, axe tests, screen readers, focus management, keyboard navigation, color contrast, or wants to audit, verify, or fix accessibility on any component. Also use when writing or debugging .test.axe.ts files, checking tabindex management, or reviewing focus trapping behavior. user_invocable: true --- diff --git a/.agents/skills/api-design/SKILL.md b/.agents/skills/api-design/SKILL.md index f1c6fb577..b4bbe9eca 100644 --- a/.agents/skills/api-design/SKILL.md +++ b/.agents/skills/api-design/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-api-design +name: api-design description: Component API design patterns following Elements conventions for properties, attributes, CSS custom properties, slots, and events. Use this skill whenever the user is designing or deciding on a component API, choosing between properties vs attributes vs slots, naming CSS custom properties, designing events, avoiding impossible states, deciding whether to reflect to an attribute, using CSS Parts, implementing the DataElement interface, or applying the internal-host pattern. Also trigger when the user asks about shorthand vs granular CSS properties or event naming conventions. --- diff --git a/.agents/skills/build-system/SKILL.md b/.agents/skills/build-system/SKILL.md index cf77c884b..e880820dc 100644 --- a/.agents/skills/build-system/SKILL.md +++ b/.agents/skills/build-system/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-build-system +name: build-system description: Wireit orchestration, build optimization, caching strategies, and build troubleshooting for the Elements monorepo. Use this skill whenever the user asks about Wireit configuration, build tasks, build dependencies, cache invalidation, build performance, adding new build tasks to package.json, cross-package dependency patterns, sideEffects declarations, CI pipeline configuration, or diagnosing slow or failing builds. Also trigger when the user mentions wireit, vite build issues, pnpm run ci, or build output problems. --- diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md index f4b405d84..df911d2e7 100644 --- a/.agents/skills/code-review/SKILL.md +++ b/.agents/skills/code-review/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-code-review +name: code-review description: Comprehensive code review process for Elements monorepo changes. Provides structured feedback on type safety, testing, documentation, and adherence to project guidelines. Use this skill whenever the user asks you to review code, check staged changes, look at a diff, give feedback before committing, review a PR or merge request, or evaluate code quality. Trigger on phrases like "review my changes," "check my code," "give me feedback," "look at my staged files," "review this PR," or "before I commit." --- diff --git a/.agents/skills/component-creation/SKILL.md b/.agents/skills/component-creation/SKILL.md index d219045af..a4df0ac36 100644 --- a/.agents/skills/component-creation/SKILL.md +++ b/.agents/skills/component-creation/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-component-creation +name: component-creation description: Guide for creating new Elements components with all required files, base classes, metadata patterns, and test boilerplate. Use this skill whenever the user wants to create, scaffold, or set up a new component from scratch, needs to understand the required 10-file structure, asks about base classes (LitElement vs BaseButton), define.ts vs index.ts patterns, static metadata, component registration, or sub-component parent relationships. Also trigger when the user mentions creating test boilerplate for all 5 test types. --- diff --git a/.agents/skills/documentation/SKILL.md b/.agents/skills/documentation/SKILL.md index 2eee7daef..36570f051 100644 --- a/.agents/skills/documentation/SKILL.md +++ b/.agents/skills/documentation/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-documentation +name: documentation description: Guidelines for writing documentation files including 11ty templates, Eleventy shortcodes, JSDoc annotations, and markdown content. Use this skill whenever the user works with documentation markdown files, Vale prose linting errors, Eleventy shortcodes (dodont, example), frontmatter, JSDoc annotations that must pass Vale, or the documentation site. Also trigger when the user mentions Vale errors, adding terms to the vocabulary, suppressing Vale rules, or writing and fixing prose in .md or .ts files. --- diff --git a/.agents/skills/examples/SKILL.md b/.agents/skills/examples/SKILL.md index d31721281..f931248d6 100644 --- a/.agents/skills/examples/SKILL.md +++ b/.agents/skills/examples/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-examples +name: examples description: Guidelines for writing example template files (*.examples.ts) including naming conventions, @summary JSDoc comments, and stateless HTML patterns. Use this skill whenever the user creates, modifies, or reviews *.examples.ts files, asks about example naming rules (PascalCase, 3-word max, no component prefix), needs to write or improve @summary JSDoc comments, encounters example-naming ESLint errors, or wants to add stateless or interactive examples to a component's example file. --- diff --git a/.agents/skills/pattern-creation/SKILL.md b/.agents/skills/pattern-creation/SKILL.md index b4b87fe91..b210664f7 100644 --- a/.agents/skills/pattern-creation/SKILL.md +++ b/.agents/skills/pattern-creation/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-pattern-creation +name: pattern-creation description: Convert validated playground templates or HTML compositions into reusable pattern files (*.examples.ts) in the pattern library. Use this skill whenever the user wants to save, persist, store, or catalog a template, prototype, composition, or playground result as a reusable pattern. Trigger on phrases like "save as pattern," "create pattern," "add to pattern library," "persist this template," "convert to examples.ts," or when the user has validated HTML and wants it stored in projects/internals/patterns/src/. --- diff --git a/.agents/skills/testing/SKILL.md b/.agents/skills/testing/SKILL.md index 6bd155db8..de01a79ee 100644 --- a/.agents/skills/testing/SKILL.md +++ b/.agents/skills/testing/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-testing +name: testing description: Write and run automated tests for Elements components including unit, accessibility, visual, SSR, and lighthouse tests. Use this skill whenever the user wants to write, create, update, or debug test files (.test.ts, .test.axe.ts, .test.visual.ts, .test.ssr.ts, .test.lighthouse.ts). Also trigger when the user asks about createFixture, removeFixture, elementIsStable, emulateClick, untilEvent, runAxe, visual baselines, theme testing, or test structure patterns like describe block naming. --- diff --git a/.agents/skills/troubleshooting/SKILL.md b/.agents/skills/troubleshooting/SKILL.md index d1f9f9b7c..edd122d5f 100644 --- a/.agents/skills/troubleshooting/SKILL.md +++ b/.agents/skills/troubleshooting/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-troubleshooting +name: troubleshooting description: Diagnose and resolve common issues including test failures, build errors, performance regressions, and development environment problems. Use this skill whenever the user reports something broken, failing, timing out, hanging, or producing unexpected results. Trigger on test failures (elementIsStable timeouts, flaky tests, screenshot diffs), build errors (Cannot find module, wireit cache issues, stale output), CI/CD failures, Git LFS problems, port conflicts, lighthouse score regressions, SSR errors, or environment setup issues. --- diff --git a/.agents/skills/typescript/SKILL.md b/.agents/skills/typescript/SKILL.md index 4f029f383..a6cdb2627 100644 --- a/.agents/skills/typescript/SKILL.md +++ b/.agents/skills/typescript/SKILL.md @@ -1,5 +1,5 @@ --- -name: repo-typescript +name: typescript description: Best practices for TypeScript code including type safety, discriminated unions, type guards, and exhaustive checking. Use this skill whenever the user asks about TypeScript patterns, type safety, type assertions (as any, as unknown), non-null assertions, discriminated unions, exhaustive switch/never patterns, type guards, boolean trap parameters, race conditions in async code, memory leaks from closures, or sequential vs parallel async operations. Also trigger when reviewing TypeScript code for unsafe patterns or refactoring types. --- diff --git a/.claude/hooks/format-prettier.sh b/.claude/hooks/format-prettier.sh deleted file mode 100755 index 71bdf3a10..000000000 --- a/.claude/hooks/format-prettier.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') - -# Exit early if no file path -if [[ -z "$FILE_PATH" ]]; then - exit 0 -fi - -# Run Prettier on the specific file -OUTPUT=$(cd "$CLAUDE_PROJECT_DIR" && pnpm exec prettier --write --ignore-unknown --no-error-on-unmatched-pattern "$FILE_PATH" 2>&1) || EXIT_CODE=$? - -if [[ ${EXIT_CODE:-0} -ne 0 && -n "$OUTPUT" ]]; then - echo "$OUTPUT" >&2 - exit 2 -fi - -exit 0 diff --git a/.claude/hooks/guard-critical-files.sh b/.claude/hooks/guard-critical-files.sh deleted file mode 100755 index a1adcec83..000000000 --- a/.claude/hooks/guard-critical-files.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') - -# Exit early if no file path -if [[ -z "$FILE_PATH" ]]; then - exit 0 -fi - -# Resolve to a path relative to the project directory for consistent matching -REL_PATH="${FILE_PATH#"$CLAUDE_PROJECT_DIR"/}" - -# Protected slow-layer infrastructure files -PROTECTED_FILES=( - "pnpm-workspace.yaml" - "commitlint.config.js" - "release.config.cjs" - "package.json" - "pnpm-lock.yaml" - ".nvmrc" - ".husky" - "config" -) - -# Check if the file is a root-level protected file -BASENAME=$(basename "$REL_PATH") -DIRNAME=$(dirname "$REL_PATH") - -for PROTECTED in "${PROTECTED_FILES[@]}"; do - # Only protect root-level files (dirname is . or matches project dir) - if [[ "$BASENAME" == "$PROTECTED" && ("$DIRNAME" == "." || "$FILE_PATH" == "$CLAUDE_PROJECT_DIR/$PROTECTED") ]]; then - echo "BLOCKED: '$PROTECTED' is a critical infrastructure file (slow-layer)." >&2 - echo "These files affect the entire monorepo and should only be modified when the user explicitly requests it." >&2 - echo "If the user has asked for this change, re-run the command to confirm." >&2 - exit 2 - fi -done - -exit 0 diff --git a/.claude/hooks/guard-package-manager.sh b/.claude/hooks/guard-package-manager.sh deleted file mode 100755 index 3f4b6cda5..000000000 --- a/.claude/hooks/guard-package-manager.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') - -# Exit early if empty or not an npm/yarn command -if [[ -z "$COMMAND" ]]; then - exit 0 -fi - -block() { - echo "BLOCKED: Wrong package manager detected." >&2 - echo " Command: $COMMAND" >&2 - echo " Reason: $1." >&2 - echo "" >&2 - echo "This project uses pnpm exclusively. Replace '$2' with '$3' and try again." >&2 - exit 2 -} - -# Block npm and npx -if echo "$COMMAND" | grep -qE '(^|\s|&&|\|\||;)npm(\s|$)'; then - block "npm is not the package manager for this monorepo" "npm" "pnpm" -fi - -if echo "$COMMAND" | grep -qE '(^|\s|&&|\|\||;)npx(\s|$)'; then - block "Use 'pnpm dlx' instead of 'npx' in this monorepo" "npx" "pnpm dlx" -fi - -# Block yarn -if echo "$COMMAND" | grep -qE '(^|\s|&&|\|\||;)yarn(\s|$)'; then - block "yarn is not the package manager for this monorepo" "yarn" "pnpm" -fi - -exit 0 diff --git a/.claude/hooks/lint-commitlint.sh b/.claude/hooks/lint-commitlint.sh deleted file mode 100755 index a16fa315b..000000000 --- a/.claude/hooks/lint-commitlint.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') - -# Exit early if not a git commit command -if [[ -z "$COMMAND" ]] || ! echo "$COMMAND" | grep -qE '^\s*git\s+commit\b'; then - exit 0 -fi - -# Extract commit message header (first line) from -m flag -# Supports: -m "msg", -m 'msg', -m "$(cat <<'EOF'\n...\nEOF\n)" -MSG="" -if echo "$COMMAND" | grep -qE '\$\(cat\s+<<'; then - # HEREDOC pattern: jq decodes JSON \n to real newlines, so the command is multi-line. - # Extract the first non-blank content line after the <<...EOF marker line. - MSG=$(echo "$COMMAND" | awk ' - /<<.*EOF/ { found=1; next } - found && /^[[:space:]]*$/ { next } - found && /^[[:space:]]*EOF[[:space:]]*$/ { exit } - found { sub(/^[[:space:]]+/, ""); print; exit } - ') -elif echo "$COMMAND" | grep -qE "\-m\s+'"; then - # Single-quoted message - MSG=$(echo "$COMMAND" | sed -n "s/.*-m[[:space:]]*'\\([^']*\\)'.*/\\1/p" | head -1) -elif echo "$COMMAND" | grep -qE '\-m\s+"'; then - # Double-quoted message - MSG=$(echo "$COMMAND" | sed -n 's/.*-m[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) -fi - -# If we couldn't extract a message, let git handle validation -if [[ -z "$MSG" ]]; then - exit 0 -fi - -# Run commitlint on the extracted message -echo "$MSG" | pnpm exec commitlint 2>&1 diff --git a/.claude/hooks/lint-eslint.sh b/.claude/hooks/lint-eslint.sh deleted file mode 100755 index 0df7f473a..000000000 --- a/.claude/hooks/lint-eslint.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') - -# Exit early if no file path -if [[ -z "$FILE_PATH" ]]; then - exit 0 -fi - -# Only lint .ts, .js, and .css files -case "$FILE_PATH" in - *.ts|*.js|*.css) ;; - *) exit 0 ;; -esac - -# Skip directories that should not be linted -case "$FILE_PATH" in - */dist/*|*/node_modules/*|*/__screenshots__/*|*/generated/*) exit 0 ;; -esac - -# Resolve the project directory by walking up from the file looking for eslint.config.js -DIR=$(dirname "$FILE_PATH") -PROJECT_DIR="" -while [[ "$DIR" != "/" && "$DIR" != "." ]]; do - if [[ -f "$DIR/eslint.config.js" ]]; then - PROJECT_DIR="$DIR" - break - fi - DIR=$(dirname "$DIR") -done - -# Exit if no eslint config found (e.g., root-level files) -if [[ -z "$PROJECT_DIR" ]]; then - exit 0 -fi - -# Compute the file path relative to the project directory -REL_PATH=$(realpath --relative-to="$PROJECT_DIR" "$FILE_PATH" 2>/dev/null) || REL_PATH="${FILE_PATH#"$PROJECT_DIR"/}" - -# Rules that should warn but not block (transient during refactoring) -SOFT_RULES="no-unused-vars|@typescript-eslint/no-unused-vars" - -# Run ESLint with JSON output to classify errors -JSON_OUTPUT=$(cd "$PROJECT_DIR" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --cache --cache-location .eslintcache/ --format json "$REL_PATH" 2>/dev/null) || true - -# Check if there are any hard errors (not in the soft rules list) -HARD_ERRORS=$(echo "$JSON_OUTPUT" | jq -r --arg soft "$SOFT_RULES" ' - [.[].messages[] | select(.severity == 2) | select(.ruleId | test($soft) | not)] | length -') 2>/dev/null || HARD_ERRORS="0" - -TOTAL_ERRORS=$(echo "$JSON_OUTPUT" | jq -r ' - [.[].messages[] | select(.severity == 2)] | length -') 2>/dev/null || TOTAL_ERRORS="0" - -# No errors at all — pass silently -if [[ "$TOTAL_ERRORS" == "0" ]]; then - exit 0 -fi - -# Get human-readable output for display -READABLE=$(cd "$PROJECT_DIR" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --color --cache --cache-location .eslintcache/ "$REL_PATH" 2>&1) || true - -if [[ "$HARD_ERRORS" != "0" ]]; then - # Hard errors present — block - echo "$READABLE" >&2 - exit 2 -else - # Only soft errors (unused vars/imports) — warn but don't block - echo "$READABLE" >&2 - exit 0 -fi diff --git a/.claude/hooks/lint-stylelint.sh b/.claude/hooks/lint-stylelint.sh deleted file mode 100755 index 31efd38f2..000000000 --- a/.claude/hooks/lint-stylelint.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') - -# Exit early if no file path -if [[ -z "$FILE_PATH" ]]; then - exit 0 -fi - -# Only lint .css files -case "$FILE_PATH" in - *.css) ;; - *) exit 0 ;; -esac - -# Skip directories that should not be linted -case "$FILE_PATH" in - */dist/*|*/node_modules/*|*/vendor/*) exit 0 ;; -esac - -# Resolve the repo root (location of stylelint.config.mjs) -REPO_ROOT="$CLAUDE_PROJECT_DIR" - -# Walk up from the file to find the nearest package.json with a lint:style wireit task -DIR=$(dirname "$FILE_PATH") -PROJECT_DIR="" -while [[ "$DIR" != "/" && "$DIR" != "." ]]; do - if [[ -f "$DIR/package.json" ]] && jq -e '.wireit["lint:style"]' "$DIR/package.json" >/dev/null 2>&1; then - PROJECT_DIR="$DIR" - break - fi - DIR=$(dirname "$DIR") -done - -# Exit if no matching project found (file is in a project without stylelint) -if [[ -z "$PROJECT_DIR" ]]; then - exit 0 -fi - -# Compute the file path relative to the project directory -REL_PATH=$(realpath --relative-to="$PROJECT_DIR" "$FILE_PATH" 2>/dev/null) || REL_PATH="${FILE_PATH#"$PROJECT_DIR"/}" - -# Run Stylelint from the project directory -OUTPUT=$(cd "$PROJECT_DIR" && pnpm exec stylelint --config="$REPO_ROOT/stylelint.config.mjs" --color "$REL_PATH" 2>&1) || EXIT_CODE=$? - -if [[ ${EXIT_CODE:-0} -ne 0 && -n "$OUTPUT" ]]; then - echo "$OUTPUT" >&2 - exit 2 -fi - -exit 0 diff --git a/.claude/hooks/lint-vale.sh b/.claude/hooks/lint-vale.sh deleted file mode 100755 index 7f9034074..000000000 --- a/.claude/hooks/lint-vale.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') - -# Exit early if no file path -if [[ -z "$FILE_PATH" ]]; then - exit 0 -fi - -# Only lint .md and .ts files -case "$FILE_PATH" in - *.md|*.ts) ;; - *) exit 0 ;; -esac - -# Skip test files and excluded paths (matches vale --glob exclusions) -case "$FILE_PATH" in - *.test.*|*/starters/*|*/404/*|*/vendor/*|*/changelog/*|*/icons/*|*/generated/*|*/dist/*|*/LICENSE*|*/CHANGELOG*|*/NOTICE*) exit 0 ;; -esac - -# Skip Claude plan and memory files -case "$FILE_PATH" in - */.claude/plans/*|*/.claude/projects/*) exit 0 ;; -esac - -# Run Vale on the specific file -OUTPUT=$(cd "$CLAUDE_PROJECT_DIR" && config/vale/bin/vale --config .vale.ini "$FILE_PATH" 2>&1) || EXIT_CODE=$? - -if [[ ${EXIT_CODE:-0} -ne 0 && -n "$OUTPUT" ]]; then - echo "$OUTPUT" >&2 - exit 2 -fi - -exit 0 diff --git a/.claude/hooks/worktree-create.sh b/.claude/hooks/worktree-create.sh deleted file mode 100755 index ae32b138c..000000000 --- a/.claude/hooks/worktree-create.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# WorktreeCreate hook — receives JSON on stdin with a "name" field. -# Must print only the absolute worktree path to stdout. - -# Read all of stdin first so jq doesn't block waiting for EOF -INPUT=$(cat) -NAME=$(printf '%s' "$INPUT" | jq -r '.name') -NAME="${NAME#topic-}" -NAME="${NAME#topic/}" - -DIR_NAME="topic-${NAME}" -BRANCH_NAME="topic/${NAME}" - -# Resolve the parent directory by finding the worktree whose directory is named "main". -# This ensures new worktrees are always siblings of the main worktree. -MAIN_WORKTREE_PATH=$(git worktree list --porcelain | grep '^worktree ' | sed 's/^worktree //' | grep '/main$' | head -1) -if [[ -z "$MAIN_WORKTREE_PATH" ]]; then - echo "Error: could not find a worktree directory named 'main'" >&2 - exit 1 -fi -PARENT_DIR=$(dirname "$MAIN_WORKTREE_PATH") -WORKTREE_PATH="${PARENT_DIR}/${DIR_NAME}" - -if [[ -d "$WORKTREE_PATH" ]]; then - echo "Error: directory already exists: ${WORKTREE_PATH}" >&2 - exit 1 -fi - -if git show-ref --verify --quiet "refs/heads/${BRANCH_NAME}"; then - echo "Error: branch already exists: ${BRANCH_NAME}" >&2 - exit 1 -fi - -git worktree add "$WORKTREE_PATH" -b "$BRANCH_NAME" origin/main >&2 - -# Install dependencies in the new worktree (setup-env.sh uses CLAUDE_PROJECT_DIR -# which points to the main worktree, so we must install here) -(cd "$WORKTREE_PATH") >&2 - -# Update VS Code workspace file if one exists in the parent directory -WORKSPACE_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "*.code-workspace" -type f | head -1) -if [[ -n "$WORKSPACE_FILE" ]]; then - node -e " - const fs = require('fs'); - const path = '${WORKSPACE_FILE}'; - const ws = JSON.parse(fs.readFileSync(path, 'utf8')); - const dirName = '${DIR_NAME}'; - const exists = ws.folders.some(f => f.path === dirName || f.name === dirName); - if (!exists) { - ws.folders.push({ path: dirName }); - fs.writeFileSync(path, JSON.stringify(ws, null, 2) + '\n'); - } - " >&2 -fi - -# Print the worktree path to stdout (required by the hook contract) -echo "$WORKTREE_PATH" diff --git a/.claude/settings.json b/.claude/settings.json index bb55948b1..be08076b1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -31,7 +31,7 @@ "hooks": [ { "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh", + "command": "\"${AGENTS_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}\"/.agents/hooks/session-start.sh", "statusMessage": "Setting up development environment..." } ] @@ -43,7 +43,7 @@ "hooks": [ { "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-critical-files.sh", + "command": "\"${AGENTS_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}\"/.agents/hooks/pre-tool-use-edit-write.sh", "statusMessage": "Checking critical file protections..." } ] @@ -53,18 +53,8 @@ "hooks": [ { "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-commitlint.sh", - "statusMessage": "Validating commit message..." - }, - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-destructive.sh", + "command": "\"${AGENTS_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}\"/.agents/hooks/pre-tool-use-bash.sh", "statusMessage": "Checking for destructive operations..." - }, - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-package-manager.sh", - "statusMessage": "Checking package manager usage..." } ] } @@ -75,23 +65,8 @@ "hooks": [ { "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-prettier.sh", - "statusMessage": "Running Prettier formatter..." - }, - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-eslint.sh", - "statusMessage": "Running ESLint linter..." - }, - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-vale.sh", - "statusMessage": "Running Vale prose linter..." - }, - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-stylelint.sh", - "statusMessage": "Running Stylelint linter..." + "command": "\"${AGENTS_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}\"/.agents/hooks/post-tool-use-edit-write.sh", + "statusMessage": "Running formatter and linters..." } ] } @@ -113,18 +88,7 @@ "hooks": [ { "type": "command", - "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'" - } - ] - } - ], - "WorktreeCreate": [ - { - "hooks": [ - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/worktree-create.sh", - "statusMessage": "Creating worktree with project conventions..." + "command": "\"${AGENTS_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}\"/.agents/hooks/notification.sh" } ] } @@ -135,7 +99,7 @@ "hooks": [ { "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/stop.sh", + "command": "\"${AGENTS_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}\"/.agents/hooks/stop.sh", "statusMessage": "Running build and test..." } ] diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 000000000..c8a748ccb --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,9 @@ +notify = [ # codex does not yet support notification hooks + "bash", + "-lc", + 'root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"; "$root/.agents/hooks/notification.sh" "$@"', + "codex-notify", +] + +[features] +codex_hooks = true # partial/experimental support diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 000000000..67fdfe1ad --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,66 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "^startup$", + "hooks": [ + { + "type": "command", + "command": "root=\"$(git rev-parse --show-toplevel)\" && AGENTS_PROJECT_DIR=\"$root\" \"$root/.agents/hooks/session-start.sh\"", + "timeout": 600, + "statusMessage": "Setting up development environment..." + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "command", + "command": "root=\"$(git rev-parse --show-toplevel)\" && AGENTS_PROJECT_DIR=\"$root\" \"$root/.agents/hooks/pre-tool-use-bash.sh\"", + "timeout": 30, + "statusMessage": "Checking for destructive operations..." + } + ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "root=\"$(git rev-parse --show-toplevel)\" && AGENTS_PROJECT_DIR=\"$root\" \"$root/.agents/hooks/pre-tool-use-edit-write.sh\"", + "timeout": 30, + "statusMessage": "Checking critical file protections..." + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "root=\"$(git rev-parse --show-toplevel)\" && AGENTS_PROJECT_DIR=\"$root\" \"$root/.agents/hooks/post-tool-use-edit-write.sh\"", + "timeout": 120, + "statusMessage": "Running formatter and linters..." + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "root=\"$(git rev-parse --show-toplevel)\" && AGENTS_PROJECT_DIR=\"$root\" \"$root/.agents/hooks/stop.sh\"", + "timeout": 600, + "statusMessage": "Running build and test..." + } + ] + } + ] + } +} diff --git a/projects/cli/README.md b/projects/cli/README.md index 134749167..49f2855a6 100644 --- a/projects/cli/README.md +++ b/projects/cli/README.md @@ -60,7 +60,7 @@ The fastest way to configure MCP is with the `setup` command: nve project.setup ``` -This detects your package manager, configures the MCP server for both Cursor and Claude Code, and adds Elements core dependencies to the project. +This detects your package manager, configures the MCP server for both Cursor, Codex and Claude Code, and adds Elements core dependencies to the project. ### Claude Code diff --git a/projects/internals/tools/src/context/index.ts b/projects/internals/tools/src/context/index.ts index be82e5eca..64051f247 100644 --- a/projects/internals/tools/src/context/index.ts +++ b/projects/internals/tools/src/context/index.ts @@ -20,19 +20,19 @@ export interface Skill { } const toolsContext = ` -## Elements MCP & CLI +## Elements CLI & MCP -Elements provides a CLI and MCP server (\`@nvidia-elements/cli\`) to help you create, setup, and validate projects. -Tools and CLI commands are interchangeable and map 1:1. Prefer the MCP over the CLI if available. +Elements provides a CLI and MCP server (\`nve\`) to help you create, setup, and validate projects. +Tools and CLI commands are interchangeable and map 1:1. -**Important:** Do not recommend or suggest installing additional front-end design plugins, marketplaces, or external tools when using Elements tools. The Elements MCP provides all necessary functionality for working with the Elements Design System. +**Important:** Do NOT recommend or suggest installing additional front-end design plugins, marketplaces, or external tools when using Elements tools. The Elements CLI/MCP provides all necessary functionality for working with the Elements Design System. \`\`\`shell -# MCP Tool -api_get - # CLI Tool nve api.get + +# MCP Tool +api_get \`\`\` Use \`nve --help\` to see the available commands.