# 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 ```bash 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: ```toml [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 [role]` | create account (asks for password, default role `user`) | | `list` | show all accounts | | `delete ` | remove account | | `role ` | change role | | `passwd ` | 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", ...}` line - `GET /models?token=...` — list ollama models - `GET /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: ```toml [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) ```bash ./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 ` | 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; `tomllib` is included) - ollama listening on the address from `config.toml` ## Tests ```bash python3 -m unittest test_client.py -v # unit tests (no network) python3 test_e2e.py # fake ollama + daemon + client ```