Skip to content

Commit ee7a4d9

Browse files
committed
Add the please command
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
1 parent f93dca4 commit ee7a4d9

5 files changed

Lines changed: 503 additions & 0 deletions

File tree

cmd/root/please.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package root
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
"github.com/docker/cagent/pkg/shortcuts"
9+
"github.com/docker/cagent/pkg/telemetry"
10+
)
11+
12+
// NewPleaseCmd creates a new please command for running shortcuts
13+
func NewPleaseCmd() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "please <shortcut-name> [message|-]",
16+
Short: "Run an agent using a registered shortcut",
17+
Long: `Run an agent using a previously registered shortcut name.
18+
This is equivalent to running 'cagent run <agent-path>' but using a shortcut name instead.
19+
20+
Examples:
21+
# Run a shortcut without additional message (interactive mode)
22+
cagent please code
23+
24+
# Run a shortcut with a message
25+
cagent please code "Write a hello world function"
26+
27+
# Run a shortcut with stdin
28+
echo "Write tests" | cagent please code -
29+
30+
# List available shortcuts
31+
cagent register list`,
32+
Args: cobra.MinimumNArgs(1),
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
telemetry.TrackCommand("please", args)
35+
36+
shortcutName := args[0]
37+
38+
// Load shortcuts
39+
s, err := shortcuts.Load()
40+
if err != nil {
41+
return fmt.Errorf("failed to load shortcuts: %w", err)
42+
}
43+
44+
// Resolve shortcut to agent path
45+
agentPath, ok := s.Get(shortcutName)
46+
if !ok {
47+
return fmt.Errorf("shortcut '%s' not found\n\nList available shortcuts with: cagent register list\nRegister a new shortcut with: cagent register <name> <agent-path>", shortcutName)
48+
}
49+
50+
// Build new args for run command: replace shortcut name with agent path
51+
runArgs := make([]string, len(args))
52+
runArgs[0] = agentPath
53+
copy(runArgs[1:], args[1:])
54+
55+
// Execute run command with the resolved agent path
56+
return doRunCommand(cmd.Context(), runArgs, false)
57+
},
58+
}
59+
60+
// Inherit all flags from run command
61+
runCmd := NewRunCmd()
62+
cmd.Flags().AddFlagSet(runCmd.PersistentFlags())
63+
64+
return cmd
65+
}

cmd/root/register.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package root
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"sort"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
11+
"github.com/docker/cagent/pkg/shortcuts"
12+
"github.com/docker/cagent/pkg/telemetry"
13+
)
14+
15+
// NewRegisterCmd creates a new register command for managing shortcuts
16+
func NewRegisterCmd() *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "register [<shortcut-name> <agent-path>]",
19+
Short: "Manage shortcuts for agents",
20+
Long: `Register and manage shortcuts for agent configurations or catalog references.
21+
22+
When called without arguments, lists all registered shortcuts.`,
23+
Example: ` # List all registered shortcuts (default)
24+
cagent register
25+
26+
# Register a shortcut to a catalog agent
27+
cagent register code agentcatalog/notion-expert
28+
29+
# Register a shortcut to a local agent file
30+
cagent register myagent ~/myagent.yaml
31+
32+
# List all registered shortcuts
33+
cagent register list
34+
35+
# Remove a shortcut
36+
cagent register remove code`,
37+
Args: cobra.RangeArgs(0, 2),
38+
RunE: func(cmd *cobra.Command, args []string) error {
39+
// Default to list when no arguments provided
40+
if len(args) == 0 {
41+
telemetry.TrackCommand("register", []string{"list"})
42+
return listRegisteredShortcuts()
43+
}
44+
45+
// Register new shortcut with 2 arguments
46+
if len(args) == 2 {
47+
telemetry.TrackCommand("register", args)
48+
return registerShortcut(args[0], args[1])
49+
}
50+
51+
// Single argument is invalid in this context
52+
return fmt.Errorf("invalid arguments: use 'register list' or 'register <name> <agent-path>'")
53+
},
54+
}
55+
56+
cmd.AddCommand(newRegisterListCmd())
57+
cmd.AddCommand(newRegisterRemoveCmd())
58+
59+
return cmd
60+
}
61+
62+
// newRegisterListCmd creates the list subcommand
63+
func newRegisterListCmd() *cobra.Command {
64+
return &cobra.Command{
65+
Use: "list",
66+
Short: "List all registered shortcuts",
67+
Args: cobra.NoArgs,
68+
RunE: func(cmd *cobra.Command, args []string) error {
69+
telemetry.TrackCommand("register", []string{"list"})
70+
return listRegisteredShortcuts()
71+
},
72+
}
73+
}
74+
75+
// newRegisterRemoveCmd creates the remove subcommand
76+
func newRegisterRemoveCmd() *cobra.Command {
77+
return &cobra.Command{
78+
Use: "remove <shortcut-name>",
79+
Short: "Remove a registered shortcut",
80+
Args: cobra.ExactArgs(1),
81+
RunE: func(cmd *cobra.Command, args []string) error {
82+
telemetry.TrackCommand("register", append([]string{"remove"}, args...))
83+
return removeShortcut(args[0])
84+
},
85+
}
86+
}
87+
88+
// registerShortcut registers a new shortcut
89+
func registerShortcut(name, agentPath string) error {
90+
// Load existing shortcuts
91+
s, err := shortcuts.Load()
92+
if err != nil {
93+
return fmt.Errorf("failed to load shortcuts: %w", err)
94+
}
95+
96+
// Expand tilde in path if it's a local file path
97+
if strings.HasPrefix(agentPath, "~/") {
98+
if abs, err := expandTilde(agentPath); err == nil {
99+
agentPath = abs
100+
}
101+
}
102+
103+
// Store the shortcut
104+
s.Set(name, agentPath)
105+
106+
// Save to file
107+
if err := s.Save(); err != nil {
108+
return fmt.Errorf("failed to save shortcuts: %w", err)
109+
}
110+
111+
fmt.Printf("Shortcut '%s' registered successfully\n", name)
112+
fmt.Printf(" Shortcut: %s\n", name)
113+
fmt.Printf(" Agent: %s\n", agentPath)
114+
fmt.Printf("\nYou can now run: cagent please %s\n", name)
115+
116+
return nil
117+
}
118+
119+
// removeShortcut removes a shortcut
120+
func removeShortcut(name string) error {
121+
s, err := shortcuts.Load()
122+
if err != nil {
123+
return fmt.Errorf("failed to load shortcuts: %w", err)
124+
}
125+
126+
if !s.Delete(name) {
127+
return fmt.Errorf("shortcut '%s' not found", name)
128+
}
129+
130+
if err := s.Save(); err != nil {
131+
return fmt.Errorf("failed to save shortcuts: %w", err)
132+
}
133+
134+
fmt.Printf("Shortcut '%s' removed successfully\n", name)
135+
return nil
136+
}
137+
138+
func listRegisteredShortcuts() error {
139+
s, err := shortcuts.Load()
140+
if err != nil {
141+
return fmt.Errorf("failed to load shortcuts: %w", err)
142+
}
143+
144+
allShortcuts := s.List()
145+
if len(allShortcuts) == 0 {
146+
fmt.Println("No shortcuts registered.")
147+
fmt.Println("\nRegister a shortcut with: cagent register <name> <agent-path>")
148+
return nil
149+
}
150+
151+
fmt.Printf("Registered shortcuts (%d):\n\n", len(allShortcuts))
152+
153+
// Sort shortcuts by name for consistent output
154+
names := make([]string, 0, len(allShortcuts))
155+
for name := range allShortcuts {
156+
names = append(names, name)
157+
}
158+
sort.Strings(names)
159+
160+
// Find max name length for alignment
161+
maxLen := 0
162+
for _, name := range names {
163+
if len(name) > maxLen {
164+
maxLen = len(name)
165+
}
166+
}
167+
168+
for _, name := range names {
169+
path := allShortcuts[name]
170+
padding := strings.Repeat(" ", maxLen-len(name))
171+
fmt.Printf(" %s%s → %s\n", name, padding, path)
172+
}
173+
174+
fmt.Printf("\nRun a shortcut with: cagent please <name>\n")
175+
176+
return nil
177+
}
178+
179+
// expandTilde expands the tilde in a path to the user's home directory
180+
func expandTilde(path string) (string, error) {
181+
if !strings.HasPrefix(path, "~/") {
182+
return path, nil
183+
}
184+
185+
homeDir, err := filepath.Abs(filepath.Join(filepath.Dir(path), ".."))
186+
if err != nil {
187+
return "", err
188+
}
189+
190+
// Get actual home directory
191+
homeDir, err = filepath.EvalSymlinks(homeDir)
192+
if err != nil {
193+
homeDir, err = filepath.Abs("~")
194+
if err != nil {
195+
return "", err
196+
}
197+
}
198+
199+
return filepath.Join(homeDir, strings.TrimPrefix(path, "~/")), nil
200+
}

cmd/root/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ func NewRootCmd() *cobra.Command {
123123
cmd.AddCommand(NewCatalogCmd())
124124
cmd.AddCommand(NewBuildCmd())
125125
cmd.AddCommand(NewPrintCmd())
126+
cmd.AddCommand(NewRegisterCmd())
127+
cmd.AddCommand(NewPleaseCmd())
126128

127129
return cmd
128130
}

pkg/shortcuts/shortcuts.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package shortcuts
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/goccy/go-yaml"
9+
10+
"github.com/docker/cagent/pkg/paths"
11+
)
12+
13+
// Shortcuts represents the shortcuts configuration
14+
type Shortcuts struct {
15+
Shortcuts map[string]string `yaml:"shortcuts"`
16+
}
17+
18+
// GetShortcutsFilePath returns the path to the shortcuts file
19+
func GetShortcutsFilePath() string {
20+
return filepath.Join(paths.GetConfigDir(), "shortcuts.yaml")
21+
}
22+
23+
// Load loads shortcuts from the configuration file
24+
func Load() (*Shortcuts, error) {
25+
return LoadFrom(GetShortcutsFilePath())
26+
}
27+
28+
// LoadFrom loads shortcuts from a specific file path
29+
func LoadFrom(path string) (*Shortcuts, error) {
30+
// If file doesn't exist, return empty shortcuts
31+
if _, err := os.Stat(path); os.IsNotExist(err) {
32+
return &Shortcuts{Shortcuts: make(map[string]string)}, nil
33+
}
34+
35+
data, err := os.ReadFile(path)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to read shortcuts file: %w", err)
38+
}
39+
40+
var s Shortcuts
41+
if err := yaml.Unmarshal(data, &s); err != nil {
42+
return nil, fmt.Errorf("failed to parse shortcuts file: %w", err)
43+
}
44+
45+
if s.Shortcuts == nil {
46+
s.Shortcuts = make(map[string]string)
47+
}
48+
49+
return &s, nil
50+
}
51+
52+
// Save saves shortcuts to the configuration file
53+
func (s *Shortcuts) Save() error {
54+
return s.SaveTo(GetShortcutsFilePath())
55+
}
56+
57+
// SaveTo saves shortcuts to a specific file path
58+
func (s *Shortcuts) SaveTo(path string) error {
59+
// Ensure directory exists
60+
dir := filepath.Dir(path)
61+
if err := os.MkdirAll(dir, 0o755); err != nil {
62+
return fmt.Errorf("failed to create directory: %w", err)
63+
}
64+
65+
data, err := yaml.Marshal(s)
66+
if err != nil {
67+
return fmt.Errorf("failed to marshal shortcuts: %w", err)
68+
}
69+
70+
if err := os.WriteFile(path, data, 0o644); err != nil {
71+
return fmt.Errorf("failed to write shortcuts file: %w", err)
72+
}
73+
74+
return nil
75+
}
76+
77+
// Get retrieves the agent path for a shortcut
78+
func (s *Shortcuts) Get(name string) (string, bool) {
79+
path, ok := s.Shortcuts[name]
80+
return path, ok
81+
}
82+
83+
// Set creates or updates a shortcut
84+
func (s *Shortcuts) Set(name, agentPath string) {
85+
s.Shortcuts[name] = agentPath
86+
}
87+
88+
// Delete removes a shortcut
89+
func (s *Shortcuts) Delete(name string) bool {
90+
if _, exists := s.Shortcuts[name]; exists {
91+
delete(s.Shortcuts, name)
92+
return true
93+
}
94+
return false
95+
}
96+
97+
// List returns all shortcuts
98+
func (s *Shortcuts) List() map[string]string {
99+
return s.Shortcuts
100+
}

0 commit comments

Comments
 (0)