From d9342a63e1cf113a0ee8244ea5ce3ef041702133 Mon Sep 17 00:00:00 2001 From: Lucas Giordano Date: Fri, 15 May 2026 11:21:05 -0400 Subject: [PATCH] fix(skill): point npx source at nottelabs/notte-skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `notte skill add` ran `npx skills add nottelabs/notte-cli`, which clones this repo and searches for SKILL.md files. After the skills/ directory was replaced by the notte-skills submodule (3eca2b3), `git clone` no longer pulls in skill content, so the tool reported "No skills found". Point the npx source at nottelabs/notte-skills (where SKILL.md actually lives) and refactor the npx invocation so add/remove/upgrade share one helper. Also add a `-f` / `--upgrade` flag that delegates to `npx skills update notte-browser` for refreshing an installed skill. Add a skill-add CI job that builds the CLI, runs `notte skill add` and `notte skill add --upgrade` in temp dirs, and asserts that .agents/skills/notte-browser/SKILL.md is written — so a future drift in the source URL or skill name fails CI loudly instead of silently saying "No skills found". Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++ README.md | 2 +- internal/cmd/skills.go | 56 +++++++++++++++++++++---------------- internal/cmd/skills_test.go | 32 +++++++++++++++++---- 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ef8e93..95128a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,3 +45,49 @@ jobs: with: files: ./coverage.out fail_ci_if_error: false + + skill-add: + # End-to-end check that `notte skill add` actually installs the skill from + # the nottelabs/notte-skills repo. Catches regressions where the npx + # source URL or skill name drifts and the tool reports "No skills found". + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Build CLI + run: go build -o notte ./cmd/notte + + - name: notte skill add + run: | + set -euo pipefail + workdir="$(mktemp -d)" + cp notte "$workdir/notte" + cd "$workdir" + ./notte skill add + if [ ! -f .agents/skills/notte-browser/SKILL.md ]; then + echo "::error::notte skill add did not install notte-browser/SKILL.md" + ls -laR .agents || true + exit 1 + fi + + - name: notte skill add --upgrade (re-runs against installed skill) + run: | + set -euo pipefail + workdir="$(mktemp -d)" + cp notte "$workdir/notte" + cd "$workdir" + ./notte skill add + ./notte skill add --upgrade + if [ ! -f .agents/skills/notte-browser/SKILL.md ]; then + echo "::error::notte skill add --upgrade did not leave notte-browser/SKILL.md installed" + exit 1 + fi diff --git a/README.md b/README.md index 98a0ea1..ddf112b 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ The `--help` output is comprehensive and most agents can figure it out from ther Add the skill to your AI coding assistant for richer context: ```bash -npx skills add nottelabs/notte-cli +npx skills add nottelabs/notte-skills ``` This works with Claude Code, Cursor, Windsurf, and other MCP-compatible assistants. diff --git a/internal/cmd/skills.go b/internal/cmd/skills.go index fae317f..f56005c 100644 --- a/internal/cmd/skills.go +++ b/internal/cmd/skills.go @@ -8,6 +8,17 @@ import ( "github.com/spf13/cobra" ) +// Skill source / skill name. The skill files live in the nottelabs/notte-skills +// repository (vendored here as the notte-skills submodule). Pointing the +// npx tool at nottelabs/notte-cli would clone this repo, where the skills +// are an empty submodule and no SKILL.md is found. +const ( + skillSource = "nottelabs/notte-skills" + skillName = "notte-browser" +) + +var skillAddUpgrade bool + var skillCmd = &cobra.Command{ Use: "skill", Short: "Manage Notte skills for AI coding assistants", @@ -22,7 +33,10 @@ var skillAddCmd = &cobra.Command{ Short: "Install the Notte skill for your AI coding assistant", Long: `Install the Notte browser automation skill using npx. -This command runs: npx skills add nottelabs/notte-cli +This command runs: npx skills add nottelabs/notte-skills + +With --upgrade (or -f), it runs: npx skills update notte-browser +to refresh an already-installed skill to the latest version. The skill enables AI coding assistants (like Cursor, Claude Code, etc.) to control browser sessions through natural language commands.`, @@ -35,7 +49,7 @@ var skillRemoveCmd = &cobra.Command{ Short: "Remove the Notte skill from your AI coding assistant", Long: `Remove the Notte browser automation skill using npx. -This command runs: npx skills remove nottelabs/notte-cli`, +This command runs: npx skills remove --skill notte-browser`, RunE: runSkillRemove, } @@ -43,45 +57,39 @@ func init() { rootCmd.AddCommand(skillCmd) skillCmd.AddCommand(skillAddCmd) skillCmd.AddCommand(skillRemoveCmd) + + skillAddCmd.Flags().BoolVarP(&skillAddUpgrade, "upgrade", "f", false, + "Force a reinstall by updating an already-installed skill to the latest version") } func runSkillAdd(cmd *cobra.Command, args []string) error { - PrintInfo("Installing Notte skill via npx...") - - // Create the npx command - npxCmd := exec.CommandContext(cmd.Context(), "npx", "skills", "add", "nottelabs/notte-cli") - - // Connect stdout and stderr to show output in real-time - npxCmd.Stdout = os.Stdout - npxCmd.Stderr = os.Stderr - npxCmd.Stdin = os.Stdin - - // Run the command - if err := npxCmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("skill installation failed with exit code %d", exitErr.ExitCode()) - } - return fmt.Errorf("failed to run npx: %w", err) + var npxArgs []string + if skillAddUpgrade { + PrintInfo("Upgrading Notte skill via npx...") + npxArgs = []string{"skills", "update", skillName} + } else { + PrintInfo("Installing Notte skill via npx...") + npxArgs = []string{"skills", "add", skillSource} } - return nil + return runNpx(cmd, "skill installation", npxArgs) } func runSkillRemove(cmd *cobra.Command, args []string) error { PrintInfo("Removing Notte skill via npx...") + return runNpx(cmd, "skill removal", []string{"skills", "remove", "--skill", skillName, "-y"}) +} - // Create the npx command - npxCmd := exec.CommandContext(cmd.Context(), "npx", "skills", "remove", "nottelabs/notte-cli") +func runNpx(cmd *cobra.Command, action string, args []string) error { + npxCmd := exec.CommandContext(cmd.Context(), "npx", args...) - // Connect stdout and stderr to show output in real-time npxCmd.Stdout = os.Stdout npxCmd.Stderr = os.Stderr npxCmd.Stdin = os.Stdin - // Run the command if err := npxCmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("skill removal failed with exit code %d", exitErr.ExitCode()) + return fmt.Errorf("%s failed with exit code %d", action, exitErr.ExitCode()) } return fmt.Errorf("failed to run npx: %w", err) } diff --git a/internal/cmd/skills_test.go b/internal/cmd/skills_test.go index b99d912..55b5238 100644 --- a/internal/cmd/skills_test.go +++ b/internal/cmd/skills_test.go @@ -5,7 +5,6 @@ import ( ) func TestSkillCommandStructure(t *testing.T) { - // Verify skill command exists and has correct properties if skillCmd == nil { t.Fatal("skillCmd is nil") } @@ -20,7 +19,6 @@ func TestSkillCommandStructure(t *testing.T) { } func TestSkillAddCommandStructure(t *testing.T) { - // Verify skill add command exists and has correct properties if skillAddCmd == nil { t.Fatal("skillAddCmd is nil") } @@ -38,8 +36,20 @@ func TestSkillAddCommandStructure(t *testing.T) { } } +func TestSkillAddUpgradeFlag(t *testing.T) { + upgrade := skillAddCmd.Flags().Lookup("upgrade") + if upgrade == nil { + t.Fatal("skillAddCmd should have an --upgrade flag") + } + if upgrade.Shorthand != "f" { + t.Errorf("expected --upgrade shorthand to be 'f', got %q", upgrade.Shorthand) + } + if upgrade.Value.Type() != "bool" { + t.Errorf("expected --upgrade to be a bool flag, got %s", upgrade.Value.Type()) + } +} + func TestSkillRemoveCommandStructure(t *testing.T) { - // Verify skill remove command exists and has correct properties if skillRemoveCmd == nil { t.Fatal("skillRemoveCmd is nil") } @@ -56,7 +66,6 @@ func TestSkillRemoveCommandStructure(t *testing.T) { t.Error("skillRemoveCmd.RunE should not be nil") } - // Check that 'rm' is an alias hasAlias := false for _, alias := range skillRemoveCmd.Aliases { if alias == "rm" { @@ -70,7 +79,6 @@ func TestSkillRemoveCommandStructure(t *testing.T) { } func TestSkillSubcommands(t *testing.T) { - // Verify both add and remove are registered as subcommands of skill subcommands := make(map[string]bool) for _, cmd := range skillCmd.Commands() { subcommands[cmd.Use] = true @@ -84,3 +92,17 @@ func TestSkillSubcommands(t *testing.T) { t.Error("'remove' command should be a subcommand of 'skill'") } } + +func TestSkillSourcePointsToSkillsRepo(t *testing.T) { + // Regression guard: the npx skills tool clones whatever repo this points + // at and searches it for SKILL.md files. The skill content lives in + // nottelabs/notte-skills; pointing at nottelabs/notte-cli (the CLI repo) + // would find only the empty submodule directory and report "No skills + // found". + if skillSource != "nottelabs/notte-skills" { + t.Errorf("skillSource should be 'nottelabs/notte-skills', got %q", skillSource) + } + if skillName != "notte-browser" { + t.Errorf("skillName should be 'notte-browser', got %q", skillName) + } +}