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 (
|
|
|
|
|
"fmt"
|
2026-04-07 11:30:09 +08:00
|
|
|
"math"
|
2026-03-04 19:17:16 +08:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Parser implements a recursive descent parser for RAGFlow CLI commands
|
|
|
|
|
type Parser struct {
|
|
|
|
|
lexer *Lexer
|
|
|
|
|
curToken Token
|
|
|
|
|
peekToken Token
|
2026-06-09 15:22:50 +08:00
|
|
|
original string
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewParser creates a new parser
|
|
|
|
|
func NewParser(input string) *Parser {
|
|
|
|
|
l := NewLexer(input)
|
2026-06-09 15:22:50 +08:00
|
|
|
p := &Parser{lexer: l, original: input}
|
2026-03-04 19:17:16 +08:00
|
|
|
// Read two tokens to initialize curToken and peekToken
|
|
|
|
|
p.nextToken()
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) nextToken() {
|
|
|
|
|
p.curToken = p.peekToken
|
|
|
|
|
p.peekToken = p.lexer.NextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse parses the input and returns a Command
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) Parse(cliMode CommandLineMode) (*Command, error) {
|
2026-03-04 19:17:16 +08:00
|
|
|
if p.curToken.Type == TokenEOF {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for meta commands (backslash commands)
|
|
|
|
|
if p.curToken.Type == TokenIdentifier && strings.HasPrefix(p.curToken.Value, "\\") {
|
|
|
|
|
return p.parseMetaCommand()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
return p.parseCommand(cliMode)
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseMetaCommand() (*Command, error) {
|
|
|
|
|
cmd := NewCommand("meta")
|
|
|
|
|
cmdName := strings.TrimPrefix(p.curToken.Value, "\\")
|
|
|
|
|
cmd.Params["command"] = strings.ToLower(cmdName)
|
|
|
|
|
|
|
|
|
|
// Parse arguments
|
|
|
|
|
var args []string
|
|
|
|
|
p.nextToken()
|
|
|
|
|
for p.curToken.Type != TokenEOF {
|
|
|
|
|
args = append(args, p.curToken.Value)
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["args"] = args
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 21:39:14 +08:00
|
|
|
func (p *Parser) parseAdminCommand() (*Command, error) {
|
|
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenLogin:
|
|
|
|
|
return p.parseAdminLoginUser()
|
2026-03-26 11:54:23 +08:00
|
|
|
case TokenLogout:
|
|
|
|
|
return p.parseAdminLogout()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenPing:
|
|
|
|
|
return p.parseAdminPingServer()
|
|
|
|
|
case TokenList:
|
|
|
|
|
return p.parseAdminListCommand()
|
|
|
|
|
case TokenShow:
|
|
|
|
|
return p.parseAdminShowCommand()
|
|
|
|
|
case TokenCreate:
|
|
|
|
|
return p.parseAdminCreateCommand()
|
|
|
|
|
case TokenDrop:
|
|
|
|
|
return p.parseAdminDropCommand()
|
|
|
|
|
case TokenAlter:
|
|
|
|
|
return p.parseAdminAlterCommand()
|
|
|
|
|
case TokenGrant:
|
|
|
|
|
return p.parseAdminGrantCommand()
|
|
|
|
|
case TokenRevoke:
|
|
|
|
|
return p.parseAdminRevokeCommand()
|
|
|
|
|
case TokenSet:
|
|
|
|
|
return p.parseAdminSetCommand()
|
|
|
|
|
case TokenUnset:
|
|
|
|
|
return p.parseAdminUnsetCommand()
|
|
|
|
|
case TokenReset:
|
|
|
|
|
return p.parseAdminResetCommand()
|
|
|
|
|
case TokenGenerate:
|
|
|
|
|
return p.parseAdminGenerateCommand()
|
|
|
|
|
case TokenImport:
|
|
|
|
|
return p.parseAdminImportCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenRetrieve:
|
|
|
|
|
return p.parseAdminRetrieveCommand()
|
2026-03-25 21:39:14 +08:00
|
|
|
case TokenParse:
|
|
|
|
|
return p.parseAdminParseCommand()
|
|
|
|
|
case TokenBenchmark:
|
|
|
|
|
return p.parseAdminBenchmarkCommand()
|
|
|
|
|
case TokenRegister:
|
|
|
|
|
return p.parseAdminRegisterCommand()
|
|
|
|
|
case TokenStartup:
|
|
|
|
|
return p.parseAdminStartupCommand()
|
|
|
|
|
case TokenShutdown:
|
|
|
|
|
return p.parseAdminShutdownCommand()
|
|
|
|
|
case TokenRestart:
|
|
|
|
|
return p.parseAdminRestartCommand()
|
2026-05-25 14:00:08 +08:00
|
|
|
case TokenStart:
|
|
|
|
|
return p.parseStartIngestion()
|
|
|
|
|
case TokenStop:
|
|
|
|
|
return p.parseStopIngestion()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenAdd:
|
|
|
|
|
return p.parseAdminAddCommand()
|
|
|
|
|
case TokenDelete:
|
|
|
|
|
return p.parseAdminDeleteCommand()
|
|
|
|
|
case TokenSave:
|
|
|
|
|
return p.parseAdminSaveCommand()
|
2026-03-25 21:39:14 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown command: %s", p.curToken.Value)
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseUserCommand() (*Command, error) {
|
2026-03-04 19:17:16 +08:00
|
|
|
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenLogin:
|
|
|
|
|
return p.parseLoginUser()
|
2026-03-26 11:54:23 +08:00
|
|
|
case TokenLogout:
|
|
|
|
|
return p.parseLogout()
|
2026-03-04 19:17:16 +08:00
|
|
|
case TokenPing:
|
|
|
|
|
return p.parsePingServer()
|
|
|
|
|
case TokenList:
|
|
|
|
|
return p.parseListCommand()
|
|
|
|
|
case TokenShow:
|
|
|
|
|
return p.parseShowCommand()
|
|
|
|
|
case TokenCreate:
|
|
|
|
|
return p.parseCreateCommand()
|
|
|
|
|
case TokenDrop:
|
|
|
|
|
return p.parseDropCommand()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenAdd:
|
|
|
|
|
return p.parseAddCommand()
|
|
|
|
|
case TokenDelete:
|
|
|
|
|
return p.parseDeleteCommand()
|
2026-03-04 19:17:16 +08:00
|
|
|
case TokenAlter:
|
|
|
|
|
return p.parseAlterCommand()
|
|
|
|
|
case TokenGrant:
|
|
|
|
|
return p.parseGrantCommand()
|
|
|
|
|
case TokenRevoke:
|
|
|
|
|
return p.parseRevokeCommand()
|
|
|
|
|
case TokenSet:
|
|
|
|
|
return p.parseSetCommand()
|
2026-03-24 20:08:36 +08:00
|
|
|
case TokenUnset:
|
|
|
|
|
return p.parseUnsetCommand()
|
2026-03-04 19:17:16 +08:00
|
|
|
case TokenReset:
|
|
|
|
|
return p.parseResetCommand()
|
|
|
|
|
case TokenGenerate:
|
|
|
|
|
return p.parseGenerateCommand()
|
|
|
|
|
case TokenImport:
|
|
|
|
|
return p.parseImportCommand()
|
2026-04-01 16:16:25 +08:00
|
|
|
case TokenInsert:
|
|
|
|
|
return p.parseInsertCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenRetrieve:
|
|
|
|
|
return p.parseRetrieveCommand()
|
2026-03-04 19:17:16 +08:00
|
|
|
case TokenParse:
|
|
|
|
|
return p.parseParseCommand()
|
|
|
|
|
case TokenBenchmark:
|
|
|
|
|
return p.parseBenchmarkCommand()
|
|
|
|
|
case TokenRegister:
|
|
|
|
|
return p.parseRegisterCommand()
|
|
|
|
|
case TokenStartup:
|
|
|
|
|
return p.parseStartupCommand()
|
|
|
|
|
case TokenShutdown:
|
|
|
|
|
return p.parseShutdownCommand()
|
|
|
|
|
case TokenRestart:
|
|
|
|
|
return p.parseRestartCommand()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenEnable:
|
|
|
|
|
return p.parseEnableCommand()
|
|
|
|
|
case TokenDisable:
|
|
|
|
|
return p.parseDisableCommand()
|
2026-04-21 16:52:32 +08:00
|
|
|
case TokenStream:
|
|
|
|
|
return p.parseStreamCommand()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenChat:
|
|
|
|
|
return p.parseChatCommand()
|
2026-04-03 18:11:23 +08:00
|
|
|
case TokenThink:
|
|
|
|
|
return p.parseThinkCommand()
|
2026-05-09 17:41:54 +08:00
|
|
|
case TokenEmbed:
|
|
|
|
|
return p.parseEmbedCommand()
|
|
|
|
|
case TokenRerank:
|
|
|
|
|
return p.parseRerankCommand()
|
2026-05-12 17:17:44 +08:00
|
|
|
case TokenASR:
|
|
|
|
|
return p.parseASRCommand()
|
|
|
|
|
case TokenTTS:
|
|
|
|
|
return p.parseTTSCommand()
|
|
|
|
|
case TokenOCR:
|
|
|
|
|
return p.parseOCRCommand()
|
2026-04-23 10:16:20 +08:00
|
|
|
case TokenCheck:
|
|
|
|
|
return p.parseCheckCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenSave:
|
|
|
|
|
return p.parseUserSaveCommand()
|
2026-04-02 20:20:35 +08:00
|
|
|
case TokenUse:
|
|
|
|
|
return p.parseUseCommand()
|
2026-04-07 09:44:51 +08:00
|
|
|
case TokenUpdate:
|
|
|
|
|
return p.parseUpdateCommand()
|
|
|
|
|
case TokenRemove:
|
|
|
|
|
return p.parseRemoveCommand()
|
2026-05-20 20:32:06 +08:00
|
|
|
case TokenGet:
|
|
|
|
|
return p.parseGetCommand()
|
Go: implement TTS for MiniMax provider and CLI testing for TTS (#14911)
### What problem does this PR solve?
This PR implement TTS for MiniMax provider and CLI testing for TTS
**The following functionalities are now supported:**
**MiniMax:**
- [x] Chat / Stream Chat
- [x] Embedding
- [x] Rerank
- [x] Model listing
- [x] Provider connection checking
- [x] Text To Speech
- [ ] OCRFile
- [ ] ~~Audio To Text~~
- [ ] ~~Balance~~
**Verified examples from the CLI:**
```plaintext
RAGFlow(user)> tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
RAGFlow(user)> stream tts with 'speech-2.8-hd@test@minimax' text 'He who desires but acts not, breeds pestilence.' play format 'wav' save './internal' param '{"voice_setting": {"voice_id": "English_radiant_girl", "speed": 1, "vol": 1, "pitch": 0}, "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "wav", "channel": 1}, "output_format": "hex"}'
Saved to directory: /home/infiniflow/Documents/development/ragflow/internal/speech-2.8-hd_output.wav
SUCCESS
```
Set `Play` to play audio in CLI
Set `Save` `PATH_TO_SAVE` to save file
Set `format` to save file in wav or mp3
Set `Param` align with official request body
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
2026-05-14 13:19:31 +08:00
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
case TokenLS, TokenCat, TokenSearch:
|
|
|
|
|
// For context engine
|
|
|
|
|
return p.parseContextEngineCommand()
|
2026-03-04 19:17:16 +08:00
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown command: %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) parseCommand(cliMode CommandLineMode) (*Command, error) {
|
2026-03-25 21:39:14 +08:00
|
|
|
if p.curToken.Type != TokenIdentifier && !isKeyword(p.curToken.Type) {
|
|
|
|
|
return nil, fmt.Errorf("expected command, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
switch cliMode {
|
|
|
|
|
case AdminMode:
|
2026-03-25 21:39:14 +08:00
|
|
|
return p.parseAdminCommand()
|
2026-06-09 15:22:50 +08:00
|
|
|
case APIMode:
|
|
|
|
|
return p.parseUserCommand()
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unknown mode: %s", cliMode)
|
2026-03-25 21:39:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
func (p *Parser) expectPeek(tokenType int) error {
|
|
|
|
|
if p.peekToken.Type != tokenType {
|
|
|
|
|
return fmt.Errorf("expected %s, got %s", tokenTypeToString(tokenType), p.peekToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) expectSemicolon() error {
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if p.peekToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("expected semicolon")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isKeyword(tokenType int) bool {
|
2026-05-25 14:00:08 +08:00
|
|
|
return tokenType >= TokenLogin && tokenType <= TokenPanic
|
2026-03-04 19:17:16 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:36:03 +08:00
|
|
|
// isCECommand checks if the given string is a Filesystem command
|
2026-03-26 21:07:06 +08:00
|
|
|
func isCECommand(s string) bool {
|
|
|
|
|
upper := strings.ToUpper(s)
|
|
|
|
|
switch upper {
|
|
|
|
|
case "LS", "LIST", "SEARCH":
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
// Helper functions for parsing
|
|
|
|
|
func (p *Parser) parseQuotedString() (string, error) {
|
|
|
|
|
if p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return "", fmt.Errorf("expected quoted string, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
return p.curToken.Value, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *Parser) parseIdentifier() (string, error) {
|
|
|
|
|
if p.curToken.Type != TokenIdentifier {
|
|
|
|
|
return "", fmt.Errorf("expected identifier, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
return p.curToken.Value, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 01:08:45 -10:00
|
|
|
func (p *Parser) parseVariableValue() (string, error) {
|
|
|
|
|
switch p.curToken.Type {
|
|
|
|
|
case TokenIdentifier, TokenQuotedString, TokenInteger, TokenFloat:
|
|
|
|
|
return p.curToken.Value, nil
|
|
|
|
|
default:
|
|
|
|
|
return "", fmt.Errorf("expected variable value, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
func (p *Parser) parseNumber() (int, error) {
|
2026-04-07 15:09:45 +08:00
|
|
|
if p.curToken.Type != TokenInteger {
|
2026-03-04 19:17:16 +08:00
|
|
|
return 0, fmt.Errorf("expected number, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
return strconv.Atoi(p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 11:30:09 +08:00
|
|
|
func (p *Parser) parseFloat() (float64, error) {
|
2026-06-08 11:49:37 +08:00
|
|
|
// Accept either TokenInteger or TokenFloat so that literals like
|
|
|
|
|
// `0.3` (which the lexer tags as TokenFloat) and `10` (TokenInteger)
|
|
|
|
|
// both parse cleanly.
|
|
|
|
|
if p.curToken.Type != TokenInteger && p.curToken.Type != TokenFloat {
|
2026-04-07 11:30:09 +08:00
|
|
|
return math.NaN(), fmt.Errorf("expected number, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
result, err := strconv.ParseFloat(p.curToken.Value, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return math.NaN(), err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 11:49:37 +08:00
|
|
|
// parseQuotedStringList consumes a bracket-delimited list of quoted strings:
|
|
|
|
|
// [ 'a', 'b', 'c' ]
|
|
|
|
|
// Empty list [] is allowed. The cursor must be positioned on '[' when called;
|
|
|
|
|
// on return, the cursor is positioned just past the closing ']'.
|
|
|
|
|
func (p *Parser) parseQuotedStringList() ([]string, error) {
|
|
|
|
|
if p.curToken.Type != TokenLBracket {
|
|
|
|
|
return nil, fmt.Errorf("expected '[', got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
p.nextToken() // skip '['
|
|
|
|
|
|
|
|
|
|
// Always return a non-nil slice so callers (and json.Marshal) see []
|
|
|
|
|
// instead of null for the empty-list case.
|
|
|
|
|
list := make([]string, 0)
|
|
|
|
|
// Allow empty list []
|
|
|
|
|
if p.curToken.Type == TokenRBracket {
|
|
|
|
|
p.nextToken() // skip ']'
|
|
|
|
|
return list, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
s, err := p.parseQuotedString()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("expected quoted string in list: %w", err)
|
|
|
|
|
}
|
|
|
|
|
list = append(list, s)
|
|
|
|
|
p.nextToken() // step past the closing quote
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type == TokenComma {
|
|
|
|
|
p.nextToken() // step past ','
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if p.curToken.Type == TokenRBracket {
|
|
|
|
|
p.nextToken() // step past ']'
|
|
|
|
|
return list, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("expected ',' or ']' in list, got %s", p.curToken.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 19:17:16 +08:00
|
|
|
func tokenTypeToString(t int) string {
|
2026-06-08 11:49:37 +08:00
|
|
|
switch t {
|
|
|
|
|
case TokenEOF:
|
|
|
|
|
return "end of input"
|
|
|
|
|
case TokenIdentifier:
|
|
|
|
|
return "identifier"
|
|
|
|
|
case TokenInteger:
|
|
|
|
|
return "integer"
|
|
|
|
|
case TokenFloat:
|
|
|
|
|
return "float"
|
|
|
|
|
case TokenQuotedString:
|
|
|
|
|
return "quoted string"
|
|
|
|
|
case TokenLBracket:
|
|
|
|
|
return "'['"
|
|
|
|
|
case TokenRBracket:
|
|
|
|
|
return "']'"
|
|
|
|
|
case TokenComma:
|
|
|
|
|
return "','"
|
|
|
|
|
case TokenSemicolon:
|
|
|
|
|
return "';'"
|
|
|
|
|
}
|
2026-03-04 19:17:16 +08:00
|
|
|
return fmt.Sprintf("token(%d)", t)
|
|
|
|
|
}
|
2026-03-26 21:07:06 +08:00
|
|
|
|
2026-06-09 15:22:50 +08:00
|
|
|
func (p *Parser) parseContextEngineCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume COMMAND
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("context_engine_command")
|
|
|
|
|
cmd.Params["command"] = p.original
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
2026-03-26 21:07:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseCEListCommand parses the ls command
|
|
|
|
|
// Syntax: ls [path] or ls datasets
|
|
|
|
|
func (p *Parser) parseCEListCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume LS/LIST
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("ce_ls")
|
|
|
|
|
|
|
|
|
|
// Check if there's a path argument
|
|
|
|
|
// Also accept TokenDatasets since "datasets" is a keyword but can be a path
|
|
|
|
|
if p.curToken.Type == TokenIdentifier || p.curToken.Type == TokenQuotedString ||
|
|
|
|
|
p.curToken.Type == TokenDatasets {
|
|
|
|
|
path := p.curToken.Value
|
|
|
|
|
// Remove quotes if present
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
path = strings.Trim(path, "\"'")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-04-30 12:36:03 +08:00
|
|
|
|
|
|
|
|
// Handle path components separated by slashes (e.g., "skills/hub1")
|
|
|
|
|
for p.curToken.Type == TokenSlash {
|
|
|
|
|
p.nextToken() // consume slash
|
|
|
|
|
if p.curToken.Type == TokenIdentifier || p.curToken.Type == TokenDatasets ||
|
|
|
|
|
p.curToken.Type == TokenAgents || p.curToken.Type == TokenChats {
|
|
|
|
|
path = path + "/" + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else if p.curToken.Type == TokenNumber {
|
|
|
|
|
// Handle version numbers like 1.0.0 (parsed as number . number . number)
|
|
|
|
|
// OR filenames starting with numbers like 3_list_compressors.pdf
|
|
|
|
|
numberPart := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Continue reading .number parts (version number format)
|
|
|
|
|
if p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
versionPart := numberPart
|
|
|
|
|
for p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
p.nextToken() // consume .
|
|
|
|
|
if p.curToken.Type == TokenNumber {
|
|
|
|
|
versionPart = versionPart + "." + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
path = path + "/" + versionPart
|
|
|
|
|
} else if p.curToken.Type == TokenIdentifier {
|
|
|
|
|
// Filename starting with number: 3_list_compressors.pdf
|
|
|
|
|
path = path + "/" + numberPart + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
// Just a number
|
|
|
|
|
path = path + "/" + numberPart
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Trailing slash, just append it
|
|
|
|
|
path = path + "/"
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.Params["path"] = path
|
2026-03-26 21:07:06 +08:00
|
|
|
} else {
|
|
|
|
|
// Default to "datasets" root
|
|
|
|
|
cmd.Params["path"] = "datasets"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional semicolon
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 12:36:03 +08:00
|
|
|
// parseCECatCommand parses the cat command
|
|
|
|
|
// Syntax: cat <path>
|
|
|
|
|
func (p *Parser) parseCECatCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume CAT
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("ce_cat")
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenIdentifier && p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected path after CAT")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path := p.curToken.Value
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
path = strings.Trim(path, "\"'")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Handle path components separated by slashes (e.g., "skills/hub1/skill/README.md")
|
|
|
|
|
for p.curToken.Type == TokenSlash {
|
|
|
|
|
p.nextToken() // consume slash
|
|
|
|
|
if p.curToken.Type == TokenIdentifier || p.curToken.Type == TokenAgents ||
|
|
|
|
|
p.curToken.Type == TokenChats || p.curToken.Type == TokenDatasets {
|
|
|
|
|
path = path + "/" + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else if p.curToken.Type == TokenNumber {
|
|
|
|
|
// Handle version numbers like 1.0.0 (parsed as number . number . number)
|
|
|
|
|
// OR filenames starting with numbers like 3_list_compressors.pdf
|
|
|
|
|
numberPart := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Continue reading .number parts (version number format)
|
|
|
|
|
if p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
versionPart := numberPart
|
|
|
|
|
for p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
p.nextToken() // consume .
|
|
|
|
|
if p.curToken.Type == TokenNumber {
|
|
|
|
|
versionPart = versionPart + "." + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
path = path + "/" + versionPart
|
|
|
|
|
} else if p.curToken.Type == TokenIdentifier {
|
|
|
|
|
// Filename starting with number: 3_list_compressors.pdf
|
|
|
|
|
path = path + "/" + numberPart + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
// Just a number
|
|
|
|
|
path = path + "/" + numberPart
|
|
|
|
|
}
|
|
|
|
|
} else if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
path = path + "/" + strings.Trim(p.curToken.Value, "\"'")
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
// Trailing slash, just append it
|
|
|
|
|
path = path + "/"
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd.Params["path"] = path
|
|
|
|
|
|
|
|
|
|
// Optional semicolon
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 21:07:06 +08:00
|
|
|
// parseCESearchCommand parses the search command
|
|
|
|
|
// Syntax: search <query> or search <query> in <path>
|
|
|
|
|
func (p *Parser) parseCESearchCommand() (*Command, error) {
|
|
|
|
|
p.nextToken() // consume SEARCH
|
|
|
|
|
|
|
|
|
|
cmd := NewCommand("ce_search")
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenIdentifier && p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected query after SEARCH")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query := p.curToken.Value
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
query = strings.Trim(query, "\"'")
|
|
|
|
|
}
|
|
|
|
|
cmd.Params["query"] = query
|
|
|
|
|
p.nextToken()
|
|
|
|
|
|
|
|
|
|
// Check for optional "in <path>" clause
|
|
|
|
|
if p.curToken.Type == TokenIdentifier && strings.ToUpper(p.curToken.Value) == "IN" {
|
|
|
|
|
p.nextToken() // consume IN
|
|
|
|
|
|
|
|
|
|
if p.curToken.Type != TokenIdentifier && p.curToken.Type != TokenQuotedString {
|
|
|
|
|
return nil, fmt.Errorf("expected path after IN")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
path := p.curToken.Value
|
|
|
|
|
if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
path = strings.Trim(path, "\"'")
|
|
|
|
|
}
|
|
|
|
|
p.nextToken()
|
2026-04-30 12:36:03 +08:00
|
|
|
|
|
|
|
|
// Handle path components separated by slashes (e.g., "skills/hub1")
|
|
|
|
|
for p.curToken.Type == TokenSlash {
|
|
|
|
|
p.nextToken() // consume slash
|
|
|
|
|
if p.curToken.Type == TokenIdentifier || p.curToken.Type == TokenAgents ||
|
|
|
|
|
p.curToken.Type == TokenChats || p.curToken.Type == TokenDatasets {
|
|
|
|
|
path = path + "/" + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
2026-05-09 17:41:54 +08:00
|
|
|
} else if p.curToken.Type == TokenNumber {
|
|
|
|
|
// Handle version numbers like 1.0.0 (parsed as number . number . number)
|
|
|
|
|
// OR filenames starting with numbers like 3_list_compressors.pdf
|
|
|
|
|
numberPart := p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
// Continue reading .number parts (version number format)
|
|
|
|
|
if p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
versionPart := numberPart
|
|
|
|
|
for p.curToken.Type == TokenIllegal && p.curToken.Value == "." {
|
|
|
|
|
p.nextToken() // consume .
|
|
|
|
|
if p.curToken.Type == TokenNumber {
|
|
|
|
|
versionPart = versionPart + "." + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-04-30 12:36:03 +08:00
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
path = path + "/" + versionPart
|
|
|
|
|
} else if p.curToken.Type == TokenIdentifier {
|
|
|
|
|
// Filename starting with number: 3_list_compressors.pdf
|
|
|
|
|
path = path + "/" + numberPart + p.curToken.Value
|
|
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
|
|
|
|
// Just a number
|
|
|
|
|
path = path + "/" + numberPart
|
2026-04-30 12:36:03 +08:00
|
|
|
}
|
2026-05-09 17:41:54 +08:00
|
|
|
} else if p.curToken.Type == TokenQuotedString {
|
|
|
|
|
path = path + "/" + strings.Trim(p.curToken.Value, "\"'")
|
2026-04-30 12:36:03 +08:00
|
|
|
p.nextToken()
|
|
|
|
|
} else {
|
2026-05-09 17:41:54 +08:00
|
|
|
// Trailing slash, just append it
|
|
|
|
|
path = path + "/"
|
|
|
|
|
break
|
2026-04-30 12:36:03 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 17:41:54 +08:00
|
|
|
cmd.Params["path"] = path
|
2026-03-26 21:07:06 +08:00
|
|
|
} else {
|
|
|
|
|
cmd.Params["path"] = "."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional semicolon
|
|
|
|
|
if p.curToken.Type == TokenSemicolon {
|
|
|
|
|
p.nextToken()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cmd, nil
|
|
|
|
|
}
|