6.0 KiB
sjcode
a terminal coding agent, powered by the ollama API
What makes it an agent
The client is not a chat front end: the model can really work in your
codebase. It runs shell commands, reads and edits files — on your machine,
in the directory you started ./coder from. Every model reply can contain
tool calls; the client executes them and feeds the real output back to the
model until the task is done.
Tools the agent can use:
| tool | what it does | asks first |
|---|---|---|
run |
run one shell command in the project directory | yes |
read |
read a file (numbered lines, start/limit) |
no |
files |
list project files (skips .git, caches, ...) |
no |
write |
create a file, or overwrite a small one | yes |
edit |
exact-text replace in a file, shows a diff | yes |
Before every run/write/edit you see exactly what will happen (command
line, or a unified diff for file changes) and press Enter to allow it, n to
decline, a to stop the agent — or just type a short instruction ("use ripgrep
instead") and the model adapts. --yes (or /auto in the session) runs
everything without asking; the sandbox keeps all file tools inside the project
directory.
Setup
cp server/config.toml.example server/config.toml # then edit port/model/limits
python3 server/server.py # starts the daemon + admin CLI
config.toml is machine-local and git-ignored; config.toml.example is the
template. On first start the daemon creates the SQLite database (sjcode.db).
Pick a coder model in [ollama] model — one that follows structured output
well, e.g. qwen2.5-coder:7b or bigger. The agent transcript is long, so the
daemon requests a large context window from ollama:
[ollama]
num_ctx = 16384 # context window; do not go below 8192 for agent mode
Daemon (server/server.py)
On startup it prints its settings and drops into a minimal admin CLI (accounts are stored in SQLite):
| command | effect |
|---|---|
create <name> [role] |
create account (asks for password, default role user) |
list |
show all accounts |
delete <name> |
remove account |
role <name> <role> |
change role |
passwd <name> |
set a new password |
usage [name] |
used minutes today vs. limit |
reset [name] |
reset the daily usage counter |
status |
show daemon settings |
quit |
stop the daemon |
While the CLI runs, an HTTP API is served in the background:
POST /login—{"username", "password"}→{"token", ...}POST /chat—{"token", "message", "model"?, "stream"?}→ reply as JSON, or (with"stream": true) as NDJSON lines{"chunk": ...}ending with one final{"response", "used_minutes", "remaining_minutes", ...}lineGET /models?token=...— list ollama modelsGET /status— health check
Every user row stores: username, password_hash, last_logged_in,
role, used_today (minutes of AI usage; resets on a new day).
SJCODE_CONFIG=/path/to/config.toml lets you run a second daemon (tests,
staging) next to a regular one.
Roles and limits
In config.toml each role gets a daily limit in minutes:
[limits]
user = 120 # default role for new accounts
admin = -1 # negative = unlimited
Login and every chat request are blocked once used_today reaches the limit.
Client (client.py)
./coder # or: python3 client.py
python3 client.py 10.0.0.5:8420 # explicit daemon address
python3 client.py --model qwen3-coder # skip the model picker
python3 client.py --yes # auto-approve every tool call (careful)
python3 client.py --no-probe # skip the reachability check (use behind
# HTTP proxies that answer on every port)
On start the client asks which server to connect to: pick one of the saved
ones, or add a new one (n). Every server you use is saved in
~/.config/sjcode/client.json together with the username and the last
model, so the next start is just Enter-Enter. Addresses without a port use
8420; host:port, http(s)://... URLs and IPv6 literals are accepted.
After login and model choice you get the agent banner and a REPL. Type tasks, not chat messages — "rename the function foo to bar in all files" is a task, "what is a monad" is just a question the agent answers in place.
Commands:
| command | effect |
|---|---|
/help |
list commands |
/server |
show the connected server |
/models |
list models on the server |
/model <n> |
switch model (saved for this server) |
/limit |
remaining minutes today (last known) |
/auto |
toggle confirmations (auto-approve all) |
/state |
auto mode, project dir, history size |
/cwd |
show the project directory |
/clear |
forget the conversation history |
/quit |
exit |
The agent keeps the last few finished tasks as context. It stops after 30 model round-trips per task; the daemon API itself is stateless.
Requirements
- Python 3.11+ (stdlib only;
tomllibis included) - ollama listening on the address from
config.toml
Tests
python3 -m unittest test_client.py -v # unit tests (no network)
python3 test_e2e.py # fake ollama + daemon + client