From 3408bd9fe9cecda7c66015fe7010edf52c6e5298 Mon Sep 17 00:00:00 2001 From: thibauult Date: Sat, 7 Nov 2026 09:38:35 +0100 Subject: [PATCH 1/5] Add root SKILL.md with a user guide for the CLI Covers installation, bridge pairing, controlling lights and rooms, scenes, switch controls, live events and the MCP server. AGENTS.md now asks to keep both skills in sync with command changes. --- AGENTS.md | 6 +++ SKILL.md | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 SKILL.md diff --git a/AGENTS.md b/AGENTS.md index 9e9d48f..f961833 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,6 +56,12 @@ Commands in the `hue` group trigger `LoadHomeIfNeeded` in PersistentPreRun, whic Config stored in `~/.openhue/config.yaml` (or `$XDG_CONFIG_HOME/openhue/`). Contains bridge IP and application key. Commands listed in `CommandsWithNoConfig` can run without setup. +### CLI Reference Skill + +`.claude/skills/openhue-cli/SKILL.md` documents every command, alias and flag of the CLI. **Whenever you add, rename, remove or change a command, subcommand, alias, flag, default or MCP tool, update that skill in the same change.** It covers the command map, the flag tables, the examples and the MCP tool list. Check it against `go run . --help`; if the two disagree, `--help` is right. + +The root `SKILL.md` is the user-facing guide (install, setup, everyday usage). Update it too when a change affects how users run the CLI. + ### Testing Pattern - Use `openhue.NewTestContext(home)` to create test contexts with mock HomeModel diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..999a6bc --- /dev/null +++ b/SKILL.md @@ -0,0 +1,150 @@ +--- +name: openhue +description: Control Philips Hue lights from the terminal with the `openhue` CLI — install it, pair it with a Hue Bridge, list and switch lights and rooms, set brightness and colors, activate, create, edit and delete scenes, configure switch buttons, and run the MCP server for LLM clients. Use when the user wants to control Hue lights, rooms or scenes, or asks how to run an `openhue ...` command. +--- + +# Using the openhue CLI + +`openhue` controls a Philips Hue Bridge on the local network. Full docs: https://www.openhue.io/cli + +When unsure about a flag, run `openhue --help`. It is always right. + +## 1. Install + +```bash +brew tap openhue/cli +brew install openhue-cli +openhue version +``` + +Docker alternative (mount the config folder): + +```bash +docker run -v "${HOME}/.openhue:/.openhue" --rm -it openhue/cli get lights +``` + +## 2. Pair with the bridge (once) + +```bash +openhue setup +``` + +`setup` finds the bridge via mDNS, then asks you to press the bridge's link button. Other options: + +| Command | Use when | +|---|---| +| `openhue discover` | Only print the bridges found on the network | +| `openhue setup -b 192.168.1.68` | mDNS discovery fails; give the bridge IP | +| `openhue config -b -k ` | You already have an application key | + +Config is saved to `~/.openhue/config.yaml` (or `$XDG_CONFIG_HOME/openhue/`). Until it exists, Hue commands print "not configured yet, please run the 'setup' command". + +## 3. Find what you have + +```bash +openhue get lights +openhue get rooms +openhue get scenes +openhue get light -r "Living Room" # filter by room +openhue get lights --json # machine-readable +``` + +- Aliases: `get` = `g`, `set` = `s`, `create` = `c`. Singular and plural both work (`light`/`lights`). +- Resources are accepted by **name or ID**. Quote names with spaces. +- Names repeat across rooms: add `-r/--room ` to pick the right one. + +## 4. Control lights and rooms + +**`--on` is required to light up**, even when you only set brightness or color. + +```bash +openhue set light Desk --on +openhue set room Studio --off +openhue set light "Hue Play TV" Desk --on --brightness 60 --rgb "#3399FF" +openhue set light Desk --on -c powder_blue +openhue set room Bedroom --on -t 400 --transition-time 30s +``` + +| Flag | Meaning | +|---|---| +| `--on` / `--off` | Power | +| `-b, --brightness` | 0–100 | +| `--rgb` | Hex color, `#RRGGBB` | +| `-c, --color` | Named color (`white`, `lime`, `powder_blue`, ...) | +| `-x` / `-y` | CIE xy color | +| `-t, --temperature` | White temperature in Mirek, 153 (cool) – 500 (warm) | +| `--transition-time` | Fade duration, e.g. `5s` | + +Up to 10 lights or rooms per command. + +## 5. Scenes + +Activate: + +```bash +openhue set scene Relax -r "Living Room" +openhue set scene Aurora -a dynamic # play the dynamic palette +``` + +Create (writes the scene only; lights do not change). It starts from the room's current light state: + +```bash +openhue create scene Work -r Office \ + --light "Desk:on,brightness=100,temperature=250" \ + --light "Ceiling:off" +``` + +Edit or copy through YAML: + +```bash +openhue get scene Cozy -r "Living Room" --yaml > cozy.yaml +openhue update scene Cozy -r "Living Room" --from-file cozy.yaml +openhue update scene Cozy --light "Shelf:on,brightness=30" --remove-light Ceiling +openhue update scene Cozy --name "Cozy Evening" +``` + +Delete (no prompt, cannot be undone — export with `--yaml` first if you want a copy): + +```bash +openhue delete scene Cozy -r "Living Room" +``` + +Per-light settings for `--light ":"`, comma-separated: `on`, `off`, `brightness=`, `rgb=`, `color=`, `temperature=`, `x=`, `y=`, `transition=`, `effect=`, `gradient=;`, `gradient-mode=`. + +Add `--dry-run` to `create`/`update scene` to preview without saving. + +## 6. Switches and dimmers + +Accessories must already be added to the bridge (e.g. with the Hue app). + +```bash +openhue get control # show button assignments +openhue set control "Hue dimmer switch" --control 1 --scene Cozy --with-off +openhue set control "Hue dimmer switch" --control 4 --scene Soho --scene Nebula --dry-run +``` + +Repeat `--scene` to cycle scenes on each press. Other flags: `--on-hold dim-up|dim-down|dim-alternate|none`, `--repeat-timeout`, `--clear`. Rotary dials are read-only. + +## 7. Watch live events + +```bash +openhue get events # streams bridge events as JSON; Ctrl+C to stop +``` + +## 8. Use from an LLM (MCP server) + +```json +{ "mcpServers": { "openhue": { "command": "openhue", "args": ["mcp"] } } } +``` + +Tools exposed: `list_lights`, `list_rooms`, `list_scenes`, `set_light`, `set_room`, `activate_scene`. + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| "not configured yet" | Run `openhue setup` | +| Bridge not discovered | `openhue setup -b ` | +| Light ignores brightness/color | Add `--on` | +| Wrong light or scene picked | Add `-r ` or use the ID from `get ... --json` | +| Need detail | Check `openhue.log` next to the config file | From 2abdbf2bc0a5e1669a36c3c8aecb3e913431bf7f Mon Sep 17 00:00:00 2001 From: thibauult Date: Sat, 7 Nov 2026 09:44:58 +0100 Subject: [PATCH 2/5] Track the CLI reference skill The 'openhue-cli' ignore rule meant for the go build output also matched .claude/skills/openhue-cli/, so the skill AGENTS.md points to was never committed. Anchor the rule to the repo root. --- .claude/skills/openhue-cli/SKILL.md | 145 ++++++++++++++++++++++++++++ .gitignore | 2 +- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/openhue-cli/SKILL.md diff --git a/.claude/skills/openhue-cli/SKILL.md b/.claude/skills/openhue-cli/SKILL.md new file mode 100644 index 0000000..a1545fb --- /dev/null +++ b/.claude/skills/openhue-cli/SKILL.md @@ -0,0 +1,145 @@ +--- +name: openhue-cli +description: Reference for the `openhue` CLI (this repo) — every command, subcommand, flag, alias and behavior for controlling Philips Hue lights, rooms, scenes and switch controls, plus setup and the MCP server. Use whenever writing, running or explaining an `openhue ...` invocation, adding or changing a CLI command, or answering "how do I do X with openhue" / "what flags does openhue set light take". Trigger: /openhue-cli, "openhue CLI", or any `openhue ` you are unsure of. +--- + +# openhue CLI + +Cobra CLI that controls a Philips Hue Bridge through [openhue-go](https://github.com/openhue/openhue-go). +Source of truth is `cmd/` — if this file and `openhue --help` disagree, trust `--help` and fix this file. + +Run from source: `go run . ` (or `make build` → `./dist`). + +## Command map + +| Group | Command | Aliases | What it does | +|---|---|---|---| +| Configuration | `setup` | | Discover the bridge (mDNS), pair by pressing the bridge button, save config | +| | `config -b -k ` | | Save bridge IP + application key manually | +| | `discover` | | Print bridges found via mDNS | +| | `completion` | | Shell completion scripts | +| | `install-skill` | | Write the root `SKILL.md` (embedded in the binary) to `~/.claude/skills/openhue/` (`$CLAUDE_CONFIG_DIR/skills` if set). `--project` → `./.claude/skills`, `--dir `, `--print` → stdout. Overwrites; reports installed / updated / already up to date | +| | `version` | | Build info | +| Philips Hue | `get light\|room\|scene\|control\|events` | `get`=`g` | Read resources | +| | `set light\|room\|scene\|control` | `set`=`s` | Change state / activate / configure | +| | `create scene` | `create`=`c` | Create a scene | +| | `update scene` | | Edit a scene | +| | `delete scene` | | Delete one scene (irreversible) | +| MCP Server | `mcp` | | stdio MCP server for LLM clients | + +Every resource subcommand accepts singular or plural (`light`/`lights`, `scene`/`scenes`, ...). + +## Config & lifecycle + +- Config file: `~/.openhue/config.yaml`, or `$XDG_CONFIG_HOME/openhue/config.yaml`. Keys: `Bridge`, `Key`, optional `log_level`. +- Log file: `openhue.log` next to the config. +- Without config, any command not in `CommandsWithNoConfig` (`setup`, `config`, `help`, `discover`, `auth`, `version`, `completion`, `install-skill`) prints "not configured yet, please run the 'setup' command" and exits 0. +- Commands in the `hue` group load the full `HomeModel` from the bridge in `PersistentPreRun` before running (one round-trip of all resources). Config/MCP commands skip it. + +## Resolving resources + +- Lights, rooms, scenes, controls, devices are accepted by **name or ID** everywhere. +- Names are not unique across rooms: use `-r/--room ` to disambiguate (`set light`, `set scene`, `get scene`, `update scene`, `delete scene`). +- Quote names with spaces: `"Living Room"`. + +## get + +`-j/--json` is a persistent flag on `get` (works on every subcommand). Bare `openhue get` lists all resources and also takes `-t/--type ` (not inherited by subcommands). +Default output is a table. + +| Command | Args | Flags | +|---|---|---| +| `get light [id\|name...]` | optional | `-r/--room` filter by room | +| `get room [id\|name...]` | optional | — | +| `get scene [id\|name...]` | optional, multiple | `-r/--room`, `--yaml` (export a single scene as a YAML definition reusable by `create`/`update scene --from-file`) | +| `get control [id...]` | optional | `-r/--room`, `-d/--device `, `--kind button\|rotary` | +| `get events` | — | Streams bridge SSE events as JSON until Ctrl+C (alpha) | + +`get control` shows per button the "Short press" / "Hold" assignment: `scene `, `cycle: a, b, ... (n)`, `time-based`, `smart-scene `, `dim-up`, or `-`. `--json` adds the raw `configuration`. + +## set light / set room + +Up to 10 targets at once. **`--on` is required to light up**, even when setting brightness/color. + +| Flag | Meaning | +|---|---| +| `--on` / `--off` | Power | +| `-b, --brightness <0-100>` | float | +| `--rgb #RRGGBB` | hex color | +| `-x, --cie-x` / `-y, --cie-y` | CIE xy color | +| `-c, --color ` | named color (`white`, `lime`, `powder_blue`, ...) | +| `-t, --temperature <153-500>` | Mirek | +| `--transition-time ` | e.g. `5s`, default `0s` | +| `-r, --room` | (`set light` only) force the room of the lights | + +```bash +openhue set light "Hue Play TV" Desk --on --brightness 60 --rgb "#3399FF" +openhue set room Studio --off +openhue set light office1 --on -t 250 --transition-time 30s +``` + +## set scene + +`openhue set scene [-r room] [-a active|dynamic|static]` — activates a scene; `-a dynamic` plays its dynamic palette. + +## set control + +Configures a button (rotary dials are read-only). Select it by control ID, or by device name/ID + `--control ` (button number on the device). The accessory must already be set up on the bridge (e.g. via the Hue app). Anything the flags do not cover is left untouched. + +| Flag | Meaning | +|---|---| +| `--scene ` | Short press recalls the scene (from the button's room). Repeat to cycle through scenes in order | +| `--with-off` | Second press turns lights off | +| `--repeat-timeout ` | Idle time before a scene cycle restarts (default 3s) | +| `--on-hold dim-up\|dim-down\|dim-alternate\|none` | Hold behavior | +| `--repeat-interval ` | Repeat interval while held | +| `--clear` | Remove the short-press action | +| `--dry-run` | Print the configuration that would be sent; send nothing | + +```bash +openhue set control "Hue dimmer switch" --control 1 --scene Cozy --with-off +openhue set control "Hue dimmer switch" --control 4 --scene Soho --scene Nebula --repeat-timeout 10s --dry-run +``` + +## create scene / update scene + +Neither command changes the real lights — they only write the scene. + +**create**: `openhue create scene -r [flags]`. Layering: current state of every light in the room → global flags → per-light `--light` → (and global flags override `--from-file`). + +**update**: `openhue update scene [-r room] [flags]`. Applies on top of the values **stored in the scene**; unmentioned lights keep their settings (effects, gradients). `--light` for a room light not yet in the scene adds it. `--from-file` is a patch. + +Shared flags: `--on`, `--off`, `-b`, `--rgb`, `-x`, `-y`, `-c`, `-t`, `--transition-time`, `--light ":"` (repeatable), `--from-file `, `--auto-dynamic`, `--speed <0-100>`, `--palette-color`, `--palette-brightness`, `--palette-temperature`, `--palette-effect` (all repeatable), `-j/--json`, `--dry-run`. +Create only: `--only-listed` (lights not given with `--light` are turned off in the scene). +Update only: `--name `, `--remove-light ` (repeatable), `--clear-palette`. + +Per-light settings (comma-separated): `on`, `off`, `brightness=<0-100>`, `rgb=<#hex>`, `x=`, `y=`, `color=`, `temperature=<153-500>`, `transition=`, `effect=`, `gradient=;;...`, `gradient-mode=`. + +```bash +openhue create scene Work -r Office --light "Desk:on,brightness=100,temperature=250" --light "Ceiling:off" +openhue create scene Aurora -r Lounge --auto-dynamic --speed 30 --palette-color blue --palette-color "#00FF88" +openhue get scene Cozy -r "Living Room" --yaml > cozy.yaml # edit, then: +openhue update scene Cozy -r "Living Room" --from-file cozy.yaml +openhue update scene Cozy --light "Shelf:on,brightness=30" --remove-light Ceiling +``` + +## delete scene + +`openhue delete scene [-r room]` — exactly one scene, no confirmation prompt, cannot be undone. Export first with `get scene --yaml` if a copy is wanted. + +## mcp + +`openhue mcp` runs a stdio MCP server. It builds its own openhue-go client from the config (does not use the preloaded `HomeModel`). Tools (defined in `cmd/mcp/tools.go`): `list_lights`, `list_rooms`, `list_scenes`, `set_light`, `set_room`, `activate_scene`. + +```json +{ "mcpServers": { "openhue": { "command": "openhue", "args": ["mcp"] } } } +``` + +## Adding or changing a command + +1. Create `cmd//_.go` with a `NewCmd...(ctx *openhue.Context)` constructor; follow an existing sibling (e.g. `cmd/get/get_control.go`). +2. Register it on its parent (`cmd//.go`); new top-level verbs go in `addCommands` in `cmd/root.go` with `GroupID` `hue` (loads the home) or `config`. +3. Resolve resources via the `Search*` helpers in `openhue/home_service.go` (name or ID). +4. Shared scene flags live in `cmd/scenecmd/flags.go`; shared light/room setters in `cmd/set/common.go`. +5. Test with `openhue.NewTestContext(home)` and `openhue/test/assert`; run `go test ./cmd/`. +6. Update this skill (command map + flag table) and the `Example:` block in the command's help text. diff --git a/.gitignore b/.gitignore index e7fe27e..5034149 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,4 @@ bin/ dist/ # go build output -openhue-cli +/openhue-cli From 5d92be9875d84beff8c092899cd7825790170b48 Mon Sep 17 00:00:00 2001 From: thibauult Date: Sat, 7 Nov 2026 09:44:58 +0100 Subject: [PATCH 3/5] Add install-skill command Embeds the root SKILL.md in the binary and writes it to ~/.claude/skills/openhue/SKILL.md (or $CLAUDE_CONFIG_DIR/skills). --project installs into ./.claude/skills, --dir into a custom skills directory, --print writes it to stdout. Re-running after an upgrade refreshes the skill. Runs without a bridge configuration. --- AGENTS.md | 2 +- README.md | 4 ++ SKILL.md | 7 ++ cmd/root.go | 4 +- cmd/skill/install.go | 146 ++++++++++++++++++++++++++++++++++++++ cmd/skill/install_test.go | 85 ++++++++++++++++++++++ main.go | 8 ++- openhue/config.go | 2 +- 8 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 cmd/skill/install.go create mode 100644 cmd/skill/install_test.go diff --git a/AGENTS.md b/AGENTS.md index f961833..74f75ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ Config stored in `~/.openhue/config.yaml` (or `$XDG_CONFIG_HOME/openhue/`). Cont `.claude/skills/openhue-cli/SKILL.md` documents every command, alias and flag of the CLI. **Whenever you add, rename, remove or change a command, subcommand, alias, flag, default or MCP tool, update that skill in the same change.** It covers the command map, the flag tables, the examples and the MCP tool list. Check it against `go run . --help`; if the two disagree, `--help` is right. -The root `SKILL.md` is the user-facing guide (install, setup, everyday usage). Update it too when a change affects how users run the CLI. +The root `SKILL.md` is the user-facing guide (install, setup, everyday usage). Update it too when a change affects how users run the CLI. It is embedded in the binary (`//go:embed` in `main.go`) and written to disk by `openhue install-skill` (`cmd/skill/`). ### Testing Pattern diff --git a/README.md b/README.md index 48070b3..f077229 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,10 @@ Check that it works: ```shell openhue help ``` +To teach Claude Code how to use the CLI, install the bundled skill: +```shell +openhue install-skill +``` To upgrade when a new version is released: ```shell brew update diff --git a/SKILL.md b/SKILL.md index 999a6bc..6cdeed0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -17,6 +17,13 @@ brew install openhue-cli openhue version ``` +Install this skill for Claude Code (re-run after each upgrade to refresh it): + +```bash +openhue install-skill # ~/.claude/skills/openhue/SKILL.md +openhue install-skill --project # ./.claude/skills/openhue/SKILL.md +``` + Docker alternative (mount the config folder): ```bash diff --git a/cmd/root.go b/cmd/root.go index e318624..3ee0635 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "openhue-cli/cmd/mcp" "openhue-cli/cmd/set" "openhue-cli/cmd/setup" + "openhue-cli/cmd/skill" "openhue-cli/cmd/update" "openhue-cli/cmd/version" "openhue-cli/openhue" @@ -76,7 +77,7 @@ func LoadHomeIfNeeded(ctx *openhue.Context, cmd *cobra.Command) { // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute(buildInfo *openhue.BuildInfo) { +func Execute(buildInfo *openhue.BuildInfo, skillContent []byte) { // load the configuration c := openhue.Config{} @@ -90,6 +91,7 @@ func Execute(buildInfo *openhue.BuildInfo) { // add sub commands addCommands(root, ctx) + root.AddCommand(skill.NewCmdInstallSkill(ctx.Io, skillContent)) // execute the root command err := root.Execute() diff --git a/cmd/skill/install.go b/cmd/skill/install.go new file mode 100644 index 0000000..2b6fec1 --- /dev/null +++ b/cmd/skill/install.go @@ -0,0 +1,146 @@ +package skill + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "openhue-cli/openhue" +) + +const ( + // Name is the folder the skill is installed into, matching the `name` in SKILL.md + Name = "openhue" + FileName = "SKILL.md" + + docShortInstall = `Install the openhue skill for Claude Code` + docLongInstall = ` +Install the openhue skill so that Claude Code (and other agents supporting skills) knows how to use the openhue CLI. + +The skill is bundled in this binary, so it always matches the installed version of the CLI. +Run the command again after upgrading openhue to refresh it. + +By default, the skill is installed for the current user in ~/.claude/skills/openhue/SKILL.md +($CLAUDE_CONFIG_DIR/skills when that variable is set).` + docExampleInstall = ` +# Install the skill for the current user +openhue install-skill + +# Install the skill in the current project only (./.claude/skills) +openhue install-skill --project + +# Install the skill in a custom skills directory +openhue install-skill --dir ~/my-agent/skills + +# Print the skill instead of installing it +openhue install-skill --print` +) + +type Options struct { + dir string + project bool + print bool +} + +// NewCmdInstallSkill creates the install-skill command that writes the given skill content on disk +func NewCmdInstallSkill(io openhue.IOStreams, content []byte) *cobra.Command { + + o := Options{} + + cmd := &cobra.Command{ + Use: "install-skill", + GroupID: "config", + Short: docShortInstall, + Long: docLongInstall, + Example: docExampleInstall, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + + if o.print { + _, err := io.Out.Write(content) + return err + } + + dir, err := o.skillsDir() + if err != nil { + return err + } + + path, status, err := Install(content, dir) + if err != nil { + return err + } + + io.Printf("[OK] Skill %s in %s\n", status, path) + return nil + }, + } + + cmd.Flags().StringVar(&o.dir, "dir", "", "Skills directory to install into (the skill goes in /openhue/SKILL.md)") + cmd.Flags().BoolVar(&o.project, "project", false, "Install in the current project (./.claude/skills) instead of for the current user") + cmd.Flags().BoolVar(&o.print, "print", false, "Print the skill to stdout instead of installing it") + cmd.MarkFlagsMutuallyExclusive("dir", "project", "print") + + return cmd +} + +// skillsDir resolves the skills directory from the options +func (o *Options) skillsDir() (string, error) { + switch { + case o.dir != "": + return o.dir, nil + case o.project: + return filepath.Join(".claude", "skills"), nil + default: + return UserSkillsDir() + } +} + +// UserSkillsDir returns the Claude Code user skills directory +func UserSkillsDir() (string, error) { + if configDir := os.Getenv("CLAUDE_CONFIG_DIR"); configDir != "" { + return filepath.Join(configDir, "skills"), nil + } + + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("unable to find the user home directory: %w", err) + } + + return filepath.Join(home, ".claude", "skills"), nil +} + +// Install writes the skill content in /openhue/SKILL.md. +// It returns the file path and whether the skill was "installed", "updated" or is "already up to date". +func Install(content []byte, skillsDir string) (string, string, error) { + + if len(content) == 0 { + return "", "", errors.New("this openhue binary does not contain the skill") + } + + path := filepath.Join(skillsDir, Name, FileName) + + status := "installed" + existing, err := os.ReadFile(path) + switch { + case err == nil && bytes.Equal(existing, content): + return path, "already up to date", nil + case err == nil: + status = "updated" + case !errors.Is(err, os.ErrNotExist): + return "", "", fmt.Errorf("unable to read %s: %w", path, err) + } + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return "", "", fmt.Errorf("unable to create %s: %w", filepath.Dir(path), err) + } + + if err := os.WriteFile(path, content, 0o644); err != nil { + return "", "", fmt.Errorf("unable to write %s: %w", path, err) + } + + return path, status, nil +} diff --git a/cmd/skill/install_test.go b/cmd/skill/install_test.go new file mode 100644 index 0000000..5f30845 --- /dev/null +++ b/cmd/skill/install_test.go @@ -0,0 +1,85 @@ +package skill + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "openhue-cli/openhue" + "openhue-cli/openhue/test/assert" +) + +var content = []byte("---\nname: openhue\n---\n# openhue\n") + +func TestNewCmdInstallSkill(t *testing.T) { + cmd := NewCmdInstallSkill(openhue.NewTestIOStreamsDiscard(), content) + + assert.ThatCmdUseIs(t, cmd, "install-skill") + assert.ThatCmdGroupIs(t, cmd, "config") +} + +func TestInstall(t *testing.T) { + dir := t.TempDir() + expected := filepath.Join(dir, "openhue", "SKILL.md") + + path, status, err := Install(content, dir) + require.NoError(t, err) + require.Equal(t, expected, path) + require.Equal(t, "installed", status) + + written, err := os.ReadFile(expected) + require.NoError(t, err) + require.Equal(t, content, written) + + _, status, err = Install(content, dir) + require.NoError(t, err) + require.Equal(t, "already up to date", status) + + newer := append(content, []byte("more\n")...) + _, status, err = Install(newer, dir) + require.NoError(t, err) + require.Equal(t, "updated", status) + + written, err = os.ReadFile(expected) + require.NoError(t, err) + require.Equal(t, newer, written) +} + +func TestInstall_EmptyContent(t *testing.T) { + _, _, err := Install(nil, t.TempDir()) + require.Error(t, err) +} + +func TestCmdInstallSkill_Dir(t *testing.T) { + dir := t.TempDir() + streams, _, out, _ := openhue.NewTestIOStreams() + + cmd := NewCmdInstallSkill(streams, content) + cmd.SetArgs([]string{"--dir", dir}) + require.NoError(t, cmd.Execute()) + + require.FileExists(t, filepath.Join(dir, "openhue", "SKILL.md")) + require.Contains(t, out.String(), "[OK] Skill installed in") +} + +func TestCmdInstallSkill_Print(t *testing.T) { + streams, _, out, _ := openhue.NewTestIOStreams() + + cmd := NewCmdInstallSkill(streams, content) + cmd.SetArgs([]string{"--print"}) + require.NoError(t, cmd.Execute()) + + require.Equal(t, string(content), out.String()) +} + +func TestCmdInstallSkill_UserDir(t *testing.T) { + dir := t.TempDir() + t.Setenv("CLAUDE_CONFIG_DIR", dir) + + cmd := NewCmdInstallSkill(openhue.NewTestIOStreamsDiscard(), content) + cmd.SetArgs([]string{}) + require.NoError(t, cmd.Execute()) + + require.FileExists(t, filepath.Join(dir, "skills", "openhue", "SKILL.md")) +} diff --git a/main.go b/main.go index 26d1482..4ff3a60 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main //go:generate make generate import ( + _ "embed" "openhue-cli/cmd" "openhue-cli/openhue" ) @@ -13,6 +14,11 @@ var ( date = "unknown" ) +// skill is the root SKILL.md, bundled so that `openhue install-skill` can write it on disk +// +//go:embed SKILL.md +var skill []byte + func main() { - cmd.Execute(openhue.NewBuildInfo(version, commit, date)) + cmd.Execute(openhue.NewBuildInfo(version, commit, date), skill) } diff --git a/openhue/config.go b/openhue/config.go index 17b1b01..2dd2366 100644 --- a/openhue/config.go +++ b/openhue/config.go @@ -16,7 +16,7 @@ import ( ) // CommandsWithNoConfig contains the list of commands that don't require the configuration to exist -var CommandsWithNoConfig = []string{"setup", "config", "help", "discover", "auth", "version", "completion"} +var CommandsWithNoConfig = []string{"setup", "config", "help", "discover", "auth", "version", "completion", "install-skill"} var configPath string func init() { From 4eaa654a930364e46788bd1dacec0693e13eeb80 Mon Sep 17 00:00:00 2001 From: thibauult Date: Sat, 7 Nov 2026 09:46:05 +0100 Subject: [PATCH 4/5] Document Docker usage and scene copy in SKILL.md Show the config mount for setup and every command (with an alias), the XDG mount, passing -b or using host networking when mDNS can't reach the bridge, and installing the skill from the container. Add a create --from-file example to copy a scene. --- SKILL.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/SKILL.md b/SKILL.md index 6cdeed0..91ad798 100644 --- a/SKILL.md +++ b/SKILL.md @@ -24,12 +24,20 @@ openhue install-skill # ~/.claude/skills/openhue/SKILL.md openhue install-skill --project # ./.claude/skills/openhue/SKILL.md ``` -Docker alternative (mount the config folder): +### Docker alternative + +The image has no config of its own: mount the host config folder on `/.openhue` for every command. An alias makes the rest of this guide work as written: ```bash -docker run -v "${HOME}/.openhue:/.openhue" --rm -it openhue/cli get lights +alias openhue='docker run -v "${HOME}/.openhue:/.openhue" --rm -it openhue/cli' +openhue setup -b 192.168.1.68 # pass the bridge IP: mDNS rarely crosses Docker's default network +openhue get lights ``` +- Config under `$XDG_CONFIG_HOME/openhue` on the host? Mount that folder instead: `-v "${XDG_CONFIG_HOME}/openhue:/.openhue"`. +- On Linux, `--network host` lets `setup` and `discover` find the bridge without `-b`. +- `install-skill` writes inside the container, so use `openhue install-skill --print > ~/.claude/skills/openhue/SKILL.md` (create the folder first). + ## 2. Pair with the bridge (once) ```bash @@ -105,7 +113,8 @@ Edit or copy through YAML: ```bash openhue get scene Cozy -r "Living Room" --yaml > cozy.yaml -openhue update scene Cozy -r "Living Room" --from-file cozy.yaml +openhue create scene "Cozy Copy" -r "Living Room" --from-file cozy.yaml # copy, original untouched +openhue update scene Cozy -r "Living Room" --from-file cozy.yaml # or edit cozy.yaml and apply it back openhue update scene Cozy --light "Shelf:on,brightness=30" --remove-light Ceiling openhue update scene Cozy --name "Cozy Evening" ``` From e2cc8597fc0dd466461f68dc89adea24ac3d1e1c Mon Sep 17 00:00:00 2001 From: thibauult Date: Sat, 7 Nov 2026 10:41:04 +0100 Subject: [PATCH 5/5] Add skills.sh listing check workflow Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/skills-sh.yml | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/skills-sh.yml diff --git a/.github/workflows/skills-sh.yml b/.github/workflows/skills-sh.yml new file mode 100644 index 0000000..247436b --- /dev/null +++ b/.github/workflows/skills-sh.yml @@ -0,0 +1,37 @@ +# Checks that the openhue skill is still discoverable by the skills CLI that +# powers https://www.skills.sh after SKILL.md changes land on main. +# +# skills.sh has no publish or update API: it reads the GitHub repo directly and +# ranks skills from install telemetry. So this only verifies the listing source; +# it does not run an install, which would add a fake install to the leaderboard. + +name: skills.sh + +on: + push: + branches: [ "main" ] + paths: + - "SKILL.md" + - ".claude/skills/**" + - ".github/workflows/skills-sh.yml" + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: List skills as the skills CLI sees them + run: | + npx --yes skills add "${{ github.repository }}" --list | sed 's/\x1b\[[0-9;]*m//g' | tee skills.txt + # The root SKILL.md is named "openhue"; fail if it is not discovered. + grep -qE '^[^A-Za-z0-9]*openhue[[:space:]]*$' skills.txt