init commit

This commit is contained in:
Luxferre
2026-10-14 00:01:03 +03:00
commit b6c71f2c2c
13 changed files with 29167 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# Bantam (C++20 port) — POSIX-compatible Makefile.
# Targets: Linux, FreeBSD, macOS, Haiku (any POSIX make: bmake, gmake, smake).
# Dependencies: libcurl and GNU readline (found via standard include/lib
# paths) for HTTP and interactive line editing. JSON is handled by the vendored
# single-header nlohmann/json.hpp (no extra dependency).
# If your headers/libs live in a non-standard prefix, override, e.g.:
# make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl -lreadline"
CXX = c++
CXXFLAGS = -std=c++20 -s -Wall -Wextra -Wpedantic -O2 -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
CPPFLAGS = -I/usr/local/include -I.
LDLIBS = -L/usr/local/lib -lcurl -lreadline
# Location of the AddressSanitizer/UBSan runtime libraries plus a replacement
# libasan_preinit.o (so the runtime initializes before main()). On a normal
# system these ship with the compiler; override with
# make debug ASANDIR= (empty) — the gcc driver finds them automatically.
ASANDIR = /tmp/asanlib
OBJS = main.o config.o util.o json.o llm.o tools.o session.o render.o editor.o
bantam: $(OBJS)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -o bantam $(OBJS) $(LDLIBS)
main.o: main.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c main.cpp
config.o: config.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c config.cpp
util.o: util.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c util.cpp
json.o: json.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c json.cpp
llm.o: llm.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c llm.cpp
tools.o: tools.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c tools.cpp
session.o: session.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c session.cpp
render.o: render.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c render.cpp
editor.o: editor.cpp bantam.hpp nlohmann/json.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c editor.cpp
clean:
rm -f $(OBJS) bantam bantam_asan
install: bantam
install -m 0755 bantam $(DESTDIR)/usr/local/bin/bantam
# Debug build with AddressSanitizer + UndefinedBehaviorSanitizer.
# Rebuilds ./bantam — run `make clean && make` afterwards to restore the
# optimized build. ASANDIR points the gcc driver (-B) at the runtime libs and
# the replacement libasan_preinit.o, and sets an rpath so they load at run time.
debug:
$(MAKE) CXXFLAGS="$(CXXFLAGS) -g -O0 -fsanitize=address,undefined -B$(ASANDIR) -Wl,-rpath,$(ASANDIR)" bantam
+400
View File
@@ -0,0 +1,400 @@
# Bantam: tiny, powerful, DIY AI agent (C++ port)
## About
Bantam is a minimalist, dependency-light AI agent that talks to any
OpenAI-compatible `/chat/completions` API. This directory is a faithful,
**modern C++20 port** of the pure-C reference implementation
(`bantam-c`), which itself tracks the Go reference implementation
(`main.go` + `term_*.go`, module `code.luxferre.top/luxferre/bantam`).
It provides the same agentic loop — autonomous tool execution, direct shell
interaction, file writing/editing, real-time response streaming, markdown
terminal rendering with box-drawing tables, Fibonacci backoff network
resilience, context-window auto-discovery, token usage tracking with prompt
cache breakdowns, and conversation compaction — implemented in C++20 using
standard, safe containers and RAII.
The terminal markdown renderer (inline styles, headings, lists, block quotes,
fenced code, and box-drawing tables) is implemented natively, so no external
Markdown library is required. Interactive line editing is handled by **GNU
readline**, which provides the prompt, history and Emacs-style key bindings,
so no hand-rolled terminal code is needed.
The whole philosophy of Bantam rests on two principles:
1. The structure must be as simple as possible for anyone to reimplement the
agent from a plain algorithm description.
2. The agent only needs a minimal set of tools: `bash` to run shell commands,
`read` to inspect files and directories, and `write` to write/edit files.
### Design goals relative to the C port
- Use standard, safe C++ containers (`std::string`, `std::vector`,
`std::map`, `std::span`) instead of raw memory areas.
- Eliminate hand-rolled allocation/string code; rely on RAII so there is far
less room for memory-allocation related vulnerabilities.
- Depend only on **libcurl** (HTTP), **GNU readline** (interactive line
editing) and the vendored single-header **nlohmann/json** library (vendored
under `nlohmann/json.hpp`); the unsafe json-c C API is no longer used.
## Repository layout
```
bantam.hpp all shared declarations (structs, prototypes)
util.cpp string building, UTF-8, prompt-injection filter, color, IDs, MD5
config.cpp config file parsing, env precedence, /models discovery
json.cpp request building + non-stream response parsing (nlohmann/json)
llm.cpp libcurl HTTP client, OpenCode headers, backoff, SSE streaming
tools.cpp bash/read/write tools, oversized-result offloading
session.cpp message copy, session save/load, compaction
render.cpp terminal markdown renderer (native, box-drawing tables)
editor.cpp GNU readline line editor + history (+ term helpers)
main.cpp REPL, agentic loop, tool dispatch, commands
Makefile POSIX-compatible build
nlohmann/ vendored nlohmann/json single-header (v3.12.0)
README.md this document
```
## Prerequisites
- A C++20 compiler (`c++` / `g++` / `clang++`) and a POSIX `make`
(`bmake`, `gmake`, `smake`, …).
- **libcurl** development packages, available via the standard include /
library search paths. The JSON layer is a vendored single-header
(`nlohmann/json.hpp`) and needs no separate dependency.
- **GNU readline** development packages (`libreadline-dev` on Debian/Ubuntu,
`readline` on Fedora/macOS) for the interactive prompt, history and line
editing. On common systems:
```bash
# Debian / Ubuntu
apt-get install libreadline-dev
# Fedora / RHEL
dnf install readline-devel
# macOS (Homebrew)
brew install readline
```
- An OpenAI-compatible API endpoint (or an API key for one).
On common systems:
```bash
# Debian / Ubuntu
apt-get install libcurl4-openssl-dev build-essential
# Fedora / RHEL
dnf install libcurl-devel
# FreeBSD
pkg install curl
# macOS (Homebrew)
brew install curl
```
If the libcurl headers or libraries live in a non-standard prefix, override
them at build time, e.g.:
```bash
make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl"
```
## Building
```bash
make # builds ./bantam (c++ -std=c++20 -O2 -s -Wall -Wextra -Wpedantic)
make clean # remove build artifacts
make install # install to $(DESTDIR)/usr/local/bin/bantam
```
The default `make` produces a single binary, `bantam`, with no runtime
dependencies beyond libc, libcurl and libreadline.
## Running
All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority
if present) from the current working directory.
1. Configure `model.cfg` (or `.bantam.cfg`, which takes priority) with your
API settings:
```ini
endpoint=https://api.kilo.ai/api/openrouter
model=openrouter/free
temperature=0.7
api_key=your_api_key_here
stream=true
context_window=200000
max_tool_res=65536
reasoning_effort=high
```
2. Interactive mode:
```bash
./bantam
```
In interactive mode, prompts can span multiple lines: press **Ctrl+J** to
insert a real line break (the cursor moves to the next line), then **Enter**
to submit the whole multi-line prompt. Bracketed paste is enabled
(`ESC[?2004h`) while editing, so a multi-line block pasted from the
clipboard is inserted as literal line breaks and only the final **Enter**
submits it. The C++ port ships its own raw-mode line editor, working
everywhere with no third-party dependencies. It supports arrow keys and
Up/Down for history, Backspace / Delete / Ctrl+D (on an empty line) to
delete, and Emacs-style editing combos: **Ctrl+A** (start of line),
**Ctrl+E** (end of line), **Ctrl+B** / **Ctrl+F** (move by character),
**Ctrl+W** (delete previous word), **Ctrl+K** (kill to end of line),
**Ctrl+U** (kill to start of line), **Ctrl+Left** / **Ctrl+Right** (move by
word), and **Home** / **End** keys. **Ctrl+C** clears the line /
interrupts an in-flight run, and **Ctrl+D** on an empty line exits.
After every interaction, Bantam displays token usage (prompt tokens,
cached / uncached breakdown when supported by the provider, completion
tokens, and context-window utilization):
```text
[openrouter/free: 1420 prompt (1000 cached, 420 uncached) + 85 completion | context: 1420/262144 (0.5%)]
```
Sessions are saved under `~/.bantam/sessions/` and can be managed with
these commands:
- `/save [name]` — save the entire conversation to a session file (named
`name`, or defaulting to the MD5 hash of the current project directory)
and generate its summary.
- `/continue` (alias `/cont`) — continue/autoload the session corresponding
to the current project directory.
- `/list` — list saved sessions (newest first) with their ids, timestamps,
message counts and summaries (marking the current project's session).
- `/load <id>` — load a saved session (exact id or unique prefix) and
continue from there.
- `/compact` — compact context down to the system message and a concise
summary using the LLM; the compaction prompt is appended to the
conversation to derive the summary, then the conversation is reset to
`[system, summary-user-message]`.
- `/cfg <param> [val]` — inspect or update a configuration parameter live
(writes to `.bantam.cfg`).
- `/model [val]` — alias for `/cfg model` (inspect or set the model).
- `/endpoint [val]` — alias for `/cfg endpoint` (inspect or set the API
endpoint).
- `/skill` — with no argument, list every skill under the configured
skills directory (or report that none are configured); with
`<name> [prompt]`, load `<skills_dir>/<name>/SKILL.md` (or an absolute
path when no skills dir is set) and send its contents prefixed to the
optional `prompt` as the next user turn.
- `/models` — query the `/models` path on the current inference endpoint
and print a plain list of supported model IDs, marking the currently
configured model with a leading `* `.
- `!<cmd>` — execute a shell command directly through `bash` without
adding the result to the conversation context.
- `/help` — show all supported commands.
- `/clear` — reset the conversation to just the system prompt.
- `/quit` — exit.
When context-window usage reaches **60% or higher**, Bantam automatically
offers to compact the conversation:
```text
Context usage is at 62.4% (163840 / 262144 tokens). Compact conversation? [Y/n]:
```
Pressing **Ctrl+C** during an active inference run or long-running shell
execution cleanly interrupts the turn without appending partial or
malformed responses to the conversation context.
The current conversation is also **auto-saved** to
`~/.bantam/sessions/<project-md5>.json` after every turn, on `/clear`,
`/load`, `/compact`, `/continue`, and on exit — so you can always run
`/continue` (or `/cont`) to resume where you left off.
3. File input mode:
```bash
./bantam prompt.txt
```
If the file's first line (or the argument) starts with `!`, the rest is run
as a direct shell command through `bash` and the program exits (the output
is not added to the conversation).
## Model configuration parameters
(shared with the Go and C implementations; `.bantam.cfg` takes priority over
`model.cfg` when both exist — both are plain `key=value` with `#` comments.
Values defined in `.bantam.cfg` override `BANTAM_*` environment variables;
when `.bantam.cfg` is absent, `BANTAM_*` environment variables override
`model.cfg` and built-in defaults.)
- `endpoint` (base OpenAI-compatible API URL, default
`https://api.kilo.ai/api/openrouter`; falls back to `BANTAM_ENDPOINT`).
- `model` (model name, default `openrouter/free`; falls back to
`BANTAM_MODEL`).
- `temperature` (model temperature, default `0.7`; falls back to
`BANTAM_TEMP` or `BANTAM_TEMPERATURE`).
- `api_key` (API key / Bearer token, optional; falls back to
`BANTAM_API_KEY`; set to `-` or left blank in the config file to explicitly
defer to `BANTAM_API_KEY`).
- `stream` (stream response tokens in real-time, default `true`; falls back to
`BANTAM_STREAM`).
- `color` (ANSI coloring: `auto` (TTY-detected, default), `always`, or
`never`; disabled by `NO_COLOR` / `BANTAM_NO_COLOR` env vars, falls back to
`BANTAM_COLOR`).
- `timeout` (HTTP timeout in seconds for LLM API calls, default `300`; bounds
connection setup and time-to-first-byte so long streaming responses are not
cut off mid-stream; falls back to `BANTAM_TIMEOUT`).
- `shell_timeout` (timeout in seconds for `bash` shell commands, default
`120`; falls back to `BANTAM_SHELL_TIMEOUT`).
- `max_al_iterations` (max tool-call loop iterations per `AL()` invocation,
default `1000`; falls back to `BANTAM_MAX_AL_ITERATIONS`).
- `context_window` (context window size in tokens, auto-discovered from
`/models` API if available, fallback to this setting, default `262144`;
falls back to `BANTAM_CONTEXT_WINDOW`).
- `max_tool_res` (maximum number of bytes of a tool result that may be sent
into the model context directly, default `65536`; larger results are written
to a unique file under `$TMPDIR/bantam/toolres` and replaced by a short
instruction naming the file and its line count so it can be read in parts
using the `read` tool, and the file is deleted once the round ends; falls
back to `BANTAM_MAX_TOOL_RES`).
- `reasoning_effort` (reasoning effort level, forwarded to chat completions
API, default `high`; falls back to `BANTAM_REASONING_EFFORT`).
- `bantam_tools_dir` (optional path to a directory of extra shell tools; when
set, Bantam appends `"Extra shell tools can be found at <dir>"` to the
system prompt at startup. Falls back to `BANTAM_TOOLS_DIR`. Agent-internal
hint, not forwarded to the API.)
- `bantam_skills_dir` (optional path to a directory of reusable *skills*, each
in its own subdirectory as `<skill_name>/SKILL.md`. When set, Bantam appends
`"Skills may be discovered and invoked from <dir>"` to the system prompt and
the `/skill <skill_name> [prompt]` command loads a skill's `SKILL.md` and
runs it as a prompt. Falls back to `BANTAM_SKILLS_DIR`. Agent-internal
hint, not forwarded to the API.)
The C++ port supports SOCKS5 proxying via the `SOCKS_PROXY` (or
`socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080`
or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` /
`HTTPS_PROXY` environment variables.
When enabled, the interactive console uses a subtle ANSI palette: the
pending-request status `...requesting...` is dimmed bold, reasoning markers
are cyan, reasoning text is dim, `[tool call: ...]` traces are yellow,
`[tool result: ...]` headers are green, tool result bodies are dim (red for
tool errors / unknown tools), and errors / network retries are red. Tool
result payloads fed back to the LLM are never colored.
## The agentic loop
1. **Initialization**: read the config file (`.bantam.cfg` if present, else
`model.cfg`). Discover the context-window size from the `/models` endpoint
(or fall back to `context_window`, default `262144` tokens). Prepare the
messages array starting with the built-in system prompt.
2. **Input processing**: take the user prompt (command-line file parameter or
interactive stdin). If prefixed with `!`, execute the command directly via
`bash` without appending to the conversation context. Otherwise append
`{"role": "user", "content": prompt}` and invoke `AL(cfg, messages)`.
3. **Agentic loop (`AL`)**:
- Send `messages` and tool definitions (`bash`, `read`, `write`) to the
OpenAI-compatible `/chat/completions` API with custom request headers (see
below) and `stream_options: {"include_usage": true}`.
- Support context cancellation (on `SIGINT` / Ctrl+C) to cleanly abort
in-flight requests without appending incomplete messages.
- On network or HTTP failure, retry using Fibonacci backoff delays
(`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`). A `4xx` error other than the
recoverable invalid-assistant-message case fast-fails.
- If `stream=true`, parse SSE data chunks (`data: {...}`) in real time to
stream reasoning content (`reasoning_content`) and response text directly
to stdout, bracketing the reasoning block with
`--- reasoning start ---` / `--- reasoning end ---` markers, rendering
Markdown and tables constrained to terminal width.
- Reconstruct the assistant message and track usage tokens
(`prompt_tokens`, `completion_tokens`, cached tokens). If `tool_calls`
exist, trace the call (`[tool call: name(args)]`), validate JSON
arguments, execute the requested tool (`bash`, `read`, or `write`; also
accepting legacy `shell_exec` and `write_file`), trace the result
(`[tool result: name]`), append the tool response
`{"role": "tool", "tool_call_id": id, "content": result}`, and repeat the
loop.
- If no tool calls remain or `max_al_iterations` is reached, return the
updated messages list and turn usage stats.
4. **Post-turn reporting & compaction**:
- Display token usage and context-window percentage.
- If context usage is `>= 60%`, prompt the user to compact.
- Compaction appends a compaction prompt as a user message to derive the
summary, invokes the LLM, and then resets the conversation to the system
prompt plus a `user` message carrying the resulting summary.
If the API rejects the request with an `Invalid assistant message: content or
tool_calls must be set` error (usually caused by a previously cut-off stream
that left an empty assistant message in the session), Bantam strips the last
`assistant`-role message from the session and retries the call.
### Custom request headers
All endpoints receive custom request headers mimicking OpenCode:
`User-Agent: opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`,
`x-opencode-client: cli`, `x-opencode-project: global`,
`x-opencode-session: <ses_...>`, and a fresh `x-opencode-request: <msg_...>`
per call (the 12-hex timestamp prefix in `x-opencode-request` is the bitwise
inverse of `x-opencode-session`). The `Authorization: Bearer <api_key>` header
is added whenever `api_key` is set and not `-`.
### Prompt-injection defense
Tool arguments and tool outputs are sanitized by filtering out non-printable
and space-like Unicode characters (keeping ASCII space, tab, newline, and
printable Unicode), protecting against indirect prompt injection before
anything is sent back to the model.
## Tool call definitions
### `read` tool
- Parameters: `filePath` (string, required; also accepts `path`); `offset`
(integer, optional, 1-indexed, default 1); `limit` (integer, optional,
default 2000).
- Action: if the target is a regular file, returns numbered lines in
`<line>: <content>` format; if a directory, returns a list of directory
entries (subdirectories include a trailing `/`).
### `write` tool
- Parameters: `filePath` (string, required; also accepts `path`); `offset`
(integer, optional, byte offset); `del_bytes` (integer, optional); `content`
(string, required, may be empty).
- Action: if `offset` and `del_bytes` are both omitted, the entire file is
overwritten with `content` (the file and any necessary parent directories
are created if missing); otherwise `content` is written at `offset`,
optionally deleting `del_bytes` bytes first (splicing prefix + content +
suffix and never appending to the end of an existing file). (Also accepted as
legacy `write_file`.)
### `bash` tool
- Parameters: `command` (string, required); `workdir` (string, optional).
- Action: run `command` via `bash -c` subject to `shell_timeout`
(default `120s`) and return `output + '\nexit: ' + exit_code`. (Also
accepted as legacy `shell_exec`.)
## Extra tools and skills
Because Bantam's built-in tools include `bash`, helper scripts can be invoked
directly by the agent through `bash`. Point Bantam at a directory of helpers
with the `bantam_tools_dir` key (or `BANTAM_TOOLS_DIR`); when set, the line
`Extra shell tools can be found at <dir>` is appended to the system prompt. A
*skill* is a reusable instruction bundle: a directory containing a `SKILL.md`
file, pointed to by `bantam_skills_dir` (or `BANTAM_SKILLS_DIR`), and run with
`/skill <name> [prompt]`.
## Memory safety
The C++ port leans on the language's standard library and RAII to minimize
memory-management hazards: all dynamic text uses `std::string`, message
collections use `std::vector`, and curl header lists and streaming state are
wrapped in RAII types (`CurlHeaders`, `StreamCtx`) that free themselves on
scope exit. There is no hand-rolled allocator and no manual `free` of message
or tool-call data, so use-after-free and double-free are structurally
unlikely. The vendored nlohmann/json library is used for all JSON parsing and
serialization instead of the unsafe json-c C API.
## Credits
Created by Luxferre in 2026, released into the public domain with no
warranties.
+195
View File
@@ -0,0 +1,195 @@
/* Bantam: tiny, powerful, DIY AI agent — modern C++20 port
* Created by Luxferre in 2026, released into the public domain.
*
* This is a C++20 rewrite of the pure-C port (bantam-c). It talks to any
* OpenAI-compatible /chat/completions API and implements the same agentic
* loop, tools, session management, markdown terminal rendering and
* raw-terminal line editor.
*
* Design goals relative to the C port:
* - Use standard, safe C++ containers (std::string, std::vector,
* std::map, std::span) instead of raw memory areas.
* - Eliminate hand-rolled allocation/string code; rely on RAII so there
* is far less room for memory-allocation related vulnerabilities.
* - Depend only on libcurl (HTTP) plus the vendored single-header
* nlohmann/json library; the unsafe json-c C API is no longer used.
*
* Feature macros: we rely on POSIX.1-2008 interfaces. */
#ifndef BANTAM_HPP
#define BANTAM_HPP
#include <cstdarg>
#include <cstdint>
#include <map>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <vector>
/* Markdown renderer state (code-fence tracking). */
struct MdState {
bool in_code = false;
};
/* ------------------------------------------------------------------ */
/* Tool call / message / usage / config structures. */
/* ------------------------------------------------------------------ */
struct ToolCall {
std::string id;
std::string name;
std::string arguments; /* raw JSON text of the arguments object */
};
struct Message {
std::string role;
std::string content; /* empty when absent */
std::string reasoning; /* empty when absent */
std::vector<ToolCall> calls;
std::string tool_call_id; /* empty when absent (tool role only) */
int calls_shown = 0;
};
struct Messages {
std::vector<Message> items;
};
struct Usage {
int prompt_tokens = 0;
int completion_tokens = 0;
int total_tokens = 0;
int cached_tokens = 0;
int context_tokens = 0;
std::string model;
};
struct Cfg {
std::string endpoint;
std::string model;
std::string api_key;
std::string color;
double temperature = 0.7;
int timeout = 300;
int shell_timeout = 120;
int max_al_iterations = 1000;
int stream = 1; /* 0/1 */
int force_stream = -1; /* -1 = auto, 0/1 = force (compaction) */
int context_window = 262144;
int max_tool_res = 65536;
std::map<std::string, std::string> raw; /* raw key/value map */
};
struct Session {
std::string id;
std::string created;
std::string summary;
Messages messages;
};
/* ------------------------------------------------------------------ */
/* Globals. */
/* ------------------------------------------------------------------ */
extern int g_col; /* color enabled (0/1) */
extern volatile int g_interrupted; /* SIGINT flag */
/* ------------------------------------------------------------------ */
/* util.cpp */
/* ------------------------------------------------------------------ */
void setup_signal_handler();
int is_terminal(int fd);
int to_int_val(std::string_view s, int def);
std::string sfmt(const char *fmt, ...);
void str_appendf(std::string &s, const char *fmt, ...);
/* UTF-8 helpers. */
int utf8_decode(const unsigned char *p, size_t avail, size_t *plen);
std::string_view utf8_truncate(std::string_view s, size_t max_bytes);
std::string filter_text(std::string_view s);
int color_enabled(const Cfg &cfg);
std::string cstr(std::string_view text, std::span<const int> codes);
void cprint(std::string_view text, std::span<const int> codes);
int visible_len(std::string_view s);
int rune_width(int cp);
void mkdir_p(std::string_view dir);
std::string project_id();
std::string bantam_tmp_dir();
std::string tool_res_dir();
std::string md5_hex(std::string_view data);
/* ------------------------------------------------------------------ */
/* config.cpp */
/* ------------------------------------------------------------------ */
const std::string *cfg_raw_get(const Cfg &c, std::string_view k);
bool internal_key(std::string_view k);
Cfg get_cfg(std::string_view path);
int set_cfg(std::string_view path, std::string_view key, std::string_view val);
std::string config_path();
int fetch_context_window(Cfg &cfg);
std::string list_models(Cfg &cfg, int *errout);
std::string tools_dir(const Cfg &c);
std::string skills_dir(const Cfg &c);
/* ------------------------------------------------------------------ */
/* json.cpp — request building & response parsing (nlohmann/json). */
/* ------------------------------------------------------------------ */
std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream);
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage);
/* ------------------------------------------------------------------ */
/* llm.cpp — HTTP client (libcurl), headers, backoff, dispatch. */
/* ------------------------------------------------------------------ */
int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code,
std::string &errmsg);
int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
std::string &errmsg, volatile int *interrupted);
/* ------------------------------------------------------------------ */
/* tools.cpp */
/* ------------------------------------------------------------------ */
std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
int timeout, volatile int *interrupted);
std::string read_file_or_dir(std::string_view path, int offset, int limit,
int *style);
std::string write_file(std::string_view path, int offset, int del_bytes,
std::string_view content, int *err);
std::string offload_tool_result(std::string_view res, int max_res,
std::vector<std::string> &tmps);
void cleanup_temps(std::vector<std::string> &tmps);
/* ------------------------------------------------------------------ */
/* session.cpp */
/* ------------------------------------------------------------------ */
void save_session(const Messages &msgs, std::string_view sid,
std::string *out_id, std::string *out_summary);
std::vector<Session> sessions_load();
std::unique_ptr<Messages> load_session(std::string_view sid, std::string *err);
std::unique_ptr<Session> find_project_session();
void autosave(const Messages &msgs);
std::unique_ptr<Messages> compact(Cfg &cfg, const Messages &msgs,
std::string *summary, std::string *err,
volatile int *interrupted);
std::string summary_of(const Messages &msgs);
/* ------------------------------------------------------------------ */
/* render.cpp — terminal markdown rendering. */
/* ------------------------------------------------------------------ */
void render_md_print(std::string_view text);
void render_md_line_print(std::string_view line, MdState &st);
void render_table_print(const std::vector<std::string> &lines);
bool is_table_line(std::string_view line);
/* ------------------------------------------------------------------ */
/* editor.cpp — GNU readline line editor + history (+ term helpers). */
/* ------------------------------------------------------------------ */
void load_history();
void save_history();
void add_history(std::string_view s);
std::string read_line(std::string_view prompt, bool &ok);
std::string read_plain(std::string_view prompt, bool &ok);
int term_width();
#endif /* BANTAM_HPP */
+276
View File
@@ -0,0 +1,276 @@
/* config.cpp — configuration file parsing, env precedence, model discovery. */
#include "bantam.hpp"
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <nlohmann/json.hpp>
#include <fstream>
#include <set>
#include <sys/stat.h>
#include <unistd.h>
#include <strings.h>
using json = nlohmann::json;
/* ----------------------------- raw map -------------------------------- */
static void cfg_raw_set(Cfg &c, std::string_view k, std::string_view v) {
c.raw[std::string(k)] = std::string(v);
}
static void cfg_init(Cfg &c) {
c = Cfg{};
c.endpoint = "https://api.kilo.ai/api/openrouter";
c.model = "openrouter/free";
c.api_key = "-";
c.temperature = 0.7;
c.timeout = 300;
c.shell_timeout = 120;
c.max_al_iterations = 1000;
c.stream = 1;
c.force_stream = -1;
c.color = "auto";
c.context_window = 262144;
c.max_tool_res = 65536;
cfg_raw_set(c, "endpoint", c.endpoint);
cfg_raw_set(c, "model", c.model);
cfg_raw_set(c, "temperature", "0.7");
cfg_raw_set(c, "api_key", c.api_key);
cfg_raw_set(c, "stream", "true");
cfg_raw_set(c, "color", "auto");
cfg_raw_set(c, "timeout", "300");
cfg_raw_set(c, "shell_timeout", "120");
cfg_raw_set(c, "max_al_iterations", "1000");
cfg_raw_set(c, "context_window", "262144");
cfg_raw_set(c, "max_tool_res", "65536");
cfg_raw_set(c, "reasoning_effort", "high");
}
const std::string *cfg_raw_get(const Cfg &c, std::string_view k) {
auto it = c.raw.find(std::string(k));
return it == c.raw.end() ? nullptr : &it->second;
}
bool internal_key(std::string_view k) {
static const char *keys[] = {
"endpoint", "model", "temperature", "stream", "api_key", "timeout",
"shell_timeout", "max_al_iterations", "color", "context_window", "max_tool_res",
"bantam_tools_dir", "bantam_skills_dir", nullptr};
for (int i = 0; keys[i]; i++) if (k == keys[i]) return true;
return false;
}
/* ----------------------------- parsing -------------------------------- */
/* parse_cfg_file records every key it sees in *file_keys so that the
* config file can take precedence over environment variables. */
static void parse_cfg_file(std::string_view path, Cfg &c, std::set<std::string> *file_keys) {
std::ifstream f(std::string(path), std::ios::binary);
if (!f) return;
std::string line;
while (std::getline(f, line)) {
std::string_view s(line);
while (!s.empty() && std::isspace((unsigned char)s.front())) s.remove_prefix(1);
if (s.empty() || s.front() == '#') continue;
auto eq = s.find('=');
if (eq == std::string_view::npos) continue;
std::string_view k = s.substr(0, eq);
std::string_view v = s.substr(eq + 1);
while (!k.empty() && std::isspace((unsigned char)k.back())) k.remove_suffix(1);
while (!v.empty() && std::isspace((unsigned char)v.front())) v.remove_prefix(1);
while (!v.empty() && std::isspace((unsigned char)v.back())) v.remove_suffix(1);
cfg_raw_set(c, k, v);
if (file_keys) file_keys->insert(std::string(k));
if (k == "endpoint") c.endpoint = std::string(v);
else if (k == "model") c.model = std::string(v);
else if (k == "api_key") c.api_key = std::string(v);
else if (k == "temperature") c.temperature = std::strtod(std::string(v).c_str(), nullptr);
else if (k == "timeout") c.timeout = to_int_val(v, c.timeout);
else if (k == "shell_timeout") c.shell_timeout = to_int_val(v, c.shell_timeout);
else if (k == "max_al_iterations") c.max_al_iterations = to_int_val(v, c.max_al_iterations);
else if (k == "stream") c.stream = (v == "true" || v == "1" || v == "yes");
else if (k == "color") c.color = std::string(v);
else if (k == "context_window") c.context_window = to_int_val(v, c.context_window);
else if (k == "max_tool_res") c.max_tool_res = to_int_val(v, c.max_tool_res);
}
}
/* Environment variables act as a FALLBACK: they only fill in values that the
* config file did not explicitly set. The config file (.bantam.cfg) is the
* single source of truth. */
static void apply_env_cfg(Cfg &c, const std::set<std::string> &file_keys) {
auto ev = [&](const char *key, const char *envname) -> const char * {
if (file_keys.count(key)) return nullptr;
const char *e = std::getenv(envname);
return (e && *e) ? e : nullptr;
};
const char *e;
if ((e = ev("endpoint", "BANTAM_ENDPOINT"))) { c.endpoint = e; cfg_raw_set(c, "endpoint", e); }
if ((e = ev("model", "BANTAM_MODEL"))) { c.model = e; cfg_raw_set(c, "model", e); }
if ((e = ev("temperature", "BANTAM_TEMP"))) { c.temperature = std::strtod(e, nullptr); cfg_raw_set(c, "temperature", e); }
else if ((e = ev("temperature", "BANTAM_TEMPERATURE"))) { c.temperature = std::strtod(e, nullptr); cfg_raw_set(c, "temperature", e); }
if ((e = ev("stream", "BANTAM_STREAM"))) { c.stream = (std::strcmp(e, "true") == 0 || std::strcmp(e, "1") == 0 || std::strcmp(e, "yes") == 0); cfg_raw_set(c, "stream", e); }
if ((e = ev("color", "BANTAM_COLOR"))) { c.color = e; cfg_raw_set(c, "color", e); }
if ((e = ev("timeout", "BANTAM_TIMEOUT"))) { c.timeout = to_int_val(e, c.timeout); cfg_raw_set(c, "timeout", e); }
if ((e = ev("shell_timeout", "BANTAM_SHELL_TIMEOUT"))) { c.shell_timeout = to_int_val(e, c.shell_timeout); cfg_raw_set(c, "shell_timeout", e); }
if ((e = ev("max_al_iterations", "BANTAM_MAX_AL_ITERATIONS"))) { c.max_al_iterations = to_int_val(e, c.max_al_iterations); cfg_raw_set(c, "max_al_iterations", e); }
if ((e = ev("context_window", "BANTAM_CONTEXT_WINDOW"))) { c.context_window = to_int_val(e, c.context_window); cfg_raw_set(c, "context_window", e); }
if ((e = ev("max_tool_res", "BANTAM_MAX_TOOL_RES"))) { c.max_tool_res = to_int_val(e, c.max_tool_res); cfg_raw_set(c, "max_tool_res", e); }
if ((e = ev("reasoning_effort", "BANTAM_REASONING_EFFORT"))) cfg_raw_set(c, "reasoning_effort", e);
if ((e = ev("bantam_tools_dir", "BANTAM_TOOLS_DIR"))) cfg_raw_set(c, "bantam_tools_dir", e);
if ((e = ev("bantam_skills_dir", "BANTAM_SKILLS_DIR"))) cfg_raw_set(c, "bantam_skills_dir", e);
}
Cfg get_cfg(std::string_view path) {
Cfg c;
cfg_init(c);
std::set<std::string> file_keys;
parse_cfg_file(path, c, &file_keys);
apply_env_cfg(c, file_keys);
/* api_key is usually only supplied via the environment; let it override a
* placeholder ("-" or empty) left in the config file. */
if (c.api_key.empty() || c.api_key == "-") {
const char *k = std::getenv("BANTAM_API_KEY");
if (k && *k) { c.api_key = k; cfg_raw_set(c, "api_key", k); }
}
return c;
}
int set_cfg(std::string_view path, std::string_view key, std::string_view val) {
std::vector<std::string> lines;
{
std::ifstream f(std::string(path), std::ios::binary);
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
size_t i = 0, n = content.size();
while (i < n) {
size_t j = content.find('\n', i);
if (j == std::string::npos) { lines.push_back(content.substr(i)); break; }
lines.push_back(content.substr(i, j - i + 1));
i = j + 1;
}
}
bool found = false;
for (auto &line : lines) {
std::string_view sv(line);
size_t k = 0;
while (k < sv.size() && std::isspace((unsigned char)sv[k])) k++;
if (k < sv.size() && sv[k] == '#') continue;
auto eq = sv.find('=');
if (eq == std::string_view::npos) continue;
std::string_view keyPart = sv.substr(k, eq - k);
while (!keyPart.empty() && std::isspace((unsigned char)keyPart.back())) keyPart.remove_suffix(1);
if (keyPart == key) {
line = sfmt("%s=%s\n", std::string(key).c_str(), std::string(val).c_str());
found = true;
}
}
if (!found) {
if (!lines.empty()) {
std::string &last = lines.back();
if (!last.empty() && last.back() != '\n') last += '\n';
}
lines.push_back(sfmt("%s=%s\n", std::string(key).c_str(), std::string(val).c_str()));
}
std::ofstream out(std::string(path), std::ios::binary);
if (!out) return -1;
for (auto &line : lines) out << line;
return 0;
}
std::string config_path() {
/* .bantam.cfg is the sole configuration file. */
return ".bantam.cfg";
}
/* ----------------------------- model discovery ------------------------ */
static int query_models_context_window(Cfg &cfg) {
std::string url = cfg.endpoint;
while (!url.empty() && url.back() == '/') url.pop_back();
url += "/models";
std::string body;
long code = 0;
std::string err;
if (http_get(cfg, url, body, code, err) != 0) return 0;
if (code >= 400) return 0;
int cw = 0;
try {
auto root = json::parse(body);
if (!root.is_object()) return 0;
json list = nullptr;
if (root.contains("data")) list = root["data"];
else if (root.contains("models")) list = root["models"];
if (list.is_array()) {
static const char *keys[] = {"context_window", "context_length", "max_context_length",
"max_model_len", "context_size", "max_tokens", "max_input_tokens", nullptr};
for (auto &item : list) {
if (cw != 0) break;
if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue;
std::string id = item["id"].get<std::string>();
if (!id.empty() && (id == cfg.model || strcasecmp(id.c_str(), cfg.model.c_str()) == 0)) {
for (int k = 0; keys[k]; k++) {
if (item.contains(keys[k]) && item[keys[k]].is_number_integer()) {
int v = item[keys[k]].get<int>();
if (v > 0) { cw = v; break; }
}
}
}
}
}
} catch (...) { return 0; }
return cw;
}
int fetch_context_window(Cfg &cfg) {
static std::map<std::string, int> cache;
std::string key = cfg.endpoint + std::string(1, '\0') + cfg.model;
auto it = cache.find(key);
if (it != cache.end()) return it->second;
int cw = query_models_context_window(cfg);
if (cw > 0) { cache[key] = cw; return cw; }
const std::string *v = cfg_raw_get(cfg, "context_window");
if (v && !v->empty()) return to_int_val(*v, 262144);
return 262144;
}
std::string list_models(Cfg &cfg, int *errout) {
std::string url = cfg.endpoint;
while (!url.empty() && url.back() == '/') url.pop_back();
url += "/models";
std::string body;
long code = 0;
std::string err;
if (http_get(cfg, url, body, code, err) != 0) { *errout = -1; return err.empty() ? "request failed" : err; }
if (code >= 400) { *errout = -1; return "HTTP error from /models"; }
std::string out;
try {
auto root = json::parse(body);
if (!root.is_object()) { *errout = -1; return "invalid JSON from /models"; }
json list = nullptr;
if (root.contains("data")) list = root["data"];
else if (root.contains("models")) list = root["models"];
bool any = false;
if (list.is_array()) {
for (auto &item : list) {
if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue;
std::string id = item["id"].get<std::string>();
if (id.empty()) continue;
any = true;
if (id == cfg.model) str_appendf(out, "* %s\n", id.c_str());
else str_appendf(out, " %s\n", id.c_str());
}
}
if (!any) return "No models returned by the endpoint.";
} catch (...) {
*errout = -1;
return "invalid JSON from /models";
}
*errout = 0;
return out;
}
static std::string cfg_or_env(const Cfg &c, std::string_view key, const char *env) {
const std::string *v = cfg_raw_get(c, key);
if (v && !v->empty()) return *v;
const char *e = std::getenv(env);
if (e && *e) return e;
return "";
}
std::string tools_dir(const Cfg &c) { return cfg_or_env(c, "bantam_tools_dir", "BANTAM_TOOLS_DIR"); }
std::string skills_dir(const Cfg &c) { return cfg_or_env(c, "bantam_skills_dir", "BANTAM_SKILLS_DIR"); }
+92
View File
@@ -0,0 +1,92 @@
/* editor.cpp — GNU readline line editor, history and term helpers
* (is_terminal, term_width). The interactive prompt is handled by
* libreadline instead of a hand-rolled raw-mode terminal editor. */
#include "bantam.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <readline/readline.h>
#include <readline/history.h>
#include <sys/ioctl.h>
#include <unistd.h>
/* ----------------------------- globals ------------------------------- */
static std::string g_hist_file;
static std::string home_dir() {
const char *h = std::getenv("HOME");
if (h && *h) return h;
return ".";
}
/* ----------------------------- history -------------------------------- */
void load_history() {
g_hist_file = home_dir() + "/.bantam_history";
using_history();
/* A missing history file is not an error. */
read_history(g_hist_file.c_str());
}
void add_history(std::string_view s) {
if (s.empty()) return;
std::string line(s);
/* Skip an entry identical to the immediately preceding one. */
if (history_length > 0) {
HIST_ENTRY *last = history_get(history_base + history_length - 1);
if (last && last->line && line == last->line) return;
}
::add_history(line.c_str());
}
void save_history() {
if (g_hist_file.empty()) return;
write_history(g_hist_file.c_str());
}
/* ----------------------------- read helpers --------------------------- */
std::string read_plain(std::string_view prompt, bool &ok) {
if (!prompt.empty()) fputs(std::string(prompt).c_str(), stdout);
fflush(stdout);
char *line = nullptr; size_t cap = 0; ssize_t n = getline(&line, &cap, stdin);
if (n < 0) { free(line); clearerr(stdin); ok = false; return ""; }
while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = '\0';
std::string r(line, (size_t)n);
free(line);
ok = true;
return r;
}
/* ----------------------------- readline editor ------------------------ */
std::string read_line(std::string_view prompt, bool &ok) {
if (!is_terminal(STDIN_FILENO)) return read_plain(prompt, ok);
/* Enter (\r) submits the line; Ctrl+J inserts a literal newline so a
* multi-line prompt can be composed before submitting. */
rl_bind_key('\n', rl_insert);
char *line = readline(prompt.empty() ? "" : std::string(prompt).c_str());
if (!line) { ok = false; return ""; }
std::string r(line);
free(line);
ok = true;
return r;
}
/* ----------------------------- term helpers --------------------------- */
int is_terminal(int fd) { return isatty(fd) != 0; }
int term_width() {
#ifdef TIOCGWINSZ
struct winsize ws{};
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col;
#endif
const char *c = std::getenv("COLUMNS");
if (c && *c) { int n = std::atoi(c); if (n > 0) return n; }
FILE *p = popen("tput cols 2>/dev/null", "r");
if (p) {
char buf[32];
if (fgets(buf, sizeof buf, p)) {
int n = std::atoi(buf);
pclose(p);
if (n > 0) return n;
} else pclose(p);
}
return 80;
}
+151
View File
@@ -0,0 +1,151 @@
/* json.cpp — request payload building and non-streaming response parsing. */
#include "bantam.hpp"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
/* The tool definitions, identical to the Go reference implementation. */
static const char *TOOLS_JSON = R"JSON([
{"type":"function","function":{
"name":"bash",
"description":"Executes a given bash command in a shell session, returning its output and exit code.",
"parameters":{"type":"object","properties":{
"command":{"type":"string","description":"The command to execute"},
"workdir":{"type":"string","description":"The working directory to run the command in. Defaults to the current directory."}
},"required":["command"]}}},
{"type":"function","function":{
"name":"read",
"description":"Read a file or directory from the local filesystem. For files, returns numbered lines prefixed as `<line>: <content>`. For directories, returns entry names with trailing slashes for subdirectories.",
"parameters":{"type":"object","properties":{
"filePath":{"type":"string","description":"The path to the file or directory to read"},
"offset":{"type":"integer","description":"The line number to start reading from (1-indexed, defaults to 1)"},
"limit":{"type":"integer","description":"The maximum number of lines to read (defaults to 2000)"}
},"required":["filePath"]}}},
{"type":"function","function":{
"name":"write",
"description":"Write content to a file. If offset and del_bytes are both omitted, the entire file is overwritten with the new content; otherwise content is written at the given byte offset, optionally deleting bytes first.",
"parameters":{"type":"object","properties":{
"filePath":{"type":"string","description":"The path to the file to write"},
"content":{"type":"string","description":"The content to write to the file"},
"offset":{"type":"integer","description":"Byte offset to start writing from. If omitted together with del_bytes, the whole file is overwritten instead. Defaults to 0 (start of file)."},
"del_bytes":{"type":"integer","description":"Bytes to delete starting at offset. If omitted together with offset, the whole file is overwritten instead."}
},"required":["filePath","content"]}}}
])JSON";
static void serialize_message(json &obj, const Message &m) {
obj["role"] = m.role.empty() ? "" : m.role;
if (!m.content.empty()) obj["content"] = m.content;
if (!m.reasoning.empty()) obj["reasoning_content"] = m.reasoning;
if (!m.calls.empty()) {
json arr = json::array();
for (auto &tc : m.calls) {
json t;
if (!tc.id.empty()) t["id"] = tc.id;
t["type"] = "function";
json fn;
fn["name"] = tc.name.empty() ? "" : tc.name;
fn["arguments"] = tc.arguments.empty() ? "{}" : tc.arguments;
t["function"] = fn;
arr.push_back(t);
}
obj["tool_calls"] = arr;
}
if (!m.tool_call_id.empty()) obj["tool_call_id"] = m.tool_call_id;
}
std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream) {
json root = json::object();
root["model"] = cfg.model;
root["temperature"] = cfg.temperature;
json arr = json::array();
for (auto &m : msgs.items) {
json mo;
serialize_message(mo, m);
arr.push_back(mo);
}
root["messages"] = arr;
root["stream"] = stream;
if (use_tools) {
try {
json tools = json::parse(TOOLS_JSON);
root["tools"] = tools;
} catch (...) {}
}
if (stream) {
json so = json::object();
so["include_usage"] = true;
root["stream_options"] = so;
}
/* Forward non-internal raw params, parsed as JSON when possible. */
for (auto &kv : cfg.raw) {
if (internal_key(kv.first)) continue;
try {
json jv = json::parse(kv.second);
root[kv.first] = jv;
} catch (...) {
root[kv.first] = kv.second;
}
}
return root.dump(-1, ' ', false, json::error_handler_t::replace);
}
static std::string json_str(const json &j, const char *k, std::string_view def = "") {
if (j.is_object() && j.contains(k) && j[k].is_string())
return j[k].get<std::string>();
return std::string(def);
}
static int json_int(const json &j, const char *k, int def = 0) {
if (j.is_object() && j.contains(k) && j[k].is_number_integer())
return j[k].get<int>();
return def;
}
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage) {
out = Message{};
usage = Usage{};
try {
auto root = json::parse(body);
if (!root.is_object() || !root.contains("choices") ||
!root["choices"].is_array() || root["choices"].empty())
return false;
auto ch = root["choices"][0];
if (!ch.is_object() || !ch.contains("message") || !ch["message"].is_object())
return false;
auto msg = ch["message"];
out.role = json_str(msg, "role", "assistant");
out.content = json_str(msg, "content");
std::string reas = json_str(msg, "reasoning_content");
if (reas.empty()) reas = json_str(msg, "reasoning");
if (reas.empty()) reas = json_str(msg, "thought");
if (!reas.empty()) out.reasoning = std::move(reas);
if (msg.contains("tool_calls") && msg["tool_calls"].is_array()) {
for (auto &tc : msg["tool_calls"]) {
if (!tc.is_object()) continue;
ToolCall t;
t.id = json_str(tc, "id");
if (tc.contains("function") && tc["function"].is_object()) {
auto fn = tc["function"];
t.name = json_str(fn, "name");
t.arguments = json_str(fn, "arguments", "{}");
}
out.calls.push_back(std::move(t));
}
}
if (root.contains("usage") && root["usage"].is_object()) {
auto u = root["usage"];
usage.prompt_tokens = json_int(u, "prompt_tokens");
usage.completion_tokens = json_int(u, "completion_tokens");
usage.total_tokens = json_int(u, "total_tokens");
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object())
usage.cached_tokens = json_int(u["prompt_tokens_details"], "cached_tokens");
if (usage.cached_tokens == 0)
usage.cached_tokens = json_int(u, "cached_tokens");
}
usage.model = json_str(root, "model");
return true;
} catch (...) {
return false;
}
}
+492
View File
@@ -0,0 +1,492 @@
/* llm.cpp — libcurl HTTP client, Fibonacci backoff,
* streaming SSE parser and the llm() dispatch function. */
#include "bantam.hpp"
#include <array>
#include <chrono>
#include <thread>
#include <cstring>
#include <curl/curl.h>
#include <memory>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
/* ----------------------------- memory callback ------------------------ */
static size_t mem_cb(char *ptr, size_t sz, size_t nm, void *ud) {
std::string *m = (std::string *)ud;
size_t total = sz * nm;
m->append((const char *)ptr, total);
return total;
}
/* ----------------------------- curl RAII ------------------------------ */
struct CurlHandle {
CURL *c;
CurlHandle() : c(curl_easy_init()) {}
~CurlHandle() { if (c) curl_easy_cleanup(c); }
CurlHandle(const CurlHandle &) = delete;
CurlHandle &operator=(const CurlHandle &) = delete;
explicit operator bool() const { return c != nullptr; }
};
struct CurlHeaders {
curl_slist *list = nullptr;
CurlHeaders() = default;
~CurlHeaders() { if (list) curl_slist_free_all(list); }
CurlHeaders(const CurlHeaders &) = delete;
CurlHeaders &operator=(const CurlHeaders &) = delete;
void append(std::string_view s) {
std::string tmp(s);
curl_slist *next = curl_slist_append(list, tmp.c_str());
if (next) list = next;
}
};
/* ----------------------------- proxy + headers ------------------------ */
static void set_proxy(CURL *c) {
const char *socks = std::getenv("SOCKS_PROXY");
if (!socks || !*socks) socks = std::getenv("socks_proxy");
if (socks && *socks) {
std::string buf;
if (std::strstr(socks, "://")) buf = socks;
else buf = "socks5://" + std::string(socks);
curl_easy_setopt(c, CURLOPT_PROXY, buf.c_str());
}
}
static void apply_llm_headers(CurlHeaders &h, const Cfg &cfg) {
h.append("Content-Type: application/json");
if (!cfg.api_key.empty() && cfg.api_key != "-") {
h.append(sfmt("Authorization: Bearer %s", cfg.api_key.c_str()));
}
}
/* ----------------------------- http_get ------------------------------- */
int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code,
std::string &errmsg) {
CurlHandle curl;
if (!curl) { errmsg = "curl init failed"; return -1; }
std::string url_s(url);
curl_easy_setopt(curl.c, CURLOPT_URL, url_s.c_str());
curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, mem_cb);
curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &body);
set_proxy(curl.c);
long t = cfg.timeout > 0 ? cfg.timeout : 30;
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, t);
curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, t);
curl_easy_setopt(curl.c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp");
CURLcode res = curl_easy_perform(curl.c);
curl_easy_getinfo(curl.c, CURLINFO_RESPONSE_CODE, &code);
if (res != CURLE_OK) {
errmsg = curl_easy_strerror(res);
return -1;
}
return 0;
}
/* ----------------------------- token estimate ------------------------- */
static int est_tokens_msgs(const Messages &msgs) {
int chars = 0;
for (auto &m : msgs.items) {
if (!m.content.empty()) chars += (int)m.content.size();
if (!m.reasoning.empty()) chars += (int)m.reasoning.size();
for (auto &tc : m.calls) {
if (!tc.name.empty()) chars += (int)tc.name.size();
if (!tc.arguments.empty()) chars += (int)tc.arguments.size();
}
}
if (chars == 0) return 0;
int t = chars / 4;
return t > 0 ? t : 1;
}
static int est_tokens_msg(const Message &m) {
int chars = 0;
if (!m.content.empty()) chars += (int)m.content.size();
if (!m.reasoning.empty()) chars += (int)m.reasoning.size();
for (auto &tc : m.calls) {
if (!tc.name.empty()) chars += (int)tc.name.size();
if (!tc.arguments.empty()) chars += (int)tc.arguments.size();
}
if (chars == 0) return 0;
int t = chars / 4;
return t > 0 ? t : 1;
}
/* ----------------------------- streaming parser ----------------------- */
struct StreamCtx {
std::string content, reas, line_buf, live_printed, buf, raw;
std::vector<std::string> tbl;
Usage last_usage;
std::vector<ToolCall> tcs;
std::vector<int> tcs_idx;
int tool_calls_shown = 0;
int tty = 0;
int live_idx = -1;
MdState md;
int in_reasoning = 0;
int done = 0;
int erase_pending = 0;
volatile int *interrupted = nullptr;
};
static void stream_upsert_tc(StreamCtx *sc, int idx, std::string_view id,
std::string_view name, std::string_view args) {
int slot = -1;
for (size_t i = 0; i < sc->tcs_idx.size(); i++)
if (sc->tcs_idx[i] == idx) { slot = (int)i; break; }
if (slot < 0) {
slot = (int)sc->tcs.size();
sc->tcs.emplace_back();
sc->tcs_idx.push_back(idx);
}
if (!id.empty()) sc->tcs[slot].id = std::string(id);
if (!name.empty()) sc->tcs[slot].name += std::string(name);
if (!args.empty()) sc->tcs[slot].arguments += std::string(args);
}
static void live_target(StreamCtx *sc, int slot, bool closed, std::string &out) {
ToolCall &t = sc->tcs[slot];
str_appendf(out, "[tool call: %s", t.name.c_str());
if (closed || !t.arguments.empty()) {
std::string fa = filter_text(t.arguments);
str_appendf(out, "(%s", fa.c_str());
if (closed) out += ")]";
}
}
static void finish_live(StreamCtx *sc, bool closed) {
if (sc->live_idx < 0) return;
std::string target;
live_target(sc, sc->live_idx, closed, target);
if (target.size() > sc->live_printed.size()) {
cprint(target.substr(sc->live_printed.size()), std::array{33});
}
sc->live_printed.clear();
sc->live_idx = -1;
/* The tool call marker must always terminate its own line so the
* following [tool result: ...] header is separated from it by a
* newline (in streaming mode the marker is shown here and not
* reprinted by the caller). */
printf("\n");
fflush(stdout);
}
static void step_live(StreamCtx *sc) {
if (!sc->tty || sc->tcs.empty()) return;
int cur = (int)sc->tcs.size() - 1;
if (sc->live_idx != -1 && sc->live_idx != cur) finish_live(sc, true);
if (sc->live_idx == -1) { sc->live_idx = cur; sc->live_printed.clear(); }
std::string target;
live_target(sc, cur, false, target);
if (target.size() > sc->live_printed.size()) {
cprint(target.substr(sc->live_printed.size()), std::array{33});
sc->live_printed += target.substr(sc->live_printed.size());
}
sc->tool_calls_shown = 1;
}
static void flush_tbl(StreamCtx *sc) {
if (!sc->tbl.empty()) {
render_table_print(sc->tbl);
sc->tbl.clear();
}
}
static void stream_maybe_erase(StreamCtx *sc) {
if (sc->erase_pending) {
if (g_col) printf("\r\033[K");
sc->erase_pending = 0;
}
}
static void stream_handle_data(StreamCtx *sc, const std::string &data) {
if (data == "[DONE]") { sc->done = 1; return; }
try {
json d = json::parse(data);
if (!d.is_object()) return;
if (d.contains("usage") && d["usage"].is_object()) {
auto u = d["usage"];
int pt = (u.contains("prompt_tokens") && u["prompt_tokens"].is_number_integer())
? u["prompt_tokens"].get<int>() : 0;
int tt = (u.contains("total_tokens") && u["total_tokens"].is_number_integer())
? u["total_tokens"].get<int>() : 0;
if (pt > 0 || tt > 0) {
sc->last_usage.prompt_tokens = pt;
sc->last_usage.completion_tokens =
(u.contains("completion_tokens") && u["completion_tokens"].is_number_integer())
? u["completion_tokens"].get<int>() : 0;
sc->last_usage.total_tokens = tt;
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object()) {
auto ptd = u["prompt_tokens_details"];
if (ptd.contains("cached_tokens") && ptd["cached_tokens"].is_number_integer())
sc->last_usage.cached_tokens = ptd["cached_tokens"].get<int>();
}
if (sc->last_usage.cached_tokens == 0 &&
u.contains("cached_tokens") && u["cached_tokens"].is_number_integer())
sc->last_usage.cached_tokens = u["cached_tokens"].get<int>();
}
}
if (d.contains("model") && d["model"].is_string())
sc->last_usage.model = d["model"].get<std::string>();
if (!d.contains("choices") || !d["choices"].is_array() || d["choices"].empty()) return;
auto ch = d["choices"][0];
if (!ch.is_object() || !ch.contains("delta") || !ch["delta"].is_object()) return;
auto delta = ch["delta"];
std::string rc;
if (delta.contains("reasoning_content") && delta["reasoning_content"].is_string())
rc = delta["reasoning_content"].get<std::string>();
if (rc.empty() && delta.contains("reasoning") && delta["reasoning"].is_string())
rc = delta["reasoning"].get<std::string>();
if (rc.empty() && delta.contains("thought") && delta["thought"].is_string())
rc = delta["thought"].get<std::string>();
if (!rc.empty()) {
stream_maybe_erase(sc);
if (!sc->in_reasoning) {
if (!sc->content.empty()) {
flush_tbl(sc);
if (!sc->line_buf.empty()) { render_md_line_print(sc->line_buf, sc->md); sc->line_buf.clear(); }
printf("\n");
}
cprint("--- reasoning start ---\n", std::array{36});
sc->in_reasoning = true;
}
cprint(rc, std::array{2});
sc->reas += rc;
}
std::string ct;
if (delta.contains("content") && delta["content"].is_string())
ct = delta["content"].get<std::string>();
if (!ct.empty()) {
stream_maybe_erase(sc);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
sc->content += ct;
sc->line_buf += ct;
while (true) {
auto nl = sc->line_buf.find('\n');
if (nl == std::string::npos) break;
std::string cur = sc->line_buf.substr(0, nl);
sc->line_buf.erase(0, nl + 1);
if (!sc->md.in_code && !is_table_line(cur)) {
flush_tbl(sc);
render_md_line_print(cur, sc->md);
} else {
sc->tbl.push_back(cur);
}
}
}
if (delta.contains("tool_calls") && delta["tool_calls"].is_array()) {
stream_maybe_erase(sc);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
if (sc->live_idx == -1) {
flush_tbl(sc);
if (!sc->line_buf.empty()) { render_md_line_print(sc->line_buf, sc->md); sc->line_buf.clear(); }
}
for (auto &tc : delta["tool_calls"]) {
if (!tc.is_object()) continue;
int idx = (tc.contains("index") && tc["index"].is_number_integer())
? tc["index"].get<int>() : 0;
std::string id, name, args;
if (tc.contains("id") && tc["id"].is_string()) id = tc["id"].get<std::string>();
if (tc.contains("function") && tc["function"].is_object()) {
auto fn = tc["function"];
if (fn.contains("name") && fn["name"].is_string()) name = fn["name"].get<std::string>();
if (fn.contains("arguments") && fn["arguments"].is_string()) args = fn["arguments"].get<std::string>();
}
stream_upsert_tc(sc, idx, id, name, args);
}
step_live(sc);
}
} catch (...) {
return;
}
}
static int stream_feed(StreamCtx *sc, const char *data, size_t len,
volatile int *interrupted) {
sc->interrupted = interrupted;
sc->buf.append(data, len);
while (true) {
auto nl = sc->buf.find('\n');
if (nl == std::string::npos) break;
std::string line = sc->buf.substr(0, nl);
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
if (line.rfind("data:", 0) == 0) {
std::string_view dd = line;
dd.remove_prefix(5);
while (!dd.empty() && dd.front() == ' ') dd.remove_prefix(1);
stream_handle_data(sc, std::string(dd));
}
sc->buf.erase(0, nl + 1);
if (sc->done) break;
if (*interrupted) return -1;
}
return 0;
}
static void stream_finish(StreamCtx *sc) {
flush_tbl(sc);
if (!sc->line_buf.empty()) {
std::string cur = sc->line_buf;
if (!sc->md.in_code && is_table_line(cur)) {
sc->tbl.push_back(cur);
flush_tbl(sc);
} else {
render_md_line_print(cur, sc->md);
}
sc->line_buf.clear();
}
if (sc->live_idx >= 0) finish_live(sc, true);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
}
static void stream_result(StreamCtx *sc, Message &out, Usage &usage) {
out = Message{};
out.role = "assistant";
out.calls_shown = sc->tool_calls_shown;
if (!sc->content.empty()) out.content = sc->content;
if (!sc->reas.empty()) out.reasoning = sc->reas;
for (auto &t : sc->tcs) out.calls.push_back(t);
usage = sc->last_usage;
if (usage.total_tokens == 0 && usage.prompt_tokens > 0)
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
}
/* ----------------------------- streaming write cb --------------------- */
static size_t stream_write_cb(char *ptr, size_t sz, size_t nm, void *ud) {
StreamCtx *sc = (StreamCtx *)ud;
size_t total = sz * nm;
if (*sc->interrupted) return 0;
sc->raw.append((const char *)ptr, total);
int r = stream_feed(sc, ptr, total, sc->interrupted);
if (r != 0 || *sc->interrupted) return 0;
return total;
}
/* ----------------------------- llm() ---------------------------------- */
static void sleep_interruptible(int secs, volatile int *interrupted) {
for (int i = 0; i < secs * 10; i++) {
if (*interrupted) return;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
static std::string http_err_body(const StreamCtx *sc, const std::string *m, long code) {
std::string_view b = sc ? std::string_view(sc->raw) : (m ? std::string_view(*m) : "");
b = utf8_truncate(b, 4000);
return sfmt("HTTP %ld: %.*s", code, (int)b.size(), b.data());
}
int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
std::string &errmsg, volatile int *interrupted) {
out = Message{};
usage = Usage{};
if (*interrupted) { errmsg = "interrupted"; return -1; }
bool stream = (cfg.force_stream >= 0)
? (cfg.force_stream != 0)
: (cfg.stream != 0);
std::string payload = build_request_json(cfg, msgs, use_tools, stream);
std::string url = cfg.endpoint;
while (!url.empty() && url.back() == '/') url.pop_back();
url += "/chat/completions";
int fib[] = {1, 1, 2, 3, 5, 8, 13, 21, 34};
int nf = 9;
std::string pend = cstr("...requesting...", std::array{1, 2});
CurlHandle curl;
if (!curl) { errmsg = "curl init failed"; return -1; }
curl_easy_setopt(curl.c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp");
CurlHeaders headers;
apply_llm_headers(headers, cfg);
curl_easy_setopt(curl.c, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl.c, CURLOPT_POST, 1L);
curl_easy_setopt(curl.c, CURLOPT_POSTFIELDS, payload.c_str());
curl_easy_setopt(curl.c, CURLOPT_POSTFIELDSIZE, (long)payload.size());
curl_easy_setopt(curl.c, CURLOPT_HTTPHEADER, headers.list);
set_proxy(curl.c);
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, cfg.timeout > 0 ? cfg.timeout : 30);
if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, cfg.timeout > 0 ? cfg.timeout : 300);
int attempt, rc = -1;
for (attempt = 0; attempt <= nf; attempt++) {
if (*interrupted) { errmsg = "interrupted"; rc = -1; break; }
if (g_col) { printf("\r%s", pend.c_str()); fflush(stdout); }
else printf("%s\n", pend.c_str());
StreamCtx sc;
std::string mbody;
CURLcode res; long code = 0;
if (stream) {
sc.tty = is_terminal(STDOUT_FILENO);
sc.interrupted = interrupted;
sc.erase_pending = 1;
curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, stream_write_cb);
curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &sc);
} else {
curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, mem_cb);
curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &mbody);
}
res = curl_easy_perform(curl.c);
if (*interrupted) {
if (g_col && (!stream || sc.erase_pending)) printf("\r\033[K");
errmsg = "interrupted";
rc = -1;
break;
}
curl_easy_getinfo(curl.c, CURLINFO_RESPONSE_CODE, &code);
int transport_err = (res != CURLE_OK);
int is4xx = (!transport_err && code >= 400 && code < 500 && code != 408 && code != 429);
if (g_col && !stream) printf("\r\033[K");
if (!transport_err && code < 400) {
if (stream) {
stream_finish(&sc);
if (sc.erase_pending) { if (g_col) printf("\r\033[K"); sc.erase_pending = 0; }
stream_result(&sc, out, usage);
} else {
parse_nonstream_response(mbody, out, usage);
}
rc = 0;
break;
}
/* error path */
if (stream) {
if (transport_err) errmsg = curl_easy_strerror(res);
else errmsg = http_err_body(&sc, nullptr, code);
} else {
if (transport_err) errmsg = curl_easy_strerror(res);
else errmsg = http_err_body(nullptr, &mbody, code);
}
if (is4xx) { rc = -1; break; }
if (attempt < nf) {
if (g_col) printf("\r\033[K");
std::string msg = sfmt("[network error: %s, retrying in %ds...]", errmsg.c_str(), fib[attempt]);
cprint(msg, std::array{31});
printf("\n");
sleep_interruptible(fib[attempt], interrupted);
}
}
if (rc == 0) {
/* The gateway may report zero usage (it does not count tokens). In that
* case fall back to a rough bytes/4 estimate so the user still sees a
* number, but never let a bogus estimate override real gateway numbers. */
if (usage.prompt_tokens == 0) {
usage.prompt_tokens = est_tokens_msgs(msgs);
usage.completion_tokens = est_tokens_msg(out);
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
}
/* Context size = what the model actually processed this turn
* (prompt + completion). Prefer real gateway numbers; only fall back to
* the estimate when none were provided. The old code set this to the
* prompt-only estimate and ignored both the completion and any real
* prompt_tokens the gateway returned. */
int ctx = usage.prompt_tokens + usage.completion_tokens;
if (ctx > usage.context_tokens) usage.context_tokens = ctx;
}
return rc;
}
+648
View File
@@ -0,0 +1,648 @@
/* main.cpp — entry point, agentic loop, REPL, command dispatch. */
#include "bantam.hpp"
#include <array>
#include <cctype>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <curl/curl.h>
#include <dirent.h>
#include <fstream>
#include <nlohmann/json.hpp>
#include <span>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
using json = nlohmann::json;
/* ----------------------------- usage / pct ---------------------------- */
static int ctx_tokens(Usage &u) {
if (u.context_tokens > u.prompt_tokens) return u.context_tokens;
return u.prompt_tokens;
}
static std::string format_usage(Usage &u, int cw) {
int base = ctx_tokens(u);
if (cw <= 0) cw = 262144;
double pct = (double)base * 100.0 / (double)cw;
std::string s;
if (!u.model.empty()) str_appendf(s, "[%s: ", u.model.c_str());
else s += "[";
if (u.cached_tokens > 0) {
int uncached = u.prompt_tokens - u.cached_tokens;
if (uncached < 0) uncached = 0;
str_appendf(s, "%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]",
u.prompt_tokens, u.cached_tokens, uncached, u.completion_tokens, base, cw, pct);
} else {
str_appendf(s, "%d prompt + %d completion | context: %d/%d (%.1f%%)]",
u.prompt_tokens, u.completion_tokens, base, cw, pct);
}
return s;
}
static double context_pct(Usage &u, int cw) {
if (cw <= 0) cw = 262144;
return (double)ctx_tokens(u) * 100.0 / (double)cw;
}
/* ----------------------------- invalid-assistant detection ------------- */
static bool is_invalid_assistant_err(const char *err) {
if (!err) return false;
std::string low = err;
for (char &c : low) c = (char)std::tolower((unsigned char)c);
return low.find("invalid assistant message") != std::string::npos
|| low.find("content or tool_calls must be set") != std::string::npos
|| low.find("tool_calls must be set") != std::string::npos
|| low.find("content must be set") != std::string::npos;
}
/* ----------------------------- sanitize args -------------------------- */
static std::string sanitize_arguments(std::string_view args) {
std::string f = filter_text(args);
json o = nullptr;
try { o = json::parse(f); } catch (...) { o = nullptr; }
if (o.is_object()) return f;
json w = json::object();
w["invalid_raw"] = f;
return w.dump(-1, ' ', false, json::error_handler_t::replace);
}
/* ----------------------------- system prompt -------------------------- */
static const char *default_system_prompt =
"You are Bantam, a tiny, powerful AI agent. Solve the user's task using three tools:\n"
"- bash: run a bash command; returns its output and exit code.\n"
"- read: read a file or directory from the local filesystem; returns numbered lines or directory entries.\n"
"- write: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.\n\n"
"Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.\n"
"Use the target system's filesystem deliberately:\n"
"- Use only $TMPDIR/bantam as the temporary directory for intermediate or scratch files (logs, downloads, temporary build outputs, etc.); create it if it does not exist.\n"
"- Create permanent artifacts in the current working directory unless the user explicitly instructs otherwise.\n"
"- Tool results larger than $MAX_TOOL_RES bytes are not inserted into the context in full: they are saved under $TMPDIR/bantam/toolres and replaced by a short note giving the file path and its total byte length. Read such a file with the read tool, paging through it with line offsets (using offset and limit) instead of loading it all at once.\n\n"
"When generating code:\n"
"- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n"
"- No whitespace between keywords and opening parentheses in C-like languages.\n"
"- Write optimally and with as few third-party dependencies as possible.\n"
"- Always test.\n"
"- No emojis in code or documentation.\n"
"- Respect AGENTS.md contents in the project.";
static std::string build_system_prompt(Cfg &cfg) {
std::string tmp = bantam_tmp_dir();
std::string sp = default_system_prompt;
size_t found = 0;
while ((found = sp.find("$TMPDIR/bantam", found)) != std::string::npos) {
sp.replace(found, std::strlen("$TMPDIR/bantam"), tmp);
found += tmp.size();
}
char mtr[32]; std::snprintf(mtr, sizeof mtr, "%d", cfg.max_tool_res);
auto fm = sp.find("$MAX_TOOL_RES");
if (fm != std::string::npos) sp.replace(fm, std::strlen("$MAX_TOOL_RES"), mtr);
std::string out = sp;
std::string td = tools_dir(cfg);
if (!td.empty()) { out += "\n\nExtra shell tools can be found at "; out += td; }
std::string sd = skills_dir(cfg);
if (!sd.empty()) { out += "\n\nSkills may be discovered and invoked from "; out += sd; }
return out;
}
/* ----------------------------- json arg helpers ----------------------- */
static std::string get_str(const json &j, const char *k) {
if (j.contains(k) && j[k].is_string()) return j[k].get<std::string>();
return "";
}
static int get_int(const json &j, const char *k, int def = 0) {
if (j.contains(k) && j[k].is_number_integer()) {
int64_t v = j[k].get<int64_t>();
if (v > INT_MAX) return INT_MAX;
if (v < INT_MIN) return INT_MIN;
return (int)v;
}
return def;
}
static std::string tool_path_arg(const json &a) {
std::string v = get_str(a, "filePath");
if (v.empty()) v = get_str(a, "path");
return v;
}
static bool trim_is_empty(std::string_view s) {
size_t i = 0;
while (i < s.size() && (s[i] == ' ' || s[i] == '\n' || s[i] == '\r' || s[i] == '\t')) i++;
return i == s.size();
}
static std::string last_role(Messages &msgs) {
if (msgs.items.empty()) return "";
return msgs.items.back().role;
}
/* ----------------------------- AL loop -------------------------------- */
static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, std::string *err) {
int done = 0;
std::vector<std::string> tmps;
int rc = 0;
for (int i = 0; i < cfg.max_al_iterations && !done; i++) {
if (*interrupted) { rc = -1; break; }
Message m; Usage u;
std::string lerr;
int lrc = llm(cfg, msgs, true, m, u, lerr, interrupted);
if (lrc != 0) {
if (*interrupted) { rc = -1; break; }
if (is_invalid_assistant_err(lerr.c_str())) {
int stripped = 0;
for (int j = (int)msgs.items.size() - 1; j >= 0; j--) {
if (msgs.items[j].role == "assistant") {
cprint("[stripped malformed assistant message]\n", std::array{33});
msgs.items.erase(msgs.items.begin() + j);
stripped = 1;
break;
}
}
if (stripped) continue;
}
if (err) *err = std::move(lerr);
rc = -1; break;
}
out.prompt_tokens += u.prompt_tokens;
out.completion_tokens += u.completion_tokens;
out.total_tokens += u.total_tokens;
out.context_tokens = u.context_tokens;
if (u.cached_tokens > 0) out.cached_tokens = u.cached_tokens;
if (!u.model.empty()) out.model = u.model;
for (auto &tc : m.calls) {
tc.arguments = sanitize_arguments(tc.arguments);
}
msgs.items.push_back(m);
if (!cfg.stream) {
if (!m.reasoning.empty()) {
cprint("--- reasoning start ---\n", std::array{36});
cprint(m.reasoning, std::array{2});
cprint("\n--- reasoning end ---\n", std::array{36});
}
if (!m.content.empty()) { render_md_print(m.content); printf("\n"); }
}
if (m.calls.empty()) {
if (!m.reasoning.empty() && trim_is_empty(m.content)) {
cprint("[auto continue: response was reasoning-only]\n", std::array{33});
Message cont; cont.role = "user"; cont.content = "continue";
msgs.items.push_back(cont);
continue;
}
done = 1; break;
}
for (size_t tci = 0; tci < m.calls.size(); tci++) {
if (*interrupted) { rc = -1; break; }
ToolCall &tc = m.calls[tci];
std::string astr = filter_text(tc.arguments);
if (!m.calls_shown) {
cprint(sfmt("[tool call: %s(%s)]", tc.name.c_str(), astr.c_str()), std::array{33});
printf("\n");
}
std::string res; int sty = 2;
json a = nullptr;
try { a = json::parse(astr); } catch (...) { a = nullptr; }
if (!a.is_object()) {
res = sfmt("[tool error: invalid JSON args for %s: parse error. Raw: %s]", tc.name.c_str(), astr.c_str());
sty = 31;
} else {
std::string fn = tc.name;
if (fn == "bash" || fn == "shell_exec") {
std::string cmd = get_str(a, "command");
std::string workdir = get_str(a, "workdir");
std::string fcmd = filter_text(cmd);
std::string fwd = filter_text(workdir);
int to = cfg.shell_timeout;
if (a.contains("timeout")) { int n = get_int(a, "timeout"); if (n > 0) to = (n > 1000) ? n / 1000 : n; }
res = shell_with_workdir(fcmd, fwd, to, interrupted);
} else if (fn == "read") {
std::string p = tool_path_arg(a);
std::string fp = filter_text(p);
int off = get_int(a, "offset", 1); if (off < 1) off = 1;
int lim = get_int(a, "limit", 2000); if (lim <= 0) lim = 2000;
res = read_file_or_dir(fp, off, lim, &sty);
} else if (fn == "write" || fn == "write_file") {
std::string p = tool_path_arg(a);
std::string fp = filter_text(p);
std::string cv = get_str(a, "content");
bool has_content = a.contains("content") && a["content"].is_string();
bool has_offset = a.contains("offset") && !a["offset"].is_null();
bool has_del = a.contains("del_bytes") && !a["del_bytes"].is_null();
int offset = has_offset ? get_int(a, "offset") : 0; if (offset < 0) offset = 0;
int del = has_del ? get_int(a, "del_bytes") : 0;
if (fp.empty()) { res = "[tool error: write requires 'filePath' parameter]"; sty = 31; }
else if (!has_content) { res = "[tool error: write requires 'content' parameter]"; sty = 31; }
else if (!has_offset && !has_del) {
std::string c = filter_text(cv);
auto sl = fp.find_last_of('/');
if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl));
else mkdir_p(".");
std::ofstream of(fp, std::ios::binary | std::ios::trunc);
if (!of) { res = sfmt("[tool error: write %s: %s]", fp.c_str(), strerror(errno)); sty = 31; }
else { of.write(c.data(), (std::streamsize)c.size()); res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
} else {
int werr = 0; std::string c = filter_text(cv);
res = write_file(fp, offset, del, c, &werr);
if (werr) sty = 31;
}
} else {
res = sfmt("Unknown tool: %s", fn.c_str());
sty = 31;
}
}
std::string fres = filter_text(res);
res = offload_tool_result(fres, cfg.max_tool_res, tmps);
{
std::string hdr = cstr("[tool result: ", std::array{32});
std::string fn2 = cstr(tc.name, std::array{32});
printf("%s%s]\n", hdr.c_str(), fn2.c_str());
std::string body = cstr(res, std::array{sty});
printf("%s\n\n", body.c_str());
}
Message tm; tm.role = "tool"; tm.tool_call_id = tc.id; tm.content = res;
msgs.items.push_back(tm);
if (*interrupted) { rc = -1; break; }
}
if (rc != 0) break;
}
if (!done && rc == 0) {
Message maxm; maxm.role = "assistant";
maxm.content = sfmt("[max AL iterations (%d) reached]", cfg.max_al_iterations);
msgs.items.push_back(maxm);
}
cleanup_temps(tmps);
return rc;
}
/* ----------------------------- run_turn ------------------------------- */
static int run_turn(Cfg &cfg, Messages &msgs, Usage &usage, std::string *err, volatile int *interrupted) {
*interrupted = 0;
usage = Usage{};
int rc = AL(cfg, msgs, usage, interrupted, err);
if (last_role(msgs) == "tool") {
Message cont; cont.role = "user"; cont.content = "continue";
msgs.items.push_back(cont);
cprint("[auto continue: last message was a tool result]\n", std::array{33});
rc = AL(cfg, msgs, usage, interrupted, err);
}
if (rc != 0) {
if (*interrupted) { cprint("\n[interrupted]\n", std::array{33}); }
else { cprint("\n[error: ", std::array{31}); cprint((!err || err->empty()) ? "error" : *err, std::array{31}); cprint("]\n", std::array{31}); }
}
return rc;
}
/* ----------------------------- direct shell --------------------------- */
static void run_direct_shell(std::string_view cmd, int timeout) {
std::string fcmd = filter_text(cmd);
cprint(sfmt("[tool call: bash({\"command\":\"%s\"})]", fcmd.c_str()), std::array{33});
printf("\n");
std::string res = shell_with_workdir(fcmd, "", timeout, &g_interrupted);
printf("%s\n", cstr("[tool result: bash]", std::array{32}).c_str());
printf("%s\n", cstr(res, std::array{2}).c_str());
}
/* ----------------------------- skills --------------------------------- */
static std::string skill_prompt(std::string_view u, Cfg &cfg) {
std::string_view rest = u.substr(6);
while (!rest.empty() && rest.front() == ' ') rest = rest.substr(1);
if (rest.empty()) return "";
std::string name(rest);
std::string prompt;
auto sp = name.find(' ');
if (sp != std::string::npos) { prompt = name.substr(sp + 1); name = name.substr(0, sp); }
std::string sd = skills_dir(cfg);
std::string path;
if (!sd.empty()) {
path = sfmt("%s/%s/SKILL.md", sd.c_str(), name.c_str());
} else if (name.find("SKILL.md") != std::string::npos) {
path = name;
} else {
path = name + "/SKILL.md";
}
struct stat st;
if (stat(path.c_str(), &st) != 0)
return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
if (S_ISDIR(st.st_mode))
return sfmt("[cannot read skill \"%s\": Is a directory]", name.c_str());
std::ifstream f(path, std::ios::binary);
if (!f) return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
while (!data.empty() && (data.back() == '\n' || data.back() == '\r')) data.pop_back();
std::string out = data;
if (!prompt.empty()) { out += "\n\n"; out += prompt; }
return out;
}
static void list_skills(Cfg &cfg) {
std::string sd = skills_dir(cfg);
if (sd.empty()) { cprint("No skills directory configured (set BANTAM_SKILLS_DIR or bantam_skills_dir).\n", std::array{33}); return; }
DIR *d = opendir(sd.c_str());
if (!d) { cprint(sfmt("[skill error: cannot read skills dir: %s]", strerror(errno)), std::array{31}); return; }
std::string names; int cnt = 0; struct dirent *de;
while ((de = readdir(d))) {
if (std::strcmp(de->d_name, ".") == 0 || std::strcmp(de->d_name, "..") == 0) continue;
std::string full = sfmt("%s/%s", sd.c_str(), de->d_name);
struct stat st; if (stat(full.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue;
std::string sk = sfmt("%s/%s/SKILL.md", sd.c_str(), de->d_name);
if (access(sk.c_str(), F_OK) == 0) { str_appendf(names, " %s\n", de->d_name); cnt++; }
}
closedir(d);
if (cnt == 0) { cprint("No skills found in ", std::array{33}); cprint(sd, std::array{33}); cprint("\n", std::array{33}); return; }
cprint("Available skills in ", std::array{1, 36}); cprint(sd, std::array{1, 36}); cprint(":\n", std::array{1, 36});
fputs(names.c_str(), stdout);
}
/* ----------------------------- compact -------------------------------- */
static void do_compact(Cfg &cfg, Messages &msgs) {
if (msgs.items.size() <= 1) { cprint("Nothing to compact yet.\n", std::array{33}); return; }
cprint("[compacting conversation...]\n", std::array{33});
std::string sum, e;
auto nm = compact(cfg, msgs, &sum, &e, &g_interrupted);
int interrupted = g_interrupted;
if (!nm) {
if (interrupted) cprint("\n[interrupted]\n", std::array{33});
else { cprint(sfmt("\n[compact failed: %s]", e.empty() ? "compact failed" : e.c_str()), std::array{31}); printf("\n"); }
return;
}
msgs = std::move(*nm);
autosave(msgs);
printf("%s%d messages]\n", cstr("[compacted to ", std::array{32}).c_str(), (int)msgs.items.size());
printf("%s", cstr("--- summary ---\n", std::array{33}).c_str());
printf("%s\n", cstr(sum, std::array{2}).c_str());
}
/* ----------------------------- main ----------------------------------- */
struct CurlGlobalGuard {
CurlGlobalGuard() { curl_global_init(CURL_GLOBAL_DEFAULT); }
~CurlGlobalGuard() { curl_global_cleanup(); }
};
int main(int argc, char **argv) {
CurlGlobalGuard curl_guard;
setup_signal_handler();
Cfg cfg = get_cfg(config_path());
cfg.context_window = fetch_context_window(cfg);
std::string sp = build_system_prompt(cfg);
g_col = color_enabled(cfg);
Messages msgs;
{
Message sys; sys.role = "system"; sys.content = sp;
msgs.items.push_back(sys);
}
if (argc > 1 && argv[1] && argv[1][0] != '\0') {
std::string p = argv[1];
struct stat st;
if (stat(p.c_str(), &st) != 0 || S_ISDIR(st.st_mode)) {
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
return 1;
}
std::ifstream f(p, std::ios::binary);
if (!f) {
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
return 1;
}
std::string u((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
size_t a = 0; while (a < u.size() && (u[a] == ' ' || u[a] == '\n' || u[a] == '\r' || u[a] == '\t')) a++;
u = u.substr(a);
if (!u.empty() && u[0] == '!') { run_direct_shell(u.substr(1), cfg.shell_timeout); return 0; }
Message um; um.role = "user"; um.content = u;
msgs.items.push_back(um);
Usage usg; std::string err;
run_turn(cfg, msgs, usg, &err, &g_interrupted);
if (!err.empty()) return 1;
cprint(format_usage(usg, cfg.context_window), std::array{2}); printf("\n");
autosave(msgs);
return 0;
}
load_history();
auto sess = find_project_session();
if (sess && sess->messages.items.size() > 1) {
cprint("[session found for this directory]\n", std::array{33});
cprint(" ", std::array{33}); cprint(sess->id, std::array{32}); printf("\n");
cprint(" ", std::array{33}); cprint(sess->summary, std::array{2}); printf("\n");
std::string created = sfmt(" created %s [%d messages]\n", sess->created.c_str(), (int)sess->messages.items.size());
cprint(created, std::array{2});
printf(" Resume this session? [Y/n]: "); fflush(stdout);
bool ok; std::string ans = read_plain("", ok);
if (ok) {
std::string a2 = ans;
size_t ai = 0; while (ai < a2.size() && (a2[ai] == ' ' || a2[ai] == '\n' || a2[ai] == '\r' || a2[ai] == '\t')) ai++;
a2 = a2.substr(ai);
while (!a2.empty() && (a2.back() == '\n' || a2.back() == '\r' || a2.back() == ' ')) a2.pop_back();
if (a2.empty() || a2[0] == 'y' || a2[0] == 'Y') {
msgs = std::move(sess->messages);
autosave(msgs);
printf("%s%s]\n", cstr("[session resumed: ", std::array{32}).c_str(), sess->id.c_str());
}
}
}
const std::string *re_ptr = cfg_raw_get(cfg, "reasoning_effort");
std::string re = (re_ptr && !re_ptr->empty()) ? *re_ptr : "high";
{
cprint("Bantam Agent ready", std::array{1, 32}); printf(" ");
cprint(" (Enter submits; Ctrl+J or paste = new line)\n", std::array{2});
std::string info = sfmt("endpoint: %s model: %s temp: %g reasoning: %s context: %d\n",
cfg.endpoint.c_str(), cfg.model.c_str(), cfg.temperature, re.c_str(), cfg.context_window);
cprint(info, std::array{2});
}
for (;;) {
g_interrupted = 0;
bool ok;
std::string u = read_line("> ", ok);
if (!ok) { printf("\n"); break; }
std::string ut = u;
size_t a = 0; while (a < ut.size() && (ut[a] == ' ' || ut[a] == '\n' || ut[a] == '\r' || ut[a] == '\t')) a++;
ut = ut.substr(a);
if (ut.empty()) continue;
add_history(u);
/* command aliases */
for (int ci = 0; ci < 2; ci++) {
const char *cmd = (ci == 0) ? "model" : "endpoint";
std::string slash = sfmt("/%s", cmd);
if (u == slash) {
u = sfmt("/cfg %s", cmd);
} else if (u.size() > slash.size() && u.rfind(slash, 0) == 0 && u[slash.size()] == ' ') {
u = sfmt("/cfg %s %s", cmd, u.c_str() + slash.size() + 1);
}
}
if (u == "/quit") break;
if (u == "/clear") {
msgs = Messages{};
Message s2; s2.role = "system"; s2.content = build_system_prompt(cfg);
msgs.items.push_back(s2);
autosave(msgs); continue;
}
if (u == "/save" || u.starts_with("/save ")) {
std::string name = u.substr(5);
size_t ni = 0; while (ni < name.size() && name[ni] == ' ') ni++;
name = name.substr(ni);
std::string sid, sm;
save_session(msgs, name, &sid, &sm);
printf("%s%s]\n", cstr("[session saved: ", std::array{32}).c_str(), sid.c_str());
cprint(" ", std::array{32}); cprint(sm, std::array{2}); printf("\n");
continue;
}
if (u == "/continue" || u == "/cont") {
std::string pid = project_id();
std::string lerr;
auto lm = load_session(pid, &lerr);
if (!lm) { cprint("No session found for current project: ", std::array{31}); cprint(lerr, std::array{31}); printf("\n"); continue; }
msgs = std::move(*lm);
autosave(msgs);
printf("%s%s]\n", cstr("[session continued: ", std::array{32}).c_str(), pid.c_str());
std::string sm = summary_of(msgs);
cprint(" ", std::array{32}); cprint(sm, std::array{32}); printf("\n");
continue;
}
if (u == "/list") {
auto ss = sessions_load();
if (ss.empty()) { cprint("No sessions saved yet.\n", std::array{33}); continue; }
std::string pid = project_id();
for (auto &s : ss) {
printf("%s", cstr(s.id, std::array{32}).c_str());
if (s.id == pid) cprint(" (current project)", std::array{33});
std::string meta = sfmt(" %s [%d msgs]\n", s.created.c_str(), (int)s.messages.items.size());
cprint(meta, std::array{2});
cprint(" ", std::array{32}); cprint(s.summary, std::array{2}); printf("\n");
}
continue;
}
if (u == "/load" || u.starts_with("/load ")) {
std::vector<std::string> parts;
{
std::string_view sv(u);
size_t i = 0;
while (i < sv.size()) {
while (i < sv.size() && sv[i] == ' ') i++;
if (i >= sv.size()) break;
size_t j = i;
while (j < sv.size() && sv[j] != ' ') j++;
parts.push_back(std::string(sv.substr(i, j - i)));
i = j;
}
}
if (parts.size() < 2) { cprint("Usage: /load <session-id>\n", std::array{31}); continue; }
std::string lerr;
auto lm = load_session(parts[1], &lerr);
if (!lm) { cprint("Session not found: ", std::array{31}); cprint(lerr, std::array{31}); printf("\n"); continue; }
msgs = std::move(*lm);
autosave(msgs);
printf("%s%s]\n", cstr("[session loaded: ", std::array{32}).c_str(), parts[1].c_str());
std::string sm = summary_of(msgs);
cprint(" ", std::array{32}); cprint(sm, std::array{32}); printf("\n");
continue;
}
if (u == "/compact") { do_compact(cfg, msgs); continue; }
if (u == "/cfg" || u.starts_with("/cfg ")) {
/* tokens after "/cfg " */
std::string rest = u.substr(4);
while (!rest.empty() && rest.front() == ' ') rest = rest.substr(1);
std::string sub, key, val;
{
size_t i = 0;
while (i < rest.size() && rest[i] != ' ') i++;
sub = rest.substr(0, i);
size_t j = i;
while (j < rest.size() && rest[j] == ' ') j++;
size_t ks = j;
while (j < rest.size() && rest[j] != ' ') j++;
key = rest.substr(ks, j - ks);
size_t vs = j;
while (vs < rest.size() && rest[vs] == ' ') vs++;
val = rest.substr(vs);
}
if (sub.empty()) { cprint("Usage: /cfg <key> [value]\n", std::array{31}); continue; }
if (sub == "get") {
if (key.empty()) { cprint("Usage: /cfg get <key>\n", std::array{31}); continue; }
const std::string *v = cfg_raw_get(cfg, key);
if (v && !v->empty()) printf("%s=%s\n", cstr(key, std::array{32}).c_str(), v->c_str());
else printf("%s not set\n", cstr(key, std::array{31}).c_str());
} else if (sub == "set") {
if (key.empty()) { cprint("Usage: /cfg set <key> <value>\n", std::array{31}); continue; }
if (set_cfg(config_path(), key, val) != 0) { cprint("[cfg error]\n", std::array{31}); continue; }
cfg = get_cfg(config_path());
if (key == "model" || key == "endpoint" || key == "api_key")
cfg.context_window = fetch_context_window(cfg);
g_col = color_enabled(cfg);
printf("%s%s=%s]\n", cstr("[config updated: ", std::array{32}).c_str(), key.c_str(), val.c_str());
} else if (key.empty()) {
/* single token: treat as get */
const std::string *v = cfg_raw_get(cfg, sub);
if (v && !v->empty()) printf("%s=%s\n", cstr(sub, std::array{32}).c_str(), v->c_str());
else printf("%s not set\n", cstr(sub, std::array{31}).c_str());
} else {
/* legacy "/cfg key value": treat as set */
if (set_cfg(config_path(), sub, key) != 0) { cprint("[cfg error]\n", std::array{31}); continue; }
cfg = get_cfg(config_path());
if (sub == "model" || sub == "endpoint" || sub == "api_key")
cfg.context_window = fetch_context_window(cfg);
g_col = color_enabled(cfg);
printf("%s%s=%s]\n", cstr("[config updated: ", std::array{32}).c_str(), sub.c_str(), key.c_str());
}
continue;
}
if (u == "/skill" || u.starts_with("/skill ")) {
std::string rest = u.substr(6);
while (!rest.empty() && rest.front() == ' ') rest = rest.substr(1);
if (rest.empty()) { list_skills(cfg); continue; }
std::string composed = skill_prompt(u, cfg);
if (composed.empty()) { cprint("/skill usage: /skill <name> [prompt]\n", std::array{31}); continue; }
if (composed.starts_with("[cannot read skill")) {
cprint(composed, std::array{31}); printf("\n"); continue;
}
u = composed;
/* fall through to a normal LLM turn */
} else if (u == "/models") {
int e2; std::string ml = list_models(cfg, &e2);
if (e2 != 0) { cprint("[error querying /models: ", std::array{31}); cprint(ml, std::array{31}); printf("]\n"); continue; }
cprint("Supported models at ", std::array{1, 36}); cprint(cfg.endpoint, std::array{1, 36}); cprint(":\n", std::array{1, 36});
render_md_print(ml);
continue;
} else if (!u.empty() && u[0] == '!') {
run_direct_shell(u.substr(1), cfg.shell_timeout); continue;
} else if (u == "/help") {
cprint("Bantam commands:\n", std::array{1, 36});
const char *cmds[][2] = {
{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save [name]", "save session (default: project MD5)"},
{"/continue", "continue project session (alias: /cont)"}, {"/list", "list sessions"}, {"/load <id>", "load session"},
{"/compact", "compact context"}, {"/cfg <k> [v]", "get/set config"}, {"/model [m]", "alias for /cfg model"},
{"/endpoint [e]", "alias for /cfg endpoint"}, {"!<cmd>", "run shell command directly"}, {"/models", "list models at endpoint"},
{"/skill <n> [p]", "load SKILL.md and run as prompt"}, {"/help", "show help"}};
for (int i = 0; i < 14; i++) {
std::string label = sfmt(" %-18s", cmds[i][0]);
cprint(label, std::array{1, 32});
cprint(cmds[i][1], std::array{1, 32});
printf("\n");
}
continue;
}
/* normal LLM turn */
{
Message um; um.role = "user"; um.content = u;
msgs.items.push_back(um);
Usage usg; std::string err;
run_turn(cfg, msgs, usg, &err, &g_interrupted);
if (!err.empty()) continue;
cprint(format_usage(usg, cfg.context_window), std::array{2}); printf("\n");
double pct = context_pct(usg, cfg.context_window);
if (pct >= 60.0 && msgs.items.size() > 1) {
std::string q = sfmt("Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: ",
pct, ctx_tokens(usg), cfg.context_window);
cprint(q, std::array{33});
bool ok2; std::string ans = read_plain("", ok2);
if (ok2) {
std::string a2 = ans;
size_t ai = 0; while (ai < a2.size() && (a2[ai] == ' ' || a2[ai] == '\n' || a2[ai] == '\r' || a2[ai] == '\t')) ai++;
a2 = a2.substr(ai);
while (!a2.empty() && (a2.back() == '\n' || a2.back() == '\r' || a2.back() == ' ')) a2.pop_back();
if (a2.empty() || a2[0] == 'y' || a2[0] == 'Y') do_compact(cfg, msgs);
}
}
autosave(msgs);
}
}
autosave(msgs);
save_history();
return 0;
}
+25526
View File
File diff suppressed because it is too large Load Diff
+475
View File
@@ -0,0 +1,475 @@
/* render.cpp — terminal markdown rendering with box-drawing tables. */
#include "bantam.hpp"
#include <array>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <span>
#include <vector>
/* ----------------------------- table helpers -------------------------- */
bool is_table_line(std::string_view line) {
std::string_view t = line;
while (!t.empty() && (t.front() == ' ' || t.front() == '\t')) t.remove_prefix(1);
if (t.find('|') == std::string_view::npos) return false;
return (t.front() == '|') || (t.back() == '|');
}
static std::string_view trim_spaces(std::string_view s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.remove_prefix(1);
return s;
}
static bool is_table_sep(const std::vector<std::string> &cells) {
if (cells.empty()) return false;
for (auto &c : cells) {
std::string_view cv = trim_spaces(c);
bool ok = true;
for (char ch : cv) if (ch != '-' && ch != ':') { ok = false; break; }
if (!ok) return false;
}
return true;
}
static std::vector<std::string> parse_table_cells(std::string_view line) {
std::string_view t = line;
while (!t.empty() && (t.front() == ' ' || t.front() == '\t')) t.remove_prefix(1);
std::string dup(t);
while (!dup.empty() && (dup.back() == '\n' || dup.back() == '\r')) dup.pop_back();
if (!dup.empty() && dup.front() == '|') dup.erase(0, 1);
if (!dup.empty() && dup.back() == '|') dup.pop_back();
size_t pipes = 0;
for (char c : dup) if (c == '|') pipes++;
std::vector<std::string> cells;
cells.reserve(pipes + 1);
std::string cur;
size_t i = 0;
while (i <= dup.size()) {
if (i == dup.size() || dup[i] == '|') {
cells.push_back(std::string(trim_spaces(cur)));
cur.clear();
} else {
cur += dup[i];
}
i++;
}
return cells;
}
/* ----------------------------- inline --------------------------------- */
static bool is_word_byte(unsigned char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') || c == '_';
}
static void wrap_pairs(std::string &dst, std::string_view src, std::string_view open,
std::string_view close, std::span<const int> codes,
bool boundary = false) {
size_t olen = open.size(), clen = close.size();
size_t p = 0;
while (p < src.size()) {
size_t o = src.find(open, p);
if (o == std::string_view::npos) { dst += src.substr(p); break; }
/* When word boundaries are enforced (the _ / __ rules) an opening
* delimiter glued to a preceding word character is not an opener. */
if (boundary && o > 0 && is_word_byte((unsigned char)src[o - 1])) {
dst += src.substr(p, o - p + olen);
p = o + olen;
continue;
}
dst += src.substr(p, o - p);
size_t c = o + olen;
while (true) {
c = src.find(close, c);
if (c == std::string_view::npos) { dst += src.substr(o); break; }
/* A closing delimiter glued to a following word character is not a
* closer (e.g. the underscores in file_name_here); keep scanning. */
if (boundary && c + clen < src.size() &&
is_word_byte((unsigned char)src[c + clen])) {
c += clen;
continue;
}
break;
}
if (c == std::string_view::npos) break; /* substr(o) already appended */
std::string_view inner = src.substr(o + olen, c - (o + olen));
dst += cstr(inner, codes);
p = c + clen;
}
}
static void apply_links(std::string &dst, std::string_view src) {
size_t p = 0;
while (p < src.size()) {
size_t lb = src.find('[', p);
if (lb == std::string_view::npos) { dst += src.substr(p); break; }
size_t rb = src.find(']', lb);
if (rb == std::string_view::npos || rb + 1 >= src.size() || src[rb + 1] != '(') {
dst += src.substr(p, lb - p + 1); p = lb + 1; continue;
}
size_t rp = src.find(')', rb + 2);
if (rp == std::string_view::npos) { dst += src.substr(p); break; }
std::string_view text = src.substr(lb + 1, rb - (lb + 1));
std::string_view url = src.substr(rb + 2, rp - (rb + 2));
dst += src.substr(p, lb - p);
std::string wt = cstr(text, std::array{4, 36});
dst += wt;
std::string wu = cstr(url, std::array{2});
dst += "(" + wu + ")";
p = rp + 1;
}
}
static std::string render_inline(std::string_view s) {
if (!g_col) return std::string(s);
if (s.empty()) return "";
std::vector<std::string> codes;
std::string step1;
size_t p = 0;
while (p < s.size()) {
if (s[p] == '`') {
size_t start = p + 1;
size_t end = s.find('`', start);
if (end != std::string_view::npos && end > start) {
std::string_view ctext = s.substr(start, end - start);
std::string ct = cstr(ctext, std::array{33});
codes.push_back(ct);
str_appendf(step1, "\x01%zu\x01", codes.size() - 1);
p = end + 1;
continue;
}
step1 += s[p]; p++;
} else {
step1 += s[p]; p++;
}
}
std::string s2; apply_links(s2, step1);
std::string s3; wrap_pairs(s3, s2, "***", "***", std::array{1, 3});
std::string s4; wrap_pairs(s4, s3, "___", "___", std::array{1, 3});
std::string s5; wrap_pairs(s5, s4, "**", "**", std::array{1});
std::string s6; wrap_pairs(s6, s5, "__", "__", std::array{1}, true);
std::string s7; wrap_pairs(s7, s6, "*", "*", std::array{3});
std::string s8; wrap_pairs(s8, s7, "_", "_", std::array{3}, true);
std::string s9; wrap_pairs(s9, s8, "~~", "~~", std::array{9});
std::string out;
size_t q = 0;
while (q < s9.size()) {
if (s9[q] == '\x01') {
size_t e = s9.find('\x01', q + 1);
if (e != std::string::npos) {
int idx = to_int_val(std::string_view(s9).substr(q + 1, e - (q + 1)), -1);
if (idx >= 0 && idx < (int)codes.size()) out += codes[idx];
q = e + 1; continue;
}
}
out += s9[q]; q++;
}
return out;
}
/* ----------------------------- line ----------------------------------- */
static int indent_spaces(std::string_view line) {
int n = 0;
while (n < (int)line.size() && line[n] == ' ') n++;
return n;
}
void render_md_line_print(std::string_view line, MdState &st) {
if (!g_col) { printf("%s\n", std::string(line).c_str()); return; }
std::string_view t = trim_spaces(line);
if (t.size() >= 3 && t.substr(0, 3) == "```") {
if (!st.in_code) {
st.in_code = true;
std::string_view lang = t.substr(3);
while (!lang.empty() && lang.front() == ' ') lang.remove_prefix(1);
std::string title;
if (!lang.empty()) title = sfmt(" [ %s ]", std::string(lang).c_str());
std::string s = "───" + title;
for (int i = 0; i < 40; i++) s += "─";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
} else {
st.in_code = false;
std::string s;
for (int i = 0; i < 50; i++) s += "─";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
return;
}
if (st.in_code) {
std::string pre = cstr(" ", std::array{2});
std::string body = cstr(line, std::array{32});
printf("%s%s\n", pre.c_str(), body.c_str());
return;
}
if (t == "---" || t == "***" || t == "___" || t == "----" || t == "------") {
std::string s;
for (int i = 0; i < 50; i++) s += "─";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
return;
}
if (!t.empty() && t[0] == '#') {
int lvl = 0;
while (lvl < (int)t.size() && t[lvl] == '#') lvl++;
if (lvl < (int)t.size() && t[lvl] == ' ') {
std::string_view htext = trim_spaces(t.substr(lvl));
if (lvl == 1) { std::string a = cstr("■ ", std::array{35}); std::string b = cstr(htext, std::array{1, 36}); printf("%s%s\n", a.c_str(), b.c_str()); }
else if (lvl == 2) { std::string a = cstr("▲ ", std::array{35}); std::string b = cstr(htext, std::array{1, 36}); printf("%s%s\n", a.c_str(), b.c_str()); }
else if (lvl == 3) { std::string a = cstr("● ", std::array{35}); std::string b = cstr(htext, std::array{1, 32}); printf("%s%s\n", a.c_str(), b.c_str()); }
else if (lvl == 4) { std::string a = cstr("◆ ", std::array{35}); std::string b = cstr(htext, std::array{1, 33}); printf("%s%s\n", a.c_str(), b.c_str()); }
else { std::string b = cstr(htext, std::array{1}); printf("%s\n", b.c_str()); }
return;
}
}
if (!t.empty() && t[0] == '>') {
std::string_view q = trim_spaces(t.substr(1));
std::string bar = cstr("▎ ", std::array{34});
std::string body = render_inline(q);
std::string cbody = cstr(body, std::array{3});
printf("%s%s\n", bar.c_str(), cbody.c_str());
return;
}
if (t.size() >= 6 && (t.substr(0, 6) == "- [ ] " || t.substr(0, 6) == "* [ ] ")) {
int ind = indent_spaces(line);
std::string box = cstr("☐ ", std::array{33});
std::string rest = render_inline(t.substr(6));
printf("%*s%s%s\n", ind, "", box.c_str(), rest.c_str());
return;
}
if (t.size() >= 6 && (t.substr(0, 6) == "- [x] " || t.substr(0, 6) == "* [x] " ||
t.substr(0, 6) == "- [X] " || t.substr(0, 6) == "* [X] ")) {
int ind = indent_spaces(line);
std::string box = cstr("☑ ", std::array{32});
std::string rest = render_inline(t.substr(6));
printf("%*s%s%s\n", ind, "", box.c_str(), rest.c_str());
return;
}
if (t.size() >= 2 && (t.substr(0, 2) == "- " || t.substr(0, 2) == "* " || t.substr(0, 2) == "+ ")) {
int ind = indent_spaces(line);
std::string bullet = cstr("• ", std::array{36});
std::string rest = render_inline(t.substr(2));
printf("%*s%s%s\n", ind, "", bullet.c_str(), rest.c_str());
return;
}
std::string_view num = t;
while (!num.empty() && std::isdigit((unsigned char)num.front())) num.remove_prefix(1);
if (!num.empty() && num.front() == '.' && num.size() > 1 && num[1] == ' ') {
int ind = indent_spaces(line);
size_t nlen = num.data() - t.data();
std::string_view nstr = t.substr(0, nlen);
std::string bullet = cstr(nstr, std::array{33});
std::string rest = render_inline(t.substr(nlen + 2));
printf("%*s%s %s\n", ind, "", bullet.c_str(), rest.c_str());
return;
}
std::string r = render_inline(line);
printf("%s\n", r.c_str());
}
/* ----------------------------- table ---------------------------------- */
static std::vector<std::string> wrap_cell(std::string_view s, int width) {
std::vector<std::string> lines;
if (width <= 0) { lines.push_back(""); return lines; }
std::string copy(s);
std::vector<std::string> blocks;
std::string cur;
size_t p = 0;
while (p < copy.size()) {
if (copy.compare(p, 4, "<br>") == 0) { blocks.push_back(cur); cur.clear(); p += 4; continue; }
if (copy.compare(p, 5, "<br/>") == 0) { blocks.push_back(cur); cur.clear(); p += 5; continue; }
if (copy.compare(p, 6, "<br />") == 0) { blocks.push_back(cur); cur.clear(); p += 6; continue; }
cur += copy[p]; p++;
}
if (!cur.empty() || blocks.empty()) blocks.push_back(cur);
std::vector<std::string> wrapped;
for (auto &cl : blocks) {
std::vector<std::string> words;
size_t wp = 0;
while (wp < cl.size()) {
while (wp < cl.size() && cl[wp] == ' ') wp++;
if (wp >= cl.size()) break;
size_t ws = wp;
while (wp < cl.size() && cl[wp] != ' ') wp++;
words.push_back(cl.substr(ws, wp - ws));
}
std::string curline;
for (auto &word : words) {
int wlen = visible_len(word);
if (wlen > width) {
std::string w = word;
size_t wi = 0;
while (wi < w.size()) {
if (!curline.empty()) { wrapped.push_back(curline); curline.clear(); }
std::string chunk;
int clen = 0;
size_t ci = wi;
while (ci < w.size() && clen < width) {
if (w[ci] == 0x1b) {
size_t j = ci + 1;
if (j < w.size() && w[j] == '[') {
j++;
while (j < w.size() && !(w[j] >= 0x40 && w[j] <= 0x7e)) j++;
if (j < w.size()) j++;
}
chunk += w.substr(ci, j - ci); ci = j;
continue;
}
size_t pl; int cp = utf8_decode((const unsigned char *)(w.data() + ci), w.size() - ci, &pl);
chunk += w.substr(ci, pl); ci += pl; clen += rune_width(cp);
}
wrapped.push_back(chunk);
wi = ci;
}
continue;
}
int curlen = visible_len(curline);
if (curline.empty()) curline = word;
else if (curlen + 1 + wlen <= width) { curline += ' '; curline += word; }
else { wrapped.push_back(curline); curline = word; }
}
if (!curline.empty()) wrapped.push_back(curline);
}
if (wrapped.empty()) wrapped.push_back("");
return wrapped;
}
static void print_row(int num_cols, const std::vector<int> &col_widths,
const std::vector<std::string> &cells, bool is_header) {
std::string s = "│";
for (int i = 0; i < num_cols; i++) {
std::string_view txt = (i < (int)cells.size()) ? std::string_view(cells[i]) : std::string_view();
int pad = col_widths[i] - visible_len(txt);
if (pad < 0) pad = 0;
std::string cell = " ";
cell.append(txt);
cell.append((size_t)pad + 1, ' ');
if (is_header) { std::string cc = cstr(cell, std::array{1}); s += cc; }
else { std::string cc = cstr(cell, std::array{2}); s += cc; }
s += "│";
}
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
void render_table_print(const std::vector<std::string> &lines) {
if (lines.empty()) return;
std::vector<std::vector<std::string>> rows_cells;
std::vector<std::string> header_cells;
bool has_header = false;
for (auto &line : lines) {
auto cells = parse_table_cells(line);
if (is_table_sep(cells)) {
if (!rows_cells.empty() && !has_header) {
header_cells = std::move(rows_cells.back());
rows_cells.pop_back();
has_header = true;
}
continue;
}
rows_cells.push_back(std::move(cells));
}
if (!has_header && lines.size() < 2) {
MdState st;
for (auto &ln : lines) render_md_line_print(ln, st);
return;
}
int num_cols = (int)header_cells.size();
for (auto &row : rows_cells) if ((int)row.size() > num_cols) num_cols = (int)row.size();
if (num_cols == 0) return;
if (has_header) { while ((int)header_cells.size() < num_cols) header_cells.push_back(""); }
std::vector<int> col_widths(num_cols, 0);
for (int i = 0; i < num_cols; i++) {
if (has_header) { int vl = visible_len(header_cells[i]); if (vl > col_widths[i]) col_widths[i] = vl; }
for (auto &row : rows_cells) {
std::string_view cell = (i < (int)row.size()) ? std::string_view(row[i]) : std::string_view();
std::string ri = render_inline(cell);
int vl = visible_len(ri);
if (vl > col_widths[i]) col_widths[i] = vl;
}
if (col_widths[i] < 3) col_widths[i] = 3;
}
int max_table_width = term_width();
if (max_table_width < 20) max_table_width = 80;
int overhead = 3 * num_cols + 1;
int avail = max_table_width - overhead;
if (avail < num_cols * 3) avail = num_cols * 3;
int tot = 0;
for (int i = 0; i < num_cols; i++) tot += col_widths[i];
while (tot > avail) {
int maxIdx = 0, maxVal = col_widths[0];
for (int i = 1; i < num_cols; i++) if (col_widths[i] > maxVal) { maxVal = col_widths[i]; maxIdx = i; }
if (maxVal <= 3) break;
col_widths[maxIdx]--; tot--;
}
{
std::string s = "┌";
for (int i = 0; i < num_cols; i++) { for (int j = 0; j < col_widths[i] + 2; j++) s += "─"; if (i < num_cols - 1) s += "┬"; }
s += "┐";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
if (has_header) {
std::vector<std::vector<std::string>> hw(num_cols);
int maxh = 1;
for (int i = 0; i < num_cols; i++) {
std::string ri = render_inline(header_cells[i]);
hw[i] = wrap_cell(ri, col_widths[i]);
if ((int)hw[i].size() > maxh) maxh = (int)hw[i].size();
}
for (int lr = 0; lr < maxh; lr++) {
std::vector<std::string> rowview(num_cols);
for (int i = 0; i < num_cols; i++) rowview[i] = (lr < (int)hw[i].size()) ? hw[i][lr] : "";
print_row(num_cols, col_widths, rowview, true);
}
{
std::string s = "├";
for (int i = 0; i < num_cols; i++) { for (int j = 0; j < col_widths[i] + 2; j++) s += "─"; if (i < num_cols - 1) s += "┼"; }
s += "┤";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
}
for (auto &row : rows_cells) {
std::vector<std::vector<std::string>> rw(num_cols);
int maxr = 1;
for (int i = 0; i < num_cols; i++) {
std::string_view cell = (i < (int)row.size()) ? std::string_view(row[i]) : std::string_view();
std::string ri = render_inline(cell);
rw[i] = wrap_cell(ri, col_widths[i]);
if ((int)rw[i].size() > maxr) maxr = (int)rw[i].size();
}
for (int lr = 0; lr < maxr; lr++) {
std::vector<std::string> rowview(num_cols);
for (int i = 0; i < num_cols; i++) rowview[i] = (lr < (int)rw[i].size()) ? rw[i][lr] : "";
print_row(num_cols, col_widths, rowview, false);
}
}
{
std::string s = "└";
for (int i = 0; i < num_cols; i++) { for (int j = 0; j < col_widths[i] + 2; j++) s += "─"; if (i < num_cols - 1) s += "┴"; }
s += "┘";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
}
/* ----------------------------- block ---------------------------------- */
void render_md_print(std::string_view text) {
if (!g_col) { if (!text.empty()) fputs(std::string(text).c_str(), stdout); return; }
if (text.empty()) return;
std::vector<std::string> tbl;
MdState st;
size_t start = 0;
while (true) {
auto nl = text.find('\n', start);
std::string_view line = (nl == std::string_view::npos) ? text.substr(start) : text.substr(start, nl - start);
std::string lstr(line);
std::string_view t = trim_spaces(lstr);
bool is_tbl = (!st.in_code && !(t.size() >= 3 && t.substr(0, 3) == "```") && is_table_line(lstr));
if (is_tbl) {
tbl.push_back(lstr);
} else {
if (!tbl.empty()) { render_table_print(tbl); tbl.clear(); }
render_md_line_print(lstr, st);
}
if (nl == std::string_view::npos) break;
start = nl + 1;
}
if (!tbl.empty()) render_table_print(tbl);
}
+259
View File
@@ -0,0 +1,259 @@
/* session.cpp — session persistence, loading, compaction. */
#include "bantam.hpp"
#include <algorithm>
#include <cstdio>
#include <ctime>
#include <dirent.h>
#include <fstream>
#include <memory>
#include <nlohmann/json.hpp>
#include <sys/stat.h>
#include <unistd.h>
using json = nlohmann::json;
static const char *compaction_prompt =
"You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely, preserving all important facts, decisions, code snippets, tool outputs, errors, and current task state so work can seamlessly continue. Output only the summary.";
/* ----------------------------- messages ------------------------------- */
static void serialize_msg_full(json &obj, const Message &m) {
obj["role"] = m.role.empty() ? "" : m.role;
if (!m.content.empty()) obj["content"] = m.content;
if (!m.reasoning.empty()) obj["reasoning_content"] = m.reasoning;
if (!m.calls.empty()) {
json arr = json::array();
for (auto &tc : m.calls) {
json t;
if (!tc.id.empty()) t["id"] = tc.id;
t["type"] = "function";
json fn;
fn["name"] = tc.name.empty() ? "" : tc.name;
fn["arguments"] = tc.arguments.empty() ? "{}" : tc.arguments;
t["function"] = fn;
arr.push_back(t);
}
obj["tool_calls"] = arr;
}
if (!m.tool_call_id.empty()) obj["tool_call_id"] = m.tool_call_id;
}
static void parse_msg_full(const json &obj, Message &m) {
m = Message{};
if (!obj.is_object()) return;
if (obj.contains("role") && obj["role"].is_string()) m.role = obj["role"].get<std::string>();
else m.role = "user";
if (obj.contains("content") && obj["content"].is_string()) m.content = obj["content"].get<std::string>();
if (obj.contains("reasoning_content") && obj["reasoning_content"].is_string())
m.reasoning = obj["reasoning_content"].get<std::string>();
if (obj.contains("tool_call_id") && obj["tool_call_id"].is_string())
m.tool_call_id = obj["tool_call_id"].get<std::string>();
if (obj.contains("tool_calls") && obj["tool_calls"].is_array()) {
for (auto &tc : obj["tool_calls"]) {
if (!tc.is_object()) continue;
ToolCall t;
if (tc.contains("id") && tc["id"].is_string()) t.id = tc["id"].get<std::string>();
if (tc.contains("function") && tc["function"].is_object()) {
auto fn = tc["function"];
if (fn.contains("name") && fn["name"].is_string()) t.name = fn["name"].get<std::string>();
if (fn.contains("arguments") && fn["arguments"].is_string()) t.arguments = fn["arguments"].get<std::string>();
}
m.calls.push_back(std::move(t));
}
}
}
/* ----------------------------- paths ---------------------------------- */
static std::string home_dir() {
const char *h = std::getenv("HOME");
if (h && *h) return h;
return ".";
}
static std::string sessions_dir() {
std::string d = home_dir() + "/.bantam/sessions";
mkdir_p(d);
return d;
}
void save_session(const Messages &msgs, std::string_view sid,
std::string *out_id, std::string *out_summary) {
std::string id = (!sid.empty()) ? std::string(sid) : project_id();
std::string d = sessions_dir();
std::string path = d + "/" + id + ".json";
json root = json::object();
root["id"] = id;
time_t now = time(nullptr);
struct tm tm_buf{};
localtime_r(&now, &tm_buf);
char ts[64];
std::strftime(ts, sizeof ts, "%Y-%m-%d %H:%M:%S", &tm_buf);
root["created"] = ts;
std::string sum = summary_of(msgs);
root["summary"] = sum;
json arr = json::array();
for (auto &m : msgs.items) {
json mo;
serialize_msg_full(mo, m);
arr.push_back(mo);
}
root["messages"] = arr;
{
std::ofstream f(path, std::ios::binary);
if (f) f << root.dump(2, ' ', false, json::error_handler_t::replace);
}
if (out_id) *out_id = id;
if (out_summary) *out_summary = sum;
}
std::vector<Session> sessions_load() {
std::vector<Session> ss;
std::string d = sessions_dir();
std::vector<std::string> files;
{
DIR *dir = opendir(d.c_str());
if (dir) {
struct dirent *de;
while ((de = readdir(dir))) {
std::string nm = de->d_name;
if (nm == "." || nm == "..") continue;
if (nm.size() < 5 || nm.substr(nm.size() - 5) != ".json") continue;
files.push_back(nm);
}
closedir(dir);
}
}
std::sort(files.begin(), files.end()); /* alphasort (ascending) */
for (auto &nm : files) {
std::string full = d + "/" + nm;
std::ifstream f(full, std::ios::binary);
if (!f) continue;
std::string buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
json root;
try { root = json::parse(buf); } catch (...) { continue; }
if (!root.is_object()) continue;
Session s;
if (root.contains("id") && root["id"].is_string()) s.id = root["id"].get<std::string>();
if (root.contains("created") && root["created"].is_string()) s.created = root["created"].get<std::string>();
if (root.contains("summary") && root["summary"].is_string()) s.summary = root["summary"].get<std::string>();
if (root.contains("messages") && root["messages"].is_array()) {
for (auto &mo : root["messages"]) {
Message m; parse_msg_full(mo, m); s.messages.items.push_back(std::move(m));
}
}
if (s.summary.empty()) s.summary = "(no summary)";
ss.push_back(std::move(s));
}
/* sort by id descending (newest first) */
std::sort(ss.begin(), ss.end(),
[](const Session &a, const Session &b) { return a.id > b.id; });
return ss;
}
std::unique_ptr<Messages> load_session(std::string_view sid, std::string *err) {
auto ss = sessions_load();
for (auto &s : ss) {
if (s.id == sid) {
auto res = std::make_unique<Messages>();
for (auto &m : s.messages.items) res->items.push_back(m);
return res;
}
}
/* prefix match */
std::vector<Session *> pref;
for (auto &s : ss) if (s.id.rfind(sid, 0) == 0) pref.push_back(&s);
if (pref.size() == 1) {
auto res = std::make_unique<Messages>();
for (auto &m : pref[0]->messages.items) res->items.push_back(m);
return res;
}
if (pref.size() > 1) {
std::string names;
for (auto *p : pref) str_appendf(names, "%s, ", p->id.c_str());
if (names.size() > 2) names.resize(names.size() - 2);
if (err) *err = sfmt("ambiguous prefix: %s", names.c_str());
} else {
if (err) *err = sfmt("session not found: %s", std::string(sid).c_str());
}
return nullptr;
}
std::unique_ptr<Session> find_project_session() {
std::string pid = project_id();
auto ss = sessions_load();
for (auto &s : ss) {
if (s.id == pid) {
auto found = std::make_unique<Session>();
*found = std::move(s);
return found;
}
}
return nullptr;
}
void autosave(const Messages &msgs) {
std::string id, sum;
save_session(msgs, project_id(), &id, &sum);
}
/* ----------------------------- summary / compact ---------------------- */
std::string summary_of(const Messages &msgs) {
for (auto &m : msgs.items) {
if (m.role == "user" && !m.content.empty()) {
std::string s;
const char *p = m.content.c_str();
bool prev_space = false;
while (*p) {
if (*p == '\n' || *p == '\t' || *p == '\r') { if (!prev_space) s += ' '; prev_space = true; }
else { s += *p; prev_space = false; }
p++;
}
if (s.size() > 80) {
size_t len = utf8_truncate(s, 80).size();
s.resize(len);
s += "...";
}
return s;
}
}
return "(empty session)";
}
std::unique_ptr<Messages> compact(Cfg &cfg, const Messages &msgs, std::string *summary,
std::string *err, volatile int *interrupted) {
if (msgs.items.empty() || msgs.items[0].role != "system") {
if (err) *err = "session has no system message";
return nullptr;
}
if (msgs.items.size() <= 1) { if (err) *err = "nothing to compact"; return nullptr; }
Messages cMsgs;
for (auto &m : msgs.items) cMsgs.items.push_back(m);
Message userp;
userp.role = "user";
userp.content = compaction_prompt;
cMsgs.items.push_back(std::move(userp));
Cfg cc = cfg; cc.stream = false; cc.force_stream = false; /* force non-streaming for clean summary */
Message m; Usage u;
std::string lerr;
int rc = llm(cc, cMsgs, false, m, u, lerr, interrupted);
if (rc != 0) { if (err) *err = lerr.empty() ? "compaction LLM call failed" : lerr; return nullptr; }
std::string s;
if (!m.content.empty()) s = m.content;
else if (!m.reasoning.empty()) s = m.reasoning;
else { if (err) *err = "LLM returned an empty summary"; return nullptr; }
size_t a = 0;
while (a < s.size() && (s[a] == ' ' || s[a] == '\n' || s[a] == '\r' || s[a] == '\t')) a++;
std::string trim = s.substr(a);
while (!trim.empty() && (trim.back() == ' ' || trim.back() == '\n' || trim.back() == '\r' || trim.back() == '\t'))
trim.pop_back();
std::string final = trim;
auto newMsgs = std::make_unique<Messages>();
Message sys; sys.role = "system";
sys.content = msgs.items[0].content;
newMsgs->items.push_back(std::move(sys));
Message sm; sm.role = "user";
sm.content = sfmt("Summary of the previous conversation:\n%s\n\nPlease continue from here.", final.c_str());
newMsgs->items.push_back(std::move(sm));
if (summary) *summary = newMsgs->items[1].content;
return newMsgs;
}
+241
View File
@@ -0,0 +1,241 @@
/* tools.cpp — bash/read/write tools and oversized result offloading. */
#include "bantam.hpp"
#include <algorithm>
#include <array>
#include <cstring>
#include <fstream>
#include <cerrno>
#include <dirent.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
/* ----------------------------- shell ---------------------------------- */
std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
int timeout, volatile int *interrupted) {
std::string fcmd = filter_text(cmd);
if (timeout <= 0) timeout = 120;
int pipefd[2];
if (pipe(pipefd) != 0) return "[tool error: pipe failed]";
pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return "[tool error: fork failed]";
}
if (pid == 0) {
/* child */
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
close(pipefd[0]);
close(pipefd[1]);
if (!workdir.empty()) chdir(std::string(workdir).c_str());
execl("/bin/bash", "bash", "-c", fcmd.c_str(), (char *)nullptr);
_exit(127);
}
close(pipefd[1]);
int flags = fcntl(pipefd[0], F_GETFL, 0);
fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK);
std::string out;
struct pollfd pfd; pfd.fd = pipefd[0]; pfd.events = POLLIN;
time_t deadline = time(nullptr) + timeout;
bool killed = false;
while (true) {
if (*interrupted) break;
int remain = (int)(deadline - time(nullptr));
if (remain <= 0) { killed = true; break; }
int pr = poll(&pfd, 1, remain * 1000);
if (pr < 0) { if (errno == EINTR) continue; break; }
if (pr == 0) { killed = true; break; }
char buf[4096];
ssize_t n = read(pipefd[0], buf, sizeof buf);
if (n > 0) out.append(buf, (size_t)n);
else if (n == 0) break; /* EOF: child exited */
else if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
else break;
}
if (*interrupted || killed) {
kill(pid, SIGKILL);
/* drain remaining output briefly */
struct pollfd dpfd; dpfd.fd = pipefd[0]; dpfd.events = POLLIN;
for (int i = 0; i < 20; i++) {
int pr = poll(&dpfd, 1, 50);
if (pr <= 0) break;
char buf[4096];
ssize_t n = read(pipefd[0], buf, sizeof buf);
if (n > 0) out.append(buf, (size_t)n);
else break;
}
}
int status = 0;
while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
close(pipefd[0]);
std::string filtered = filter_text(out);
/* trim trailing whitespace */
while (!filtered.empty() &&
(filtered.back() == '\n' || filtered.back() == '\r' ||
filtered.back() == ' ' || filtered.back() == '\t'))
filtered.pop_back();
if (*interrupted) {
return filtered + "\n\nexit: -1";
}
if (killed) {
return sfmt("%s\n\n[shell timeout after %ds]\nexit: -1", filtered.c_str(), timeout);
}
int code = 0;
if (WIFEXITED(status)) code = WEXITSTATUS(status);
else if (WIFSIGNALED(status)) code = -1;
return sfmt("%s\n\nexit: %d", filtered.c_str(), code);
}
/* ----------------------------- read ----------------------------------- */
std::string read_file_or_dir(std::string_view path, int offset, int limit, int *style) {
std::string p(path);
/* trim */
size_t s = 0;
while (s < p.size() && (p[s] == ' ' || p[s] == '\t')) s++;
p = p.substr(s);
if (p.empty()) { *style = 31; return "[tool error: read requires 'filePath' parameter]"; }
struct stat st;
if (stat(p.c_str(), &st) != 0) {
*style = 31;
return sfmt("[tool error: read %s: %s]", p.c_str(), strerror(errno));
}
if (S_ISDIR(st.st_mode)) {
std::vector<std::string> raw_names;
{
DIR *dir = opendir(p.c_str());
if (!dir) { *style = 31; return "[tool error: read (directory listing failed)]"; }
struct dirent *de;
while ((de = readdir(dir))) {
std::string nm = de->d_name;
if (nm == "." || nm == "..") continue;
raw_names.push_back(std::move(nm));
}
closedir(dir);
}
std::sort(raw_names.begin(), raw_names.end());
std::string b;
for (auto &nm : raw_names) {
std::string full = p + "/" + nm;
struct stat ds;
bool isdir = (stat(full.c_str(), &ds) == 0 && S_ISDIR(ds.st_mode));
b += nm;
if (isdir) b += '/';
b += '\n';
}
if (!b.empty()) b.pop_back(); /* trailing newline */
*style = 2;
return b;
}
/* regular file */
std::ifstream f(p, std::ios::binary);
if (!f) { *style = 31; return sfmt("[tool error: read %s: %s]", p.c_str(), strerror(errno)); }
if (offset < 1) offset = 1;
if (limit <= 0) limit = 2000;
std::string b;
std::string line;
int line_num = 0, read_count = 0;
while (std::getline(f, line)) {
line_num++;
if (line_num < offset) continue;
read_count++;
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back();
std::string_view lview = utf8_truncate(line, 2000);
str_appendf(b, "%d: ", line_num);
b.append(lview);
b += '\n';
if (read_count >= limit) break;
}
if (!b.empty()) b.pop_back(); /* trailing newline */
*style = 2;
return b;
}
/* ----------------------------- write ---------------------------------- */
std::string write_file(std::string_view path, int offset, int del_bytes,
std::string_view content, int *err) {
*err = 0;
std::string p(path);
size_t s = 0;
while (s < p.size() && (p[s] == ' ' || p[s] == '\t')) s++;
p = p.substr(s);
if (p.empty()) { *err = 1; return "[tool error: path is required]"; }
if (offset < 0) offset = 0;
if (del_bytes < 0) del_bytes = 0;
std::string data;
struct stat st;
if (stat(p.c_str(), &st) == 0) {
if (S_ISDIR(st.st_mode)) {
*err = 1;
return sfmt("[tool error: write %s: Is a directory]", p.c_str());
}
std::ifstream f(p, std::ios::binary);
if (!f) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); }
data.assign((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
} else {
auto slash = p.find_last_of('/');
if (slash != std::string::npos) mkdir_p(std::string_view(p).substr(0, slash));
else mkdir_p(".");
}
if ((size_t)offset > data.size()) {
data.resize((size_t)offset, '\0');
}
size_t end_del = (size_t)offset + (size_t)del_bytes;
size_t prefix_len = (size_t)offset;
size_t suffix_len = (end_del < data.size()) ? (data.size() - end_del) : 0;
size_t clen = content.size();
size_t newlen = prefix_len + clen + suffix_len;
std::string nd;
nd.reserve(newlen);
nd.append(data, 0, prefix_len);
nd.append(content);
if (suffix_len) nd.append(data, end_del, suffix_len);
{
std::ofstream out(p, std::ios::binary | std::ios::trunc);
if (!out) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); }
out.write(nd.data(), (std::streamsize)nd.size());
}
return sfmt("Successfully wrote %zu bytes to %s", clen, p.c_str());
}
/* ----------------------------- offload -------------------------------- */
std::string offload_tool_result(std::string_view res, int max_res,
std::vector<std::string> &tmps) {
if (max_res <= 0) max_res = 65536;
if (res.size() <= (size_t)max_res) return std::string(res);
std::string dir = tool_res_dir();
std::string tpl = dir + "/toolres-XXXXXX";
int fd = mkstemp(tpl.data());
if (fd < 0) return std::string(res);
size_t written = 0;
bool write_ok = true;
while (written < res.size()) {
ssize_t w = write(fd, res.data() + written, res.size() - written);
if (w < 0) {
if (errno == EINTR) continue;
write_ok = false;
break;
}
written += (size_t)w;
}
close(fd);
if (!write_ok) { unlink(tpl.c_str()); return std::string(res); }
tmps.push_back(tpl);
return sfmt("[tool result too large for context: %zu bytes (limit %d). The full output was saved to %s. Read it with the read tool; to read it partially, use offset and limit. The file is %zu bytes long.]",
res.size(), max_res, tpl.c_str(), res.size());
}
void cleanup_temps(std::vector<std::string> &tmps) {
for (auto &t : tmps) unlink(t.c_str());
tmps.clear();
}
+349
View File
@@ -0,0 +1,349 @@
/* util.cpp — string building, UTF-8, filtering, color, IDs, MD5, misc. */
#include "bantam.hpp"
#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <signal.h>
#include <sys/stat.h>
#include <unistd.h>
/* ----------------------------- formatting ----------------------------- */
static std::string vfmt(const char *fmt, va_list ap) {
va_list cp;
va_copy(cp, ap);
int need = std::vsnprintf(nullptr, 0, fmt, cp);
va_end(cp);
if (need < 0) return {};
std::string s;
s.resize((size_t)need);
std::vsnprintf(s.data(), (size_t)need + 1, fmt, ap);
return s;
}
std::string sfmt(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
std::string s = vfmt(fmt, ap);
va_end(ap);
return s;
}
void str_appendf(std::string &s, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
std::string more = vfmt(fmt, ap);
va_end(ap);
s += more;
}
/* ----------------------------- UTF-8 ---------------------------------- */
int utf8_decode(const unsigned char *p, size_t avail, size_t *plen) {
if (avail == 0) { *plen = 0; return 0xFFFD; }
unsigned char b0 = p[0];
if (b0 < 0x80) { *plen = 1; return b0; }
if ((b0 & 0xE0) == 0xC0) {
if (avail < 2 || (p[1] & 0xC0) != 0x80) { *plen = 1; return 0xFFFD; }
int cp = ((b0 & 0x1F) << 6) | (p[1] & 0x3F);
if (cp < 0x80) { *plen = 1; return 0xFFFD; }
*plen = 2; return cp;
}
if ((b0 & 0xF0) == 0xE0) {
if (avail < 3 || (p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80) { *plen = 1; return 0xFFFD; }
int cp = ((b0 & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F);
if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF)) { *plen = 1; return 0xFFFD; }
*plen = 3; return cp;
}
if ((b0 & 0xF8) == 0xF0) {
if (avail < 4 || (p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80 || (p[3] & 0xC0) != 0x80) { *plen = 1; return 0xFFFD; }
int cp = ((b0 & 0x07) << 18) | ((p[1] & 0x3F) << 12) | ((p[2] & 0x3F) << 6) | (p[3] & 0x3F);
if (cp < 0x10000 || cp > 0x10FFFF) { *plen = 1; return 0xFFFD; }
*plen = 4; return cp;
}
*plen = 1; return 0xFFFD;
}
std::string_view utf8_truncate(std::string_view s, size_t max_bytes) {
if (s.size() <= max_bytes) return s;
const unsigned char *p = (const unsigned char *)s.data();
size_t pos = 0, avail = s.size();
while (avail > 0) {
size_t pl;
utf8_decode(p + pos, avail, &pl);
if (pl == 0 || pos + pl > max_bytes) break;
pos += pl;
avail -= pl;
}
return s.substr(0, pos);
}
/* ----------------------------- filtering ------------------------------ */
static bool kept_rune(int r) {
if (r == ' ' || r == '\t' || r == '\n') return true;
if (r < 0x20) return false; /* C0 controls */
if (r == 0x7f) return false; /* DEL */
if (r >= 0x80 && r <= 0x9f) return false; /* C1 controls */
switch (r) {
case 0x00a0: case 0x00ad:
case 0x2000: case 0x2001: case 0x2002: case 0x2003: case 0x2004:
case 0x2005: case 0x2006: case 0x2007: case 0x2008: case 0x2009:
case 0x200a: case 0x2028: case 0x2029: case 0x202f: case 0x205f:
case 0x3000:
case 0x200b: case 0x200c: case 0x200d: case 0x2060: case 0xfeff:
case 0x200e: case 0x200f:
case 0x202a: case 0x202b: case 0x202c: case 0x202d: case 0x202e:
return false;
}
return true;
}
std::string filter_text(std::string_view s) {
std::string out;
out.reserve(s.size());
const unsigned char *p = (const unsigned char *)s.data();
size_t avail = s.size();
while (avail > 0) {
size_t pl;
int cp = utf8_decode(p, avail, &pl);
if (cp == 0xFFFD && pl == 1) { p++; avail--; continue; }
if (kept_rune(cp)) out.append((const char *)p, pl);
p += pl; avail -= pl;
}
return out;
}
/* ----------------------------- color ---------------------------------- */
int g_col = 0;
volatile int g_interrupted = 0;
std::string cstr(std::string_view text, std::span<const int> codes) {
if (!g_col || codes.empty() || text.empty()) return std::string(text);
std::string s;
s.reserve(text.size() + 16);
s += "\033[";
for (size_t i = 0; i < codes.size(); i++) {
if (i) s += ';';
s += sfmt("%d", codes[i]);
}
s += "m";
s += text;
s += "\033[0m";
return s;
}
void cprint(std::string_view text, std::span<const int> codes) {
std::string w = cstr(text, codes);
std::fputs(w.c_str(), stdout);
}
int visible_len(std::string_view s) {
int n = 0;
const unsigned char *p = (const unsigned char *)s.data();
size_t avail = s.size();
while (avail > 0) {
if (p[0] == 0x1b) {
size_t i = 1;
if (i < avail && p[1] == '[') {
i++;
while (i < avail && !(p[i] >= 0x40 && p[i] <= 0x7e)) i++;
if (i < avail) i++;
} else {
i = 1;
}
p += i; avail -= i;
continue;
}
if (p[0] == 0x01 || p[0] == 0x02) { p++; avail--; continue; }
size_t pl;
int cp = utf8_decode(p, avail, &pl);
n += rune_width(cp);
p += pl; avail -= pl;
}
return n;
}
/* Display width of a single Unicode code point. Most code points occupy one
* terminal column, but East-Asian Wide/Fullwidth characters (notably CJK) and
* many emoji occupy two. Combining marks and zero-width characters occupy
* none. Without this, tables containing Chinese text were misaligned because
* visible_len counted every rune as a single column. */
int rune_width(int cp) {
if (cp < 0x20) return 0;
if (cp == 0x7f) return 0;
/* combining marks / zero-width */
if ((cp >= 0x0300 && cp <= 0x036f) || (cp >= 0x0483 && cp <= 0x0489) ||
(cp >= 0x0591 && cp <= 0x05bd) || (cp >= 0x0610 && cp <= 0x061a) ||
(cp >= 0x064b && cp <= 0x065f) || (cp >= 0x0670 && cp <= 0x0670) ||
(cp >= 0x06d6 && cp <= 0x06dc) || (cp >= 0x0e31 && cp <= 0x0e3a) ||
(cp >= 0x0e47 && cp <= 0x0e4e) || (cp >= 0x0eb1 && cp <= 0x0eb9) ||
(cp >= 0x0ebb && cp <= 0x0ebd) || (cp >= 0x0ec8 && cp <= 0x0ecd) ||
(cp >= 0x200b && cp <= 0x200f) || (cp >= 0x202a && cp <= 0x202e) ||
(cp >= 0x2060 && cp <= 0x2064) || (cp >= 0xfe00 && cp <= 0xfe0f) ||
(cp == 0xfeff) || (cp >= 0xfff9 && cp <= 0xfffb) ||
(cp >= 0x1d173 && cp <= 0x1d17a))
return 0;
/* East Asian Wide / Fullwidth */
if (cp == 0x3000) return 2;
if (cp >= 0x1100 && cp <= 0x115f) return 2;
if (cp >= 0x2e80 && cp <= 0x303e) return 2;
if (cp >= 0x3041 && cp <= 0x33ff) return 2;
if (cp >= 0x3400 && cp <= 0x4dbf) return 2;
if (cp >= 0x4e00 && cp <= 0x9fff) return 2;
if (cp >= 0xa000 && cp <= 0xa4cf) return 2;
if (cp >= 0xac00 && cp <= 0xd7a3) return 2;
if (cp >= 0xf900 && cp <= 0xfaff) return 2;
if (cp >= 0xfe30 && cp <= 0xfe4f) return 2;
if (cp >= 0xff00 && cp <= 0xff60) return 2;
if (cp >= 0xffe0 && cp <= 0xffe6) return 2;
if (cp >= 0x1f300 && cp <= 0x1faff) return 2;
if (cp >= 0x20000 && cp <= 0x3fffd) return 2;
return 1;
}
/* ----------------------------- paths ---------------------------------- */
void mkdir_p(std::string_view dir) {
if (dir.empty()) return;
std::string path(dir);
for (size_t i = 1; i < path.size(); i++) {
if (path[i] == '/') {
path[i] = '\0';
mkdir(path.c_str(), 0755);
path[i] = '/';
}
}
mkdir(path.c_str(), 0755);
}
std::string bantam_tmp_dir() {
const char *tmp = std::getenv("TMPDIR");
if (!tmp || !*tmp) tmp = "/tmp";
std::string p = std::string(tmp) + "/bantam";
mkdir_p(p);
return p;
}
std::string tool_res_dir() {
std::string base = bantam_tmp_dir();
std::string p = base + "/toolres";
mkdir_p(p);
return p;
}
std::string project_id() {
char cwd[4096];
if (!getcwd(cwd, sizeof cwd)) std::strcpy(cwd, ".");
char *abs = realpath(cwd, nullptr);
std::string src = abs ? abs : cwd;
if (abs) free(abs);
return md5_hex(src);
}
/* ----------------------------- MD5 ------------------------------------ */
#define MD5_ROTLEFT(x, c) (((x) << (c)) | ((x) >> (32 - (c))))
struct MD5_CTX { uint32_t state[4]; uint32_t count[2]; unsigned char buf[64]; };
static void md5_transform(uint32_t state[4], const unsigned char block[64]);
static void md5_init(MD5_CTX *c) {
c->state[0] = 0x67452301; c->state[1] = 0xefcdab89;
c->state[2] = 0x98badcfe; c->state[3] = 0x10325476;
c->count[0] = c->count[1] = 0;
}
static void md5_update(MD5_CTX *c, const unsigned char *in, size_t len) {
uint32_t bits = (uint32_t)(c->count[0] >> 3) & 0x3F;
if ((c->count[0] += (uint32_t)(len << 3)) < (uint32_t)(len << 3)) c->count[1]++;
c->count[1] += (uint32_t)(len >> 29);
while (len--) {
c->buf[bits++] = *in++;
if (bits == 64) { md5_transform(c->state, c->buf); bits = 0; }
}
}
static void md5_final(unsigned char digest[16], MD5_CTX *c) {
unsigned char bits[8];
for (int i = 0; i < 8; i++) bits[i] = (unsigned char)((c->count[i >> 2] >> ((i & 3) * 8)) & 0xFF);
unsigned char pad = (unsigned char)((c->count[0] >> 3) & 0x3F);
unsigned char padlen = (pad < 56) ? (56 - pad) : (120 - pad);
unsigned char p = 0x80;
md5_update(c, &p, 1);
unsigned char zero = 0;
for (int i = 0; i < padlen - 1; i++) md5_update(c, &zero, 1);
md5_update(c, bits, 8);
for (int i = 0; i < 4; i++) {
digest[i * 4 + 0] = (unsigned char)(c->state[i] & 0xFF);
digest[i * 4 + 1] = (unsigned char)((c->state[i] >> 8) & 0xFF);
digest[i * 4 + 2] = (unsigned char)((c->state[i] >> 16) & 0xFF);
digest[i * 4 + 3] = (unsigned char)((c->state[i] >> 24) & 0xFF);
}
}
static void md5_transform(uint32_t state[4], const unsigned char b[64]) {
uint32_t a = state[0], d = state[3], c = state[2], e = state[1];
uint32_t x[16];
for (int i = 0, j = 0; i < 16; i++, j += 4)
x[i] = (uint32_t)b[j] | ((uint32_t)b[j+1] << 8) | ((uint32_t)b[j+2] << 16) | ((uint32_t)b[j+3] << 24);
static const uint32_t K[64] = {
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391};
static const int S[64] = {
7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22,
5,9,14,20, 5,9,14,20, 5,9,14,20, 5,9,14,20,
4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23,
6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21};
for (int i = 0; i < 64; i++) {
uint32_t f; int g;
if (i < 16) { f = (e & c) | (~e & d); g = i; }
else if (i < 32) { f = (d & e) | (~d & c); g = (5 * i + 1) % 16; }
else if (i < 48) { f = e ^ c ^ d; g = (3 * i + 5) % 16; }
else { f = c ^ (e | (~d)); g = (7 * i) % 16; }
f = f + a + K[i] + x[g];
a = d; d = c; c = e;
e = e + MD5_ROTLEFT(f, S[i]);
}
state[0] += a; state[1] += e; state[2] += c; state[3] += d;
}
std::string md5_hex(std::string_view data) {
MD5_CTX ctx; md5_init(&ctx);
md5_update(&ctx, (const unsigned char *)data.data(), data.size());
unsigned char dig[16];
md5_final(dig, &ctx);
std::string out;
out.reserve(33);
static const char *hx = "0123456789abcdef";
for (int i = 0; i < 16; i++) {
out += hx[dig[i] >> 4];
out += hx[dig[i] & 0xF];
}
return out;
}
/* ----------------------------- misc ----------------------------------- */
int to_int_val(std::string_view s, int def) {
if (s.empty()) return def;
std::string tmp(s);
char *end;
long v = std::strtol(tmp.c_str(), &end, 10);
if (end == tmp.c_str() || *end != '\0') return def;
return (int)v;
}
/* ----------------------------- signal --------------------------------- */
static void sigint_handler(int sig) {
(void)sig;
g_interrupted = 1;
}
void setup_signal_handler() {
struct sigaction sa;
std::memset(&sa, 0, sizeof sa);
sa.sa_handler = sigint_handler;
sigaction(SIGINT, &sa, nullptr);
}
int color_enabled(const Cfg &cfg) {
const char *nc = std::getenv("NO_COLOR");
if (nc && *nc) return 0;
const char *bnc = std::getenv("BANTAM_NO_COLOR");
if (bnc && *bnc) return 0;
const std::string &c = cfg.color;
if (c == "always") return 1;
if (c == "never") return 0;
return is_terminal(STDOUT_FILENO);
}