From 8bb11b19d8f5273729a947a0805251f08719d845 Mon Sep 17 00:00:00 2001 From: tanmay11k Date: Sun, 22 Feb 2026 01:07:02 -0500 Subject: [PATCH] Initial commit: Discord-Claude Gateway with event-driven agent runtime --- references/nanoclaw | 1 + src/agent-runtime.ts | 108 +++++++++++++++++++++++++------------------ src/discord-bot.ts | 5 +- 3 files changed, 68 insertions(+), 46 deletions(-) create mode 160000 references/nanoclaw diff --git a/references/nanoclaw b/references/nanoclaw new file mode 160000 index 0000000..1980d97 --- /dev/null +++ b/references/nanoclaw @@ -0,0 +1 @@ +Subproject commit 1980d97d90971f8979b2d1277c1b8db67803b08a diff --git a/src/agent-runtime.ts b/src/agent-runtime.ts index e5feacc..6dc6ba2 100644 --- a/src/agent-runtime.ts +++ b/src/agent-runtime.ts @@ -1,4 +1,4 @@ -import { execFile } from "node:child_process"; +import { spawn } from "node:child_process"; import { writeFile, unlink } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -171,70 +171,88 @@ export class AgentRuntime { sessionId?: string, ): Promise { return new Promise((resolve, reject) => { + // Build args — keep it minimal and match what works on the CLI directly const args: string[] = [ "-p", promptText, "--output-format", "json", - "--system-prompt-file", systemPromptFile, "--dangerously-skip-permissions", + "--append-system-prompt-file", systemPromptFile, + "--verbose", ]; if (sessionId) { args.push("--resume", sessionId); } - if (this.config.allowedTools.length > 0) { - args.push("--allowedTools", ...this.config.allowedTools); + // --allowedTools expects each tool as a separate quoted arg + for (const tool of this.config.allowedTools) { + args.push("--allowedTools", tool); } 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( - this.config.claudeCliPath, - args, - { - timeout: this.config.queryTimeoutMs, - maxBuffer: 10 * 1024 * 1024, - encoding: "utf-8", - }, - (error, stdout, stderr) => { - 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") { - reject(new Error("Query timed out")); - return; - } - reject(new Error(`Claude CLI error: ${stderr || error.message}`)); + const child = spawn(this.config.claudeCliPath, args, { + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data: Buffer) => { + const chunk = data.toString(); + stderr += chunk; + // Log stderr in real-time so we can see what's happening + 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; } - console.log(`[DEBUG] Claude CLI stdout (${stdout.length} chars): ${stdout.slice(0, 300)}...`); - if (stderr) { - console.log(`[DEBUG] Claude CLI stderr: ${stderr.slice(0, 300)}`); - } - - 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(), - }); - } - }, - ); + resolve(parsed); + } catch { + resolve({ + type: "result", + result: stdout.trim(), + }); + } + }); child.on("error", (err) => { + clearTimeout(timer); console.error(`[DEBUG] Failed to spawn Claude CLI: ${err.message}`); reject(new Error(`Failed to spawn Claude CLI: ${err.message}`)); }); diff --git a/src/discord-bot.ts b/src/discord-bot.ts index 9158806..dd51f48 100644 --- a/src/discord-bot.ts +++ b/src/discord-bot.ts @@ -21,7 +21,10 @@ export function shouldIgnoreMessage(message: { author: { bot: boolean } }): bool } 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 {