Add initial skills: caldav, netbird, ourgroceries, planka

This commit is contained in:
Bobby
2026-04-01 14:29:17 +01:00
commit cd5694aa13
7 changed files with 1121 additions and 0 deletions
+38
View File
@@ -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
View File
@@ -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
```
+84
View File
@@ -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
+137
View File
@@ -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.
+391
View File
@@ -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()
+130
View File
@@ -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
View File
@@ -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
```