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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/chzyer/readline"
|
"github.com/chzyer/readline"
|
||||||
|
|
@ -20,6 +23,20 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"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() {
|
func agentCmd() {
|
||||||
message := ""
|
message := ""
|
||||||
sessionKey := "cli:default"
|
sessionKey := "cli:default"
|
||||||
|
|
@ -30,7 +47,7 @@ func agentCmd() {
|
||||||
switch args[i] {
|
switch args[i] {
|
||||||
case "--debug", "-d":
|
case "--debug", "-d":
|
||||||
logger.SetLevel(logger.DEBUG)
|
logger.SetLevel(logger.DEBUG)
|
||||||
fmt.Println("🔍 Debug mode enabled")
|
fmt.Println("Debug mode enabled")
|
||||||
case "-m", "--message":
|
case "-m", "--message":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
message = args[i+1]
|
message = args[i+1]
|
||||||
|
|
@ -90,16 +107,25 @@ func agentCmd() {
|
||||||
}
|
}
|
||||||
fmt.Printf("\n%s %s\n", logo, response)
|
fmt.Printf("\n%s %s\n", logo, response)
|
||||||
} else {
|
} 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)
|
interactiveMode(agentLoop, sessionKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
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{
|
rl, err := readline.NewEx(&readline.Config{
|
||||||
Prompt: prompt,
|
Prompt: chatPrompt,
|
||||||
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
|
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"),
|
||||||
HistoryLimit: 100,
|
HistoryLimit: 100,
|
||||||
InterruptPrompt: "^C",
|
InterruptPrompt: "^C",
|
||||||
|
|
@ -113,6 +139,8 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
}
|
}
|
||||||
defer rl.Close()
|
defer rl.Close()
|
||||||
|
|
||||||
|
hipicoSessionKey := "cli:hipico"
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, err := rl.Readline()
|
line, err := rl.Readline()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -134,21 +162,101 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
switch mode {
|
||||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
case modePico:
|
||||||
if err != nil {
|
if input == "/cmd" {
|
||||||
fmt.Printf("Error: %v\n", err)
|
mode = modeCmd
|
||||||
continue
|
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 <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) {
|
func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
mode := modePico
|
||||||
|
hipicoSessionKey := "cli:hipico"
|
||||||
|
|
||||||
for {
|
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')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
|
@ -169,13 +277,169 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
switch mode {
|
||||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
case modePico:
|
||||||
if err != nil {
|
if input == "/cmd" {
|
||||||
fmt.Printf("Error: %v\n", err)
|
mode = modeCmd
|
||||||
continue
|
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 <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