# jcode A minimal, single-file coding agent (~1,200 lines of Python) for local LLMs — with optional cloud fallback. jcode connects to a local **LM Studio** or **Ollama** server (or, via `settings.json`, to **OpenAI**, **Anthropic**, or **Gemini**), takes a task in plain language, and works autonomously inside a sandboxed `./code` directory: it reads, writes, and edits files, runs commands, searches the web, looks at images/screenshots/PDF pages, writes its own smoke tests, and keeps fixing the code until the tests pass. ``` _ _ (_) ___ ___ __| | ___ | |/ __/ _ \ / _ | / _ \ | | (_| (_) | (_| || __/ _/ |\___\___/ \____| \___| |__/ ``` ## Features - **Autonomous test-fix loop** — the agent writes a test for every change, runs it, analyzes failures, fixes the code, and repeats until green (up to `MAX_STEPS`, default 200). - **Sandboxed** — the file tools read and write only inside `./code`; the `skills/` folder next to it is readable but read-only. Every path is resolved first (`..`, `~` and symlinks included) and then checked, so `../settings.json`, `/etc/passwd` or a symlink pointing outside are refused with a clear error, and an absolute path into `./code` is used as is instead of being re-rooted. `search_files` rejects globs with `..` and skips symlinks that leave the project. `run_command` is not path-checked (a shell can reach anything your user can); it starts in `./code`. Destructive shell commands (`rm -rf /`, `sudo`, `dd`, `mkfs`, shutdown, fork bombs, `curl | sh`, …) are refused. - **Zero-config local backends** — auto-detects LM Studio (port 1234) first, then Ollama (port 11434). - **Cloud providers via `settings.json`** — switch to OpenAI, Anthropic, or Gemini at runtime with `/backend `; all are used through their OpenAI-compatible endpoints, so the single `openai` client dependency covers everything. - **Project guidelines via `AGENTS.md`** — project-specific rules (coding style, per-language test conventions) are loaded from `code/AGENTS.md` and injected as binding instructions. - **Skills** — reusable instruction packs in `skills//SKILL.md` (Claude-Code-compatible YAML frontmatter + Markdown, with links to further files in the skill folder). The catalog goes into the system prompt; the model loads a skill on demand with `load_skill`, or you preload one with `/ task`. Five skills are included: `code-review` (diff-scoped review with verified findings), `save-load-json` (state persistence with a completeness check), `browser-test` (headless Puppeteer harness), `single-file-webapp` (patterns + static checker) and `threejs-csg` (three.js with boolean operations, loaders and exporters). - **Vision** — the agent can look at images, screenshots and PDF pages (see the `view_image`/`take_screenshot`/`view_pdf_page` tools below) to visually check its own work, e.g. a generated plot or a running GUI. - **Session-wide context management** — token usage is tracked (server-reported or estimated); when the context nears its limit, older history is compacted into an LLM-written summary so long sessions keep working. The threshold is configurable in `settings.json` (`autoCompactWindow`, Claude-Code-compatible). - **Adjustable reasoning effort** — defaults to `low`; `/reasoning high|medium|low|max|none` changes the model's thinking effort at runtime (backend-dependent; `none` turns it off). - **Robust against flaky servers** — visible retries with backoff for connection errors, timeouts and 5xx; if the backend goes down, the session is preserved and the task can simply be resubmitted. A 4xx rejection is not retried: if the request contained images, they are stripped and the task continues text-only (models without vision); if the context is too long, the history is compacted and the request repeated once; otherwise the server's message is shown. - **Loop guard** — an identical tool call repeated `MAX_REPEATS` times in a row first triggers a forced reflection step (expected / observed / hypotheses / one experiment); if the model loops again, the task is stopped with the session intact. A no-op `edit_file` (old = new) is rejected, so it can't feed a loop. - **Transparent** — every request, tool call, and result is logged to `logFile.md`; file changes are shown as colored unified diffs in the terminal. The stats line after each response shows time, generated tokens, tok/s and the prompt size the server actually received (`context N`) — compare it with the context length configured in LM Studio/Ollama: if the server's window is smaller than `autoCompactWindow`, the server silently truncates the oldest messages (including the system prompt and skills) long before jcode compacts. - **Tools available to the model:** - `list_files` (skips `node_modules`, `.git`, caches and build folders — shown as one line with their entry count — and caps the output at 300 entries), `read_file` (optionally with line numbers and a `start_line`/`end_line` range, so large files aren't re-read whole), `search_files` (regex grep over the sandbox, hits as `file:line: content`) - `write_file`, `edit_file` (search/replace), `insert_lines`, `delete_lines` - `run_command` (runs in bash wherever it is installed — on Debian/Ubuntu `/bin/sh` is dash, which lacks `pipefail`, `[[ ]]`, `source`, `{a,b}`, `echo -e` and misparses `&>`; pipes report the exit code of the first failing command; blocks known-destructive patterns; `pip`/`pip3` calls are redirected to jcode's own Python interpreter; background processes must redirect their output — `cmd > x.log 2>&1 &` — or the call blocks until the timeout; on timeout the whole process tree is killed and the output collected so far is returned) - `web_search` (DuckDuckGo) and `fetch_url` (fetches a page and extracts its text) - `view_image` (png/jpg/gif/webp/bmp/svg), `take_screenshot` (whole screen, e.g. of a GUI app under test), `view_pdf_page` (renders one PDF page) — images are delivered to the model as an inline image right after the tool result - `load_skill` (returns a skill's `SKILL.md` or one of its linked files) ## Requirements - Python 3.10+ - `pip install openai prompt_toolkit ddgs` - One of: - [LM Studio](https://lmstudio.ai/) with the local server running (default `http://localhost:1234`), or - [Ollama](https://ollama.com/) (`http://localhost:11434`), or - an API key for OpenAI / Anthropic / Gemini (see `settings.json` below) - Optional, only needed for the vision tools (the agent will tell you what's missing and how to install it when it tries to use one): - `Pillow` — `view_image`, `take_screenshot`, `view_pdf_page` - `cairosvg` — `view_image` on `.svg` files - `pymupdf` — `view_pdf_page` - `certifi` — CA bundle for `fetch_url`; falls back to the system default if not installed. Set `JCODE_INSECURE_SSL=1` to disable TLS verification (e.g. behind a re-signing corporate proxy) — use with care. ## Usage ```bash # One-shot task python jcode.py "Write fib.py with fib(n) and a smoke test that checks fib(10)==55." # Interactive mode (multiline input; history is kept across tasks) python jcode.py ``` All generated files land in `./code`, which is created automatically. ### Interactive commands | Command | Effect | |---|---| | `/status` | Show context usage, message counts, model, sandbox | | `/new`, `/neu`, `/reset` | Clear the session history | | `/models` | List models available on the current backend | | `/model ` | Switch model | | `/backend ` | Switch to `lm-studio`, `ollama`, or a provider from `settings.json` (e.g. `openai`, `anthropic`, `gemini`) | | `/reasoning ` | Set reasoning effort: `high`, `medium`, `low` (startup default), `max`, or `none` (off); no argument or `default` resets to the backend default | | `/max-turns N` | Set the step limit per task | | `/skills` | List the skills found in `skills/` | | `/ ` | Run a task with that skill's instructions preloaded (e.g. `/threejs-csg Make a plate with four holes`) | | `Ctrl+C` | Abort the current input or running task (session is preserved) | | `Ctrl+D`, or `Ctrl+C` on an empty prompt | Quit | ## Configuration ### settings.json (optional) Placed next to `jcode.py`. Defines cloud providers, optionally the backend to start on, and the context compaction threshold. API keys are read from environment variables — don't put literal keys in the file. ```json { "autoCompactEnabled": true, "autoCompactWindow": "256k", "default_provider": "", "providers": { "openai": { "base_url": "https://api.openai.com/v1", "api_key_env": "OPENAI_API_KEY", "model": "gpt-4.1" }, "anthropic": { "base_url": "https://api.anthropic.com/v1/", "api_key_env": "ANTHROPIC_API_KEY", "model": "claude-sonnet-4-5" }, "gemini": { "base_url": "https://generativelanguage.googleapis.com/v1beta/openai/", "api_key_env": "GEMINI_API_KEY", "model": "gemini-2.5-pro" } } } ``` - `autoCompactEnabled` / `autoCompactWindow` (same keys and notation as Claude Code): history compaction kicks in once the context exceeds `autoCompactWindow` tokens (`"256k"`, `"1m"` or a plain number; default 250 000). `false` disables compaction entirely. `/status` shows the active threshold. - `default_provider`: name of a provider to activate at startup; leave `""` to use the auto-detected local backend. - Each provider needs `base_url` (an OpenAI-compatible endpoint), `api_key_env` (environment variable holding the key), and optionally `model`. - If the file is missing or malformed, jcode prints a warning and runs purely locally. ```bash export ANTHROPIC_API_KEY=sk-ant-... python jcode.py Task> /backend anthropic ``` ### AGENTS.md (optional) If `code/AGENTS.md` exists, it is loaded at session start and treated as binding project guidelines — e.g. "simplicity first", surgical changes only, and the per-language verification conventions (how to compile/test C, C++, Java, PHP, Python, HTML, CSS). Keeping language-specific rules here rather than in the system prompt keeps every request lean and lets you adapt conventions per project without touching code. ### Skills (optional) A skill is a folder `skills//` next to `jcode.py` containing a `SKILL.md` — the same layout and frontmatter Claude Code uses, so skills can be shared between both: ``` skills/ ├── browser-test/ # shot.js, actions-example.js ├── code-review/ # review_prep.py, checklist.md ├── save-load-json/ # js.md, python.md, check_roundtrip.py ├── single-file-webapp/ # patterns.md, check_html.py └── threejs-csg/ ├── SKILL.md # frontmatter (name, description) + instructions ├── csg.md # linked from SKILL.md as ./csg.md ├── template.html # linked from SKILL.md as ./template.html └── loaders.md ``` ```markdown --- name: threejs-csg description: > Build 3D scenes with three.js and apply boolean operations (subtract, union, intersect) to meshes. Use for any three.js, WebGL or CSG task. --- # Instructions ... see [csg.md](./csg.md) and [template.html](./template.html) ``` - `name` and `description` are read from the frontmatter (`name` falls back to the folder name). The frontmatter parser is deliberately minimal: `key: value` lines, plus indented continuation lines for `>`/`|` block scalars — no PyYAML needed. - At session start, the names and descriptions of all skills are added to the system prompt, and the model can call `load_skill(name)` at any time; the body of `SKILL.md` (frontmatter stripped) is returned together with a list of the other files in the folder. - **Automatic selection**: at the start of every task, jcode asks the model in one tool-less request which skills match the task and injects their `SKILL.md` into the task message. Small models reliably ignore a "call load_skill first" instruction in the system prompt but reliably answer a direct question — one cheap request instead of a run that hallucinates an API. `/ task` skips the selection and uses that skill. - Relative links in the Markdown (`./forms.md`, `[x](forms.md)`) refer to files in the skill folder; the model reads them with `load_skill(name, "forms.md")` (or `read_file`/`list_files` with `skills//`, `/` or the absolute path — a garbled prefix in front of `/skills//` is tolerated, since models mangle long paths; the skills folder is readable but read-only: `run_command` refuses `sed -i`, redirects, `open(..., 'w')` etc. on it, so the model copies a file into the project before changing it). Scripts can be run via their absolute path, which is included in the tool result. - `/ task` injects the skill text directly into the task so no extra tool call is needed; `/skills` lists what was found. Included skills: | Skill | Contents | Why | |---|---|---| | `browser-test` | `shot.js`: a Puppeteer harness that opens an HTML file via `file://` (no server), records JS exceptions / `console.error` / failed requests, runs an `actions.js` (click, shift-click, set input, key, eval, **upload** through the real file dialog, **download** through the real export button) and writes numbered screenshots; exit code 1 on any error. Probes WebGL and retries with SwiftShader flags (`--check` shows what works), traces every step and has its own watchdog, so a hang is reported with the step it stuck in instead of a silent timeout. Rules: test the control the user uses, not a stand-in; keep tests, fixtures and `node_modules` in the project. Needs `npm install puppeteer` in the project (or `CHROME_PATH` + `puppeteer-core`). | Without it the agent spends dozens of steps starting web servers and re-inventing a screenshot script, and finds runtime errors late. | | `single-file-webapp` | Rules for one-file ES-module apps (no inline `onclick` with module scripts, one state object + `render()`, JSON serialization with nested groups, Blob download / FileReader import, `window.__app` test hook), `patterns.md` with snippets, `check_html.py` (syntax-checks every script block with `node --check`, validates the import map, flags inline handlers and missing element ids). | Targets the most common bug class in generated single-file apps: module scope vs. inline handlers, and exporters that silently miss nested data. | | `code-review` | `review_prep.py` (stdlib only): scopes the review to the git diff (or given files), prints per file the changed line ranges, an outline of functions **with line ranges**, compiler/linter output restricted to bug-relevant rules (ruff/pyflakes, `node --check` incl. `