Learn how to manage and organize commands in a hierarchical structure using Clay's repository system
The repositories package (github.com/go-go-golems/clay/pkg/repositories) provides a flexible way to manage and organize commands in a hierarchical structure, with support for file system watching and dynamic updates.
The repository system is built around a common interface that allows different repository implementations, including:
The system supports:
The repository system is built around the RepositoryInterface which defines the core functionality that all repository implementations must provide:
type RepositoryInterface interface {
// LoadCommands initializes the repository by loading all commands
LoadCommands(helpSystem *help.HelpSystem, options ...cmds.CommandDescriptionOption) error
// Add adds one or more commands to the repository
Add(commands ...cmds.Command)
// Remove removes commands with the given prefixes from the repository
Remove(prefixes ...[]string)
// CollectCommands returns all commands under a given prefix
CollectCommands(prefix []string, recurse bool) []cmds.Command
// GetCommand returns a single command by its full path name
GetCommand(name string) (cmds.Command, bool)
// FindNode returns the TrieNode at the given prefix
FindNode(prefix []string) *trie.TrieNode
// GetRenderNode returns a RenderNode for visualization purposes
GetRenderNode(prefix []string) (*trie.RenderNode, bool)
// ListTools returns all commands as tools for MCP compatibility
ListTools(ctx context.Context, cursor string) ([]mcp.Tool, string, error)
// Watch sets up file system watching for the repository
Watch(ctx context.Context, options ...watcher.Option) error
}
The interface provides several key operations:
Command Management:
LoadCommands: Initialize the repository with commandsAdd: Add new commands to the repositoryRemove: Remove commands by their prefix pathsCommand Retrieval:
CollectCommands: Get all commands under a prefixGetCommand: Find a specific command by its full pathFindNode: Access the underlying trie structureGetRenderNode: Get a visualization-friendly representationTool Integration:
ListTools: Convert commands to MCP-compatible toolsFile System Watching:
Watch: Set up file system watching for dynamic updatesThe repository system supports dynamic updates through file system watching:
// Set up watching with options
err := repo.Watch(ctx,
watcher.WithMask("**/*.yaml"), // Only watch yaml files
watcher.WithBreakOnError(false), // Continue on errors
)
The watcher will:
You can create custom repository implementations by implementing the RepositoryInterface. Common use cases include:
// Example custom repository
type CustomRepository struct {
// Your custom fields
}
// Implement interface methods
func (c *CustomRepository) LoadCommands(helpSystem *help.HelpSystem,
options ...cmds.CommandDescriptionOption) error {
// Your implementation
return nil
}
func (c *CustomRepository) Add(commands ...cmds.Command) {
// Your implementation
}
// ... implement other methods ...
func (c *CustomRepository) Watch(ctx context.Context, options ...watcher.Option) error {
// Implement file system watching if needed
return nil
}
The package provides two main implementations:
Repository: The standard implementation using a trie data structure
repo := repositories.NewRepository(
repositories.WithDirectories(...),
repositories.WithFiles(...),
)
MultiRepository: An implementation that can mount other repositories
mr := repositories.NewMultiRepository()
mr.Mount("/path", someRepository)
import (
"github.com/go-go-golems/clay/pkg/repositories"
"github.com/go-go-golems/glazed/pkg/help"
)
// Create a new repository with both directories and individual files
repo := repositories.NewRepository(
repositories.WithDirectories(repositories.Directory{
FS: os.DirFS("/path/to/commands"),
RootDirectory: ".",
RootDocDirectory: "doc",
WatchDirectory: "/path/to/commands",
Name: "my-commands",
SourcePrefix: "file",
}),
repositories.WithFiles([]string{
"/path/to/specific/command1.yaml",
"/path/to/specific/command2.yaml",
}),
)
// Initialize help system
helpSystem := help.NewHelpSystem()
// Load commands
err := repo.LoadCommands(helpSystem)
if err != nil {
log.Fatal(err)
}
// Set up watching
ctx := context.Background()
go func() {
err := repo.Watch(ctx)
if err != nil {
log.Printf("Watch error: %v", err)
}
}()
You can load commands from both directories and individual files:
repo := repositories.NewRepository(
repositories.WithDirectories(
repositories.Directory{
FS: os.DirFS("/path/to/commands1"),
RootDirectory: ".",
Name: "commands1",
},
),
repositories.WithFiles([]string{
"/path/to/command1.yaml",
"/path/to/command2.yaml",
}),
)
The multi-repository allows mounting multiple repositories under different paths, creating a unified command hierarchy:
import (
"github.com/go-go-golems/clay/pkg/repositories"
)
// Create individual repositories
baseRepo := repositories.NewRepository(...)
toolsRepo := repositories.NewRepository(...)
pluginsRepo := repositories.NewRepository(...)
// Create multi-repository
mr := repositories.NewMultiRepository()
// Mount repositories at different paths
mr.Mount("/", baseRepo) // Root mount
mr.Mount("/tools", toolsRepo) // Tools under /tools
mr.Mount("/plugins", pluginsRepo) // Plugins under /plugins
// Load commands from all repositories
err := mr.LoadCommands(helpSystem)
if err != nil {
log.Fatal(err)
}
// Set up watching for all repositories
go func() {
err := mr.Watch(ctx)
if err != nil {
log.Printf("Watch error: %v", err)
}
}()
Multi-repositories handle paths differently based on mount points:
Root-mounted repositories (/):
my-command stays as my-commandPath-mounted repositories:
build in /tools becomes tools/build// Access commands through their full paths
rootCmd, found := mr.GetCommand("my-command") // From root repository
toolCmd, found := mr.GetCommand("tools/build") // From tools repository
pluginCmd, found := mr.GetCommand("plugins/my-plugin") // From plugins repository
// Collect commands from specific paths
toolCommands := mr.CollectCommands([]string{"tools"}, true) // All commands under /tools
The repository allows you to collect commands by prefix:
// Get all commands from all mounted repositories
allCommands := mr.CollectCommands([]string{}, true)
// Get commands under specific prefix
subCommands := mr.CollectCommands([]string{"group", "subgroup"}, true)
// Get commands without recursion
directCommands := mr.CollectCommands([]string{"group"}, false)
The repository system can watch both directories and individual files for changes and automatically update commands.
import (
"context"
"github.com/go-go-golems/clay/pkg/watcher"
)
ctx := context.Background()
err := repo.Watch(ctx,
watcher.WithMask("**/*.yaml"), // Only watch yaml files
watcher.WithBreakOnError(false), // Continue on errors
)
if err != nil {
log.Fatal(err)
}
The watcher will automatically:
You can customize the watch behavior by providing additional options:
repo.Watch(ctx,
watcher.WithWriteCallback(func(path string) error {
log.Printf("File changed: %s", path)
return nil
}),
watcher.WithRemoveCallback(func(path string) error {
log.Printf("File removed: %s", path)
return nil
}),
)
Commands in a repository are organized in a trie structure, allowing for efficient lookup and hierarchical organization.
Commands are identified by their path components:
// Command at root
repo.InsertCommand([]string{}, rootCommand)
// Command in group
repo.InsertCommand([]string{"group"}, groupCommand)
// Command in nested group
repo.InsertCommand([]string{"group", "subgroup"}, subCommand)
// Find a specific command
cmd, found := repo.GetCommand("group/subgroup/command")
// Find a node in the command tree
node := repo.FindNode([]string{"group", "subgroup"})
// Remove a specific command
removedCmds := repo.Remove([]string{"group", "subgroup", "command"})
// Remove all commands under a prefix
removedCmds := repo.Remove([]string{"group"})
The package provides helper functions for loading commands from multiple sources:
commands, err := repositories.LoadCommandsFromInputs(
commandLoader,
[]string{
"/path/to/command1.yaml",
"/path/to/commands/directory",
},
)
The package includes helpers for integrating with Cobra command-line applications:
rootCmd := &cobra.Command{}
commands, err := repositories.LoadRepositories(
helpSystem,
rootCmd,
[]*repositories.Repository{repo},
// Additional cobra parser options...
)
repo := repositories.NewRepository(
repositories.WithCommandLoader(myLoader),
repositories.WithDirectories(myDirectory),
)
// Load initial commands
err := repo.LoadCommands(helpSystem)
if err != nil {
log.Fatal(err)
}
// Start watching for changes
go func() {
err := repo.Watch(ctx)
if err != nil {
log.Printf("Watch error: %v", err)
}
}()
// Create repositories with different sources
localRepo := repositories.NewRepository(
repositories.WithDirectories(repositories.Directory{
FS: os.DirFS("/local/commands"),
RootDirectory: ".",
}),
)
pluginRepo := repositories.NewRepository(
repositories.WithFiles([]string{
"/plugins/plugin1.yaml",
"/plugins/plugin2.yaml",
}),
)
// Create and configure multi-repository
mr := repositories.NewMultiRepository()
mr.Mount("/", localRepo)
mr.Mount("/plugins", pluginRepo)
// Watch both repositories
go func() {
err := mr.Watch(ctx)
if err != nil {
log.Printf("Watch error: %v", err)
}
}()
This setup creates a repository that automatically reloads commands when files change, making it ideal for development environments or dynamic command systems.
The CommandRepository provides a lightweight, in-memory implementation of the repository interface that focuses solely on command organization without any file system dependencies. It's ideal for scenarios where you just need to manage and organize commands programmatically.
import "github.com/go-go-golems/clay/pkg/repositories"
// Create a basic command repository
repo := repositories.NewCommandRepository()
// Create with a name
repo := repositories.NewCommandRepository(
repositories.WithCommandRepositoryName("my-commands"),
)
The CommandRepository provides two ways to add commands:
// Commands will be organized based on their Description().Parents
repo.Add(command1, command2)
// Add commands under a custom path
repo.AddUnderPath([]string{"group", "subgroup"}, command1, command2)
// Commands will be accessible as "group/subgroup/command1" etc.
Commands are organized in a trie structure just like the full Repository:
// Add commands at different levels
repo.Add(rootCommand) // At root
repo.AddUnderPath([]string{"tools"}, toolCommand) // Under "tools"
repo.AddUnderPath([]string{"tools", "network"}, networkCommand) // Nested
// Retrieve commands
allCommands := repo.CollectCommands([]string{}, true) // All commands
toolCommands := repo.CollectCommands([]string{"tools"}, true) // All tool commands
networkTools := repo.CollectCommands([]string{"tools", "network"}, false) // Direct network tools
The CommandRepository provides:
Pure in-memory storage:
Full path-based organization:
Simple API:
RepositoryInterface compatibility:
The CommandRepository is particularly useful for:
// Create a test repository
testRepo := repositories.NewCommandRepository()
testRepo.Add(mockCommands...)
// Create commands programmatically
repo := repositories.NewCommandRepository()
for _, config := range configs {
cmd := createCommandFromConfig(config)
repo.AddUnderPath([]string{"generated", config.Type}, cmd)
}
// Create a temporary command structure
tempRepo := repositories.NewCommandRepository()
tempRepo.AddUnderPath([]string{"session", sessionID}, sessionCommands...)
// Add plugin commands under their own namespace
pluginRepo := repositories.NewCommandRepository()
for _, plugin := range plugins {
pluginRepo.AddUnderPath([]string{"plugins", plugin.Name}, plugin.Commands...)
}
The main differences from the full Repository implementation are:
No File System Integration:
Simplified Implementation:
Focus on Command Management:
Here's an example of using CommandRepository to build a hierarchical command menu:
// Create the repository
menuRepo := repositories.NewCommandRepository(
repositories.WithCommandRepositoryName("menu"),
)
// Add commands in categories
menuRepo.AddUnderPath([]string{"file"},
newCommand, openCommand, saveCommand)
menuRepo.AddUnderPath([]string{"edit"},
cutCommand, copyCommand, pasteCommand)
menuRepo.AddUnderPath([]string{"view", "zoom"},
zoomInCommand, zoomOutCommand, resetZoomCommand)
// Get commands for a specific menu
fileCommands := menuRepo.CollectCommands([]string{"file"}, false)
zoomCommands := menuRepo.CollectCommands([]string{"view", "zoom"}, false)
// Get all commands
allCommands := menuRepo.CollectCommands([]string{}, true)