feat: add command mode with /cmd, /pico, /hipico, /byepico commands
Add three interactive modes to the CLI agent: - /cmd: switch to command mode for direct shell command execution - /pico: switch back to AI chat mode - /hipico <msg>: invoke AI assistance from within command mode - /byepico: end AI assistance and return to command mode Command mode supports cd with directory tracking, cross-platform shell execution (sh on Unix, PowerShell on Windows), and contextual AI assistance that knows the current working directory. https://claude.ai/code/session_01PoHBfe7eKmhHvVF12W3xZ2
This commit is contained in:
parent
7cbfa89a96
commit
49237b3b59
1 changed files with 283 additions and 19 deletions
|
|
@ -5,11 +5,14 @@ package main
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
|
|
@ -20,6 +23,20 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
)
|
||||
|
||||
// Interactive mode identifiers
|
||||
const (
|
||||
modePico = "pico" // Chat mode (default) - input goes to AI agent
|
||||
modeCmd = "cmd" // Command mode - input executed as shell commands
|
||||
modeHiPico = "hipico" // AI-assisted mode within cmd - multi-turn AI conversation
|
||||
)
|
||||
|
||||
// cmdWorkingDir tracks the current working directory for command mode.
|
||||
var cmdWorkingDir string
|
||||
|
||||
func init() {
|
||||
cmdWorkingDir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
func agentCmd() {
|
||||
message := ""
|
||||
sessionKey := "cli:default"
|
||||
|
|
@ -30,7 +47,7 @@ func agentCmd() {
|
|||
switch args[i] {
|
||||
case "--debug", "-d":
|
||||
logger.SetLevel(logger.DEBUG)
|
||||
fmt.Println("🔍 Debug mode enabled")
|
||||
fmt.Println("Debug mode enabled")
|
||||
case "-m", "--message":
|
||||
if i+1 < len(args) {
|
||||
message = args[i+1]
|
||||
|
|
@ -90,16 +107,25 @@ func agentCmd() {
|
|||
}
|
||||
fmt.Printf("\n%s %s\n", logo, response)
|
||||
} else {
|
||||
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n\n", logo)
|
||||
fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n", logo)
|
||||
fmt.Println(" /cmd - switch to command mode")
|
||||
fmt.Println(" /pico - switch to chat mode")
|
||||
fmt.Println(" /hipico - AI assistance in command mode")
|
||||
fmt.Println(" /byepico - end AI assistance")
|
||||
fmt.Println()
|
||||
interactiveMode(agentLoop, sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
prompt := fmt.Sprintf("%s You: ", logo)
|
||||
chatPrompt := fmt.Sprintf("%s You: ", logo)
|
||||
cmdPrompt := "$ "
|
||||
hipicoPrompt := fmt.Sprintf("%s> ", logo)
|
||||
|
||||
mode := modePico
|
||||
|
||||
rl, err := readline.NewEx(&readline.Config{
|
||||
Prompt: prompt,
|
||||
Prompt: chatPrompt,
|
||||
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
|
||||
HistoryLimit: 100,
|
||||
InterruptPrompt: "^C",
|
||||
|
|
@ -113,6 +139,8 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||
}
|
||||
defer rl.Close()
|
||||
|
||||
hipicoSessionKey := "cli:hipico"
|
||||
|
||||
for {
|
||||
line, err := rl.Readline()
|
||||
if err != nil {
|
||||
|
|
@ -134,21 +162,101 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||
return
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case modePico:
|
||||
if input == "/cmd" {
|
||||
mode = modeCmd
|
||||
rl.SetPrompt(cmdPrompt)
|
||||
fmt.Println("Switched to command mode. Type /pico to return to chat.")
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s %s\n\n", logo, response)
|
||||
|
||||
case modeCmd:
|
||||
if input == "/pico" {
|
||||
mode = modePico
|
||||
rl.SetPrompt(chatPrompt)
|
||||
fmt.Println("Switched to chat mode. Type /cmd to return to command mode.")
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(input, "/hipico") {
|
||||
initialMsg := strings.TrimSpace(strings.TrimPrefix(input, "/hipico"))
|
||||
if initialMsg == "" {
|
||||
fmt.Println("Usage: /hipico <message>")
|
||||
fmt.Println("Example: /hipico check the log files for error messages")
|
||||
continue
|
||||
}
|
||||
|
||||
mode = modeHiPico
|
||||
rl.SetPrompt(hipicoPrompt)
|
||||
|
||||
contextPrefix := fmt.Sprintf("[Command mode context: working directory is %s]\n\n", cmdWorkingDir)
|
||||
|
||||
fmt.Printf("\n%s AI assistance started. Type /byepico to end.\n\n", logo)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, contextPrefix+initialMsg, hipicoSessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
mode = modeCmd
|
||||
rl.SetPrompt(cmdPrompt)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s %s\n\n", logo, response)
|
||||
continue
|
||||
}
|
||||
|
||||
executeShellCommand(input)
|
||||
|
||||
case modeHiPico:
|
||||
if input == "/byepico" {
|
||||
mode = modeCmd
|
||||
rl.SetPrompt(cmdPrompt)
|
||||
fmt.Println("AI assistance ended. Back to command mode.")
|
||||
continue
|
||||
}
|
||||
|
||||
if input == "/pico" {
|
||||
mode = modePico
|
||||
rl.SetPrompt(chatPrompt)
|
||||
fmt.Println("AI assistance ended. Switched to chat mode.")
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, hipicoSessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n%s %s\n\n", logo, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
mode := modePico
|
||||
hipicoSessionKey := "cli:hipico"
|
||||
|
||||
for {
|
||||
switch mode {
|
||||
case modePico:
|
||||
fmt.Printf("%s You: ", logo)
|
||||
case modeCmd:
|
||||
fmt.Print("$ ")
|
||||
case modeHiPico:
|
||||
fmt.Printf("%s> ", logo)
|
||||
}
|
||||
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
|
|
@ -169,13 +277,169 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||
return
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case modePico:
|
||||
if input == "/cmd" {
|
||||
mode = modeCmd
|
||||
fmt.Println("Switched to command mode. Type /pico to return to chat.")
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n%s %s\n\n", logo, response)
|
||||
|
||||
case modeCmd:
|
||||
if input == "/pico" {
|
||||
mode = modePico
|
||||
fmt.Println("Switched to chat mode. Type /cmd to return to command mode.")
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(input, "/hipico") {
|
||||
initialMsg := strings.TrimSpace(strings.TrimPrefix(input, "/hipico"))
|
||||
if initialMsg == "" {
|
||||
fmt.Println("Usage: /hipico <message>")
|
||||
fmt.Println("Example: /hipico check the log files for error messages")
|
||||
continue
|
||||
}
|
||||
|
||||
mode = modeHiPico
|
||||
contextPrefix := fmt.Sprintf("[Command mode context: working directory is %s]\n\n", cmdWorkingDir)
|
||||
fmt.Printf("\n%s AI assistance started. Type /byepico to end.\n\n", logo)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, contextPrefix+initialMsg, hipicoSessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
mode = modeCmd
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s %s\n\n", logo, response)
|
||||
continue
|
||||
}
|
||||
|
||||
executeShellCommand(input)
|
||||
|
||||
case modeHiPico:
|
||||
if input == "/byepico" {
|
||||
mode = modeCmd
|
||||
fmt.Println("AI assistance ended. Back to command mode.")
|
||||
continue
|
||||
}
|
||||
|
||||
if input == "/pico" {
|
||||
mode = modePico
|
||||
fmt.Println("AI assistance ended. Switched to chat mode.")
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, hipicoSessionKey)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n%s %s\n\n", logo, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeShellCommand runs a shell command in the current working directory
|
||||
// and prints the output. It also handles the cd command to change directories.
|
||||
func executeShellCommand(input string) {
|
||||
// Handle cd command specially to update working directory
|
||||
if strings.HasPrefix(input, "cd ") || input == "cd" {
|
||||
handleCd(input)
|
||||
return
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", input)
|
||||
} else {
|
||||
cmd = exec.Command("sh", "-c", input)
|
||||
}
|
||||
cmd.Dir = cmdWorkingDir
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
|
||||
if stdout.Len() > 0 {
|
||||
fmt.Print(stdout.String())
|
||||
if !strings.HasSuffix(stdout.String(), "\n") {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
fmt.Print(stderr.String())
|
||||
if !strings.HasSuffix(stderr.String(), "\n") {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
fmt.Printf("Exit code: %d\n", exitErr.ExitCode())
|
||||
} else {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleCd handles the cd command to change the working directory for command mode.
|
||||
func handleCd(input string) {
|
||||
parts := strings.Fields(input)
|
||||
var target string
|
||||
|
||||
if len(parts) < 2 {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
target = home
|
||||
} else {
|
||||
target = parts[1]
|
||||
}
|
||||
|
||||
// Handle ~ expansion
|
||||
if strings.HasPrefix(target, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if target == "~" {
|
||||
target = home
|
||||
} else if len(target) > 1 && target[1] == '/' {
|
||||
target = filepath.Join(home, target[2:])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle relative paths
|
||||
if !filepath.IsAbs(target) {
|
||||
target = filepath.Join(cmdWorkingDir, target)
|
||||
}
|
||||
|
||||
target = filepath.Clean(target)
|
||||
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
fmt.Printf("cd: %v\n", err)
|
||||
return
|
||||
}
|
||||
if !info.IsDir() {
|
||||
fmt.Printf("cd: %s: Not a directory\n", target)
|
||||
return
|
||||
}
|
||||
|
||||
cmdWorkingDir = target
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue