diff --git a/cmd/picoclaw/cmd_agent.go b/cmd/picoclaw/cmd_agent.go index 8658c9d32..b67bfd734 100644 --- a/cmd/picoclaw/cmd_agent.go +++ b/cmd/picoclaw/cmd_agent.go @@ -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 } - ctx := context.Background() - response, err := agentLoop.ProcessDirect(ctx, input, sessionKey) - if err != nil { - fmt.Printf("Error: %v\n", err) - continue - } + 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 + } - fmt.Printf("\n%s %s\n\n", logo, response) + 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 ") + 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 { - fmt.Printf("%s You: ", logo) + 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 } - ctx := context.Background() - response, err := agentLoop.ProcessDirect(ctx, input, sessionKey) - if err != nil { - fmt.Printf("Error: %v\n", err) - continue - } + switch mode { + case modePico: + if input == "/cmd" { + mode = modeCmd + fmt.Println("Switched to command mode. Type /pico to return to chat.") + continue + } - fmt.Printf("\n%s %s\n\n", logo, response) + 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 ") + 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 +}