Refactor remaining skills with resources pattern and allowed-tools
- k8s-quick-status: Add scripts/quick-status.sh, allowed-tools - sysadmin-health: Add scripts/health-check.sh, allowed-tools - usage: Add scripts/usage_report.py, simplify SKILL.md - programmer-add-project: Add allowed-tools All skills now: - Have executable scripts for main operations - Use allowed-tools to restrict capabilities - Have improved descriptions with trigger phrases - Follow the "Skill with Bundled Resources" pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: usage
|
||||
description: Track and report model usage across sessions
|
||||
description: Track and report model usage across sessions. Use when asked about usage, stats, or session history.
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
@@ -10,13 +10,13 @@ allowed-tools:
|
||||
|
||||
Query session history and report usage statistics.
|
||||
|
||||
## Data Sources
|
||||
## Quick Command
|
||||
|
||||
| Source | Status | Data Available |
|
||||
|--------|--------|----------------|
|
||||
| `history/index.json` | Available | Session IDs, start times |
|
||||
| `history/*.jsonl` | Future | Session content, commands, tokens |
|
||||
| `usage/config.json` | Available | Log level, preferences |
|
||||
```bash
|
||||
python3 ~/.claude/skills/usage/scripts/usage_report.py [range]
|
||||
```
|
||||
|
||||
Ranges: `today`, `week` (default), `month`, `all`
|
||||
|
||||
## Command Routing
|
||||
|
||||
@@ -27,11 +27,29 @@ Query session history and report usage statistics.
|
||||
| `/usage week` | Last 7 days |
|
||||
| `/usage month` | Last 30 days |
|
||||
| `/usage all` | All time stats |
|
||||
| `/usage --by agent` | Group by agent |
|
||||
| `/usage --by skill` | Group by skill |
|
||||
| `/usage --by model` | Group by model tier |
|
||||
| `/usage --set-level <level>` | Set log level |
|
||||
| `/usage --show-config` | Show current config |
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Source | Status | Data |
|
||||
|--------|--------|------|
|
||||
| `history/index.json` | Available | Session IDs, start times |
|
||||
| `usage/config.json` | Available | Log level, preferences |
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
📊 Usage Summary — Week
|
||||
|
||||
Sessions: 12
|
||||
Total time: 8h 32m
|
||||
Days active: 5
|
||||
Model: opus (primary)
|
||||
|
||||
📅 Sessions by Date:
|
||||
2026-01-01: 3 sessions (95m)
|
||||
2025-12-31: 4 sessions (120m)
|
||||
...
|
||||
```
|
||||
|
||||
## Log Levels
|
||||
|
||||
@@ -41,111 +59,6 @@ Query session history and report usage statistics.
|
||||
| `standard` | + agents, skills, tokens (default) |
|
||||
| `detailed` | + commands, delegations, errors |
|
||||
|
||||
## Implementation
|
||||
|
||||
### Query Sessions
|
||||
|
||||
```python
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
LOCAL_TZ = ZoneInfo('America/Los_Angeles')
|
||||
HISTORY_DIR = Path.home() / ".claude/state/personal-assistant/history"
|
||||
CONFIG_PATH = Path.home() / ".claude/state/usage/config.json"
|
||||
|
||||
def load_sessions():
|
||||
index_path = HISTORY_DIR / "index.json"
|
||||
if not index_path.exists():
|
||||
return []
|
||||
with open(index_path) as f:
|
||||
data = json.load(f)
|
||||
return data.get("sessions", [])
|
||||
|
||||
def filter_sessions(sessions, range_type="week"):
|
||||
now = datetime.now(LOCAL_TZ)
|
||||
if range_type == "today":
|
||||
cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
elif range_type == "week":
|
||||
cutoff = now - timedelta(days=7)
|
||||
elif range_type == "month":
|
||||
cutoff = now - timedelta(days=30)
|
||||
else: # all
|
||||
return sessions
|
||||
|
||||
filtered = []
|
||||
for s in sessions:
|
||||
started = datetime.fromisoformat(s["started"])
|
||||
if started >= cutoff:
|
||||
filtered.append(s)
|
||||
return filtered
|
||||
|
||||
def estimate_duration(sessions):
|
||||
"""Estimate session duration from consecutive start times."""
|
||||
if not sessions:
|
||||
return []
|
||||
|
||||
sorted_sessions = sorted(sessions, key=lambda s: s["started"])
|
||||
for i, session in enumerate(sorted_sessions):
|
||||
if i + 1 < len(sorted_sessions):
|
||||
start = datetime.fromisoformat(session["started"])
|
||||
next_start = datetime.fromisoformat(sorted_sessions[i + 1]["started"])
|
||||
duration = (next_start - start).total_seconds() / 60
|
||||
# Cap at 2 hours (likely session gap)
|
||||
session["duration_mins"] = min(duration, 120)
|
||||
else:
|
||||
# Current/last session: estimate 30 mins
|
||||
session["duration_mins"] = 30
|
||||
return sorted_sessions
|
||||
```
|
||||
|
||||
### Generate Report
|
||||
|
||||
```python
|
||||
def generate_report(sessions, range_type="week"):
|
||||
total_mins = sum(s.get("duration_mins", 0) for s in sessions)
|
||||
hours = int(total_mins // 60)
|
||||
mins = int(total_mins % 60)
|
||||
|
||||
report = f"""
|
||||
📊 Usage Summary — {range_type.title()}
|
||||
|
||||
Sessions: {len(sessions)}
|
||||
Total time: {hours}h {mins}m
|
||||
Model: opus (primary)
|
||||
"""
|
||||
return report
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
📊 Usage Summary — Last 7 Days
|
||||
|
||||
Sessions: 12
|
||||
Total time: 8h 32m
|
||||
Model: opus (primary)
|
||||
|
||||
┌─────────────┬──────────┬────────┐
|
||||
│ Agent │ Sessions │ Time │
|
||||
├─────────────┼──────────┼────────┤
|
||||
│ PA │ 12 │ 8h 32m │
|
||||
└─────────────┴──────────┴────────┘
|
||||
|
||||
Note: Detailed metrics available when session
|
||||
content logging is enabled.
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
When `history/*.jsonl` files are available:
|
||||
- Parse actual commands used
|
||||
- Detect skill invocations
|
||||
- Count delegations (sonnet/haiku)
|
||||
- Estimate token usage
|
||||
- Track errors
|
||||
|
||||
## Policy
|
||||
|
||||
- Read-only operations
|
||||
|
||||
109
skills/usage/scripts/usage_report.py
Executable file
109
skills/usage/scripts/usage_report.py
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate usage statistics from session history."""
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
LOCAL_TZ = ZoneInfo('America/Los_Angeles')
|
||||
HISTORY_DIR = Path.home() / ".claude/state/personal-assistant/history"
|
||||
CONFIG_PATH = Path.home() / ".claude/state/usage/config.json"
|
||||
|
||||
|
||||
def load_sessions():
|
||||
"""Load sessions from history index."""
|
||||
index_path = HISTORY_DIR / "index.json"
|
||||
if not index_path.exists():
|
||||
return []
|
||||
with open(index_path) as f:
|
||||
data = json.load(f)
|
||||
return data.get("sessions", [])
|
||||
|
||||
|
||||
def filter_sessions(sessions, range_type="week"):
|
||||
"""Filter sessions by time range."""
|
||||
now = datetime.now(LOCAL_TZ)
|
||||
if range_type == "today":
|
||||
cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
elif range_type == "week":
|
||||
cutoff = now - timedelta(days=7)
|
||||
elif range_type == "month":
|
||||
cutoff = now - timedelta(days=30)
|
||||
else: # all
|
||||
return sessions
|
||||
|
||||
filtered = []
|
||||
for s in sessions:
|
||||
try:
|
||||
started = datetime.fromisoformat(s["started"])
|
||||
if started.tzinfo is None:
|
||||
started = started.replace(tzinfo=LOCAL_TZ)
|
||||
if started >= cutoff:
|
||||
filtered.append(s)
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
return filtered
|
||||
|
||||
|
||||
def estimate_duration(sessions):
|
||||
"""Estimate session duration from consecutive start times."""
|
||||
if not sessions:
|
||||
return []
|
||||
|
||||
sorted_sessions = sorted(sessions, key=lambda s: s["started"])
|
||||
for i, session in enumerate(sorted_sessions):
|
||||
if i + 1 < len(sorted_sessions):
|
||||
start = datetime.fromisoformat(session["started"])
|
||||
next_start = datetime.fromisoformat(sorted_sessions[i + 1]["started"])
|
||||
duration = (next_start - start).total_seconds() / 60
|
||||
# Cap at 2 hours (likely session gap)
|
||||
session["duration_mins"] = min(duration, 120)
|
||||
else:
|
||||
# Current/last session: estimate 30 mins
|
||||
session["duration_mins"] = 30
|
||||
return sorted_sessions
|
||||
|
||||
|
||||
def generate_report(sessions, range_type="week"):
|
||||
"""Generate usage report."""
|
||||
sessions = estimate_duration(sessions)
|
||||
total_mins = sum(s.get("duration_mins", 0) for s in sessions)
|
||||
hours = int(total_mins // 60)
|
||||
mins = int(total_mins % 60)
|
||||
|
||||
# Group by date
|
||||
by_date = {}
|
||||
for s in sessions:
|
||||
date = s["started"][:10]
|
||||
by_date.setdefault(date, []).append(s)
|
||||
|
||||
report = f"""📊 Usage Summary — {range_type.title()}
|
||||
|
||||
Sessions: {len(sessions)}
|
||||
Total time: {hours}h {mins}m
|
||||
Days active: {len(by_date)}
|
||||
Model: opus (primary)
|
||||
"""
|
||||
|
||||
if by_date:
|
||||
report += "\n📅 Sessions by Date:\n"
|
||||
for date in sorted(by_date.keys(), reverse=True)[:7]:
|
||||
count = len(by_date[date])
|
||||
day_mins = sum(s.get("duration_mins", 0) for s in by_date[date])
|
||||
report += f" {date}: {count} sessions ({int(day_mins)}m)\n"
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
range_type = sys.argv[1] if len(sys.argv) > 1 else "week"
|
||||
|
||||
sessions = load_sessions()
|
||||
filtered = filter_sessions(sessions, range_type)
|
||||
report = generate_report(filtered, range_type)
|
||||
print(report)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user