目标:实现一个可交互的 REPL 循环,让用户能够持续输入指令并接收 AI 响应。
Claude Code 从零复刻教程 - 完整大纲
Claude Code 从零复刻教程 第 1 篇:项目初始化与 CLI 骨架
Claude Code 从零复刻教程 第 2 篇:REPL 循环实现
学习目标
1
完成本篇后,你将能够:
- 理解 REPL 循环的工作原理
- 使用 Node.js 的
readline模块实现交互式输入 - 设计优雅的对话状态管理
- 实现退出命令和特殊指令处理
- 添加输入历史记录和快捷键支持
核心概念
什么是 REPL?
REPL 是 Read-Eval-Print Loop 的缩写,代表一种交互式编程环境:
┌─────────────────────────────────────┐
│ Read → Eval → Print → Loop │
│ (读取) (执行) (输出) (循环) │
└─────────────────────────────────────┘
工作流程:
- Read:读取用户输入
- Eval:执行/处理输入
- Print:输出结果
- Loop:回到第一步,等待下一次输入
Claude Code 的 REPL 特点
| 特性 | 说明 |
|---|---|
| 持续对话 | 保持上下文,支持多轮对话 |
| 特殊命令 | /help, /exit, /clear 等 |
| 快捷键 | Ctrl+C 退出,Ctrl+D EOF |
| 历史记录 | 上下箭头浏览历史输入 |
| 多行输入 | Shift+Enter 换行 |
代码实现
1. 项目结构
在上一篇的基础上,更新项目结构:
my-claude/
├── src/
│ ├── index.ts # 入口(保持不变)
│ ├── main.ts # 主程序(更新)
│ └── repl.ts # REPL 核心(新增)
├── package.json
└── tsconfig.json
2. REPL 核心模块
创建 src/repl.ts:
typescript
import * as readline from 'readline';
import { stdin as input, stdout as output } from 'process';
import pc from 'picocolors';
/**
* REPL 配置选项
*/
export interface REPLConfig {
/** 提示符 */
prompt?: string;
/** 欢迎信息 */
welcomeMessage?: string;
/** 是否显示时间戳 */
showTimestamp?: boolean;
}
/**
* REPL 回调函数类型
*/
export type REPLHandler = (input: string) => Promise<string | void>;
/**
* REPL 会话状态
*/
interface SessionState {
/** 输入历史 */
history: string[];
/** 当前历史索引 */
historyIndex: number;
/** 是否正在运行 */
isRunning: boolean;
/** 多行输入缓冲区 */
multilineBuffer: string;
/** 是否在多行模式 */
isMultiline: boolean;
}
/**
* 创建并启动 REPL 会话
*/
export function startREPL(handler: REPLHandler, config: REPLConfig = {}): void {
const {
prompt = '❯ ',
welcomeMessage = 'Welcome to My Claude! Type /help for commands.',
showTimestamp = false,
} = config;
// 初始化会话状态
const state: SessionState = {
history: [],
historyIndex: -1,
isRunning: true,
multilineBuffer: '',
isMultiline: false,
};
// 创建 readline 接口
const rl = readline.createInterface({
input,
output,
prompt: pc.cyan(prompt),
historySize: 1000,
});
// 显示欢迎信息
console.log(pc.green(welcomeMessage));
console.log();
// 显示初始提示符
rl.prompt();
// 处理输入行
rl.on('line', async (line: string) => {
const trimmedLine = line.trim();
// 处理多行输入模式
if (state.isMultiline) {
if (trimmedLine === '```') {
// 结束多行输入
state.isMultiline = false;
const fullInput = state.multilineBuffer;
state.multilineBuffer = '';
rl.setPrompt(pc.cyan(prompt));
await processInput(fullInput, handler, rl, state, showTimestamp);
} else {
// 继续收集多行输入
state.multilineBuffer += line + '\n';
rl.setPrompt(pc.gray('... '));
rl.prompt();
}
return;
}
// 处理空输入
if (!trimmedLine) {
rl.prompt();
return;
}
// 处理特殊命令
if (await handleCommand(trimmedLine, state, rl)) {
return;
}
// 检查是否开始多行输入(以 ```开头)
if (trimmedLine === '```') {
state.isMultiline = true;
state.multilineBuffer = '';
rl.setPrompt(pc.gray('... '));
rl.prompt();
return;
}
// 处理普通输入
await processInput(trimmedLine, handler, rl, state, showTimestamp);
});
// 处理 Ctrl+C (SIGINT)
rl.on('SIGINT', () => {
if (state.isMultiline) {
// 取消多行输入
state.isMultiline = false;
state.multilineBuffer = '';
console.log(pc.yellow('\n[多行输入已取消]'));
rl.setPrompt(pc.cyan(prompt));
rl.prompt();
} else {
gracefulExit(rl, state);
}
});
// 处理 Ctrl+D (EOF)
rl.on('close', () => {
gracefulExit(rl, state);
});
}
/**
* 处理特殊命令
* @returns 如果处理了命令返回 true
*/
async function handleCommand(
input: string,
state: SessionState,
rl: readline.Interface
): Promise<boolean> {
switch (input.toLowerCase()) {
case '/exit':
case '/quit':
case 'exit':
case 'quit':
gracefulExit(rl, state);
return true;
case '/help':
showHelp();
rl.prompt();
return true;
case '/clear':
console.clear();
rl.prompt();
return true;
case '/history':
showHistory(state.history);
rl.prompt();
return true;
default:
return false;
}
}
/**
* 显示帮助信息
*/
function showHelp(): void {
console.log(pc.bold('\n📚 可用命令:\n'));
console.log(` ${pc.cyan('/help')} 显示此帮助信息`);
console.log(` ${pc.cyan('/exit')} 退出程序`);
console.log(` ${pc.cyan('/clear')} 清屏`);
console.log(` ${pc.cyan('/history')} 显示输入历史`);
console.log(` ${pc.cyan('```')} 开始/结束多行输入`);
console.log();
console.log(pc.gray('快捷键:'));
console.log(` ${pc.gray('Ctrl+C')} 退出(或取消多行输入)`);
console.log(` ${pc.gray('Ctrl+D')} 退出`);
console.log(` ${pc.gray('↑/↓')} 浏览历史输入`);
console.log();
}
/**
* 显示输入历史
*/
function showHistory(history: string[]): void {
if (history.length === 0) {
console.log(pc.yellow('暂无输入历史'));
return;
}
console.log(pc.bold('\n📜 输入历史(最近 20 条):\n'));
const recent = history.slice(-20);
recent.forEach((item, index) => {
const num = (history.length - recent.length + index + 1).toString().padStart(3);
const truncated = item.length > 50 ? item.slice(0, 50) + '...' : item;
console.log(` ${pc.gray(num)} ${truncated}`);
});
console.log();
}
/**
* 处理用户输入
*/
async function processInput(
input: string,
handler: REPLHandler,
rl: readline.Interface,
state: SessionState,
showTimestamp: boolean
): Promise<void> {
// 记录到历史
state.history.push(input);
// 显示时间戳
if (showTimestamp) {
const timestamp = new Date().toLocaleTimeString();
console.log(pc.gray(`[${timestamp}]`));
}
try {
// 调用处理器
const result = await handler(input);
// 输出结果
if (result !== undefined) {
console.log(result);
}
} catch (error) {
console.error(pc.red('错误:'), error instanceof Error ? error.message : String(error));
}
console.log(); // 空行分隔
rl.prompt();
}
/**
* 优雅退出
*/
function gracefulExit(rl: readline.Interface, state: SessionState): void {
if (!state.isRunning) return;
state.isRunning = false;
console.log(pc.yellow('\n👋 再见!'));
rl.close();
process.exit(0);
}
3. 更新主程序
更新 src/main.ts:
typescript
import { program } from 'commander';
import pc from 'picocolors';
import { startREPL } from './repl';
// 模拟 AI 响应(第 4 篇将替换为真实 API 调用)
async function mockAIResponse(input: string): Promise<string> {
// 模拟延迟
await new Promise(resolve => setTimeout(resolve, 500));
const responses = [
`我收到了你的消息:"${input}"`,
`这是一个模拟响应。在实际实现中,这里会调用 Claude API。`,
`输入长度:${input.length} 个字符`,
];
return responses.join('\n');
}
/**
* 主程序入口
*/
export async function main(): Promise<void> {
program
.name('my-claude')
.description('My Claude - 一个 Claude Code 复刻项目')
.version('0.1.0')
.option('-v, --verbose', '显示详细日志')
.parse();
const options = program.opts();
if (options.verbose) {
console.log(pc.gray('详细模式已开启'));
}
// 启动 REPL
startREPL(
async (input) => {
// 这里处理用户输入
if (options.verbose) {
console.log(pc.gray(`[处理输入] ${input}`));
}
// 调用 AI(目前是模拟)
return await mockAIResponse(input);
},
{
prompt: '❯ ',
welcomeMessage: '🤖 My Claude v0.1.0\n 基于 Claude Code 架构复刻',
showTimestamp: options.verbose,
}
);
}
4. 更新 package.json
添加新的脚本:
json
{
"name": "my-claude",
"version": "0.1.0",
"description": "My Claude - 一个 Claude Code 复刻项目",
"type": "module",
"main": "dist/index.js",
"bin": {
"my-claude": "dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"clean": "rm -rf dist"
},
"keywords": ["claude", "ai", "cli"],
"author": "",
"license": "MIT",
"dependencies": {
"commander": "^11.0.0",
"picocolors": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.0.0",
"typescript": "^5.0.0"
}
}
运行演示
安装依赖
bash
cd my-claude
npm install
运行程序
bash
npm run dev
交互示例
🤖 My Claude v0.1.0
基于 Claude Code 架构复刻
❯ 你好
我收到了你的消息:"你好"
这是一个模拟响应。在实际实现中,这里会调用 Claude API。
输入长度:2 个字符
❯ /help
📚 可用命令:
/help 显示此帮助信息
/exit 退出程序
/clear 清屏
/history 显示输入历史
```开始/结束多行输入
快捷键:
Ctrl+C 退出(或取消多行输入)
Ctrl+D 退出
↑/↓ 浏览历史输入
❯ ```
... 这是一段
... 多行输入
... ```
我收到了你的消息:"这是一段
多行输入
"
这是一个模拟响应。在实际实现中,这里会调用 Claude API。
输入长度:16 个字符
❯ /exit
👋 再见!
原理深入
Node.js readline 模块
readline 是 Node.js 内置模块,提供了逐行读取流的接口:
typescript
import * as readline from 'readline';
const rl = readline.createInterface({
input: process.stdin, // 标准输入
output: process.stdout, // 标准输出
prompt: '> ', // 提示符
historySize: 1000, // 历史记录大小
});
关键事件:
| 事件 | 触发时机 |
|---|---|
line |
用户按下 Enter |
SIGINT |
用户按下 Ctrl+C |
close |
用户按下 Ctrl+D 或调用 rl.close() |
多行输入实现
多行输入通过状态机实现:
状态:NORMAL
└── 输入 ```──→ 状态:MULTILINE
└── 输入 ```──→ 状态:NORMAL
└── 其他输入 ──→ 继续 MULTILINE
快捷键处理
| 快捷键 | 处理方式 |
|---|---|
| Ctrl+C | rl.on('SIGINT', ...) |
| Ctrl+D | rl.on('close', ...) |
| ↑/↓ | readline 内置历史浏览 |
| Tab | readline 内置自动补全(可扩展) |
练习作业
基础练习
-
添加新命令 :实现
/time命令,显示当前时间答案
typescript// 在 repl.ts 的 handleCommand 函数中添加 case '/time': const now = new Date(); console.log(pc.cyan(`\n🕐 当前时间: ${now.toLocaleString()}\n`)); rl.prompt(); return true; -
美化输出:为 AI 响应添加边框装饰
答案
typescript// 在 processInput 函数中修改输出部分 function formatOutput(text: string): string { const lines = text.split('\n'); const maxLength = Math.max(...lines.map(l => l.length)); const border = '─'.repeat(maxLength + 4); const formatted = lines .map(line => `│ ${line.padEnd(maxLength)} │`) .join('\n'); return `┌${border}┐\n${formatted}\n└${border}┘`; } // 使用 console.log(formatOutput(result)); -
输入计数:显示这是第几次对话
答案
typescript// 在 SessionState 中添加计数器 interface SessionState { history: string[]; historyIndex: number; isRunning: boolean; multilineBuffer: string; isMultiline: boolean; inputCount: number; // 新增 } // 初始化时 const state: SessionState = { // ... 其他字段 inputCount: 0, }; // 处理输入时递增 async function processInput(...) { state.inputCount++; console.log(pc.gray(`[第 ${state.inputCount} 次对话]`)); // ... }
进阶练习
-
自动补全:实现基于历史的 Tab 自动补全
答案
typescript// 创建 readline 接口时添加 completer const rl = readline.createInterface({ input, output, prompt: pc.cyan(prompt), historySize: 1000, completer: (line: string) => { const completions = state.history.filter(h => h.startsWith(line)); const hits = completions.length ? completions : state.history; return [hits, line]; }, }); -
会话保存:将对话历史保存到文件,下次启动时加载
答案
typescriptimport * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; const HISTORY_FILE = path.join(os.homedir(), '.myclaude', 'history.json'); // 保存历史 async function saveHistory(history: string[]): Promise<void> { await fs.mkdir(path.dirname(HISTORY_FILE), { recursive: true }); await fs.writeFile(HISTORY_FILE, JSON.stringify(history), 'utf-8'); } // 加载历史 async function loadHistory(): Promise<string[]> { try { const content = await fs.readFile(HISTORY_FILE, 'utf-8'); return JSON.parse(content); } catch { return []; } } // 在 startREPL 中初始化时加载 state.history = await loadHistory(); // 退出时保存 function gracefulExit(rl: readline.Interface, state: SessionState): void { saveHistory(state.history); // ... } -
语法高亮:对代码块进行简单的语法高亮
答案
typescriptfunction highlightCode(text: string): string { // 提取代码块 const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; return text.replace(codeBlockRegex, (match, lang, code) => { // 简单的关键字高亮 let highlighted = code .replace(/\b(function|const|let|var|return|if|else|for|while)\b/g, pc.cyan('$1')) .replace(/\b(true|false|null|undefined)\b/g, pc.yellow('$1')) .replace(/\/\/.*$/gm, pc.gray('$&')) .replace(/"[^"]*"/g, pc.green('$&')); return pc.gray('```') + (lang ? pc.yellow(lang) : '') + '\n' + highlighted + pc.gray('```'); }); }
挑战练习
-
多会话支持 :实现
/session new创建新会话,/session list列出所有会话答案
typescript// 会话管理 interface Session { id: string; name: string; history: string[]; createdAt: Date; } const sessions: Map<string, Session> = new Map(); let currentSessionId: string = 'default'; // 在 handleCommand 中添加 case '/session': const subCmd = args[0]; if (subCmd === 'new') { const newSession: Session = { id: Date.now().toString(), name: args[1] || `Session ${sessions.size + 1}`, history: [], createdAt: new Date(), }; sessions.set(newSession.id, newSession); currentSessionId = newSession.id; console.log(pc.green(`✓ 创建新会话: ${newSession.name}`)); } else if (subCmd === 'list') { console.log(pc.bold('\n📁 会话列表:\n')); sessions.forEach(s => { const marker = s.id === currentSessionId ? pc.green('●') : pc.gray('○'); console.log(` ${marker} ${s.name} (${s.history.length} 条消息)`); }); console.log(); } rl.prompt(); return true; -
实时打字效果:模拟 AI 逐字输出的打字机效果
答案
typescriptasync function typewriterEffect(text: string, delay: number = 30): Promise<void> { for (const char of text) { process.stdout.write(char); await new Promise(resolve => setTimeout(resolve, delay)); } console.log(); // 换行 } // 在 processInput 中使用 try { const result = await handler(input); if (result) { await typewriterEffect(result, 20); } } catch (error) { // ... }
下一篇预告
第 3 篇:参数解析与配置管理
我们将实现:
- 环境变量管理(ANTHROPIC_API_KEY)
- 配置文件系统(JSON/YAML)
- 配置验证和默认值
- 敏感信息加密存储