cmd

package
v1.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultMaxIterations int = 20
	DefaultMaxRetries    int = 5
)

Variables

View Source
var RootCmd = &cobra.Command{
	Use:   "chat-agent",
	Short: "Chat Agent CLI tool",
	Long:  `A command line interface for llm agent`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if err := logger.Init(); err != nil {
			return err
		}

		cfg, err := config.LoadConfig(configPath)
		if err != nil {
			return err
		}

		availableChats = cfg.Chats

		chatName, _ := cmd.Flags().GetString("chat")
		debug, _ := cmd.Flags().GetBool("debug")

		if chatName == "" {
			for name, item := range cfg.Chats {
				if item.Default {
					chatName = name
					break
				}
			}
		}
		if chatName == "" {
			return fmt.Errorf("Please specify the chat")
		}
		currentChatName = chatName

		cwd, err := os.Getwd()
		if err != nil {
			cwd = "."
		}
		sessionID := filepath.Base(cwd)
		if sessionID == "" || sessionID == "/" {
			sessionID = "cli-root"
		}

		session, err := chatbot.InitChatSession(cmd.Context(), cfg, chatName, sessionID, debug)
		if err != nil {
			return err
		}
		defer func() {
			if session != nil {
				if err := session.Close(); err != nil {
					fmt.Printf("Error closing session: %v\n", err)
				}
			}
		}()

		placeholder := "Send a message (/h for help)"
		homeDir, err := os.UserHomeDir()
		if err != nil {
			return err
		}
		historyPath := filepath.Join(homeDir, ".chat-agent", "history")
		scanner, err := readline.New(readline.Prompt{
			Prompt:         ">>> ",
			AltPrompt:      "... ",
			Placeholder:    placeholder,
			AltPlaceholder: `Use """ to end multi-line input`,
		}, readline.WithHistoryFile(historyPath))
		if err != nil {
			return err
		}
		scanner.UnsetRawMode()
		fmt.Print(readline.StartBracketedPaste)
		defer fmt.Printf(readline.EndBracketedPaste)

		welcome, _ := cmd.Flags().GetString("welcome")
		fmt.Printf("%s\n", welcome)

		msgCount := session.GetMessageCount()
		if msgCount > 0 {
			fmt.Printf("[Context restored from previous session: %d messages]\n", msgCount)
		}

		persistenceStore := session.PersistenceStore()
		cb := chatbot.NewChatBot(context.WithValue(cmd.Context(), "debug", debug), session.Agent, session.Manager, scanner, persistenceStore)

		// ignore ctrl+c and break llm generate
		var chatCancel context.CancelFunc = func() {}
		sigChan := make(chan os.Signal, 1)
		signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
		go func() {
			for {
				<-sigChan
				chatCancel()
			}
		}()

		chatctx, cancel := context.WithCancel(cmd.Context())
		chatCancel = cancel
		if startAt != "" {
			err = cb.StreamChat(chatctx, startAt)
			if err != nil {
				os.Stderr.WriteString("\nerror: " + err.Error() + "\n")
				return nil
			}
		} else if once != "" {

			err = cb.StreamChat(chatctx, once)
			if err != nil {
				os.Stderr.WriteString("\nerror: " + err.Error() + "\n")
			}
			return nil
		}

		// chat loop
		var sb strings.Builder
		var multiline MultilineState
		for {
			if scanner.Prompt.Placeholder != placeholder {
				scanner.Prompt.Placeholder = placeholder
				scanner.HistoryEnable()
			}
			line, err := scanner.Readline()
			switch {
			case errors.Is(err, io.EOF):
				fmt.Println()
				return nil
			case errors.Is(err, readline.ErrInterrupt):
				if line == "" {
					fmt.Println("\nUse Ctrl + d or /q to exit.")
				}

				scanner.Prompt.UseAlt = false
				sb.Reset()

				continue
			case err != nil:
				return err
			}

			switch {
			case multiline != MultilineNone:

				before, ok := strings.CutSuffix(line, `"""`)
				sb.WriteString(before)
				if !ok {
					fmt.Fprintln(&sb)
					continue
				}
				multiline = MultilineNone
				scanner.Prompt.UseAlt = false
			case strings.HasPrefix(line, `"""`):
				line := strings.TrimPrefix(line, `"""`)
				line, ok := strings.CutSuffix(line, `"""`)
				sb.WriteString(line)
				if !ok {

					fmt.Fprintln(&sb)
					multiline = MultilinePrompt
					scanner.Prompt.UseAlt = true
				}
			case scanner.Pasting:
				fmt.Fprintln(&sb, line)
				continue
			default:
				sb.WriteString(line)
			}

			if sb.Len() > 0 && multiline == MultilineNone {
				chatctx, cancel := context.WithCancel(cmd.Context())
				chatCancel = cancel
				input := strings.TrimSpace(sb.String())

				if !disableLocalCommand && strings.HasPrefix(input, "/t ") {
					localcmd := strings.TrimSpace(strings.TrimPrefix(input, "/t"))
					if err := utils.PopenStream(chatctx, localcmd); err != nil {
						os.Stderr.WriteString("exec local cmd error: " + err.Error() + "\n")
					}
					sb.Reset()
					continue
				}

				if strings.HasPrefix(input, "/s ") {
					targetName := strings.TrimSpace(strings.TrimPrefix(input, "/s"))
					if targetName == "" {
						printChats()
					} else if newSession, err := switchChat(cmd.Context(), cfg, targetName, debug, session, sessionID); err != nil {
						fmt.Printf("Error switching chat: %v\n", err)
					} else {
						session = newSession
						currentChatName = targetName
						persistenceStore := session.PersistenceStore()
						cb = chatbot.NewChatBot(context.WithValue(cmd.Context(), "debug", debug), session.Agent, session.Manager, scanner, persistenceStore)
						fmt.Printf("Switched to chat: %s\n", targetName)
					}
					sb.Reset()
					continue
				}

				switch input {
				case "/help", "/h":
					printHelp()
				case "/clear", "/c":
					session.Clear()
					fmt.Println("The conversation context is cleared")
				case "/redo", "/r":
					lastMsg := session.GetLastUserMessage()
					if lastMsg == "" {
						fmt.Println("No previous user message to redo")
					} else {
						session.RemoveLastRound()
						fmt.Printf("Redoing last message: %s\n", lastMsg)
						chatctx, cancel := context.WithCancel(cmd.Context())
						chatCancel = cancel
						err = cb.StreamChat(chatctx, lastMsg)
						session, cb = handleStreamError(err, cmd.Context(), cfg, debug, session, sessionID, scanner, cb)
					}
				case "/keep", "/k":
					if err := session.OnKeep(); err != nil {
						fmt.Printf("Error executing keep hook: %v\n", err)
					} else {
						fmt.Println("Session keep hook executed successfully")
					}
				case "/history", "/i":
					os.Stdout.WriteString(session.Manager.GetSummary())
					fmt.Println()
				case "/debug-context":
					for _, msg := range session.Manager.GetMessages() {
						fmt.Println(msg)
						fmt.Println("===")
					}
				case "/tools", "/l":
					printTools(session.Tools)
				case "/chat":
					printChats()
				case "/quit", "/exit", "/bye", "/q":
					os.Stdout.WriteString("bye!\n")
					return nil
				default:
					err = cb.StreamChat(chatctx, input)
					session, cb = handleStreamError(err, cmd.Context(), cfg, debug, session, sessionID, scanner, cb)
				}
				sb.Reset()
			}
		}
	},
}

RootCmd represents the base command when called without any subcommands

Functions

func AccessLogMiddleware

func AccessLogMiddleware(next http.Handler) http.Handler

AccessLogMiddleware logs each HTTP request in a combined-log-like format. If basic auth is active, the authenticated username is included.

func BasicAuthMiddleware

func BasicAuthMiddleware(credentials map[string]string) func(http.Handler) http.Handler

BasicAuthMiddleware creates a middleware for HTTP Basic Authentication. It accepts a map of username->password pairs and authenticates against any of them. On successful auth, the username is stored in the request context under authUserKey.

func Execute

func Execute()

Types

type ApprovalItem

type ApprovalItem struct {
	Approved bool   `json:"approved"`
	Reason   string `json:"reason,omitempty"`
}

ApprovalItem represents a single approval result

type ApprovalResponsePayload

type ApprovalResponsePayload struct {
	ApprovalID string                  `json:"approval_id"`
	Results    map[string]ApprovalItem `json:"results"`
}

ApprovalResponsePayload represents the approval response from the client

type ChatRequest

type ChatRequest struct {
	ChatName string        `json:"chat_name"`
	Message  string        `json:"message"`
	Files    []FilePayload `json:"files,omitempty"`
}

type ChatState

type ChatState struct {
	ChatSession *chatbot.ChatSession
	ChatBot     *chatbot.ChatBot
}

ChatState represents the state of a single chat within a session

type FilePayload

type FilePayload struct {
	URL      string `json:"url"`
	Type     string `json:"type"`
	Name     string `json:"name"`
	FileSize int64  `json:"file_size,omitempty"`
}

FilePayload represents a file in the chat request

type MultilineState

type MultilineState int
const (
	MultilineNone MultilineState = iota
	MultilinePrompt
)

type SessionInfo

type SessionInfo struct {
	ID        string
	ChatName  string                // Current active chat
	Chats     map[string]*ChatState // All chats in this session
	CreatedAt time.Time
}

type SessionManager

type SessionManager struct {
	// contains filtered or unexported fields
}

SessionManager manages chat sessions

func NewSessionManager

func NewSessionManager(cfg *config.Config) *SessionManager

func (*SessionManager) AddSession

func (sm *SessionManager) AddSession(sessionID string, chatName string, chatSession *chatbot.ChatSession)

func (*SessionManager) CloseAllSessions

func (sm *SessionManager) CloseAllSessions()

func (*SessionManager) GetChatState

func (sm *SessionManager) GetChatState(sessionID string, chatName string) (*ChatState, bool)

GetChatState gets the chat state for a specific chat in a session

func (*SessionManager) GetSession

func (sm *SessionManager) GetSession(sessionID string) (*SessionInfo, bool)

func (*SessionManager) RemoveSession

func (sm *SessionManager) RemoveSession(sessionID string)

func (*SessionManager) UpdateChatSessionWithBot

func (sm *SessionManager) UpdateChatSessionWithBot(sessionID string, chatName string, chatSession *chatbot.ChatSession, chatBot *chatbot.ChatBot)

UpdateChatSessionWithBot updates session with both ChatSession and ChatBot for a specific chat

type WebSocketHandler

type WebSocketHandler struct {
	// contains filtered or unexported fields
}

WebSocketHandler handles WebSocket connections

func NewWebSocketHandler

func NewWebSocketHandler(cfg *config.Config) *WebSocketHandler

NewWebSocketHandler creates a new WebSocket handler

func (*WebSocketHandler) CloseAllSessions

func (h *WebSocketHandler) CloseAllSessions()

func (*WebSocketHandler) HandleWebSocket

func (h *WebSocketHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request)

HandleWebSocket handles a WebSocket connection

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL