Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,52 @@ Everything happens in memory; there is no HTML renderer or external process.

---

## Agent Skills

`md2png` features an integrated **Agent Skills** system, teaching AI coding agents how to interact with the CLI effectively, avoid common pitfalls, and respect formatting limits. You can install, inspect, update, and remove these skills.

Skills are securely installed into `.agents/skills/` (for the project scope) or your user's home directory (`--scope user`).

### Installing a skill

You can install a skill from a remote repository, a local path, or the official builtin skill:

```bash
# Install the official builtin skill
./md2png skill install md2png

# Install from a remote GitHub repository
./md2png skill install owner/repository cool-skill

# Install from a local directory
./md2png skill install ./my-local-skill my-skill
```

### Managing skills

List all installed skills:
```bash
./md2png skill list
```

Update a specific skill, or update all skills at once:
```bash
./md2png skill update cool-skill
./md2png skill update --all
```

If you have made local changes to a skill's files, `update` will refuse to overwrite them. Use `--force` to discard your local edits.

Inspect metadata for an installed skill:
```bash
./md2png skill inspect cool-skill
```

Remove a skill:
```bash
./md2png skill remove cool-skill
```

## Roadmap

- [x] Tables
Expand Down
8 changes: 8 additions & 0 deletions builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package md2png

import (
"embed"
)

//go:embed skills/md2png/SKILL.md
var BuiltinSkillFS embed.FS
6 changes: 6 additions & 0 deletions cmd/md2png/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ import (
"strings"

"github.com/arran4/md2png"
"github.com/arran4/md2png/skill"
)

func main() {
if len(os.Args) >= 2 && os.Args[1] == "skill" {
skill.Execute(os.Args[2:])
return
}

in := flag.String("in", "", "Input Markdown file (default: stdin if empty)")
out := flag.String("out", "out.png", "Output image file (.png, .jpg, or .gif)")
width := flag.Int("width", 1024, "Output image width in pixels")
Expand Down
6 changes: 6 additions & 0 deletions cmd/md2view/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"path/filepath"

"github.com/arran4/md2png"
"github.com/arran4/md2png/skill"
"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
Expand All @@ -22,6 +23,11 @@ import (
)

func main() {
if len(os.Args) >= 2 && os.Args[1] == "skill" {
skill.Execute(os.Args[2:])
return
}

in := flag.String("in", "", "Input Markdown file (default: stdin if empty)")
width := flag.Int("width", 1024, "Output image width in pixels")
margin := flag.Int("margin", 48, "Margin in pixels")
Expand Down
209 changes: 209 additions & 0 deletions skill/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package skill

import (
"flag"
"fmt"
"os"
)

// Execute runs the skill subcommand group.
func Execute(args []string) {
if len(args) == 0 {
usage()
os.Exit(1)
}

cmd := args[0]
cmdArgs := args[1:]

switch cmd {
case "install":
RunInstall(cmdArgs)
case "update":
RunUpdate(cmdArgs)
case "remove":
RunRemove(cmdArgs)
case "list":
RunList(cmdArgs)
case "inspect", "info":
RunInspect(cmdArgs)
case "doctor":
RunDoctor(cmdArgs)
case "help":
usage()
default:
fmt.Fprintf(os.Stderr, "Unknown skill command: %s\n", cmd)
usage()
os.Exit(1)
}
}

func usage() {
fmt.Fprintf(os.Stderr, "Usage: skill <command> [options]\n\n")
fmt.Fprintf(os.Stderr, "Commands:\n")
fmt.Fprintf(os.Stderr, " install <source> [name] Install a skill\n")
fmt.Fprintf(os.Stderr, " update [name] Update a skill (or --all)\n")
fmt.Fprintf(os.Stderr, " remove <name> Remove a skill\n")
fmt.Fprintf(os.Stderr, " list List installed skills\n")
fmt.Fprintf(os.Stderr, " inspect <name> Inspect an installed skill\n")
fmt.Fprintf(os.Stderr, " doctor Check for issues with installed skills\n")
fmt.Fprintf(os.Stderr, "\n")
}

func parseScope(scopeStr string) (Scope, error) {
switch scopeStr {
case "user":
return ScopeUser, nil
case "project":
return ScopeProject, nil
default:
return "", fmt.Errorf("invalid scope: %s (must be 'user' or 'project')", scopeStr)
}
}

func RunInstall(args []string) {
fs := flag.NewFlagSet("install", flag.ExitOnError)
scopeStr := fs.String("scope", "project", "Installation scope: 'user' or 'project'")
agentTarget := fs.String("agent", "", "Target agent (e.g., codex, claude, copilot, cursor)")
fs.Parse(args)

Check failure on line 68 in skill/cmd.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `fs.Parse` is not checked (errcheck)

if fs.NArg() < 1 {
fmt.Fprintf(os.Stderr, "Usage: skill install <source> [name] [flags]\n")
os.Exit(1)
}

source := fs.Arg(0)
name := fs.Arg(1)

scope, err := parseScope(*scopeStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

err = InstallSkill(source, name, scope, *agentTarget)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to install skill: %v\n", err)
os.Exit(1)
}
}

func RunUpdate(args []string) {
fs := flag.NewFlagSet("update", flag.ExitOnError)
all := fs.Bool("all", false, "Update all skills")
force := fs.Bool("force", false, "Force update even if locally modified")
scopeStr := fs.String("scope", "project", "Installation scope: 'user' or 'project'")
agentTarget := fs.String("agent", "", "Target agent")
fs.Parse(args)

Check failure on line 97 in skill/cmd.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `fs.Parse` is not checked (errcheck)

scope, err := parseScope(*scopeStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if *all {
err = UpdateAllSkills(scope, *agentTarget, *force)
} else {
if fs.NArg() < 1 {
fmt.Fprintf(os.Stderr, "Usage: skill update <name> [flags] or --all\n")
os.Exit(1)
}
name := fs.Arg(0)
err = UpdateSkill(name, scope, *agentTarget, *force)
}

if err != nil {
fmt.Fprintf(os.Stderr, "Failed to update skill(s): %v\n", err)
os.Exit(1)
}
}

func RunRemove(args []string) {
fs := flag.NewFlagSet("remove", flag.ExitOnError)
scopeStr := fs.String("scope", "project", "Installation scope: 'user' or 'project'")
agentTarget := fs.String("agent", "", "Target agent")
fs.Parse(args)

Check failure on line 126 in skill/cmd.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `fs.Parse` is not checked (errcheck)

if fs.NArg() < 1 {
fmt.Fprintf(os.Stderr, "Usage: skill remove <name> [flags]\n")
os.Exit(1)
}

name := fs.Arg(0)
scope, err := parseScope(*scopeStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

err = RemoveSkill(name, scope, *agentTarget)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to remove skill: %v\n", err)
os.Exit(1)
}
}

func RunList(args []string) {
fs := flag.NewFlagSet("list", flag.ExitOnError)
scopeStr := fs.String("scope", "project", "Installation scope: 'user' or 'project'")
agentTarget := fs.String("agent", "", "Target agent")
format := fs.String("format", "text", "Output format (text, json)")
fs.Parse(args)

scope, err := parseScope(*scopeStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

err = ListSkills(scope, *agentTarget, *format)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list skills: %v\n", err)
os.Exit(1)
}
}

func RunInspect(args []string) {
fs := flag.NewFlagSet("inspect", flag.ExitOnError)
scopeStr := fs.String("scope", "project", "Installation scope: 'user' or 'project'")
agentTarget := fs.String("agent", "", "Target agent")
fs.Parse(args)

if fs.NArg() < 1 {
fmt.Fprintf(os.Stderr, "Usage: skill inspect <name> [flags]\n")
os.Exit(1)
}

name := fs.Arg(0)
scope, err := parseScope(*scopeStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

err = InspectSkill(name, scope, *agentTarget)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to inspect skill: %v\n", err)
os.Exit(1)
}
}

func RunDoctor(args []string) {
fs := flag.NewFlagSet("doctor", flag.ExitOnError)
scopeStr := fs.String("scope", "project", "Installation scope: 'user' or 'project'")
agentTarget := fs.String("agent", "", "Target agent")
fs.Parse(args)

scope, err := parseScope(*scopeStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

err = DoctorSkills(scope, *agentTarget)
if err != nil {
fmt.Fprintf(os.Stderr, "Doctor found issues: %v\n", err)
os.Exit(1)
}
}
Loading
Loading