diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..6192276 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "will-homelab-dev", + "description": "Development marketplace for will-homelab plugin", + "owner": { + "name": "Will" + }, + "plugins": [ + { + "name": "will-homelab", + "description": "Personal assistant and multi-agent system for homelab management", + "version": "1.0.0", + "source": "./", + "author": { + "name": "Will" + } + } + ] +} diff --git a/commands/README.md b/commands/README.md new file mode 100644 index 0000000..451ea5a --- /dev/null +++ b/commands/README.md @@ -0,0 +1,81 @@ +# Commands + +Slash commands for quick actions. User-invoked (type `/command` to trigger). + +## Available Commands + +### Top-Level + +| Command | Aliases | Description | +|---------|---------|-------------| +| `/pa` | `/assistant`, `/ask` | Personal assistant entrypoint | +| `/programmer` | | Code development tasks | +| `/gcal` | `/calendar`, `/cal` | Google Calendar access | +| `/usage` | `/stats` | View usage statistics | + +### Kubernetes (`/k8s:*`) + +| Command | Description | +|---------|-------------| +| `/k8s:cluster-status` | Quick cluster health overview | +| `/k8s:deploy` | Deploy applications to cluster | +| `/k8s:diagnose` | Diagnose Kubernetes issues | + +### System Admin (`/sysadmin:*`) + +| Command | Description | +|---------|-------------| +| `/sysadmin:health` | System health check | +| `/sysadmin:update` | System package updates | +| `/sysadmin:autonomy` | Set autonomy level for session | + +## Command Format + +```yaml +--- +name: command-name +description: What this command does +aliases: [alias1, alias2] +invokes: skill:skill-name # or workflow: or agent: +--- + +# Command Title + +Instructions for Claude when command is invoked. +``` + +## Command vs Skill + +| Aspect | Command | Skill | +|--------|---------|-------| +| **Invocation** | User types `/command` | Claude decides automatically | +| **Discovery** | Explicit | Based on description matching | +| **Use case** | Quick actions, shortcuts | Domain expertise, workflows | + +## Directory Structure + +``` +commands/ +├── README.md +├── pa.md +├── gcal.md +├── usage.md +├── programmer.md +├── k8s/ +│ ├── cluster-status.md +│ ├── deploy.md +│ └── diagnose.md +└── sysadmin/ + ├── health.md + ├── update.md + └── autonomy.md +``` + +Namespaced commands use subdirectories (e.g., `k8s/deploy.md` → `/k8s:deploy`). + +## Adding Commands + +1. Create `commands/name.md` (or `commands/namespace/name.md`) +2. Add YAML frontmatter with name, description, invokes +3. Write instructions in Markdown body +4. Update `component-registry.json` if needed for routing diff --git a/hooks/README.md b/hooks/README.md index 7faa04d..fd1c5e2 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -7,6 +7,7 @@ Event handlers that run automatically during Claude Code sessions. | Event | Script | Purpose | |-------|--------|---------| | `SessionStart` | `scripts/session-start.sh` | Load context, check for pending items | +| `PreCompact` | `scripts/pre-compact.sh` | Remind to preserve context before compaction | ## Hook Events diff --git a/hooks/hooks.json b/hooks/hooks.json index fff6408..e649a85 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -9,6 +9,16 @@ } ] } + ], + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/scripts/pre-compact.sh" + } + ] + } ] } } diff --git a/hooks/scripts/pre-compact.sh b/hooks/scripts/pre-compact.sh new file mode 100755 index 0000000..9f6d42a --- /dev/null +++ b/hooks/scripts/pre-compact.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Pre-compact hook - saves context before compaction +# Output reminds Claude to summarize important context + +set -euo pipefail + +echo "PreCompact:Callback hook success: Success" +echo "PreCompact hook additional context: " +echo "Context compaction is about to occur. Before compacting:" +echo "1. Note any pending tasks or decisions that need to be preserved" +echo "2. Summarize key context that should persist" +echo "3. If working on a multi-step task, note current progress" +echo "" diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..0f0f732 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,105 @@ +# Skills + +Agent skills that extend Claude's capabilities. Model-invoked (Claude decides when to use them). + +## Available Skills + +| Skill | Description | Scripts | +|-------|-------------|---------| +| `gmail` | Gmail read access | `check_unread.py`, `check_urgent.py`, `search.py` | +| `gcal` | Google Calendar access | `agenda.py`, `next_event.py` | +| `k8s-quick-status` | Kubernetes cluster health | `quick-status.sh` | +| `sysadmin-health` | Arch Linux health check | `health-check.sh` | +| `usage` | Session usage tracking | `usage_report.py` | +| `programmer-add-project` | Register projects | (workflow only) | + +## Skill Structure + +Each skill uses the "Resources Pattern": + +``` +skills/skill-name/ +├── SKILL.md # Main instructions (required) +├── scripts/ # Executable helpers +│ ├── action1.py +│ └── action2.sh +└── references/ # Documentation + └── patterns.md +``` + +## Skill Format + +```yaml +--- +name: skill-name +description: What it does. Use when [trigger conditions]. +allowed-tools: + - Bash + - Read +--- + +# Skill Name + +## Quick Commands + +```bash +./scripts/main-action.py [args] +``` + +## Request Routing + +| User Request | Action | +|--------------|--------| +| "do X" | Run script with args | + +## Policy + +- Read-only / Write allowed +- Model tier preferences +``` + +## Key Elements + +### Description + +Critical for discovery. Include: +- What the skill does +- When Claude should use it (trigger phrases) + +### allowed-tools + +Restricts which tools Claude can use: +- `Bash` - Run commands/scripts +- `Read` - Read files +- `Write` - Write files +- `Edit` - Edit files + +### Scripts + +Standalone executables that do the work: +- Python: `#!/usr/bin/env python3` +- Bash: `#!/bin/bash` +- Must be executable: `chmod +x` + +### References + +Supporting documentation: +- Query patterns +- API reference +- Examples + +## Adding a Skill + +1. Create `skills/name/SKILL.md` +2. Add scripts to `skills/name/scripts/` +3. Make scripts executable +4. Update `component-registry.json` with triggers +5. Test: ask Claude something that should trigger it + +## Skill vs Command + +| Aspect | Skill | Command | +|--------|-------|---------| +| **Invocation** | Claude decides | User types `/command` | +| **Discovery** | Based on description | Explicit | +| **Use case** | Domain expertise | Quick actions | diff --git a/skills/gcal/SKILL.md b/skills/gcal/SKILL.md index 7d39dec..39c6715 100644 --- a/skills/gcal/SKILL.md +++ b/skills/gcal/SKILL.md @@ -1,48 +1,59 @@ --- name: gcal -description: Google Calendar read access — agenda overview, event details, smart summaries +description: Google Calendar read access — agenda, events, schedule. Use when asked about calendar, meetings, schedule, or what's on today/tomorrow. allowed-tools: - Bash + - Read --- # Google Calendar Skill Access Google Calendar via Python API. Uses OAuth credentials at `~/.gmail-mcp/`. -## Command Routing +## Quick Commands -### 1. Exact Subcommand Match (priority) +```bash +GCAL_PY=~/.claude/mcp/gmail/venv/bin/python +SCRIPTS=~/.claude/skills/gcal/scripts -| Input | Action | -|-------|--------| -| `today` | Today's agenda | -| `tomorrow` | Tomorrow's agenda | -| `week` | Next 7 days, grouped by day | -| `next` | Next upcoming event only | -| `summary` | Sonnet-powered week analysis | +# Today's agenda +$GCAL_PY $SCRIPTS/agenda.py today -### 2. Natural Language Fallback +# Tomorrow's agenda +$GCAL_PY $SCRIPTS/agenda.py tomorrow -| Input | Routes To | -|-------|-----------| -| "what's on today", "today's meetings" | today | -| "this week", "next 7 days" | week | -| "next meeting", "what's next" | next | -| "am I busy tomorrow", "tomorrow's schedule" | tomorrow | -| "overview", "summarize my week" | summary | +# This week (7 days) +$GCAL_PY $SCRIPTS/agenda.py week -### 3. Smart Default (no args) +# Next event only +$GCAL_PY $SCRIPTS/next_event.py +``` + +## Script Reference + +| Script | Purpose | Args | +|--------|---------|------| +| `agenda.py` | List events for time range | `today`, `tomorrow`, `week` | +| `next_event.py` | Next upcoming event | none | + +## Request Routing + +| User Request | Script | Args | +|--------------|--------|------| +| "What's on today?" | `agenda.py` | `today` | +| "Tomorrow's schedule" | `agenda.py` | `tomorrow` | +| "This week" | `agenda.py` | `week` | +| "Next meeting" | `next_event.py` | none | +| "Am I free at 3pm?" | `agenda.py` | `today` (then analyze) | + +### Smart Default (no args) - Before 6pm → today - After 6pm → tomorrow -### 4. Ambiguous Input +## Delegation Helper (Advanced) -Ask for clarification rather than guess. - -## Delegated Operations - -Use the delegation helper for cost-efficient operations: +For LLM-assisted operations: ```bash GCAL_PY=~/.claude/mcp/gmail/venv/bin/python @@ -50,27 +61,13 @@ HELPER=~/.claude/mcp/delegation/gcal_delegate.py # Haiku tier - fetch and format $GCAL_PY $HELPER today -$GCAL_PY $HELPER tomorrow -$GCAL_PY $HELPER week -$GCAL_PY $HELPER next # Sonnet tier - analyze (spawns claude --model sonnet) $GCAL_PY $HELPER summary ``` -## Delegation Tiers - -| Subcommand | Tier | Reason | -|------------|------|--------| -| today, tomorrow, week, next | Haiku | Fetch + format only | -| summary | Sonnet | Requires understanding/analysis | - ## Output Format -The helper returns JSON. Format for user as: - -### Simple List (today/tomorrow/next) - ``` 📅 Today — Thursday, Jan 2 @@ -83,49 +80,25 @@ The helper returns JSON. Format for user as: No more events today. ``` -### Grouped by Day (week) - -``` -📅 This Week — Jan 2-8 - -━━━ Thursday, Jan 2 ━━━ - 9:00 AM Team standup (30m) - 2:00 PM Project review (1h) - -━━━ Friday, Jan 3 ━━━ - 11:00 AM Client call (1h) -``` - -### Context Fields (show when available) +### Context Fields - 📍 Location or meeting link - 👥 Attendee count -- 📝 Description snippet (first ~50 chars) +- 📝 Description snippet (if relevant) ## Error Handling ### Missing Calendar Scope -If helper reports scope error: - ``` Calendar access not authorized. To fix: 1. Delete cached token: rm ~/.gmail-mcp/token.json 2. Run /gcal today to re-authenticate with Calendar scope ``` -### No Events - -``` -📅 Today — Thursday, Jan 2 - -No events scheduled. -``` - ## Policy -- Read-only operations only -- Show context (attendees, location) by default -- Summarize results, don't dump raw data -- Start with lowest capable model tier -- Escalate only when task complexity requires +- **Read-only** operations only +- Show **context** (attendees, location) by default +- **Summarize** results, don't dump raw data +- Start with **lowest capable** model tier diff --git a/skills/gcal/scripts/next_event.py b/skills/gcal/scripts/next_event.py new file mode 100755 index 0000000..0c7a83a --- /dev/null +++ b/skills/gcal/scripts/next_event.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Get the next upcoming calendar event.""" +import os +from datetime import datetime + +# Set credentials path +os.environ.setdefault('GMAIL_CREDENTIALS_PATH', os.path.expanduser('~/.gmail-mcp/credentials.json')) + +from gmail_mcp.utils.GCP.gmail_auth import get_calendar_service + + +def format_event(event): + """Format a single event for display.""" + start = event['start'].get('dateTime', event['start'].get('date')) + + if 'T' in start: + start_dt = datetime.fromisoformat(start.replace('Z', '+00:00')) + time_str = start_dt.strftime('%I:%M %p').lstrip('0') + date_str = start_dt.strftime('%A, %b %d') + else: + time_str = "All day" + date_str = start + + summary = event.get('summary', '(No title)') + location = event.get('location', '') + + print(f"📅 Next Event — {date_str}") + print() + print(f" {time_str} {summary}") + if location: + print(f" 📍 {location}") + + # Show attendees if available + attendees = event.get('attendees', []) + if attendees: + print(f" 👥 {len(attendees)} attendees") + + +def main(): + service = get_calendar_service() + now = datetime.utcnow().isoformat() + 'Z' + + events_result = service.events().list( + calendarId='primary', + timeMin=now, + maxResults=1, + singleEvents=True, + orderBy='startTime' + ).execute() + + events = events_result.get('items', []) + + if not events: + print("📅 No upcoming events") + return + + format_event(events[0]) + + +if __name__ == '__main__': + main()