跳到主要内容

Claude Code CLI 代码集成完整攻略

目录

  1. 概述
  2. 前置准备
  3. 核心实现
  4. 完整代码
  5. 使用示例
  6. 最佳实践
  7. 故障排除

概述

本攻略提供了在Node.js应用中直接调用Claude Code CLI的完整解决方案,基于对Claude Code Base Action的深度分析,提取了所有核心技术要点。

为什么需要直接调用

  • 灵活性: 在任何Node.js环境中使用,不限于GitHub Actions
  • 集成性: 可以整合到现有的应用程序和工作流中
  • 可控性: 完全控制执行参数、输出处理和错误处理
  • 扩展性: 可以根据需求自定义功能和输出格式

前置准备

1. 环境要求

按照Claude-Code-Cli

2. Node.js依赖

{
"dependencies": {
"@types/node": "^20.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}

3. 系统要求

  • Unix/Linux/macOS (支持mkfifo命令)
  • Node.js 18+
  • 已安装jq命令行工具 (用于JSON处理)

核心实现

关键技术要点

  1. 工作目录管理: Claude必须在正确的项目目录下运行
  2. Named Pipes通信: 使用命名管道避免stdin/stdout复杂性
  3. 流式输出处理: 实时解析结构化JSON输出
  4. 进程生命周期管理: 超时控制和优雅终止
  5. 错误处理和资源清理: 确保系统稳定性

接口设计

export interface ClaudeExecutionOptions {
prompt: string; // 执行的prompt
model?: string; // 模型名称
maxTurns?: number; // 最大对话轮数
allowedTools?: string[]; // 允许的工具列表
disallowedTools?: string[]; // 禁用的工具列表
systemPrompt?: string; // 系统提示词
appendSystemPrompt?: string; // 追加系统提示词
mcpConfig?: string; // MCP配置路径
timeoutMinutes?: number; // 超时时间(分钟)
outputFile?: string; // 输出文件路径
workingDirectory?: string; // 工作目录(关键!)
}

export interface ClaudeExecutionResult {
success: boolean; // 执行是否成功
exitCode: number; // 退出代码
output: string; // 原始输出
executionLog?: any[]; // 结构化执行日志
outputFile?: string; // 输出文件路径
}

完整代码

import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import { writeFile, unlink, mkdir } from 'fs/promises';
import { createWriteStream } from 'fs';
import path from 'path';

const execAsync = promisify(exec);

export class ClaudeCodeRunner {
private tempDir: string;

constructor(tempDir: string = '/tmp/claude-runner') {
this.tempDir = tempDir;
}

/**
* 执行Claude Code命令
*/
async runClaude(options: ClaudeExecutionOptions): Promise<ClaudeExecutionResult> {
// 保存原始工作目录
const originalCwd = process.cwd();

try {
// 1. 切换到指定工作目录(关键步骤!)
if (options.workingDirectory) {
console.log(`🔄 Changing to working directory: ${options.workingDirectory}`);
process.chdir(options.workingDirectory);
}

// 2. 确保临时目录存在
await mkdir(this.tempDir, { recursive: true });

const promptPath = path.join(this.tempDir, `prompt-${Date.now()}.txt`);
const pipePath = path.join(this.tempDir, `claude_pipe-${Date.now()}`);
const outputFile = options.outputFile ||
path.join(this.tempDir, `claude-output-${Date.now()}.json`);

try {
// 3. 准备prompt文件
await writeFile(promptPath, options.prompt);
console.log(`📝 Prompt written to: ${promptPath}`);

// 4. 创建命名管道
await this.createNamedPipe(pipePath);
console.log(`🔗 Named pipe created: ${pipePath}`);

// 5. 构建Claude命令参数
const claudeArgs = this.buildClaudeArgs(options);
console.log(`⚙️ Claude args: ${claudeArgs.join(' ')}`);

// 6. 执行Claude进程
const result = await this.executeClaudeProcess(
claudeArgs,
promptPath,
pipePath,
outputFile,
options
);

return result;
} finally {
// 7. 清理临时文件
await this.cleanup([promptPath, pipePath]);
}
} finally {
// 8. 恢复原始工作目录
process.chdir(originalCwd);
}
}

/**
* 创建命名管道
*/
private async createNamedPipe(pipePath: string): Promise<void> {
try {
await unlink(pipePath);
} catch (e) {
// 忽略文件不存在的错误
}
await execAsync(`mkfifo "${pipePath}"`);
}

/**
* 构建Claude命令行参数
*/
private buildClaudeArgs(options: ClaudeExecutionOptions): string[] {
const args = ['-p', '--verbose', '--output-format', 'stream-json'];

if (options.model) {
args.push('--model', options.model);
}
if (options.maxTurns) {
args.push('--max-turns', options.maxTurns.toString());
}
if (options.allowedTools?.length) {
args.push('--allowedTools', options.allowedTools.join(','));
}
if (options.disallowedTools?.length) {
args.push('--disallowedTools', options.disallowedTools.join(','));
}
if (options.systemPrompt) {
args.push('--system-prompt', options.systemPrompt);
}
if (options.appendSystemPrompt) {
args.push('--append-system-prompt', options.appendSystemPrompt);
}
if (options.mcpConfig) {
args.push('--mcp-config', options.mcpConfig);
}

return args;
}

/**
* 执行Claude进程
*/
private async executeClaudeProcess(
claudeArgs: string[],
promptPath: string,
pipePath: string,
outputFile: string,
options: ClaudeExecutionOptions
): Promise<ClaudeExecutionResult> {
return new Promise((resolve) => {
let output = '';
let resolved = false;

console.log(`🚀 Starting Claude process...`);

// 启动Claude进程 - 继承当前工作目录
const claudeProcess = spawn('claude', claudeArgs, {
stdio: ['pipe', 'pipe', 'inherit'],
// 不需要显式设置cwd,会继承process.cwd()
env: {
...process.env,
// 可以在这里添加其他环境变量
}
});

// 设置超时
const timeoutMs = (options.timeoutMinutes || 10) * 60 * 1000;
const timeoutId = setTimeout(() => {
if (!resolved) {
console.error(`⏰ Claude process timed out after ${timeoutMs / 1000} seconds`);
claudeProcess.kill('SIGTERM');
setTimeout(() => claudeProcess.kill('SIGKILL'), 5000);
resolved = true;
resolve({
success: false,
exitCode: 124,
output,
outputFile
});
}
}, timeoutMs);

// 处理输出
claudeProcess.stdout.on('data', (data) => {
const text = data.toString();
output += text;

// 实时输出处理
this.processStreamOutput(text);
});

// 处理进程退出
claudeProcess.on('close', async (code) => {
if (!resolved) {
clearTimeout(timeoutId);
resolved = true;

console.log(`✅ Claude process completed with exit code: ${code}`);

// 处理输出并保存到文件
const executionLog = await this.processOutputToJson(output, outputFile);

resolve({
success: code === 0,
exitCode: code || 0,
output,
executionLog,
outputFile
});
}
});

claudeProcess.on('error', (error) => {
if (!resolved) {
console.error('❌ Claude process error:', error);
clearTimeout(timeoutId);
resolved = true;
resolve({
success: false,
exitCode: 1,
output,
outputFile
});
}
});

// 通过命名管道发送prompt
this.sendPromptThroughPipe(promptPath, pipePath, claudeProcess);
});
}

/**
* 处理流式输出
*/
private processStreamOutput(text: string): void {
const lines = text.split('\n');
lines.forEach((line) => {
if (line.trim() === '') return;

try {
const parsed = JSON.parse(line);
console.log('📊', JSON.stringify(parsed, null, 2));
} catch (e) {
console.log('📝', line);
}
});
}

/**
* 通过命名管道发送prompt
*/
private async sendPromptThroughPipe(
promptPath: string,
pipePath: string,
claudeProcess: any
): Promise<void> {
// 启动cat进程发送prompt到管道
const catProcess = spawn('cat', [promptPath], {
stdio: ['ignore', 'pipe', 'inherit']
});

const pipeStream = createWriteStream(pipePath);
catProcess.stdout.pipe(pipeStream);

// 从管道读取并发送到Claude
const pipeProcess = spawn('cat', [pipePath]);
pipeProcess.stdout.pipe(claudeProcess.stdin);

// 错误处理
catProcess.on('error', (error) => {
console.error('❌ Cat process error:', error);
pipeStream.destroy();
});

pipeProcess.on('error', (error) => {
console.error('❌ Pipe process error:', error);
claudeProcess.kill('SIGTERM');
});
}

/**
* 处理输出为JSON格式
*/
private async processOutputToJson(output: string, outputFile: string): Promise<any[]> {
try {
// 将输出保存到临时文件
const tempFile = outputFile.replace('.json', '.txt');
await writeFile(tempFile, output);

// 使用jq处理为JSON数组
const { stdout: jsonOutput } = await execAsync(`jq -s '.' "${tempFile}"`);
await writeFile(outputFile, jsonOutput);

console.log(`💾 Execution log saved to: ${outputFile}`);
return JSON.parse(jsonOutput);
} catch (e) {
console.warn(`⚠️ Failed to process output to JSON: ${e}`);
return [];
}
}

/**
* 清理临时文件
*/
private async cleanup(paths: string[]): Promise<void> {
for (const path of paths) {
try {
await unlink(path);
} catch (e) {
// 忽略清理错误
}
}
console.log('🧹 Temporary files cleaned up');
}

/**
* 便捷方法:将结果输出到Markdown文件
*/
async runAndSaveToMarkdown(
options: ClaudeExecutionOptions,
markdownPath: string
): Promise<ClaudeExecutionResult> {
const result = await this.runClaude(options);

// 创建Markdown内容
const markdownContent = this.formatResultAsMarkdown(result, options);
await writeFile(markdownPath, markdownContent);

console.log(`📄 Results saved to Markdown: ${markdownPath}`);
return result;
}

/**
* 格式化结果为Markdown
*/
private formatResultAsMarkdown(
result: ClaudeExecutionResult,
options: ClaudeExecutionOptions
): string {
const timestamp = new Date().toISOString();

return `# Claude Code Execution Result

## Execution Info
- **Timestamp**: ${timestamp}
- **Success**: ${result.success ? '✅' : '❌'}
- **Exit Code**: ${result.exitCode}
- **Model**: ${options.model || 'default'}
- **Max Turns**: ${options.maxTurns || 'unlimited'}
- **Working Directory**: ${options.workingDirectory || process.cwd()}

## Prompt
\`\`\`
${options.prompt}
\`\`\`

## Output
\`\`\`
${result.output}
\`\`\`

## Execution Log
${result.executionLog ? '```json\n' + JSON.stringify(result.executionLog, null, 2) + '\n```' : 'No execution log available'}

---
*Generated by Claude Code Runner at ${timestamp}*
`;
}
}

使用示例

1. 基本使用

import { ClaudeCodeRunner } from './claude-code-runner';

async function basicExample() {
const runner = new ClaudeCodeRunner();

try {
const result = await runner.runClaude({
prompt: "分析当前项目的结构,生成一个项目概述",
model: "claude-3-5-sonnet-20241022",
maxTurns: 5,
allowedTools: ["Read", "Glob", "Grep", "Bash"],
timeoutMinutes: 15,
workingDirectory: process.cwd() // 明确指定项目目录
});

console.log('✅ Execution completed:', result.success);
if (result.success) {
console.log('📊 Output preview:', result.output.substring(0, 500) + '...');
} else {
console.error('❌ Execution failed with exit code:', result.exitCode);
}
} catch (error) {
console.error('❌ Error:', error);
}
}

2. 保存到Markdown文件

async function markdownExample() {
const runner = new ClaudeCodeRunner();

try {
const result = await runner.runAndSaveToMarkdown({
prompt: "帮我优化这个函数的性能,并解释优化原理",
model: "claude-3-5-sonnet-20241022",
systemPrompt: "你是一个性能优化专家,专注于提供详细的代码分析和优化建议。",
maxTurns: 3,
allowedTools: ["Read", "Edit", "Grep"],
timeoutMinutes: 10,
workingDirectory: "/path/to/your/project"
}, './claude-optimization-result.md');

console.log('📄 Results saved to: ./claude-optimization-result.md');
} catch (error) {
console.error('❌ Error:', error);
}
}

3. 批量处理

async function batchProcessing() {
const runner = new ClaudeCodeRunner();
const tasks = [
{
name: "代码审查",
prompt: "审查src目录下的所有TypeScript文件,找出潜在问题",
outputFile: "./reviews/code-review.md"
},
{
name: "文档生成",
prompt: "为主要的API函数生成文档",
outputFile: "./docs/api-docs.md"
},
{
name: "测试建议",
prompt: "分析测试覆盖率,提供测试改进建议",
outputFile: "./reports/test-suggestions.md"
}
];

for (const task of tasks) {
console.log(`🔄 Processing: ${task.name}`);

try {
const result = await runner.runAndSaveToMarkdown({
prompt: task.prompt,
model: "claude-3-5-sonnet-20241022",
maxTurns: 5,
allowedTools: ["Read", "Glob", "Grep"],
timeoutMinutes: 20,
workingDirectory: process.cwd()
}, task.outputFile);

console.log(`${task.name} completed: ${result.success}`);
} catch (error) {
console.error(`${task.name} failed:`, error);
}
}
}

最佳实践

1. 工作目录管理

// ✅ 推荐:明确指定工作目录
const result = await runner.runClaude({
workingDirectory: "/absolute/path/to/project",
// ... 其他选项
});

// ❌ 避免:依赖当前目录
const result = await runner.runClaude({
// workingDirectory 未指定
});

2. 工具权限控制

// ✅ 推荐:明确指定允许的工具
const result = await runner.runClaude({
allowedTools: ["Read", "Glob", "Grep"], // 只读操作
prompt: "分析代码结构"
});

// ⚠️ 谨慎:允许写入操作
const result = await runner.runClaude({
allowedTools: ["Read", "Write", "Edit", "Bash"], // 包含写入操作
prompt: "优化代码"
});

3. 超时和资源管理

// ✅ 推荐:设置合理的超时时间
const result = await runner.runClaude({
timeoutMinutes: 15, // 根据任务复杂度调整
maxTurns: 5, // 限制对话轮数
});

4. 错误处理

async function robustExecution() {
const runner = new ClaudeCodeRunner();

try {
const result = await runner.runClaude(options);

if (!result.success) {
console.error(`Claude execution failed with exit code: ${result.exitCode}`);
console.error(`Output: ${result.output}`);
return;
}

// 处理成功结果
console.log('Success!', result.output);

} catch (error) {
if (error.code === 'ENOENT') {
console.error('❌ Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code');
} else if (error.code === 'EACCES') {
console.error('❌ Permission denied. Check file permissions.');
} else {
console.error('❌ Unexpected error:', error);
}
}
}

故障排除

常见问题及解决方案

1. Claude CLI未找到

Error: spawn claude ENOENT

解决方案:

npm install -g @anthropic-ai/claude-code@latest
which claude # 验证安装路径

2. 权限问题

Error: EACCES: permission denied

解决方案:

# 检查临时目录权限
ls -la /tmp/claude-runner/
chmod 755 /tmp/claude-runner/

# 或使用不同的临时目录
const runner = new ClaudeCodeRunner('./temp/claude');

3. Named Pipe创建失败

Error: mkfifo command not found

解决方案:

  • 确保在Unix/Linux/macOS系统上运行
  • Windows用户需要使用WSL或Docker

4. jq命令未找到

Error: jq: command not found

解决方案:

# macOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

# CentOS/RHEL
sudo yum install jq

调试技巧

1. 启用详细日志

// 在环境变量中启用调试
process.env.DEBUG = 'claude-runner:*';

2. 保留临时文件

class DebugClaudeCodeRunner extends ClaudeCodeRunner {
private async cleanup(paths: string[]): Promise<void> {
console.log('🐛 Debug mode: Skipping cleanup');
console.log('📁 Temp files:', paths);
// 不删除临时文件,便于调试
}
}

3. 输出分析

const result = await runner.runClaude(options);

// 分析原始输出
console.log('Raw output length:', result.output.length);
console.log('Exit code:', result.exitCode);
console.log('Execution log entries:', result.executionLog?.length || 0);

// 保存调试信息
await writeFile('./debug-output.txt', result.output);

总结

这个完整攻略提供了在Node.js中直接调用Claude Code CLI的所有必要信息:

  1. 核心实现: 基于Named Pipes的稳定进程间通信
  2. 工作目录管理: 确保Claude在正确的项目上下文中运行
  3. 完整的错误处理: 超时控制、资源清理和异常处理
  4. 灵活的输出处理: 支持实时输出和结构化日志
  5. 便捷的集成接口: 易于集成到现有应用中

通过遵循这个攻略,你可以在任何Node.js项目中稳定地使用Claude Code的强大功能。