diff --git a/.claude/hooks/ts_lint.py b/.claude/hooks/ts_lint.py new file mode 100644 index 0000000000..306a0e97be --- /dev/null +++ b/.claude/hooks/ts_lint.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import json +import sys +import subprocess +from pathlib import Path + + +def main(): + try: + # Read input data from stdin + input_data = json.load(sys.stdin) + + tool_input = input_data.get("tool_input", {}) + print(tool_input) + + # Get file path from tool input + file_path = tool_input.get("file_path") + if not file_path: + sys.exit(0) + + # Only check TypeScript/JavaScript files + if not file_path.endswith((".ts", ".tsx", ".js", ".jsx")): + sys.exit(0) + + # Check if file exists + if not Path(file_path).exists(): + sys.exit(0) + + # Run ESLint to check for errors and style violations + try: + result = subprocess.run( + ["npx", "eslint", file_path, "--format", "compact"], + capture_output=True, + text=True, + timeout=30, + ) + + if result.returncode != 0 and (result.stdout or result.stderr): + # Log the error for debugging + log_file = Path(__file__).parent.parent / "eslint_errors.json" + error_output = result.stdout or result.stderr + error_entry = { + "file_path": file_path, + "errors": error_output, + "session_id": input_data.get("session_id"), + } + + # Load existing errors or create new list + if log_file.exists(): + with open(log_file, "r") as f: + errors = json.load(f) + else: + errors = [] + + errors.append(error_entry) + + # Save errors + with open(log_file, "w") as f: + json.dump(errors, f, indent=2) + + # Send error message to stderr for LLM to see + print(f"ESLint errors found in {file_path}:", file=sys.stderr) + print(error_output, file=sys.stderr) + + # Exit with code 2 to signal LLM to correct + sys.exit(2) + + except subprocess.TimeoutExpired: + print("ESLint check timed out", file=sys.stderr) + sys.exit(0) + except FileNotFoundError: + # ESLint not available, skip check + sys.exit(0) + + except json.JSONDecodeError as e: + print(f"Error parsing JSON input: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error in eslint hook: {e}", file=sys.stderr) + sys.exit(1) + + +main() \ No newline at end of file diff --git a/.claude/hooks/use_yarn.py b/.claude/hooks/use_yarn.py new file mode 100644 index 0000000000..2e8d3bf49e --- /dev/null +++ b/.claude/hooks/use_yarn.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import json +import sys +import re +from pathlib import Path + +def main(): + try: + # Read input data from stdin + input_data = json.load(sys.stdin) + # tool_name = input_data.get("tool_name") + tool_input = input_data.get("tool_input", {}) + command = tool_input.get("command", "") + + if not command: + sys.exit(0) + + # Check for npm, bun, pnpm commands and npx/bunx commands + npm_pattern = r"\bnpm\s+" + npx_pattern = r"\bnpx\s+" + bun_pattern = r"\bbun\s+" + bunx_pattern = r"\bbunx\s+" + pnpm_pattern = r"\bpnpm\s+" + + blocked_command = None + suggested_command = None + + if re.search(npm_pattern, command): + blocked_command = command + suggested_command = re.sub(r"\bnpm\b", "yarn", command) + elif re.search(npx_pattern, command): + blocked_command = command + suggested_command = re.sub(r"\bnpx\b", "yarn dlx", command) + elif re.search(bun_pattern, command): + blocked_command = command + suggested_command = re.sub(r"\bbun\b", "yarn", command) + elif re.search(bunx_pattern, command): + blocked_command = command + suggested_command = re.sub(r"\bbunx\b", "yarn dlx", command) + elif re.search(pnpm_pattern, command): + blocked_command = command + suggested_command = re.sub(r"\bpnpm\b", "yarn", command) + + if blocked_command: + # Log the usage attempt + log_file = Path(__file__).parent.parent / "yarn_enforcement.json" + log_entry = { + "session_id": input_data.get("session_id"), + "blocked_command": blocked_command, + "suggested_command": suggested_command, + } + + # Load existing logs or create new list + if log_file.exists(): + with open(log_file, "r") as f: + logs = json.load(f) + else: + logs = [] + + logs.append(log_entry) + + # Save logs + with open(log_file, "w") as f: + json.dump(logs, f, indent=2) + + # Send error message to stderr for LLM to see + print("Error: Use 'yarn/yarn dlx' instead of 'npm/npx/bun/bunx/pnpm'", file=sys.stderr) + + # Exit with code 2 to signal LLM to correct + sys.exit(2) + + except json.JSONDecodeError as e: + print(f"Error parsing JSON input: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error in use-yarn hook: {e}", file=sys.stderr) + sys.exit(1) + +main() \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..d417565126 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,48 @@ +{ + "permissions": { + "allow": [ + "Bash(yarn)", + "Bash(yarn add:*)", + "Bash(yarn install:*)", + "Bash(yarn config:*)", + "Bash(yarn run lint)", + "Bash(yarn run:*)", + "Bash(yarn dlx:*)" + ], + "deny": [ + "Read(.env)", + "Read(**/.env*)", + "Read(**/env*)", + "Read(**/*.pem)", + "Read(**/*.key)", + "Read(**/*.crt)", + "Read(**/*.cert)", + "Read(**/secrets/**)", + "Read(**/credentials/**)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "uv run ~/.claude/hooks/use_yarn.py" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "uv run ~/.claude/hooks/ts_lint.py" + } + ] + } + ] + } +} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000000..f94524a90e --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,59 @@ +name: Claude PR Assistant + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude-code-action: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) + runs-on: ubuntu-latest + permissions: + # TODO: change to write when team feels ready enough to handle creation of PR's directly from Claude + contents: read + pull-requests: read + issues: read + id-token: write + # contents: write + # pull-requests: write + # issues: write + # id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude PR Action + uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Or use OAuth token instead: + # claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + timeout_minutes: '60' + allowed_tools: | + mcp__github__create_pull_request + + custom_instructions: | + You MUST open a draft pull request after creating a branch. + You MUST create a pull request after completing your task. + You can create pull requests using the `mcp__github__create_pull_request` tool. + # Optional: Restrict network access to specific domains only + # experimental_allowed_domains: | + # .anthropic.com + # .github.com + # api.github.com + # .githubusercontent.com + # bun.sh + # registry.npmjs.org + # .blob.core.windows.net diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..17b5d4542a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Development + +- `yarn dev` - Start development server (automatically runs `build:metadata` first) +- `yarn` - Install dependencies +- `yarn build` - Build production version (runs `build:metadata` first) +- `yarn start` - Start production server + +### Code Quality + +- `yarn lint` - Run ESLint +- `yarn format` - Format code with Prettier +- `yarn ts` - Run TypeScript type checking without emitting files + +### Utility + +- `yarn build:metadata` - Pull metadata from external sources (required before dev/build) +- `yarn analyze` - Build with bundle analyzer +- `yarn convert-spec` - Convert API specification files + +## Architecture + +### Technology Stack + +- **Framework**: Next.js 15 (App Router not used - pages directory structure) +- **React**: 19.1.0 with TypeScript +- **Styling**: Tailwind CSS 4.x with PostCSS +- **State Management**: React Query (@tanstack/react-query) for server state +- **Charts**: ECharts library for data visualization +- **Authentication**: PocketBase for auth with custom AuthProvider +- **Web3**: Wagmi + Viem for blockchain interactions +- **Search**: Meilisearch with React InstantSearch + +### Project Structure + +- `src/pages/` - Next.js pages (not App Router) +- `src/components/` - Reusable UI components +- `src/containers/` - Page-specific container components +- `src/api/` - API utilities and client functions +- `src/hooks/` - Custom React hooks +- `src/utils/` - Utility functions +- `src/layout/` - Layout components +- `src/constants/` - Application constants +- `src/contexts/` - React contexts +- `public/` - Static assets +- `scripts/` - Build and deployment scripts + +### Key Architectural Patterns + +- **Component Organization**: Components are organized by feature/domain in containers, with shared components in the components directory +- **Data Fetching**: Uses React Query for server state management with custom hooks +- **Styling**: Tailwind CSS with custom utility classes and CSS variables for theming +- **Charts**: ECharts wrapper components with TypeScript interfaces for props +- **Routing**: Next.js file-based routing with extensive URL redirects in next.config.js +- **Type Safety**: TypeScript with interfaces defined in types.ts files throughout the codebase + +### Configuration + +- TypeScript config disables strict mode but enables forceConsistentCasingInFileNames +- Path aliases: `~/*` maps to `./src/*` and `~/public/*` maps to `./public/*` +- Static page generation timeout increased to 5 minutes for large datasets +- Image optimization configured for external domains (icons.llama.fi, etc.) + +### Data Flow + +- External APIs provide DeFi protocol data and metrics +- Metadata is pulled during build process via `scripts/pullMetadata.js` +- React Query manages caching and synchronization of server state +- Charts consume processed data through ECharts wrapper components +- Search functionality uses Meilisearch instance search + +### Development Notes + +- Development server requires metadata build step before starting +- Uses Redis (ioredis) for caching in production +- Extensive URL redirect configuration for legacy route compatibility +- Analytics integration with Fathom +- Bundle analysis available via `yarn analyze` + +## Coding Style Guidelines + +- **File Naming**: Use PascalCase for React components (`ChainOverview.tsx`), camelCase for utilities (`useAnalytics.tsx`), lowercase-with-dashes for directories (`chain-overview/`) +- **TypeScript**: Use interfaces with `I` prefix for props (`IChartProps`), avoid `any` type, use named exports for components (default exports only for pages) +- **Imports**: Use path aliases `~/` for src imports, group imports by: external libraries, internal components, utilities, types +- **Components**: Use React.memo() for performance-critical components, functional components with hooks, place logic (state, effects) at top of function +- **Styling**: Use Tailwind CSS with CSS custom properties for theming (`--cards-bg`, `--text1`), use data attributes for conditional styling (`data-[align=center]:justify-center`) +- **Data Management**: Use React Query for server state, utility functions from `~/utils` for formatting (`formattedNum()`, `formattedPercent()`), proper useEffect cleanup +- **Performance**: Use virtualization for large lists (@tanstack/react-virtual), React.lazy() for code splitting, useCallback/useMemo for optimization