chore: add Entire CLI, .claude config, CLAUDE.md auto-generation
Enable auto-commit tracking, git-sync hooks, session recovery, and anonymous identity for the new repo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
356
.entire/generate-claude-md.sh
Executable file
356
.entire/generate-claude-md.sh
Executable file
@@ -0,0 +1,356 @@
|
||||
#!/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="<!-- auto-generated by generate-claude-md.sh -->"
|
||||
|
||||
# --- 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 <tag>`
|
||||
|
||||
**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<VERSION>/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 <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 <delta-cloud-208e@device.local>`
|
||||
- Wrong: `root <root@hostname.example.com>` or `John <john@gmail.com>`
|
||||
- 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)"
|
||||
Reference in New Issue
Block a user