disclaw/dist/agent/runner.js
Nick Tabeling ca461d0da6
Some checks failed
CI / build-and-test (ubuntu-latest) (pull_request) Has been cancelled
CI / build-and-test (windows-latest) (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
fix(env): remove CI=true from sanitizedEnv, breaks Claude Code OAuth login
2026-04-09 13:18:25 +02:00

260 lines
No EOL
9.5 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAgent = runAgent;
const cross_spawn_1 = __importDefault(require("cross-spawn"));
const path = __importStar(require("path"));
const identity_1 = require("./identity");
const loader_1 = require("../config/loader");
const resolve_claude_1 = require("../runtime/resolve-claude");
const env_1 = require("../runtime/env");
/**
* Resolves the Claude CLI timeout from configuration.
*/
function resolveTimeoutMs() {
try {
const rootDir = path.resolve(__dirname, "../..");
const config = (0, loader_1.loadDisclawConfig)(rootDir);
if (config.claude_timeout_seconds) {
return config.claude_timeout_seconds * 1000;
}
}
catch {
// Config not available, use default
}
return 120_000;
}
/**
* Builds the full prompt string including conversation history context.
* The agent identity is handled by CLAUDE.md in the workspace directory,
* which Claude Code reads automatically.
*/
function buildPrompt(userMessage, conversationHistory, channelName) {
const parts = [];
// Add conversation history if present (oldest first)
const history = [...conversationHistory].reverse();
if (history.length > 0) {
parts.push("Previous conversation context:");
parts.push("");
for (const msg of history) {
const author = msg.author_name ?? msg.role;
parts.push(`[${msg.role}] ${author}: ${msg.content}`);
parts.push("");
}
parts.push("---");
parts.push("");
}
// Add runtime context
parts.push(`Discord channel: #${channelName}`);
parts.push(`Date: ${new Date().toISOString().split("T")[0]}`);
parts.push("");
// Add the actual user message
parts.push(`New message:`);
parts.push(userMessage);
return parts.join("\n");
}
/**
* Parses the JSON output from `claude -p --output-format json`.
* Extracts the text content from the response.
*
* The JSON output format contains a "result" field with the response text,
* or an array of content blocks with type "text".
*/
function parseClaudeJsonOutput(raw) {
const trimmed = raw.trim();
if (!trimmed) {
return "(no response from agent)";
}
try {
const parsed = JSON.parse(trimmed);
// Format 1: { result: "text" }
if (typeof parsed.result === "string") {
return parsed.result;
}
// Format 2: { content: [{ type: "text", text: "..." }] } or similar
if (Array.isArray(parsed.content)) {
const texts = [];
for (const block of parsed.content) {
if (block && block.type === "text" && typeof block.text === "string") {
texts.push(block.text);
}
}
if (texts.length > 0) {
return texts.join("\n");
}
}
// Format 3: Direct array of content blocks
if (Array.isArray(parsed)) {
const texts = [];
for (const block of parsed) {
if (block && block.type === "text" && typeof block.text === "string") {
texts.push(block.text);
}
}
if (texts.length > 0) {
return texts.join("\n");
}
}
// Fallback: stringify the whole response
return typeof parsed === "string" ? parsed : JSON.stringify(parsed, null, 2);
}
catch {
// Not valid JSON -- return the raw output as plain text
return trimmed;
}
}
/**
* Runs the Claude Code CLI as a child process and returns the text response.
*
* The CLI is invoked with:
* claude -p "prompt" --output-format json
*
* The working directory is set to the workspace path so Claude Code
* automatically reads the CLAUDE.md file for agent identity and context.
*
* Timeout defaults to 120 seconds. The process is killed if it exceeds
* the timeout.
*/
async function runAgent(options) {
const { workspacePath, channelName, userMessage, conversationHistory, abortController, } = options;
// Verify the workspace has an agent.yaml (validates it exists) and get agent name
const identity = (0, identity_1.loadAgentIdentity)(workspacePath);
const claudeCommand = await (0, resolve_claude_1.resolveClaude)();
const prompt = buildPrompt(userMessage, conversationHistory, channelName);
const timeoutMs = resolveTimeoutMs();
// Load config for env_blocklist
let envBlocklist = [];
try {
const rootDir = path.resolve(__dirname, "../..");
const config = (0, loader_1.loadDisclawConfig)(rootDir);
envBlocklist = config.env_blocklist;
}
catch {
// Config not available, use empty blocklist
}
return new Promise((resolve, reject) => {
// IMPORTANT: --bare prevents CLAUDE.md walk-up through parent directories.
// Without this flag every agent inherits the DisClaw project CLAUDE.md as
// its identity (and any other CLAUDE.md files up the directory tree).
// --append-system-prompt-file injects the workspace-specific CLAUDE.md
// explicitly so each agent keeps its own isolated identity.
const args = [
"-p",
prompt,
"--output-format",
"json",
"--bare",
"--append-system-prompt-file",
path.join(workspacePath, "CLAUDE.md"),
];
const child = (0, cross_spawn_1.default)(claudeCommand, args, {
cwd: workspacePath,
env: (0, env_1.sanitizedEnv)({ DISCLAW_AGENT_NAME: identity.name }, envBlocklist),
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
let stdout = "";
let stderr = "";
let killed = false;
// Collect stdout
if (child.stdout) {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (data) => {
stdout += data;
});
}
// Collect stderr
if (child.stderr) {
child.stderr.setEncoding("utf8");
child.stderr.on("data", (data) => {
stderr += data;
});
}
// Timeout handling
const timer = setTimeout(() => {
killed = true;
child.kill("SIGTERM");
// Give it a moment to die gracefully, then force kill
setTimeout(() => {
try {
child.kill("SIGKILL");
}
catch {
// Already dead
}
}, 5000);
}, timeoutMs);
// Abort controller support
if (abortController) {
abortController.signal.addEventListener("abort", () => {
killed = true;
child.kill("SIGTERM");
});
}
child.on("error", (err) => {
clearTimeout(timer);
if (err.code === "ENOENT") {
reject(new Error(`Claude Code CLI not found at "${claudeCommand}". ` +
`Install it with: npm install -g @anthropic-ai/claude-code\n` +
`Or set CLAUDE_PATH in your .env file.`));
}
else {
reject(new Error(`Failed to start Claude Code CLI: ${err.message}`));
}
});
child.on("close", (code) => {
clearTimeout(timer);
if (killed) {
reject(new Error(`Claude Code CLI timed out after ${timeoutMs / 1000} seconds.`));
return;
}
if (code !== 0) {
const errorDetail = stderr.trim() || stdout.trim() || `exit code ${code}`;
reject(new Error(`Claude Code CLI error: ${errorDetail}`));
return;
}
const response = parseClaudeJsonOutput(stdout);
resolve(response);
});
// Close stdin immediately since we pass everything via args
if (child.stdin) {
child.stdin.end();
}
});
}
//# sourceMappingURL=runner.js.map