s-app-scoder-agent 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.scoderrc.example.json +97 -0
  2. package/LICENSE +21 -21
  3. package/README.md +600 -468
  4. package/dist/agent.js +841 -197
  5. package/dist/agents.js +70 -0
  6. package/dist/compaction.js +114 -0
  7. package/dist/config.js +178 -12
  8. package/dist/context.js +119 -14
  9. package/dist/cost.js +56 -6
  10. package/dist/guardrails.js +423 -0
  11. package/dist/index.js +129 -76
  12. package/dist/json-schema.js +58 -0
  13. package/dist/llm.js +123 -9
  14. package/dist/mcp.js +397 -70
  15. package/dist/model-catalog.js +175 -0
  16. package/dist/permissions.js +328 -35
  17. package/dist/prompt-builder.js +47 -0
  18. package/dist/prompt.js +103 -0
  19. package/dist/provider-anthropic.js +113 -35
  20. package/dist/provider-registry.js +116 -0
  21. package/dist/retry.js +3 -2
  22. package/dist/sandbox.js +96 -0
  23. package/dist/server.js +32 -2
  24. package/dist/session.js +282 -11
  25. package/dist/shell-scanner.js +112 -0
  26. package/dist/skills.js +122 -0
  27. package/dist/snapshot.js +170 -0
  28. package/dist/steering.js +35 -0
  29. package/dist/tools.js +513 -19
  30. package/dist/tui/app.js +505 -198
  31. package/dist/tui/at-complete.js +93 -0
  32. package/dist/tui/chat-views.js +146 -0
  33. package/dist/tui/chrome.js +96 -0
  34. package/dist/tui/command-registry.js +189 -0
  35. package/dist/tui/commands.js +200 -2
  36. package/dist/tui/image.js +149 -0
  37. package/dist/tui/input.js +214 -0
  38. package/dist/tui/markdown.js +229 -0
  39. package/dist/tui/mouse.js +20 -0
  40. package/dist/tui/permission-modal.js +34 -0
  41. package/dist/tui/review.js +64 -0
  42. package/dist/tui/session-picker.js +68 -0
  43. package/dist/tui/sidebar.js +99 -0
  44. package/dist/tui/state.js +192 -6
  45. package/dist/tui/theme.js +90 -0
  46. package/dist/tui/vim.js +60 -0
  47. package/dist/watch.js +52 -0
  48. package/package.json +16 -8
  49. package/dist/tui-direct.mjs +0 -4
package/README.md CHANGED
@@ -1,468 +1,600 @@
1
- # scoder — Autonomous Coding Agent CLI
2
-
3
- [![CI](https://github.com/nbhson/app-scoder-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/nbhson/app-scoder-agent/actions/workflows/ci.yml)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
- [![node](https://img.shields.io/node/v/app-scoder-agent)](https://www.npmjs.com/package/app-scoder-agent)
6
-
7
- **scoder** is a command-line coding agent that autonomously writes, edits, and builds code until your project compiles without errors. It mirrors the workflow of opencode, cline, and Claude Code, but runs as a single standalone CLI tool.
8
-
9
- ## Installation
10
-
11
- **Requirements:** Node.js ≥ 18
12
-
13
- ### Install from GitHub (current method)
14
-
15
- The package is not yet on the npm registry — install straight from this repo:
16
-
17
- ```bash
18
- # Clones, builds (via the prepare script), and installs the `scoder` command globally
19
- npm install -g github:nbhson/app-scoder-agent
20
- ```
21
-
22
- Or manually from a local clone:
23
-
24
- ```bash
25
- git clone https://github.com/nbhson/app-scoder-agent.git
26
- cd app-scoder-agent
27
- npm install
28
- npm run build
29
- npm install -g . # or: npm link
30
- ```
31
-
32
- > Upgrading: pull the latest code and re-run `npm install -g .` (or point the global install at the repo again).
33
-
34
- ### Try without installing
35
-
36
- ```bash
37
- npx github:nbhson/app-scoder-agent "your task here"
38
- ```
39
-
40
- ### From npm (planned)
41
-
42
- Once published to the npm registry, installation will be a one-liner:
43
-
44
- ```bash
45
- npm install -g app-scoder-agent
46
- # or run ad-hoc:
47
- npx app-scoder-agent "your task"
48
- ```
49
-
50
- ## Quick Start
51
-
52
- ```bash
53
- # One-time setup wizard — writes .scoderrc.json (base URL, key, model, build cmd)
54
- scoder init
55
-
56
- # …or set your API key manually
57
- export SCODER_API_KEY=sk-... # any OpenAI-compatible key (OpenAI, Groq, OpenRouter, Ollama, …)
58
- export OPENAI_BASE_URL=https://api.openai.com/v1 # optional, for custom endpoints
59
-
60
- # Run a task
61
- scoder "add a ping function to src/network.ts"
62
-
63
- # Interactive REPL (supports /help, /clear, /cost, /model)
64
- scoder
65
- ```
66
-
67
- ## How It Works
68
-
69
- ```
70
- You (task) → LLM plans → Tools execute code → Build runs → Success? → Done
71
- ↑ |
72
- └── Build failed? ─┘ (auto-loop)
73
- ```
74
-
75
- **Key constraint:** scoder runs autonomously without stopping for confirmation prompts. If the build fails, it reads the error, patches the code, and retries automatically — until the build passes or the max-step limit is reached.
76
-
77
- ## Tool Set
78
-
79
- | Tool | Description |
80
- |------|-------------|
81
- | `read_file` | Read a file with line numbers |
82
- | `write_file` | Overwrite/create a file |
83
- | `edit_file` | Find-and-replace (unique match required) |
84
- | `list_files` | Recursive directory listing |
85
- | `run_command` | Execute any shell command (subject to permission policy) |
86
- | `web_fetch` | Fetch a public HTTP(S) URL → readable text (HTML→text, JSON pretty-printed) |
87
-
88
- File writes (`write_file` / `edit_file`) are symlink-safe: paths are resolved via `realpath` and cannot escape the working directory, even through a malicious symlink. `read_file` refuses files larger than 2MB with guidance to use `grep`/`head`/`tail`.
89
-
90
- ### Web access (`web_fetch`)
91
-
92
- The agent can read the internet through the built-in `web_fetch` tool — it fetches exact URLs (it cannot search) and returns readable plain text: HTML is converted to text (scripts/styles stripped, entities decoded, title extracted), JSON is pretty-printed, binary content is refused. The system prompt instructs the agent *when to fetch* (external/API docs, URLs in error messages, current versions, changelogs) and when not to (answer available locally).
93
-
94
- Safety defaults:
95
-
96
- - **SSRF protection** — `localhost`, private and link-local addresses (10.x, 172.16/12, 192.168.x, 127.x, 169.254.x, IPv6 ULA/link-local) are blocked. Set `SCODER_WEB_ALLOW_PRIVATE=1` to allow local dev servers.
97
- - Only `http`/`https`; up to 5 redirects — every hop is re-validated against the private-address block list.
98
- - 30 s timeout, SIGINT-abortable; body capped at 512 KB; extracted text capped at 20k chars (configurable via `max_chars`).
99
- - HTML is converted to plain text (no JS execution); images/archives/PDFs are refused.
100
- - Read-only — the tool never writes locally, so it is allowed even in `readonly` permission mode.
101
-
102
- ## Configuration
103
-
104
- ### Environment variables
105
-
106
- | Variable | Default | Description |
107
- |----------|---------|-------------|
108
- | `SCODER_API_KEY` | — | API key (required) |
109
- | `SCODER_MODEL` | `gpt-4o-mini` | Model name |
110
- | `SCODER_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint |
111
- | `SCODER_BUILD_CMD` | auto-detected | Build/compile command |
112
- | `SCODER_MAX_STEPS` | `40` | Max loop iterations before giving up |
113
- | `SCODER_PERMISSION` | `auto` | Permission mode: `auto` \| `ask` \| `readonly` |
114
- | `SCODER_STREAM` | `1` | `0` to disable streaming LLM output |
115
- | `SCODER_STREAM` | `1` | `0` to disable streaming LLM output |
116
- | `SCODER_SERVER_TOKEN` | — | `scoder serve` only: require `Authorization: Bearer <token>` |
117
- | `SCODER_WEB_ALLOW_PRIVATE` | — | Set to `1` to let `web_fetch` reach localhost/private addresses (local dev) |
118
- | `SCODER_NO_APIKEY_WARNING` | — | Set to `1` to suppress the apiKey-in-rc warning |
119
-
120
- ### Per-project config file (.scoderrc.json)
121
-
122
- For any OpenAI-compatible endpoint, just point `baseUrl`, `apiKey`, and `model` at it — no code changes needed. Create `.scoderrc.json` in your project root (see `.scoderrc.example.json`):
123
-
124
- ```json
125
- {
126
- "baseUrl": "https://api.groq.com/openai/v1",
127
- "apiKey": "gsk_...",
128
- "model": "llama-3.3-70b-versatile"
129
- }
130
- ```
131
-
132
- Then simply run `scoder "your task"` — values are picked up automatically.
133
-
134
- #### All fields
135
-
136
- | Field | Default | Description |
137
- |---|---|---|
138
- | `baseUrl` | `https://api.openai.com/v1` | OpenAI-compatible endpoint (must support `/chat/completions` with tool calling) |
139
- | `apiKey` | — | API key (⚠️ prefer `SCODER_API_KEY` env var, see warning below) |
140
- | `model` | `gpt-4o-mini` | Model name served by the endpoint |
141
- | `buildCmd` | auto-detected | Build/compile command (stop condition) |
142
- | `maxSteps` | `40` | Max autonomous loop iterations |
143
- | `stream` | `true` | Stream LLM output token-by-token |
144
- | `provider` | auto-detected | `openai` \| `anthropic` |
145
- | `permission` | `auto` | `auto` \| `ask` \| `readonly` |
146
- | `mcpServers` | — | MCP stdio servers (see below) |
147
- | `permissions` | — | `{ mode, allowCommands, denyCommands }` |
148
-
149
- #### Precedence
150
-
151
- Values are merged from highest to lowest priority — your `.scoderrc.json` value only applies when no flag/env is set:
152
-
153
- 1. **CLI flags** — `-m`, `--base-url`, `-k`, `--provider`, `-B`
154
- 2. **Environment variables** — `SCODER_API_KEY` / `OPENAI_API_KEY`, `SCODER_BASE_URL`, `SCODER_MODEL`, …
155
- 3. **`.scoderrc.json`** (or `.scoderrc`) in the project root — project-local only; there is no global rc file. For a machine-wide default, set the `SCODER_*` env vars in your shell profile instead
156
- 4. Built-in defaults
157
-
158
- > 💡 If your config in `.scoderrc.json` seems ignored, check `echo $SCODER_API_KEY` — env vars override the file.
159
-
160
- #### Examples for common providers
161
-
162
- ```jsonc
163
- // Local Ollama
164
- {
165
- "baseUrl": "http://localhost:11434/v1",
166
- "apiKey": "ollama",
167
- "model": "qwen2.5-scoder:32b"
168
- }
169
- ```
170
-
171
- ```jsonc
172
- // DeepSeek
173
- {
174
- "baseUrl": "https://api.deepseek.com/v1",
175
- "apiKey": "sk-...",
176
- "model": "deepseek-chat"
177
- }
178
- ```
179
-
180
- #### Notes
181
-
182
- - Models starting with `claude-`, or a `baseUrl` containing `api.anthropic.com`, automatically switch to the native Anthropic provider — no `provider` field needed.
183
- - If an `apiKey` is present in the rc file, scoder prints a warning on every run. Silence it with `SCODER_NO_APIKEY_WARNING=1`.
184
-
185
- > ⚠️ **Security:** never commit `apiKey` in `.scoderrc.json` — add the file to `.gitignore` and prefer `SCODER_API_KEY` env var or the `-k` flag. A pre-commit check is available at `scripts/check-secrets.mjs`.
186
-
187
- ## Permissions & Safety
188
-
189
- scoder ships with a layered permission system for `run_command`:
190
-
191
- | Mode | Behavior |
192
- |------|----------|
193
- | `auto` (default) | Autonomous — runs shell commands without confirmation (original behavior) |
194
- | `ask` | Prompts before each shell command not covered by `allowCommands` |
195
- | `readonly` | Shell commands are disabled entirely; the agent can only read/list files. MCP tools are also disabled (they bypass the permission policy) |
196
-
197
- Layered checks, in order:
198
-
199
- 1. **Catastrophic deny list** — `rm -rf /`, `dd of=/dev/...`, fork bombs, `curl | sh`, etc. are **always denied**, in every mode.
200
- 2. **`denyCommands`** (config) — always denied.
201
- 3. **Session allowlist** — commands you approved with `[a] always allow` in this session.
202
- 4. **`allowCommands`** (config) — auto-approved.
203
- 5. **Mode** — `auto` runs everything else; `ask` prompts; `readonly` denies.
204
-
205
- In `ask` mode the prompt offers `[y] yes / [a] always allow "<prefix>" / [n] no / [q] quit`. With `-y` (non-interactive), commands needing confirmation are denied instead of hanging CI.
206
-
207
- ## MCP Servers (stdio)
208
-
209
- scoder can use tools from [MCP](https://modelcontextprotocol.io) servers configured in `.scoderrc.json`:
210
-
211
- ```json
212
- {
213
- "mcpServers": {
214
- "filesystem": {
215
- "command": "npx",
216
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
217
- },
218
- "github": {
219
- "command": "npx",
220
- "args": ["-y", "@modelcontextprotocol/server-github"],
221
- "env": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" }
222
- }
223
- }
224
- }
225
- ```
226
-
227
- - Tools are exposed to the LLM as `mcp__<server>__<tool>` (no name collisions with built-ins)
228
- - Servers connect at agent start; a failing server only logs a warning — the agent keeps running with its built-in tools
229
- - `{env:VAR}` substitution keeps secrets out of config files
230
- - Built-in tools always take priority over MCP tools with colliding names
231
- - ⚠️ **MCP tools bypass the built-in permission system** (no command classification, no diff preview). In `readonly` mode they are therefore disabled entirely; in `auto`/`ask` mode they run unsandboxed — only add MCP servers you trust.
232
-
233
- ## Sessions
234
-
235
- Every run auto-saves its state (messages, steps, build result) to `.scoder/sessions/<id>.json` after each step. If the run is interrupted (Ctrl+C, crash, max steps), you can continue where it left off:
236
-
237
- ```bash
238
- scoder list # list saved sessions
239
- scoder resume last # continue the most recent session
240
- scoder resume <id> "follow-up" # resume + add a new instruction
241
- ```
242
-
243
- Sessions older than 7 days are pruned automatically on the next run. Use `--no-save` to disable (e.g. in CI).
244
-
245
- ## Streaming
246
-
247
- scoder streams everything it can, live, as it happens:
248
-
249
- - **LLM tokens** — assistant text renders character-by-character (OpenAI-compatible SSE and native Anthropic streaming). If a provider doesn't support streaming, scoder automatically falls back to non-streaming requests. Disable with `SCODER_STREAM=0`.
250
- - **Tool output** — `run_command` and build output print live (dimmed block) while the command runs, instead of appearing only after it finishes. Live output is capped at 20k chars; the full output still goes to the LLM.
251
- - **SIGINT-safe** — Ctrl+C kills the running command immediately; streamed rendering never blocks the event loop.
252
- - Output honors `NO_COLOR` and degrades to plain text when piped (CI-friendly).
253
-
254
- ## Full-Screen TUI (`scoder tui`)
255
-
256
- **Mission Control for the build loop** — a terminal UI built with [Ink](https://github.com/vadimdemedes/ink) (React for terminals), driven by the same `AgentEvent` stream the HTTP API exposes:
257
-
258
- ```bash
259
- scoder tui # start an interactive full-screen session
260
- scoder tui "fix the bug" # auto-submit an initial task
261
- ```
262
-
263
- Features:
264
-
265
- - **Build Rail** (top bar) — brand, build lamp (`● build ok` / `● build fail` / `no build yet`), permission mode, and a step fuel gauge (`▰▰▰▱▱▱▱▱▱▱`) that fills as the agent burns through its step budget
266
- - **Chat pane** — your messages, streaming assistant text, and every tool call with live stdout/stderr preview (`✔/✘` status, spinner while running), prefixed with a flight-recorder step number (`03▸`)
267
- - **Diff cards** (`✎`) — every file write/edit renders as a unified diff (`+` green / `-` red / `@@` cyan hunk headers); the same diff also flows out of the HTTP SSE stream
268
- - **Status bar** — model, token usage (input/output/cached), keybind hints
269
- - **Slash commands** — `/help`, `/clear`, `/cost`, `/model`, `/exit` (identical to the REPL)
270
- - **Multi-turn** — keep typing follow-ups after a task finishes; the conversation context carries over (same as the REPL)
271
- - **Scrollback** — `PgUp`/`PgDn` page through history; `Esc` returns to the live tail (press again to abort)
272
- - **Keybinds** — `Enter` send · `PgUp`/`PgDn` scroll · `Esc` reset scroll / abort · `Ctrl+C` quit
273
- - Non-TTY environments (CI, pipes) automatically fall back to the classic REPL
274
- - `permission: "ask"` is not supported in the TUI (no readline prompts on the alt screen) — it falls back to `auto`
275
-
276
- ## HTTP Server Mode (`scoder serve`)
277
-
278
- Run scoder as a local HTTP service — submit tasks programmatically and consume progress as Server-Sent Events:
279
-
280
- ```bash
281
- scoder serve # http://127.0.0.1:0 (random port, printed at startup)
282
- scoder serve --port 4114 # fixed port
283
- SCODER_SERVER_TOKEN=secret scoder serve --host 127.0.0.1
284
- ```
285
-
286
- | Endpoint | Method | Description |
287
- |----------|--------|-------------|
288
- | `/api/health` | GET | Liveness: `{ ok, running, runs }` |
289
- | `/api/runs` | POST | Start a run: `{ "task": "...", "workdir?", "buildCmd?", "model?", "provider?", "maxSteps?", "permission?" }` → `202 { id, eventsUrl }` |
290
- | `/api/runs` | GET | List runs with status/timing |
291
- | `/api/runs/<id>` | GET | Run status + final result |
292
- | `/api/runs/<id>/events` | GET | **SSE stream** — full event replay, then live, closes on finish |
293
- | `/api/runs/<id>/abort` | POST | Abort a running run (equivalent to SIGINT) |
294
-
295
- SSE event types: `run_start`, `step`, `llm_delta` (assistant text), `tool_call`, `tool_output` (live stdout/stderr chunks), `tool_result`, `llm_usage`, `build`, `finish`. The stream replays every event from the beginning, then stays live, and closes after `finish` — safe to (re)connect at any time.
296
-
297
- Notes:
298
-
299
- - Runs are always non-interactive; `permission: "ask"` is rejected — use `auto` or `readonly`.
300
- - Binds `127.0.0.1` by default. Set `SCODER_SERVER_TOKEN` to require `Authorization: Bearer <token>` on every request.
301
- - Max 4 concurrent runs (extra requests get `429`); per-run overrides: `workdir`, `buildCmd`, `model`, `provider`, `maxSteps`, `permission`.
302
-
303
- ## Logging
304
-
305
- Enable a structured JSONL event log with `--log-file` (or `SCODER_LOG_FILE=1`, or `SCODER_LOG_FILE=/path/to/log.jsonl`):
306
-
307
- ```bash
308
- scoder --log-file "fix the failing test"
309
- cat .scoder/logs/<session-id>.jsonl | jq 'select(.type == "error")'
310
- ```
311
-
312
- Events: `step` (with token usage), `tool` (args, ok, elapsedMs, denied reason), `build`, `finish`. One JSON object per line — pipe to `jq` or any log consumer.
313
-
314
- ## Reliability
315
-
316
- scoder is designed for unattended use. Key guarantees:
317
-
318
- - **Request timeouts** — LLM calls have a 120s timeout; on timeout, the agent retries or fails gracefully (never hangs indefinitely).
319
- - **SIGINT handling** — pressing Ctrl+C cancels the in-flight LLM request immediately, kills the running shell command (SIGTERM → SIGKILL), saves the session, and exits with status 130. You can resume with `scoder resume last`.
320
- - **Graceful retries** — transient LLM API failures (HTTP 429/5xx, network errors) are retried with exponential backoff and jitter (up to 3 retries, honoring `Retry-After`). Client errors (401/403/404) fail fast.
321
- - **Double-build prevention** — when the agent itself runs the build command, scoder reuses that result instead of running the build twice.
322
- - **Build diff preview** — run `scoder --permission ask` to see a unified diff before every file write/edit.
323
-
324
- > **Design note:** the build command (stop condition) always runs without a permission prompt, even in `readonly` mode — the agent must be able to verify its stop condition. Builds are invoked as-is from your config (`buildCmd`) or auto-detection, never invented by the LLM. Shell commands *chosen by the LLM* (`run_command` tool) go through the full permission policy above.
325
-
326
- ### Auto-detected build commands
327
-
328
- scoder detects the build command automatically:
329
-
330
- | Project type | Detected command |
331
- |---|---|
332
- | `package.json` with `scripts.build` | `npm run build` |
333
- | `Makefile` | `make` |
334
- | `go.mod` | `go build ./...` |
335
- | `Cargo.toml` | `cargo build` |
336
- | `tsconfig.json` | `npx tsc --noEmit` |
337
- | `pyproject.toml` | `python -m compileall .` |
338
-
339
- ## CLI Options
340
-
341
- ```
342
- scoder [options] "<task>" Run a task autonomously until the build passes
343
- scoder Start interactive session
344
- scoder init Setup wizard — writes .scoderrc.json (+ connection test)
345
- scoder tui [task] Full-screen terminal UI (Mission Control)
346
- scoder serve [--port] [--host] HTTP server mode (see above)
347
- scoder --help | --version Help and version
348
- ```
349
-
350
- | Flag | Description |
351
- |------|-------------|
352
- | `-m <model>` | Model to use |
353
- | `--base-url <url>` | OpenAI-compatible API base URL |
354
- | `-k <key>` | API key |
355
- | `-B, --build <cmd>` | Build command (stop condition) |
356
- | `-d, --dir <path>` | Working directory |
357
- | `-s, --max-steps <n>` | Max iterations (default 40) |
358
- | `--permission <mode>` | `auto` \| `ask` \| `readonly` shell-command policy (default `auto`) |
359
- | `-y` | Non-interactive mode (denies commands needing confirmation) |
360
- | `--provider <name>` | `openai` \| `anthropic` (env SCODER_PROVIDER; auto-detected) |
361
- | `--no-save` | Disable session auto-save to `.scoder/sessions/` |
362
- | `--log-file` | Write a structured JSONL event log to `.scoder/logs/` |
363
- | `--port <n>` | (`serve`) TCP port, `0` = random free port |
364
- | `--host <addr>` | (`serve`) Bind address (default `127.0.0.1`) |
365
-
366
- ## Examples
367
-
368
- ```bash
369
- # Run a task (auto-saves session to .scoder/sessions/)
370
- scoder -B "npm test" "fix the null pointer in UserService"
371
-
372
- # Resume after a crash or interrupt
373
- scoder resume last "now add unit tests for the fix"
374
-
375
- # Interactive mode with permission guard
376
- scoder --permission ask "npm install express and wire up a route"
377
-
378
- # Custom LLM (Groq, Ollama, vLLM, OpenRouter…)
379
- scoder --base-url https://api.groq.com/openai/v1 -k $GROQ_KEY \
380
- "add input validation to the CLI"
381
-
382
- # Anthropic (auto-detected when model starts with 'claude-')
383
- scoder -m claude-sonnet-4-5 "refactor the parser module"
384
-
385
- # CI: non-interactive, no session saving
386
- scoder --no-save -y "run the full migration"
387
-
388
- # Run as an HTTP service and consume progress via SSE
389
- scoder serve --port 4114 &
390
- curl -s -X POST localhost:4114/api/runs -H 'Content-Type: application/json' \
391
- -d '{"task": "add input validation to the CLI"}'
392
- curl -N http://127.0.0.1:4114/api/runs/<id>/events # live progress
393
-
394
- # Fetch external docs during a task (agent decides when, via web_fetch)
395
- scoder "upgrade express to v5 — check the official migration guide first"
396
- ```
397
-
398
- ## Architecture
399
-
400
- ```
401
- ┌─────────────────────────────────────────────┐
402
- │ CLI (src/index.ts) │
403
- │ arg parsing, REPL, serve, tui, SIGINT │
404
- └───────┬─────────────────────┬───────────────┘
405
- │ │
406
- ┌───────▼───────────┐ ┌──────▼─────────────────────┐
407
- │ TUI (src/tui/) │ │ Agent (src/agent.ts) │
408
- │ Ink/React full- │ │ Closed-loop orchestrator │
409
- │ screen chat UI, │─▶│ history trimming, error │
410
- │ renders the same │ │ injection │
411
- │ AgentEvent stream│ │ emits AgentEvent │
412
- └───────────────────┘ └──┬──────────┬──────────────┘
413
- │ │
414
- │ └────────────────────┐
415
- ▼ ▼
416
- ┌───────────────────────────────┐ ┌──────────────────────────┐
417
- │ LLM Client (src/llm.ts) │ │ Server (src/server.ts) │
418
- │ OpenAI-compatible + native │ │ HTTP API + SSE │
419
- │ Anthropic provider │ │ runs in background │
420
- └──────────────┬────────────────┘ └──────────────────────────┘
421
- ▼
422
- ┌────────────────────────────────────────┐
423
- │ Tool Registry (src/tools.ts) │
424
- │ + web_fetch (src/webfetch.ts) │
425
- └──────────────┬─────────────────────────┘
426
- ▼
427
- ┌───────────────────────────────┐
428
- │ Build Checker (src/build.ts) │
429
- │ run build → exit 0 = stop │
430
- └───────────────────────────────┘
431
- ```
432
-
433
- ## Integration Testing
434
-
435
- ```bash
436
- npm test # runs: build → unit tests (node --test) → e2e closed-loop → MCP smoke
437
- npm run lint # eslint flat config (typescript-eslint + eqeqeq, prefer-const...)
438
- npm run typecheck # tsc --noEmit
439
- ```
440
-
441
- Test suites (run with `npm test` — auto-discovers `test/*.test.mjs`):
442
- - `test/permissions.test.mjs` — catastrophic patterns, mode classification, allowlist word-boundary, timeout-denies
443
- - `test/permission-prompt.test.mjs` — interactive prompt timeout behavior
444
- - `test/tools.test.mjs` — safePath traversal, symlink escape, file-size cap, write/edit/list, live `run_command` output streaming
445
- - `test/webfetch.test.mjs` — HTML→text, JSON, redirects, SSRF guard, binary refusal, truncation
446
- - `test/server.test.mjs` — HTTP API: run lifecycle via SSE, abort, auth, live `tool_output` events
447
- - `test/tui.test.mjs` — full-screen TUI in a real PTY (via `test/pty-run.py`): closed loop render + slash commands (`/help`, `/cost`) + clean exit
448
- - `test/tui-state.test.mjs` — event reducer unit tests: diff items, build lamp, abort marking, slash-command parsing & help formatting
449
- - `test/core.test.mjs` — unified diff, retry backoff, context trimming
450
- - `test/config.test.mjs` — env/rc/cli precedence, logFile mapping, permission lists
451
- - `test/session.test.mjs` — session store, smart prune, resolve
452
- - `test/cost.test.mjs` — token usage & cost aggregation
453
- - `test/anthropic.test.mjs` — message translation + native SSE streaming
454
- - `test/llm.test.mjs` — OpenAI-compatible chat + SSE streaming + redaction + wire-format normalization
455
- - `test/run-e2e.mjs` — closed-loop integration (mock LLM server)
456
- - `test/run-mcp-test.mjs` — MCP smoke (mock stdio server)
457
-
458
- ## Exit Codes
459
-
460
- | Code | Meaning |
461
- |------|---------|
462
- | `0` | Task completed — build passed |
463
- | `1` | Build failed after max steps |
464
- | `2` | Fatal error (no API key, network error, etc.) |
465
-
466
- ## License
467
-
468
- MIT
1
+ # scoder — Autonomous Coding Agent CLI
2
+
3
+ [![CI](https://github.com/nbhson/app-scoder-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/nbhson/app-scoder-agent/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
6
+ [![npm](https://img.shields.io/badge/npm-s--app--scoder--agent-orange)](https://www.npmjs.com/package/s-app-scoder-agent)
7
+
8
+ **scoder** is a command-line coding agent that autonomously writes, edits, and builds code until your project compiles without errors. Inspired by *Cline*, *Claude Code*, and *OpenCode*, but runs as a single standalone CLI tool.
9
+
10
+ ## Installation
11
+
12
+ **Requirements:** Node.js ≥ 20
13
+
14
+ ### Install from npm (Recommended)
15
+
16
+ ```bash
17
+ npm install -g s-app-scoder-agent
18
+ ```
19
+
20
+ ### Install from GitHub
21
+
22
+ ```bash
23
+ npm install -g github:nbhson/app-scoder-agent
24
+ ```
25
+
26
+ ### From local clone
27
+
28
+ ```bash
29
+ git clone https://github.com/nbhson/app-scoder-agent.git
30
+ cd app-scoder-agent
31
+ npm install && npm run build && npm install -g .
32
+ ```
33
+
34
+ ### Try without installing
35
+
36
+ ```bash
37
+ npx s-app-scoder-agent "your task here"
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```bash
43
+ # One-time setup wizard — writes .scoderrc.json (base URL, key, model, build cmd)
44
+ scoder init
45
+
46
+ # …or set your API key manually
47
+ export SCODER_API_KEY=sk-... # any OpenAI-compatible key (OpenAI, Groq, OpenRouter, Ollama, …)
48
+ export OPENAI_BASE_URL=https://api.openai.com/v1 # optional, for custom endpoints
49
+
50
+ # Run a task
51
+ scoder "add a ping function to src/network.ts"
52
+
53
+ # Interactive REPL (supports /help, /clear, /cost, /model, /skills, /skill, /mcp, /review, /permission)
54
+ scoder
55
+ ```
56
+
57
+ ## How It Works
58
+
59
+ ```
60
+ You (task) → LLM plans → Tools execute code → Build runs → Success? → Done
61
+ ↑ |
62
+ └── Build failed? ─┘ (auto-loop)
63
+ ```
64
+
65
+ **Key constraint:** scoder runs autonomously without stopping for confirmation prompts. If the build fails, it reads the error, patches the code, and retries automatically — until the build passes or the max-step limit is reached.
66
+
67
+ ## Tool Set
68
+
69
+ | Tool | Description |
70
+ |------|-------------|
71
+ | `read_file` | Read a file with line numbers |
72
+ | `write_file` | Overwrite/create a file |
73
+ | `edit_file` | Find-and-replace (unique match required) |
74
+ | `list_files` | Recursive directory listing |
75
+ | `run_command` | Execute any shell command (subject to permission policy) |
76
+ | `web_fetch` | Fetch a public HTTP(S) URL → readable text (HTML→text, JSON pretty-printed) |
77
+ | `write_todos` | Live plan for multi-step tasks (checkbox list, kept current) |
78
+ | `load_skill` | Load a SKILL.md into context when the task matches its description |
79
+ | `mcp__<server>__<tool>` | Tools from configured MCP servers (routed transparently) |
80
+
81
+ File writes (`write_file` / `edit_file`) are symlink-safe: paths are resolved via `realpath` and cannot escape the working directory, even through a malicious symlink. `read_file` refuses files larger than 2MB with guidance to use `grep`/`head`/`tail`.
82
+
83
+ ### Web access (`web_fetch`)
84
+
85
+ The agent can read the internet through the built-in `web_fetch` tool — it fetches exact URLs (it cannot search) and returns readable plain text: HTML is converted to text (scripts/styles stripped, entities decoded, title extracted), JSON is pretty-printed, binary content is refused. The system prompt instructs the agent *when to fetch* (external/API docs, URLs in error messages, current versions, changelogs) and when not to (answer available locally).
86
+
87
+ Safety defaults:
88
+
89
+ - **SSRF protection** — `localhost`, private and link-local addresses (10.x, 172.16/12, 192.168.x, 127.x, 169.254.x, IPv6 ULA/link-local) are blocked. Set `SCODER_WEB_ALLOW_PRIVATE=1` to allow local dev servers.
90
+ - Only `http`/`https`; up to 5 redirects — every hop is re-validated against the private-address block list.
91
+ - 30 s timeout, SIGINT-abortable; body capped at 512 KB; extracted text capped at 20k chars (configurable via `max_chars`).
92
+ - HTML is converted to plain text (no JS execution); images/archives/PDFs are refused.
93
+ - Read-only — the tool never writes locally, so it is allowed even in `readonly` permission mode.
94
+
95
+ ## Configuration
96
+
97
+ ### Environment variables
98
+
99
+ | Variable | Default | Description |
100
+ |----------|---------|-------------|
101
+ | `SCODER_API_KEY` | — | API key (required) |
102
+ | `SCODER_MODEL` | `gpt-4o-mini` | Model name |
103
+ | `SCODER_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint |
104
+ | `SCODER_BUILD_CMD` | auto-detected | Build/compile command |
105
+ | `SCODER_MAX_STEPS` | `40` | Max loop iterations before giving up |
106
+ | `SCODER_PERMISSION` | `auto` | Permission mode: `auto` \| `ask` \| `readonly` |
107
+ | `SCODER_STREAM` | `1` | `0` to disable streaming LLM output |
108
+ | `SCODER_STREAM` | `1` | `0` to disable streaming LLM output |
109
+ | `SCODER_SERVER_TOKEN` | — | `scoder serve` only: require `Authorization: Bearer <token>` |
110
+ | `SCODER_WEB_ALLOW_PRIVATE` | — | Set to `1` to let `web_fetch` reach localhost/private addresses (local dev) |
111
+ | `SCODER_NO_APIKEY_WARNING` | — | Set to `1` to suppress the apiKey-in-rc warning |
112
+
113
+ ### Per-project config file (.scoderrc.json)
114
+
115
+ For any OpenAI-compatible endpoint, just point `baseUrl`, `apiKey`, and `model` at it — no code changes needed. Create `.scoderrc.json` in your project root (see `.scoderrc.example.json`):
116
+
117
+ ```json
118
+ {
119
+ "baseUrl": "https://api.groq.com/openai/v1",
120
+ "apiKey": "gsk_...",
121
+ "model": "llama-3.3-70b-versatile"
122
+ }
123
+ ```
124
+
125
+ Then simply run `scoder "your task"` — values are picked up automatically.
126
+
127
+ ### Global config (~/.scoder/config.json)
128
+
129
+ Machine-wide defaults shared by **all** projects: `mcpServers`, `model`, `baseUrl`, `permission`, `permissions` lists, `modelPricing`… The project `.scoderrc.json` wins field-by-field; `mcpServers` and lists merge per-key (project entries override same-named global ones). API keys are safe here — the file lives in your home dir, outside any repo.
130
+
131
+ #### All fields
132
+
133
+ | Field | Default | Description |
134
+ |---|---|---|
135
+ | `baseUrl` | `https://api.openai.com/v1` | OpenAI-compatible endpoint (must support `/chat/completions` with tool calling) |
136
+ | `apiKey` | — | API key (⚠️ prefer `SCODER_API_KEY` env var, see warning below) |
137
+ | `model` | `gpt-4o-mini` | Model name served by the endpoint |
138
+ | `buildCmd` | auto-detected | Build/compile command (stop condition) |
139
+ | `maxSteps` | `40` | Max autonomous loop iterations |
140
+ | `stream` | `true` | Stream LLM output token-by-token |
141
+ | `provider` | auto-detected | `openai` \| `anthropic` |
142
+ | `permission` | `auto` | `auto` \| `ask` \| `readonly` |
143
+ | `mcpServers` | — | MCP servers — stdio (`command`) hoặc HTTP (`url`) (see below) |
144
+ | `permissions` | — | `{ mode, allowCommands, denyCommands }` |
145
+
146
+ #### Precedence
147
+
148
+ Values are merged from highest to lowest priority — your `.scoderrc.json` value only applies when no flag/env is set:
149
+
150
+ 1. **CLI flags** — `-m`, `--base-url`, `-k`, `--provider`, `-B`
151
+ 2. **Environment variables** — `SCODER_API_KEY` / `OPENAI_API_KEY`, `SCODER_BASE_URL`, `SCODER_MODEL`, …
152
+ 3. **`.scoderrc.json`** (or `.scoderrc`) in the project root, merged over the global `~/.scoder/config.json` — project values win field-by-field; `mcpServers`/lists/pricing merge per-key
153
+ 4. Built-in defaults
154
+
155
+ > 💡 If your config in `.scoderrc.json` seems ignored, check `echo $SCODER_API_KEY` — env vars override the file.
156
+
157
+ #### Examples for common providers
158
+
159
+ ```jsonc
160
+ // Local Ollama
161
+ {
162
+ "baseUrl": "http://localhost:11434/v1",
163
+ "apiKey": "ollama",
164
+ "model": "qwen2.5-scoder:32b"
165
+ }
166
+ ```
167
+
168
+ ```jsonc
169
+ // DeepSeek
170
+ {
171
+ "baseUrl": "https://api.deepseek.com/v1",
172
+ "apiKey": "sk-...",
173
+ "model": "deepseek-chat"
174
+ }
175
+ ```
176
+
177
+ #### Notes
178
+
179
+ - Models starting with `claude-`, or a `baseUrl` containing `api.anthropic.com`, automatically switch to the native Anthropic provider — no `provider` field needed.
180
+ - If an `apiKey` is present in the rc file, scoder prints a warning on every run. Silence it with `SCODER_NO_APIKEY_WARNING=1`.
181
+
182
+ > ⚠️ **Security:** never commit `apiKey` in `.scoderrc.json` — add the file to `.gitignore` and prefer `SCODER_API_KEY` env var or the `-k` flag. A pre-commit check is available at `scripts/check-secrets.mjs`.
183
+
184
+ ## Permissions & Safety
185
+
186
+ scoder ships with a layered permission system for `run_command`:
187
+
188
+ | Mode | Behavior |
189
+ |------|----------|
190
+ | `auto` (default) | Autonomous — runs shell commands without confirmation (original behavior) |
191
+ | `ask` | Prompts before each shell command not covered by `allowCommands` |
192
+ | `readonly` | Shell commands are disabled entirely; the agent can only read/list files. MCP tools are also disabled (they bypass the permission policy) |
193
+
194
+ Layered checks, in order:
195
+
196
+ 1. **Catastrophic deny list** — `rm -rf /`, `dd of=/dev/...`, fork bombs, `curl | sh`, etc. are **always denied**, in every mode.
197
+ 2. **`denyCommands`** (config) — always denied.
198
+ 3. **Session allowlist** — commands you approved with `[a] always allow` in this session.
199
+ 4. **`allowCommands`** (config) — auto-approved.
200
+ 5. **Mode** — `auto` runs everything else; `ask` prompts; `readonly` denies.
201
+
202
+ In `ask` mode the prompt offers `[y] yes / [a] always allow "<prefix>" / [n] no / [q] quit`. With `-y` (non-interactive), commands needing confirmation are denied instead of hanging CI.
203
+
204
+ ## Skills (SKILL.md)
205
+
206
+ scoder can load reusable instruction packs ("skills") into the conversation. A skill is a directory with a `SKILL.md` (YAML-lite frontmatter + markdown body):
207
+
208
+ ```
209
+ .scoder/skills/code-review/SKILL.md # project-level
210
+ ~/.scoder/skills/code-review/SKILL.md # global (project overrides on name clash)
211
+ ```
212
+
213
+ ```markdown
214
+ ---
215
+ name: code-review
216
+ description: Review code for bugs, security issues, and style
217
+ allowed-tools: read_file, grep
218
+ license: MIT
219
+ when-to-use: Run after any multi-file change before committing
220
+ ---
221
+ Read the current diff. Check for: null derefs, injection risks, ...
222
+ ```
223
+
224
+ - **Advertised to the model** — every run's system prompt lists skill names + descriptions; the agent reads a matching `SKILL.md` itself via `read_file` when a task fits
225
+ - **Explicit load** — `/skill <name>` stages the skill's content; it is injected as context before your next task
226
+ - **`/skills`** lists everything discovered (both scopes)
227
+ - **Metadata (Anthropic convention)** — `description` is required (a skill without one is skipped with a warning); optional `allowed-tools` (comma-separated tool list, informational — enforcement goes through permission rules), `license`, `when-to-use`
228
+ - **Validation warnings** — duplicate names across scopes warn (project wins); missing `description` skips the skill; warnings surface in the log/notice stream
229
+ - **Hot-reload** — `.scoderrc` and `.scoder/skills/` are watched (debounced); a skill added mid-session is advertised on the next turn, no restart needed
230
+
231
+ ## Slash commands (REPL + TUI)
232
+
233
+ | Command | Description |
234
+ |---|---|
235
+ | `/help` | command list |
236
+ | `/clear` | reset conversation + agent context |
237
+ | `/cost` | session token usage |
238
+ | `/model` | active model/provider/build |
239
+ | `/skills` | list discovered skills |
240
+ | `/skill <name>` | stage a skill for the next task |
241
+ | `/mcp` | MCP servers + tools; exec one directly: `/mcp <server> <tool> [json\|text]` or `/mcp mcp__server__tool [json\|text]` — non-JSON text binds to the tool's first string parameter |
242
+ | `/review <prompt>` | Prompt gate (prompt-gate MCP): LLM rewrites your prompt, you pick original/rewritten/update/cancel — only the approved prompt is used |
243
+ | `/permission <auto\|readonly>` | switch permission mode at runtime |
244
+ | `/exit` | quit |
245
+
246
+ In the TUI, **Tab** completes commands (and `/skill` names, `/mcp <server> <tool>` arguments), **↑/↓** recall sent-message history.
247
+
248
+ ### @file mentions
249
+
250
+ Prefix any path with `@` in your task to inline its content into the agent's context:
251
+
252
+ ```bash
253
+ scoder "fix the bug in @src/agent.ts"
254
+ scoder "refactor the handlers in @src/handlers/ and update @README.md"
255
+ ```
256
+
257
+ - `@src/file.ts` — file content is injected into the task; `@"path with spaces.ts"` for paths containing spaces
258
+ - `@src/dir/` — directory listing injected instead
259
+ - Emails (`bob@example.com`) are never treated as mentions; missing paths produce a visible note the LLM can react to; paths are contained to the working directory
260
+
261
+ ## Planning tool
262
+
263
+ For multi-step tasks the agent writes a live plan with `write_todos` (`[x]` completed · `[~]` in progress · `[ ]` pending) — rendered inline in the TUI and included in session state.
264
+
265
+ ## Guardrails (Guardrail Engine)
266
+
267
+ The autonomous loop is wrapped by a configurable rule engine (`src/guardrails.ts`) — every rule is a pure function over the step context that returns a decision (`inject` a reminder / `finish` / `verify-build` / `trim`), and every triggered decision is logged to the JSONL log (`type: "guardrail"`) and surfaced as an `AgentEvent` for frontends:
268
+
269
+ | Rule | What it does |
270
+ |---|---|
271
+ | `wrap-up` | Injects a converge-now reminder 2 steps before the budget ends |
272
+ | `chat-done` | Ends conversational turns (marker-based) without a build |
273
+ | `completion-claim` | Never trusts "the task is complete" — forces an independent build verification |
274
+ | `text-only-streak` | Ends the run after 2 consecutive tool-less steps with no file changes (the "chào" failure mode) |
275
+ | `continue-nudge` | Nudges plain-text answers back to work (no build demand when no files changed — suggests the `<<CHAT_DONE>>` exit instead) |
276
+ | `doom-loop` | Intervenes after 3× identical consecutive tool-call batches |
277
+ | `build-stop` | Ends the run when the exact build command succeeds; re-verifies on failure |
278
+ | `history-trim` | Char-budget history trimming with whole-turn drops + old-tool-result pruning |
279
+
280
+ Thresholds are configurable via `GuardrailOptions` (`textOnlyStreakLimit`, `doomLoopRepeats`, `wrapUpStepsBeforeEnd`) without touching the loop, and custom rules can be added via `GuardrailEngine.register()`.
281
+
282
+ ## Usage & cost metrics
283
+
284
+ `scoder cost` reads the JSONL logs and reports per session and total: LLM calls, tokens (in/out/cached), estimated cost, **steps, average LLM latency, tool calls, guardrail interventions, and the session success rate**:
285
+
286
+ ```
287
+ 20261031-110353-d034 calls=1 in=4,042 out=88 cost≈$0.0022 | steps=1 llm-avg=7.5s tools=1 guardrails=1 ✓success
288
+ TOTAL (1 session(s)) ... | success-rate=1/1
289
+ ```
290
+
291
+ The system prompt is versioned (`PROMPT_VERSION` in `src/prompt.ts`) and stamped into every session log (`type: "prompt_version"`) so prompt quality can be compared across versions.
292
+
293
+ ## MCP Servers (stdio / HTTP)
294
+
295
+ scoder can use tools from [MCP](https://modelcontextprotocol.io) servers configured in `.scoderrc.json`:
296
+
297
+ ```json
298
+ {
299
+ "mcpServers": {
300
+ "filesystem": {
301
+ "command": "npx",
302
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
303
+ },
304
+ "github": {
305
+ "command": "npx",
306
+ "args": ["-y", "@modelcontextprotocol/server-github"],
307
+ "env": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" }
308
+ },
309
+ "remote": {
310
+ "url": "https://mcp.example.com/mcp",
311
+ "headers": { "authorization": "Bearer {env:MCP_TOKEN}" }
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ - Tools are exposed to the LLM as `mcp__<server>__<tool>` (no name collisions with built-ins)
318
+ - Servers connect at agent start; a failing server only logs a warning — the agent keeps running with its built-in tools
319
+ - `{env:VAR}` substitution keeps secrets out of config files (both `env` and `headers`)
320
+ - **Transports**: `command` → stdio (line-delimited JSON-RPC); `url` → Streamable HTTP (POST JSON-RPC, JSON or SSE responses, `Mcp-Session-Id` echo). Force with `"type": "stdio" | "http"`
321
+ - **Resilience**: a dead stdio server reconnects with exponential backoff (max 5 attempts) and refreshes its tool list; HTTP calls are stateless and retry on the next call
322
+ - Built-in tools always take priority over MCP tools with colliding names
323
+ - ⚠️ **MCP tools bypass the built-in permission system** (no command classification, no diff preview). In `readonly` mode they are therefore disabled entirely; in `auto`/`ask` mode they run unsandboxed — only add MCP servers you trust.
324
+
325
+ ### Prompt gate (`/review`)
326
+
327
+ If the [`prompt-gate`](https://github.com/nbhson/s-universal-prompt-gate-mcp) MCP server is configured (`~/.scoder/config.json`), `/review <prompt>` runs the gated rewrite flow — like OpenCode's `review-and-select-prompt`:
328
+
329
+ ```
330
+ /review phân tích project
331
+ → [1] light [2] balanced [3] detailed ← you pick the rewrite level
332
+ → ORIGINAL vs REWRITTEN shown side by side
333
+ → [1] original [2] rewritten [3] update <changes> [4] cancel
334
+ → ✔ prompt approved: <the exact text you approved>
335
+ ```
336
+
337
+ - **You approve exactly what you read** — the approved text is echoed verbatim from the server's pending result, never re-typed by the model or you
338
+ - `[3] update` loops: describe changes, the prompt is rewritten again, review repeats until you pick original/rewritten/cancel
339
+ - Cancel approves nothing; LLM errors fail loudly (no silent fallback to the original)
340
+
341
+ ## Sessions
342
+
343
+ Every run auto-saves its state (messages, steps, build result) to `.scoder/sessions/<id>.json` after each step. If the run is interrupted (Ctrl+C, crash, max steps), you can continue where it left off:
344
+
345
+ ```bash
346
+ scoder list # list saved sessions
347
+ scoder resume last # continue the most recent session
348
+ scoder resume <id> "follow-up" # resume + add a new instruction
349
+ ```
350
+
351
+ Sessions older than 7 days are pruned automatically on the next run. Use `--no-save` to disable (e.g. in CI).
352
+
353
+ ## Streaming
354
+
355
+ scoder streams everything it can, live, as it happens:
356
+
357
+ - **LLM tokens** — assistant text renders character-by-character (OpenAI-compatible SSE and native Anthropic streaming). If a provider doesn't support streaming, scoder automatically falls back to non-streaming requests. Disable with `SCODER_STREAM=0`.
358
+ - **Tool output** — `run_command` and build output print live (dimmed block) while the command runs, instead of appearing only after it finishes. Live output is capped at 20k chars; the full output still goes to the LLM.
359
+ - **SIGINT-safe** — Ctrl+C kills the running command immediately; streamed rendering never blocks the event loop.
360
+ - Output honors `NO_COLOR` and degrades to plain text when piped (CI-friendly).
361
+
362
+ ## Full-Screen TUI (`scoder tui`)
363
+
364
+ **Mission Control for the build loop** — a terminal UI built with [Ink](https://github.com/vadimdemedes/ink) (React for terminals), driven by the same `AgentEvent` stream the HTTP API exposes:
365
+
366
+ ```bash
367
+ scoder tui # start an interactive full-screen session
368
+ scoder tui "fix the bug" # auto-submit an initial task
369
+ ```
370
+
371
+ Features:
372
+
373
+ - **Alternate screen buffer** (vim/htop style) — the app owns the full screen: the header stays pinned at the top, input + status bar at the bottom, the chat pane scrolls internally, and your terminal content is restored on exit. Terminals without alt-screen support (`TERM=dumb`, non-TTY) automatically render inline instead.
374
+ - **Build Rail** (pinned top bar, single row) — brand, build lamp (`✓ ok` / `✗ fail` / `no build yet`), permission mode, and a center region that shows either the phase breadcrumb (`plan › explore › edit › build › verify`) or the latest guardrail notice spanning the full available width (e.g. `⚠ trả lời text chưa dùng tool — nhắc tiếp tục`)
375
+ - **Sidebar (≥92 cols)** — SESSION (model + workdir, no permission line), METRICS (step fuel gauge, tokens, tok/s, ctx input/limit, cost, elapsed, guardrail count), PHASES, BUILD, MCP, PLAN (live checklist at the bottom; resets on each new user message)
376
+ - **Chat pane** — your messages with a full colored border + `you ▸` label, streaming assistant text with a live block cursor (`▋`), and every tool call with live stdout/stderr preview (`✔/✘` status, spinner while running, wall-clock duration per tool), prefixed with a flight-recorder step number (`03▸`)
377
+ - **Header row 2** — (no longer present; phase breadcrumb + metrics moved into the sidebar / status bar to avoid duplication)
378
+ - **Diff cards** (`✎`) — every file write/edit renders as a unified diff (`+` green / `-` red / `@@` cyan hunk headers); the same diff also flows out of the HTTP SSE stream
379
+ - **Status bar** — model, token usage (input/output/cached), scroll position, keybind hints
380
+ - **Slash commands** — `/help`, `/clear`, `/cost`, `/model`, `/skills`, `/skill`, `/mcp`, `/review`, `/permission`, `/exit` (identical to the REPL)
381
+ - **Multi-turn** — keep typing follow-ups after a task finishes; the conversation context carries over (same as the REPL)
382
+ - **Scrollback** — `PgUp`/`PgDn` page in 0.8-viewport steps, `Home`/`End` jump to top/bottom, `Esc` returns to the live tail (press again to abort); rendering is capped at the last 200 items for smooth scrolling on long runs
383
+ - **Keybinds** — `Enter` send · `PgUp`/`PgDn` scroll · `Esc` reset scroll / abort · `Ctrl+C` quit
384
+ - Non-TTY environments (CI, pipes) automatically fall back to the classic REPL
385
+ - `permission: "ask"` is not supported in the TUI (no readline prompts on the alt screen) — it falls back to `auto`
386
+
387
+ ## HTTP Server Mode (`scoder serve`)
388
+
389
+ Run scoder as a local HTTP service — submit tasks programmatically and consume progress as Server-Sent Events:
390
+
391
+ ```bash
392
+ scoder serve # http://127.0.0.1:0 (random port, printed at startup)
393
+ scoder serve --port 4114 # fixed port
394
+ SCODER_SERVER_TOKEN=secret scoder serve --host 127.0.0.1
395
+ ```
396
+
397
+ | Endpoint | Method | Description |
398
+ |----------|--------|-------------|
399
+ | `/api/health` | GET | Liveness: `{ ok, running, runs }` |
400
+ | `/api/runs` | POST | Start a run: `{ "task": "...", "workdir?", "buildCmd?", "model?", "provider?", "maxSteps?", "permission?" }` → `202 { id, eventsUrl }` |
401
+ | `/api/runs` | GET | List runs with status/timing |
402
+ | `/api/runs/<id>` | GET | Run status + final result |
403
+ | `/api/runs/<id>/events` | GET | **SSE stream** — full event replay, then live, closes on finish |
404
+ | `/api/runs/<id>/abort` | POST | Abort a running run (equivalent to SIGINT) |
405
+
406
+ SSE event types: `run_start`, `step`, `llm_delta` (assistant text), `tool_call`, `tool_output` (live stdout/stderr chunks), `tool_result`, `llm_usage`, `build`, `finish`. The stream replays every event from the beginning, then stays live, and closes after `finish` — safe to (re)connect at any time.
407
+
408
+ Notes:
409
+
410
+ - Runs are always non-interactive; `permission: "ask"` is rejected — use `auto` or `readonly`.
411
+ - Binds `127.0.0.1` by default. Set `SCODER_SERVER_TOKEN` to require `Authorization: Bearer <token>` on every request.
412
+ - Max 4 concurrent runs (extra requests get `429`); per-run overrides: `workdir`, `buildCmd`, `model`, `provider`, `maxSteps`, `permission`.
413
+
414
+ ## Logging
415
+
416
+ Enable a structured JSONL event log with `--log-file` (or `SCODER_LOG_FILE=1`, or `SCODER_LOG_FILE=/path/to/log.jsonl`):
417
+
418
+ ```bash
419
+ scoder --log-file "fix the failing test"
420
+ cat .scoder/logs/<session-id>.jsonl | jq 'select(.type == "error")'
421
+ ```
422
+
423
+ Events: `step` (with `durationMs` LLM latency + tool-call count), `tool` (args, ok, elapsedMs, denied reason), `build`, `guardrail` (rule/action/reason for every Guardrail Engine decision), `prompt_version`, `finish`. One JSON object per line — pipe to `jq` or any log consumer.
424
+
425
+ ## Reliability
426
+
427
+ scoder is designed for unattended use. Key guarantees:
428
+
429
+ - **Request timeouts** — LLM calls have a 120s timeout; on timeout, the agent retries or fails gracefully (never hangs indefinitely).
430
+ - **SIGINT handling** — pressing Ctrl+C cancels the in-flight LLM request immediately, kills the running shell command (SIGTERM → SIGKILL), saves the session, and exits with status 130. You can resume with `scoder resume last`.
431
+ - **Graceful retries** — transient LLM API failures (HTTP 429/5xx, network errors) are retried with exponential backoff and jitter (up to 3 retries, honoring `Retry-After`). Client errors (401/403/404) fail fast.
432
+ - **Double-build prevention** — when the agent itself runs the build command, scoder reuses that result instead of running the build twice.
433
+ - **Build diff preview** — run `scoder --permission ask` to see a unified diff before every file write/edit.
434
+
435
+ > **Design note:** the build command (stop condition) always runs without a permission prompt, even in `readonly` mode — the agent must be able to verify its stop condition. Builds are invoked as-is from your config (`buildCmd`) or auto-detection, never invented by the LLM. Shell commands *chosen by the LLM* (`run_command` tool) go through the full permission policy above.
436
+
437
+ ### Auto-detected build commands
438
+
439
+ scoder detects the build command automatically:
440
+
441
+ | Project type | Detected command |
442
+ |---|---|
443
+ | `package.json` with `scripts.build` | `npm run build` |
444
+ | `Makefile` | `make` |
445
+ | `go.mod` | `go build ./...` |
446
+ | `Cargo.toml` | `cargo build` |
447
+ | `tsconfig.json` | `npx tsc --noEmit` |
448
+ | `pyproject.toml` | `python -m compileall .` |
449
+
450
+ ## CLI Options
451
+
452
+ ```
453
+ scoder [options] "<task>" Run a task autonomously until the build passes
454
+ scoder Start interactive session
455
+ scoder init Setup wizard — writes .scoderrc.json (+ connection test)
456
+ scoder tui [task] Full-screen terminal UI (Mission Control)
457
+ scoder serve [--port] [--host] HTTP server mode (see above)
458
+ scoder --help | --version Help and version
459
+ ```
460
+
461
+ | Flag | Description |
462
+ |------|-------------|
463
+ | `-m <model>` | Model to use |
464
+ | `--base-url <url>` | OpenAI-compatible API base URL |
465
+ | `-k <key>` | API key |
466
+ | `-B, --build <cmd>` | Build command (stop condition) |
467
+ | `-d, --dir <path>` | Working directory |
468
+ | `-s, --max-steps <n>` | Max iterations (default 40) |
469
+ | `--permission <mode>` | `auto` \| `ask` \| `readonly` shell-command policy (default `auto`) |
470
+ | `-y` | Non-interactive mode (denies commands needing confirmation) |
471
+ | `--provider <name>` | `openai` \| `anthropic` (env SCODER_PROVIDER; auto-detected) |
472
+ | `--no-save` | Disable session auto-save to `.scoder/sessions/` |
473
+ | `--log-file` | Write a structured JSONL event log to `.scoder/logs/` |
474
+ | `--port <n>` | (`serve`) TCP port, `0` = random free port |
475
+ | `--host <addr>` | (`serve`) Bind address (default `127.0.0.1`) |
476
+
477
+ ## Examples
478
+
479
+ ```bash
480
+ # Run a task (auto-saves session to .scoder/sessions/)
481
+ scoder -B "npm test" "fix the null pointer in UserService"
482
+
483
+ # Resume after a crash or interrupt
484
+ scoder resume last "now add unit tests for the fix"
485
+
486
+ # Interactive mode with permission guard
487
+ scoder --permission ask "npm install express and wire up a route"
488
+
489
+ # Custom LLM (Groq, Ollama, vLLM, OpenRouter…)
490
+ scoder --base-url https://api.groq.com/openai/v1 -k $GROQ_KEY \
491
+ "add input validation to the CLI"
492
+
493
+ # Anthropic (auto-detected when model starts with 'claude-')
494
+ scoder -m claude-sonnet-4-5 "refactor the parser module"
495
+
496
+ # CI: non-interactive, no session saving
497
+ scoder --no-save -y "run the full migration"
498
+
499
+ # Run as an HTTP service and consume progress via SSE
500
+ scoder serve --port 4114 &
501
+ curl -s -X POST localhost:4114/api/runs -H 'Content-Type: application/json' \
502
+ -d '{"task": "add input validation to the CLI"}'
503
+ curl -N http://127.0.0.1:4114/api/runs/<id>/events # live progress
504
+
505
+ # Fetch external docs during a task (agent decides when, via web_fetch)
506
+ scoder "upgrade express to v5 — check the official migration guide first"
507
+ ```
508
+
509
+ ## Architecture
510
+
511
+ ```
512
+ ┌─────────────────────────────────────────────┐
513
+ │ CLI (src/index.ts) │
514
+ │ arg parsing, REPL, serve, tui, SIGINT │
515
+ └───────┬─────────────────────┬───────────────┘
516
+ │ │
517
+ ┌───────▼───────────┐ ┌──────▼─────────────────────┐
518
+ │ TUI (src/tui/) │ │ Agent (src/agent.ts) │
519
+ │ Ink/React alt- │ │ Closed-loop orchestrator │
520
+ │ screen UI, │─▶│ + Guardrail Engine │
521
+ │ renders the same │ │ (src/guardrails.ts) │
522
+ │ AgentEvent stream│ │ emits AgentEvent │
523
+ └───────────────────┘ └──┬──────────┬──────────────┘
524
+ │ │
525
+ │ └────────────────────┐
526
+ ▼ ▼
527
+ ┌───────────────────────────────┐ ┌──────────────────────────┐
528
+ │ LLM Client (src/llm.ts) │ │ Server (src/server.ts) │
529
+ │ OpenAI-compatible + native │ │ HTTP API + SSE │
530
+ │ Anthropic provider │ │ runs in background │
531
+ └──────────────┬────────────────┘ └──────────────────────────┘
532
+ ▼
533
+ ┌────────────────────────────────────────┐
534
+ │ Tool Registry (src/tools.ts) │
535
+ │ + web_fetch (src/webfetch.ts) │
536
+ └──────────────┬─────────────────────────┘
537
+ ▼
538
+ ┌───────────────────────────────┐
539
+ │ Build Checker (src/build.ts) │
540
+ │ run build → exit 0 = stop │
541
+ └───────────────────────────────┘
542
+ ```
543
+
544
+ ## Integration Testing
545
+
546
+ ```bash
547
+ npm test # runs: build → unit tests (node --test) → e2e closed-loop → MCP smoke
548
+ npm run lint # eslint flat config (typescript-eslint + eqeqeq, prefer-const...)
549
+ npm run typecheck # tsc --noEmit
550
+ ```
551
+
552
+ Test suites (run with `npm test` — auto-discovers `test/*.test.mjs`):
553
+ - `test/permissions.test.mjs` — catastrophic patterns, mode classification, allowlist word-boundary, timeout-denies
554
+ - `test/permission-prompt.test.mjs` — interactive prompt timeout behavior
555
+ - `test/tools.test.mjs` — safePath traversal, symlink escape, file-size cap, write/edit/list, live `run_command` output streaming
556
+ - `test/webfetch.test.mjs` — HTML→text, JSON, redirects, SSRF guard, binary refusal, truncation
557
+ - `test/server.test.mjs` — HTTP API: run lifecycle via SSE, abort, auth, live `tool_output` events
558
+ - `test/tui.test.mjs` — full-screen TUI in a real PTY (via `test/pty-run.py`): closed loop render + slash commands (`/help`, `/cost`) + clean exit
559
+ - `test/tui-state.test.mjs` — event reducer unit tests: diff items, build lamp, abort marking, slash-command parsing & help formatting
560
+ - `test/review-flow.test.mjs` — `/review` prompt-gate wiring: both pending stages, approved/cancelled outcomes, verbatim `rewritten_prompt` echo (security)
561
+ - `test/core.test.mjs` — unified diff, retry backoff, context trimming, @file mention expansion
562
+ - `test/config.test.mjs` — env/rc/cli precedence, logFile mapping, permission lists
563
+ - `test/session.test.mjs` — session store, smart prune, resolve
564
+ - `test/cost.test.mjs` — token usage & cost aggregation, session metrics (latency/success/guardrails)
565
+ - `test/guardrails.test.mjs` — Guardrail Engine: rule registry, default rules, configurable thresholds, decision logging, priority order
566
+ - `test/prompt.test.mjs` — system prompt versioning (`PROMPT_VERSION`) + dynamic assembly
567
+ - `test/anthropic.test.mjs` — message translation + native SSE streaming
568
+ - `test/llm.test.mjs` — OpenAI-compatible chat + SSE streaming + redaction + wire-format normalization
569
+ - `test/run-e2e.mjs` — closed-loop integration (mock LLM server)
570
+ - `test/run-mcp-test.mjs` — MCP smoke (mock stdio server)
571
+
572
+ ## Exit Codes
573
+
574
+ | Code | Meaning |
575
+ |------|---------|
576
+ | `0` | Task completed — build passed |
577
+ | `1` | Build failed after max steps |
578
+ | `2` | Fatal error (no API key, network error, etc.) |
579
+
580
+ ## Documentation
581
+
582
+ See [docs/](docs/) for detailed guides:
583
+
584
+ | Doc | Description |
585
+ |-----|-------------|
586
+ | [Installation](docs/installation.md) | Install from npm, GitHub, or local clone |
587
+ | [Configuration](docs/configuration.md) | Environment variables, config file, permissions |
588
+ | [Commands](docs/commands.md) | CLI commands & flags reference |
589
+ | [Architecture](docs/architecture.md) | System design & module overview |
590
+ | [TUI Guide](docs/tui.md) | Full-screen terminal UI |
591
+ | [Guardrails](docs/guardrails.md) | Safety & progress engine |
592
+ | [MCP Integration](docs/mcp.md) | Model Context Protocol servers |
593
+ | [HTTP API](docs/api.md) | Server mode & SSE API |
594
+ | [Sessions](docs/sessions.md) | Session persistence & resume |
595
+ | [Skills](docs/skills.md) | SKILL.md instruction packs |
596
+ | [Testing](docs/testing.md) | Test suites & coverage |
597
+
598
+ ## License
599
+
600
+ MIT