corecoder 0.7.0__tar.gz → 0.9.0__tar.gz
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.
- {corecoder-0.7.0 → corecoder-0.9.0}/.github/workflows/ci.yml +6 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/PKG-INFO +49 -31
- {corecoder-0.7.0 → corecoder-0.9.0}/README.md +46 -28
- {corecoder-0.7.0 → corecoder-0.9.0}/README_CN.md +46 -28
- {corecoder-0.7.0 → corecoder-0.9.0}/article/00-index.md +7 -7
- {corecoder-0.7.0 → corecoder-0.9.0}/article/00-index_EN.md +7 -7
- {corecoder-0.7.0 → corecoder-0.9.0}/article/01-the-loop.md +1 -1
- {corecoder-0.7.0 → corecoder-0.9.0}/article/01-the-loop_EN.md +1 -1
- {corecoder-0.7.0 → corecoder-0.9.0}/article/02-tools.md +5 -5
- {corecoder-0.7.0 → corecoder-0.9.0}/article/02-tools_EN.md +5 -5
- {corecoder-0.7.0 → corecoder-0.9.0}/article/03-llm-and-cost.md +6 -4
- {corecoder-0.7.0 → corecoder-0.9.0}/article/03-llm-and-cost_EN.md +6 -4
- {corecoder-0.7.0 → corecoder-0.9.0}/article/04-context.md +1 -1
- {corecoder-0.7.0 → corecoder-0.9.0}/article/04-context_EN.md +1 -1
- {corecoder-0.7.0 → corecoder-0.9.0}/article/05-parallel-and-subagents.md +18 -27
- {corecoder-0.7.0 → corecoder-0.9.0}/article/05-parallel-and-subagents_EN.md +18 -27
- {corecoder-0.7.0 → corecoder-0.9.0}/article/06-session-and-cli.md +2 -1
- {corecoder-0.7.0 → corecoder-0.9.0}/article/06-session-and-cli_EN.md +2 -1
- {corecoder-0.7.0 → corecoder-0.9.0}/article/07-build-your-own.md +5 -5
- {corecoder-0.7.0 → corecoder-0.9.0}/article/07-build-your-own_EN.md +5 -5
- {corecoder-0.7.0 → corecoder-0.9.0}/article/08-extensibility.md +3 -3
- {corecoder-0.7.0 → corecoder-0.9.0}/article/08-extensibility_EN.md +3 -3
- corecoder-0.9.0/article/claude-code-source-analysis.md +332 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/__init__.py +1 -1
- corecoder-0.9.0/corecoder/checkpoints.py +93 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/cli.py +37 -2
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/demo.py +1 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/llm.py +45 -13
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/mcp.py +41 -12
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/permissions.py +37 -4
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/session.py +2 -2
- corecoder-0.9.0/corecoder/tools/agent.py +143 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/todo.py +3 -3
- corecoder-0.9.0/examples/mcp_filesystem.json +8 -0
- corecoder-0.9.0/examples/mcp_filesystem.md +43 -0
- corecoder-0.9.0/examples/multi_agent_demo.py +56 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/examples/plan_hooks_demo.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/pyproject.toml +4 -3
- corecoder-0.9.0/tests/conftest.py +39 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_checkpoints.py +36 -2
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_core.py +310 -7
- corecoder-0.9.0/tests/test_live_smoke.py +52 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_mcp.py +81 -0
- corecoder-0.9.0/tests/test_multi_agent.py +193 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_permissions.py +26 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_plan_mode.py +5 -12
- corecoder-0.9.0/tests/test_repl.py +140 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_session.py +20 -0
- corecoder-0.9.0/tests/test_slash_commands.py +130 -0
- corecoder-0.7.0/corecoder/checkpoints.py +0 -44
- corecoder-0.7.0/corecoder/tools/agent.py +0 -72
- corecoder-0.7.0/tests/conftest.py +0 -11
- {corecoder-0.7.0 → corecoder-0.9.0}/.github/workflows/publish.yml +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/.gitignore +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/LICENSE +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/assets/demo-plan-hooks.gif +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/assets/demo.png +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/assets/demo_en.png +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/__main__.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/agent.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/config.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/context.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/hooks.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/prompt.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/shell.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/__init__.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/base.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/bash.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/edit.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/glob_tool.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/grep.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/read.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/corecoder/tools/write.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/__init__.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_demo.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_hooks.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_litellm.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_safety_matrix.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_shell.py +0 -0
- {corecoder-0.7.0 → corecoder-0.9.0}/tests/test_tools.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: corecoder
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Minimal AI coding agent (
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Minimal AI coding agent (2,908 lines of Python) inspired by Claude Code. Works with any LLM. (formerly NanoCoder)
|
|
5
5
|
Project-URL: Homepage, https://github.com/he-yufeng/CoreCoder
|
|
6
6
|
Project-URL: Repository, https://github.com/he-yufeng/CoreCoder
|
|
7
7
|
Project-URL: Issues, https://github.com/he-yufeng/CoreCoder/issues
|
|
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
# CoreCoder
|
|
39
39
|
|
|
40
|
-
**The nanoGPT of coding agents. A 1.
|
|
40
|
+
**The nanoGPT of coding agents. A 1.4k-line engine inside 2,908 readable lines of pure Python: understand how a coding agent actually works, then fork your own.**
|
|
41
41
|
|
|
42
42
|
*learn from it · fork it · ship something better*
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
[](https://python.org)
|
|
48
48
|
[](LICENSE)
|
|
49
49
|
[](https://github.com/he-yufeng/CoreCoder/actions)
|
|
50
|
-
[](article/00-index_EN.md)
|
|
51
51
|
[](article/00-index_EN.md)
|
|
52
52
|
|
|
53
53
|
</div>
|
|
@@ -60,7 +60,7 @@ Description-Content-Type: text/markdown
|
|
|
60
60
|
|
|
61
61
|
| | CoreCoder | Claude Code | aider | nanoGPT |
|
|
62
62
|
|---|---|---|---|---|
|
|
63
|
-
| Lines of code | ~1,
|
|
63
|
+
| Lines of code | ~1,405 engine / 2,908 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
|
|
64
64
|
| Time to read it all | one afternoon | can't (closed) | a few days of slogging | one afternoon |
|
|
65
65
|
| Breakpoint, change, rerun? | yes, every line | no | yes, but there's a lot | yes |
|
|
66
66
|
| What it's for | understand one, then fork your own | production coding assistant | terminal pair-programming | minimal GPT for teaching |
|
|
@@ -71,9 +71,9 @@ The nanoGPT column is there as a reference point: minimal, readable, but it teac
|
|
|
71
71
|
|
|
72
72
|
I've always felt coding agents get talked about as if they were arcane. Strip a tool like Claude Code or Cursor all the way down and the core is a `while` loop wrapped around a large model, plus seven or eight tools that let it actually do things. The hard part was never the loop; it's everything the loop has to cope with once it meets the real world. CoreCoder is the minimal version that writes that core out honestly.
|
|
73
73
|
|
|
74
|
-
The engine (loop, model interface, context, tools, sessions) is 1,
|
|
74
|
+
The engine (loop, model interface, context, tools, sessions) is 1,405 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 25 files: 2,908 physical lines, 2,364 net, every one short enough to read in a single sitting. The growth since the original 1,161-line snapshot went into visible features: plan mode, hooks and checkpoints, each documented below.
|
|
75
75
|
|
|
76
|
-
And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first.
|
|
76
|
+
And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first. The suite has 242 offline tests and an opt-in live smoke test. But the point of it running isn't to become your daily driver. It runs so the walkthrough can't lie: a reference that shows how an agent works has to actually work.
|
|
77
77
|
|
|
78
78
|
The code came out of a public teardown: open analyses have already exposed a lot of the load-bearing architecture inside production agents like Claude Code. I took the most essential layer and rewrote it honestly, in as little code as I could. So reading CoreCoder is roughly like reading a runnable, annotated take on how that kind of agent works, except it's only a minimal reimplementation, sitting right there on your machine for you to take apart and change.
|
|
79
79
|
|
|
@@ -107,7 +107,9 @@ Give it a model and a key and it goes. It speaks the OpenAI-compatible API by de
|
|
|
107
107
|
| OmniRoute | `OPENAI_API_KEY=your-key OPENAI_BASE_URL=http://localhost:20128/v1 CORECODER_MODEL=auto` |
|
|
108
108
|
| Local Ollama | `OPENAI_API_KEY=ollama OPENAI_BASE_URL=http://localhost:11434/v1 CORECODER_MODEL=qwen2.5-coder` |
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
On a 400 that names `temperature`, CoreCoder retries without it so the provider can apply its supported default. This includes Kimi's unquoted rejection; no `CORECODER_TEMPERATURE` override is needed.
|
|
111
|
+
|
|
112
|
+
Kimi, Qwen and the like are the same two variables; for providers that don't even offer an OpenAI-compatible endpoint, the optional LiteLLM backend (`pip install "corecoder[litellm]"`) routes to a hundred-plus of them. The third essay goes into this in detail. Thinking models are first-class too: deepseek-reasoner, kimi-k3 and friends stream their chain-of-thought, and CoreCoder shows it dimmed as it works. Plain answers keep reasoning out of the conversation history, while Kimi and DeepSeek tool-call turns carry `reasoning_content` into the next request as their thinking protocols require. The key can be `export`ed directly or dropped into a `.env` at the project root, which is loaded on startup. Then:
|
|
111
113
|
|
|
112
114
|
Smoke-tested end to end (read the file, edit it, run it, report back) against DeepSeek, Qwen3 and Kimi K2 via a single OpenRouter-compatible endpoint; each completed the full loop. One note for one-shot scripts: `-p` refuses mutating tools unless you pass `--yes`, by design.
|
|
113
115
|
|
|
@@ -123,18 +125,18 @@ Laid out flat, the whole project is this big. Skim it before you clone and you'l
|
|
|
123
125
|
```
|
|
124
126
|
corecoder/
|
|
125
127
|
├── agent.py agent loop + parallel tool exec 240 lines ← start here
|
|
126
|
-
├── llm.py streaming client + retry + cost
|
|
128
|
+
├── llm.py streaming client + retry + cost 364 lines
|
|
127
129
|
├── context.py three-tier context compaction 220 lines
|
|
128
130
|
├── session.py save / resume + path-traversal guard 97 lines
|
|
129
|
-
├── permissions.py consent for mutating tools
|
|
131
|
+
├── permissions.py consent for mutating tools 81 lines
|
|
130
132
|
├── hooks.py Pre/PostToolUse shell hooks 87 lines
|
|
131
133
|
├── shell.py POSIX shell routing (Git Bash on Windows) 61 lines
|
|
132
|
-
├── mcp.py MCP stdio client for external tools
|
|
134
|
+
├── mcp.py MCP stdio client for external tools 237 lines
|
|
133
135
|
├── prompt.py system prompt 41 lines
|
|
134
|
-
├── cli.py REPL + slash commands + one-shot
|
|
136
|
+
├── cli.py REPL + slash commands + one-shot 393 lines
|
|
135
137
|
├── config.py env-var config 55 lines
|
|
136
|
-
├── checkpoints.py /undo snapshot and restore
|
|
137
|
-
├── demo.py offline end-to-end demo
|
|
138
|
+
├── checkpoints.py /undo snapshot and restore 93 lines
|
|
139
|
+
├── demo.py offline end-to-end demo 101 lines
|
|
138
140
|
└── tools/
|
|
139
141
|
├── bash.py shell + dangerous-command gate + cd 203 lines
|
|
140
142
|
├── edit.py unique-match search/replace + diff 99 lines
|
|
@@ -143,10 +145,11 @@ corecoder/
|
|
|
143
145
|
├── read.py file read 56 lines
|
|
144
146
|
├── write.py file write 46 lines
|
|
145
147
|
├── todo.py agent-maintained task checklist 79 lines
|
|
146
|
-
├── agent.py sub-agent
|
|
148
|
+
├── agent.py sub-agent teams + isolation 143 lines
|
|
147
149
|
└── base.py tool base class 32 lines
|
|
148
150
|
examples/
|
|
149
|
-
|
|
151
|
+
├── plan_hooks_demo.py offline plan mode + hooks demo (no API key)
|
|
152
|
+
└── multi_agent_demo.py offline three-role parallel demo (no API key)
|
|
150
153
|
```
|
|
151
154
|
|
|
152
155
|
Eight tools: `bash`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `todo_write` (a task checklist the agent maintains for itself), and `agent` (which spawns a sub-agent). Everything else is the CLI shell, config, and packaging wrapped around that engine core. If `~/.corecoder/mcp.json` exists, its MCP servers join the eight as extra `mcp__*` tools; the MCP section below covers it.
|
|
@@ -170,7 +173,7 @@ def chat(self, user_input):
|
|
|
170
173
|
return "(hit the round limit)"
|
|
171
174
|
```
|
|
172
175
|
|
|
173
|
-
That's the whole thing. The core skeleton is about twenty lines; counting parallel execution and the bookkeeping after a Ctrl+C interrupt, maybe forty. Almost everything else in CoreCoder's thousand-odd lines is there to clean up the mess the loop runs into once it meets the real world. `llm.py` ends up the biggest file in the
|
|
176
|
+
That's the whole thing. The core skeleton is about twenty lines; counting parallel execution and the bookkeeping after a Ctrl+C interrupt, maybe forty. Almost everything else in CoreCoder's thousand-odd lines is there to clean up the mess the loop runs into once it meets the real world. `llm.py` ends up the biggest file in the engine, not because calling a model is hard, but because a streamed response splinters each tool call's arguments into fragments you have to restitch in order, a provider will hand you half a JSON object or a null `usage` field, and 429s, timeouts, dropped connections and 5xx all need backoff-and-retry while the other 4xx should just raise. A stream can also die after it has already started, so the retry wraps the whole request, not just the connect. That unglamorous grunt work, not the loop, is where the real engineering of taking an agent from demo to delivery actually lives; the third essay follows it down to the line.
|
|
174
177
|
|
|
175
178
|
Three decisions are worth a closer look, because they're the kind of call you can only make after you've understood how others did it, and they're judgments you can lift straight into your own fork.
|
|
176
179
|
|
|
@@ -180,6 +183,20 @@ Three decisions are worth a closer look, because they're the kind of call you ca
|
|
|
180
183
|
|
|
181
184
|
**You constrain a sub-agent by withholding the tool, not by writing rules and hoping it obeys.** A spawned sub-agent gets an isolated context and its own separate history, with a toolset exactly one item shorter than the parent's: the `agent` tool itself, so it can't recursively spawn more sub-agents. Handing it one fewer tool is cleaner than legislating a rule after the fact. It also reuses the parent's model connection (its spend folded into the same running total), truncates its output once it runs past 5,000 characters down to just the opening, and runs on a shorter round limit than the parent. The same restraint, end to end.
|
|
182
185
|
|
|
186
|
+
### Multi-agent, without an orchestration framework
|
|
187
|
+
|
|
188
|
+
The same `agent` tool now fans out a small team. Give each worker a name and an independent task; it gets a fresh message history and an empty checklist, while permission and hooks stay on the parent's contract:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
report = agent_tool.execute(tasks=[
|
|
192
|
+
{"name": "researcher", "task": "Map the parser and its failure modes"},
|
|
193
|
+
{"name": "implementer", "task": "Propose the smallest fix"},
|
|
194
|
+
{"name": "reviewer", "task": "Attack the proposed fix with edge cases"},
|
|
195
|
+
])
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
One to eight workers run up to four at a time, each through the normal agent loop. Reports come back in input order, not completion order, and one worker's exception does not cancel its siblings. Shared token accounting and interactive permission prompts are serialized, so concurrency doesn't lose cost or interleave consent. There is no scheduler, shared message bus, recursive spawning, or second agent loop. `examples/multi_agent_demo.py` runs the researcher / implementer / reviewer pattern offline with scripted findings; no API key and no model-quality claim.
|
|
199
|
+
|
|
183
200
|
Every one of these *whys* is traced down to the actual lines of code in the series below.
|
|
184
201
|
|
|
185
202
|
## The source-reading series · 8 bilingual essays
|
|
@@ -188,7 +205,7 @@ I also wrote a bilingual source-reading series, one intro plus eight parts, each
|
|
|
188
205
|
|
|
189
206
|
- **[Intro · Read Claude Code through CoreCoder, then build your own](article/00-index_EN.md)**
|
|
190
207
|
- **[01 · An agent, at its core, is a `while` loop](article/01-the-loop_EN.md)** — the main loop in `agent.py`, interrupts, and the round limit
|
|
191
|
-
- **[02 · The tool system: letting the model act, safely](article/02-tools_EN.md)** — the
|
|
208
|
+
- **[02 · The tool system: letting the model act, safely](article/02-tools_EN.md)** — the eight tools in `tools/` and the bash safety gate
|
|
192
209
|
- **[03 · Plug in any LLM, and keep the bill honest](article/03-llm-and-cost_EN.md)** — `llm.py`'s provider wrapper, retries, and cost accounting
|
|
193
210
|
- **[04 · Surviving a long task on a finite window](article/04-context_EN.md)** — `context.py`'s three-tier compaction and orphaned tool messages
|
|
194
211
|
- **[05 · Parallel execution and sub-agents](article/05-parallel-and-subagents_EN.md)** — thread-pool concurrency and sub-agent isolation
|
|
@@ -200,8 +217,8 @@ I also wrote a bilingual source-reading series, one intro plus eight parts, each
|
|
|
200
217
|
|
|
201
218
|
Once you understand it, the natural next step is to fork. Getting started doesn't take much:
|
|
202
219
|
|
|
203
|
-
- **Swap in a model you actually use.** It's the two env vars from above; `llm.py` (
|
|
204
|
-
- **Add a tool of your own.** Write a new file against the tool base class in `tools/base.py` (
|
|
220
|
+
- **Swap in a model you actually use.** It's the two env vars from above; `llm.py` (364 lines) is the entry point for all provider adaptation.
|
|
221
|
+
- **Add a tool of your own.** Write a new file against the tool base class in `tools/base.py` (32 lines): run tests, fetch a page, call an LSP, whatever. The end of the second essay walks you through your first one by hand.
|
|
205
222
|
- **Rewrite the system prompt.** `prompt.py` is all of 41 lines; change one line and you'll watch the agent's temperament shift. It's the cheapest "change one thing, see a result" in the whole project.
|
|
206
223
|
- **Import it as a library.** The top level exports `Agent`, `LLM`, and `Config`, ready to embed in your own program:
|
|
207
224
|
|
|
@@ -216,7 +233,7 @@ Going deeper, the directions are out in the open too. None of the following is i
|
|
|
216
233
|
|
|
217
234
|
- **The dangerous-command blocking in bash is just a regex blacklist.** It guards against slips, not a security sandbox. Facing untrusted input means reaching for seccomp or container isolation. This is the hardest of the four; it goes all the way down to the syscall and isolation layer.
|
|
218
235
|
- **Retry is only exponential backoff.** No fallback model, no hard dollar budget. Follow `llm.py` down and add a fallback model chain plus a stop-on-over-budget gate; the change stays mostly inside that one file.
|
|
219
|
-
- **
|
|
236
|
+
- **Agent teams join before returning.** Named workers already run concurrently with isolated contexts; asynchronous background jobs, streaming progress and role-specific tool presets remain open extension points rather than another orchestration layer to add by default.
|
|
220
237
|
- **No RAG, and the MCP client speaks tools only.** Retrieval-based code location for big repos is still open, and `mcp.py` leaves resources and prompts unimplemented on purpose. Either one is a real way to grow from a minimal core into your own stronger agent.
|
|
221
238
|
|
|
222
239
|
The README only points; the seventh essay picks up the code details for each. Pick one and start; that's the whole reason the core is kept this small.
|
|
@@ -233,16 +250,17 @@ Inside the REPL, `/help` lists everything; these are the ones you'll reach for:
|
|
|
233
250
|
/undo revert the most recent file change
|
|
234
251
|
/plan toggle plan mode (read-only, then a plan to approve)
|
|
235
252
|
/save /sessions save / list sessions
|
|
253
|
+
/mcp MCP server status; /mcp reconnect <name> revives a dead one
|
|
236
254
|
quit / exit exit (Ctrl+C cancels the current round)
|
|
237
255
|
```
|
|
238
256
|
|
|
239
|
-
Session IDs are sanitized to safe characters before they become filenames, every archive lands under `~/.corecoder/sessions`, and a malicious session name can't traverse out.
|
|
257
|
+
Session IDs are sanitized to safe characters before they become filenames, every archive lands under `~/.corecoder/sessions`, and a malicious session name can't traverse out. Undo history persists the same way: checkpoints land in `~/.corecoder/checkpoints.json`, so `/undo` still reaches back after a restart. The cost estimate behind `/tokens` reads a built-in price table, and `~/.corecoder/pricing.json` overrides any entry in it, so a new model or a moved price lands without waiting for a release.
|
|
240
258
|
|
|
241
259
|
## Permissions
|
|
242
260
|
|
|
243
261
|
Read-only tools (`read_file`, `glob`, `grep`, `todo_write`) run the moment the model asks. The mutating ones (`edit_file`, `write_file`, `bash`, and spawning a sub-agent) stop for consent first, and the REPL banner shows which mode you're in:
|
|
244
262
|
|
|
245
|
-
- In the REPL you get one prompt per call: allow once, always allow this tool, or deny. "Always" is remembered per tool
|
|
263
|
+
- In the REPL you get one prompt per call: allow once, always allow this tool, or deny. "Always" is remembered per tool and persists in `~/.corecoder/permissions.json` across restarts; a sub-agent inherits the same layer, so consent follows the work wherever it happens.
|
|
246
264
|
- In one-shot mode (`-p`) there is nobody to ask, so a mutating call is refused on the spot and the refusal goes back to the model as an ordinary tool result: the loop never hangs on input that can't arrive. Pass `--yes` to approve everything up front (scripts, CI).
|
|
247
265
|
- The decision itself is pure logic in `permissions.py`, with the terminal only supplying the prompt callback. You can unit-test consent without a TTY, or reuse the layer in your own embedding.
|
|
248
266
|
|
|
@@ -271,8 +289,8 @@ Two worth stealing (the commands lean on `jq`, the usual suspect):
|
|
|
271
289
|
# the same turn instead of waiting for CI.
|
|
272
290
|
{
|
|
273
291
|
"PostToolUse": [{
|
|
274
|
-
"matcher": "
|
|
275
|
-
"command": "f=$(jq -r .tool_input.
|
|
292
|
+
"matcher": "edit_file",
|
|
293
|
+
"command": "f=$(jq -r .tool_input.file_path); ruff check \"$f\" 2>&1 | head -20"
|
|
276
294
|
}]
|
|
277
295
|
}
|
|
278
296
|
|
|
@@ -280,8 +298,8 @@ Two worth stealing (the commands lean on `jq`, the usual suspect):
|
|
|
280
298
|
# touch. Exit code 2 vetoes the call and the message reaches the model.
|
|
281
299
|
{
|
|
282
300
|
"PreToolUse": [{
|
|
283
|
-
"matcher": "
|
|
284
|
-
"command": "case \"$(jq -r .tool_input.
|
|
301
|
+
"matcher": "edit_file",
|
|
302
|
+
"command": "case \"$(jq -r .tool_input.file_path)\" in .env*|*/secrets/*|*.pem) echo 'that path is off-limits' >&2; exit 2;; esac"
|
|
285
303
|
}]
|
|
286
304
|
}
|
|
287
305
|
```
|
|
@@ -300,7 +318,7 @@ Drop a `mcp.json` under `~/.corecoder` and tools from any MCP server join the ag
|
|
|
300
318
|
}
|
|
301
319
|
```
|
|
302
320
|
|
|
303
|
-
Each configured server starts as a subprocess at launch, handshakes, and lists its tools; every one is registered as `mcp__<server>__<tool>`, so hook matchers and the consent gate treat it exactly like a built-in. MCP tools stay out of the read-only set, meaning the agent asks before running one. The handshake gets fifteen seconds, a call gets sixty, and a server that dies or never answers fails that one call as an ordinary tool result instead of killing the loop. The client speaks the tools slice of the protocol (initialize, tools/list, tools/call) and nothing else, which keeps the whole thing inside `mcp.py` at about
|
|
321
|
+
Each configured server starts as a subprocess at launch, handshakes, and lists its tools; every one is registered as `mcp__<server>__<tool>`, so hook matchers and the consent gate treat it exactly like a built-in. MCP tools stay out of the read-only set, meaning the agent asks before running one. The handshake gets fifteen seconds, a call gets sixty, and a server that dies or never answers fails that one call as an ordinary tool result instead of killing the loop. The REPL's `/mcp` lists every server with its state, and `/mcp reconnect <name>` starts a dead one fresh: new process, new handshake, and the tools the agent already holds work again with no restart. The client speaks the tools slice of the protocol (initialize, tools/list, tools/call) and nothing else, which keeps the whole thing inside `mcp.py` at about 240 lines. With no `mcp.json` there is no MCP and nothing changes. A runnable starter config lives in [`examples/mcp_filesystem.md`](examples/mcp_filesystem.md): the reference filesystem server, scoped to one directory, behind the same consent gate.
|
|
304
322
|
|
|
305
323
|
## Related Projects
|
|
306
324
|
|
|
@@ -314,10 +332,10 @@ If working through CoreCoder was useful, here are a few other tools I've built a
|
|
|
314
332
|
|
|
315
333
|
## Contributing / License
|
|
316
334
|
|
|
317
|
-
Before you send anything, run `pytest tests/ -q` (
|
|
335
|
+
Before you send anything, run `pytest tests/ -q` (240 tests), `ruff check`, and `compileall`, and make sure they're green. MIT licensed: fork it, learn from it, ship something better. A mention of this project is appreciated.
|
|
318
336
|
|
|
319
337
|
---
|
|
320
338
|
|
|
321
|
-
By [Yufeng He](https://github.com/he-yufeng), formerly at Moonshot AI (Kimi). I earlier wrote a fairly complete [Claude Code source analysis](
|
|
339
|
+
By [Yufeng He](https://github.com/he-yufeng), formerly at Moonshot AI (Kimi). I earlier wrote a fairly complete [Claude Code source analysis](article/claude-code-source-analysis.md) (written in Chinese); this project is its hands-on counterpart: that one walks you through reading it, this one through rebuilding it.
|
|
322
340
|
|
|
323
341
|
> CoreCoder was formerly named NanoCoder; it was renamed to avoid confusion with [Nano-Collective/nanocoder](https://github.com/Nano-Collective/nanocoder), and old links redirect here automatically.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# CoreCoder
|
|
4
4
|
|
|
5
|
-
**The nanoGPT of coding agents. A 1.
|
|
5
|
+
**The nanoGPT of coding agents. A 1.4k-line engine inside 2,908 readable lines of pure Python: understand how a coding agent actually works, then fork your own.**
|
|
6
6
|
|
|
7
7
|
*learn from it · fork it · ship something better*
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://python.org)
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
[](https://github.com/he-yufeng/CoreCoder/actions)
|
|
15
|
-
[](article/00-index_EN.md)
|
|
16
16
|
[](article/00-index_EN.md)
|
|
17
17
|
|
|
18
18
|
</div>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
| | CoreCoder | Claude Code | aider | nanoGPT |
|
|
27
27
|
|---|---|---|---|---|
|
|
28
|
-
| Lines of code | ~1,
|
|
28
|
+
| Lines of code | ~1,405 engine / 2,908 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
|
|
29
29
|
| Time to read it all | one afternoon | can't (closed) | a few days of slogging | one afternoon |
|
|
30
30
|
| Breakpoint, change, rerun? | yes, every line | no | yes, but there's a lot | yes |
|
|
31
31
|
| What it's for | understand one, then fork your own | production coding assistant | terminal pair-programming | minimal GPT for teaching |
|
|
@@ -36,9 +36,9 @@ The nanoGPT column is there as a reference point: minimal, readable, but it teac
|
|
|
36
36
|
|
|
37
37
|
I've always felt coding agents get talked about as if they were arcane. Strip a tool like Claude Code or Cursor all the way down and the core is a `while` loop wrapped around a large model, plus seven or eight tools that let it actually do things. The hard part was never the loop; it's everything the loop has to cope with once it meets the real world. CoreCoder is the minimal version that writes that core out honestly.
|
|
38
38
|
|
|
39
|
-
The engine (loop, model interface, context, tools, sessions) is 1,
|
|
39
|
+
The engine (loop, model interface, context, tools, sessions) is 1,405 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 25 files: 2,908 physical lines, 2,364 net, every one short enough to read in a single sitting. The growth since the original 1,161-line snapshot went into visible features: plan mode, hooks and checkpoints, each documented below.
|
|
40
40
|
|
|
41
|
-
And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first.
|
|
41
|
+
And it really runs: reads and writes files, executes shell, spawns sub-agents, compacts context in three tiers, and tells you the tokens and dollars a run burned whenever you ask. Anything that would mutate your disk or run a command stops for your consent first. The suite has 242 offline tests and an opt-in live smoke test. But the point of it running isn't to become your daily driver. It runs so the walkthrough can't lie: a reference that shows how an agent works has to actually work.
|
|
42
42
|
|
|
43
43
|
The code came out of a public teardown: open analyses have already exposed a lot of the load-bearing architecture inside production agents like Claude Code. I took the most essential layer and rewrote it honestly, in as little code as I could. So reading CoreCoder is roughly like reading a runnable, annotated take on how that kind of agent works, except it's only a minimal reimplementation, sitting right there on your machine for you to take apart and change.
|
|
44
44
|
|
|
@@ -72,7 +72,9 @@ Give it a model and a key and it goes. It speaks the OpenAI-compatible API by de
|
|
|
72
72
|
| OmniRoute | `OPENAI_API_KEY=your-key OPENAI_BASE_URL=http://localhost:20128/v1 CORECODER_MODEL=auto` |
|
|
73
73
|
| Local Ollama | `OPENAI_API_KEY=ollama OPENAI_BASE_URL=http://localhost:11434/v1 CORECODER_MODEL=qwen2.5-coder` |
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
On a 400 that names `temperature`, CoreCoder retries without it so the provider can apply its supported default. This includes Kimi's unquoted rejection; no `CORECODER_TEMPERATURE` override is needed.
|
|
76
|
+
|
|
77
|
+
Kimi, Qwen and the like are the same two variables; for providers that don't even offer an OpenAI-compatible endpoint, the optional LiteLLM backend (`pip install "corecoder[litellm]"`) routes to a hundred-plus of them. The third essay goes into this in detail. Thinking models are first-class too: deepseek-reasoner, kimi-k3 and friends stream their chain-of-thought, and CoreCoder shows it dimmed as it works. Plain answers keep reasoning out of the conversation history, while Kimi and DeepSeek tool-call turns carry `reasoning_content` into the next request as their thinking protocols require. The key can be `export`ed directly or dropped into a `.env` at the project root, which is loaded on startup. Then:
|
|
76
78
|
|
|
77
79
|
Smoke-tested end to end (read the file, edit it, run it, report back) against DeepSeek, Qwen3 and Kimi K2 via a single OpenRouter-compatible endpoint; each completed the full loop. One note for one-shot scripts: `-p` refuses mutating tools unless you pass `--yes`, by design.
|
|
78
80
|
|
|
@@ -88,18 +90,18 @@ Laid out flat, the whole project is this big. Skim it before you clone and you'l
|
|
|
88
90
|
```
|
|
89
91
|
corecoder/
|
|
90
92
|
├── agent.py agent loop + parallel tool exec 240 lines ← start here
|
|
91
|
-
├── llm.py streaming client + retry + cost
|
|
93
|
+
├── llm.py streaming client + retry + cost 364 lines
|
|
92
94
|
├── context.py three-tier context compaction 220 lines
|
|
93
95
|
├── session.py save / resume + path-traversal guard 97 lines
|
|
94
|
-
├── permissions.py consent for mutating tools
|
|
96
|
+
├── permissions.py consent for mutating tools 81 lines
|
|
95
97
|
├── hooks.py Pre/PostToolUse shell hooks 87 lines
|
|
96
98
|
├── shell.py POSIX shell routing (Git Bash on Windows) 61 lines
|
|
97
|
-
├── mcp.py MCP stdio client for external tools
|
|
99
|
+
├── mcp.py MCP stdio client for external tools 237 lines
|
|
98
100
|
├── prompt.py system prompt 41 lines
|
|
99
|
-
├── cli.py REPL + slash commands + one-shot
|
|
101
|
+
├── cli.py REPL + slash commands + one-shot 393 lines
|
|
100
102
|
├── config.py env-var config 55 lines
|
|
101
|
-
├── checkpoints.py /undo snapshot and restore
|
|
102
|
-
├── demo.py offline end-to-end demo
|
|
103
|
+
├── checkpoints.py /undo snapshot and restore 93 lines
|
|
104
|
+
├── demo.py offline end-to-end demo 101 lines
|
|
103
105
|
└── tools/
|
|
104
106
|
├── bash.py shell + dangerous-command gate + cd 203 lines
|
|
105
107
|
├── edit.py unique-match search/replace + diff 99 lines
|
|
@@ -108,10 +110,11 @@ corecoder/
|
|
|
108
110
|
├── read.py file read 56 lines
|
|
109
111
|
├── write.py file write 46 lines
|
|
110
112
|
├── todo.py agent-maintained task checklist 79 lines
|
|
111
|
-
├── agent.py sub-agent
|
|
113
|
+
├── agent.py sub-agent teams + isolation 143 lines
|
|
112
114
|
└── base.py tool base class 32 lines
|
|
113
115
|
examples/
|
|
114
|
-
|
|
116
|
+
├── plan_hooks_demo.py offline plan mode + hooks demo (no API key)
|
|
117
|
+
└── multi_agent_demo.py offline three-role parallel demo (no API key)
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
Eight tools: `bash`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `todo_write` (a task checklist the agent maintains for itself), and `agent` (which spawns a sub-agent). Everything else is the CLI shell, config, and packaging wrapped around that engine core. If `~/.corecoder/mcp.json` exists, its MCP servers join the eight as extra `mcp__*` tools; the MCP section below covers it.
|
|
@@ -135,7 +138,7 @@ def chat(self, user_input):
|
|
|
135
138
|
return "(hit the round limit)"
|
|
136
139
|
```
|
|
137
140
|
|
|
138
|
-
That's the whole thing. The core skeleton is about twenty lines; counting parallel execution and the bookkeeping after a Ctrl+C interrupt, maybe forty. Almost everything else in CoreCoder's thousand-odd lines is there to clean up the mess the loop runs into once it meets the real world. `llm.py` ends up the biggest file in the
|
|
141
|
+
That's the whole thing. The core skeleton is about twenty lines; counting parallel execution and the bookkeeping after a Ctrl+C interrupt, maybe forty. Almost everything else in CoreCoder's thousand-odd lines is there to clean up the mess the loop runs into once it meets the real world. `llm.py` ends up the biggest file in the engine, not because calling a model is hard, but because a streamed response splinters each tool call's arguments into fragments you have to restitch in order, a provider will hand you half a JSON object or a null `usage` field, and 429s, timeouts, dropped connections and 5xx all need backoff-and-retry while the other 4xx should just raise. A stream can also die after it has already started, so the retry wraps the whole request, not just the connect. That unglamorous grunt work, not the loop, is where the real engineering of taking an agent from demo to delivery actually lives; the third essay follows it down to the line.
|
|
139
142
|
|
|
140
143
|
Three decisions are worth a closer look, because they're the kind of call you can only make after you've understood how others did it, and they're judgments you can lift straight into your own fork.
|
|
141
144
|
|
|
@@ -145,6 +148,20 @@ Three decisions are worth a closer look, because they're the kind of call you ca
|
|
|
145
148
|
|
|
146
149
|
**You constrain a sub-agent by withholding the tool, not by writing rules and hoping it obeys.** A spawned sub-agent gets an isolated context and its own separate history, with a toolset exactly one item shorter than the parent's: the `agent` tool itself, so it can't recursively spawn more sub-agents. Handing it one fewer tool is cleaner than legislating a rule after the fact. It also reuses the parent's model connection (its spend folded into the same running total), truncates its output once it runs past 5,000 characters down to just the opening, and runs on a shorter round limit than the parent. The same restraint, end to end.
|
|
147
150
|
|
|
151
|
+
### Multi-agent, without an orchestration framework
|
|
152
|
+
|
|
153
|
+
The same `agent` tool now fans out a small team. Give each worker a name and an independent task; it gets a fresh message history and an empty checklist, while permission and hooks stay on the parent's contract:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
report = agent_tool.execute(tasks=[
|
|
157
|
+
{"name": "researcher", "task": "Map the parser and its failure modes"},
|
|
158
|
+
{"name": "implementer", "task": "Propose the smallest fix"},
|
|
159
|
+
{"name": "reviewer", "task": "Attack the proposed fix with edge cases"},
|
|
160
|
+
])
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
One to eight workers run up to four at a time, each through the normal agent loop. Reports come back in input order, not completion order, and one worker's exception does not cancel its siblings. Shared token accounting and interactive permission prompts are serialized, so concurrency doesn't lose cost or interleave consent. There is no scheduler, shared message bus, recursive spawning, or second agent loop. `examples/multi_agent_demo.py` runs the researcher / implementer / reviewer pattern offline with scripted findings; no API key and no model-quality claim.
|
|
164
|
+
|
|
148
165
|
Every one of these *whys* is traced down to the actual lines of code in the series below.
|
|
149
166
|
|
|
150
167
|
## The source-reading series · 8 bilingual essays
|
|
@@ -153,7 +170,7 @@ I also wrote a bilingual source-reading series, one intro plus eight parts, each
|
|
|
153
170
|
|
|
154
171
|
- **[Intro · Read Claude Code through CoreCoder, then build your own](article/00-index_EN.md)**
|
|
155
172
|
- **[01 · An agent, at its core, is a `while` loop](article/01-the-loop_EN.md)** — the main loop in `agent.py`, interrupts, and the round limit
|
|
156
|
-
- **[02 · The tool system: letting the model act, safely](article/02-tools_EN.md)** — the
|
|
173
|
+
- **[02 · The tool system: letting the model act, safely](article/02-tools_EN.md)** — the eight tools in `tools/` and the bash safety gate
|
|
157
174
|
- **[03 · Plug in any LLM, and keep the bill honest](article/03-llm-and-cost_EN.md)** — `llm.py`'s provider wrapper, retries, and cost accounting
|
|
158
175
|
- **[04 · Surviving a long task on a finite window](article/04-context_EN.md)** — `context.py`'s three-tier compaction and orphaned tool messages
|
|
159
176
|
- **[05 · Parallel execution and sub-agents](article/05-parallel-and-subagents_EN.md)** — thread-pool concurrency and sub-agent isolation
|
|
@@ -165,8 +182,8 @@ I also wrote a bilingual source-reading series, one intro plus eight parts, each
|
|
|
165
182
|
|
|
166
183
|
Once you understand it, the natural next step is to fork. Getting started doesn't take much:
|
|
167
184
|
|
|
168
|
-
- **Swap in a model you actually use.** It's the two env vars from above; `llm.py` (
|
|
169
|
-
- **Add a tool of your own.** Write a new file against the tool base class in `tools/base.py` (
|
|
185
|
+
- **Swap in a model you actually use.** It's the two env vars from above; `llm.py` (364 lines) is the entry point for all provider adaptation.
|
|
186
|
+
- **Add a tool of your own.** Write a new file against the tool base class in `tools/base.py` (32 lines): run tests, fetch a page, call an LSP, whatever. The end of the second essay walks you through your first one by hand.
|
|
170
187
|
- **Rewrite the system prompt.** `prompt.py` is all of 41 lines; change one line and you'll watch the agent's temperament shift. It's the cheapest "change one thing, see a result" in the whole project.
|
|
171
188
|
- **Import it as a library.** The top level exports `Agent`, `LLM`, and `Config`, ready to embed in your own program:
|
|
172
189
|
|
|
@@ -181,7 +198,7 @@ Going deeper, the directions are out in the open too. None of the following is i
|
|
|
181
198
|
|
|
182
199
|
- **The dangerous-command blocking in bash is just a regex blacklist.** It guards against slips, not a security sandbox. Facing untrusted input means reaching for seccomp or container isolation. This is the hardest of the four; it goes all the way down to the syscall and isolation layer.
|
|
183
200
|
- **Retry is only exponential backoff.** No fallback model, no hard dollar budget. Follow `llm.py` down and add a fallback model chain plus a stop-on-over-budget gate; the change stays mostly inside that one file.
|
|
184
|
-
- **
|
|
201
|
+
- **Agent teams join before returning.** Named workers already run concurrently with isolated contexts; asynchronous background jobs, streaming progress and role-specific tool presets remain open extension points rather than another orchestration layer to add by default.
|
|
185
202
|
- **No RAG, and the MCP client speaks tools only.** Retrieval-based code location for big repos is still open, and `mcp.py` leaves resources and prompts unimplemented on purpose. Either one is a real way to grow from a minimal core into your own stronger agent.
|
|
186
203
|
|
|
187
204
|
The README only points; the seventh essay picks up the code details for each. Pick one and start; that's the whole reason the core is kept this small.
|
|
@@ -198,16 +215,17 @@ Inside the REPL, `/help` lists everything; these are the ones you'll reach for:
|
|
|
198
215
|
/undo revert the most recent file change
|
|
199
216
|
/plan toggle plan mode (read-only, then a plan to approve)
|
|
200
217
|
/save /sessions save / list sessions
|
|
218
|
+
/mcp MCP server status; /mcp reconnect <name> revives a dead one
|
|
201
219
|
quit / exit exit (Ctrl+C cancels the current round)
|
|
202
220
|
```
|
|
203
221
|
|
|
204
|
-
Session IDs are sanitized to safe characters before they become filenames, every archive lands under `~/.corecoder/sessions`, and a malicious session name can't traverse out.
|
|
222
|
+
Session IDs are sanitized to safe characters before they become filenames, every archive lands under `~/.corecoder/sessions`, and a malicious session name can't traverse out. Undo history persists the same way: checkpoints land in `~/.corecoder/checkpoints.json`, so `/undo` still reaches back after a restart. The cost estimate behind `/tokens` reads a built-in price table, and `~/.corecoder/pricing.json` overrides any entry in it, so a new model or a moved price lands without waiting for a release.
|
|
205
223
|
|
|
206
224
|
## Permissions
|
|
207
225
|
|
|
208
226
|
Read-only tools (`read_file`, `glob`, `grep`, `todo_write`) run the moment the model asks. The mutating ones (`edit_file`, `write_file`, `bash`, and spawning a sub-agent) stop for consent first, and the REPL banner shows which mode you're in:
|
|
209
227
|
|
|
210
|
-
- In the REPL you get one prompt per call: allow once, always allow this tool, or deny. "Always" is remembered per tool
|
|
228
|
+
- In the REPL you get one prompt per call: allow once, always allow this tool, or deny. "Always" is remembered per tool and persists in `~/.corecoder/permissions.json` across restarts; a sub-agent inherits the same layer, so consent follows the work wherever it happens.
|
|
211
229
|
- In one-shot mode (`-p`) there is nobody to ask, so a mutating call is refused on the spot and the refusal goes back to the model as an ordinary tool result: the loop never hangs on input that can't arrive. Pass `--yes` to approve everything up front (scripts, CI).
|
|
212
230
|
- The decision itself is pure logic in `permissions.py`, with the terminal only supplying the prompt callback. You can unit-test consent without a TTY, or reuse the layer in your own embedding.
|
|
213
231
|
|
|
@@ -236,8 +254,8 @@ Two worth stealing (the commands lean on `jq`, the usual suspect):
|
|
|
236
254
|
# the same turn instead of waiting for CI.
|
|
237
255
|
{
|
|
238
256
|
"PostToolUse": [{
|
|
239
|
-
"matcher": "
|
|
240
|
-
"command": "f=$(jq -r .tool_input.
|
|
257
|
+
"matcher": "edit_file",
|
|
258
|
+
"command": "f=$(jq -r .tool_input.file_path); ruff check \"$f\" 2>&1 | head -20"
|
|
241
259
|
}]
|
|
242
260
|
}
|
|
243
261
|
|
|
@@ -245,8 +263,8 @@ Two worth stealing (the commands lean on `jq`, the usual suspect):
|
|
|
245
263
|
# touch. Exit code 2 vetoes the call and the message reaches the model.
|
|
246
264
|
{
|
|
247
265
|
"PreToolUse": [{
|
|
248
|
-
"matcher": "
|
|
249
|
-
"command": "case \"$(jq -r .tool_input.
|
|
266
|
+
"matcher": "edit_file",
|
|
267
|
+
"command": "case \"$(jq -r .tool_input.file_path)\" in .env*|*/secrets/*|*.pem) echo 'that path is off-limits' >&2; exit 2;; esac"
|
|
250
268
|
}]
|
|
251
269
|
}
|
|
252
270
|
```
|
|
@@ -265,7 +283,7 @@ Drop a `mcp.json` under `~/.corecoder` and tools from any MCP server join the ag
|
|
|
265
283
|
}
|
|
266
284
|
```
|
|
267
285
|
|
|
268
|
-
Each configured server starts as a subprocess at launch, handshakes, and lists its tools; every one is registered as `mcp__<server>__<tool>`, so hook matchers and the consent gate treat it exactly like a built-in. MCP tools stay out of the read-only set, meaning the agent asks before running one. The handshake gets fifteen seconds, a call gets sixty, and a server that dies or never answers fails that one call as an ordinary tool result instead of killing the loop. The client speaks the tools slice of the protocol (initialize, tools/list, tools/call) and nothing else, which keeps the whole thing inside `mcp.py` at about
|
|
286
|
+
Each configured server starts as a subprocess at launch, handshakes, and lists its tools; every one is registered as `mcp__<server>__<tool>`, so hook matchers and the consent gate treat it exactly like a built-in. MCP tools stay out of the read-only set, meaning the agent asks before running one. The handshake gets fifteen seconds, a call gets sixty, and a server that dies or never answers fails that one call as an ordinary tool result instead of killing the loop. The REPL's `/mcp` lists every server with its state, and `/mcp reconnect <name>` starts a dead one fresh: new process, new handshake, and the tools the agent already holds work again with no restart. The client speaks the tools slice of the protocol (initialize, tools/list, tools/call) and nothing else, which keeps the whole thing inside `mcp.py` at about 240 lines. With no `mcp.json` there is no MCP and nothing changes. A runnable starter config lives in [`examples/mcp_filesystem.md`](examples/mcp_filesystem.md): the reference filesystem server, scoped to one directory, behind the same consent gate.
|
|
269
287
|
|
|
270
288
|
## Related Projects
|
|
271
289
|
|
|
@@ -279,10 +297,10 @@ If working through CoreCoder was useful, here are a few other tools I've built a
|
|
|
279
297
|
|
|
280
298
|
## Contributing / License
|
|
281
299
|
|
|
282
|
-
Before you send anything, run `pytest tests/ -q` (
|
|
300
|
+
Before you send anything, run `pytest tests/ -q` (240 tests), `ruff check`, and `compileall`, and make sure they're green. MIT licensed: fork it, learn from it, ship something better. A mention of this project is appreciated.
|
|
283
301
|
|
|
284
302
|
---
|
|
285
303
|
|
|
286
|
-
By [Yufeng He](https://github.com/he-yufeng), formerly at Moonshot AI (Kimi). I earlier wrote a fairly complete [Claude Code source analysis](
|
|
304
|
+
By [Yufeng He](https://github.com/he-yufeng), formerly at Moonshot AI (Kimi). I earlier wrote a fairly complete [Claude Code source analysis](article/claude-code-source-analysis.md) (written in Chinese); this project is its hands-on counterpart: that one walks you through reading it, this one through rebuilding it.
|
|
287
305
|
|
|
288
306
|
> CoreCoder was formerly named NanoCoder; it was renamed to avoid confusion with [Nano-Collective/nanocoder](https://github.com/Nano-Collective/nanocoder), and old links redirect here automatically.
|