Add initial skills: caldav, netbird, ourgroceries, planka
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# Claude Code Skills
|
||||
|
||||
A collection of Claude Code skills for use with the agent-bobby setup.
|
||||
|
||||
## Usage
|
||||
|
||||
Clone this repo into your `.claude/skills/` directory:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.dcglab.co.uk/steve/skills ~/.claude/skills
|
||||
```
|
||||
|
||||
Or clone individual skills:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.dcglab.co.uk/steve/skills /tmp/skills
|
||||
cp -r /tmp/skills/caldav ~/.claude/skills/
|
||||
```
|
||||
|
||||
## Skills
|
||||
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| `caldav` | Sync and query CalDAV calendars using vdirsyncer + khal |
|
||||
| `netbird` | Manage NetBird private mesh networks via the REST API |
|
||||
| `ourgroceries` | Manage OurGroceries shopping lists |
|
||||
| `planka` | Manage Planka project boards using the pcli CLI |
|
||||
|
||||
## Structure
|
||||
|
||||
Each skill follows the standard Claude Code skill format:
|
||||
|
||||
```
|
||||
<skill-name>/
|
||||
SKILL.md # Skill definition and instructions
|
||||
scripts/ # Helper scripts (optional)
|
||||
references/ # Reference documentation (optional)
|
||||
```
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: caldav-calendar
|
||||
description: Sync and query CalDAV calendars (iCloud, Google, Fastmail, Nextcloud, etc.) using vdirsyncer + khal. Works on Linux.
|
||||
emoji:📅
|
||||
---
|
||||
|
||||
# CalDAV Calendar (vdirsyncer + khal)
|
||||
|
||||
**vdirsyncer** syncs CalDAV calendars to local `.ics` files. **khal** reads and writes them.
|
||||
|
||||
## Sync First
|
||||
|
||||
Always sync before querying or after making changes:
|
||||
```bash
|
||||
vdirsyncer sync
|
||||
```
|
||||
|
||||
## View Events
|
||||
|
||||
```bash
|
||||
khal list # Today
|
||||
khal list today 7d # Next 7 days
|
||||
khal list tomorrow # Tomorrow
|
||||
khal list 2026-01-15 2026-01-20 # Date range
|
||||
khal list -a Work today # Specific calendar
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
```bash
|
||||
khal search "meeting"
|
||||
khal search "dentist" --format "{start-date} {title}"
|
||||
```
|
||||
|
||||
## Create Events
|
||||
|
||||
```bash
|
||||
khal new 2026-01-15 10:00 11:00 "Meeting title"
|
||||
khal new 2026-01-15 "All day event"
|
||||
khal new tomorrow 14:00 15:30 "Call" -a Work
|
||||
khal new 2026-01-15 10:00 11:00 "With notes" :: Description goes here
|
||||
```
|
||||
|
||||
After creating, sync to push changes:
|
||||
```bash
|
||||
vdirsyncer sync
|
||||
```
|
||||
|
||||
## Edit Events (interactive)
|
||||
|
||||
`khal edit` is interactive — requires a TTY. Use tmux if automating:
|
||||
|
||||
```bash
|
||||
khal edit "search term"
|
||||
khal edit -a CalendarName "search term"
|
||||
khal edit --show-past "old event"
|
||||
```
|
||||
|
||||
Menu options:
|
||||
- `s` → edit summary
|
||||
- `d` → edit description
|
||||
- `t` → edit datetime range
|
||||
- `l` → edit location
|
||||
- `D` → delete event
|
||||
- `n` → skip (save changes, next match)
|
||||
- `q` → quit
|
||||
|
||||
After editing, sync:
|
||||
```bash
|
||||
vdirsyncer sync
|
||||
```
|
||||
|
||||
## Delete Events
|
||||
|
||||
Use `khal edit`, then press `D` to delete.
|
||||
|
||||
## Output Formats
|
||||
|
||||
For scripting:
|
||||
```bash
|
||||
khal list --format "{start-date} {start-time}-{end-time} {title}" today 7d
|
||||
khal list --format "{uid} | {title} | {calendar}" today
|
||||
```
|
||||
|
||||
Placeholders: `{title}`, `{description}`, `{start}`, `{end}`, `{start-date}`, `{start-time}`, `{end-date}`, `{end-time}`, `{location}`, `{calendar}`, `{uid}`
|
||||
|
||||
## Caching
|
||||
|
||||
khal caches events in `~/.local/share/khal/khal.db`. If data looks stale after syncing:
|
||||
```bash
|
||||
rm ~/.local/share/khal/khal.db
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### 1. Configure vdirsyncer (`~/.config/vdirsyncer/config`)
|
||||
|
||||
Example for iCloud:
|
||||
```ini
|
||||
[general]
|
||||
status_path = "~/.local/share/vdirsyncer/status/"
|
||||
|
||||
[pair icloud_calendar]
|
||||
a = "icloud_remote"
|
||||
b = "icloud_local"
|
||||
collections = ["from a", "from b"]
|
||||
conflict_resolution = "a wins"
|
||||
|
||||
[storage icloud_remote]
|
||||
type = "caldav"
|
||||
url = "https://caldav.icloud.com/"
|
||||
username = "your@icloud.com"
|
||||
password.fetch = ["command", "cat", "~/.config/vdirsyncer/icloud_password"]
|
||||
|
||||
[storage icloud_local]
|
||||
type = "filesystem"
|
||||
path = "~/.local/share/vdirsyncer/calendars/"
|
||||
fileext = ".ics"
|
||||
```
|
||||
|
||||
Provider URLs:
|
||||
- iCloud: `https://caldav.icloud.com/`
|
||||
- Google: Use `google_calendar` storage type
|
||||
- Fastmail: `https://caldav.fastmail.com/dav/calendars/user/EMAIL/`
|
||||
- Nextcloud: `https://YOUR.CLOUD/remote.php/dav/calendars/USERNAME/`
|
||||
|
||||
### 2. Configure khal (`~/.config/khal/config`)
|
||||
|
||||
```ini
|
||||
[calendars]
|
||||
[[my_calendars]]
|
||||
path = ~/.local/share/vdirsyncer/calendars/*
|
||||
type = discover
|
||||
|
||||
[default]
|
||||
default_calendar = Home
|
||||
highlight_event_days = True
|
||||
|
||||
[locale]
|
||||
timeformat = %H:%M
|
||||
dateformat = %Y-%m-%d
|
||||
```
|
||||
|
||||
### 3. Discover and sync
|
||||
|
||||
```bash
|
||||
vdirsyncer discover # First time only
|
||||
vdirsyncer sync
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: netbird
|
||||
description: Manage NetBird private mesh networks via the REST API. Use when Steve wants to interact with his self-hosted NetBird instance at https://net.dcglab.co.uk for tasks like listing peers, managing groups, creating setup keys, configuring policies, managing routes, or checking network status. Triggers on phrases like "netbird", "check my netbird", "list peers", "create setup key", "manage netbird groups", "netbird policy", "netbird routes".
|
||||
---
|
||||
|
||||
# NetBird Skill
|
||||
|
||||
Manage your self-hosted NetBird mesh network via the REST API.
|
||||
|
||||
## Configuration
|
||||
|
||||
The skill expects a `NETBIRD_API_TOKEN` environment variable containing your Personal Access Token (PAT). Create one in your NetBird dashboard under User settings.
|
||||
|
||||
Base URL: `https://net.dcglab.co.uk/api`
|
||||
|
||||
## Quick Start
|
||||
|
||||
Use the provided Python script for common operations:
|
||||
|
||||
```bash
|
||||
# List all peers
|
||||
python3 scripts/netbird_cli.py peers list
|
||||
|
||||
# List all groups
|
||||
python3 scripts/netbird_cli.py groups list
|
||||
|
||||
# Create a setup key
|
||||
python3 scripts/netbird_cli.py setup-keys create --name "server-key" --type reusable --expires 86400
|
||||
|
||||
# List policies
|
||||
python3 scripts/netbird_cli.py policies list
|
||||
```
|
||||
|
||||
## Available Resources
|
||||
|
||||
- **API Reference**: See [references/api-reference.md](references/api-reference.md) for detailed endpoint documentation
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Peers
|
||||
- List, get details, update, delete peers
|
||||
- Check connection status, IP addresses, groups
|
||||
|
||||
### Groups
|
||||
- Create, list, update, delete groups
|
||||
- Manage peer memberships
|
||||
|
||||
### Setup Keys
|
||||
- Create reusable or one-off keys
|
||||
- Set expiration and auto-assign groups
|
||||
- Revoke keys
|
||||
|
||||
### Policies
|
||||
- Create network access policies
|
||||
- Define rules with sources, destinations, ports, protocols
|
||||
- Enable/disable policies
|
||||
|
||||
### Routes
|
||||
- Create network routes
|
||||
- Assign to peer groups
|
||||
|
||||
### DNS
|
||||
- Manage nameserver groups
|
||||
- Configure DNS settings
|
||||
|
||||
### Users
|
||||
- List users and service users
|
||||
- Manage user roles and groups
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require a Bearer token header:
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
For self-hosted instances, create a Personal Access Token (PAT) in the NetBird dashboard under User settings → Personal Access Tokens.
|
||||
|
||||
## API Patterns
|
||||
|
||||
- All endpoints return JSON
|
||||
- IDs are string-based (e.g., `chacbco6lnnbn6cg5s90`)
|
||||
- Dates are ISO 8601 format
|
||||
- Boolean fields use JSON true/false
|
||||
@@ -0,0 +1,137 @@
|
||||
# NetBird API Reference
|
||||
|
||||
Base URL: `https://net.dcglab.co.uk/api`
|
||||
|
||||
## Authentication
|
||||
|
||||
Header: `Authorization: Bearer <token>`
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Peers
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/peers` | List all peers |
|
||||
| GET | `/api/peers/{peerId}` | Get peer details |
|
||||
| PUT | `/api/peers/{peerId}` | Update peer |
|
||||
| DELETE | `/api/peers/{peerId}` | Delete peer |
|
||||
|
||||
**Peer Object Fields:**
|
||||
- `id` - Unique identifier
|
||||
- `name` - Peer name
|
||||
- `ip` - NetBird IP address
|
||||
- `connection_ip` - Public connection IP
|
||||
- `connected` - Boolean connection status
|
||||
- `last_seen` - ISO timestamp
|
||||
- `os` - Operating system
|
||||
- `hostname` - System hostname
|
||||
- `groups` - Array of group objects
|
||||
- `ssh_enabled` - Boolean SSH access
|
||||
- `user_id` - Owner user ID
|
||||
- `dns_label` - DNS name
|
||||
- `approval_required` - Boolean pending approval
|
||||
|
||||
### Groups
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/groups` | List all groups |
|
||||
| POST | `/api/groups` | Create group |
|
||||
| GET | `/api/groups/{groupId}` | Get group details |
|
||||
| PUT | `/api/groups/{groupId}` | Update group |
|
||||
| DELETE | `/api/groups/{groupId}` | Delete group |
|
||||
|
||||
**Group Object Fields:**
|
||||
- `id` - Unique identifier
|
||||
- `name` - Group name
|
||||
- `peers_count` - Number of peers
|
||||
- `resources_count` - Number of resources
|
||||
- `issued` - Creation source (api, etc.)
|
||||
|
||||
### Setup Keys
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/setup-keys` | List all setup keys |
|
||||
| POST | `/api/setup-keys` | Create setup key |
|
||||
| GET | `/api/setup-keys/{keyId}` | Get key details |
|
||||
| PUT | `/api/setup-keys/{keyId}` | Update key |
|
||||
| DELETE | `/api/setup-keys/{keyId}` | Delete key |
|
||||
|
||||
**Create Setup Key Body:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"type": "one-off" | "reusable",
|
||||
"expires_in": 86400,
|
||||
"auto_groups": ["group-id-1", "group-id-2"]
|
||||
}
|
||||
```
|
||||
|
||||
### Policies
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/policies` | List all policies |
|
||||
| POST | `/api/policies` | Create policy |
|
||||
| GET | `/api/policies/{policyId}` | Get policy details |
|
||||
| PUT | `/api/policies/{policyId}` | Update policy |
|
||||
| DELETE | `/api/policies/{policyId}` | Delete policy |
|
||||
|
||||
**Policy Rule Fields:**
|
||||
- `name` - Rule name
|
||||
- `description` - Rule description
|
||||
- `enabled` - Boolean status
|
||||
- `action` - "accept" or "drop"
|
||||
- `protocol` - "tcp", "udp", "icmp", "all"
|
||||
- `ports` - Array of port strings (e.g., ["80", "443"])
|
||||
- `sources` - Array of source group IDs
|
||||
- `destinations` - Array of destination group IDs
|
||||
- `bidirectional` - Boolean
|
||||
|
||||
### Routes
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/routes` | List all routes |
|
||||
| POST | `/api/routes` | Create route |
|
||||
| GET | `/api/routes/{routeId}` | Get route details |
|
||||
| PUT | `/api/routes/{routeId}` | Update route |
|
||||
| DELETE | `/api/routes/{routeId}` | Delete route |
|
||||
|
||||
### DNS
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/dns/nameservers` | List nameserver groups |
|
||||
| POST | `/api/dns/nameservers` | Create nameserver group |
|
||||
| GET | `/api/dns/nameservers/{nsgroupId}` | Get nameserver group |
|
||||
| PUT | `/api/dns/nameservers/{nsgroupId}` | Update nameserver group |
|
||||
| DELETE | `/api/dns/nameservers/{nsgroupId}` | Delete nameserver group |
|
||||
| GET | `/api/dns/settings` | Get DNS settings |
|
||||
| PUT | `/api/dns/settings` | Update DNS settings |
|
||||
|
||||
### Users
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/users` | List all users |
|
||||
| POST | `/api/users` | Create service user or invite |
|
||||
| GET | `/api/users/{userId}` | Get user details |
|
||||
| PUT | `/api/users/{userId}` | Update user |
|
||||
| DELETE | `/api/users/{userId}` | Delete user |
|
||||
| POST | `/api/users/{userId}/invite` | Resend invitation |
|
||||
| POST | `/api/users/{userId}/approve` | Approve pending user |
|
||||
| DELETE | `/api/users/{userId}/reject` | Reject pending user |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Filter by Name
|
||||
Many list endpoints support `?name=` query parameter for exact match filtering.
|
||||
|
||||
### Service Users
|
||||
Set `is_service_user: true` when creating users for automation/API access.
|
||||
|
||||
### Auto Groups
|
||||
When creating setup keys or users, specify `auto_groups` to automatically assign peers to groups.
|
||||
@@ -0,0 +1,391 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
NetBird CLI - Simple command-line interface for NetBird API
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
BASE_URL = "https://net.dcglab.co.uk/api"
|
||||
|
||||
|
||||
def get_token() -> str:
|
||||
"""Get API token from environment."""
|
||||
token = os.environ.get("NETBIRD_API_TOKEN")
|
||||
if not token:
|
||||
print("Error: NETBIRD_API_TOKEN environment variable not set", file=sys.stderr)
|
||||
print("Create a Personal Access Token in your NetBird dashboard:", file=sys.stderr)
|
||||
print(" User settings → Personal Access Tokens", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return token
|
||||
|
||||
|
||||
def api_request(
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict] = None,
|
||||
params: Optional[Dict] = None,
|
||||
) -> Any:
|
||||
"""Make an API request to NetBird."""
|
||||
token = get_token()
|
||||
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
if params:
|
||||
query = "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
|
||||
url = f"{url}?{query}"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method=method,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if data:
|
||||
req.data = json.dumps(data).encode("utf-8")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
if response.status == 204:
|
||||
return None
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8")
|
||||
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
|
||||
try:
|
||||
error_json = json.loads(error_body)
|
||||
print(json.dumps(error_json, indent=2), file=sys.stderr)
|
||||
except:
|
||||
print(error_body, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Connection error: {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def format_peer(peer: Dict) -> str:
|
||||
"""Format peer for display."""
|
||||
status = "🟢" if peer.get("connected") else "🔴"
|
||||
name = peer.get("name", "unknown")
|
||||
ip = peer.get("ip", "N/A")
|
||||
hostname = peer.get("hostname", "N/A")
|
||||
os_info = peer.get("os", "N/A")
|
||||
return f"{status} {name} ({hostname}) - IP: {ip} - OS: {os_info}"
|
||||
|
||||
|
||||
def format_group(group: Dict) -> str:
|
||||
"""Format group for display."""
|
||||
name = group.get("name", "unknown")
|
||||
gid = group.get("id", "N/A")[:8]
|
||||
peers = group.get("peers_count", 0)
|
||||
return f"{name} ({gid}...) - {peers} peers"
|
||||
|
||||
|
||||
def format_policy(policy: Dict) -> str:
|
||||
"""Format policy for display."""
|
||||
name = policy.get("name", "unknown")
|
||||
enabled = "✅" if policy.get("enabled") else "❌"
|
||||
rules_count = len(policy.get("rules", []))
|
||||
return f"{enabled} {name} - {rules_count} rules"
|
||||
|
||||
|
||||
def format_setup_key(key: Dict) -> str:
|
||||
"""Format setup key for display."""
|
||||
name = key.get("name", "unknown")
|
||||
key_type = key.get("type", "unknown")
|
||||
revoked = "🚫 REVOKED" if key.get("revoked") else ""
|
||||
auto_groups = ", ".join(key.get("auto_groups", [])) or "none"
|
||||
return f"{name} ({key_type}) - Auto-groups: {auto_groups} {revoked}"
|
||||
|
||||
|
||||
def format_route(route: Dict) -> str:
|
||||
"""Format route for display."""
|
||||
desc = route.get("description", "unknown")
|
||||
network = route.get("network", "N/A")
|
||||
enabled = "✅" if route.get("enabled") else "❌"
|
||||
return f"{enabled} {desc} - {network}"
|
||||
|
||||
|
||||
# ===== PEERS =====
|
||||
|
||||
def list_peers():
|
||||
"""List all peers."""
|
||||
peers = api_request("GET", "/peers")
|
||||
if not peers:
|
||||
print("No peers found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(peers)} peer(s):\n")
|
||||
for peer in peers:
|
||||
print(format_peer(peer))
|
||||
|
||||
|
||||
def get_peer(peer_id: str):
|
||||
"""Get peer details."""
|
||||
peer = api_request("GET", f"/peers/{peer_id}")
|
||||
print(json.dumps(peer, indent=2))
|
||||
|
||||
|
||||
def delete_peer(peer_id: str):
|
||||
"""Delete a peer."""
|
||||
api_request("DELETE", f"/peers/{peer_id}")
|
||||
print(f"Peer {peer_id} deleted.")
|
||||
|
||||
|
||||
# ===== GROUPS =====
|
||||
|
||||
def list_groups():
|
||||
"""List all groups."""
|
||||
groups = api_request("GET", "/groups")
|
||||
if not groups:
|
||||
print("No groups found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(groups)} group(s):\n")
|
||||
for group in groups:
|
||||
print(format_group(group))
|
||||
|
||||
|
||||
def get_group(group_id: str):
|
||||
"""Get group details."""
|
||||
group = api_request("GET", f"/groups/{group_id}")
|
||||
print(json.dumps(group, indent=2))
|
||||
|
||||
|
||||
def create_group(name: str, peers: Optional[List[str]] = None):
|
||||
"""Create a new group."""
|
||||
data = {"name": name}
|
||||
if peers:
|
||||
data["peers"] = peers
|
||||
|
||||
group = api_request("POST", "/groups", data=data)
|
||||
print(f"Group created: {group.get('name')} (ID: {group.get('id')})")
|
||||
|
||||
|
||||
def delete_group(group_id: str):
|
||||
"""Delete a group."""
|
||||
api_request("DELETE", f"/groups/{group_id}")
|
||||
print(f"Group {group_id} deleted.")
|
||||
|
||||
|
||||
# ===== SETUP KEYS =====
|
||||
|
||||
def list_setup_keys():
|
||||
"""List all setup keys."""
|
||||
keys = api_request("GET", "/setup-keys")
|
||||
if not keys:
|
||||
print("No setup keys found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(keys)} setup key(s):\n")
|
||||
for key in keys:
|
||||
print(format_setup_key(key))
|
||||
|
||||
|
||||
def create_setup_key(name: str, key_type: str, expires: int, auto_groups: Optional[List[str]] = None):
|
||||
"""Create a new setup key."""
|
||||
data = {
|
||||
"name": name,
|
||||
"type": key_type,
|
||||
"expires_in": expires,
|
||||
"auto_groups": auto_groups or []
|
||||
}
|
||||
|
||||
key = api_request("POST", "/setup-keys", data=data)
|
||||
print(f"Setup key created: {key.get('name')}")
|
||||
print(f"Key: {key.get('key')}")
|
||||
print(f"ID: {key.get('id')}")
|
||||
|
||||
|
||||
def revoke_setup_key(key_id: str):
|
||||
"""Revoke a setup key."""
|
||||
api_request("PUT", f"/setup-keys/{key_id}", data={"revoked": True, "auto_groups": []})
|
||||
print(f"Setup key {key_id} revoked.")
|
||||
|
||||
|
||||
# ===== POLICIES =====
|
||||
|
||||
def list_policies():
|
||||
"""List all policies."""
|
||||
policies = api_request("GET", "/policies")
|
||||
if not policies:
|
||||
print("No policies found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(policies)} policy(ies):\n")
|
||||
for policy in policies:
|
||||
print(format_policy(policy))
|
||||
|
||||
|
||||
def get_policy(policy_id: str):
|
||||
"""Get policy details."""
|
||||
policy = api_request("GET", f"/policies/{policy_id}")
|
||||
print(json.dumps(policy, indent=2))
|
||||
|
||||
|
||||
def delete_policy(policy_id: str):
|
||||
"""Delete a policy."""
|
||||
api_request("DELETE", f"/policies/{policy_id}")
|
||||
print(f"Policy {policy_id} deleted.")
|
||||
|
||||
|
||||
# ===== ROUTES =====
|
||||
|
||||
def list_routes():
|
||||
"""List all routes."""
|
||||
routes = api_request("GET", "/routes")
|
||||
if not routes:
|
||||
print("No routes found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(routes)} route(s):\n")
|
||||
for route in routes:
|
||||
print(format_route(route))
|
||||
|
||||
|
||||
def get_route(route_id: str):
|
||||
"""Get route details."""
|
||||
route = api_request("GET", f"/routes/{route_id}")
|
||||
print(json.dumps(route, indent=2))
|
||||
|
||||
|
||||
# ===== USERS =====
|
||||
|
||||
def list_users():
|
||||
"""List all users."""
|
||||
users = api_request("GET", "/users")
|
||||
if not users:
|
||||
print("No users found.")
|
||||
return
|
||||
|
||||
print(f"Found {len(users)} user(s):\n")
|
||||
for user in users:
|
||||
name = user.get("name", user.get("email", "unknown"))
|
||||
role = user.get("role", "unknown")
|
||||
service = "[service]" if user.get("is_service_user") else ""
|
||||
print(f"{name} ({role}) {service}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="NetBird CLI")
|
||||
subparsers = parser.add_subparsers(dest="resource", help="Resource type")
|
||||
|
||||
# Peers
|
||||
peers_parser = subparsers.add_parser("peers", help="Manage peers")
|
||||
peers_sub = peers_parser.add_subparsers(dest="action")
|
||||
peers_sub.add_parser("list", help="List all peers")
|
||||
peers_get = peers_sub.add_parser("get", help="Get peer details")
|
||||
peers_get.add_argument("peer_id", help="Peer ID")
|
||||
peers_del = peers_sub.add_parser("delete", help="Delete a peer")
|
||||
peers_del.add_argument("peer_id", help="Peer ID")
|
||||
|
||||
# Groups
|
||||
groups_parser = subparsers.add_parser("groups", help="Manage groups")
|
||||
groups_sub = groups_parser.add_subparsers(dest="action")
|
||||
groups_sub.add_parser("list", help="List all groups")
|
||||
groups_get = groups_sub.add_parser("get", help="Get group details")
|
||||
groups_get.add_argument("group_id", help="Group ID")
|
||||
groups_create = groups_sub.add_parser("create", help="Create a group")
|
||||
groups_create.add_argument("name", help="Group name")
|
||||
groups_create.add_argument("--peers", nargs="+", help="Peer IDs to add")
|
||||
groups_del = groups_sub.add_parser("delete", help="Delete a group")
|
||||
groups_del.add_argument("group_id", help="Group ID")
|
||||
|
||||
# Setup Keys
|
||||
keys_parser = subparsers.add_parser("setup-keys", help="Manage setup keys")
|
||||
keys_sub = keys_parser.add_subparsers(dest="action")
|
||||
keys_sub.add_parser("list", help="List all setup keys")
|
||||
keys_create = keys_sub.add_parser("create", help="Create a setup key")
|
||||
keys_create.add_argument("--name", required=True, help="Key name")
|
||||
keys_create.add_argument("--type", choices=["one-off", "reusable"], default="reusable", help="Key type")
|
||||
keys_create.add_argument("--expires", type=int, default=86400, help="Expiration in seconds (default: 86400 = 24h)")
|
||||
keys_create.add_argument("--auto-groups", nargs="+", help="Group IDs to auto-assign")
|
||||
keys_revoke = keys_sub.add_parser("revoke", help="Revoke a setup key")
|
||||
keys_revoke.add_argument("key_id", help="Key ID")
|
||||
|
||||
# Policies
|
||||
policies_parser = subparsers.add_parser("policies", help="Manage policies")
|
||||
policies_sub = policies_parser.add_subparsers(dest="action")
|
||||
policies_sub.add_parser("list", help="List all policies")
|
||||
policies_get = policies_sub.add_parser("get", help="Get policy details")
|
||||
policies_get.add_argument("policy_id", help="Policy ID")
|
||||
policies_del = policies_sub.add_parser("delete", help="Delete a policy")
|
||||
policies_del.add_argument("policy_id", help="Policy ID")
|
||||
|
||||
# Routes
|
||||
routes_parser = subparsers.add_parser("routes", help="Manage routes")
|
||||
routes_sub = routes_parser.add_subparsers(dest="action")
|
||||
routes_sub.add_parser("list", help="List all routes")
|
||||
routes_get = routes_sub.add_parser("get", help="Get route details")
|
||||
routes_get.add_argument("route_id", help="Route ID")
|
||||
|
||||
# Users
|
||||
users_parser = subparsers.add_parser("users", help="Manage users")
|
||||
users_sub = users_parser.add_subparsers(dest="action")
|
||||
users_sub.add_parser("list", help="List all users")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.resource:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Handle commands
|
||||
if args.resource == "peers":
|
||||
if args.action == "list":
|
||||
list_peers()
|
||||
elif args.action == "get":
|
||||
get_peer(args.peer_id)
|
||||
elif args.action == "delete":
|
||||
delete_peer(args.peer_id)
|
||||
|
||||
elif args.resource == "groups":
|
||||
if args.action == "list":
|
||||
list_groups()
|
||||
elif args.action == "get":
|
||||
get_group(args.group_id)
|
||||
elif args.action == "create":
|
||||
create_group(args.name, args.peers)
|
||||
elif args.action == "delete":
|
||||
delete_group(args.group_id)
|
||||
|
||||
elif args.resource == "setup-keys":
|
||||
if args.action == "list":
|
||||
list_setup_keys()
|
||||
elif args.action == "create":
|
||||
create_setup_key(args.name, args.type, args.expires, args.auto_groups)
|
||||
elif args.action == "revoke":
|
||||
revoke_setup_key(args.key_id)
|
||||
|
||||
elif args.resource == "policies":
|
||||
if args.action == "list":
|
||||
list_policies()
|
||||
elif args.action == "get":
|
||||
get_policy(args.policy_id)
|
||||
elif args.action == "delete":
|
||||
delete_policy(args.policy_id)
|
||||
|
||||
elif args.resource == "routes":
|
||||
if args.action == "list":
|
||||
list_routes()
|
||||
elif args.action == "get":
|
||||
get_route(args.route_id)
|
||||
|
||||
elif args.resource == "users":
|
||||
if args.action == "list":
|
||||
list_users()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: ourgroceries
|
||||
description: Manage OurGroceries shopping lists. Use when Steve wants to add, view, cross off, or remove items from his OurGroceries shopping lists. Also use when Steve mentions groceries, shopping list, or needs to track items to buy.
|
||||
---
|
||||
|
||||
# OurGroceries Skill
|
||||
|
||||
Manage Steve's OurGroceries shopping lists via the `ourgroceries` Python package.
|
||||
|
||||
## Setup
|
||||
|
||||
**Credentials stored in:** `~/.config/ourgroceries/credentials.conf` (mode 600)
|
||||
|
||||
File format:
|
||||
```
|
||||
username=steve@myfamilyemail.co.uk
|
||||
password=A98538
|
||||
```
|
||||
|
||||
## Core Operations
|
||||
|
||||
### List All Lists
|
||||
```python
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from ourgroceries import OurGroceries
|
||||
|
||||
async def main():
|
||||
og = OurGroceries('USERNAME', 'PASSWORD')
|
||||
await og.login()
|
||||
lists = await og.get_my_lists()
|
||||
for l in lists.get('lists', []):
|
||||
print(f\"{l['id']} - {l['name']} ({l['listType']})\")
|
||||
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
|
||||
### Get List Items
|
||||
```python
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from ourgroceries import OurGroceries
|
||||
|
||||
async def main():
|
||||
og = OurGroceries('USERNAME', 'PASSWORD')
|
||||
await og.login()
|
||||
items = await og.get_list_items('LIST_ID')
|
||||
for item in items.get('list', {}).get('items', []):
|
||||
crossed = '✓' if item.get('crossedOff') else ' '
|
||||
print(f\"[{crossed}] {item.get('value')} (id: {item.get('id')})\")
|
||||
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
|
||||
### Add Item to List
|
||||
```python
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from ourgroceries import OurGroceries
|
||||
|
||||
async def main():
|
||||
og = OurGroceries('USERNAME', 'PASSWORD')
|
||||
await og.login()
|
||||
result = await og.add_item_to_list('LIST_ID', 'Item name', category='Produce')
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
|
||||
### Toggle Item Crossed Off
|
||||
```python
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from ourgroceries import OurGroceries
|
||||
|
||||
async def main():
|
||||
og = OurGroceries('USERNAME', 'PASSWORD')
|
||||
await og.login()
|
||||
# cross_off=True marks as done, cross_off=False unmarks
|
||||
result = await og.toggle_item_crossed_off('LIST_ID', 'ITEM_ID', cross_off=True)
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
|
||||
### Remove Item from List
|
||||
```python
|
||||
python3 -c "
|
||||
import asyncio
|
||||
from ourgroceries import OurGroceries
|
||||
|
||||
async def main():
|
||||
og = OurGroceries('USERNAME', 'PASSWORD')
|
||||
await og.login()
|
||||
result = await og.remove_item_from_list('LIST_ID', 'ITEM_ID')
|
||||
print(result)
|
||||
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
|
||||
## Workflows
|
||||
|
||||
### "Add X to my shopping list"
|
||||
1. Get list ID from `get_my_lists()` (look for list named "Shopping" or "Main")
|
||||
2. Add item with `add_item_to_list(list_id, item_name)`
|
||||
|
||||
### "What's on my shopping list?"
|
||||
1. Get list ID
|
||||
2. Fetch items with `get_list_items(list_id)`
|
||||
3. Display with checkboxes for crossed-off status
|
||||
|
||||
### "Clear crossed-off items"
|
||||
1. Use `delete_all_crossed_off_from_list(list_id)`
|
||||
|
||||
## Steve's Lists
|
||||
|
||||
Stored in `TOOLS.md`:
|
||||
- **Shopping List:** `OIqZ7A85kWCITf9wspo_ae`
|
||||
- **July 2026 Holiday:** `Cfl-96OqEU2IObGAUd7S0f`
|
||||
|
||||
## Notes
|
||||
|
||||
- `add_items_to_list(list_id, items)` accepts a list of tuples `(value, category, note)` for bulk adding
|
||||
- Use `auto_category=True` to let OurGroceries guess the category
|
||||
- List IDs are strings like `abc123xyz`
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: planka
|
||||
description: Manage Planka project boards using the pcli CLI. Use when the user wants to interact with Planka boards, cards, lists, tasks, labels, or comments.
|
||||
---
|
||||
|
||||
# Planka Skill
|
||||
|
||||
Manage Planka project boards using the pcli CLI. Use when the user wants to interact with Planka boards, cards, lists, tasks, labels, or comments.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure environment variables are set:
|
||||
```bash
|
||||
export PLANKA_URL="https://planka.example.com"
|
||||
export PLANKA_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
Ensure `pcli` is in PATH and `jq` is installed.
|
||||
|
||||
## Global Flags
|
||||
|
||||
All commands (apart from import/export) accept: `--format json|table`, `--url <url>`, `--api-key <key>`, `--log-level debug|info|warn|error`
|
||||
|
||||
## Commands
|
||||
|
||||
### Status Overview
|
||||
|
||||
```bash
|
||||
pcli status
|
||||
```
|
||||
|
||||
Returns summary of all boards, lists, and card counts (open/closed per list).
|
||||
|
||||
### Projects
|
||||
|
||||
```bash
|
||||
pcli project list
|
||||
pcli project get <project-id>
|
||||
pcli project create --name "Name" --type private # type: private or shared
|
||||
pcli project delete <project-id>
|
||||
|
||||
# Export/Import
|
||||
pcli project export <project-id-or-name> [--board <name-or-id>] > backup.json
|
||||
pcli project import --file backup.json
|
||||
pcli project import < backup.json
|
||||
```
|
||||
|
||||
Export outputs a portable JSON file (names, not IDs). Import creates all resources with fresh IDs.
|
||||
Import fails if the project+board name combination already exists on the target.
|
||||
Comments are exported with `(Original comment by <username>)` prefix for attribution.
|
||||
|
||||
**Note:** Export outputs its own envelope format (`{version, exportedAt, project}`) directly — not the standard `{data, error}` envelope. Import progress and errors go to stderr.
|
||||
|
||||
### Boards
|
||||
|
||||
```bash
|
||||
pcli board list
|
||||
pcli board get <board-id> # includes lists and cards
|
||||
pcli board actions <board-id> [--limit N]
|
||||
pcli board create --project <project-id> --name "Board Name" [--position N]
|
||||
pcli board delete <board-id>
|
||||
```
|
||||
|
||||
### Lists (Board Columns)
|
||||
|
||||
```bash
|
||||
pcli list create --board <board-id> --name "Column Name" [--type active|closed] [--position N]
|
||||
pcli list get <list-id>
|
||||
pcli list update <list-id> [--name "..."] [--type active|closed] [--color "..."] [--position N] [--board <board-id>]
|
||||
pcli list delete <list-id>
|
||||
```
|
||||
|
||||
### Cards
|
||||
|
||||
```bash
|
||||
# List (one of --board or --list required, mutually exclusive)
|
||||
pcli card list --board <board-id> [--limit N]
|
||||
pcli card list --list <list-id> [--limit N]
|
||||
|
||||
# CRUD
|
||||
pcli card get <card-id>
|
||||
pcli card create --list <list-id> --name "Name" [--description "..."] [--type project|story] [--position N] [--due-date "ISO8601"] [--due-completed]
|
||||
pcli card update <card-id> [--name "..."] [--description "..."] [--type ...] [--position N] [--due-date "..."] [--due-completed]
|
||||
pcli card delete <card-id>
|
||||
pcli card duplicate <card-id> --name "Copy" [--position N]
|
||||
pcli card move <card-id> --list <target-list-id> [--position N]
|
||||
|
||||
# Members
|
||||
pcli card assign <card-id> --user <user-id>
|
||||
pcli card unassign <card-id> --user <user-id>
|
||||
|
||||
# Labels
|
||||
pcli card add-label <card-id> --label <label-id>
|
||||
pcli card remove-label <card-id> --label <label-id>
|
||||
|
||||
# Actions
|
||||
pcli card actions <card-id> [--limit N]
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
```bash
|
||||
pcli comment list --card <card-id> [--limit N]
|
||||
pcli comment create --card <card-id> --text "..."
|
||||
pcli comment update <comment-id> --text "..."
|
||||
pcli comment delete <comment-id>
|
||||
```
|
||||
|
||||
### Task Lists
|
||||
|
||||
```bash
|
||||
pcli task-list create --card <card-id> --name "Checklist" [--position N] [--show-on-front] [--hide-completed]
|
||||
pcli task-list get <task-list-id>
|
||||
pcli task-list update <task-list-id> [--name "..."] [--position N] [--show-on-front] [--hide-completed]
|
||||
pcli task-list delete <task-list-id>
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
```bash
|
||||
pcli task create --task-list <task-list-id> --name "Item" [--position N] [--completed]
|
||||
pcli task update <task-id> [--name "..."] [--position N] [--completed]
|
||||
pcli task delete <task-id>
|
||||
```
|
||||
|
||||
### Labels
|
||||
|
||||
```bash
|
||||
pcli label create --board <board-id> --name "Bug" --color "berry-red" [--position N]
|
||||
pcli label update <label-id> [--name "..."] [--color "..."] [--position N]
|
||||
pcli label delete <label-id>
|
||||
```
|
||||
|
||||
## Extracting IDs from Output
|
||||
|
||||
All responses use `{"data": ..., "error": null}`. Extract IDs with jq:
|
||||
|
||||
```bash
|
||||
# Single object
|
||||
pcli card create --list <id> --name "X" | jq -r '.data.id'
|
||||
|
||||
# Array
|
||||
pcli card list --board <id> | jq -r '.data[].id'
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Create a complete Kanban board
|
||||
|
||||
```bash
|
||||
# Create board
|
||||
BOARD_ID=$(pcli board create --project <project-id> --name "Development Board" | jq -r '.data.id')
|
||||
|
||||
# Create columns
|
||||
pcli list create --board $BOARD_ID --name "Backlog" --type "active" --position 0
|
||||
TODO_ID=$(pcli list create --board $BOARD_ID --name "To Do" --type "active" --position 65536 | jq -r '.data.id')
|
||||
PROGRESS_ID=$(pcli list create --board $BOARD_ID --name "In Progress" --type "active" --position 131072 | jq -r '.data.id')
|
||||
DONE_ID=$(pcli list create --board $BOARD_ID --name "Done" --type "closed" --position 196608 | jq -r '.data.id')
|
||||
|
||||
# View the board
|
||||
pcli board get $BOARD_ID --format table
|
||||
```
|
||||
|
||||
### Create a card with a checklist
|
||||
|
||||
```bash
|
||||
CARD_ID=$(pcli card create --list <list-id> --name "Task" | jq -r '.data.id')
|
||||
TL_ID=$(pcli task-list create --card $CARD_ID --name "Steps" | jq -r '.data.id')
|
||||
pcli task create --task-list $TL_ID --name "Step 1"
|
||||
pcli task create --task-list $TL_ID --name "Step 2"
|
||||
```
|
||||
|
||||
### Move all cards between lists
|
||||
|
||||
```bash
|
||||
pcli card list --list <source-list-id> | jq -r '.data[].id' | while read id; do
|
||||
pcli card move $id --list <target-list-id>
|
||||
done
|
||||
```
|
||||
|
||||
### Export and import a project
|
||||
|
||||
```bash
|
||||
# Export entire project
|
||||
pcli project export "My Project" > project-backup.json
|
||||
|
||||
# Export single board
|
||||
pcli project export "My Project" --board "Sprint Board" > board-backup.json
|
||||
|
||||
# Import to another instance (or same instance if project+board combo doesn't exist)
|
||||
pcli project import --file project-backup.json
|
||||
```
|
||||
Reference in New Issue
Block a user