From c4a67b7d02c9b0e0b004b0e82687ad200968aa4d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Dec 2025 17:23:45 +0100 Subject: [PATCH] feat: refresh skills metadata and toggles --- apps/macos/Sources/Clawdis/SkillsModels.swift | 1 + .../Sources/Clawdis/SkillsSettings.swift | 89 +++++++++++++----- docs/mac/skills.md | 3 +- docs/skills.md | 9 +- skills/bird/SKILL.md | 2 +- skills/blucli/SKILL.md | 2 +- skills/brave-search/SKILL.md | 2 +- skills/camsnap/SKILL.md | 2 +- skills/clawdis-browser/SKILL.md | 2 +- skills/clawdis-cron/SKILL.md | 2 +- skills/clawdis-nodes/SKILL.md | 1 + skills/clawdis-notify/SKILL.md | 1 + skills/eightctl/SKILL.md | 2 +- skills/gemini/SKILL.md | 2 +- skills/gog/SKILL.md | 2 +- skills/imsg/SKILL.md | 2 +- skills/mcporter/SKILL.md | 2 +- skills/nano-banana-pro/SKILL.md | 2 +- skills/nano-pdf/SKILL.md | 2 +- skills/openai-image-gen/SKILL.md | 2 +- skills/openai-whisper-api/SKILL.md | 2 +- skills/openai-whisper/SKILL.md | 2 +- skills/openhue/SKILL.md | 2 +- skills/oracle/SKILL.md | 2 +- skills/peekaboo/SKILL.md | 2 +- skills/qmd/SKILL.md | 2 +- skills/sag/SKILL.md | 2 +- skills/sonoscli/SKILL.md | 2 +- skills/spotify-player/SKILL.md | 2 +- skills/summarize/SKILL.md | 2 +- skills/video-frames/SKILL.md | 2 +- skills/wacli/SKILL.md | 2 +- src/agents/skills-install.ts | 43 ++++++--- src/agents/skills-status.ts | 91 ++++++++++++++----- src/agents/skills.ts | 30 ++++-- 35 files changed, 221 insertions(+), 99 deletions(-) diff --git a/apps/macos/Sources/Clawdis/SkillsModels.swift b/apps/macos/Sources/Clawdis/SkillsModels.swift index ac6fab03f..6f07cffa1 100644 --- a/apps/macos/Sources/Clawdis/SkillsModels.swift +++ b/apps/macos/Sources/Clawdis/SkillsModels.swift @@ -15,6 +15,7 @@ struct SkillStatus: Codable, Identifiable { let baseDir: String let skillKey: String let primaryEnv: String? + let emoji: String? let always: Bool let disabled: Bool let eligible: Bool diff --git a/apps/macos/Sources/Clawdis/SkillsSettings.swift b/apps/macos/Sources/Clawdis/SkillsSettings.swift index 6dc51a4f9..5472b831b 100644 --- a/apps/macos/Sources/Clawdis/SkillsSettings.swift +++ b/apps/macos/Sources/Clawdis/SkillsSettings.swift @@ -96,32 +96,30 @@ private struct SkillRow: View { private var missingConfig: [String] { self.skill.missing.config } var body: some View { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 10) { HStack(alignment: .top, spacing: 12) { - VStack(alignment: .leading, spacing: 4) { - Text(self.skill.name) - .font(.headline) + Text(self.skill.emoji ?? "✨") + .font(.title2) + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(self.skill.name) + .font(.headline) + self.statusBadge + } Text(self.skill.description) .font(.subheadline) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) - Text(self.sourceLabel) - .font(.caption) - .foregroundStyle(.secondary) + self.metaRow } Spacer() - self.statusBadge } if self.skill.disabled { Text("Disabled in config") .font(.caption) .foregroundStyle(.secondary) - } else if self.skill.eligible { - Text("Enabled") - .font(.caption) - .foregroundStyle(.secondary) - } else { + } else if !self.skill.eligible { self.missingSummary } @@ -140,7 +138,18 @@ private struct SkillRow: View { } private var sourceLabel: String { - self.skill.source.replacingOccurrences(of: "clawdis-", with: "") + switch self.skill.source { + case "clawdis-bundled": + return "Bundled" + case "clawdis-managed": + return "Managed" + case "clawdis-workspace": + return "Workspace" + case "clawdis-extra": + return "Extra" + default: + return self.skill.source + } } private var statusBadge: some View { @@ -156,7 +165,33 @@ private struct SkillRow: View { .foregroundStyle(.orange) } } - .font(.subheadline) + .font(.caption) + } + + private var metaRow: some View { + HStack(spacing: 10) { + SkillTag(text: self.sourceLabel) + HStack(spacing: 6) { + Text(self.enabledLabel) + .font(.caption) + .foregroundStyle(.secondary) + Toggle("", isOn: self.enabledBinding) + .toggleStyle(.switch) + .labelsHidden() + .disabled(self.isBusy) + } + Spacer(minLength: 0) + } + } + + private var enabledLabel: String { + self.skill.disabled ? "Disabled" : "Enabled" + } + + private var enabledBinding: Binding { + Binding( + get: { !self.skill.disabled }, + set: { self.onToggleEnabled($0) }) } @ViewBuilder @@ -199,16 +234,6 @@ private struct SkillRow: View { private var actionRow: some View { HStack(spacing: 8) { - if self.skill.disabled { - Button("Enable") { self.onToggleEnabled(true) } - .buttonStyle(.borderedProminent) - .disabled(self.isBusy) - } else { - Button("Disable") { self.onToggleEnabled(false) } - .buttonStyle(.bordered) - .disabled(self.isBusy) - } - ForEach(self.installOptions) { option in Button(option.label) { self.onInstall(option) } .buttonStyle(.borderedProminent) @@ -254,6 +279,20 @@ private struct SkillRow: View { } } +private struct SkillTag: View { + let text: String + + var body: some View { + Text(self.text) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background(Color.secondary.opacity(0.12)) + .clipShape(Capsule()) + } +} + private struct EnvEditorState: Identifiable { let skillKey: String let skillName: String diff --git a/docs/mac/skills.md b/docs/mac/skills.md index 3e5b55d74..968205ae0 100644 --- a/docs/mac/skills.md +++ b/docs/mac/skills.md @@ -13,8 +13,9 @@ The macOS app surfaces Clawdis skills via the gateway; it does not parse skills - Requirements are derived from `metadata.clawdis.requires` in each `SKILL.md`. ## Install actions -- `metadata.clawdis.install` defines install options (brew/node/go/pnpm/git/shell). +- `metadata.clawdis.install` defines install options (brew/node/go/pnpm/shell). - The app calls `skills.install` to run installers on the gateway host. +- The gateway surfaces only one preferred installer when multiple are provided (brew when available, otherwise node manager from `skillsInstall`). ## Env/API keys - The app stores keys in `~/.clawdis/clawdis.json` under `skills.`. diff --git a/docs/skills.md b/docs/skills.md index e270d35bb..761b514df 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -54,11 +54,12 @@ metadata: {"clawdis":{"requires":{"bins":["uv"],"env":["GEMINI_API_KEY"],"config Fields under `metadata.clawdis`: - `always: true` — always include the skill (skip other gates). +- `emoji` — optional emoji used by the macOS Skills UI. - `requires.bins` — list; each must exist on `PATH`. - `requires.env` — list; env var must exist **or** be provided in config. - `requires.config` — list of `clawdis.json` paths that must be truthy. - `primaryEnv` — env var name associated with `skills..apiKey`. -- `install` — optional array of installer specs used by the macOS Skills UI (brew/node/go/pnpm/git/shell). +- `install` — optional array of installer specs used by the macOS Skills UI (brew/node/go/pnpm/shell). Installer example: @@ -66,10 +67,14 @@ Installer example: --- name: gemini description: Use Gemini CLI for coding assistance and Google search lookups. -metadata: {"clawdis":{"requires":{"bins":["gemini"]},"install":[{"id":"brew","kind":"brew","formula":"gemini-cli","bins":["gemini"],"label":"Install Gemini CLI (brew)"}]}} +metadata: {"clawdis":{"emoji":"♊️","requires":{"bins":["gemini"]},"install":[{"id":"brew","kind":"brew","formula":"gemini-cli","bins":["gemini"],"label":"Install Gemini CLI (brew)"}]}} --- ``` +Notes: +- If multiple installers are listed, the gateway picks a **single** preferred option (brew when available, otherwise node). +- Node installs honor `skillsInstall.nodeManager` in `clawdis.json` (default: npm). + If no `metadata.clawdis` is present, the skill is always eligible (unless disabled in config). ## Config overrides (`~/.clawdis/clawdis.json`) diff --git a/skills/bird/SKILL.md b/skills/bird/SKILL.md index c60ff2e69..9bb3d3581 100644 --- a/skills/bird/SKILL.md +++ b/skills/bird/SKILL.md @@ -1,7 +1,7 @@ --- name: bird description: X/Twitter CLI for reading, searching, and posting via cookies or Sweetistics. -metadata: {"clawdis":{"requires":{"bins":["bird"]},"install":[{"id":"pnpm-build","kind":"shell","command":"if [ ! -d ~/Projects/bird ]; then git clone https://github.com/steipete/bird.git ~/Projects/bird; fi && cd ~/Projects/bird && pnpm install && pnpm run binary","bins":["bird"],"label":"Clone + build bird (pnpm)"}]}} +metadata: {"clawdis":{"emoji":"🐦","requires":{"bins":["bird"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/bird","bins":["bird"],"label":"Install bird (brew)"}]}} --- # bird diff --git a/skills/blucli/SKILL.md b/skills/blucli/SKILL.md index 8d19df7a2..b3cecb299 100644 --- a/skills/blucli/SKILL.md +++ b/skills/blucli/SKILL.md @@ -1,7 +1,7 @@ --- name: blucli description: BluOS CLI (blu) for discovery, playback, grouping, and volume. -metadata: {"clawdis":{"requires":{"bins":["blu"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/blucli/cmd/blu@latest","bins":["blu"],"label":"Install blucli (go)"}]}} +metadata: {"clawdis":{"emoji":"🫐","requires":{"bins":["blu"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/blucli/cmd/blu@latest","bins":["blu"],"label":"Install blucli (go)"}]}} --- # blucli (blu) diff --git a/skills/brave-search/SKILL.md b/skills/brave-search/SKILL.md index 512f1f3e2..ca8ae52b7 100644 --- a/skills/brave-search/SKILL.md +++ b/skills/brave-search/SKILL.md @@ -1,7 +1,7 @@ --- name: brave-search description: Web search and content extraction via Brave Search API. -metadata: {"clawdis":{"requires":{"bins":["node"],"env":["BRAVE_API_KEY"]},"primaryEnv":"BRAVE_API_KEY"}} +metadata: {"clawdis":{"emoji":"🦁","requires":{"bins":["node"],"env":["BRAVE_API_KEY"]},"primaryEnv":"BRAVE_API_KEY"}} --- # Brave Search diff --git a/skills/camsnap/SKILL.md b/skills/camsnap/SKILL.md index 3175ca695..035fcbb9e 100644 --- a/skills/camsnap/SKILL.md +++ b/skills/camsnap/SKILL.md @@ -1,7 +1,7 @@ --- name: camsnap description: Capture frames or clips from RTSP/ONVIF cameras. -metadata: {"clawdis":{"requires":{"bins":["camsnap"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/camsnap","bins":["camsnap"],"label":"Install camsnap (brew)"}]}} +metadata: {"clawdis":{"emoji":"📸","requires":{"bins":["camsnap"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/camsnap","bins":["camsnap"],"label":"Install camsnap (brew)"}]}} --- # camsnap diff --git a/skills/clawdis-browser/SKILL.md b/skills/clawdis-browser/SKILL.md index 34791bea1..ff0696286 100644 --- a/skills/clawdis-browser/SKILL.md +++ b/skills/clawdis-browser/SKILL.md @@ -1,7 +1,7 @@ --- name: clawdis-browser description: Control clawd's dedicated browser (tabs, snapshots, actions) via the clawdis CLI. -metadata: {"clawdis":{"requires":{"config":["browser.enabled"]}}} +metadata: {"clawdis":{"emoji":"🧭","requires":{"config":["browser.enabled"]}}} --- # Clawdis Browser diff --git a/skills/clawdis-cron/SKILL.md b/skills/clawdis-cron/SKILL.md index df76f9893..f482fc773 100644 --- a/skills/clawdis-cron/SKILL.md +++ b/skills/clawdis-cron/SKILL.md @@ -1,7 +1,7 @@ --- name: clawdis-cron description: Schedule jobs and wakeups via Clawdis Gateway cron.* RPC. -metadata: {"clawdis":{"always":true}} +metadata: {"clawdis":{"emoji":"⏰","always":true}} --- # Clawdis Cron diff --git a/skills/clawdis-nodes/SKILL.md b/skills/clawdis-nodes/SKILL.md index 79e52e49b..e3d056eb3 100644 --- a/skills/clawdis-nodes/SKILL.md +++ b/skills/clawdis-nodes/SKILL.md @@ -1,6 +1,7 @@ --- name: clawdis-nodes description: Discover, interpret, and target Clawdis nodes (paired devices) via the Gateway/CLI. Use when an agent must find available nodes, choose the best target machine, or reason about presence vs node availability (Tailnet/Tailscale optional). +metadata: {"clawdis":{"emoji":"🛰️"}} --- # Clawdis Nodes diff --git a/skills/clawdis-notify/SKILL.md b/skills/clawdis-notify/SKILL.md index b3d9b94a3..2597fc290 100644 --- a/skills/clawdis-notify/SKILL.md +++ b/skills/clawdis-notify/SKILL.md @@ -1,6 +1,7 @@ --- name: clawdis-notify description: Send system notifications to specific Clawdis nodes (macOS computers) via the Gateway and CLI. Use when you need to alert a person or confirm a remote action on a particular machine, or when an agent must push a notification to another computer. +metadata: {"clawdis":{"emoji":"🔔"}} --- # Clawdis Notify diff --git a/skills/eightctl/SKILL.md b/skills/eightctl/SKILL.md index 5bbb65fdf..a63b92327 100644 --- a/skills/eightctl/SKILL.md +++ b/skills/eightctl/SKILL.md @@ -1,7 +1,7 @@ --- name: eightctl description: Control Eight Sleep pods (status, temperature, alarms, schedules). -metadata: {"clawdis":{"requires":{"bins":["eightctl"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/eightctl/cmd/eightctl@latest","bins":["eightctl"],"label":"Install eightctl (go)"}]}} +metadata: {"clawdis":{"emoji":"🎛️","requires":{"bins":["eightctl"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/eightctl/cmd/eightctl@latest","bins":["eightctl"],"label":"Install eightctl (go)"}]}} --- # eightctl diff --git a/skills/gemini/SKILL.md b/skills/gemini/SKILL.md index 28cd581e7..a485402f7 100644 --- a/skills/gemini/SKILL.md +++ b/skills/gemini/SKILL.md @@ -1,7 +1,7 @@ --- name: gemini description: Gemini CLI for one-shot Q&A, summaries, and generation. -metadata: {"clawdis":{"requires":{"bins":["gemini"]},"install":[{"id":"brew","kind":"brew","formula":"gemini-cli","bins":["gemini"],"label":"Install Gemini CLI (brew)"}]}} +metadata: {"clawdis":{"emoji":"♊️","requires":{"bins":["gemini"]},"install":[{"id":"brew","kind":"brew","formula":"gemini-cli","bins":["gemini"],"label":"Install Gemini CLI (brew)"}]}} --- # Gemini CLI diff --git a/skills/gog/SKILL.md b/skills/gog/SKILL.md index c0b91d58f..1b6d70511 100644 --- a/skills/gog/SKILL.md +++ b/skills/gog/SKILL.md @@ -1,7 +1,7 @@ --- name: gog description: Google Workspace CLI for Gmail, Calendar, Drive, and Contacts. -metadata: {"clawdis":{"requires":{"bins":["gog"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/gogcli","bins":["gog"],"label":"Install gog (brew)"}]}} +metadata: {"clawdis":{"emoji":"🎮","requires":{"bins":["gog"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/gogcli","bins":["gog"],"label":"Install gog (brew)"}]}} --- # gog diff --git a/skills/imsg/SKILL.md b/skills/imsg/SKILL.md index 6bc0b7190..d2dc9e422 100644 --- a/skills/imsg/SKILL.md +++ b/skills/imsg/SKILL.md @@ -1,7 +1,7 @@ --- name: imsg description: iMessage/SMS CLI for listing chats, history, watch, and sending. -metadata: {"clawdis":{"requires":{"bins":["imsg"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/imsg/cmd/imsg@latest","bins":["imsg"],"label":"Install imsg (go)"}]}} +metadata: {"clawdis":{"emoji":"📨","requires":{"bins":["imsg"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/imsg/cmd/imsg@latest","bins":["imsg"],"label":"Install imsg (go)"}]}} --- # imsg diff --git a/skills/mcporter/SKILL.md b/skills/mcporter/SKILL.md index 7e5c74086..bbcf8152f 100644 --- a/skills/mcporter/SKILL.md +++ b/skills/mcporter/SKILL.md @@ -1,7 +1,7 @@ --- name: mcporter description: Manage and call MCP servers (list, call, auth, daemon). -metadata: {"clawdis":{"requires":{"bins":["mcporter"]},"install":[{"id":"node","kind":"node","package":"mcporter","bins":["mcporter"],"label":"Install mcporter (node)"}]}} +metadata: {"clawdis":{"emoji":"📦","requires":{"bins":["mcporter"]},"install":[{"id":"node","kind":"node","package":"mcporter","bins":["mcporter"],"label":"Install mcporter (node)"}]}} --- # mcporter diff --git a/skills/nano-banana-pro/SKILL.md b/skills/nano-banana-pro/SKILL.md index d4a47c153..ae460e4da 100644 --- a/skills/nano-banana-pro/SKILL.md +++ b/skills/nano-banana-pro/SKILL.md @@ -1,7 +1,7 @@ --- name: nano-banana-pro description: Generate or edit images via Gemini 3 Pro Image (Nano Banana Pro). -metadata: {"clawdis":{"requires":{"bins":["uv"],"env":["GEMINI_API_KEY"]},"primaryEnv":"GEMINI_API_KEY","install":[{"id":"uv-brew","kind":"brew","formula":"uv","bins":["uv"],"label":"Install uv (brew)"}]}} +metadata: {"clawdis":{"emoji":"🍌","requires":{"bins":["uv"],"env":["GEMINI_API_KEY"]},"primaryEnv":"GEMINI_API_KEY","install":[{"id":"uv-brew","kind":"brew","formula":"uv","bins":["uv"],"label":"Install uv (brew)"}]}} --- # Nano Banana Pro (Gemini 3 Pro Image) diff --git a/skills/nano-pdf/SKILL.md b/skills/nano-pdf/SKILL.md index 2ff85b9f6..3f2deef4e 100644 --- a/skills/nano-pdf/SKILL.md +++ b/skills/nano-pdf/SKILL.md @@ -1,7 +1,7 @@ --- name: nano-pdf description: Edit PDFs with natural-language instructions using the nano-pdf CLI. -metadata: {"clawdis":{"requires":{"bins":["nano-pdf"]},"install":[{"id":"pipx","kind":"shell","command":"python3 -m pip install --user pipx && python3 -m pipx ensurepath && pipx install nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (pipx)"},{"id":"pip","kind":"shell","command":"python3 -m pip install --user nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (pip --user)"}]}} +metadata: {"clawdis":{"emoji":"📄","requires":{"bins":["nano-pdf"]},"install":[{"id":"pipx","kind":"shell","command":"python3 -m pip install --user pipx && python3 -m pipx ensurepath && pipx install nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (pipx)"},{"id":"pip","kind":"shell","command":"python3 -m pip install --user nano-pdf","bins":["nano-pdf"],"label":"Install nano-pdf (pip --user)"}]}} --- # nano-pdf diff --git a/skills/openai-image-gen/SKILL.md b/skills/openai-image-gen/SKILL.md index 0e6a38a7c..eba190087 100644 --- a/skills/openai-image-gen/SKILL.md +++ b/skills/openai-image-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: openai-image-gen description: Batch-generate images via OpenAI Images API. Random prompt sampler + `index.html` gallery. -metadata: {"clawdis":{"requires":{"bins":["python3"],"env":["OPENAI_API_KEY"]},"primaryEnv":"OPENAI_API_KEY","install":[{"id":"python-brew","kind":"brew","formula":"python","bins":["python3"],"label":"Install Python (brew)"}]}} +metadata: {"clawdis":{"emoji":"🖼️","requires":{"bins":["python3"],"env":["OPENAI_API_KEY"]},"primaryEnv":"OPENAI_API_KEY","install":[{"id":"python-brew","kind":"brew","formula":"python","bins":["python3"],"label":"Install Python (brew)"}]}} --- # OpenAI Image Gen diff --git a/skills/openai-whisper-api/SKILL.md b/skills/openai-whisper-api/SKILL.md index 72a7766d0..f1bd05dfb 100644 --- a/skills/openai-whisper-api/SKILL.md +++ b/skills/openai-whisper-api/SKILL.md @@ -1,7 +1,7 @@ --- name: openai-whisper-api description: Transcribe audio via OpenAI Audio Transcriptions API (Whisper). -metadata: {"clawdis":{"requires":{"bins":["curl"],"env":["OPENAI_API_KEY"]},"primaryEnv":"OPENAI_API_KEY"}} +metadata: {"clawdis":{"emoji":"☁️","requires":{"bins":["curl"],"env":["OPENAI_API_KEY"]},"primaryEnv":"OPENAI_API_KEY"}} --- # OpenAI Whisper API (curl) diff --git a/skills/openai-whisper/SKILL.md b/skills/openai-whisper/SKILL.md index 6dc4650a3..a9af9d002 100644 --- a/skills/openai-whisper/SKILL.md +++ b/skills/openai-whisper/SKILL.md @@ -1,7 +1,7 @@ --- name: openai-whisper description: Local speech-to-text with the Whisper CLI (no API key). -metadata: {"clawdis":{"requires":{"bins":["whisper"]},"install":[{"id":"brew","kind":"brew","formula":"openai-whisper","bins":["whisper"],"label":"Install OpenAI Whisper (brew)"}]}} +metadata: {"clawdis":{"emoji":"🎙️","requires":{"bins":["whisper"]},"install":[{"id":"brew","kind":"brew","formula":"openai-whisper","bins":["whisper"],"label":"Install OpenAI Whisper (brew)"}]}} --- # Whisper (CLI) diff --git a/skills/openhue/SKILL.md b/skills/openhue/SKILL.md index 761c8416e..975c12966 100644 --- a/skills/openhue/SKILL.md +++ b/skills/openhue/SKILL.md @@ -1,7 +1,7 @@ --- name: openhue description: Control Philips Hue lights/scenes via the OpenHue CLI. -metadata: {"clawdis":{"requires":{"bins":["openhue"]},"install":[{"id":"brew","kind":"brew","formula":"openhue/cli/openhue-cli","bins":["openhue"],"label":"Install OpenHue CLI (brew)"}]}} +metadata: {"clawdis":{"emoji":"💡","requires":{"bins":["openhue"]},"install":[{"id":"brew","kind":"brew","formula":"openhue/cli/openhue-cli","bins":["openhue"],"label":"Install OpenHue CLI (brew)"}]}} --- # OpenHue CLI diff --git a/skills/oracle/SKILL.md b/skills/oracle/SKILL.md index 1b0cee504..4b57f5f8d 100644 --- a/skills/oracle/SKILL.md +++ b/skills/oracle/SKILL.md @@ -1,7 +1,7 @@ --- name: oracle description: Run a second-model review or debug session with the oracle CLI. -metadata: {"clawdis":{"requires":{"bins":["oracle"]},"install":[{"id":"node","kind":"node","package":"@steipete/oracle","bins":["oracle"],"label":"Install oracle (node)"}]}} +metadata: {"clawdis":{"emoji":"🧿","requires":{"bins":["oracle"]},"install":[{"id":"node","kind":"node","package":"@steipete/oracle","bins":["oracle"],"label":"Install oracle (node)"}]}} --- # oracle diff --git a/skills/peekaboo/SKILL.md b/skills/peekaboo/SKILL.md index ff503e79d..db4cce9c6 100644 --- a/skills/peekaboo/SKILL.md +++ b/skills/peekaboo/SKILL.md @@ -1,7 +1,7 @@ --- name: peekaboo description: Capture and automate macOS UI with the Peekaboo CLI. -metadata: {"clawdis":{"requires":{"bins":["peekaboo"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/peekaboo","bins":["peekaboo"],"label":"Install Peekaboo (brew)"}]}} +metadata: {"clawdis":{"emoji":"👀","requires":{"bins":["peekaboo"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/peekaboo","bins":["peekaboo"],"label":"Install Peekaboo (brew)"}]}} --- # Peekaboo diff --git a/skills/qmd/SKILL.md b/skills/qmd/SKILL.md index 1a0bd667f..9dd41fced 100644 --- a/skills/qmd/SKILL.md +++ b/skills/qmd/SKILL.md @@ -1,7 +1,7 @@ --- name: qmd description: Local search/indexing CLI (BM25 + vectors + rerank) with MCP mode. -metadata: {"clawdis":{"requires":{"bins":["qmd"]},"install":[{"id":"node","kind":"node","package":"https://github.com/tobi/qmd","bins":["qmd"],"label":"Install qmd (node)"}]}} +metadata: {"clawdis":{"emoji":"📝","requires":{"bins":["qmd"]},"install":[{"id":"node","kind":"node","package":"https://github.com/tobi/qmd","bins":["qmd"],"label":"Install qmd (node)"}]}} --- # qmd diff --git a/skills/sag/SKILL.md b/skills/sag/SKILL.md index f1ea42791..9f8f5dbdd 100644 --- a/skills/sag/SKILL.md +++ b/skills/sag/SKILL.md @@ -1,7 +1,7 @@ --- name: sag description: ElevenLabs text-to-speech with mac-style say UX. -metadata: {"clawdis":{"requires":{"bins":["sag"],"env":["ELEVENLABS_API_KEY"]},"primaryEnv":"ELEVENLABS_API_KEY","install":[{"id":"brew","kind":"brew","formula":"steipete/tap/sag","bins":["sag"],"label":"Install sag (brew)"}]}} +metadata: {"clawdis":{"emoji":"🗣️","requires":{"bins":["sag"],"env":["ELEVENLABS_API_KEY"]},"primaryEnv":"ELEVENLABS_API_KEY","install":[{"id":"brew","kind":"brew","formula":"steipete/tap/sag","bins":["sag"],"label":"Install sag (brew)"}]}} --- # sag diff --git a/skills/sonoscli/SKILL.md b/skills/sonoscli/SKILL.md index 4d1b508be..e66f76298 100644 --- a/skills/sonoscli/SKILL.md +++ b/skills/sonoscli/SKILL.md @@ -1,7 +1,7 @@ --- name: sonoscli description: Control Sonos speakers (discover/status/play/volume/group). -metadata: {"clawdis":{"requires":{"bins":["sonos"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/sonoscli/cmd/sonos@latest","bins":["sonos"],"label":"Install sonoscli (go)"}]}} +metadata: {"clawdis":{"emoji":"🔊","requires":{"bins":["sonos"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/sonoscli/cmd/sonos@latest","bins":["sonos"],"label":"Install sonoscli (go)"}]}} --- # Sonos CLI diff --git a/skills/spotify-player/SKILL.md b/skills/spotify-player/SKILL.md index b60611b37..837f77102 100644 --- a/skills/spotify-player/SKILL.md +++ b/skills/spotify-player/SKILL.md @@ -1,7 +1,7 @@ --- name: spotify-player description: Terminal Spotify client (TUI + CLI commands) for playback and search. -metadata: {"clawdis":{"requires":{"bins":["spotify_player"]},"install":[{"id":"brew","kind":"brew","formula":"spotify_player","bins":["spotify_player"],"label":"Install spotify-player (brew)"}]}} +metadata: {"clawdis":{"emoji":"🎵","requires":{"bins":["spotify_player"]},"install":[{"id":"brew","kind":"brew","formula":"spotify_player","bins":["spotify_player"],"label":"Install spotify-player (brew)"}]}} --- # spotify_player diff --git a/skills/summarize/SKILL.md b/skills/summarize/SKILL.md index 790ea7c59..7ceda97cc 100644 --- a/skills/summarize/SKILL.md +++ b/skills/summarize/SKILL.md @@ -1,7 +1,7 @@ --- name: summarize description: Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube). -metadata: {"clawdis":{"requires":{"bins":["summarize"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/summarize","bins":["summarize"],"label":"Install summarize (brew)"},{"id":"node","kind":"node","package":"@steipete/summarize","bins":["summarize"],"label":"Install summarize (node)"}]}} +metadata: {"clawdis":{"emoji":"🧾","requires":{"bins":["summarize"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/summarize","bins":["summarize"],"label":"Install summarize (brew)"},{"id":"node","kind":"node","package":"@steipete/summarize","bins":["summarize"],"label":"Install summarize (node)"}]}} --- # Summarize diff --git a/skills/video-frames/SKILL.md b/skills/video-frames/SKILL.md index 40ca89113..e47af4018 100644 --- a/skills/video-frames/SKILL.md +++ b/skills/video-frames/SKILL.md @@ -1,7 +1,7 @@ --- name: video-frames description: Extract frames or short clips from videos using ffmpeg. -metadata: {"clawdis":{"requires":{"bins":["ffmpeg"]},"install":[{"id":"brew","kind":"brew","formula":"ffmpeg","bins":["ffmpeg"],"label":"Install ffmpeg (brew)"}]}} +metadata: {"clawdis":{"emoji":"🎞️","requires":{"bins":["ffmpeg"]},"install":[{"id":"brew","kind":"brew","formula":"ffmpeg","bins":["ffmpeg"],"label":"Install ffmpeg (brew)"}]}} --- # Video Frames (ffmpeg) diff --git a/skills/wacli/SKILL.md b/skills/wacli/SKILL.md index 5e3bd84f9..6335e9b88 100644 --- a/skills/wacli/SKILL.md +++ b/skills/wacli/SKILL.md @@ -1,7 +1,7 @@ --- name: wacli description: WhatsApp CLI for sync, search, and sending messages. -metadata: {"clawdis":{"requires":{"bins":["wacli"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/wacli/cmd/wacli@latest","bins":["wacli"],"label":"Install wacli (go)"}]}} +metadata: {"clawdis":{"emoji":"📱","requires":{"bins":["wacli"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/wacli/cmd/wacli@latest","bins":["wacli"],"label":"Install wacli (go)"}]}} --- # wacli diff --git a/src/agents/skills-install.ts b/src/agents/skills-install.ts index 804fae219..c44c3bf4d 100644 --- a/src/agents/skills-install.ts +++ b/src/agents/skills-install.ts @@ -2,15 +2,19 @@ import { runCommandWithTimeout } from "../process/exec.js"; import { resolveUserPath } from "../utils.js"; import { loadWorkspaceSkillEntries, + resolveSkillsInstallPreferences, type SkillEntry, type SkillInstallSpec, + type SkillsInstallPreferences, } from "./skills.js"; +import type { ClawdisConfig } from "../config/config.js"; export type SkillInstallRequest = { workspaceDir: string; skillName: string; installId: string; timeoutMs?: number; + config?: ClawdisConfig; }; export type SkillInstallResult = { @@ -40,7 +44,24 @@ function runShell(command: string, timeoutMs: number) { return runCommandWithTimeout(["/bin/zsh", "-lc", command], { timeoutMs }); } -function buildInstallCommand(spec: SkillInstallSpec): { +function buildNodeInstallCommand( + packageName: string, + prefs: SkillsInstallPreferences, +): string[] { + switch (prefs.nodeManager) { + case "pnpm": + return ["pnpm", "add", "-g", packageName]; + case "bun": + return ["bun", "add", "-g", packageName]; + default: + return ["npm", "install", "-g", packageName]; + } +} + +function buildInstallCommand( + spec: SkillInstallSpec, + prefs: SkillsInstallPreferences, +): { argv: string[] | null; shell: string | null; cwd?: string; @@ -55,7 +76,10 @@ function buildInstallCommand(spec: SkillInstallSpec): { case "node": { if (!spec.package) return { argv: null, shell: null, error: "missing node package" }; - return { argv: ["npm", "install", "-g", spec.package], shell: null }; + return { + argv: buildNodeInstallCommand(spec.package, prefs), + shell: null, + }; } case "go": { if (!spec.module) @@ -74,18 +98,6 @@ function buildInstallCommand(spec: SkillInstallSpec): { const cmd = `cd ${JSON.stringify(repoPath)} && pnpm install && pnpm run ${JSON.stringify(spec.script)}`; return { argv: null, shell: cmd }; } - case "git": { - if (!spec.url || !spec.destination) { - return { - argv: null, - shell: null, - error: "missing git url/destination", - }; - } - const dest = resolveUserPath(spec.destination); - const cmd = `if [ -d ${JSON.stringify(dest)} ]; then echo "Already cloned"; else git clone ${JSON.stringify(spec.url)} ${JSON.stringify(dest)}; fi`; - return { argv: null, shell: cmd }; - } case "shell": { if (!spec.command) return { argv: null, shell: null, error: "missing shell command" }; @@ -127,7 +139,8 @@ export async function installSkill( }; } - const command = buildInstallCommand(spec); + const prefs = resolveSkillsInstallPreferences(params.config); + const command = buildInstallCommand(spec, prefs); if (command.error) { return { ok: false, diff --git a/src/agents/skills-status.ts b/src/agents/skills-status.ts index a4b628a03..c78191a8c 100644 --- a/src/agents/skills-status.ts +++ b/src/agents/skills-status.ts @@ -8,8 +8,10 @@ import { loadWorkspaceSkillEntries, resolveConfigPath, resolveSkillConfig, + resolveSkillsInstallPreferences, type SkillEntry, type SkillInstallSpec, + type SkillsInstallPreferences, } from "./skills.js"; export type SkillStatusConfigCheck = { @@ -33,6 +35,7 @@ export type SkillStatusEntry = { baseDir: string; skillKey: string; primaryEnv?: string; + emoji?: string; always: boolean; disabled: boolean; eligible: boolean; @@ -60,45 +63,78 @@ function resolveSkillKey(entry: SkillEntry): string { return entry.clawdis?.skillKey ?? entry.skill.name; } -function normalizeInstallOptions(entry: SkillEntry): SkillInstallOption[] { +function selectPreferredInstallSpec( + install: SkillInstallSpec[], + prefs: SkillsInstallPreferences, +): { spec: SkillInstallSpec; index: number } | undefined { + if (install.length === 0) return undefined; + const indexed = install.map((spec, index) => ({ spec, index })); + const findKind = (kind: SkillInstallSpec["kind"]) => + indexed.find((item) => item.spec.kind === kind); + + const brewSpec = findKind("brew"); + const nodeSpec = findKind("node"); + const goSpec = findKind("go"); + const pnpmSpec = findKind("pnpm"); + const shellSpec = findKind("shell"); + + if (prefs.preferBrew && hasBinary("brew") && brewSpec) return brewSpec; + if (nodeSpec) return nodeSpec; + if (brewSpec) return brewSpec; + if (goSpec) return goSpec; + if (pnpmSpec) return pnpmSpec; + if (shellSpec) return shellSpec; + return indexed[0]; +} + +function normalizeInstallOptions( + entry: SkillEntry, + prefs: SkillsInstallPreferences, +): SkillInstallOption[] { const install = entry.clawdis?.install ?? []; if (install.length === 0) return []; - return install.map((spec, index) => { - const id = (spec.id ?? `${spec.kind}-${index}`).trim(); - const bins = spec.bins ?? []; - let label = (spec.label ?? "").trim(); - if (!label) { - if (spec.kind === "brew" && spec.formula) { - label = `Install ${spec.formula} (brew)`; - } else if (spec.kind === "node" && spec.package) { - label = `Install ${spec.package} (node)`; - } else if (spec.kind === "go" && spec.module) { - label = `Install ${spec.module} (go)`; - } else if (spec.kind === "pnpm" && spec.repoPath) { - label = `Install ${spec.repoPath} (pnpm)`; - } else if (spec.kind === "git" && spec.url) { - label = `Clone ${spec.url}`; - } else { - label = "Run installer"; - } + const preferred = selectPreferredInstallSpec(install, prefs); + if (!preferred) return []; + const { spec, index } = preferred; + const id = (spec.id ?? `${spec.kind}-${index}`).trim(); + const bins = spec.bins ?? []; + let label = (spec.label ?? "").trim(); + if (spec.kind === "node" && spec.package) { + label = `Install ${spec.package} (${prefs.nodeManager})`; + } + if (!label) { + if (spec.kind === "brew" && spec.formula) { + label = `Install ${spec.formula} (brew)`; + } else if (spec.kind === "node" && spec.package) { + label = `Install ${spec.package} (${prefs.nodeManager})`; + } else if (spec.kind === "go" && spec.module) { + label = `Install ${spec.module} (go)`; + } else if (spec.kind === "pnpm" && spec.repoPath) { + label = `Install ${spec.repoPath} (pnpm)`; + } else { + label = "Run installer"; } - return { + } + return [ + { id, kind: spec.kind, label, bins, - }; - }); + }, + ]; } function buildSkillStatus( entry: SkillEntry, config?: ClawdisConfig, + prefs?: SkillsInstallPreferences, ): SkillStatusEntry { const skillKey = resolveSkillKey(entry); const skillConfig = resolveSkillConfig(config, skillKey); const disabled = skillConfig?.enabled === false; const always = entry.clawdis?.always === true; + const emoji = entry.clawdis?.emoji ?? entry.frontmatter.emoji; const requiredBins = entry.clawdis?.requires?.bins ?? []; const requiredEnv = entry.clawdis?.requires?.env ?? []; @@ -145,6 +181,7 @@ function buildSkillStatus( baseDir: entry.skill.baseDir, skillKey, primaryEnv: entry.clawdis?.primaryEnv, + emoji, always, disabled, eligible, @@ -155,7 +192,10 @@ function buildSkillStatus( }, missing, configChecks, - install: normalizeInstallOptions(entry), + install: normalizeInstallOptions( + entry, + prefs ?? resolveSkillsInstallPreferences(config), + ), }; } @@ -171,9 +211,12 @@ export function buildWorkspaceSkillStatus( opts?.managedSkillsDir ?? path.join(CONFIG_DIR, "skills"); const skillEntries = opts?.entries ?? loadWorkspaceSkillEntries(workspaceDir, opts); + const prefs = resolveSkillsInstallPreferences(opts?.config); return { workspaceDir, managedSkillsDir, - skills: skillEntries.map((entry) => buildSkillStatus(entry, opts?.config)), + skills: skillEntries.map((entry) => + buildSkillStatus(entry, opts?.config, prefs), + ), }; } diff --git a/src/agents/skills.ts b/src/agents/skills.ts index 98a910575..5c6e0b2d9 100644 --- a/src/agents/skills.ts +++ b/src/agents/skills.ts @@ -13,7 +13,7 @@ import { CONFIG_DIR, resolveUserPath } from "../utils.js"; export type SkillInstallSpec = { id?: string; - kind: "brew" | "node" | "go" | "pnpm" | "git" | "shell"; + kind: "brew" | "node" | "go" | "pnpm" | "shell"; label?: string; bins?: string[]; formula?: string; @@ -21,8 +21,6 @@ export type SkillInstallSpec = { module?: string; repoPath?: string; script?: string; - url?: string; - destination?: string; command?: string; }; @@ -30,6 +28,7 @@ export type ClawdisSkillMetadata = { always?: boolean; skillKey?: string; primaryEnv?: string; + emoji?: string; requires?: { bins?: string[]; env?: string[]; @@ -38,6 +37,11 @@ export type ClawdisSkillMetadata = { install?: SkillInstallSpec[]; }; +export type SkillsInstallPreferences = { + preferBrew: boolean; + nodeManager: "npm" | "pnpm" | "bun"; +}; + type ParsedSkillFrontmatter = Record; export type SkillEntry = { @@ -141,7 +145,6 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined { kind !== "node" && kind !== "go" && kind !== "pnpm" && - kind !== "git" && kind !== "shell" ) { return undefined; @@ -160,8 +163,6 @@ function parseInstallSpec(input: unknown): SkillInstallSpec | undefined { if (typeof raw.module === "string") spec.module = raw.module; if (typeof raw.repoPath === "string") spec.repoPath = raw.repoPath; if (typeof raw.script === "string") spec.script = raw.script; - if (typeof raw.url === "string") spec.url = raw.url; - if (typeof raw.destination === "string") spec.destination = raw.destination; if (typeof raw.command === "string") spec.command = raw.command; return spec; @@ -179,6 +180,21 @@ const DEFAULT_CONFIG_VALUES: Record = { "browser.enabled": true, }; +export function resolveSkillsInstallPreferences( + config?: ClawdisConfig, +): SkillsInstallPreferences { + const raw = config?.skillsInstall; + const preferBrew = raw?.preferBrew ?? true; + const managerRaw = + typeof raw?.nodeManager === "string" ? raw.nodeManager.trim() : ""; + const manager = managerRaw.toLowerCase(); + const nodeManager = + manager === "pnpm" || manager === "bun" || manager === "npm" + ? (manager as SkillsInstallPreferences["nodeManager"]) + : "npm"; + return { preferBrew, nodeManager }; +} + export function resolveConfigPath( config: ClawdisConfig | undefined, pathStr: string, @@ -253,6 +269,8 @@ function resolveClawdisMetadata( return { always: typeof clawdisObj.always === "boolean" ? clawdisObj.always : undefined, + emoji: + typeof clawdisObj.emoji === "string" ? clawdisObj.emoji : undefined, skillKey: typeof clawdisObj.skillKey === "string" ? clawdisObj.skillKey