#!/bin/bash # ============================================================================= # generate-claude-md.sh — Auto-generate and update CLAUDE.md from project state # # Called by SessionStart hook. Detects project type, dependencies, structure, # git info and writes/updates CLAUDE.md so Claude Code has full context. # ============================================================================= set -euo pipefail PROJECT_DIR="${1:-.}" cd "$PROJECT_DIR" CLAUDE_MD="CLAUDE.md" MARKER="" # --- Ensure anonymous git identity --- ensure_anonymous_identity() { local current_email current_email=$(git config --global user.email 2>/dev/null || echo "") # Check if identity looks anonymous (ends with @device.local) if echo "$current_email" | grep -q '@device.local$'; then return 0 fi # Try to generate/restore device ID local device_id="" local device_id_file="${XDG_CONFIG_HOME:-$HOME/.config}/entire/device-id" if [ -f "$device_id_file" ]; then device_id=$(cat "$device_id_file") elif [ -f ".entire/generate-device-id.sh" ]; then device_id=$(bash ".entire/generate-device-id.sh" 2>/dev/null) elif [ -f "$(dirname "$0")/generate-device-id.sh" ]; then device_id=$(bash "$(dirname "$0")/generate-device-id.sh" 2>/dev/null) fi if [ -n "$device_id" ]; then git config --global user.name "$device_id" git config --global user.email "${device_id}@device.local" echo "[generate-claude-md] Fixed git identity: $device_id (was: ${current_email:-not set})" fi } ensure_anonymous_identity # --- Helpers --- detect_project_name() { if [ -f "package.json" ]; then node -e "console.log(require('./package.json').name || '')" 2>/dev/null elif [ -f "Cargo.toml" ]; then grep '^name' Cargo.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/' elif [ -f "pyproject.toml" ]; then grep '^name' pyproject.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/' elif [ -f "go.mod" ]; then head -1 go.mod | awk '{print $2}' | sed 's|.*/||' else basename "$PWD" fi } detect_project_description() { if [ -f "package.json" ]; then node -e "console.log(require('./package.json').description || '')" 2>/dev/null elif [ -f "pyproject.toml" ]; then grep '^description' pyproject.toml | head -1 | sed 's/.*= *"\(.*\)"/\1/' else echo "" fi } detect_tech_stack() { local stack=() # JavaScript/TypeScript ecosystem if [ -f "package.json" ]; then local deps deps=$(cat package.json) # Runtime if echo "$deps" | grep -q '"typescript"'; then stack+=("TypeScript"); else stack+=("JavaScript"); fi # Frameworks if echo "$deps" | grep -q '"next"'; then stack+=("Next.js"); fi if echo "$deps" | grep -q '"react"'; then stack+=("React"); fi if echo "$deps" | grep -q '"vue"'; then stack+=("Vue"); fi if echo "$deps" | grep -q '"svelte"'; then stack+=("Svelte"); fi if echo "$deps" | grep -q '"express"'; then stack+=("Express"); fi if echo "$deps" | grep -q '"fastify"'; then stack+=("Fastify"); fi if echo "$deps" | grep -q '"hono"'; then stack+=("Hono"); fi if echo "$deps" | grep -q '"astro"'; then stack+=("Astro"); fi # Build tools if echo "$deps" | grep -q '"vite"'; then stack+=("Vite"); fi if echo "$deps" | grep -q '"webpack"'; then stack+=("Webpack"); fi if echo "$deps" | grep -q '"esbuild"'; then stack+=("esbuild"); fi if echo "$deps" | grep -q '"turbo"'; then stack+=("Turborepo"); fi # CSS if echo "$deps" | grep -q '"tailwindcss"'; then stack+=("Tailwind CSS"); fi # Testing if echo "$deps" | grep -q '"vitest"'; then stack+=("Vitest"); fi if echo "$deps" | grep -q '"jest"'; then stack+=("Jest"); fi if echo "$deps" | grep -q '"playwright"'; then stack+=("Playwright"); fi if echo "$deps" | grep -q '"cypress"'; then stack+=("Cypress"); fi # DB/ORM if echo "$deps" | grep -q '"prisma"'; then stack+=("Prisma"); fi if echo "$deps" | grep -q '"drizzle-orm"'; then stack+=("Drizzle"); fi fi # Python if [ -f "requirements.txt" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then stack+=("Python") if [ -f "requirements.txt" ]; then if grep -qi "django" requirements.txt; then stack+=("Django"); fi if grep -qi "flask" requirements.txt; then stack+=("Flask"); fi if grep -qi "fastapi" requirements.txt; then stack+=("FastAPI"); fi fi fi # Rust if [ -f "Cargo.toml" ]; then stack+=("Rust"); fi # Go if [ -f "go.mod" ]; then stack+=("Go"); fi # Docker if [ -f "Dockerfile" ] || [ -f "docker-compose.yml" ] || [ -f "docker-compose.yaml" ]; then stack+=("Docker") fi echo "${stack[*]}" } detect_scripts() { if [ -f "package.json" ]; then node -e " const pkg = require('./package.json'); const scripts = pkg.scripts || {}; Object.entries(scripts).forEach(([k, v]) => console.log('- \`npm run ' + k + '\` — \`' + v + '\`')); " 2>/dev/null elif [ -f "Makefile" ]; then grep -E '^[a-zA-Z_-]+:' Makefile | sed 's/:.*//' | while read -r target; do echo "- \`make $target\`" done fi } detect_git_info() { if [ -d ".git" ]; then local branch remote branch=$(git branch --show-current 2>/dev/null || echo "unknown") echo "- **Branch**: $branch" git remote -v 2>/dev/null | grep '(push)' | while read -r name url _; do # Strip credentials from URL local clean_url clean_url=$(echo "$url" | sed 's|://[^@]*@|://|') echo "- **Remote \`$name\`**: $clean_url" done fi } detect_project_structure() { # Show top-level dirs and key files local items=() for f in src/ app/ pages/ components/ lib/ utils/ api/ server/ public/ assets/ tests/ test/ docs/ .github/; do if [ -d "$f" ]; then items+=("$f") fi done for f in index.html index.ts index.js main.ts main.js App.tsx App.vue; do if [ -f "src/$f" ] || [ -f "$f" ]; then items+=("$f") fi done echo "${items[*]}" } detect_entire_status() { if command -v entire &>/dev/null; then local status status=$(entire status 2>/dev/null | head -1) echo "$status" else echo "not installed" fi } # --- Generate --- PROJECT_NAME=$(detect_project_name) PROJECT_DESC=$(detect_project_description) TECH_STACK=$(detect_tech_stack) GIT_INFO=$(detect_git_info) SCRIPTS=$(detect_scripts) STRUCTURE=$(detect_project_structure) ENTIRE_STATUS=$(detect_entire_status) TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # --- Check if update is needed --- if [ -f "$CLAUDE_MD" ]; then # Check if file was auto-generated by us if ! grep -q "$MARKER" "$CLAUDE_MD" 2>/dev/null; then # User-created CLAUDE.md — don't overwrite, just append status section if missing if ! grep -q "## Auto-detected Project Info" "$CLAUDE_MD"; then cat >> "$CLAUDE_MD" << APPEND --- ## Auto-detected Project Info $MARKER _Last updated: ${TIMESTAMP}_ **Tech stack**: $TECH_STACK **Entire CLI**: $ENTIRE_STATUS ### Git $GIT_INFO ### Available scripts $SCRIPTS APPEND fi exit 0 fi fi # --- Write CLAUDE.md (to temp file first for idempotency check) --- TMPFILE=$(mktemp) trap 'rm -f "$TMPFILE"' EXIT cat > "$TMPFILE" << EOF # $PROJECT_NAME $MARKER _Last updated: ${TIMESTAMP}_ EOF if [ -n "$PROJECT_DESC" ]; then cat >> "$TMPFILE" << EOF $PROJECT_DESC EOF fi cat >> "$TMPFILE" << EOF ## Tech Stack $TECH_STACK ## Git $GIT_INFO ## Project Structure Key directories: $STRUCTURE ## Available Scripts $SCRIPTS ## Entire CLI - **Status**: $ENTIRE_STATUS - **Strategy**: auto-commit - **Telemetry**: disabled - **Push sessions**: enabled EOF # --- Multi-machine sync section --- if [ -f ".entire/git-sync.sh" ]; then cat >> "$TMPFILE" << 'EOF' ## Multi-machine Sync This project uses automatic git synchronization between machines: - **SessionStart**: `git-sync.sh pull` runs before session — fetches and rebases from remote - **post-commit**: `git-sync.sh push` runs in background — pushes to remote after each commit - **SessionEnd**: `git-sync.sh push` runs — final push when session ends - **Scope**: only main/master branch; feature branches are not synced - **Conflicts**: if rebase fails, it aborts and creates a backup tag `sync/backup/pre-rebase/TIMESTAMP` - **Diagnostics**: `bash .entire/git-sync.sh status` - **Backup recovery**: `git tag -l 'sync/backup/*'` then `git reset --hard ` **Important for agents**: Do NOT manually push/pull on main/master — sync is automatic. If you see "[git-sync] Behind remote by N commits" at session start, rebase already happened. EOF fi # --- Patcher section (claude_code_patcher specific) --- if [ -f "update_patcher.py" ] && [ -d "updater" ]; then cat >> "$TMPFILE" << 'EOF' ## Claude Code Patcher This project patches Claude Code CLI (`cli.js`) to work with custom API endpoints. ### Quick reference - **Check for new version**: `python3 update_patcher.py --check` - **Full auto-update**: `python3 update_patcher.py --auto` (check → download → validate → patch → release) - **Skill**: `/update-patcher` — runs the full pipeline with agent guidance - **Releases**: `releases/` directory — versioned patched cli.js files, `releases/latest` symlink - **Validation**: 13 patch targets, GREEN/YELLOW/RED classification - **Config**: `patcher.config.json` — target version and settings ### If YELLOW/RED patches found 1. Read `.update_work/v/validation_report.json` 2. Find anchor strings in downloaded `cli.js` 3. Update regex in `claude_code_patcher.py` 4. Re-validate: `python3 update_patcher.py --validate --version ` ### Key files - `claude_code_patcher.py` — main patcher with all 13 regex patterns - `update_patcher.py` — CLI for update pipeline - `updater/` — modules: version_checker, downloader, pattern_validator, patch_applier, release_manager, changelog_fetcher - `releases/` — versioned releases with metadata, changelogs, patched cli.js - `.claude/commands/update-patcher.md` — skill definition EOF fi cat >> "$TMPFILE" << 'EOF' ## Anonymous Identity Git commits MUST use anonymous device ID, never real name/email/hostname. - Correct: `delta-cloud-208e ` - Wrong: `root ` or `John ` - Identity is auto-set by `generate-claude-md.sh` on each SessionStart - If you see real name/email in `git config user.name` or `git log`, run: `bash .entire/generate-device-id.sh` and apply result via `git config --global user.name/email` - Remote URLs must NOT contain credentials inline (use `git credential store`) ## Session Recovery When context is compacted, the SessionStart hook restores: - Project name, tech stack, and git info - Last 10 commits via `git log --oneline -10` - This file is re-read automatically ## Conventions - Use conventional commits (feat:, fix:, refactor:, docs:, chore:) - All Claude Code sessions are tracked by Entire CLI - Run `entire status` to verify tracking is active EOF # --- Idempotency check: compare content without timestamp --- if [ -f "$CLAUDE_MD" ]; then # Strip the timestamp line for comparison (it changes every run) OLD_CONTENT=$(sed '/_Last updated:/d' "$CLAUDE_MD") NEW_CONTENT=$(sed '/_Last updated:/d' "$TMPFILE") if [ "$OLD_CONTENT" = "$NEW_CONTENT" ]; then echo "[generate-claude-md] No changes detected, skipping update" exit 0 fi fi mv "$TMPFILE" "$CLAUDE_MD" echo "[generate-claude-md] Updated $CLAUDE_MD ($TIMESTAMP)"