Skip to content
Open
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
189 changes: 189 additions & 0 deletions cmd/skill_install_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package cmd

import (
"context"
"database/sql"
"fmt"
"os"
"os/signal"
"path/filepath"
"text/tabwriter"

_ "github.com/jackc/pgx/v5/stdlib"
"github.com/spf13/cobra"

"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/skills"
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
)

func skillsInstallCmd() *cobra.Command {
var ref string
cmd := &cobra.Command{
Use: "install [name|url]",
Short: "Install a skill from registry or GitHub",
Long: `Install a skill from the GoClaw registry (by slug) or directly from a GitHub repo.

Examples:
goclaw skills install shopee-product-finder
goclaw skills install github.com/user/repo
goclaw skills install owner/repo@v1.0 --ref main`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := runSkillInstall(args[0], ref); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
cmd.Flags().StringVar(&ref, "ref", "", "Git ref, tag, or branch to install")
return cmd
}

func runSkillInstall(input, refOverride string) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

// Parse skill reference.
skillRef, err := skills.ParseSkillRef(input)
if err != nil {
return err
}

// Override ref if flag provided.
if refOverride != "" {
skillRef.Ref = refOverride
}

// Resolve registry slug → owner/repo.
cfgPath := resolveConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
dataDir := cfg.ResolvedDataDir()

owner, repo := skillRef.Owner, skillRef.Repo
if skillRef.IsRegistry {
fmt.Printf("Resolving %q from registry...\n", input)
cacheDir := filepath.Join(dataDir, "cache")
registry := skills.NewRegistryClient(cacheDir)
var err error
owner, repo, err = registry.Resolve(ctx, input)
if err != nil {
return err
}
}

// Fetch from GitHub.
fmt.Printf("Fetching %s/%s", owner, repo)
if skillRef.Ref != "" {
fmt.Printf("@%s", skillRef.Ref)
}
fmt.Println("...")

tmpDir, err := skills.FetchFromGitHub(ctx, owner, repo, skillRef.Ref)
if err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
defer os.RemoveAll(tmpDir)

// Connect DB.
db, err := connectDBForCLI()
if err != nil {
return fmt.Errorf("database: %w", err)
}
defer db.Close()

// Build installer.
skillsStoreDir := filepath.Join(dataDir, "skills-store")
if err := os.MkdirAll(skillsStoreDir, 0755); err != nil {
return err
}

store := pg.NewPGSkillStore(db, skillsStoreDir)
loader := loadSkillsLoader()
installer := skills.NewInstaller(store, skillsStoreDir, loader)

// Install.
fmt.Println("Installing...")
ownerID := fmt.Sprintf("github:%s/%s", owner, repo)
result, err := installer.Install(ctx, tmpDir, ownerID)
if err != nil {
return err
}

// Print result.
fmt.Printf("\nDone! Skill installed:\n")
fmt.Printf(" Name: %s\n", result.Name)
fmt.Printf(" Slug: %s\n", result.Slug)
fmt.Printf(" Version: %d\n", result.Version)
fmt.Printf(" ID: %s\n", result.ID)
if result.DepsWarning != "" {
fmt.Printf("\n ⚠ Dependencies: %s\n", result.DepsWarning)
}
return nil
}

func skillsSearchCmd() *cobra.Command {
return &cobra.Command{
Use: "search [query]",
Short: "Search the skill registry",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
cfgPath := resolveConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: load config: %v\n", err)
os.Exit(1)
}
cacheDir := filepath.Join(cfg.ResolvedDataDir(), "cache")
registry := skills.NewRegistryClient(cacheDir)

results, err := registry.Search(ctx, args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if len(results) == 0 {
fmt.Println("No skills found matching query.")
return
}

tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(tw, "SLUG\tREPO\tDESCRIPTION\n")
for _, r := range results {
desc := r.Description
if len(desc) > 60 {
desc = desc[:57] + "..."
}
fmt.Fprintf(tw, "%s\t%s\t%s\n", r.Slug, r.Repo, desc)
}
tw.Flush()
},
}
}

// connectDBForCLI opens a minimal database connection for CLI commands.
func connectDBForCLI() (*sql.DB, error) {
cfgPath := resolveConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
return nil, fmt.Errorf("load config: %w", err)
}
dsn := cfg.Database.PostgresDSN
if dsn == "" {
return nil, fmt.Errorf("GOCLAW_POSTGRES_DSN is not set")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("cannot connect to database: %w", err)
}
return db, nil
}
57 changes: 57 additions & 0 deletions cmd/skill_remove_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cmd

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"

"github.com/nextlevelbuilder/goclaw/internal/config"
"github.com/nextlevelbuilder/goclaw/internal/skills"
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
)

func skillsRemoveCmd() *cobra.Command {
return &cobra.Command{
Use: "remove [slug]",
Short: "Remove an installed skill",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := runSkillRemove(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}
}

func runSkillRemove(slug string) error {
ctx := context.Background()

db, err := connectDBForCLI()
if err != nil {
return fmt.Errorf("database: %w", err)
}
defer db.Close()

cfgPath := resolveConfigPath()
cfg, err := config.Load(cfgPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
dataDir := cfg.ResolvedDataDir()
skillsStoreDir := filepath.Join(dataDir, "skills-store")

store := pg.NewPGSkillStore(db, skillsStoreDir)
loader := loadSkillsLoader()
installer := skills.NewInstaller(store, skillsStoreDir, loader)

if err := installer.Uninstall(ctx, slug); err != nil {
return err
}

fmt.Printf("Skill %q removed.\n", slug)
return nil
}
3 changes: 3 additions & 0 deletions cmd/skills_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ func skillsCmd() *cobra.Command {
}
cmd.AddCommand(skillsListCmd())
cmd.AddCommand(skillsShowCmd())
cmd.AddCommand(skillsInstallCmd())
cmd.AddCommand(skillsRemoveCmd())
cmd.AddCommand(skillsSearchCmd())
return cmd
}

Expand Down
2 changes: 1 addition & 1 deletion docs/00-architecture-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ flowchart TD
| `internal/store/pg/` | PostgreSQL implementations (`database/sql` + `pgx/v5`) |
| `internal/bootstrap/` | System prompt files (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, BOOTSTRAP.md) + seeding + truncation |
| `internal/config/` | Config loading (JSON5) + env var overlay |
| `internal/skills/` | SKILL.md loader (5-tier hierarchy) + BM25 search + hot-reload via fsnotify |
| `internal/skills/` | SKILL.md loader (5-tier hierarchy) + BM25 search + hot-reload via fsnotify. Skill Hub: GitHub fetcher, registry client (curated skill index), installer (multi-stage validation→copy→DB→deps) |
| `internal/channels/` | Channel manager + adapters: Telegram (forum topics, STT, bot commands), Feishu/Lark (streaming cards, media), Zalo OA, Zalo Personal, Discord, WhatsApp, Slack |
| `internal/mcp/` | MCP server bridge (stdio, SSE, streamable-HTTP transports) |
| `internal/scheduler/` | Lane-based concurrency control (main, subagent, cron, team lanes) with per-session serialization. Per-edition rate limits (`MaxSubagentConcurrent`, `MaxSubagentDepth`) with tenant-scoped concurrency |
Expand Down
2 changes: 2 additions & 0 deletions docs/16-skill-publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

How agents create, register, and manage skills programmatically through the `publish_skill` builtin tool, working in tandem with the `skill-creator` core skill.

**Related:** See [22-skill-hub-installation.md](./22-skill-hub-installation.md) for user-driven skill discovery and CLI installation from public registry or GitHub.

---

## 1. Overview
Expand Down
29 changes: 28 additions & 1 deletion docs/17-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ All notable changes to GoClaw Gateway are documented here. Format follows [Keep
- **Functional options pattern**: Telegram provider refactored to `telegram.New()` with `WithXxxStore()` option setters for cleaner initialization
- **File organization**: Subagent code split into focused modules: `subagent.go`, `subagent_roster.go`, `subagent_spawn.go`. Spawn tool split: `spawn_tool.go` + `spawn_tool_actions.go`

#### Skill Hub: Discovery & Installation (2026-03-31)
- **Skill Hub CLI:** New commands `goclaw skills install`, `remove`, `search` for user-driven skill discovery and lifecycle
- **Registry support:** Curated skill registry at `https://raw.githubusercontent.com/goclaw-hub/registry/main/index.json` with local caching (1-hour TTL)
- **GitHub fetcher:** Tarball download via GitHub API with security hardening (50 MB limit, 500-file limit, path traversal guards, symlink skip)
- **Skill installer:** Multi-stage orchestration (validate→copy→DB→deps→reload) with concurrent safety (advisory locks)
- **Registry client:** JSON-based skill index resolution and fetch with HTTPS enforcement and env override support
- **Installation commands:**
- `goclaw skills install shopee-product-finder` — Install by registry slug
- `goclaw skills install owner/repo` — Install from GitHub directly
- `goclaw skills install owner/repo@v1.0 --ref main` — Install specific ref
- `goclaw skills search <query>` — Search registry index
- `goclaw skills remove <slug>` — Remove installed skill
- **Security features:** Package name validation (stdlib blocklist), SKILL.md content guard, tar bomb prevention, path traversal hardening
- **Versioned storage:** Skills stored in `skills-store/{slug}/{version}/` with version increments on re-install
- **Dependency validation:** Post-install scanning mirrors `publish_skill` tool; warns on missing deps (does not archive)
- **Hot-reload:** BumpVersion() invalidates loader cache; next access loads from filesystem automatically
- **Files added:**
- `internal/skills/github_fetcher.go` — Tarball download + secure extraction
- `internal/skills/registry_client.go` — Registry index fetch + cache
- `internal/skills/installer.go` — Installation orchestrator
- `cmd/skill_install_cmd.go` — `skills install` CLI handler
- `cmd/skill_remove_cmd.go` — `skills remove` CLI handler

#### Runtime & Packages Management (2026-03-17)
- **Packages page**: New "Packages" page in Web UI under System group for managing installed packages
- **HTTP API endpoints**: GET/POST `/v1/packages`, `/v1/packages/install`, `/v1/packages/uninstall`, GET `/v1/packages/runtimes`
Expand Down Expand Up @@ -144,9 +167,13 @@ All notable changes to GoClaw Gateway are documented here. Format follows [Keep

### Documentation

- Added `22-skill-hub-installation.md` — Skill Hub overview, CLI commands, registry architecture, fetcher, installer, security model, error handling
- Updated `16-skill-publishing.md` — Added cross-reference to Skill Hub for user-driven discovery and installation
- Updated `00-architecture-overview.md` — Added Skill Hub components to module map (github_fetcher, registry_client, installer)
- Updated `CLAUDE.md` — Added Skill Hub CLI commands and updated `internal/skills/` description
- Updated `18-http-api.md` — Added section 17 for Runtime & Packages Management endpoints
- Updated `09-security.md` — Added Docker entrypoint documentation, pkg-helper architecture, privilege separation
- Updated `17-changelog.md` — New entries for packages management, Docker security, and auth fix
- Updated `17-changelog.md` — New entries for packages management, Docker security, auth fix, and Skill Hub
- Added `18-http-api.md` — Complete HTTP REST API reference (all endpoints, auth, error codes)
- Added `19-websocket-rpc.md` — Complete WebSocket RPC method catalog (64+ methods, permission matrix)
- Added `20-api-keys-auth.md` — API key authentication, RBAC scopes, security model, usage examples
Expand Down
Loading