feat(commands): add /exec command and ! prefix shell execution
Register /exec as a built-in command so shell commands can be run from pico (AI) mode without switching sessions. Also makes ! a universal shell-exec prefix: unknown !foo commands fall back to /exec instead of passing through to the agent. - pkg/commands/cmd_exec.go: new /exec command definition - pkg/commands/runtime.go: add ExecCmd callback to Runtime - pkg/commands/builtin.go: register execCommand() - pkg/commands/executor.go: ! prefix fallback to /exec for unknown commands - pkg/agent/loop.go: wire ExecCmd callback in buildCommandsRuntime() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a93f5a80cd
commit
d17233e195
5 changed files with 59 additions and 0 deletions
|
|
@ -1957,6 +1957,14 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt
|
||||||
return agent.TotalPromptTokens.Load(), agent.TotalCompletionTokens.Load(), agent.TotalRequests.Load()
|
return agent.TotalPromptTokens.Load(), agent.TotalCompletionTokens.Load(), agent.TotalRequests.Load()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Shell command execution — wraps executeCmdMode with session context
|
||||||
|
ExecCmd: func(ctx context.Context, command string) (string, error) {
|
||||||
|
if agent == nil {
|
||||||
|
return "", fmt.Errorf("no agent available")
|
||||||
|
}
|
||||||
|
return al.executeCmdMode(ctx, agent, command, opts.SessionKey, opts.Channel, opts.ChatID)
|
||||||
|
},
|
||||||
|
|
||||||
// One-shot AI query for /hipico (stays in modeCmd, separate session key)
|
// One-shot AI query for /hipico (stays in modeCmd, separate session key)
|
||||||
RunOneShot: func(ctx context.Context, message string) (string, error) {
|
RunOneShot: func(ctx context.Context, message string) (string, error) {
|
||||||
if agent == nil {
|
if agent == nil {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ func BuiltinDefinitions() []Definition {
|
||||||
cmdModeCommand(),
|
cmdModeCommand(),
|
||||||
picoModeCommand(),
|
picoModeCommand(),
|
||||||
hipicoCmnd(),
|
hipicoCmnd(),
|
||||||
|
execCommand(),
|
||||||
editCommand(),
|
editCommand(),
|
||||||
// Info and session management
|
// Info and session management
|
||||||
usageCommand(),
|
usageCommand(),
|
||||||
|
|
|
||||||
38
pkg/commands/cmd_exec.go
Normal file
38
pkg/commands/cmd_exec.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// execCommand registers /exec (alias: !) for running shell commands.
|
||||||
|
// Available in both pico and cmd mode; in cmd mode bare text is rewritten to /exec
|
||||||
|
// before dispatch so all shell execution flows through this single handler.
|
||||||
|
func execCommand() Definition {
|
||||||
|
return Definition{
|
||||||
|
Name: "exec",
|
||||||
|
Description: "Execute a shell command",
|
||||||
|
Usage: "/exec <command>",
|
||||||
|
Handler: func(ctx context.Context, req Request, rt *Runtime) error {
|
||||||
|
if rt == nil || rt.ExecCmd == nil {
|
||||||
|
return req.Reply(unavailableMsg)
|
||||||
|
}
|
||||||
|
// Strip the leading command token ("/exec", "!exec", or "!" fallback)
|
||||||
|
// and treat the remainder as the shell command to run.
|
||||||
|
args := strings.TrimSpace(req.Text)
|
||||||
|
if idx := strings.IndexAny(args, " \t"); idx >= 0 {
|
||||||
|
args = strings.TrimSpace(args[idx:])
|
||||||
|
} else {
|
||||||
|
args = ""
|
||||||
|
}
|
||||||
|
if args == "" {
|
||||||
|
return req.Reply("Usage: /exec <command>\nExample: /exec ls -la")
|
||||||
|
}
|
||||||
|
result, err := rt.ExecCmd(ctx, args)
|
||||||
|
if err != nil {
|
||||||
|
return req.Reply("Error: " + err.Error())
|
||||||
|
}
|
||||||
|
return req.Reply(result)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package commands
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Outcome int
|
type Outcome int
|
||||||
|
|
@ -44,6 +45,14 @@ func (e *Executor) Execute(ctx context.Context, req Request) ExecuteResult {
|
||||||
|
|
||||||
def, found := e.reg.Lookup(cmdName)
|
def, found := e.reg.Lookup(cmdName)
|
||||||
if !found {
|
if !found {
|
||||||
|
// "!foo" where foo is not a registered command → fall back to /exec.
|
||||||
|
// This makes ! a universal shell-exec prefix: !ls, !git status, etc.
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(req.Text), "!") {
|
||||||
|
if execDef, ok := e.reg.Lookup("exec"); ok {
|
||||||
|
req.Text = "/exec " + strings.TrimSpace(req.Text)[1:]
|
||||||
|
return e.executeDefinition(ctx, req, execDef)
|
||||||
|
}
|
||||||
|
}
|
||||||
return ExecuteResult{Outcome: OutcomePassthrough, Command: cmdName}
|
return ExecuteResult{Outcome: OutcomePassthrough, Command: cmdName}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ type Runtime struct {
|
||||||
// Token and model usage stats
|
// Token and model usage stats
|
||||||
GetTokenUsage func() (promptTokens, completionTokens, requests int64)
|
GetTokenUsage func() (promptTokens, completionTokens, requests int64)
|
||||||
|
|
||||||
|
// Shell command execution — delegates to the loop's executeCmdMode with proper path/session handling
|
||||||
|
ExecCmd func(ctx context.Context, command string) (string, error)
|
||||||
|
|
||||||
// One-shot AI query from cmd mode (/hipico)
|
// One-shot AI query from cmd mode (/hipico)
|
||||||
RunOneShot func(ctx context.Context, message string) (string, error)
|
RunOneShot func(ctx context.Context, message string) (string, error)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue