diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..09809e9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,102 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Task", + "hooks": [ + { + "type": "command", + "command": "entire hooks claude-code post-task" + } + ] + }, + { + "matcher": "TodoWrite", + "hooks": [ + { + "type": "command", + "command": "entire hooks claude-code post-todo" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Task", + "hooks": [ + { + "type": "command", + "command": "entire hooks claude-code pre-task" + } + ] + } + ], + "SessionEnd": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "entire hooks claude-code session-end" + }, + { + "type": "command", + "command": "bash .entire/git-sync.sh push 2>/dev/null || true" + } + ] + } + ], + "SessionStart": [ + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "bash .entire/git-sync.sh pull 2>&1 || true; echo '=== CONTEXT RESTORED AFTER COMPACTION ==='; echo \"Project: $(basename $(git rev-parse --show-toplevel 2>/dev/null || pwd))\"; echo 'Strategy: auto-commit'; echo \"Remote: $(git remote get-url $(git remote | head -1) 2>/dev/null || echo 'not set')\"; echo '=== Recent commits ==='; git log --oneline -10 2>/dev/null; echo '=== END CONTEXT ==='" + } + ] + }, + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "bash .entire/git-sync.sh pull 2>&1 || true; bash .entire/generate-claude-md.sh . 2>/dev/null; entire hooks claude-code session-start" + } + ] + } + ], + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "entire hooks claude-code stop" + }, + { + "type": "command", + "command": "bash .entire/git-sync.sh push 2>/dev/null || true" + } + ] + } + ], + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "entire hooks claude-code user-prompt-submit" + } + ] + } + ] + }, + "permissions": { + "defaultMode": "bypassPermissions", + "deny": [ + "Read(./.entire/metadata/**)" + ] + } +} diff --git a/.entire/.gitignore b/.entire/.gitignore new file mode 100644 index 0000000..2cffdef --- /dev/null +++ b/.entire/.gitignore @@ -0,0 +1,4 @@ +tmp/ +settings.local.json +metadata/ +logs/ diff --git a/.entire/generate-claude-md.sh b/.entire/generate-claude-md.sh new file mode 100755 index 0000000..a2c774e --- /dev/null +++ b/.entire/generate-claude-md.sh @@ -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="" + +# --- 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)" diff --git a/.entire/generate-device-id.sh b/.entire/generate-device-id.sh new file mode 100755 index 0000000..f722294 --- /dev/null +++ b/.entire/generate-device-id.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# ============================================================================= +# generate-device-id.sh — Generate anonymous device nickname for git commits +# +# Creates a persistent device identity like "phantom-fox-a3b2" that: +# - Hides real username, hostname, and IP +# - Stays the same across sessions on the same machine +# - Is unique per device (based on hardware ID hash) +# ============================================================================= + +set -euo pipefail + +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/entire" +DEVICE_ID_FILE="$CONFIG_DIR/device-id" + +# If already generated, just output it +if [ -f "$DEVICE_ID_FILE" ]; then + cat "$DEVICE_ID_FILE" + exit 0 +fi + +# --- Generate hardware fingerprint --- +get_machine_id() { + # Try multiple sources for a stable machine ID + if [ -f "/etc/machine-id" ]; then + cat /etc/machine-id + elif [ -f "/var/lib/dbus/machine-id" ]; then + cat /var/lib/dbus/machine-id + elif command -v ioreg &>/dev/null; then + # macOS: IOPlatformSerialNumber + ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | grep -o '"IOPlatformSerialNumber" = "[^"]*"' | sed 's/.*= "//;s/"//' + elif command -v wmic &>/dev/null; then + # Windows + wmic csproduct get uuid 2>/dev/null | tail -1 | tr -d ' \r\n' + else + # Fallback: hostname + username hash + echo "$(hostname)$(whoami)$(date +%s)" + fi +} + +# --- Word lists for readable nicknames --- +ADJECTIVES=( + phantom silent cosmic rapid swift + frozen arctic lunar solar neon + hidden shadow cipher binary quantum + iron steel chrome cobalt silver + alpha delta omega sigma theta + rogue ghost echo pulse drift +) + +NOUNS=( + fox wolf hawk bear lynx + node core byte mesh grid + spark flame pulse wave beam + tower vault forge nexus axis + rover scout pilot agent proxy + cloud storm frost ember flare +) + +# --- Deterministic selection from hardware ID --- +MACHINE_ID=$(get_machine_id) +if command -v sha256sum &>/dev/null; then + HASH=$(echo -n "$MACHINE_ID" | sha256sum | cut -c1-8) +elif command -v shasum &>/dev/null; then + HASH=$(echo -n "$MACHINE_ID" | shasum -a 256 | cut -c1-8) +else + HASH=$(echo -n "$MACHINE_ID" | md5sum 2>/dev/null | cut -c1-8 || echo "00000000") +fi + +# Convert first 4 hex chars to indices +ADJ_IDX=$(( 16#${HASH:0:2} % ${#ADJECTIVES[@]} )) +NOUN_IDX=$(( 16#${HASH:2:2} % ${#NOUNS[@]} )) +SUFFIX="${HASH:4:4}" + +DEVICE_NAME="${ADJECTIVES[$ADJ_IDX]}-${NOUNS[$NOUN_IDX]}-${SUFFIX}" + +# --- Persist --- +mkdir -p "$CONFIG_DIR" +echo "$DEVICE_NAME" > "$DEVICE_ID_FILE" + +echo "$DEVICE_NAME" diff --git a/.entire/git-sync.sh b/.entire/git-sync.sh new file mode 100755 index 0000000..1fc875e --- /dev/null +++ b/.entire/git-sync.sh @@ -0,0 +1,583 @@ +#!/bin/bash +# ============================================================================= +# git-sync.sh — Multi-machine git sync for Entire CLI projects +# ============================================================================= +# Usage: +# bash .entire/git-sync.sh pull # SessionStart: fetch + rebase +# bash .entire/git-sync.sh push # post-commit: push to remote (background) +# bash .entire/git-sync.sh status # Diagnostics +# bash .entire/git-sync.sh cleanup # Remove old backup tags +# ============================================================================= + +set -uo pipefail + +SYNC_DIR=".entire/tmp" +SYNC_LOG="$SYNC_DIR/sync.log" +SYNC_LOCK="$SYNC_DIR/sync.lock" +NETWORK_TIMEOUT=30 +BACKUP_TAG_PREFIX="sync/backup" +DAILY_TAG_PREFIX="sync/daily" +BACKUP_MAX_AGE_DAYS=7 +DAILY_MAX_AGE_DAYS=30 +ARCHIVE_INTERVAL_DAYS=3 +ARCHIVE_BRANCH="archive/snapshots" +ARCHIVE_LOCAL_DIR=".entire/tmp/archives" +STALE_LOCK_SECONDS=120 + +# --- Logging --- +log() { + mkdir -p "$SYNC_DIR" + local ts + ts=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$ts] $*" >> "$SYNC_LOG" +} + +log_and_echo() { + log "$@" + echo "[git-sync] $*" +} + +# --- Lock management --- +acquire_lock() { + mkdir -p "$SYNC_DIR" + + # Remove stale lock + if [ -f "$SYNC_LOCK" ]; then + local lock_pid lock_age + lock_pid=$(cat "$SYNC_LOCK" 2>/dev/null || echo "") + lock_age=$(( $(date +%s) - $(stat -c %Y "$SYNC_LOCK" 2>/dev/null || stat -f %m "$SYNC_LOCK" 2>/dev/null || echo 0) )) + + if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then + log "Removing lock from dead process (PID: $lock_pid)" + rm -f "$SYNC_LOCK" + elif [ "$lock_age" -gt "$STALE_LOCK_SECONDS" ]; then + log "Removing stale lock (age: ${lock_age}s)" + rm -f "$SYNC_LOCK" + else + log "Lock held by process $lock_pid (age: ${lock_age}s), skipping" + return 1 + fi + fi + + echo $$ > "$SYNC_LOCK" + return 0 +} + +release_lock() { + rm -f "$SYNC_LOCK" +} + +# --- Detect current branch --- +current_branch() { + git symbolic-ref --short HEAD 2>/dev/null +} + +# --- Check if we should sync (only main/master) --- +should_sync() { + local branch + branch=$(current_branch) + if [ -z "$branch" ]; then + log "Detached HEAD, skipping sync" + return 1 + fi + case "$branch" in + main|master) return 0 ;; + *) + log "Branch '$branch' is not main/master, skipping sync" + return 1 + ;; + esac +} + +# --- Detect remote for current branch --- +detect_remote() { + local branch + branch=$(current_branch) + local remote + remote=$(git config "branch.${branch}.remote" 2>/dev/null) + if [ -n "$remote" ]; then + echo "$remote" + return 0 + fi + + # Fallback: try first available remote + remote=$(git remote | head -1) + if [ -n "$remote" ]; then + echo "$remote" + return 0 + fi + + return 1 +} + +# --- Create backup tag --- +create_backup_tag() { + local ts + ts=$(date '+%Y%m%d-%H%M%S') + local tag="${BACKUP_TAG_PREFIX}/pre-rebase/${ts}" + git tag "$tag" HEAD 2>/dev/null && log "Backup tag: $tag" +} + +# --- Daily snapshot tag with summary --- +create_daily_tag() { + local today + today=$(date '+%Y-%m-%d') + local tag="${DAILY_TAG_PREFIX}/${today}" + + # Skip if today's tag already exists + if git tag -l "$tag" 2>/dev/null | grep -q .; then + return 0 + fi + + # Find yesterday's tag or the most recent daily tag + local prev_ref="" + prev_ref=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | sort -r | head -1) + + # Build summary of commits since last daily tag (or last 24h) + local summary="" + if [ -n "$prev_ref" ]; then + summary=$(git log --oneline "${prev_ref}..HEAD" 2>/dev/null | head -20) + else + summary=$(git log --oneline --since="yesterday" 2>/dev/null | head -20) + fi + + if [ -z "$summary" ]; then + summary="(no new commits)" + fi + + local commit_count + if [ -n "$prev_ref" ]; then + commit_count=$(git rev-list --count "${prev_ref}..HEAD" 2>/dev/null || echo 0) + else + commit_count=$(git log --oneline --since="yesterday" 2>/dev/null | wc -l) + fi + + # Create annotated tag with summary as message + local msg + msg=$(printf "Daily snapshot %s (%s commits)\n\n%s" "$today" "$commit_count" "$summary") + git tag -a "$tag" -m "$msg" HEAD 2>/dev/null && log "Daily tag: $tag ($commit_count commits)" +} + +# --- Periodic archive: full snapshot to archive/snapshots branch --- +create_periodic_archive() { + local today + today=$(date '+%Y-%m-%d') + local branch + branch=$(current_branch) + local project_name + project_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)") + + # Check if archive branch exists, get last archive date + local last_archive_date="0000-00-00" + if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then + # Get date from last commit message on archive branch + last_archive_date=$(git log "$ARCHIVE_BRANCH" -1 --format="%s" 2>/dev/null | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1 || echo "0000-00-00") + fi + + # Check interval: skip if last archive is less than ARCHIVE_INTERVAL_DAYS ago + if [ "$last_archive_date" != "0000-00-00" ]; then + local last_epoch today_epoch + last_epoch=$(date -d "$last_archive_date" +%s 2>/dev/null || date -j -f '%Y-%m-%d' "$last_archive_date" +%s 2>/dev/null || echo 0) + today_epoch=$(date +%s) + local diff_days=$(( (today_epoch - last_epoch) / 86400 )) + if [ "$diff_days" -lt "$ARCHIVE_INTERVAL_DAYS" ]; then + log "Archive skipped: last archive $last_archive_date ($diff_days days ago, interval=${ARCHIVE_INTERVAL_DAYS})" + return 0 + fi + fi + + # Check if there are new commits since last archive + local new_commits=0 + if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then + # Find the source commit hash stored in last archive + local last_source_hash + last_source_hash=$(git log "$ARCHIVE_BRANCH" -1 --format="%b" 2>/dev/null | sed -n 's/^Source: \([a-f0-9]*\).*/\1/p' | head -1) + if [ -n "$last_source_hash" ] && git rev-parse --verify "$last_source_hash" &>/dev/null; then + new_commits=$(git rev-list --count "${last_source_hash}..HEAD" 2>/dev/null || echo 0) + else + new_commits=1 # Can't determine, assume changes + fi + else + new_commits=$(git rev-list --count HEAD 2>/dev/null || echo 1) + fi + + if [ "$new_commits" -eq 0 ]; then + log "Archive skipped: 0 new commits since last archive" + return 0 + fi + + log_and_echo "Creating archive ($new_commits new commits since last snapshot)..." + + local short_hash + short_hash=$(git rev-parse --short HEAD 2>/dev/null) + local archive_name="${project_name}_${today}_${short_hash}.tar.gz" + + # Create archive in temp + local tmpdir + tmpdir=$(mktemp -d) + + if ! git archive --format=tar.gz --prefix="${project_name}/" -o "${tmpdir}/${archive_name}" HEAD 2>>"$SYNC_LOG"; then + log "Archive creation failed" + rm -rf "$tmpdir" + return 1 + fi + + local size + size=$(du -h "${tmpdir}/${archive_name}" | cut -f1) + + # Build changelog + local changelog="${tmpdir}/CHANGELOG.md" + local prev_tag + prev_tag=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | sort -r | head -1) + + { + echo "# Archive ${today} (${branch}@${short_hash})" + echo "" + echo "**Project**: ${project_name}" + echo "**Branch**: ${branch}" + echo "**Commit**: ${short_hash} ($(git log -1 --format='%s' HEAD 2>/dev/null))" + echo "**Size**: ${size}" + echo "**Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "" + echo "## Commits since last archive" + echo "" + + if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then + local last_src + last_src=$(git log "$ARCHIVE_BRANCH" -1 --format="%b" 2>/dev/null | sed -n 's/^Source: \([a-f0-9]*\).*/\1/p' | head -1) + if [ -n "$last_src" ] && git rev-parse --verify "$last_src" &>/dev/null; then + git log --oneline "${last_src}..HEAD" 2>/dev/null | while read -r line; do + echo "- ${line}" + done + else + git log --oneline -20 HEAD 2>/dev/null | while read -r line; do + echo "- ${line}" + done + fi + else + git log --oneline -50 HEAD 2>/dev/null | while read -r line; do + echo "- ${line}" + done + fi + } > "$changelog" + + # Save local copy + mkdir -p "$ARCHIVE_LOCAL_DIR" + cp "${tmpdir}/${archive_name}" "$ARCHIVE_LOCAL_DIR/" + cp "${tmpdir}/CHANGELOG.md" "${ARCHIVE_LOCAL_DIR}/CHANGELOG_${today}.md" + log "Local archive copy: ${ARCHIVE_LOCAL_DIR}/${archive_name}" + + # Cleanup local archives older than 30 days + local cutoff + cutoff=$(date -d "-30 days" '+%Y-%m-%d' 2>/dev/null || \ + date -v "-30d" '+%Y-%m-%d' 2>/dev/null || echo "0000-00-00") + for f in "${ARCHIVE_LOCAL_DIR}/${project_name}_"*.tar.gz; do + [ -f "$f" ] || continue + local fdate + fdate=$(echo "$f" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | tail -1) + if [ -n "$fdate" ] && [ "$fdate" \< "$cutoff" ]; then + rm -f "$f" + # Also remove matching changelog + rm -f "${ARCHIVE_LOCAL_DIR}/CHANGELOG_${fdate}.md" + fi + done + + # Switch to archive branch (orphan if new), commit, switch back + local original_branch + original_branch=$(current_branch) + local archive_stashed=false + + # Safety trap: restore branch on interrupt + archive_cleanup() { + if [ "$(current_branch 2>/dev/null)" != "$original_branch" ]; then + git checkout "$original_branch" 2>/dev/null || true + fi + if [ "$archive_stashed" = true ]; then + git stash pop 2>/dev/null || true + fi + } + trap archive_cleanup INT TERM + + if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then + git stash push -m "archive-stash" 2>/dev/null && archive_stashed=true + fi + + if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then + git checkout "$ARCHIVE_BRANCH" 2>>"$SYNC_LOG" + else + git checkout --orphan "$ARCHIVE_BRANCH" 2>>"$SYNC_LOG" + git rm -rf . 2>/dev/null || true + fi + + # Copy archive and changelog + cp "${tmpdir}/${archive_name}" . + cp "${tmpdir}/CHANGELOG.md" . + git add "${archive_name}" CHANGELOG.md 2>/dev/null + + # Commit with source hash in body for tracking + local full_hash + full_hash=$(git rev-parse "$original_branch" 2>/dev/null || echo "$short_hash") + git commit -m "snapshot ${today} (${branch}@${short_hash}, ${size})" \ + -m "Source: ${full_hash}" \ + 2>>"$SYNC_LOG" + + # Push archive branch + local remote + remote=$(detect_remote) || true + if [ -n "$remote" ]; then + timeout "$NETWORK_TIMEOUT" git push "$remote" "$ARCHIVE_BRANCH" 2>>"$SYNC_LOG" || \ + log "Archive branch push failed" + fi + + # Switch back and remove trap + git checkout "$original_branch" 2>>"$SYNC_LOG" + if [ "$archive_stashed" = true ]; then + git stash pop 2>/dev/null || true + fi + trap - INT TERM + + rm -rf "$tmpdir" + log_and_echo "Archive committed to ${ARCHIVE_BRANCH}: ${archive_name} (${size}, ${new_commits} commits)" +} + +# --- Push tags to remote --- +push_tags() { + local remote + remote=$(detect_remote) || return 0 + + # Push only sync/* tags (not all tags) + local tag + while IFS= read -r tag; do + [ -z "$tag" ] && continue + timeout "$NETWORK_TIMEOUT" git push "$remote" "refs/tags/$tag" 2>>"$SYNC_LOG" || \ + log "Failed to push tag: $tag" + done < <(git tag -l "sync/*" 2>/dev/null) + log "Sync tags pushed to $remote" +} + +# --- PULL: fetch + rebase --- +do_pull() { + if ! should_sync; then + echo "[git-sync] Skipped (not on main/master)" + return 0 + fi + + local remote + remote=$(detect_remote) || { + echo "[git-sync] No remote configured, skipping pull" + return 0 + } + + local branch + branch=$(current_branch) + + if ! acquire_lock; then + echo "[git-sync] Another sync in progress, skipping" + return 0 + fi + trap release_lock EXIT + + # Fetch with timeout + log "Fetching from $remote..." + if ! timeout "$NETWORK_TIMEOUT" git fetch "$remote" "$branch" 2>>"$SYNC_LOG"; then + log_and_echo "Fetch failed (network timeout or error), continuing with local state" + return 0 + fi + + # Create daily snapshot tag and periodic archive after fetch (has latest remote state) + create_daily_tag + create_periodic_archive + + # Check if we're behind + local behind + behind=$(git rev-list --count "HEAD..${remote}/${branch}" 2>/dev/null || echo 0) + if [ "$behind" -eq 0 ]; then + log_and_echo "Already up to date with ${remote}/${branch}" + return 0 + fi + + log_and_echo "Behind ${remote}/${branch} by $behind commit(s), syncing..." + + # Create backup tag before rebase + create_backup_tag + + # Stash dirty changes if any + local stashed=false + if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then + log "Stashing dirty working tree..." + git stash push -m "git-sync auto-stash $(date '+%Y%m%d-%H%M%S')" 2>>"$SYNC_LOG" && stashed=true + fi + + # Rebase + if git rebase "${remote}/${branch}" 2>>"$SYNC_LOG"; then + log_and_echo "Rebased successfully onto ${remote}/${branch} (+$behind commits)" + else + log_and_echo "WARNING: Rebase conflict! Aborting rebase, backup tag preserved" + git rebase --abort 2>/dev/null + if [ "$stashed" = true ]; then + git stash pop 2>>"$SYNC_LOG" || true + fi + return 1 + fi + + # Restore stashed changes + if [ "$stashed" = true ]; then + if git stash pop 2>>"$SYNC_LOG"; then + log "Stash restored successfully" + else + log_and_echo "WARNING: Stash pop conflict! Your changes are saved in git stash" + fi + fi + + return 0 +} + +# --- PUSH: push to remote --- +do_push() { + if ! should_sync; then + return 0 + fi + + local remote + remote=$(detect_remote) || return 0 + + local branch + branch=$(current_branch) + + if ! acquire_lock; then + return 0 + fi + trap release_lock EXIT + + log "Pushing to ${remote}/${branch}..." + if timeout "$NETWORK_TIMEOUT" git push "$remote" "$branch" 2>>"$SYNC_LOG"; then + log "Push successful" + # Push tags to remote after successful push + push_tags + else + log "Push failed (likely behind remote). Will reconcile at next session pull." + fi +} + +# --- STATUS: diagnostics --- +do_status() { + local branch + branch=$(current_branch) + echo "[git-sync] Status" + echo " Branch: ${branch:-DETACHED}" + + if ! should_sync; then + echo " Sync: DISABLED (not on main/master)" + return 0 + fi + + local remote + remote=$(detect_remote) || { + echo " Remote: NONE" + echo " Sync: DISABLED (no remote)" + return 0 + } + echo " Remote: $remote" + + # Fetch silently for status check + timeout "$NETWORK_TIMEOUT" git fetch "$remote" "$branch" 2>/dev/null + + local ahead behind + ahead=$(git rev-list --count "${remote}/${branch}..HEAD" 2>/dev/null || echo "?") + behind=$(git rev-list --count "HEAD..${remote}/${branch}" 2>/dev/null || echo "?") + echo " Ahead: $ahead | Behind: $behind" + + local backup_count daily_count + backup_count=$(git tag -l "${BACKUP_TAG_PREFIX}/*" 2>/dev/null | wc -l) + daily_count=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | wc -l) + echo " Backup tags: $backup_count" + echo " Daily snapshots: $daily_count" + + # Show recent daily tags with summaries + local latest_daily + latest_daily=$(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null | sort -r | head -3) + if [ -n "$latest_daily" ]; then + echo " Recent daily snapshots:" + echo "$latest_daily" | while read -r dtag; do + local dmsg + dmsg=$(git tag -n1 "$dtag" 2>/dev/null | sed "s|^${dtag}\s*||") + echo " $dtag — $dmsg" + done + fi + + # Show archive branch info + if git rev-parse --verify "$ARCHIVE_BRANCH" &>/dev/null; then + local archive_count + archive_count=$(git log "$ARCHIVE_BRANCH" --oneline 2>/dev/null | wc -l) + echo " Archive branch: $ARCHIVE_BRANCH ($archive_count snapshots)" + echo " Recent archives:" + git log "$ARCHIVE_BRANCH" --oneline -3 2>/dev/null | sed 's/^/ /' + else + echo " Archive branch: not created yet (interval: every ${ARCHIVE_INTERVAL_DAYS} days)" + fi + + if [ -f "$SYNC_LOG" ]; then + echo " Last log entries:" + tail -5 "$SYNC_LOG" | sed 's/^/ /' + fi + + echo " Sync: ENABLED" +} + +# --- CLEANUP: remove old backup tags --- +do_cleanup() { + local cutoff + cutoff=$(date -d "-${BACKUP_MAX_AGE_DAYS} days" '+%Y%m%d' 2>/dev/null || \ + date -v "-${BACKUP_MAX_AGE_DAYS}d" '+%Y%m%d' 2>/dev/null || echo "00000000") + + local count=0 + while IFS= read -r tag; do + [ -z "$tag" ] && continue + # Extract date from tag: sync/backup/pre-rebase/YYYYMMDD-HHMMSS + local tag_date + tag_date=$(echo "$tag" | grep -oE '[0-9]{8}' | head -1) + if [ -n "$tag_date" ] && [ "$tag_date" -lt "$cutoff" ] 2>/dev/null; then + git tag -d "$tag" 2>/dev/null + count=$((count + 1)) + fi + done < <(git tag -l "${BACKUP_TAG_PREFIX}/*" 2>/dev/null) + + # Clean old daily tags (older than DAILY_MAX_AGE_DAYS) + local daily_cutoff + daily_cutoff=$(date -d "-${DAILY_MAX_AGE_DAYS} days" '+%Y-%m-%d' 2>/dev/null || \ + date -v "-${DAILY_MAX_AGE_DAYS}d" '+%Y-%m-%d' 2>/dev/null || echo "0000-00-00") + + while IFS= read -r tag; do + [ -z "$tag" ] && continue + local tag_date + tag_date=$(echo "$tag" | sed "s|${DAILY_TAG_PREFIX}/||") + if [ "$tag_date" \< "$daily_cutoff" ] 2>/dev/null; then + git tag -d "$tag" 2>/dev/null + count=$((count + 1)) + fi + done < <(git tag -l "${DAILY_TAG_PREFIX}/*" 2>/dev/null) + + log_and_echo "Cleaned up $count old backup/daily tag(s)" + + # Trim sync log if > 1000 lines + if [ -f "$SYNC_LOG" ]; then + local lines + lines=$(wc -l < "$SYNC_LOG") + if [ "$lines" -gt 1000 ]; then + tail -500 "$SYNC_LOG" > "${SYNC_LOG}.tmp" && mv "${SYNC_LOG}.tmp" "$SYNC_LOG" + log "Trimmed sync log from $lines to 500 lines" + fi + fi +} + +# --- Main --- +case "${1:-}" in + pull) do_pull ;; + push) do_push ;; + status) do_status ;; + cleanup) do_cleanup ;; + *) + echo "Usage: bash .entire/git-sync.sh {pull|push|status|cleanup}" + exit 1 + ;; +esac diff --git a/.entire/settings.json b/.entire/settings.json new file mode 100644 index 0000000..df56c63 --- /dev/null +++ b/.entire/settings.json @@ -0,0 +1,11 @@ +{ + "strategy": "auto-commit", + "enabled": true, + "telemetry": false, + "strategy_options": { + "push_sessions": true, + "summarize": { + "enabled": true + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2ca991e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# gitea-token-access + +_Last updated: 2026-02-21T14:51:31Z_ + +## Tech Stack + + +## Git +- **Branch**: master +- **Remote `sensey`**: https://git.sensey24.ru/aibot777/gitea-token-access.git + +## Project Structure +Key directories: docs/ + +## Available Scripts + + +## Entire CLI +- **Status**: Enabled (auto-commit) +- **Strategy**: auto-commit +- **Telemetry**: disabled +- **Push sessions**: enabled + +## 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. + +## 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