Initial commit: Discord-Claude Gateway with event-driven agent runtime
This commit is contained in:
1
references/nanoclaw
Submodule
1
references/nanoclaw
Submodule
Submodule references/nanoclaw added at 1980d97d90
@@ -1,4 +1,4 @@
|
|||||||
import { execFile } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { writeFile, unlink } from "node:fs/promises";
|
import { writeFile, unlink } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
@@ -171,70 +171,88 @@ export class AgentRuntime {
|
|||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
): Promise<ClaudeJsonResponse> {
|
): Promise<ClaudeJsonResponse> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// Build args — keep it minimal and match what works on the CLI directly
|
||||||
const args: string[] = [
|
const args: string[] = [
|
||||||
"-p", promptText,
|
"-p", promptText,
|
||||||
"--output-format", "json",
|
"--output-format", "json",
|
||||||
"--system-prompt-file", systemPromptFile,
|
|
||||||
"--dangerously-skip-permissions",
|
"--dangerously-skip-permissions",
|
||||||
|
"--append-system-prompt-file", systemPromptFile,
|
||||||
|
"--verbose",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
args.push("--resume", sessionId);
|
args.push("--resume", sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.allowedTools.length > 0) {
|
// --allowedTools expects each tool as a separate quoted arg
|
||||||
args.push("--allowedTools", ...this.config.allowedTools);
|
for (const tool of this.config.allowedTools) {
|
||||||
|
args.push("--allowedTools", tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push("--max-turns", "25");
|
args.push("--max-turns", "25");
|
||||||
|
|
||||||
console.log(`[DEBUG] Spawning Claude CLI: ${this.config.claudeCliPath} -p "${promptText}" --output-format json --system-prompt-file ${systemPromptFile} ... (${args.length} args total)`);
|
console.log(`[DEBUG] Spawning: ${this.config.claudeCliPath} args=${JSON.stringify(args.slice(0, 8))}... (${args.length} total)`);
|
||||||
|
|
||||||
const child = execFile(
|
const child = spawn(this.config.claudeCliPath, args, {
|
||||||
this.config.claudeCliPath,
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
args,
|
});
|
||||||
{
|
|
||||||
timeout: this.config.queryTimeoutMs,
|
let stdout = "";
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
let stderr = "";
|
||||||
encoding: "utf-8",
|
|
||||||
},
|
child.stdout.on("data", (data: Buffer) => {
|
||||||
(error, stdout, stderr) => {
|
stdout += data.toString();
|
||||||
if (error) {
|
});
|
||||||
console.error(`[DEBUG] Claude CLI error: code=${error.code}, killed=${error.killed}, stderr=${stderr?.slice(0, 500)}`);
|
|
||||||
if (error.killed || error.code === "ETIMEDOUT") {
|
child.stderr.on("data", (data: Buffer) => {
|
||||||
reject(new Error("Query timed out"));
|
const chunk = data.toString();
|
||||||
return;
|
stderr += chunk;
|
||||||
}
|
// Log stderr in real-time so we can see what's happening
|
||||||
reject(new Error(`Claude CLI error: ${stderr || error.message}`));
|
if (chunk.trim()) {
|
||||||
|
console.log(`[DEBUG] Claude stderr: ${chunk.trim().slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.log(`[DEBUG] Timeout reached, killing Claude CLI process`);
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
reject(new Error("Query timed out"));
|
||||||
|
}, this.config.queryTimeoutMs);
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
|
||||||
|
console.log(`[DEBUG] Claude CLI exited: code=${code}, stdout=${stdout.length} chars`);
|
||||||
|
if (stdout.length > 0) {
|
||||||
|
console.log(`[DEBUG] Claude stdout preview: ${stdout.slice(0, 300)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0 && code !== null) {
|
||||||
|
reject(new Error(`Claude CLI error (exit ${code}): ${stderr.slice(0, 500) || "unknown error"}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lines = stdout.trim().split("\n");
|
||||||
|
const lastLine = lines[lines.length - 1];
|
||||||
|
const parsed = JSON.parse(lastLine) as ClaudeJsonResponse;
|
||||||
|
|
||||||
|
if (parsed.is_error) {
|
||||||
|
reject(new Error(`Claude error: ${parsed.result ?? "Unknown error"}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DEBUG] Claude CLI stdout (${stdout.length} chars): ${stdout.slice(0, 300)}...`);
|
resolve(parsed);
|
||||||
if (stderr) {
|
} catch {
|
||||||
console.log(`[DEBUG] Claude CLI stderr: ${stderr.slice(0, 300)}`);
|
resolve({
|
||||||
}
|
type: "result",
|
||||||
|
result: stdout.trim(),
|
||||||
try {
|
});
|
||||||
const lines = stdout.trim().split("\n");
|
}
|
||||||
const lastLine = lines[lines.length - 1];
|
});
|
||||||
const parsed = JSON.parse(lastLine) as ClaudeJsonResponse;
|
|
||||||
|
|
||||||
if (parsed.is_error) {
|
|
||||||
reject(new Error(`Claude error: ${parsed.result ?? "Unknown error"}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(parsed);
|
|
||||||
} catch (parseError) {
|
|
||||||
resolve({
|
|
||||||
type: "result",
|
|
||||||
result: stdout.trim(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
child.on("error", (err) => {
|
child.on("error", (err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
console.error(`[DEBUG] Failed to spawn Claude CLI: ${err.message}`);
|
console.error(`[DEBUG] Failed to spawn Claude CLI: ${err.message}`);
|
||||||
reject(new Error(`Failed to spawn Claude CLI: ${err.message}`));
|
reject(new Error(`Failed to spawn Claude CLI: ${err.message}`));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ export function shouldIgnoreMessage(message: { author: { bot: boolean } }): bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractPromptFromMention(content: string, botId: string): string {
|
export function extractPromptFromMention(content: string, botId: string): string {
|
||||||
return content.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim();
|
// Remove user mentions (<@ID> or <@!ID>) and role mentions (<@&ID>) for the bot
|
||||||
|
return content
|
||||||
|
.replace(/<@[!&]?\d+>/g, "")
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DiscordBot {
|
export class DiscordBot {
|
||||||
|
|||||||
Reference in New Issue
Block a user