commit fd0fb8eabde275c7ee679dcd5474d1710c517ab9 Author: Azat Date: Mon Feb 2 22:43:15 2026 +0100 Initial claude skill with skills access diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..cff2c3f --- /dev/null +++ b/SKILL.md @@ -0,0 +1,106 @@ +--- +name: claude +description: Claude Code CLI with skills access for self-modification +metadata: + version: "1.0.0" + vibestack: + main: false +--- + +# Claude Skill + +[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) with full access to the skills directory for autonomous agent capabilities. + +## Features + +- Claude Code CLI installation +- HTTP API for chat and execution +- Access to `/skills` directory for self-modification +- System prompt with skills context +- Streaming responses via SSE +- Auto-registers with Caddy if present + +## Configuration + +| Variable | Description | Default | +|----------|-------------|---------| +| `CLAUDE_PORT` | HTTP API port | `8888` | +| `CLAUDE_DOMAIN` | Domain for Caddy auto-config | (none) | +| `ANTHROPIC_API_KEY` | Anthropic API key | (required) | +| `CLAUDE_CODE_OAUTH_TOKEN` | OAuth token (alternative to API key) | (optional) | +| `CLAUDE_MODEL` | Model to use | `claude-sonnet-4-20250514` | +| `CLAUDE_MAX_TURNS` | Max conversation turns | `10` | +| `SKILLS_DIR` | Skills directory | `/skills` | + +## HTTP API + +### Chat (Streaming) + +```bash +curl -N http://localhost:8888/chat \ + -H "Content-Type: application/json" \ + -d '{"message": "List all skills and their purpose"}' +``` + +### Execute (One-shot) + +```bash +curl http://localhost:8888/execute \ + -H "Content-Type: application/json" \ + -d '{"prompt": "Create a new skill called hello-world that responds with Hello World"}' +``` + +### Health Check + +```bash +curl http://localhost:8888/health +``` + +## Capabilities + +Claude has access to: + +1. **Skills Directory** (`/skills/`) + - Read all skill configurations + - Modify existing skills + - Create new skills + - Push changes to Git + +2. **System Tools** + - Bash commands + - File operations + - Git operations + - Network requests + +3. **Other Skills** + - Query DuckDB + - Read logs from Loki + - Check metrics + +## System Prompt + +Claude receives context about: +- Available skills and their structure +- How to create/modify skills +- Git workflow for pushing changes +- API endpoints for other skills + +## Security + +- Claude runs with full system access +- Ensure proper authentication on the API endpoint +- Use `CLAUDE_DOMAIN` with Caddy for HTTPS +- Consider running behind authelia for protected access + +## Self-Modification Example + +```bash +curl http://localhost:8888/execute \ + -d '{"prompt": "Add a /hello endpoint to the caddy skill that returns a greeting"}' +``` + +Claude will: +1. Read the caddy skill files +2. Modify the necessary scripts +3. Commit and push changes +4. Report what was done diff --git a/scripts/autorun.sh b/scripts/autorun.sh new file mode 100644 index 0000000..d11d84f --- /dev/null +++ b/scripts/autorun.sh @@ -0,0 +1,190 @@ +#!/bin/bash +set -e + +SKILLS_DIR="${SKILLS_DIR:-/skills}" +SKILL_DIR="$(dirname "$(dirname "$0")")" + +# Install Node.js (required for Claude CLI) +install_node() { + if command -v node &>/dev/null; then + echo "Node.js already installed: $(node --version)" + return 0 + fi + + echo "Installing Node.js..." + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + + echo "Node.js installed: $(node --version)" +} + +# Install Claude Code CLI +install_claude() { + if command -v claude &>/dev/null; then + echo "Claude CLI already installed: $(claude --version)" + return 0 + fi + + echo "Installing Claude Code CLI..." + npm install -g @anthropic-ai/claude-code + + echo "Claude CLI installed: $(claude --version)" +} + +# Install socat for HTTP server +install_deps() { + if command -v socat &>/dev/null && command -v jq &>/dev/null; then + return 0 + fi + + echo "Installing dependencies..." + apt-get update && apt-get install -y socat jq +} + +# Generate system prompt with skills context +generate_system_prompt() { + local prompt_file="$SKILL_DIR/prompts/system.md" + + cat > "$prompt_file" << 'EOF' +# VibeStack Agent + +You are an autonomous agent running inside a VibeStack skill-based container. You have full access to the system and can modify skills. + +## Environment + +- Skills directory: /skills +- Each skill is a separate Git repository +- Skills have: SKILL.md (metadata), scripts/autorun.sh, scripts/run.sh + +## Available Skills + +EOF + + # List available skills + for skill_dir in "$SKILLS_DIR"/*/; do + local skill_name=$(basename "$skill_dir") + local skill_md="$skill_dir/SKILL.md" + + if [ -f "$skill_md" ]; then + local description=$(yq -r '.description // "No description"' < <(sed -n '/^---$/,/^---$/p' "$skill_md" | sed '1d;$d') 2>/dev/null || echo "No description") + echo "- **$skill_name**: $description" >> "$prompt_file" + fi + done + + cat >> "$prompt_file" << 'EOF' + +## Creating/Modifying Skills + +1. Skills are in /skills/{name}/ +2. Edit files directly +3. Commit and push changes: + ```bash + cd /skills/{name} + git add . + git commit -m "Description of changes" + git push + ``` + +## Skill Structure + +``` +skill-name/ +├── SKILL.md # YAML frontmatter + documentation +├── scripts/ +│ ├── autorun.sh # Setup/install (runs on skill load) +│ └── run.sh # Main entry point +└── config/ # Optional config files +``` + +## SKILL.md Format + +```yaml +--- +name: skill-name +description: What it does +metadata: + version: "1.0.0" + vibestack: + main: false + metrics-port: 8080 # If skill exposes metrics + requires: # Dependencies + - other-skill +--- + +# Skill Name + +Documentation... +``` + +## Other Skill APIs + +- **DuckDB**: POST http://localhost:8432/query {"sql": "..."} +- **Loki**: GET http://localhost:3100/loki/api/v1/query?query={job="skills"} +- **Metrics**: GET http://localhost:9090/metrics + +## Git Configuration + +Git is configured with token auth. You can push to: +- https://git.vibe-overflow.com/azat/{skill-name} + +To create a new skill repo, use the Gitea API or ask the user. +EOF + + echo "System prompt generated: $prompt_file" +} + +# Configure Caddy if present +configure_caddy() { + local caddy_dir="$SKILLS_DIR/caddy" + local claude_port="${CLAUDE_PORT:-8888}" + local claude_domain="${CLAUDE_DOMAIN:-}" + + if [ ! -d "$caddy_dir" ]; then + echo "Caddy not found - Claude API on port $claude_port" + return 0 + fi + + echo "Caddy detected - configuring reverse proxy..." + mkdir -p "$caddy_dir/snippets.d" + + local snippet="$caddy_dir/snippets.d/claude.caddy" + + if [ -n "$claude_domain" ]; then + cat > "$snippet" << EOF +# Auto-generated by claude skill +$claude_domain { + reverse_proxy localhost:$claude_port +} +EOF + echo "Caddy config: $claude_domain -> localhost:$claude_port" + else + cat > "$snippet" << EOF +# Auto-generated by claude skill +# Add to your site block: +# handle /claude/* { +# uri strip_prefix /claude +# reverse_proxy localhost:$claude_port +# } +EOF + echo "Caddy snippet created (manual config needed)" + fi +} + +# Validate authentication +check_auth() { + if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then + echo "WARNING: No authentication configured!" + echo "Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN" + else + echo "Authentication configured" + fi +} + +install_node +install_claude +install_deps +generate_system_prompt +configure_caddy +check_auth + +echo "Claude setup complete" diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..b3174fd --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,175 @@ +#!/bin/bash +set -e + +SKILL_DIR="$(dirname "$(dirname "$0")")" +CLAUDE_PORT="${CLAUDE_PORT:-8888}" +CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-20250514}" +CLAUDE_MAX_TURNS="${CLAUDE_MAX_TURNS:-10}" +SKILLS_DIR="${SKILLS_DIR:-/skills}" +SYSTEM_PROMPT_FILE="$SKILL_DIR/prompts/system.md" + +# Create HTTP handler +create_handler() { + cat > /tmp/claude_handler.sh << 'HANDLER' +#!/bin/bash + +SKILL_DIR="${SKILL_DIR}" +CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-20250514}" +CLAUDE_MAX_TURNS="${CLAUDE_MAX_TURNS:-10}" +SKILLS_DIR="${SKILLS_DIR:-/skills}" +SYSTEM_PROMPT_FILE="${SYSTEM_PROMPT_FILE}" + +# Read HTTP request +read -r request_line +method=$(echo "$request_line" | cut -d' ' -f1) +path=$(echo "$request_line" | cut -d' ' -f2) + +# Read headers +content_length=0 +while read -r header; do + header=$(echo "$header" | tr -d '\r') + [ -z "$header" ] && break + if [[ "$header" =~ ^[Cc]ontent-[Ll]ength:\ *([0-9]+) ]]; then + content_length="${BASH_REMATCH[1]}" + fi +done + +# Read body +body="" +if [ "$content_length" -gt 0 ]; then + body=$(head -c "$content_length") +fi + +# Response helper +send_response() { + local status="$1" + local content_type="$2" + local body="$3" + local body_length=${#body} + + printf "HTTP/1.1 %s\r\n" "$status" + printf "Content-Type: %s\r\n" "$content_type" + printf "Content-Length: %d\r\n" "$body_length" + printf "Connection: close\r\n" + printf "\r\n" + printf "%s" "$body" +} + +# Health check +if [ "$path" = "/health" ]; then + if command -v claude &>/dev/null; then + send_response "200 OK" "application/json" '{"status":"ok","claude":"available"}' + else + send_response "503 Service Unavailable" "application/json" '{"status":"error","claude":"not found"}' + fi + exit 0 +fi + +# Execute endpoint (one-shot) +if [ "$path" = "/execute" ] && [ "$method" = "POST" ]; then + prompt=$(echo "$body" | jq -r '.prompt // empty') + + if [ -z "$prompt" ]; then + send_response "400 Bad Request" "application/json" '{"error":"Missing prompt field"}' + exit 0 + fi + + # Build claude command + cd "$SKILLS_DIR" + + # Run claude with system prompt + result=$(claude --print \ + --model "$CLAUDE_MODEL" \ + --max-turns "$CLAUDE_MAX_TURNS" \ + --system-prompt "$(cat "$SYSTEM_PROMPT_FILE")" \ + --allowedTools "Bash,Read,Write,Edit,Glob,Grep" \ + "$prompt" 2>&1) || { + error_msg=$(echo "$result" | jq -Rs '.') + send_response "500 Internal Server Error" "application/json" "{\"success\":false,\"error\":$error_msg}" + exit 0 + } + + # Return result + result_json=$(echo "$result" | jq -Rs '.') + send_response "200 OK" "application/json" "{\"success\":true,\"result\":$result_json}" + exit 0 +fi + +# Chat endpoint (simplified, non-streaming for now) +if [ "$path" = "/chat" ] && [ "$method" = "POST" ]; then + message=$(echo "$body" | jq -r '.message // empty') + session_id=$(echo "$body" | jq -r '.session_id // empty') + + if [ -z "$message" ]; then + send_response "400 Bad Request" "application/json" '{"error":"Missing message field"}' + exit 0 + fi + + cd "$SKILLS_DIR" + + # Build command + claude_args=( + "--print" + "--model" "$CLAUDE_MODEL" + "--max-turns" "1" + "--system-prompt" "$(cat "$SYSTEM_PROMPT_FILE")" + "--allowedTools" "Bash,Read,Write,Edit,Glob,Grep" + ) + + # Resume session if provided + if [ -n "$session_id" ]; then + claude_args+=("--resume" "$session_id") + fi + + result=$(claude "${claude_args[@]}" "$message" 2>&1) || { + error_msg=$(echo "$result" | jq -Rs '.') + send_response "500 Internal Server Error" "application/json" "{\"success\":false,\"error\":$error_msg}" + exit 0 + } + + result_json=$(echo "$result" | jq -Rs '.') + send_response "200 OK" "application/json" "{\"success\":true,\"response\":$result_json}" + exit 0 +fi + +# Skills list +if [ "$path" = "/skills" ] && [ "$method" = "GET" ]; then + skills="[]" + for skill_dir in "$SKILLS_DIR"/*/; do + skill_name=$(basename "$skill_dir") + skill_md="$skill_dir/SKILL.md" + if [ -f "$skill_md" ]; then + desc=$(yq -r '.description // "No description"' < <(sed -n '/^---$/,/^---$/p' "$skill_md" | sed '1d;$d') 2>/dev/null || echo "") + skills=$(echo "$skills" | jq --arg name "$skill_name" --arg desc "$desc" '. + [{"name":$name,"description":$desc}]') + fi + done + send_response "200 OK" "application/json" "$skills" + exit 0 +fi + +# Not found +send_response "404 Not Found" "application/json" '{"error":"Endpoints: POST /execute, POST /chat, GET /skills, GET /health"}' +HANDLER + + chmod +x /tmp/claude_handler.sh + + # Export vars for handler + export SKILL_DIR + export CLAUDE_MODEL + export CLAUDE_MAX_TURNS + export SKILLS_DIR + export SYSTEM_PROMPT_FILE +} + +# Serve HTTP API +serve_api() { + echo "Starting Claude HTTP API on port $CLAUDE_PORT..." + echo "Model: $CLAUDE_MODEL" + echo "Max turns: $CLAUDE_MAX_TURNS" + echo "Working directory: $SKILLS_DIR" + + exec socat TCP-LISTEN:$CLAUDE_PORT,reuseaddr,fork EXEC:"/tmp/claude_handler.sh" +} + +create_handler +serve_api