Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0d112cd
feat(skill): add SKILL.md frontmatter parser
ArangoGutierrez May 14, 2026
33d54b1
feat(skill): embed catalog and expose Catalog()
ArangoGutierrez May 14, 2026
510a0f5
feat(skill): add Renderer interface and Claude renderer
ArangoGutierrez May 14, 2026
664ae24
feat(skill): add Cursor renderer
ArangoGutierrez May 14, 2026
4760a4e
feat(skill): add Codex renderer
ArangoGutierrez May 14, 2026
94272a7
feat(skill): add Gemini renderer
ArangoGutierrez May 14, 2026
8802972
feat(skill): installer with atomic write and idempotent splice
ArangoGutierrez May 14, 2026
33ee510
feat(cli): add 'holodeck skill list' subcommand
ArangoGutierrez May 14, 2026
64199a1
feat(cli): add 'holodeck skill add' subcommand
ArangoGutierrez May 14, 2026
31f870f
feat(cli): wire skill command into main
ArangoGutierrez May 14, 2026
98b88bc
docs(skill): author using-holodeck skill content
ArangoGutierrez May 14, 2026
a847541
docs: add Agentic skills section to README
ArangoGutierrez May 14, 2026
e315475
chore(deps): promote yaml.v2 from indirect to direct
ArangoGutierrez May 15, 2026
b0e85d6
fix(cli): correct skill add doc examples to use flag-first ordering
ArangoGutierrez May 15, 2026
8b16d6a
fix(skill): satisfy CI lint rules (errcheck, gosec, ST1023)
ArangoGutierrez May 17, 2026
68a91b9
docs(skill): align using-holodeck SKILL.md with actual command surface
ArangoGutierrez May 17, 2026
937af63
feat(skill): validate skill names with allowlist regex
ArangoGutierrez May 17, 2026
675cbfc
fix(skill): correct 'os ami' invocation syntax in using-holodeck
ArangoGutierrez May 17, 2026
7e30727
fix(skill): correct env.yaml schema and 'os list' flag claim in SKILL.md
ArangoGutierrez May 17, 2026
54e9ca6
test(skill): cover length-cap and rename embedded-space case
ArangoGutierrez May 17, 2026
5209032
fix(skill): correct three SKILL.md hallucinations on dryrun/update/auth
ArangoGutierrez May 17, 2026
c234b82
test(skill): add 64-char upper-bound acceptance case for skill name r…
ArangoGutierrez May 17, 2026
8dcf45d
test(skill): guard against positional-then-flag examples in skill cmd…
ArangoGutierrez May 17, 2026
093d0be
fix(skill): use flag-first ordering in skill command help examples
ArangoGutierrez May 17, 2026
a61e442
docs(skill): document flag-first ordering pitfall in using-holodeck S…
ArangoGutierrez May 17, 2026
0311f62
docs(skill): clarify fileName usage in Catalog walker
ArangoGutierrez May 21, 2026
5aebea7
chore(skill): use sigs.k8s.io/yaml for frontmatter, drop yaml.v2 dep
ArangoGutierrez May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,33 @@ Platform: Linux/Darwin. On Windows, the chown step is a no-op.
`rsync + ssh + remote-run` blocks in your workflow with a direct
`kubectl --kubeconfig=$GITHUB_WORKSPACE/kubeconfig …` step.

## Agentic skills

Holodeck ships an embedded catalog of agentic skills that teach an AI
coding agent how to drive the CLI correctly. List the catalog:

```bash
holodeck skill list
```

Install a skill into your AI agent's native format:

```bash
# Claude Code (project-local: ./.claude/skills/<name>/SKILL.md)
holodeck skill add --claude using-holodeck

# Multiple agents at once
holodeck skill add --claude --cursor --codex --gemini using-holodeck

# Or install everything for every agent, user-wide
holodeck skill add --all --all-agents --global
```

Supported agents: Claude Code, Cursor, Codex CLI, Gemini CLI. Skills
are short markdown guides authored against the actual CLI behavior;
they version with the code so updates land alongside the features
they describe.

## 📂 More

- [Examples](docs/examples/)
Expand Down
2 changes: 2 additions & 0 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/NVIDIA/holodeck/cmd/cli/list"
oscmd "github.com/NVIDIA/holodeck/cmd/cli/os"
"github.com/NVIDIA/holodeck/cmd/cli/scp"
"github.com/NVIDIA/holodeck/cmd/cli/skill"
"github.com/NVIDIA/holodeck/cmd/cli/ssh"
"github.com/NVIDIA/holodeck/cmd/cli/status"
"github.com/NVIDIA/holodeck/cmd/cli/update"
Expand Down Expand Up @@ -146,6 +147,7 @@ Examples:
list.NewCommand(log),
oscmd.NewCommand(log),
scp.NewCommand(log),
skill.NewCommand(log),
ssh.NewCommand(log),
status.NewCommand(log),
update.NewCommand(log),
Expand Down
166 changes: 166 additions & 0 deletions cmd/cli/skill/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package skill

import (
"fmt"
"strings"

pkgskill "github.com/NVIDIA/holodeck/pkg/skill"

cli "github.com/urfave/cli/v2"
)

func (c *command) buildAddCommand() *cli.Command {
return &cli.Command{
Name: "add",
Usage: "Install one or more skills for one or more agents",
ArgsUsage: "[skill-name]",
Description: `Install a skill from the embedded catalog into one or more agents.

Targets: --claude, --cursor, --codex, --gemini (or --all-agents).
Skill selection: positional <skill-name>, or --all for every skill.

Examples:
holodeck skill add --claude using-holodeck
holodeck skill add --claude --cursor --global using-holodeck
holodeck skill add --all --all-agents`,
Flags: []cli.Flag{
&cli.BoolFlag{Name: "claude", Usage: "install for Claude Code", Destination: &c.claude},
&cli.BoolFlag{Name: "cursor", Usage: "install for Cursor IDE", Destination: &c.cursor},
&cli.BoolFlag{Name: "codex", Usage: "install for Codex CLI", Destination: &c.codex},
&cli.BoolFlag{Name: "gemini", Usage: "install for Gemini CLI", Destination: &c.gemini},
&cli.BoolFlag{Name: "all-agents", Usage: "install for all four agents", Destination: &c.allAgents},
&cli.BoolFlag{Name: "all", Usage: "install every skill in the catalog (mutually exclusive with positional name)", Destination: &c.all},
&cli.BoolFlag{Name: "global", Usage: "write to the user-wide config dir instead of CWD", Destination: &c.global},
&cli.BoolFlag{Name: "force", Usage: "overwrite existing per-file installs without prompting", Destination: &c.force},
&cli.BoolFlag{Name: "stdout", Usage: "print rendered output instead of writing (requires exactly one skill and one agent)", Destination: &c.stdout},
&cli.BoolFlag{Name: "dry-run", Usage: "print install paths without writing", Destination: &c.dryRun},
},
Action: c.runAdd,
}
}

func (c *command) runAdd(ctx *cli.Context) error {
renderers, err := c.selectedRenderers()
if err != nil {
return err
}
if len(renderers) == 0 {
return fmt.Errorf("must specify at least one of --claude/--cursor/--codex/--gemini/--all-agents")
}

catalog, err := pkgskill.Catalog()
if err != nil {
return fmt.Errorf("loading catalog: %w", err)
}

skills, err := c.selectedSkills(ctx, catalog)
if err != nil {
return err
}

if c.stdout && (len(skills) != 1 || len(renderers) != 1) {
return fmt.Errorf("--stdout requires exactly one skill and one agent (got %d skills, %d agents)",
len(skills), len(renderers))
}

for _, s := range skills {
for _, r := range renderers {
if c.stdout {
rendered, err := r.Render(s)
if err != nil {
return err
}
if _, err := c.out.Write(rendered); err != nil {
return err
}
continue
}
path, err := pkgskill.Install(r, s, pkgskill.InstallOptions{
Global: c.global,
Force: c.force,
DryRun: c.dryRun,
Stdout: c.out,
})
if err != nil {
return err
}
if !c.dryRun {
if _, err := fmt.Fprintf(c.out, "installed %s for %s -> %s\n", s.Name, r.AgentName(), path); err != nil {
return err
}
}
}
}
return nil
}

func (c *command) selectedRenderers() ([]pkgskill.Renderer, error) {
if c.allAgents {
c.claude = true
c.cursor = true
c.codex = true
c.gemini = true
}
var rs []pkgskill.Renderer
if c.claude {
rs = append(rs, pkgskill.NewClaudeRenderer())
}
if c.cursor {
rs = append(rs, pkgskill.NewCursorRenderer())
}
if c.codex {
rs = append(rs, pkgskill.NewCodexRenderer())
}
if c.gemini {
rs = append(rs, pkgskill.NewGeminiRenderer())
}
return rs, nil
}

func (c *command) selectedSkills(ctx *cli.Context, catalog []pkgskill.Skill) ([]pkgskill.Skill, error) {
name := ""
if ctx.NArg() > 0 {
name = ctx.Args().First()
}
if c.all && name != "" {
return nil, fmt.Errorf("--all is mutually exclusive with a skill name")
}
if !c.all && name == "" {
return nil, fmt.Errorf("skill name required (or pass --all); available: %s",
strings.Join(catalogNames(catalog), ", "))
}
if c.all {
return catalog, nil
}
for _, s := range catalog {
if s.Name == name {
return []pkgskill.Skill{s}, nil
}
}
return nil, fmt.Errorf("skill %q not found; available: %s",
name, strings.Join(catalogNames(catalog), ", "))
}

func catalogNames(catalog []pkgskill.Skill) []string {
names := make([]string, len(catalog))
for i, s := range catalog {
names[i] = s.Name
}
return names
}
Loading