From 45098512438ce6892a4aa54f80d2e2193dd64ec1 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sat, 15 Aug 2026 16:58:25 +0300 Subject: [PATCH] added command execution via ! --- README.md | 5 +++-- main.go | 24 ++++++++++++++++++++++-- main_test.go | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8c081b2..7a8e76a 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ All implementations read the same `model.cfg` and `system.txt` from the current - `/load ` — load a saved session (exact id or unique prefix) and continue from there - `/compact` — summarize the conversation with the LLM and compact the context down to just the system message plus the summary - `/cfg [val]` — inspect or update a configuration parameter in `model.cfg` live + - `!` — execute a shell command directly through `shell_exec` without adding the result to the conversation context (Go port) - `/help` — show all supported commands - `/clear` — reset the conversation to just the system prompt - `/quit` — exit @@ -92,8 +93,8 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l 1. Read system prompt from `system.txt` (default if missing). 2. Read model parameters from `model.cfg` (`key=value` format). 3. Prepare a new message list with the system prompt (`role: "system"`). -4. Read the first command-line parameter. If non-empty, read user prompt from the specified file, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, and exit. -5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If equal to `/save`, write the whole `messages` array to `~/.bantam/sessions/.json` (with an auto-generated summary) and return to step 5. If equal to `/list`, print saved sessions and their summaries and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg `) or update `model.cfg` live (`/cfg `) and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/autosave.json`. +4. Read the first command-line parameter. If non-empty, read user prompt from the specified file. If prefixed with `!`, execute the shell command directly via `shell_exec` and exit. Otherwise, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, and exit. +5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If equal to `/save`, write the whole `messages` array to `~/.bantam/sessions/.json` (with an auto-generated summary) and return to step 5. If equal to `/list`, print saved sessions and their summaries and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg `) or update `model.cfg` live (`/cfg `) and return to step 5. If starting with `!`, execute the command directly via `shell_exec` without adding the result to `messages` and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/autosave.json`. 6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, and go to step 5. ### Agentic loop (`AL(cfg, messages)`) function diff --git a/main.go b/main.go index c2fca13..d5b9316 100644 --- a/main.go +++ b/main.go @@ -1241,7 +1241,18 @@ func main() { fmt.Println(c("Error: file '"+p+"' not found.", 31)) os.Exit(1) } - msgs = append(msgs, Message{Role: "user", Content: strp(strings.TrimSpace(string(data)))}) + u := strings.TrimSpace(string(data)) + if strings.HasPrefix(u, "!") { + cmd := strings.TrimSpace(strings.TrimPrefix(u, "!")) + if cmd != "" { + astr, _ := json.Marshal(map[string]string{"command": cmd}) + fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33)) + res := shell(cmd, cfg.ShellTimeout) + fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2)) + } + return + } + msgs = append(msgs, Message{Role: "user", Content: strp(u)}) if msgs, err = AL(&cfg, msgs, sp, 0); err != nil { fmt.Println(c("[error: "+err.Error()+"]", 31)) os.Exit(1) @@ -1338,9 +1349,18 @@ func main() { fmt.Println(c("Usage: /cfg [val]", 31)) } continue + case strings.HasPrefix(u, "!"): + cmd := strings.TrimSpace(strings.TrimPrefix(u, "!")) + if cmd != "" { + astr, _ := json.Marshal(map[string]string{"command": cmd}) + fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33)) + res := shell(cmd, cfg.ShellTimeout) + fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2)) + } + continue case u == "/help": fmt.Println(c("Bantam commands:", 1, 36)) - for _, kv := range [][2]string{{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save", "save session"}, {"/list", "list sessions"}, {"/load ", "load session"}, {"/compact", "compact context"}, {"/cfg [v]", "get/set config"}, {"/help", "show help"}} { + for _, kv := range [][2]string{{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save", "save session"}, {"/list", "list sessions"}, {"/load ", "load session"}, {"/compact", "compact context"}, {"/cfg [v]", "get/set config"}, {"!", "run shell command directly"}, {"/help", "show help"}} { fmt.Println(c(fmt.Sprintf(" %-15s", kv[0]), 1, 32) + kv[1]) } continue diff --git a/main_test.go b/main_test.go index aa54813..cdc4cc9 100644 --- a/main_test.go +++ b/main_test.go @@ -1589,5 +1589,21 @@ func TestRenderMDNoColorFallback(t *testing.T) { } } +func TestDirectShellExecution(t *testing.T) { + cmd := "echo direct_exec_test" + res := shell(cmd, 10) + if !strings.HasPrefix(res, "direct_exec_test") || !strings.Contains(res, "exit: 0") { + t.Errorf("direct shell exec failed, got %q", res) + } + + // Verify that command stripping preserves arguments + u := "! echo hello world" + stripped := strings.TrimSpace(strings.TrimPrefix(u, "!")) + if stripped != "echo hello world" { + t.Errorf("stripped command = %q, want %q", stripped, "echo hello world") + } +} + +