Skip to content

Commit 7744648

Browse files
anthhubclaude
andcommitted
feat: Chapter 6 — Complete file operation tools
Demo code: - Add tools/FileWriteTool/ with auto directory creation, write stats - Add tools/FileEditTool/ with exact string replacement, uniqueness check (matches real Claude Code's Edit approach over unified diff) - Add tools/GlobTool/ using Bun.Glob with 1000 result limit - Register all 3 in tools.ts — now 7 tools total: Read-only (concurrent): Echo, Read, Grep, Glob Read-write (serial): Bash, Write, Edit Documentation: - Add "Hands-on: Complete File Operation Tools" to Ch6 (zh-CN/en) - Explain why Edit uses string replacement over diff Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1e0ec85 commit 7744648

6 files changed

Lines changed: 376 additions & 1 deletion

File tree

demo/tools.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import { buildTool } from "./Tool.js";
1818
// ─── 从独立模块导入增强版工具 ──────────────────────────────────────────────────
1919
import { BashTool } from "./tools/BashTool/index.js";
2020
import { FileReadTool } from "./tools/FileReadTool/index.js";
21+
import { FileWriteTool } from "./tools/FileWriteTool/index.js";
22+
import { FileEditTool } from "./tools/FileEditTool/index.js";
2123
import { GrepTool } from "./tools/GrepTool/index.js";
24+
import { GlobTool } from "./tools/GlobTool/index.js";
2225

2326
// ─── 内联工具定义 ──────────────────────────────────────────────────────────────
2427

@@ -56,8 +59,11 @@ const EchoTool = buildTool({
5659
export const allTools: Tool[] = [
5760
EchoTool,
5861
FileReadTool,
62+
FileWriteTool,
63+
FileEditTool,
5964
BashTool,
6065
GrepTool,
66+
GlobTool,
6167
];
6268

6369
/**
@@ -81,4 +87,4 @@ export function getToolsForAPI(): APIToolDefinition[] {
8187
}
8288

8389
// 导出各个工具(供直接引用或测试)
84-
export { EchoTool, FileReadTool, BashTool, GrepTool };
90+
export { EchoTool, FileReadTool, FileWriteTool, FileEditTool, BashTool, GrepTool, GlobTool };

demo/tools/FileEditTool/index.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* tools/FileEditTool/index.ts - 精确编辑工具
3+
*
4+
* 对应真实 Claude Code: src/tools/FileEditTool/
5+
* 核心思路:通过 old_string -> new_string 精确替换,
6+
* 而非整文件重写。这样 AI 只需发送 diff,节省 token。
7+
*
8+
* 真实版本还包含:模糊匹配、缩进修正、冲突检测、
9+
* 多处替换确认等。
10+
*/
11+
12+
import { buildTool } from "../../Tool.js";
13+
14+
export const FileEditTool = buildTool({
15+
name: "Edit",
16+
description: "通过精确字符串替换编辑文件。指定 old_string(要替换的内容)和 new_string(替换后的内容)。old_string 必须在文件中唯一匹配。",
17+
inputSchema: {
18+
type: "object",
19+
properties: {
20+
file_path: { type: "string", description: "文件的绝对路径" },
21+
old_string: { type: "string", description: "要被替换的原始文本(必须唯一匹配)" },
22+
new_string: { type: "string", description: "替换后的新文本" },
23+
},
24+
required: ["file_path", "old_string", "new_string"],
25+
},
26+
isReadOnly: false,
27+
async call(input) {
28+
const filePath = String(input.file_path);
29+
const oldString = String(input.old_string);
30+
const newString = String(input.new_string);
31+
32+
try {
33+
const file = Bun.file(filePath);
34+
const exists = await file.exists();
35+
if (!exists) {
36+
return { content: `Error: File not found: ${filePath}`, isError: true };
37+
}
38+
39+
const content = await file.text();
40+
41+
// 检查 old_string 是否存在
42+
const index = content.indexOf(oldString);
43+
if (index === -1) {
44+
return {
45+
content: `Error: old_string not found in ${filePath}. Make sure the string matches exactly (including whitespace and indentation).`,
46+
isError: true,
47+
};
48+
}
49+
50+
// 检查 old_string 是否唯一
51+
const secondIndex = content.indexOf(oldString, index + 1);
52+
if (secondIndex !== -1) {
53+
return {
54+
content: `Error: old_string matches multiple locations in ${filePath}. Provide more surrounding context to make it unique.`,
55+
isError: true,
56+
};
57+
}
58+
59+
// 执行替换
60+
const newContent = content.substring(0, index) + newString + content.substring(index + oldString.length);
61+
await Bun.write(filePath, newContent);
62+
63+
// 计算变更统计
64+
const oldLines = oldString.split("\n").length;
65+
const newLines = newString.split("\n").length;
66+
const diffLines = newLines - oldLines;
67+
const diffStr = diffLines > 0 ? `+${diffLines}` : diffLines < 0 ? `${diffLines}` : "±0";
68+
69+
return {
70+
content: `Edited ${filePath}: replaced ${oldLines} lines with ${newLines} lines (${diffStr} lines)`,
71+
};
72+
} catch (e) {
73+
return { content: `Error editing file: ${e}`, isError: true };
74+
}
75+
},
76+
});

demo/tools/FileWriteTool/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* tools/FileWriteTool/index.ts - 文件写入工具
3+
*
4+
* 对应真实 Claude Code: src/tools/FileWriteTool/
5+
* 真实版本还包含:文件备份、权限检查、符号链接安全、
6+
* 大文件警告等。我们实现核心写入功能。
7+
*/
8+
9+
import { buildTool } from "../../Tool.js";
10+
import { existsSync, mkdirSync } from "fs";
11+
import { dirname } from "path";
12+
13+
export const FileWriteTool = buildTool({
14+
name: "Write",
15+
description: "将内容写入指定文件。如果文件不存在会自动创建(包括中间目录)。如果文件已存在会覆盖。",
16+
inputSchema: {
17+
type: "object",
18+
properties: {
19+
file_path: { type: "string", description: "文件的绝对路径" },
20+
content: { type: "string", description: "要写入的完整文件内容" },
21+
},
22+
required: ["file_path", "content"],
23+
},
24+
isReadOnly: false,
25+
async call(input) {
26+
const filePath = String(input.file_path);
27+
const content = String(input.content);
28+
29+
try {
30+
// 确保目录存在
31+
const dir = dirname(filePath);
32+
if (!existsSync(dir)) {
33+
mkdirSync(dir, { recursive: true });
34+
}
35+
36+
const existed = existsSync(filePath);
37+
await Bun.write(filePath, content);
38+
39+
const lines = content.split("\n").length;
40+
const bytes = new TextEncoder().encode(content).length;
41+
const action = existed ? "Updated" : "Created";
42+
43+
return {
44+
content: `${action} ${filePath} (${lines} lines, ${bytes} bytes)`,
45+
};
46+
} catch (e) {
47+
return { content: `Error writing file: ${e}`, isError: true };
48+
}
49+
},
50+
});

demo/tools/GlobTool/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* tools/GlobTool/index.ts - 文件路径匹配工具
3+
*
4+
* 对应真实 Claude Code: src/tools/GlobTool/
5+
* 用于快速发现文件,如 "**\/*.ts" 找到所有 TypeScript 文件。
6+
* 使用 Bun.Glob 实现。
7+
*/
8+
9+
import { buildTool } from "../../Tool.js";
10+
import { Glob } from "bun";
11+
12+
export const GlobTool = buildTool({
13+
name: "Glob",
14+
description: "使用 glob 模式查找匹配的文件路径。如 '**/*.ts' 查找所有 TypeScript 文件。结果按路径排序。",
15+
inputSchema: {
16+
type: "object",
17+
properties: {
18+
pattern: { type: "string", description: "Glob 模式,如 '**/*.ts'、'src/**/*.js'" },
19+
path: { type: "string", description: "搜索的根目录,默认当前目录" },
20+
},
21+
required: ["pattern"],
22+
},
23+
isReadOnly: true,
24+
async call(input) {
25+
const pattern = String(input.pattern);
26+
const searchPath = String(input.path ?? ".");
27+
28+
try {
29+
const glob = new Glob(pattern);
30+
const matches: string[] = [];
31+
32+
for await (const file of glob.scan({ cwd: searchPath, dot: false })) {
33+
matches.push(file);
34+
if (matches.length >= 1000) break; // 限制结果数量
35+
}
36+
37+
matches.sort();
38+
39+
if (matches.length === 0) {
40+
return { content: "No files matched the pattern." };
41+
}
42+
43+
let content = matches.join("\n");
44+
if (matches.length >= 1000) {
45+
content += "\n... [results limited to 1000 files]";
46+
}
47+
48+
return { content: `Found ${matches.length} files:\n${content}` };
49+
} catch (e) {
50+
return { content: `Error: ${e}`, isError: true };
51+
}
52+
},
53+
});

docs/en/06-service-layer.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
7. [Analytics Service](#analytics-service)
1212
8. [Hands-on: Streaming API Client](#hands-on-streaming-api-client)
1313
9. [Key Takeaways & What's Next](#key-takeaways--whats-next)
14+
10. [Hands-on: Complete File Operation Tools](#hands-on-complete-file-operation-tools)
1415

1516
---
1617

@@ -721,4 +722,98 @@ Chapter 7 covers the **Permission System** — how Claude Code decides whether a
721722

722723
---
723724

725+
## Hands-on: Complete File Operation Tools
726+
727+
> **This section is another major upgrade to the demo.** We add three file operation tools — FileWriteTool, FileEditTool, and GlobTool — giving mini-claude full file read/write and search capabilities.
728+
729+
### Project Structure Update
730+
731+
```
732+
demo/
733+
├── tools/
734+
│ ├── BashTool/
735+
│ │ └── index.ts
736+
│ ├── FileReadTool/
737+
│ │ └── index.ts
738+
│ ├── GrepTool/
739+
│ │ └── index.ts
740+
│ ├── FileWriteTool/
741+
│ │ └── index.ts # ← New: create/overwrite files
742+
│ ├── FileEditTool/
743+
│ │ └── index.ts # ← New: precise string replacement
744+
│ └── GlobTool/
745+
│ └── index.ts # ← New: file path matching
746+
├── tools.ts # Updated: register new tools
747+
├── query.ts
748+
├── main.ts
749+
├── Tool.ts
750+
├── context.ts
751+
├── services/api/
752+
├── utils/
753+
│ └── messages.ts
754+
└── types/
755+
```
756+
757+
### Three New Tools Explained
758+
759+
| Tool | Function | Key Design |
760+
|------|----------|------------|
761+
| FileWriteTool (Write) | Create/overwrite files | Auto-creates intermediate directories; reports line count and byte size |
762+
| FileEditTool (Edit) | Precise string replacement | old_string must match uniquely; saves tokens compared to full file rewrite |
763+
| GlobTool (Glob) | File path matching | Uses Bun.Glob; results capped at 1000 |
764+
765+
**FileWriteTool** accepts `file_path` and `content` parameters, writing content to the specified path. If parent directories don't exist, they are created recursively (`mkdir -p` semantics). After writing, it returns line count and byte size so the model can confirm the operation result.
766+
767+
**FileEditTool** accepts `file_path`, `old_string`, and `new_string` parameters, finding `old_string` in the file and replacing it with `new_string`. The core constraint is that **old_string must match uniquely in the file** — if it matches zero times or multiple times, the tool returns an error. This design prevents replacements at wrong locations.
768+
769+
**GlobTool** accepts `pattern` and an optional `path` parameter, matching file paths using glob patterns. It uses the `Bun.Glob` API internally, with results capped at 1000 files to prevent excessive token consumption.
770+
771+
### Why String Replacement Instead of Diff?
772+
773+
This is a design choice worth reflecting on. Traditional file editing tools might use unified diff format, but Claude Code chose the old_string → new_string string replacement approach for several reasons:
774+
775+
- **AI generates precise old_string → new_string more reliably than unified diff** — the model only needs to copy the original text to modify and write the replacement, without computing correct line numbers and hunk headers
776+
- **Unique match checking prevents edits at wrong locations** — if old_string appears multiple times in the file, the tool refuses to execute and returns an error, forcing the model to provide more context for precise targeting
777+
- **Real Claude Code uses the same approach** — this isn't a simplification, but a production-validated optimal solution
778+
779+
### Tool Landscape
780+
781+
mini-claude now has 7 tools, categorized by read/write characteristics:
782+
783+
```
784+
Read-only tools (can run concurrently): Echo, Read, Grep, Glob
785+
Read-write tools (must run serially): Bash, Write, Edit
786+
```
787+
788+
Read-only tools don't modify filesystem state and can safely run concurrently. Read-write tools may change the filesystem and must run serially to avoid race conditions. This classification becomes critical in Chapter 7's permission system — read-write tools require user confirmation, while read-only tools can execute automatically.
789+
790+
### Running the Demo
791+
792+
```bash
793+
cd demo && bun run main.ts
794+
```
795+
796+
Try these interactions to verify the new tools:
797+
798+
```
799+
you> Create a hello.txt file with content "Hello, World!"
800+
you> Change "World" to "Claude" in hello.txt
801+
you> Find all .ts files in the current directory
802+
```
803+
804+
### Mapping to Real Claude Code
805+
806+
| Demo File | Real File | What's Simplified |
807+
|-----------|-----------|-------------------|
808+
| `tools/FileWriteTool/index.ts` | `src/tools/FileWriteTool/` | No permission checks, no symlink protection |
809+
| `tools/FileEditTool/index.ts` | `src/tools/FileEditTool/` | No replace_all mode, no syntax validation |
810+
| `tools/GlobTool/index.ts` | `src/tools/GlobTool/` | No gitignore filtering, no sort-by-mtime |
811+
| `tools.ts` (registers 7 tools) | `src/tools.ts` | No lazy loading, no feature flag filtering |
812+
813+
### Next Chapter Preview
814+
815+
Chapter 7 will implement the permission system, ensuring dangerous operations (like `rm -rf`, writing to system files) require user confirmation. Read-write tools will be strictly controlled, while read-only tools can execute freely.
816+
817+
---
818+
724819
*Source references: `src/services/api/client.ts`, `src/services/api/withRetry.ts`, `src/QueryEngine.ts`, `src/query.ts`, `src/services/compact/compact.ts`, `src/services/compact/autoCompact.ts`, `src/services/tokenEstimation.ts`, `src/cost-tracker.ts`, `src/services/analytics/index.ts`, `src/services/analytics/growthbook.ts`*

0 commit comments

Comments
 (0)