diff --git a/go.mod b/go.mod index 139d21f861..3a47d85eb4 100644 --- a/go.mod +++ b/go.mod @@ -40,10 +40,12 @@ require ( github.com/leodido/go-urn v1.2.4 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.3 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/peterh/liner v1.2.2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index eb856996c4..23b9cdd0d1 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -84,6 +86,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -152,6 +156,7 @@ golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcs golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 14edea1b2c..dd88c6c6a3 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -17,57 +17,91 @@ package cli import ( - "bufio" "fmt" "os" "strings" + "github.com/peterh/liner" ) +// HistoryFile returns the path to the history file +func HistoryFile() string { + return os.Getenv("HOME") + "/" + historyFileName +} + +const historyFileName = ".ragflow_cli_history" + // CLI represents the command line interface type CLI struct { - parser *Parser client *RAGFlowClient prompt string running bool + line *liner.State } // NewCLI creates a new CLI instance func NewCLI() (*CLI, error) { + // Create liner first + line := liner.NewLiner() + + // Create client with password prompt using liner + client := NewRAGFlowClient("user") // Default to user mode + client.PasswordPrompt = line.PasswordPrompt + return &CLI{ prompt: "RAGFlow> ", - client: NewRAGFlowClient("user"), // Default to user mode + client: client, + line: line, }, nil } // Run starts the interactive CLI func (c *CLI) Run() error { c.running = true - scanner := bufio.NewScanner(os.Stdin) + + // Load history from file + histFile := HistoryFile() + if f, err := os.Open(histFile); err == nil { + c.line.ReadHistory(f) + f.Close() + } + + // Save history on exit + defer func() { + if f, err := os.Create(histFile); err == nil { + c.line.WriteHistory(f) + f.Close() + } + c.line.Close() + }() fmt.Println("Welcome to RAGFlow CLI") fmt.Println("Type \\? for help, \\q to quit") fmt.Println() for c.running { - fmt.Print(c.prompt) - - if !scanner.Scan() { - break + input, err := c.line.Prompt(c.prompt) + if err != nil { + fmt.Printf("Error reading input: %v\n", err) + continue } - input := scanner.Text() input = strings.TrimSpace(input) if input == "" { continue } + // Add to history (skip meta commands) + if !strings.HasPrefix(input, "\\") { + c.line.AppendHistory(input) + } + if err := c.execute(input); err != nil { fmt.Printf("Error: %v\n", err) } } - return scanner.Err() + return nil } func (c *CLI) execute(input string) error { @@ -125,9 +159,9 @@ SQL Commands: ... and many more Meta Commands: - \\? or \\h - Show this help - \\q or \\quit - Exit CLI - \\c or \\clear - Clear screen + \? or \h - Show this help + \q or \quit - Exit CLI + \c or \clear - Clear screen For more information, see documentation. ` diff --git a/internal/cli/client.go b/internal/cli/client.go index d4cd8dc2c3..99c97ba151 100644 --- a/internal/cli/client.go +++ b/internal/cli/client.go @@ -26,10 +26,14 @@ import ( "unsafe" ) +// PasswordPromptFunc is a function type for password input +type PasswordPromptFunc func(prompt string) (string, error) + // RAGFlowClient handles API interactions with the RAGFlow server type RAGFlowClient struct { - HTTPClient *HTTPClient - ServerType string // "admin" or "user" + HTTPClient *HTTPClient + ServerType string // "admin" or "user" + PasswordPrompt PasswordPromptFunc // Function for password input } // NewRAGFlowClient creates a new RAGFlow client @@ -61,10 +65,20 @@ func (c *RAGFlowClient) LoginUser(cmd *Command) error { } // Get password from user input (hidden) - fmt.Printf("password for %s: ", email) - password, err := readPassword() - if err != nil { - return fmt.Errorf("failed to read password: %w", err) + var password string + if c.PasswordPrompt != nil { + pwd, err := c.PasswordPrompt(fmt.Sprintf("password for %s: ", email)) + if err != nil { + return fmt.Errorf("failed to read password: %w", err) + } + password = pwd + } else { + fmt.Printf("password for %s: ", email) + pwd, err := readPassword() + if err != nil { + return fmt.Errorf("failed to read password: %w", err) + } + password = pwd } password = strings.TrimSpace(password)