Add ai-code-project-template repo files.
This commit is contained in:
119
.claude/tools/make-check.sh
Executable file
119
.claude/tools/make-check.sh
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Claude Code make check hook script
|
||||
# Intelligently finds and runs 'make check' from the appropriate directory
|
||||
|
||||
# Ensure proper environment for make to find /bin/sh
|
||||
export PATH="/bin:/usr/bin:$PATH"
|
||||
export SHELL="/bin/bash"
|
||||
#
|
||||
# Expected JSON input format from stdin:
|
||||
# {
|
||||
# "session_id": "abc123",
|
||||
# "transcript_path": "/path/to/transcript.jsonl",
|
||||
# "cwd": "/path/to/project/subdir",
|
||||
# "hook_event_name": "PostToolUse",
|
||||
# "tool_name": "Write",
|
||||
# "tool_input": {
|
||||
# "file_path": "/path/to/file.txt",
|
||||
# "content": "..."
|
||||
# },
|
||||
# "tool_response": {
|
||||
# "filePath": "/path/to/file.txt",
|
||||
# "success": true
|
||||
# }
|
||||
# }
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read JSON from stdin
|
||||
JSON_INPUT=$(cat)
|
||||
|
||||
# Debug: Log the JSON input to a file (comment out in production)
|
||||
# echo "DEBUG: JSON received at $(date):" >> /tmp/make-check-debug.log
|
||||
# echo "$JSON_INPUT" >> /tmp/make-check-debug.log
|
||||
|
||||
# Parse fields from JSON (using simple grep/sed for portability)
|
||||
CWD=$(echo "$JSON_INPUT" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
|
||||
TOOL_NAME=$(echo "$JSON_INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || echo "")
|
||||
|
||||
# Check if tool operation was successful
|
||||
SUCCESS=$(echo "$JSON_INPUT" | grep -o '"success"[[:space:]]*:[[:space:]]*[^,}]*' | sed 's/.*"success"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/' || echo "")
|
||||
|
||||
# Extract file_path from tool_input if available
|
||||
FILE_PATH=$(echo "$JSON_INPUT" | grep -o '"tool_input"[[:space:]]*:[[:space:]]*{[^}]*}' | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' || true)
|
||||
|
||||
# If tool operation failed, exit early
|
||||
if [[ "${SUCCESS:-}" == "false" ]]; then
|
||||
echo "Skipping 'make check' - tool operation failed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Log what tool was used
|
||||
if [[ -n "${TOOL_NAME:-}" ]]; then
|
||||
echo "Post-hook for $TOOL_NAME tool"
|
||||
fi
|
||||
|
||||
# Determine the starting directory
|
||||
# Priority: 1) Directory of edited file, 2) CWD, 3) Current directory
|
||||
START_DIR=""
|
||||
if [[ -n "${FILE_PATH:-}" ]]; then
|
||||
# Use directory of the edited file
|
||||
FILE_DIR=$(dirname "$FILE_PATH")
|
||||
if [[ -d "$FILE_DIR" ]]; then
|
||||
START_DIR="$FILE_DIR"
|
||||
echo "Using directory of edited file: $FILE_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$START_DIR" ]] && [[ -n "${CWD:-}" ]]; then
|
||||
START_DIR="$CWD"
|
||||
elif [[ -z "$START_DIR" ]]; then
|
||||
START_DIR=$(pwd)
|
||||
fi
|
||||
|
||||
# Function to find project root (looks for .git or Makefile going up the tree)
|
||||
find_project_root() {
|
||||
local dir="$1"
|
||||
while [[ "$dir" != "/" ]]; do
|
||||
if [[ -f "$dir/Makefile" ]] || [[ -d "$dir/.git" ]]; then
|
||||
echo "$dir"
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to check if make target exists
|
||||
make_target_exists() {
|
||||
local dir="$1"
|
||||
local target="$2"
|
||||
if [[ -f "$dir/Makefile" ]]; then
|
||||
# Check if target exists in Makefile
|
||||
make -C "$dir" -n "$target" &>/dev/null
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Start from the determined directory
|
||||
cd "$START_DIR"
|
||||
|
||||
# Check if there's a local Makefile with 'check' target
|
||||
if make_target_exists "." "check"; then
|
||||
echo "Running 'make check' in directory: $START_DIR"
|
||||
make check
|
||||
else
|
||||
# Find the project root
|
||||
PROJECT_ROOT=$(find_project_root "$START_DIR")
|
||||
|
||||
if [[ -n "$PROJECT_ROOT" ]] && make_target_exists "$PROJECT_ROOT" "check"; then
|
||||
echo "Running 'make check' from project root: $PROJECT_ROOT"
|
||||
cd "$PROJECT_ROOT"
|
||||
make check
|
||||
else
|
||||
echo "Error: No Makefile with 'check' target found in current directory or project root"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
218
.claude/tools/notify.sh
Executable file
218
.claude/tools/notify.sh
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Claude Code notification hook script
|
||||
# Reads JSON from stdin and sends desktop notifications
|
||||
#
|
||||
# Expected JSON input format:
|
||||
# {
|
||||
# "session_id": "abc123",
|
||||
# "transcript_path": "/path/to/transcript.jsonl",
|
||||
# "cwd": "/path/to/project",
|
||||
# "hook_event_name": "Notification",
|
||||
# "message": "Task completed successfully"
|
||||
# }
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Check for debug flag
|
||||
DEBUG=false
|
||||
LOG_FILE="/tmp/claude-code-notify-$(date +%Y%m%d-%H%M%S).log"
|
||||
if [[ "${1:-}" == "--debug" ]]; then
|
||||
DEBUG=true
|
||||
shift
|
||||
fi
|
||||
|
||||
# Debug logging function
|
||||
debug_log() {
|
||||
if [[ "$DEBUG" == "true" ]]; then
|
||||
local msg="[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') - $*"
|
||||
echo "$msg" >&2
|
||||
echo "$msg" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
debug_log "Script started with args: $*"
|
||||
debug_log "Working directory: $(pwd)"
|
||||
debug_log "Platform: $(uname -s)"
|
||||
|
||||
# Read JSON from stdin
|
||||
debug_log "Reading JSON from stdin..."
|
||||
JSON_INPUT=$(cat)
|
||||
debug_log "JSON input received: $JSON_INPUT"
|
||||
|
||||
# Parse JSON fields (using simple grep/sed for portability)
|
||||
debug_log "Parsing JSON fields..."
|
||||
MESSAGE=$(echo "$JSON_INPUT" | grep -o '"message"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"message"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
CWD=$(echo "$JSON_INPUT" | grep -o '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
SESSION_ID=$(echo "$JSON_INPUT" | grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
debug_log "Parsed MESSAGE: $MESSAGE"
|
||||
debug_log "Parsed CWD: $CWD"
|
||||
debug_log "Parsed SESSION_ID: $SESSION_ID"
|
||||
|
||||
# Get project name from cwd
|
||||
PROJECT=""
|
||||
debug_log "Determining project name..."
|
||||
if [[ -n "$CWD" ]]; then
|
||||
debug_log "CWD is not empty, checking if it's a git repo..."
|
||||
# Check if it's a git repo
|
||||
if [[ -d "$CWD/.git" ]]; then
|
||||
debug_log "Found .git directory, attempting to get git remote..."
|
||||
cd "$CWD"
|
||||
PROJECT=$(basename -s .git "$(git config --get remote.origin.url 2>/dev/null || true)" 2>/dev/null || true)
|
||||
[[ -z "$PROJECT" ]] && PROJECT=$(basename "$CWD")
|
||||
debug_log "Git-based project name: $PROJECT"
|
||||
else
|
||||
debug_log "Not a git repo, using directory name"
|
||||
PROJECT=$(basename "$CWD")
|
||||
debug_log "Directory-based project name: $PROJECT"
|
||||
fi
|
||||
else
|
||||
debug_log "CWD is empty, PROJECT will remain empty"
|
||||
fi
|
||||
|
||||
# Set app name
|
||||
APP_NAME="Claude Code"
|
||||
|
||||
# Fallback if message is empty
|
||||
[[ -z "$MESSAGE" ]] && MESSAGE="Notification"
|
||||
|
||||
# Add session info to help identify which terminal/tab
|
||||
SESSION_SHORT=""
|
||||
if [[ -n "$SESSION_ID" ]]; then
|
||||
# Get last 6 chars of session ID for display
|
||||
SESSION_SHORT="${SESSION_ID: -6}"
|
||||
debug_log "Session short ID: $SESSION_SHORT"
|
||||
fi
|
||||
|
||||
debug_log "Final values:"
|
||||
debug_log " APP_NAME: $APP_NAME"
|
||||
debug_log " PROJECT: $PROJECT"
|
||||
debug_log " MESSAGE: $MESSAGE"
|
||||
debug_log " SESSION_SHORT: $SESSION_SHORT"
|
||||
|
||||
# Platform-specific notification
|
||||
PLATFORM="$(uname -s)"
|
||||
debug_log "Detected platform: $PLATFORM"
|
||||
case "$PLATFORM" in
|
||||
Darwin*) # macOS
|
||||
if [[ -n "$PROJECT" ]]; then
|
||||
osascript -e "display notification \"$MESSAGE\" with title \"$APP_NAME\" subtitle \"$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}\""
|
||||
else
|
||||
osascript -e "display notification \"$MESSAGE\" with title \"$APP_NAME\""
|
||||
fi
|
||||
;;
|
||||
|
||||
Linux*)
|
||||
debug_log "Linux platform detected, checking if WSL..."
|
||||
# Check if WSL
|
||||
if grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
debug_log "WSL detected, will use Windows toast notifications"
|
||||
# WSL - use Windows toast notifications
|
||||
if [[ -n "$PROJECT" ]]; then
|
||||
debug_log "Sending WSL notification with project: $PROJECT"
|
||||
powershell.exe -Command "
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
|
||||
\$APP_ID = '$APP_NAME'
|
||||
\$template = @\"
|
||||
<toast><visual><binding template='ToastText02'>
|
||||
<text id='1'>$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}</text>
|
||||
<text id='2'>$MESSAGE</text>
|
||||
</binding></visual></toast>
|
||||
\"@
|
||||
\$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
\$xml.LoadXml(\$template)
|
||||
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$xml
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(\$APP_ID).Show(\$toast)
|
||||
" 2>/dev/null || echo "[$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}] $MESSAGE"
|
||||
else
|
||||
debug_log "Sending WSL notification without project (message only)"
|
||||
powershell.exe -Command "
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
|
||||
\$APP_ID = '$APP_NAME'
|
||||
\$template = @\"
|
||||
<toast><visual><binding template='ToastText01'>
|
||||
<text id='1'>$MESSAGE</text>
|
||||
</binding></visual></toast>
|
||||
\"@
|
||||
\$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
\$xml.LoadXml(\$template)
|
||||
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$xml
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(\$APP_ID).Show(\$toast)
|
||||
" 2>/dev/null || echo "$MESSAGE"
|
||||
fi
|
||||
else
|
||||
# Native Linux - use notify-send
|
||||
if command -v notify-send >/dev/null 2>&1; then
|
||||
if [[ -n "$PROJECT" ]]; then
|
||||
notify-send "<b>$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}</b>" "$MESSAGE"
|
||||
else
|
||||
notify-send "Claude Code" "$MESSAGE"
|
||||
fi
|
||||
else
|
||||
if [[ -n "$PROJECT" ]]; then
|
||||
echo "[$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}] $MESSAGE"
|
||||
else
|
||||
echo "$MESSAGE"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
CYGWIN*|MINGW*|MSYS*) # Windows
|
||||
if [[ -n "$PROJECT" ]]; then
|
||||
powershell.exe -Command "
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
|
||||
\$APP_ID = '$APP_NAME'
|
||||
\$template = @\"
|
||||
<toast><visual><binding template='ToastText02'>
|
||||
<text id='1'>$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}</text>
|
||||
<text id='2'>$MESSAGE</text>
|
||||
</binding></visual></toast>
|
||||
\"@
|
||||
\$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
\$xml.LoadXml(\$template)
|
||||
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$xml
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(\$APP_ID).Show(\$toast)
|
||||
" 2>/dev/null || echo "[$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}] $MESSAGE"
|
||||
else
|
||||
powershell.exe -Command "
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
||||
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
||||
|
||||
\$APP_ID = '$APP_NAME'
|
||||
\$template = @\"
|
||||
<toast><visual><binding template='ToastText01'>
|
||||
<text id='1'>$MESSAGE</text>
|
||||
</binding></visual></toast>
|
||||
\"@
|
||||
\$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||
\$xml.LoadXml(\$template)
|
||||
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$xml
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier(\$APP_ID).Show(\$toast)
|
||||
" 2>/dev/null || echo "$MESSAGE"
|
||||
fi
|
||||
;;
|
||||
|
||||
*) # Unknown OS
|
||||
if [[ -n "$PROJECT" ]]; then
|
||||
echo "[$PROJECT${SESSION_SHORT:+ ($SESSION_SHORT)}] $MESSAGE"
|
||||
else
|
||||
echo "$MESSAGE"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
debug_log "Script completed"
|
||||
if [[ "$DEBUG" == "true" ]]; then
|
||||
echo "[DEBUG] Log file saved to: $LOG_FILE" >&2
|
||||
fi
|
121
.claude/tools/subagent-logger.py
Executable file
121
.claude/tools/subagent-logger.py
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
def ensure_log_directory() -> Path:
|
||||
"""Ensure the log directory exists and return its path."""
|
||||
# Get the project root (where the script is called from)
|
||||
project_root = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
|
||||
log_dir = project_root / ".data" / "subagent-logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
return log_dir
|
||||
|
||||
|
||||
def create_log_entry(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Create a structured log entry from the hook data."""
|
||||
tool_input = data.get("tool_input", {})
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"session_id": data.get("session_id"),
|
||||
"cwd": data.get("cwd"),
|
||||
"subagent_type": tool_input.get("subagent_type"),
|
||||
"description": tool_input.get("description"),
|
||||
"prompt_length": len(tool_input.get("prompt", "")),
|
||||
"prompt": tool_input.get("prompt", ""), # Store full prompt for debugging
|
||||
}
|
||||
|
||||
|
||||
def log_subagent_usage(data: dict[str, Any]) -> None:
|
||||
"""Log subagent usage to a daily log file."""
|
||||
log_dir = ensure_log_directory()
|
||||
|
||||
# Create daily log file
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
log_file = log_dir / f"subagent-usage-{today}.jsonl"
|
||||
|
||||
# Create log entry
|
||||
log_entry = create_log_entry(data)
|
||||
|
||||
# Append to log file (using JSONL format for easy parsing)
|
||||
with open(log_file, "a") as f:
|
||||
f.write(json.dumps(log_entry) + "\n")
|
||||
|
||||
# Also create/update a summary file
|
||||
update_summary(log_dir, log_entry)
|
||||
|
||||
|
||||
def update_summary(log_dir: Path, log_entry: dict[str, Any]) -> None:
|
||||
"""Update the summary file with aggregated statistics."""
|
||||
summary_file = log_dir / "summary.json"
|
||||
|
||||
# Load existing summary or create new one
|
||||
if summary_file.exists():
|
||||
with open(summary_file) as f:
|
||||
summary = json.load(f)
|
||||
else:
|
||||
summary = {
|
||||
"total_invocations": 0,
|
||||
"subagent_counts": {},
|
||||
"first_invocation": None,
|
||||
"last_invocation": None,
|
||||
"sessions": set(),
|
||||
}
|
||||
|
||||
# Convert sessions to set if loading from JSON (where it's a list)
|
||||
if isinstance(summary.get("sessions"), list):
|
||||
summary["sessions"] = set(summary["sessions"])
|
||||
|
||||
# Update summary
|
||||
summary["total_invocations"] += 1
|
||||
|
||||
subagent_type = log_entry["subagent_type"]
|
||||
if subagent_type:
|
||||
summary["subagent_counts"][subagent_type] = summary["subagent_counts"].get(subagent_type, 0) + 1
|
||||
|
||||
if not summary["first_invocation"]:
|
||||
summary["first_invocation"] = log_entry["timestamp"]
|
||||
summary["last_invocation"] = log_entry["timestamp"]
|
||||
|
||||
if log_entry["session_id"]:
|
||||
summary["sessions"].add(log_entry["session_id"])
|
||||
|
||||
# Convert sessions set to list for JSON serialization
|
||||
summary_to_save = summary.copy()
|
||||
summary_to_save["sessions"] = list(summary["sessions"])
|
||||
summary_to_save["unique_sessions"] = len(summary["sessions"])
|
||||
|
||||
# Save updated summary
|
||||
with open(summary_file, "w") as f:
|
||||
json.dump(summary_to_save, f, indent=2)
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
# Silently fail to not disrupt Claude's workflow
|
||||
print(f"Warning: Could not parse JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
# Only process if this is a Task tool for subagents
|
||||
if data.get("hook_event_name") == "PreToolUse" and data.get("tool_name") == "Task":
|
||||
try:
|
||||
log_subagent_usage(data)
|
||||
except Exception as e:
|
||||
# Log error but don't block Claude's operation
|
||||
print(f"Warning: Failed to log subagent usage: {e}", file=sys.stderr)
|
||||
|
||||
# Always exit successfully to not block Claude's workflow
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user