Move the action from Deno to Node.js and Vite+
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
// Runs Claude Code for one profile and returns its final result text.
|
||||
import { createInterface } from "node:readline";
|
||||
import type { Readable } from "node:stream";
|
||||
|
||||
import { isClaudeAuthFailure } from "./auth.ts";
|
||||
import type { ClaudeFailureFields } from "./auth.ts";
|
||||
import { chunks } from "./command.ts";
|
||||
import { agentCommand } from "./isolation.ts";
|
||||
import { isObject } from "./json.ts";
|
||||
import type { JsonObject } from "./json.ts";
|
||||
import { AgentAuthError, agentEnvironment } from "./profiles.ts";
|
||||
import { PROCESS_ENV, WORKSPACE } from "./run-context.ts";
|
||||
import type { RuntimeProfile } from "./runtime.ts";
|
||||
|
||||
// Claude Code's stream-json `result` event, in its own field names.
|
||||
type ClaudeResult = ClaudeFailureFields &
|
||||
Partial<Record<"is_error", boolean>> & {
|
||||
subtype: string;
|
||||
};
|
||||
|
||||
// Echoes stderr as it arrives and resolves with all of it.
|
||||
async function copyError(stream: Readable): Promise<string> {
|
||||
const decoder = new TextDecoder();
|
||||
let error = "";
|
||||
for await (const chunk of chunks(stream)) {
|
||||
const text = decoder.decode(chunk, { stream: true });
|
||||
error += text;
|
||||
console.error(text);
|
||||
}
|
||||
return error + decoder.decode();
|
||||
}
|
||||
|
||||
// Prints the visible text of each streamed message part.
|
||||
function printParts(event: JsonObject): void {
|
||||
const content = isObject(event.message) ? event.message.content : undefined;
|
||||
for (const part of Array.isArray(content) ? content : []) {
|
||||
if (isObject(part)) {
|
||||
const text = part.thinking ?? part.text ?? part.name;
|
||||
if (typeof text === "string" && text !== "") {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toResult(event: JsonObject): ClaudeResult {
|
||||
const { is_error: isError, result, subtype, api_error_status: apiErrorStatus } = event;
|
||||
return {
|
||||
...(typeof isError === "boolean" && { is_error: isError }),
|
||||
...(typeof result === "string" && { result }),
|
||||
subtype: String(subtype),
|
||||
...((typeof apiErrorStatus === "number" || apiErrorStatus === null) && {
|
||||
api_error_status: apiErrorStatus,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Prints each streamed message part and returns the final `result` event.
|
||||
async function readEvents(stream: Readable): Promise<ClaudeResult | undefined> {
|
||||
let result: ClaudeResult | undefined;
|
||||
for await (const line of createInterface({ input: stream, crlfDelay: Infinity })) {
|
||||
if (line !== "") {
|
||||
const event: unknown = JSON.parse(line);
|
||||
if (isObject(event)) {
|
||||
printParts(event);
|
||||
if (event.type === "result") {
|
||||
result = toResult(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function checkResult(result: ClaudeResult | undefined, error: string): string {
|
||||
if (result?.is_error === true) {
|
||||
const failure = new Error(result.result ?? `claude ended with ${result.subtype}`);
|
||||
if (isClaudeAuthFailure(result)) {
|
||||
throw new AgentAuthError("claude sign-in failed", { cause: failure });
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
if (result?.result === undefined) {
|
||||
throw new Error(error.trim() || `claude ended with ${result?.subtype ?? "no result"}`);
|
||||
}
|
||||
return result.result;
|
||||
}
|
||||
|
||||
export async function runClaude(
|
||||
profile: RuntimeProfile,
|
||||
token: string,
|
||||
prompt: string,
|
||||
): Promise<string> {
|
||||
const args = [
|
||||
"--print",
|
||||
"--dangerously-skip-permissions",
|
||||
"--model",
|
||||
profile.model,
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--verbose",
|
||||
prompt,
|
||||
];
|
||||
const claude = agentCommand(profile, "claude", args, {
|
||||
cwd: WORKSPACE,
|
||||
env: { ...agentEnvironment(PROCESS_ENV, profile, token), IS_SANDBOX: "1" },
|
||||
clearEnv: true,
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
}).spawn();
|
||||
const errorText = copyError(claude.stderr);
|
||||
const result = await readEvents(claude.stdout);
|
||||
const [status, error] = await Promise.all([claude.status, errorText]);
|
||||
const body = checkResult(result, error);
|
||||
if (!status.success) {
|
||||
throw new Error(error.trim() || `claude exited with ${status.code}`);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
Reference in New Issue
Block a user