From vibe-desk-display
Pairs with and sends notifications (text, rich layouts, buzzer) to a VibeDesk display over LAN. Automatically sends task-done + usage data. Can be triggered by voice for pairing and threshold tuning.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vibe-desk-display:vibe-desk-displayThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Discover, pair, and send notifications to a VibeDesk display on the local network.
Discover, pair, and send notifications to a VibeDesk display on the local network. After pairing, Claude proactively sends a task-done notification + usage data when completing a task.
Path: ~/.config/autonomous-lcd.json
{
"devices": [
{
"device_id": "lcd-bd4a14",
"label": "My Display",
"last_known_ip": "192.168.1.42",
"last_seen_at": "2026-05-18T10:30:00Z"
}
],
"default_device_id": "lcd-bd4a14"
}
Device ID format: lcd-<6 lowercase hex digits> (from last 3 bytes of MAC address).
Find devices on the LAN using the bundled discovery script.
When to use: during pairing, or when a notification fails with connection error/timeout.
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/discover.py
Output: JSON array of found devices, e.g.:
[{"device_id": "lcd-bd4a14", "ip": "192.168.1.42"}]
How it works (automatic):
On failure: exits with code 1, stderr contains {"error": "not_found"}. Suggest: device powered on? Same WiFi?
One-time setup to register a new device. Uses OTP code displayed on the device screen — no need to read device ID stickers.
When to use: "pair my display", "add a screen", "set up device".
Only show simple, non-technical messages to the user during pairing:
Do NOT show to user: device IDs, IP addresses, config paths, HTTP status codes, raw JSON, error tracebacks, or internal debug info. These are implementation details.
Bash output: All bash commands in the pair flow should redirect stdout to /dev/null or use a wrapper that only prints a single friendly message (e.g. "done" or "ok"). Never let raw JSON, device IDs, or HTTP responses appear in the transcript. Pipe discovery output into a variable and process silently within the same script.
{
"play_sound": 20,
"items": [
{ "type": "text", "text": "Pairing", "x": 0, "y": 0, "width": 220, "align": "center", "size": 3, "color": "#7eb8da" },
{ "type": "text", "text": "<CODE>", "x": 0, "y": 35, "width": 220, "align": "center", "size": 4, "color": "#e8dcc8" },
{ "type": "text", "text": "Enter this code", "x": 0, "y": 85, "width": 220, "align": "center", "size": 2, "color": "#9a9488" }
]
}
Skip devices that fail to respond — don't mention them to the user.~/.config/autonomous-lcd.json:
devices[]default_device_id0600{"text": "Paired with Claude", "color": "green", "play_sound": 20}Re-pairing same device ID is an update, not an error.
Remove a device.
When to use: "unpair my display", "remove my display", "stop display updates".
~/.config/autonomous-lcd.json.default_device_id, clear it (or set to next device if any remain).Send a notification to a paired device. This is the most frequently used operation.
When to use: "show on my screen", "notify my display", "ping my display when done". Also use proactively at end of long tasks (build, test, deploy).
default_device_id from config, ORLegacy text mode — simple notification:
{
"text": "Build passed in 3m 12s",
"size": 2,
"color": "green",
"play_sound": 20
}
text (required) — max 500 chars, strip markdown to plain textsize (optional) — 1 to 4, default 2color (optional) — red, green, blue, yellow, white, or hex #RRGGBBplay_sound — always include 20 (claude_style) unless user specifies otherwiseRich layout mode — positioned items:
{
"play_sound": 20,
"items": [
{ "type": "text", "text": "Title", "x": 12, "y": 0, "width": 196, "size": 4, "color": "#ffffff" },
{ "type": "rect", "x": 6, "y": 34, "width": 208, "height": 72, "radius": 10, "color": "#171d18" },
{ "type": "progress", "x": 18, "y": 78, "width": 184, "height": 14, "radius": 4, "value": 50, "color": "#9bad67", "bg_color": "#2f2f2f" }
]
}
When items[] has at least one valid item, rich layout is used and text is ignored.
Coordinate system:
(0, 0) = top-left of content area(219, 116)Limits:
value: 0..100, radius: 0..50, stroke_width: 1..32import json, urllib.request
payload = json.dumps(<json_payload>).encode()
req = urllib.request.Request(
"http://<last_known_ip>:3000/lcd",
data=payload,
headers={
"Content-Type": "application/json",
"X-Device-ID": "<device_id>",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=3) as resp:
print(f"HTTP {resp.status}")
print(resp.read().decode())
| Status | Action |
|---|---|
| 200 | Success. Update last_seen_at in config. |
| 400 | Malformed payload. Check JSON. |
| 403 | Device ID mismatch. Run rediscovery. |
| 413 | Text >500 chars. Truncate to 497 + "...", retry once. |
| Connection error/timeout | Cached IP stale. Run rediscovery (section 1), retry once. |
If user asks for dry run, show the payload and target URL without sending.
Handled automatically by the Stop hook (scripts/on-stop-done.py). Every time Claude finishes a response, the hook:
>= the configured threshold → sends usage section 1 (5-hour) after 5s, then usage section 2 (7-day) after another 5s.Rate-limited to once per 60 seconds. No action needed from Claude — the hook runs automatically.
usage_thresholdSet usage_threshold (integer, 0–100) in ~/.config/autonomous-lcd.json to control when usage is shown after Task Done. Default: 80.
{
"devices": [...],
"default_device_id": "...",
"usage_threshold": 80
}
Let the user retune the auto-warning threshold in plain language — no need to edit JSON by hand.
When to use: "change my usage threshold", "warn me earlier", "set warning threshold to 60", "only warn me near the limit", "show usage sooner", "I want alerts at 90%".
Procedure:
~/.config/autonomous-lcd.json (if missing → tell the user to pair a display first).usage_threshold to the new value, preserving all other keys (devices, default_device_id, etc.). Write back as valid JSON with file permission 0600.Note: usage_threshold only controls when usage auto-displays after a task. The progress-bar colors (green <60, orange 60–79, red ≥80) are independent and do not change with this value.
Fetch real usage data from the Claude Code API and display immediately. Used by the /vibe-desk-display:usage slash command or as part of the Task Done flow (section 5).
When to use: "show my usage on display", "update usage now", "refresh display".
import json, os, subprocess
def _find_strings(obj):
if isinstance(obj, str):
yield obj
elif isinstance(obj, dict):
for v in obj.values():
yield from _find_strings(v)
elif isinstance(obj, list):
for v in obj:
yield from _find_strings(v)
def get_token():
cred_path = os.path.expanduser("~/.claude/.credentials.json")
if os.path.exists(cred_path):
with open(cred_path) as f:
data = json.load(f)
for v in _find_strings(data):
if v.startswith("sk-ant-oat"):
return v
try:
kc = subprocess.run(
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
capture_output=True, text=True
)
if kc.returncode == 0:
data = json.loads(kc.stdout.strip())
for v in _find_strings(data):
if v.startswith("sk-ant-oat"):
return v
except Exception:
pass
return None
import urllib.request
from datetime import datetime, timezone
def fetch_usage(token):
req = urllib.request.Request(
"https://api.anthropic.com/api/oauth/usage",
headers={
"Authorization": f"Bearer {token}",
"anthropic-beta": "oauth-2025-04-20",
},
)
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
def time_left(iso_str):
if not iso_str:
return "N/A"
diff = datetime.fromisoformat(iso_str) - datetime.now(timezone.utc)
total_sec = int(diff.total_seconds())
if total_sec <= 0:
return "now"
h = total_sec // 3600
m = (total_sec % 3600) // 60
if h > 24:
return f"{h // 24}d {h % 24}h"
return f"{h}h {m}m"
token = get_token()
usage = fetch_usage(token)
pct_5h = int(usage["five_hour"]["utilization"])
pct_7d = int(usage["seven_day"]["utilization"])
reset_5h = time_left(usage["five_hour"]["resets_at"])
reset_7d = time_left(usage["seven_day"]["resets_at"])
Section 1 — Header + Current (5-hour):
{
"play_sound": 20,
"items": [
{ "type": "text", "text": "Usage", "x": 0, "y": 5, "width": 220, "align": "center", "size": 4, "color": "#d4845a" },
{ "type": "text", "text": "<pct_5h>%", "x": 18, "y": 38, "width": 100, "size": 4, "color": "<progress_color>" },
{ "type": "text", "text": "Current", "x": 100, "y": 46, "width": 105, "align": "right", "size": 1, "color": "#7eb8da" },
{ "type": "progress", "x": 18, "y": 70, "width": 184, "height": 12, "radius": 6, "value": <pct_5h>, "color": "<progress_color>", "bg_color": "#3a3a3a" },
{ "type": "text", "text": "Resets in <reset_5h>", "x": 18, "y": 90, "width": 180, "size": 1, "color": "#e8dcc8" }
]
}
Section 2 — Weekly (7-day) + status:
{
"play_sound": 20,
"items": [
{ "type": "text", "text": "<pct_7d>%", "x": 18, "y": 10, "width": 100, "size": 4, "color": "#e8dcc8" },
{ "type": "text", "text": "Weekly", "x": 100, "y": 18, "width": 105, "align": "right", "size": 1, "color": "#a09888" },
{ "type": "progress", "x": 18, "y": 45, "width": 184, "height": 12, "radius": 6, "value": <pct_7d>, "color": "<progress_color>", "bg_color": "#3a3a3a" },
{ "type": "text", "text": "Resets in <reset_7d>", "x": 18, "y": 63, "width": 180, "size": 1, "color": "#9a9488" },
{ "type": "text", "text": "* Baking...", "x": 0, "y": 87, "width": 220, "align": "center", "size": 2, "color": "#d4845a" }
]
}
Send section 1 first (with play_sound: 20), wait 5 seconds, then send section 2 (with play_sound: 0).
Progress bar color: green #6b8f4e (<60%), orange #d4845a (60-79%), red #c0392b (≥80%).
Rate limit: The usage API rate-limits hard. Do NOT poll faster than once per minute.
A privacy-first, on-device builder profile derived from the user's local
Claude Code session transcripts (~/.claude/projects/**/*.jsonl). Inspired by
Paxel, but nothing leaves the machine — no network, no upload.
When to use: "show my builder profile", "what kind of coder am I", "/vibe-desk-display:insights", "rotate my insights on the display".
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/insights.py --display # rotate all cards
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/insights.py --json # numbers only
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/insights.py --card N # send one card (daemon use)
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/insights.py --fresh # bypass the 1h cache
The engine computes: archetype (e.g. "The Architect", "Night Owl"), day streak,
peak coding hour, sessions/prompts, top model, token economy + cache-hit rate,
steering style (words/prompt, course-correct rate), velocity (tools/turn, edits),
and top projects. The profile is cached to ~/.config/autonomous-lcd-insights.json
(TTL 1h) so the daemon doesn't re-scan every transcript each tick.
archetype → rhythm → top model → token economy → builder style → top projects → top tool
show_insightsWhen the launchd usage daemon (lcd-usage-daemon.py) runs, it appends one
rotating insight card after the usage sections, advancing through the rotation
each tick (index in ~/.config/autonomous-lcd-insights.idx). Set
"show_insights": false in ~/.config/autonomous-lcd.json to disable this and
keep the ambient display usage-only. Default: true. Insight failures are
swallowed and never affect the usage display.
text — Render text at position
| Field | Type | Default | Description |
|---|---|---|---|
text | string | (required) | Text content (max 95 chars) |
size | int 1-4 | 2 | Font size |
align | string | left | left, center, right |
width | int | 0 | Layout width (0 = remaining) |
color | string | blue | Text color |
rect — Rectangle or rounded rectangle
| Field | Type | Default | Description |
|---|---|---|---|
width | int | (required) | Width |
height | int | (required) | Height |
radius | int | 0 | Corner radius |
filled | bool | true | Fill or outline |
progress — Progress bar
| Field | Type | Default | Description |
|---|---|---|---|
width | int | (required) | Width |
height | int | (required) | Height |
value | int 0-100 | (required) | Percentage |
radius | int | 0 | Corner radius |
bg_color | string | #303030 | Track color |
line — Line between two points
| Field | Type | Default | Description |
|---|---|---|---|
x2 | int | (required) | End X |
y2 | int | (required) | End Y |
stroke_width | int | 1 | Thickness |
circle — Circle (set width = height)
| Field | Type | Default | Description |
|---|---|---|---|
width | int | (required) | Diameter |
height | int | (required) | = width |
filled | bool | true | Fill or outline |
ellipse — Same fields as circle, different width/height.
arc — Arc stroke over angle range
| Field | Type | Default | Description |
|---|---|---|---|
width | int | (required) | Bounding width |
height | int | (required) | Bounding height |
start_angle | int | 0 | Start angle |
end_angle | int | 360 | End angle |
stroke_width | int | 1 | Arc thickness |
triangle — Triangle from 3 points
| Field | Type | Default | Description |
|---|---|---|---|
x2 | int | (required) | Point 2 X |
y2 | int | (required) | Point 2 Y |
x3 | int | (required) | Point 3 X |
y3 | int | (required) | Point 3 Y |
filled | bool | true | Fill or outline |
polygon — Polygon from point list
| Field | Type | Default | Description |
|---|---|---|---|
points | string | (required) | x1,y1; x2,y2; x3,y3 ... (max 95 chars) |
filled | bool | true | Fill or outline |
ring — Circular outline over angle range
| Field | Type | Default | Description |
|---|---|---|---|
width | int | (required) | Bounding width |
height | int | (required) | Bounding height |
start_angle | int | 0 | Start angle |
end_angle | int | 360 | End angle (360 = full ring) |
stroke_width | int | 1 | Ring thickness |
pie — Filled wedge shape
| Field | Type | Default | Description |
|---|---|---|---|
width | int | (required) | Bounding width |
height | int | (required) | Bounding height |
start_angle | int | 0 | Start angle |
end_angle | int | 360 | End angle |
| Field | Type | Default | Description |
|---|---|---|---|
type | string | text | Item type |
x | int | 0 | X position in viewport |
y | int | 0 | Y position in viewport |
color | string | root color | Item color |
bg_color | string | #303030 | Background (progress) |
play_sound can be included in any request (both legacy and rich mode). Plays once when payload is accepted. Requires text or items — cannot be sent standalone.
| Index | Name | Style |
|---|---|---|
| 0 | (muted) | Sound off |
| 1 | triple_ping | Sharp high triple ping |
| 2 | lift_chime | Smooth rising chime |
| 3 | bright_fanfare | Bright success fanfare |
| 4 | micro_success | Short positive triad |
| 5 | soft_drop | Soft descending confirmation |
| 6 | double_tick | Quick double tick and resolve |
| 7 | alert_fall | Short alert drop |
| 8 | clean_pop | Modern light popup sound |
| 9 | confirm_arc | Rising confirm arc |
| 10 | status_ready | Compact ready cue |
| 11 | soft_descend | Calm descending tone |
| 12 | gentle_open | Friendly open cue |
| 13 | focus_ping | Clean focused ping |
| 14 | tap_rise | Tap then rise |
| 15 | resolve_down | Downward resolve |
| 16 | signal_up | Fast upward signal |
| 17 | digital_bloom | Slightly synthetic bloom |
| 18 | notify_peak | Peak notification tone |
| 19 | warning_soft | Soft warning fall |
| 20 | claude_style | Crisp bright code-style notification |
Default notification sound: 20 (claude_style).
Copy-paste ready payloads for quick testing.
{
"play_sound": 20,
"items": [
{ "type": "text", "text": "Usage", "x": 0, "y": 5, "width": 220, "align": "center", "size": 4, "color": "#d4845a" },
{ "type": "text", "text": "41%", "x": 18, "y": 38, "width": 100, "size": 4, "color": "#6b8f4e" },
{ "type": "text", "text": "Current", "x": 100, "y": 46, "width": 105, "align": "right", "size": 1, "color": "#7eb8da" },
{ "type": "progress", "x": 18, "y": 70, "width": 184, "height": 12, "radius": 6, "value": 41, "color": "#6b8f4e", "bg_color": "#3a3a3a" },
{ "type": "text", "text": "Resets in 1h 56m", "x": 18, "y": 90, "width": 180, "size": 1, "color": "#e8dcc8" }
]
}
{
"play_sound": 20,
"items": [
{ "type": "text", "text": "48%", "x": 18, "y": 10, "width": 100, "size": 4, "color": "#e8dcc8" },
{ "type": "text", "text": "Weekly", "x": 100, "y": 18, "width": 105, "align": "right", "size": 1, "color": "#a09888" },
{ "type": "progress", "x": 18, "y": 45, "width": 184, "height": 12, "radius": 6, "value": 48, "color": "#6b8f4e", "bg_color": "#3a3a3a" },
{ "type": "text", "text": "Resets in 1d 11h", "x": 18, "y": 63, "width": 180, "size": 1, "color": "#9a9488" },
{ "type": "text", "text": "* Baking...", "x": 0, "y": 87, "width": 220, "align": "center", "size": 2, "color": "#d4845a" }
]
}
{
"text": "Build passed in 3m 12s",
"size": 2,
"color": "green",
"play_sound": 20
}
{
"play_sound": 20,
"items": [
{ "type": "circle", "x": 10, "y": 12, "width": 28, "height": 28, "color": "#7cb342", "filled": true },
{ "type": "ellipse", "x": 52, "y": 10, "width": 48, "height": 30, "color": "#ffffff", "filled": false, "stroke_width": 2 },
{ "type": "line", "x": 8, "y": 64, "x2": 206, "y2": 64, "color": "#4fc3f7", "stroke_width": 3 },
{ "type": "arc", "x": 118, "y": 10, "width": 38, "height": 38, "color": "#ffb300", "stroke_width": 4, "start_angle": 0, "end_angle": 270 },
{ "type": "triangle", "x": 22, "y": 84, "x2": 44, "y2": 112, "x3": 4, "y3": 112, "color": "#ef5350", "filled": true },
{ "type": "pie", "x": 166, "y": 82, "width": 36, "height": 36, "color": "#66bb6a", "start_angle": 0, "end_angle": 120 }
]
}
X-Device-ID header required (no X-Token)."play_sound": 20 unless user specifies otherwise.0600.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub autonomous-ai/autonomous-desk --plugin vibe-desk-display