Skip to content

Commit 1e0ec85

Browse files
anthhubclaude
andcommitted
feat: Chapter 5 — Standalone tool modules with enhanced features
Demo code: - Add tools/BashTool/ with timeout, output truncation (50KB limit) - Add tools/FileReadTool/ with line numbers, offset/limit params - Add tools/GrepTool/ with file type filtering, result limiting - Refactor tools.ts to import from standalone modules - EchoTool remains inline (teaching purpose) Each tool is now a self-contained module matching real Claude Code's tools/ directory pattern — independent development, testing, maintenance. Documentation: - Add "Hands-on: Standalone Tool Modules" to Ch5 (zh-CN and en) - Explain self-contained module pattern and enhancement details Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d1eba46 commit 1e0ec85

6 files changed

Lines changed: 382 additions & 109 deletions

File tree

demo/tools.ts

Lines changed: 11 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,26 @@
77
* QueryEngine 在调用 API 时,将注册表中的工具转换为 API 参数格式;
88
* 收到 tool_use 响应时,根据名称在注册表中查找并执行工具。
99
*
10-
* 真实 Claude Code 在此文件中导入 40+ 个工具
11-
* 并通过 feature() 宏条件加载。我们先注册几个示例工具
10+
* 第 5 章:工具已迁移到独立的 tools/ 目录中
11+
* 每个工具一个子目录,支持更丰富的参数和更好的错误处理
1212
*/
1313

1414
import type { Tool, APIToolDefinition } from "./types/index.js";
1515
import { toolToAPIFormat } from "./types/index.js";
1616
import { buildTool } from "./Tool.js";
1717

18-
// ─── 内置工具定义 ──────────────────────────────────────────────────────────
19-
// 第 5 章会将这些移到独立的 tools/ 目录中
20-
// 目前先在这里定义简单版本,验证注册表机制
18+
// ─── 从独立模块导入增强版工具 ──────────────────────────────────────────────────
19+
import { BashTool } from "./tools/BashTool/index.js";
20+
import { FileReadTool } from "./tools/FileReadTool/index.js";
21+
import { GrepTool } from "./tools/GrepTool/index.js";
22+
23+
// ─── 内联工具定义 ──────────────────────────────────────────────────────────────
2124

2225
/**
2326
* EchoTool - 最简单的工具,用于验证工具系统
2427
*
2528
* 这不是真实 Claude Code 中的工具,只是教学用的 hello world
29+
* 保留内联定义,因为它只用于教学演示。
2630
*/
2731
const EchoTool = buildTool({
2832
name: "Echo",
@@ -40,108 +44,6 @@ const EchoTool = buildTool({
4044
},
4145
});
4246

43-
/**
44-
* ReadTool - 读取文件内容(简化版)
45-
*
46-
* 对应真实 Claude Code: src/tools/FileReadTool/
47-
* 第 5 章会实现完整版本(支持行号范围、二进制文件检测等)
48-
*/
49-
const ReadTool = buildTool({
50-
name: "Read",
51-
description: "读取指定文件的内容",
52-
inputSchema: {
53-
type: "object",
54-
properties: {
55-
file_path: { type: "string", description: "文件的绝对路径" },
56-
},
57-
required: ["file_path"],
58-
},
59-
isReadOnly: true,
60-
async call(input) {
61-
try {
62-
const filePath = String(input.file_path);
63-
const file = Bun.file(filePath);
64-
const content = await file.text();
65-
return { content };
66-
} catch (e) {
67-
return { content: `Error reading file: ${e}`, isError: true };
68-
}
69-
},
70-
});
71-
72-
/**
73-
* BashTool - 执行 shell 命令(简化版)
74-
*
75-
* 对应真实 Claude Code: src/tools/BashTool/
76-
* 第 5 章会实现完整版本(支持超时、工作目录、信号处理等)
77-
*/
78-
const BashTool = buildTool({
79-
name: "Bash",
80-
description: "执行 shell 命令并返回输出",
81-
inputSchema: {
82-
type: "object",
83-
properties: {
84-
command: { type: "string", description: "要执行的 shell 命令" },
85-
},
86-
required: ["command"],
87-
},
88-
isReadOnly: false, // shell 命令可能有副作用
89-
async call(input) {
90-
try {
91-
const proc = Bun.spawn(["sh", "-c", String(input.command)], {
92-
stdout: "pipe",
93-
stderr: "pipe",
94-
});
95-
const stdout = await new Response(proc.stdout).text();
96-
const stderr = await new Response(proc.stderr).text();
97-
const exitCode = await proc.exited;
98-
99-
let content = stdout;
100-
if (stderr) content += `\nSTDERR:\n${stderr}`;
101-
if (exitCode !== 0) content += `\nExit code: ${exitCode}`;
102-
103-
return { content, isError: exitCode !== 0 };
104-
} catch (e) {
105-
return { content: `Error executing command: ${e}`, isError: true };
106-
}
107-
},
108-
});
109-
110-
/**
111-
* GrepTool - 搜索文件内容(简化版)
112-
*
113-
* 对应真实 Claude Code: src/tools/GrepTool/
114-
* 底层使用 ripgrep,这里简化为调用 grep 命令
115-
*/
116-
const GrepTool = buildTool({
117-
name: "Grep",
118-
description: "在文件中搜索匹配的文本模式",
119-
inputSchema: {
120-
type: "object",
121-
properties: {
122-
pattern: { type: "string", description: "搜索的正则表达式" },
123-
path: { type: "string", description: "搜索的目录或文件路径" },
124-
},
125-
required: ["pattern"],
126-
},
127-
isReadOnly: true,
128-
async call(input) {
129-
try {
130-
const pattern = String(input.pattern);
131-
const searchPath = String(input.path ?? ".");
132-
const proc = Bun.spawn(
133-
["grep", "-rn", "--include=*.ts", "--include=*.js", pattern, searchPath],
134-
{ stdout: "pipe", stderr: "pipe" }
135-
);
136-
const stdout = await new Response(proc.stdout).text();
137-
await proc.exited;
138-
return { content: stdout || "No matches found." };
139-
} catch (e) {
140-
return { content: `Error: ${e}`, isError: true };
141-
}
142-
},
143-
});
144-
14547
// ─── 工具注册表 ──────────────────────────────────────────────────────────────
14648

14749
/**
@@ -153,7 +55,7 @@ const GrepTool = buildTool({
15355
*/
15456
export const allTools: Tool[] = [
15557
EchoTool,
156-
ReadTool,
58+
FileReadTool,
15759
BashTool,
15860
GrepTool,
15961
];
@@ -179,4 +81,4 @@ export function getToolsForAPI(): APIToolDefinition[] {
17981
}
18082

18183
// 导出各个工具(供直接引用或测试)
182-
export { EchoTool, ReadTool, BashTool, GrepTool };
84+
export { EchoTool, FileReadTool, BashTool, GrepTool };

demo/tools/BashTool/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* tools/BashTool/index.ts - Shell 命令执行工具
3+
*
4+
* 对应真实 Claude Code: src/tools/BashTool/
5+
* 真实版本还包含:命令安全分析、工作目录切换、信号处理、
6+
* 进程组管理、输出截断策略等。我们实现核心功能。
7+
*/
8+
9+
import { buildTool } from "../../Tool.js";
10+
11+
const MAX_OUTPUT_LENGTH = 50000; // 50KB 输出限制
12+
13+
export const BashTool = buildTool({
14+
name: "Bash",
15+
description: "在 shell 中执行命令并返回输出。用于运行测试、安装依赖、查看文件列表等。",
16+
inputSchema: {
17+
type: "object",
18+
properties: {
19+
command: { type: "string", description: "要执行的 shell 命令" },
20+
timeout: { type: "number", description: "超时时间(毫秒),默认 30000" },
21+
description: { type: "string", description: "命令的用途说明(可选)" },
22+
},
23+
required: ["command"],
24+
},
25+
isReadOnly: false,
26+
async call(input) {
27+
const command = String(input.command);
28+
const timeout = Number(input.timeout ?? 30000);
29+
30+
try {
31+
const proc = Bun.spawn(["sh", "-c", command], {
32+
stdout: "pipe",
33+
stderr: "pipe",
34+
env: { ...process.env, TERM: "dumb" }, // 禁用颜色转义
35+
});
36+
37+
// 超时处理
38+
const timer = setTimeout(() => proc.kill(), timeout);
39+
40+
const stdout = await new Response(proc.stdout).text();
41+
const stderr = await new Response(proc.stderr).text();
42+
const exitCode = await proc.exited;
43+
44+
clearTimeout(timer);
45+
46+
// 格式化输出
47+
let content = stdout;
48+
if (stderr) content += (content ? "\n" : "") + `STDERR:\n${stderr}`;
49+
if (exitCode !== 0) content += `\nExit code: ${exitCode}`;
50+
51+
// 截断过长输出
52+
if (content.length > MAX_OUTPUT_LENGTH) {
53+
content = content.substring(0, MAX_OUTPUT_LENGTH) +
54+
`\n... [output truncated, ${content.length} total chars]`;
55+
}
56+
57+
return { content: content || "(no output)", isError: exitCode !== 0 };
58+
} catch (e) {
59+
return { content: `Error executing command: ${e}`, isError: true };
60+
}
61+
},
62+
});

demo/tools/FileReadTool/index.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* tools/FileReadTool/index.ts - 文件读取工具
3+
*
4+
* 对应真实 Claude Code: src/tools/FileReadTool/
5+
* 真实版本还包含:二进制文件检测、图片/PDF 读取、
6+
* 编码检测、符号链接解析等。
7+
*/
8+
9+
import { buildTool } from "../../Tool.js";
10+
11+
export const FileReadTool = buildTool({
12+
name: "Read",
13+
description: "读取文件内容。支持指定起始行和行数限制。对于大文件,建议使用 offset 和 limit 参数只读取需要的部分。",
14+
inputSchema: {
15+
type: "object",
16+
properties: {
17+
file_path: { type: "string", description: "文件的绝对路径" },
18+
offset: { type: "number", description: "起始行号(从 0 开始,可选)" },
19+
limit: { type: "number", description: "读取的最大行数(可选,默认 2000)" },
20+
},
21+
required: ["file_path"],
22+
},
23+
isReadOnly: true,
24+
async call(input) {
25+
const filePath = String(input.file_path);
26+
const offset = Number(input.offset ?? 0);
27+
const limit = Number(input.limit ?? 2000);
28+
29+
try {
30+
const file = Bun.file(filePath);
31+
const exists = await file.exists();
32+
if (!exists) {
33+
return { content: `Error: File not found: ${filePath}`, isError: true };
34+
}
35+
36+
const text = await file.text();
37+
const allLines = text.split("\n");
38+
const selectedLines = allLines.slice(offset, offset + limit);
39+
40+
// 添加行号(模拟 cat -n 格式)
41+
const numbered = selectedLines
42+
.map((line, i) => `${String(offset + i + 1).padStart(6)}\t${line}`)
43+
.join("\n");
44+
45+
let content = numbered;
46+
if (offset + limit < allLines.length) {
47+
content += `\n... [${allLines.length - offset - limit} more lines]`;
48+
}
49+
50+
return { content };
51+
} catch (e) {
52+
return { content: `Error reading file: ${e}`, isError: true };
53+
}
54+
},
55+
});

demo/tools/GrepTool/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* tools/GrepTool/index.ts - 代码搜索工具
3+
*
4+
* 对应真实 Claude Code: src/tools/GrepTool/
5+
* 真实版本使用 ripgrep (rg) 而非 grep,性能更好。
6+
* 这里简化为 grep,后续可替换为 rg。
7+
*/
8+
9+
import { buildTool } from "../../Tool.js";
10+
11+
export const GrepTool = buildTool({
12+
name: "Grep",
13+
description: "在文件中搜索匹配的文本模式。支持正则表达式。适合在代码库中查找函数定义、变量引用等。",
14+
inputSchema: {
15+
type: "object",
16+
properties: {
17+
pattern: { type: "string", description: "搜索的正则表达式模式" },
18+
path: { type: "string", description: "搜索的目录或文件路径,默认当前目录" },
19+
include: { type: "string", description: "文件类型过滤(如 '*.ts')" },
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+
const include = input.include ? String(input.include) : undefined;
28+
29+
try {
30+
const args = ["grep", "-rn"];
31+
if (include) args.push(`--include=${include}`);
32+
args.push(pattern, searchPath);
33+
34+
const proc = Bun.spawn(args, {
35+
stdout: "pipe",
36+
stderr: "pipe",
37+
});
38+
39+
const stdout = await new Response(proc.stdout).text();
40+
const stderr = await new Response(proc.stderr).text();
41+
const exitCode = await proc.exited;
42+
43+
if (exitCode === 1 && !stderr) {
44+
return { content: "No matches found." };
45+
}
46+
if (exitCode > 1 || stderr) {
47+
return { content: `Error: ${stderr}`, isError: true };
48+
}
49+
50+
// 限制输出行数
51+
const lines = stdout.split("\n");
52+
if (lines.length > 200) {
53+
return {
54+
content: lines.slice(0, 200).join("\n") +
55+
`\n... [${lines.length - 200} more matches]`,
56+
};
57+
}
58+
59+
return { content: stdout || "No matches found." };
60+
} catch (e) {
61+
return { content: `Error: ${e}`, isError: true };
62+
}
63+
},
64+
});

0 commit comments

Comments
 (0)