Files
kandev/apps/backend/internal/agent/agents/helpers.go
T
2026-02-14 15:54:11 +00:00

130 lines
3.6 KiB
Go

package agents
import (
"bytes"
"context"
"fmt"
"os/exec"
"strings"
"time"
)
// CmdBuilder constructs CLI command slices using a fluent API.
type CmdBuilder struct {
args []string
}
// Cmd starts building a command from a base command and arguments.
func Cmd(base ...string) *CmdBuilder {
return &CmdBuilder{args: append([]string{}, base...)}
}
// Model appends a model flag if model is non-empty.
// flag args support {model} placeholder, e.g. NewParam("--model", "{model}").
func (b *CmdBuilder) Model(flag Param, model string) *CmdBuilder {
if flag.IsEmpty() || model == "" {
return b
}
for _, arg := range flag.args {
b.args = append(b.args, strings.ReplaceAll(arg, "{model}", model))
}
return b
}
// Resume appends a resume flag with sessionID if applicable.
// Skipped when sessionID is empty, nativeResume is true, or flag is empty.
func (b *CmdBuilder) Resume(flag Param, sessionID string, nativeResume bool) *CmdBuilder {
if sessionID == "" || nativeResume || flag.IsEmpty() {
return b
}
b.args = append(b.args, flag.args...)
b.args = append(b.args, sessionID)
return b
}
// Permissions appends per-tool permission flags when not auto-approving.
// Each tool gets: flag tool:ask-user (e.g. --permission launch-process:ask-user).
func (b *CmdBuilder) Permissions(flag string, tools []string, opts CommandOptions) *CmdBuilder {
if opts.AutoApprove || flag == "" || len(tools) == 0 {
return b
}
for _, tool := range tools {
b.args = append(b.args, flag, tool+":ask-user")
}
return b
}
// Settings appends CLI flags for enabled permission settings.
func (b *CmdBuilder) Settings(settings map[string]PermissionSetting, values map[string]bool) *CmdBuilder {
if settings == nil || values == nil {
return b
}
for settingName, setting := range settings {
if !setting.Supported || setting.ApplyMethod != "cli_flag" || setting.CLIFlag == "" {
continue
}
value, exists := values[settingName]
if !exists || !value {
continue
}
if setting.CLIFlagValue != "" {
b.args = append(b.args, setting.CLIFlag, setting.CLIFlagValue)
} else {
b.args = append(b.args, strings.Fields(setting.CLIFlag)...)
}
}
return b
}
// Prompt appends a prompt flag if prompt is non-empty.
// flag args support {prompt} placeholder, e.g. NewParam("--prompt", "{prompt}").
// If flag is empty, the prompt is appended as a positional argument.
func (b *CmdBuilder) Prompt(flag Param, prompt string) *CmdBuilder {
if prompt == "" {
return b
}
if flag.IsEmpty() {
b.args = append(b.args, prompt)
return b
}
for _, arg := range flag.args {
b.args = append(b.args, strings.ReplaceAll(arg, "{prompt}", prompt))
}
return b
}
// Flag appends arbitrary flag parts to the command.
func (b *CmdBuilder) Flag(parts ...string) *CmdBuilder {
b.args = append(b.args, parts...)
return b
}
// Build returns the final Command value.
func (b *CmdBuilder) Build() Command {
return Command{args: b.args}
}
// execAndParse runs a command with a timeout and parses stdout with the given parser function.
func execAndParse(ctx context.Context, timeout time.Duration, parse func(string) ([]Model, error), name string, args ...string) ([]Model, error) {
if timeout <= 0 {
timeout = 30 * time.Second
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("command timed out after %v", timeout)
}
return nil, fmt.Errorf("command failed: %w (stderr: %s)", err, stderr.String())
}
return parse(stdout.String())
}