2026-03-04 19:17:16 +08:00
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cli
import (
2026-03-24 20:08:36 +08:00
"errors"
"flag"
2026-03-04 19:17:16 +08:00
"fmt"
"os"
2026-03-12 13:33:13 +08:00
"os/signal"
"strconv"
2026-03-04 19:17:16 +08:00
"strings"
2026-03-12 13:33:13 +08:00
"syscall"
2026-03-24 20:08:36 +08:00
2026-03-11 19:14:18 +08:00
"github.com/peterh/liner"
2026-03-24 20:08:36 +08:00
"golang.org/x/term"
"gopkg.in/yaml.v3"
2026-03-04 19:17:16 +08:00
)
2026-03-24 20:08:36 +08:00
// ConfigFile represents the rf.yml configuration file structure
type ConfigFile struct {
Host string ` yaml:"host" `
User string ` yaml:"user" `
Password string ` yaml:"password" `
APIToken string ` yaml:"api_token" `
}
// ConnectionArgs holds the parsed command line arguments
type ConnectionArgs struct {
Host string
Port int
Password string
Key string
Type string
Username string
Command string
ShowHelp bool
}
// LoadDefaultConfigFile reads the rf.yml file from current directory if it exists
func LoadDefaultConfigFile ( ) ( * ConfigFile , error ) {
// Try to read rf.yml from current directory
data , err := os . ReadFile ( "rf.yml" )
if err != nil {
// File doesn't exist, return nil without error
if os . IsNotExist ( err ) {
return nil , nil
}
return nil , err
}
var config ConfigFile
if err = yaml . Unmarshal ( data , & config ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse rf.yml: %v" , err )
}
return & config , nil
}
// LoadConfigFileFromPath reads a config file from the specified path
func LoadConfigFileFromPath ( path string ) ( * ConfigFile , error ) {
data , err := os . ReadFile ( path )
if err != nil {
return nil , fmt . Errorf ( "failed to read config file %s: %v" , path , err )
}
var config ConfigFile
if err = yaml . Unmarshal ( data , & config ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse config file %s: %v" , path , err )
}
return & config , nil
}
// parseHostPort parses a host:port string and returns host and port
func parseHostPort ( hostPort string ) ( string , int , error ) {
if hostPort == "" {
return "" , - 1 , nil
}
// Split host and port
parts := strings . Split ( hostPort , ":" )
if len ( parts ) != 2 {
return "" , - 1 , fmt . Errorf ( "invalid host format, expected host:port, got: %s" , hostPort )
}
host := parts [ 0 ]
port , err := strconv . Atoi ( parts [ 1 ] )
if err != nil {
return "" , - 1 , fmt . Errorf ( "invalid port number: %s" , parts [ 1 ] )
}
return host , port , nil
}
// ParseConnectionArgs parses command line arguments similar to Python's parse_connection_args
func ParseConnectionArgs ( args [ ] string ) ( * ConnectionArgs , error ) {
// First, scan args to check for help and config file
var configFilePath string
var hasOtherFlags bool
for i := 0 ; i < len ( args ) ; i ++ {
arg := args [ i ]
if arg == "--help" {
return & ConnectionArgs { ShowHelp : true } , nil
} else if arg == "-f" && i + 1 < len ( args ) {
configFilePath = args [ i + 1 ]
i ++
} else if strings . HasPrefix ( arg , "-" ) && arg != "-f" {
// Check if it's a flag (not a command)
if arg == "-h" || arg == "-p" || arg == "-w" || arg == "-k" ||
arg == "-u" || arg == "-admin" || arg == "-user" {
hasOtherFlags = true
}
}
}
// Load config file with priority: -f > rf.yml > none
var config * ConfigFile
var err error
if configFilePath != "" {
// User specified config file via -f
config , err = LoadConfigFileFromPath ( configFilePath )
if err != nil {
return nil , err
}
} else {
// Try default rf.yml
config , err = LoadDefaultConfigFile ( )
if err != nil {
return nil , err
}
}
if config != nil {
if hasOtherFlags {
return nil , fmt . Errorf ( "cannot use command line flags (-h, -p, -w, -k, -u, -admin, -user) when using config file. Please use config file or command line flags, not both" )
}
return buildArgsFromConfig ( config , args )
}
// Create a new flag set
fs := flag . NewFlagSet ( "ragflow_cli" , flag . ContinueOnError )
// Define flags
host := fs . String ( "h" , "127.0.0.1" , "Admin or RAGFlow service host" )
port := fs . Int ( "p" , - 1 , "Admin or RAGFlow service port (default: 9381 for admin, 9380 for user)" )
password := fs . String ( "w" , "" , "Superuser password" )
key := fs . String ( "k" , "" , "API key for authentication" )
_ = fs . String ( "f" , "" , "Path to config file (YAML format)" ) // Already parsed above
_ = fs . Bool ( "admin" , false , "Run in admin mode (default)" )
userMode := fs . Bool ( "user" , false , "Run in user mode" )
username := fs . String ( "u" , "" , "Username (email). In admin mode defaults to admin@ragflow.io, in user mode required" )
// Parse the arguments
if err = fs . Parse ( args ) ; err != nil {
return nil , fmt . Errorf ( "failed to parse arguments: %v" , err )
}
// Otherwise, use command line flags
return buildArgsFromFlags ( host , port , password , key , userMode , username , fs . Args ( ) )
}
// buildArgsFromConfig builds ConnectionArgs from config file
func buildArgsFromConfig ( config * ConfigFile , remainingArgs [ ] string ) ( * ConnectionArgs , error ) {
result := & ConnectionArgs { }
// Parse host:port from config file
if config . Host != "" {
host , port , err := parseHostPort ( config . Host )
if err != nil {
return nil , fmt . Errorf ( "invalid host in config file: %v" , err )
}
result . Host = host
result . Port = port
} else {
result . Host = "127.0.0.1"
}
// Apply auth info from config
result . Username = config . User
result . Password = config . Password
result . Key = config . APIToken
// Determine mode: if config has auth info, use user mode
if config . User != "" || config . APIToken != "" {
result . Type = "user"
} else {
result . Type = "admin"
result . Username = "admin@ragflow.io"
}
// Set default port if not specified in config
if result . Port == - 1 {
if result . Type == "admin" {
result . Port = 9381
} else {
result . Port = 9380
}
}
// Get command from remaining args (no need for quotes or semicolon)
if len ( remainingArgs ) > 0 {
result . Command = strings . Join ( remainingArgs , " " ) + ";"
}
return result , nil
}
// buildArgsFromFlags builds ConnectionArgs from command line flags
func buildArgsFromFlags ( host * string , port * int , password * string , key * string , userMode * bool , username * string , remainingArgs [ ] string ) ( * ConnectionArgs , error ) {
result := & ConnectionArgs {
Host : * host ,
Port : * port ,
Password : * password ,
Key : * key ,
Username : * username ,
}
// Determine mode
if * userMode {
result . Type = "user"
} else {
result . Type = "admin"
}
// Set default port based on type if not specified
if result . Port == - 1 {
if result . Type == "admin" {
result . Port = 9383
} else {
result . Port = 9384
}
}
// Determine username based on mode
if result . Type == "admin" && result . Username == "" {
result . Username = "admin@ragflow.io"
}
// Get command from remaining args (no need for quotes or semicolon)
if len ( remainingArgs ) > 0 {
result . Command = strings . Join ( remainingArgs , " " ) + ";"
}
return result , nil
}
// PrintUsage prints the CLI usage information
func PrintUsage ( ) {
fmt . Println ( ` RAGFlow CLI Client
Usage : ragflow_cli [ options ] [ command ]
Options :
- h string Admin or RAGFlow service host ( default "127.0.0.1" )
- p int Admin or RAGFlow service port ( default 9381 for admin , 9380 for user )
- w string Superuser password
- k string API key for authentication
- f string Path to config file ( YAML format )
- admin Run in admin mode ( default )
- user Run in user mode
- u string Username ( email ) . In admin mode defaults to admin @ ragflow . io
-- help Show this help message
Configuration File :
The CLI will automatically read rf . yml from the current directory if it exists .
Use - f to specify a custom config file path .
Config file format :
host : 127.0 .0 .1 : 9380
user : your - email @ example . com
password : your - password
api_token : your - api - token
When using a config file , you cannot use other command line flags except - help .
The command line is only for the SQL command .
Commands :
SQL commands like : LOGIN USER ' email ' ; LIST USERS ; etc .
` )
}
2026-03-11 19:14:18 +08:00
// HistoryFile returns the path to the history file
func HistoryFile ( ) string {
return os . Getenv ( "HOME" ) + "/" + historyFileName
}
const historyFileName = ".ragflow_cli_history"
2026-03-04 19:17:16 +08:00
// CLI represents the command line interface
type CLI struct {
client * RAGFlowClient
prompt string
running bool
2026-03-11 19:14:18 +08:00
line * liner . State
2026-03-24 20:08:36 +08:00
args * ConnectionArgs
2026-03-04 19:17:16 +08:00
}
// NewCLI creates a new CLI instance
func NewCLI ( ) ( * CLI , error ) {
2026-03-24 20:08:36 +08:00
return NewCLIWithArgs ( nil )
}
// NewCLIWithArgs creates a new CLI instance with connection arguments
func NewCLIWithArgs ( args * ConnectionArgs ) ( * CLI , error ) {
2026-03-11 19:14:18 +08:00
// Create liner first
line := liner . NewLiner ( )
2026-03-24 20:08:36 +08:00
// Determine server type
serverType := "user"
if args != nil && args . Type != "" {
serverType = args . Type
}
2026-03-11 19:14:18 +08:00
// Create client with password prompt using liner
2026-03-24 20:08:36 +08:00
client := NewRAGFlowClient ( serverType )
2026-03-11 19:14:18 +08:00
client . PasswordPrompt = line . PasswordPrompt
2026-03-24 20:08:36 +08:00
// Apply connection arguments if provided
client . HTTPClient . Host = args . Host
client . HTTPClient . Port = args . Port
// Set prompt based on server type
prompt := "RAGFlow> "
if serverType == "admin" {
prompt = "RAGFlow(admin)> "
}
2026-03-04 19:17:16 +08:00
return & CLI {
2026-03-24 20:08:36 +08:00
prompt : prompt ,
2026-03-11 19:14:18 +08:00
client : client ,
line : line ,
2026-03-24 20:08:36 +08:00
args : args ,
2026-03-04 19:17:16 +08:00
} , nil
}
// Run starts the interactive CLI
func ( c * CLI ) Run ( ) error {
2026-03-24 20:08:36 +08:00
if c . args . Type == "admin" {
// Allow 3 attempts for password verification
maxAttempts := 3
for attempt := 1 ; attempt <= maxAttempts ; attempt ++ {
var input string
var err error
// Check if terminal supports password masking
if term . IsTerminal ( int ( os . Stdin . Fd ( ) ) ) {
input , err = c . line . PasswordPrompt ( "Please input your password: " )
} else {
// Terminal doesn't support password masking, use regular prompt
fmt . Println ( "Warning: This terminal does not support secure password input" )
input , err = c . line . Prompt ( "Please input your password (will be visible): " )
}
if err != nil {
fmt . Printf ( "Error reading input: %v\n" , err )
return err
}
input = strings . TrimSpace ( input )
if input == "" {
if attempt < maxAttempts {
fmt . Println ( "Password cannot be empty, please try again" )
continue
}
return errors . New ( "no password provided after 3 attempts" )
}
// Set the password for verification
c . args . Password = input
if err = c . VerifyAuth ( ) ; err != nil {
if attempt < maxAttempts {
fmt . Printf ( "Authentication failed: %v (%d/%d attempts)\n" , err , attempt , maxAttempts )
continue
}
return fmt . Errorf ( "authentication failed after %d attempts: %v" , maxAttempts , err )
}
// Authentication successful
break
}
}
2026-03-04 19:17:16 +08:00
c . running = true
2026-03-11 19:14:18 +08:00
// 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 ( )
} ( )
2026-03-04 19:17:16 +08:00
fmt . Println ( "Welcome to RAGFlow CLI" )
fmt . Println ( "Type \\? for help, \\q to quit" )
fmt . Println ( )
for c . running {
2026-03-11 19:14:18 +08:00
input , err := c . line . Prompt ( c . prompt )
if err != nil {
fmt . Printf ( "Error reading input: %v\n" , err )
continue
2026-03-04 19:17:16 +08:00
}
input = strings . TrimSpace ( input )
if input == "" {
continue
}
2026-03-11 19:14:18 +08:00
// Add to history (skip meta commands)
if ! strings . HasPrefix ( input , "\\" ) {
c . line . AppendHistory ( input )
}
2026-03-24 20:08:36 +08:00
if err = c . execute ( input ) ; err != nil {
fmt . Printf ( "CLI error: %v\n" , err )
2026-03-04 19:17:16 +08:00
}
}
2026-03-11 19:14:18 +08:00
return nil
2026-03-04 19:17:16 +08:00
}
func ( c * CLI ) execute ( input string ) error {
p := NewParser ( input )
cmd , err := p . Parse ( )
if err != nil {
return err
}
if cmd == nil {
return nil
}
// Handle meta commands
if cmd . Type == "meta" {
return c . handleMetaCommand ( cmd )
}
// Execute the command using the client
2026-03-24 20:08:36 +08:00
var result ResponseIf
result , err = c . client . ExecuteCommand ( cmd )
if result != nil {
result . PrintOut ( )
}
2026-03-04 19:17:16 +08:00
return err
}
func ( c * CLI ) handleMetaCommand ( cmd * Command ) error {
command := cmd . Params [ "command" ] . ( string )
2026-03-12 13:33:13 +08:00
args , _ := cmd . Params [ "args" ] . ( [ ] string )
2026-03-04 19:17:16 +08:00
switch command {
case "q" , "quit" , "exit" :
fmt . Println ( "Goodbye!" )
c . running = false
case "?" , "h" , "help" :
c . printHelp ( )
case "c" , "clear" :
// Clear screen (simple approach)
fmt . Print ( "\033[H\033[2J" )
2026-03-12 13:33:13 +08:00
case "admin" :
c . client . ServerType = "admin"
c . prompt = "RAGFlow(admin)> "
2026-03-24 20:08:36 +08:00
fmt . Println ( "Switched to ADMIN mode" )
2026-03-12 13:33:13 +08:00
case "user" :
c . client . ServerType = "user"
c . prompt = "RAGFlow> "
2026-03-24 20:08:36 +08:00
fmt . Println ( "Switched to USER mode" )
2026-03-12 13:33:13 +08:00
case "host" :
if len ( args ) == 0 {
fmt . Printf ( "Current host: %s\n" , c . client . HTTPClient . Host )
} else {
c . client . HTTPClient . Host = args [ 0 ]
fmt . Printf ( "Host set to: %s\n" , args [ 0 ] )
}
case "port" :
if len ( args ) == 0 {
fmt . Printf ( "Current port: %d\n" , c . client . HTTPClient . Port )
} else {
port , err := strconv . Atoi ( args [ 0 ] )
if err != nil {
return fmt . Errorf ( "invalid port number: %s" , args [ 0 ] )
}
if port < 1 || port > 65535 {
return fmt . Errorf ( "port must be between 1 and 65535" )
}
c . client . HTTPClient . Port = port
fmt . Printf ( "Port set to: %d\n" , port )
}
case "status" :
fmt . Printf ( "Server: %s:%d (mode: %s)\n" , c . client . HTTPClient . Host , c . client . HTTPClient . Port , c . client . ServerType )
2026-03-04 19:17:16 +08:00
default :
return fmt . Errorf ( "unknown meta command: \\%s" , command )
}
return nil
}
func ( c * CLI ) printHelp ( ) {
help := `
RAGFlow CLI Help
== == == == == == == ==
2026-03-12 13:33:13 +08:00
Meta Commands :
\ admin - Switch to ADMIN mode ( port 9381 )
\ user - Switch to USER mode ( port 9380 )
\ host [ ip ] - Show or set server host ( default : 127.0 .0 .1 )
\ port [ num ] - Show or set server port ( default : 9380 for user , 9381 for admin )
\ status - Show current connection status
\ ? or \ h - Show this help
\ q or \ quit - Exit CLI
\ c or \ clear - Clear screen
2026-03-24 20:08:36 +08:00
Commands ( User Mode ) :
2026-03-04 19:17:16 +08:00
LOGIN USER ' email ' ; - Login as user
REGISTER USER ' name ' AS ' nickname ' PASSWORD ' pwd ' ; - Register new user
SHOW VERSION ; - Show version info
2026-03-12 13:33:13 +08:00
PING ; - Ping server
LIST DATASETS ; - List user datasets
LIST AGENTS ; - List user agents
LIST CHATS ; - List user chats
LIST MODEL PROVIDERS ; - List model providers
LIST DEFAULT MODELS ; - List default models
2026-03-24 20:08:36 +08:00
LIST TOKENS ; - List API tokens
CREATE TOKEN ; - Create new API token
DROP TOKEN ' token_value ' ; - Delete an API token
SET TOKEN ' token_value ' ; - Set and validate API token
SHOW TOKEN ; - Show current API token
UNSET TOKEN ; - Remove current API token
Commands ( Admin Mode ) :
2026-03-04 19:17:16 +08:00
LIST USERS ; - List all users
2026-03-12 13:33:13 +08:00
SHOW USER ' email ' ; - Show user details
CREATE USER ' email ' ' password ' ; - Create new user
DROP USER ' email ' ; - Delete user
ALTER USER PASSWORD ' email ' ' new_password ' ; - Change user password
ALTER USER ACTIVE ' email ' on / off ; - Activate / deactivate user
GRANT ADMIN ' email ' ; - Grant admin role
REVOKE ADMIN ' email ' ; - Revoke admin role
2026-03-04 19:17:16 +08:00
LIST SERVICES ; - List services
2026-03-12 13:33:13 +08:00
SHOW SERVICE < id > ; - Show service details
2026-03-04 19:17:16 +08:00
PING ; - Ping server
... and many more
Meta Commands :
2026-03-11 19:14:18 +08:00
\ ? or \ h - Show this help
\ q or \ quit - Exit CLI
\ c or \ clear - Clear screen
2026-03-04 19:17:16 +08:00
For more information , see documentation .
`
fmt . Println ( help )
}
// Cleanup performs cleanup before exit
func ( c * CLI ) Cleanup ( ) {
fmt . Println ( "\nCleaning up..." )
}
2026-03-12 13:33:13 +08:00
// RunInteractive runs the CLI in interactive mode
func RunInteractive ( ) error {
cli , err := NewCLI ( )
if err != nil {
return fmt . Errorf ( "failed to create CLI: %v" , err )
}
// Handle interrupt signal
sigChan := make ( chan os . Signal , 1 )
signal . Notify ( sigChan , syscall . SIGINT , syscall . SIGTERM )
go func ( ) {
<- sigChan
cli . Cleanup ( )
os . Exit ( 0 )
} ( )
return cli . Run ( )
}
2026-03-24 20:08:36 +08:00
// RunSingleCommand executes a single command and exits
func ( c * CLI ) RunSingleCommand ( command string ) error {
// Execute the command
if err := c . execute ( command ) ; err != nil {
return err
}
return nil
}
// VerifyAuth verifies authentication if needed
func ( c * CLI ) VerifyAuth ( ) error {
if c . args == nil {
return nil
}
if c . args . Username == "" {
return fmt . Errorf ( "username is required" )
}
if c . args . Password == "" {
return fmt . Errorf ( "password is required" )
}
// Create login command with username and password
cmd := NewCommand ( "login_user" )
cmd . Params [ "email" ] = c . args . Username
cmd . Params [ "password" ] = c . args . Password
_ , err := c . client . ExecuteCommand ( cmd )
return err
}