From a583f234b5fb819c65d2d1c48a3fe0ad62e769ff Mon Sep 17 00:00:00 2001 From: Lucas Giordano Date: Fri, 15 May 2026 11:21:05 -0400 Subject: [PATCH 1/2] 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) + } +} From 872b7a002146e666ce616734202b0acbb934d9b8 Mon Sep 17 00:00:00 2001 From: Lucas Giordano Date: Tue, 19 May 2026 20:52:17 -0700 Subject: [PATCH 2/2] fix(skill): skip npx interactive prompt with --yes The skill-add CI job failed: `npx skills add` found the skill but then hit its interactive agent picker ("Which agents do you want to install to?"). With no TTY the picker aborts and installs nothing, so the job's SKILL.md assertion failed. Locally this was masked because npx auto-detects the agent when run inside Claude Code. Forward the CLI's global --yes flag to npx as `-y`, which skips the picker and installs to all detected assistants. This matches how the rest of the cmd package gates non-interactive behavior (confirm.go). Run the CI job with `notte --yes skill add`. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 7 ++++--- internal/cmd/skills.go | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95128a9..6b39727 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,8 @@ jobs: workdir="$(mktemp -d)" cp notte "$workdir/notte" cd "$workdir" - ./notte skill add + # --yes skips the interactive agent picker (no TTY in CI). + ./notte --yes 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 @@ -85,8 +86,8 @@ jobs: workdir="$(mktemp -d)" cp notte "$workdir/notte" cd "$workdir" - ./notte skill add - ./notte skill add --upgrade + ./notte --yes skill add + ./notte --yes 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 diff --git a/internal/cmd/skills.go b/internal/cmd/skills.go index f56005c..b958a9a 100644 --- a/internal/cmd/skills.go +++ b/internal/cmd/skills.go @@ -38,6 +38,10 @@ 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. +npx skills prompts interactively to pick which AI assistants to install +to. Pass --yes (-y) to skip that prompt and install to all detected +assistants — required when running non-interactively (CI, scripts). + The skill enables AI coding assistants (like Cursor, Claude Code, etc.) to control browser sessions through natural language commands.`, RunE: runSkillAdd, @@ -77,10 +81,21 @@ func runSkillAdd(cmd *cobra.Command, args []string) error { 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"}) + return runNpx(cmd, "skill removal", []string{"skills", "remove", "--skill", skillName}) } +// runNpx executes `npx ` wired to the current stdio. +// +// `npx skills` prompts interactively (an agent picker for `add`, a scope +// prompt for `update`/`remove`). With no terminal to answer them those +// prompts silently abort and nothing is installed — which is how a previous +// CI run "succeeded" while installing nothing. Forward the global --yes flag +// as `-y` so non-interactive callers can opt into skipping the prompts. func runNpx(cmd *cobra.Command, action string, args []string) error { + if yesFlag { + args = append(args, "-y") + } + npxCmd := exec.CommandContext(cmd.Context(), "npx", args...) npxCmd.Stdout = os.Stdout