Roo-Code/src/integrations/claude-code/run.ts

225 lines
5.1 KiB
TypeScript

import * as vscode from "vscode"
import type Anthropic from "@anthropic-ai/sdk"
import { execa } from "execa"
import { ClaudeCodeMessage } from "./types"
import readline from "readline"
import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types"
import * as os from "os"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
type ClaudeCodeOptions = {
systemPrompt: string
messages: Anthropic.Messages.MessageParam[]
path?: string
modelId?: string
}
type ProcessState = {
partialData: string | null
error: Error | null
stderrLogs: string
exitCode: number | null
}
export async function* runClaudeCode(
options: ClaudeCodeOptions & { maxOutputTokens?: number },
): AsyncGenerator<ClaudeCodeMessage | string> {
const process = runProcess(options)
const rl = readline.createInterface({
input: process.stdout,
})
try {
const processState: ProcessState = {
error: null,
stderrLogs: "",
exitCode: null,
partialData: null,
}
process.stderr.on("data", (data) => {
processState.stderrLogs += data.toString()
})
process.on("close", (code) => {
processState.exitCode = code
})
process.on("error", (err) => {
processState.error = err
})
for await (const line of rl) {
if (processState.error) {
throw processState.error
}
if (line.trim()) {
const chunk = parseChunk(line, processState)
if (!chunk) {
continue
}
yield chunk
}
}
// We rely on the assistant message. If the output was truncated, it's better having a poorly formatted message
// from which to extract something, than throwing an error/showing the model didn't return any messages.
if (processState.partialData && processState.partialData.startsWith(`{"type":"assistant"`)) {
yield processState.partialData
}
const { exitCode } = await process
if (exitCode !== null && exitCode !== 0) {
const errorOutput = processState.error?.message || processState.stderrLogs?.trim()
throw new Error(
`Claude Code process exited with code ${exitCode}.${errorOutput ? ` Error output: ${errorOutput}` : ""}`,
)
}
} finally {
rl.close()
if (!process.killed) {
process.kill()
}
}
}
// We want the model to use our custom tool format instead of built-in tools.
// Disabling built-in tools prevents tool-only responses and ensures text output.
const claudeCodeTools = [
"Task",
"Bash",
"Glob",
"Grep",
"LS",
"exit_plan_mode",
"Read",
"Edit",
"MultiEdit",
"Write",
"NotebookRead",
"NotebookEdit",
"WebFetch",
"TodoRead",
"TodoWrite",
"WebSearch",
].join(",")
const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
function runProcess({
systemPrompt,
messages,
path,
modelId,
maxOutputTokens,
}: ClaudeCodeOptions & { maxOutputTokens?: number }) {
const claudePath = path || "claude"
const isWindows = os.platform() === "win32"
// Build args based on platform
const args = ["-p"]
// Pass system prompt as flag on non-Windows, via stdin on Windows (avoids cmd length limits)
if (!isWindows) {
args.push("--system-prompt", systemPrompt)
}
args.push(
"--verbose",
"--output-format",
"stream-json",
"--disallowedTools",
claudeCodeTools,
// Roo Code will handle recursive calls
"--max-turns",
"1",
)
if (modelId) {
args.push("--model", modelId)
}
const child = execa(claudePath, args, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
// Use the configured value, or the environment variable, or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
maxOutputTokens?.toString() ||
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ||
CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(),
},
cwd,
maxBuffer: 1024 * 1024 * 1000,
timeout: CLAUDE_CODE_TIMEOUT,
})
// Prepare stdin data: Windows gets both system prompt & messages (avoids 8191 char limit),
// other platforms get messages only (avoids Linux E2BIG error from ~128KiB execve limit)
let stdinData: string
if (isWindows) {
stdinData = JSON.stringify({
systemPrompt,
messages,
})
} else {
stdinData = JSON.stringify(messages)
}
// Use setImmediate to ensure process is spawned before writing (prevents stdin race conditions)
setImmediate(() => {
try {
child.stdin.write(stdinData, "utf8", (error: Error | null | undefined) => {
if (error) {
console.error("Error writing to Claude Code stdin:", error)
child.kill()
}
})
child.stdin.end()
} catch (error) {
console.error("Error accessing Claude Code stdin:", error)
child.kill()
}
})
return child
}
function parseChunk(data: string, processState: ProcessState) {
if (processState.partialData) {
processState.partialData += data
const chunk = attemptParseChunk(processState.partialData)
if (!chunk) {
return null
}
processState.partialData = null
return chunk
}
const chunk = attemptParseChunk(data)
if (!chunk) {
processState.partialData = data
}
return chunk
}
function attemptParseChunk(data: string): ClaudeCodeMessage | null {
try {
return JSON.parse(data)
} catch (error) {
console.error("Error parsing chunk:", error, data.length)
return null
}
}