Skip to content

Commit 43c76d6

Browse files
committed
Added input mode to fix parsing.
1 parent 716188a commit 43c76d6

6 files changed

Lines changed: 194 additions & 18 deletions

File tree

cmd/lumo/main.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,46 @@ func main() {
6565
}
6666

6767
// Process command from arguments
68+
// Join arguments with spaces, preserving quotes if present
6869
command := strings.Join(os.Args[1:], " ")
6970

71+
// In AI-first mode (default), we don't need special handling for quoted strings
72+
// as everything will be treated as an AI query by default unless it has a specific prefix
73+
// or is a single executable command in command-first mode.
74+
75+
// However, we still want to handle the case where a command might be a quoted string
76+
// that was split by the shell, for better user experience
77+
if len(os.Args) > 2 && !cfg.CommandFirstMode {
78+
// If we have multiple arguments and none of them start with a prefix like "lumo:" or "shell:",
79+
// it might be a quoted string that was split
80+
hasPrefix := false
81+
for _, prefix := range []string{"lumo:", "shell:", "ask:", "ai:", "auto:", "agent:",
82+
"health:", "syshealth:", "report:", "sysreport:", "chat:", "talk:", "config:",
83+
"speed:", "speedtest:", "speed-test:", "magic:", "clipboard", "connect"} {
84+
if strings.HasPrefix(command, prefix) {
85+
hasPrefix = true
86+
break
87+
}
88+
}
89+
90+
if !hasPrefix {
91+
// In AI-first mode, treat it as an AI query by default
92+
cmd := &nlp.Command{
93+
Type: nlp.CommandTypeAI,
94+
Intent: command,
95+
Parameters: make(map[string]string),
96+
RawInput: command,
97+
}
98+
result, err := exec.Execute(cmd)
99+
if err != nil {
100+
fmt.Fprintf(os.Stderr, "Error executing command: %v\n", err)
101+
os.Exit(1)
102+
}
103+
term.Display(result)
104+
os.Exit(0)
105+
}
106+
}
107+
70108
// Special handling for commands with specific prefixes
71109
if strings.HasPrefix(command, "lumo:") || strings.HasPrefix(command, "shell:") {
72110
// Handle shell commands

lumo

9.16 MB
Binary file not shown.

pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Config struct {
2222
MaxHistorySize int `json:"max_history_size"`
2323
EnableLogging bool `json:"enable_logging"`
2424
EnableShellInInteractive bool `json:"enable_shell_in_interactive"`
25+
CommandFirstMode bool `json:"command_first_mode"`
2526

2627
// Agent mode settings
2728
EnableAgentMode bool `json:"enable_agent_mode"`
@@ -61,6 +62,7 @@ func DefaultConfig() *Config {
6162
MaxHistorySize: 1000,
6263
EnableLogging: true,
6364
EnableShellInInteractive: false, // Shell commands disabled in interactive mode by default
65+
CommandFirstMode: false, // Default to AI-first mode (treat input as AI queries by default)
6466
EnableAgentMode: true, // Agent mode enabled by default
6567
EnableAgentREPL: true, // REPL mode enabled by default
6668
AgentConfirmBeforeExecution: true, // Confirm before execution by default

pkg/executor/config.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ func (e *Executor) executeConfigCommand(cmd *nlp.Command) (*Result, error) {
4747
• config:ollama set <url> Set Ollama URL
4848
• config:ollama test Test connection to Ollama server
4949
50+
• config:mode show Show current input mode
51+
• config:mode ai Set AI-first mode (default)
52+
• config:mode command Set command-first mode
53+
5054
╰──────────────────────────────────────────────────────────╯
5155
`,
5256
IsError: false,
@@ -64,6 +68,8 @@ func (e *Executor) executeConfigCommand(cmd *nlp.Command) (*Result, error) {
6468
return e.handleKeyConfig(parts[1:], cmd)
6569
case "ollama":
6670
return e.handleOllamaConfig(parts[1:], cmd)
71+
case "mode":
72+
return e.handleModeConfig(parts[1:], cmd)
6773
default:
6874
return &Result{
6975
Output: fmt.Sprintf("Unknown configuration command: %s\nUse 'config:' for help.", parts[0]),
@@ -506,6 +512,91 @@ func (e *Executor) handleOllamaConfig(args []string, cmd *nlp.Command) (*Result,
506512
}
507513
}
508514

515+
// handleModeConfig handles input mode configuration commands
516+
func (e *Executor) handleModeConfig(args []string, cmd *nlp.Command) (*Result, error) {
517+
if len(args) == 0 {
518+
return &Result{
519+
Output: "Missing mode command. Use 'show', 'ai', or 'command'.",
520+
IsError: true,
521+
CommandRun: cmd.RawInput,
522+
}, nil
523+
}
524+
525+
switch args[0] {
526+
case "show":
527+
// Show current mode
528+
modeStr := "AI-first"
529+
if e.config.CommandFirstMode {
530+
modeStr = "Command-first"
531+
}
532+
533+
output := fmt.Sprintf(`
534+
╭─────────────────── 🔧 Input Mode ─────────────────────────╮
535+
536+
Current input mode: %s
537+
538+
• AI-first mode: Treats all input as AI queries by default
539+
unless it starts with a specific command prefix.
540+
541+
• Command-first mode: Treats input as shell commands if it
542+
looks like a command, otherwise as an AI query.
543+
544+
╰──────────────────────────────────────────────────────────╯
545+
`, modeStr)
546+
547+
return &Result{
548+
Output: output,
549+
IsError: false,
550+
CommandRun: cmd.RawInput,
551+
}, nil
552+
553+
case "ai":
554+
// Set AI-first mode
555+
e.config.CommandFirstMode = false
556+
557+
// Save the configuration
558+
if err := e.config.Save(); err != nil {
559+
return &Result{
560+
Output: fmt.Sprintf("Error saving configuration: %v", err),
561+
IsError: true,
562+
CommandRun: cmd.RawInput,
563+
}, nil
564+
}
565+
566+
return &Result{
567+
Output: "Input mode set to AI-first. Lumo will now treat all input as AI queries by default unless it starts with a specific command prefix.",
568+
IsError: false,
569+
CommandRun: cmd.RawInput,
570+
}, nil
571+
572+
case "command":
573+
// Set Command-first mode
574+
e.config.CommandFirstMode = true
575+
576+
// Save the configuration
577+
if err := e.config.Save(); err != nil {
578+
return &Result{
579+
Output: fmt.Sprintf("Error saving configuration: %v", err),
580+
IsError: true,
581+
CommandRun: cmd.RawInput,
582+
}, nil
583+
}
584+
585+
return &Result{
586+
Output: "Input mode set to Command-first. Lumo will now treat input as shell commands if it looks like a command, otherwise as an AI query.",
587+
IsError: false,
588+
CommandRun: cmd.RawInput,
589+
}, nil
590+
591+
default:
592+
return &Result{
593+
Output: fmt.Sprintf("Unknown mode command: %s. Use 'show', 'ai', or 'command'.", args[0]),
594+
IsError: true,
595+
CommandRun: cmd.RawInput,
596+
}, nil
597+
}
598+
}
599+
509600
// handleKeyConfig handles API key configuration commands
510601
func (e *Executor) handleKeyConfig(args []string, cmd *nlp.Command) (*Result, error) {
511602
if len(args) == 0 {

pkg/executor/executor.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,23 @@ func (e *Executor) executeShellCommand(cmd *nlp.Command) (*Result, error) {
261261
}, nil
262262
}
263263

264+
// Check if the command exists before trying to execute it
265+
_, err := exec.LookPath(parts[0])
266+
if err != nil {
267+
// Command doesn't exist, provide a helpful error message
268+
suggestion := ""
269+
if len(parts) > 1 {
270+
// If there are multiple words, suggest using it as an AI query
271+
suggestion = fmt.Sprintf("\n\nDid you mean to ask AI about \"%s\"? Try: lumo ask:\"%s\"", cmd.Intent, cmd.Intent)
272+
}
273+
274+
return &Result{
275+
Output: fmt.Sprintf("Error: exec: \"%s\": executable file not found in $PATH%s", parts[0], suggestion),
276+
IsError: true,
277+
CommandRun: cmd.RawInput,
278+
}, nil
279+
}
280+
264281
// Create the command
265282
shellCmd := exec.Command(parts[0], parts[1:]...)
266283

pkg/nlp/parser.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -203,24 +203,36 @@ func (p *Parser) Parse(input string) (*Command, error) {
203203
// Check if this is a command-line argument (first argument is the program name)
204204
args := os.Args
205205
if len(args) > 1 && input == strings.Join(args[1:], " ") {
206-
// Check if the input looks like a natural language query
207-
// Natural language queries typically:
208-
// 1. Start with a capital letter (questions, sentences)
209-
// 2. Contain multiple words with spaces
210-
// 3. End with a question mark (for questions)
211-
// 4. Don't contain shell command special characters like |, >, <, etc.
212-
213-
// If it looks like a natural language query, treat it as an AI query
214-
if isNaturalLanguageQuery(input) {
215-
cmd.Type = CommandTypeAI
206+
// Split the input into words
207+
words := strings.Fields(input)
208+
209+
// Check if it's a single word that exists as an executable in PATH
210+
if len(words) == 1 {
211+
_, err := exec.LookPath(words[0])
212+
if err == nil && p.config.CommandFirstMode {
213+
// It's a single word that exists as a command and we're in command-first mode
214+
cmd.Type = CommandTypeShell
215+
cmd.Intent = input
216+
return cmd, nil
217+
}
218+
}
219+
220+
// If we're in command-first mode, check if it looks like a natural language query
221+
if p.config.CommandFirstMode {
222+
// If it looks like a natural language query, treat it as an AI query
223+
if IsNaturalLanguageQuery(input) {
224+
cmd.Type = CommandTypeAI
225+
cmd.Intent = input
226+
return cmd, nil
227+
}
228+
229+
// Otherwise, treat it as a shell command in command-first mode
230+
cmd.Type = CommandTypeShell
216231
cmd.Intent = input
217232
return cmd, nil
218233
}
219234

220-
// Otherwise, treat it as a shell command
221-
cmd.Type = CommandTypeShell
222-
cmd.Intent = input
223-
return cmd, nil
235+
// In AI-first mode (default), we treat everything as AI query unless it's a specific exception
224236
}
225237

226238
// Check if this looks like a speed test query
@@ -230,7 +242,7 @@ func (p *Parser) Parse(input string) (*Command, error) {
230242
return cmd, nil
231243
}
232244

233-
// Check if this looks like a task that should use agent mode
245+
// In AI-first mode, check if this looks like a task that should use agent mode
234246
if isAgentTask(input) && p.config.EnableAgentMode {
235247
cmd.Type = CommandTypeAgent
236248
cmd.Intent = input
@@ -243,9 +255,9 @@ func (p *Parser) Parse(input string) (*Command, error) {
243255
return cmd, nil
244256
}
245257

246-
// isNaturalLanguageQuery determines if a string is likely to be a natural language query
247-
// rather than a shell command
248-
func isNaturalLanguageQuery(input string) bool {
258+
// IsNaturalLanguageQuery determines if a string is likely to be a natural language query
259+
// rather than a shell command. This is exported for use in other packages.
260+
func IsNaturalLanguageQuery(input string) bool {
249261
// Trim the input
250262
input = strings.TrimSpace(input)
251263

@@ -305,6 +317,22 @@ func isNaturalLanguageQuery(input string) bool {
305317
}
306318
}
307319

320+
// Check for common action verbs that are likely part of natural language queries
321+
// but might be confused with shell commands
322+
actionVerbs := []string{"create", "find", "list", "show", "get", "make", "setup", "install",
323+
"configure", "backup", "search", "organize", "clean", "delete", "remove", "update", "check", "analyze"}
324+
325+
if len(words) > 1 {
326+
firstWord := strings.ToLower(words[0])
327+
for _, verb := range actionVerbs {
328+
if firstWord == verb {
329+
// If it's an action verb followed by at least one more word, it's likely a natural language query
330+
// e.g., "create folder" is a natural language query, not a shell command
331+
return true
332+
}
333+
}
334+
}
335+
308336
// Check if the input is a single word that doesn't exist as a command
309337
// This is a heuristic to prevent treating unknown commands as shell commands
310338
if len(words) == 1 {

0 commit comments

Comments
 (0)