added command execution via !

This commit is contained in:
Luxferre
2026-08-15 16:58:25 +03:00
parent 4b55c27fee
commit 4509851243
3 changed files with 41 additions and 4 deletions
+3 -2
View File
@@ -60,6 +60,7 @@ All implementations read the same `model.cfg` and `system.txt` from the current
- `/load <id>` — 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 <param> [val]` — inspect or update a configuration parameter in `model.cfg` live
- `!<cmd>` — 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/<id>.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 <param>`) or update `model.cfg` live (`/cfg <param> <val>`) 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/<id>.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 <param>`) or update `model.cfg` live (`/cfg <param> <val>`) 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
+22 -2
View File
@@ -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 <param> [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 <id>", "load session"}, {"/compact", "compact context"}, {"/cfg <k> [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 <id>", "load session"}, {"/compact", "compact context"}, {"/cfg <k> [v]", "get/set config"}, {"!<cmd>", "run shell command directly"}, {"/help", "show help"}} {
fmt.Println(c(fmt.Sprintf(" %-15s", kv[0]), 1, 32) + kv[1])
}
continue
+16
View File
@@ -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")
}
}