corecoder 0.4.1__tar.gz → 0.4.2__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.
Files changed (54) hide show
  1. {corecoder-0.4.1 → corecoder-0.4.2}/PKG-INFO +6 -5
  2. {corecoder-0.4.1 → corecoder-0.4.2}/README.md +5 -4
  3. {corecoder-0.4.1 → corecoder-0.4.2}/README_CN.md +5 -4
  4. {corecoder-0.4.1 → corecoder-0.4.2}/article/00-index.md +1 -1
  5. {corecoder-0.4.1 → corecoder-0.4.2}/article/00-index_EN.md +1 -1
  6. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/__init__.py +1 -1
  7. corecoder-0.4.2/corecoder/checkpoints.py +38 -0
  8. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/cli.py +8 -0
  9. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/edit.py +2 -0
  10. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/write.py +2 -0
  11. {corecoder-0.4.1 → corecoder-0.4.2}/pyproject.toml +1 -1
  12. corecoder-0.4.2/tests/test_checkpoints.py +54 -0
  13. {corecoder-0.4.1 → corecoder-0.4.2}/tests/test_core.py +37 -3
  14. {corecoder-0.4.1 → corecoder-0.4.2}/.github/workflows/ci.yml +0 -0
  15. {corecoder-0.4.1 → corecoder-0.4.2}/.github/workflows/publish.yml +0 -0
  16. {corecoder-0.4.1 → corecoder-0.4.2}/.gitignore +0 -0
  17. {corecoder-0.4.1 → corecoder-0.4.2}/LICENSE +0 -0
  18. {corecoder-0.4.1 → corecoder-0.4.2}/article/01-the-loop.md +0 -0
  19. {corecoder-0.4.1 → corecoder-0.4.2}/article/01-the-loop_EN.md +0 -0
  20. {corecoder-0.4.1 → corecoder-0.4.2}/article/02-tools.md +0 -0
  21. {corecoder-0.4.1 → corecoder-0.4.2}/article/02-tools_EN.md +0 -0
  22. {corecoder-0.4.1 → corecoder-0.4.2}/article/03-llm-and-cost.md +0 -0
  23. {corecoder-0.4.1 → corecoder-0.4.2}/article/03-llm-and-cost_EN.md +0 -0
  24. {corecoder-0.4.1 → corecoder-0.4.2}/article/04-context.md +0 -0
  25. {corecoder-0.4.1 → corecoder-0.4.2}/article/04-context_EN.md +0 -0
  26. {corecoder-0.4.1 → corecoder-0.4.2}/article/05-parallel-and-subagents.md +0 -0
  27. {corecoder-0.4.1 → corecoder-0.4.2}/article/05-parallel-and-subagents_EN.md +0 -0
  28. {corecoder-0.4.1 → corecoder-0.4.2}/article/06-session-and-cli.md +0 -0
  29. {corecoder-0.4.1 → corecoder-0.4.2}/article/06-session-and-cli_EN.md +0 -0
  30. {corecoder-0.4.1 → corecoder-0.4.2}/article/07-build-your-own.md +0 -0
  31. {corecoder-0.4.1 → corecoder-0.4.2}/article/07-build-your-own_EN.md +0 -0
  32. {corecoder-0.4.1 → corecoder-0.4.2}/assets/demo.png +0 -0
  33. {corecoder-0.4.1 → corecoder-0.4.2}/assets/demo_en.png +0 -0
  34. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/__main__.py +0 -0
  35. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/agent.py +0 -0
  36. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/config.py +0 -0
  37. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/context.py +0 -0
  38. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/demo.py +0 -0
  39. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/llm.py +0 -0
  40. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/prompt.py +0 -0
  41. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/session.py +0 -0
  42. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/__init__.py +0 -0
  43. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/agent.py +0 -0
  44. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/base.py +0 -0
  45. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/bash.py +0 -0
  46. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/glob_tool.py +0 -0
  47. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/grep.py +0 -0
  48. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/read.py +0 -0
  49. {corecoder-0.4.1 → corecoder-0.4.2}/corecoder/tools/todo.py +0 -0
  50. {corecoder-0.4.1 → corecoder-0.4.2}/tests/__init__.py +0 -0
  51. {corecoder-0.4.1 → corecoder-0.4.2}/tests/test_demo.py +0 -0
  52. {corecoder-0.4.1 → corecoder-0.4.2}/tests/test_litellm.py +0 -0
  53. {corecoder-0.4.1 → corecoder-0.4.2}/tests/test_session.py +0 -0
  54. {corecoder-0.4.1 → corecoder-0.4.2}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: corecoder
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Minimal AI coding agent (~1,000 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
@@ -37,7 +37,7 @@ Description-Content-Type: text/markdown
37
37
 
38
38
  # CoreCoder
39
39
 
40
- **The nanoGPT of coding agents. 1,081 lines of pure Python — understand how a coding agent actually works, then fork your own.**
40
+ **The nanoGPT of coding agents. 1,201 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
  [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://python.org)
48
48
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
49
49
  [![Tests](https://github.com/he-yufeng/CoreCoder/actions/workflows/ci.yml/badge.svg)](https://github.com/he-yufeng/CoreCoder/actions)
50
- [![engine](https://img.shields.io/badge/engine-1081_LoC-blue)](article/00-index_EN.md)
50
+ [![engine](https://img.shields.io/badge/engine-1201_LoC-blue)](article/00-index_EN.md)
51
51
  [![essays](https://img.shields.io/badge/source--reading-8_bilingual-orange)](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,081 engine / 1,714 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
63
+ | Lines of code | ~1,201 engine / 2,008 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,7 +71,7 @@ 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,081 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 18 files: 1,714 physical lines, 1,385 net, every one short enough to read in a single sitting.
74
+ The engine (loop, model interface, context, tools, sessions) is 1,201 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 21 files: 2,008 physical lines, 1,614 net, every one short enough to read in a single sitting.
75
75
 
76
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. 103 tests, all green. 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
 
@@ -219,6 +219,7 @@ Inside the REPL, `/help` lists everything; these are the ones you'll reach for:
219
219
  /compact compact the context by hand
220
220
  /tokens token usage and cost estimate
221
221
  /diff files changed this session
222
+ /undo revert the most recent file change
222
223
  /save /sessions save / list sessions
223
224
  quit / exit exit (Ctrl+C cancels the current round)
224
225
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  # CoreCoder
4
4
 
5
- **The nanoGPT of coding agents. 1,081 lines of pure Python — understand how a coding agent actually works, then fork your own.**
5
+ **The nanoGPT of coding agents. 1,201 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
  [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://python.org)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
14
14
  [![Tests](https://github.com/he-yufeng/CoreCoder/actions/workflows/ci.yml/badge.svg)](https://github.com/he-yufeng/CoreCoder/actions)
15
- [![engine](https://img.shields.io/badge/engine-1081_LoC-blue)](article/00-index_EN.md)
15
+ [![engine](https://img.shields.io/badge/engine-1201_LoC-blue)](article/00-index_EN.md)
16
16
  [![essays](https://img.shields.io/badge/source--reading-8_bilingual-orange)](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,081 engine / 1,714 total | hundreds of thousands (closed) | tens of thousands of Python | ~600 (two files) |
28
+ | Lines of code | ~1,201 engine / 2,008 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,7 +36,7 @@ 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,081 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 18 files: 1,714 physical lines, 1,385 net, every one short enough to read in a single sitting.
39
+ The engine (loop, model interface, context, tools, sessions) is 1,201 lines once you drop blank lines and comments. Counting the outer CLI, config and packaging too, the whole package is 21 files: 2,008 physical lines, 1,614 net, every one short enough to read in a single sitting.
40
40
 
41
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. 103 tests, all green. 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
 
@@ -184,6 +184,7 @@ Inside the REPL, `/help` lists everything; these are the ones you'll reach for:
184
184
  /compact compact the context by hand
185
185
  /tokens token usage and cost estimate
186
186
  /diff files changed this session
187
+ /undo revert the most recent file change
187
188
  /save /sessions save / list sessions
188
189
  quit / exit exit (Ctrl+C cancels the current round)
189
190
  ```
@@ -2,7 +2,7 @@
2
2
 
3
3
  # CoreCoder
4
4
 
5
- **编程 agent 里的 nanoGPT。1081 行纯 Python,读懂一个 coding agent 到底怎么运作,再 fork 出你自己的。**
5
+ **编程 agent 里的 nanoGPT。1201 行纯 Python,读懂一个 coding agent 到底怎么运作,再 fork 出你自己的。**
6
6
 
7
7
  *learn from it · fork it · ship something better*
8
8
 
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://python.org)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
14
14
  [![Tests](https://github.com/he-yufeng/CoreCoder/actions/workflows/ci.yml/badge.svg)](https://github.com/he-yufeng/CoreCoder/actions)
15
- [![engine](https://img.shields.io/badge/engine-1081_LoC-blue)](article/)
15
+ [![engine](https://img.shields.io/badge/engine-1201_LoC-blue)](article/)
16
16
  [![源码导读](https://img.shields.io/badge/源码导读-8篇双语-orange)](article/)
17
17
 
18
18
  </div>
@@ -25,7 +25,7 @@
25
25
 
26
26
  | | CoreCoder | Claude Code | aider | nanoGPT |
27
27
  |---|---|---|---|---|
28
- | 代码量 | 引擎约 1081 行 / 整包 1714 行 | 几十万行(闭源) | 数万行 Python | 约 600 行(两个文件) |
28
+ | 代码量 | 引擎约 1201 行 / 整包 2008 行 | 几十万行(闭源) | 数万行 Python | 约 600 行(两个文件) |
29
29
  | 读完要多久 | 一个下午 | 读不了(闭源) | 得啃几天 | 一个下午 |
30
30
  | 能不能下断点改了再跑 | 能,每一行 | 不能 | 能,但量大 | 能 |
31
31
  | 定位 | 读懂并 fork 出你自己的 agent | 生产级编程助手 | 终端结对编程 | 教学用最小 GPT |
@@ -36,7 +36,7 @@ nanoGPT 那一列是拿来对照的:它最小、可读,但教的是训一个
36
36
 
37
37
  我一直觉得 coding agent 被讲得太玄了。把 Claude Code、Cursor 这类工具扒到底,核心是一个 while 循环套着一个大模型,外加七八个让它能真正动手的工具。难的从来不是这个循环,而是循环跑进真实世界以后要兜的那些底。CoreCoder 就是把这个核心老老实实写出来的最小版本。
38
38
 
39
- 引擎部分(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1081 行。连最外层的 CLI、配置、打包一起算,整个包 18 个文件、物理 1714 行、净 1385 行,每个文件都短到能一口气读完。
39
+ 引擎部分(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1201 行。连最外层的 CLI、配置、打包一起算,整个包 21 个文件、物理 2008 行、净 1614 行,每个文件都短到能一口气读完。
40
40
 
41
41
  它真能跑:读写文件、执行 shell、派子 agent、分三层压上下文,还能随时把这趟烧掉的 token 和美元数报给你,103 个测试是绿的。但能跑不是为了劝你拿去日用,而是为了让这份「注释」不撒谎:一个解释 agent 怎么运作的范例,自己得真能运作。
42
42
 
@@ -184,6 +184,7 @@ README 只给方向,每条的代码细节第七篇接着讲。挑一个动手
184
184
  /compact 手动压缩上下文
185
185
  /tokens 查看 token 用量和费用估算
186
186
  /diff 查看本次会话改过的文件
187
+ /undo 撤销最近一次文件改动
187
188
  /save /sessions 保存 / 列出会话
188
189
  quit / exit 退出(Ctrl+C 取消当前回合)
189
190
  ```
@@ -8,7 +8,7 @@
8
8
 
9
9
  因为 Claude Code 本体太大了。它的代码量在几十万行的量级,光一个负责跑 shell 命令的 BashTool 就上千行,真要逐行读完,多数人会在第三个文件就关掉编辑器。可 agent 的核心其实没那么复杂,复杂的是工程化之后那些为真实世界兜底的边角:模型半路被打断怎么办、上下文塞满了怎么办、十个工具调用同时回来怎么办、provider 偶尔抽风返回 500 又怎么办。这些东西摊在几十万行里,你很难一眼看到骨架。
10
10
 
11
- CoreCoder 做的事,是把这套骨架压到一千行出头的纯 Python。准确说,agent 引擎(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1081 行;连最外层的 CLI 终端、配置和打包都算上,整个包去掉空行和注释 1385 行,物理代码 1714 行。每个文件都短到能一口气读完,跑起来又确实是个能改代码、能跑命令、能自己开子任务、能压缩上下文、能算钱的 agent。它不是玩具。它是把生产级 agent 的每个设计决策,拣出最核心的那版,用最少的代码诚实地写一遍。
11
+ CoreCoder 做的事,是把这套骨架压到一千行出头的纯 Python。准确说,agent 引擎(循环、模型接口、上下文、工具、会话)去掉空行和注释是 1201 行;连最外层的 CLI 终端、配置和打包都算上,整个包去掉空行和注释 1614 行,物理代码 2008 行。每个文件都短到能一口气读完,跑起来又确实是个能改代码、能跑命令、能自己开子任务、能压缩上下文、能算钱的 agent。它不是玩具。它是把生产级 agent 的每个设计决策,拣出最核心的那版,用最少的代码诚实地写一遍。
12
12
 
13
13
  读它,约等于读一份 Claude Code 的「可运行注释版」。区别在于,CoreCoder 的每一行你都能在自己机器上断点、改、跑、看它出什么效果。本系列里我引用的每一个行数、每一段代码,都是从仓库里现读现核的,不是凭印象。这点我后面会反复较真,因为编码 agent 这个领域,太多文章在凭感觉编数字。
14
14
 
@@ -8,7 +8,7 @@ First, use CoreCoder, an open source project whose core runs to just over a thou
8
8
 
9
9
  Because Claude Code itself is too big. Its codebase runs into the hundreds of thousands of lines, and the BashTool alone, the part that runs shell commands, is over a thousand. Read it line by line and most people close the editor by the third file. Yet the core of an agent isn't really that complicated. What's complicated is everything engineered around it to survive the real world: what to do when the model gets interrupted mid-stream, when the context window fills up, when ten tool calls come back at once, when a provider hiccups and returns a 500. Spread across hundreds of thousands of lines, that skeleton is hard to see.
10
10
 
11
- What CoreCoder does is squeeze the skeleton down to just over a thousand lines of pure Python. To be precise, the agent engine (the loop, model interface, context, tools, session) is 1081 lines once you drop blank lines and comments; add the outermost CLI terminal, config, and packaging and the whole package is 1385 lines without blanks and comments, 1714 physical. Every file is short enough to read in one sitting, and what runs is genuinely an agent: it edits code, runs commands, spins up its own subtasks, compresses context, tracks cost. It is not a toy. It takes every design decision a production agent makes, picks the most essential version of each, and writes it out honestly in the least code it can.
11
+ What CoreCoder does is squeeze the skeleton down to just over a thousand lines of pure Python. To be precise, the agent engine (the loop, model interface, context, tools, session) is 1201 lines once you drop blank lines and comments; add the outermost CLI terminal, config, and packaging and the whole package is 1614 lines without blanks and comments, 2008 physical. Every file is short enough to read in one sitting, and what runs is genuinely an agent: it edits code, runs commands, spins up its own subtasks, compresses context, tracks cost. It is not a toy. It takes every design decision a production agent makes, picks the most essential version of each, and writes it out honestly in the least code it can.
12
12
 
13
13
  Reading it is close to reading a runnable, annotated edition of Claude Code. The difference is that every line of CoreCoder is something you can breakpoint, change, run, and watch on your own machine. Every line count and every snippet I quote in this series is read straight out of the repository, not recalled from memory. I'll keep being pedantic about that, because in the coding-agent space far too many write-ups just make the numbers up.
14
14
 
@@ -1,6 +1,6 @@
1
1
  """CoreCoder - Minimal AI coding agent inspired by Claude Code's architecture."""
2
2
 
3
- __version__ = "0.4.1"
3
+ __version__ = "0.4.2"
4
4
 
5
5
  from corecoder.agent import Agent
6
6
  from corecoder.config import Config
@@ -0,0 +1,38 @@
1
+ """Session-scoped undo for file mutations.
2
+
3
+ edit_file and write_file record a checkpoint before touching a file; /undo
4
+ pops the latest one and restores the previous bytes (or removes the file if
5
+ it did not exist). In-memory only: undo history dies with the process, and
6
+ bash side effects are not tracked, only the two file-writing tools.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ # (path, prior bytes or None if the file did not exist)
12
+ _stack: list[tuple[str, bytes | None]] = []
13
+
14
+
15
+ def record(path: Path) -> None:
16
+ """Capture the pre-mutation state of path. Call right before writing."""
17
+ _stack.append((str(path), path.read_bytes() if path.exists() else None))
18
+
19
+
20
+ def undo() -> str:
21
+ """Restore the most recent checkpoint."""
22
+ if not _stack:
23
+ return "Nothing to undo."
24
+ path_str, prior = _stack.pop()
25
+ p = Path(path_str)
26
+ if prior is None:
27
+ p.unlink(missing_ok=True)
28
+ return f"Removed {path_str} (created this session)."
29
+ p.write_bytes(prior)
30
+ return f"Restored {path_str}."
31
+
32
+
33
+ def pending() -> int:
34
+ return len(_stack)
35
+
36
+
37
+ def clear() -> None:
38
+ _stack.clear()
@@ -213,6 +213,13 @@ def _repl(agent: Agent, config: Config):
213
213
  for f in sorted(_changed_files):
214
214
  console.print(f" [cyan]{f}[/cyan]")
215
215
  continue
216
+ if user_input == "/undo":
217
+ from .checkpoints import pending, undo
218
+ console.print(undo())
219
+ left = pending()
220
+ if left:
221
+ console.print(f"[dim]{left} more checkpoint(s) on the stack.[/dim]")
222
+ continue
216
223
  if user_input == "/sessions":
217
224
  sessions = list_sessions()
218
225
  if not sessions:
@@ -261,6 +268,7 @@ def _show_help():
261
268
  " /tokens Show token usage\n"
262
269
  " /compact Compress conversation context\n"
263
270
  " /diff Show files modified this session\n"
271
+ " /undo Revert the most recent file change\n"
264
272
  " /save Save session to disk\n"
265
273
  " /sessions List saved sessions\n"
266
274
  " quit Exit CoreCoder\n"
@@ -10,6 +10,7 @@ import difflib
10
10
  from pathlib import Path
11
11
  from typing import ClassVar
12
12
 
13
+ from ..checkpoints import record as _record_checkpoint
13
14
  from .base import Tool
14
15
 
15
16
  # track files changed this session for /diff
@@ -67,6 +68,7 @@ class EditFileTool(Tool):
67
68
  )
68
69
 
69
70
  new_content = content.replace(old_string, new_string, 1)
71
+ _record_checkpoint(p)
70
72
  p.write_text(new_content, encoding="utf-8")
71
73
  _changed_files.add(str(p))
72
74
 
@@ -3,6 +3,7 @@
3
3
  from pathlib import Path
4
4
  from typing import ClassVar
5
5
 
6
+ from ..checkpoints import record as _record_checkpoint
6
7
  from .base import Tool
7
8
  from .edit import _changed_files
8
9
 
@@ -32,6 +33,7 @@ class WriteFileTool(Tool):
32
33
  try:
33
34
  p = Path(file_path).expanduser().resolve()
34
35
  p.parent.mkdir(parents=True, exist_ok=True)
36
+ _record_checkpoint(p)
35
37
  p.write_text(content, encoding="utf-8")
36
38
  _changed_files.add(str(p))
37
39
  n_lines = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "corecoder"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Minimal AI coding agent (~1,000 lines of Python) inspired by Claude Code. Works with any LLM. (formerly NanoCoder)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,54 @@
1
+ """Undo checkpoints for edit_file / write_file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from corecoder import checkpoints
6
+ from corecoder.tools.edit import EditFileTool
7
+ from corecoder.tools.write import WriteFileTool
8
+
9
+
10
+ def setup_function():
11
+ checkpoints.clear()
12
+
13
+
14
+ def test_edit_then_undo_restores_previous_bytes(tmp_path):
15
+ f = tmp_path / "a.py"
16
+ f.write_text("v1\n", encoding="utf-8")
17
+ assert EditFileTool().execute(str(f), "v1", "v2").startswith("Edited")
18
+ assert f.read_text() == "v2\n"
19
+
20
+ assert checkpoints.undo() == f"Restored {f}."
21
+ assert f.read_text() == "v1\n"
22
+
23
+
24
+ def test_undo_removes_file_created_by_write(tmp_path):
25
+ f = tmp_path / "new.py"
26
+ assert not f.exists()
27
+ WriteFileTool().execute(str(f), "print(1)\n")
28
+ assert f.exists()
29
+
30
+ assert checkpoints.undo() == f"Removed {f} (created this session)."
31
+ assert not f.exists()
32
+
33
+
34
+ def test_undo_pops_one_mutation_at_a_time(tmp_path):
35
+ f = tmp_path / "a.py"
36
+ f.write_text("v1\n", encoding="utf-8")
37
+ EditFileTool().execute(str(f), "v1", "v2")
38
+ EditFileTool().execute(str(f), "v2", "v3")
39
+
40
+ assert checkpoints.pending() == 2
41
+ checkpoints.undo()
42
+ assert f.read_text() == "v2\n"
43
+ checkpoints.undo()
44
+ assert f.read_text() == "v1\n"
45
+
46
+
47
+ def test_failed_edit_leaves_no_checkpoint(tmp_path):
48
+ f = tmp_path / "a.py"
49
+ f.write_text("v1\n", encoding="utf-8")
50
+ # old_string absent -> error before any write
51
+ result = EditFileTool().execute(str(f), "missing", "v2")
52
+ assert result.startswith("Error:")
53
+ assert checkpoints.pending() == 0
54
+ assert checkpoints.undo() == "Nothing to undo."
@@ -1,6 +1,6 @@
1
1
  """Tests for core modules: config, context, session, imports."""
2
2
 
3
- import tomllib
3
+ import re
4
4
  from pathlib import Path
5
5
  from typing import ClassVar
6
6
 
@@ -12,8 +12,42 @@ from corecoder.tools import get_tool
12
12
 
13
13
 
14
14
  def test_version():
15
- pyproject = tomllib.loads(Path("pyproject.toml").read_text())
16
- assert __version__ == pyproject["project"]["version"]
15
+ # regex instead of tomllib: the latter only exists on 3.11+ and CI runs 3.10
16
+ m = re.search(r'(?m)^version = "([^"]+)"', Path("pyproject.toml").read_text())
17
+ assert m is not None
18
+ assert __version__ == m.group(1)
19
+
20
+
21
+ def test_readme_line_counts_are_current():
22
+ # The LoC numbers are the brand of this repo. If the engine or the
23
+ # package grows, the README has to move with it, and this test is the
24
+ # alarm: update the badge and the prose in README.md and README_CN.md.
25
+ root = Path(__file__).resolve().parent.parent
26
+ engine_files = [
27
+ root / "corecoder" / name
28
+ for name in ("agent.py", "llm.py", "context.py", "session.py")
29
+ ]
30
+ engine_files += sorted((root / "corecoder" / "tools").glob("*.py"))
31
+ package_files = sorted((root / "corecoder").rglob("*.py"))
32
+
33
+ def net_lines(path: Path) -> int:
34
+ return sum(
35
+ 1
36
+ for line in path.read_text(encoding="utf-8").splitlines()
37
+ if line.strip() and not line.strip().startswith("#")
38
+ )
39
+
40
+ engine = sum(net_lines(f) for f in engine_files)
41
+ physical = sum(
42
+ len(f.read_text(encoding="utf-8").splitlines()) for f in package_files
43
+ )
44
+ package_net = sum(net_lines(f) for f in package_files)
45
+
46
+ readme = (root / "README.md").read_text(encoding="utf-8")
47
+ assert f"engine-{engine}_LoC" in readme
48
+ assert f"{len(package_files)} files" in readme
49
+ assert f"{physical:,} physical lines" in readme
50
+ assert f"{package_net:,} net" in readme
17
51
 
18
52
 
19
53
  def test_public_api_exports():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes