import { MatrixClient, SimpleFsStorageProvider } from "matrix-bot-sdk"; import { runChatTask } from "./runner.js"; import { routeMessage, chatReply } from "./router.js"; const HOMESERVER_URL = process.env.MATRIX_HOMESERVER_URL; const ACCESS_TOKEN = process.env.MATRIX_BOT_TOKEN; const CONTROL_ROOM_ID = process.env.MATRIX_CONTROL_ROOM_ID; const GITEA_URL = process.env.GITEA_URL; // Set explicitly rather than fetched via client.getUserId() — that call hits /whoami, // which (like /joined_rooms before it) 404s against Continuwuity for reasons unrelated // to the endpoint itself. This is also the only reliable way to filter the bot's own // messages now that there's no command prefix to naturally exclude them by. const BOT_USER_ID = process.env.MATRIX_BOT_USER_ID; const KNOWN_REPOS = (process.env.KNOWN_REPOS || "") .split(",") .map((r) => r.trim()) .filter(Boolean); const MAX_REPLY_LENGTH = 4000; function truncate(text) { if (text.length <= MAX_REPLY_LENGTH) return text; return `${text.slice(0, MAX_REPLY_LENGTH)}\n\n[truncated]`; } export async function startMatrixBot() { if (!HOMESERVER_URL || !ACCESS_TOKEN || !CONTROL_ROOM_ID) { console.warn("Matrix env vars not set — skipping bot startup"); return; } if (!BOT_USER_ID) { console.warn("MATRIX_BOT_USER_ID not set — bot could reply to its own messages, skipping startup"); return; } const storage = new SimpleFsStorageProvider("/workspace/matrix-bot-storage.json"); const client = new MatrixClient(HOMESERVER_URL, ACCESS_TOKEN, storage); client.on("room.invite", async (roomId) => { try { await client.joinRoom(roomId); } catch (err) { console.error("failed to join invited room", roomId, err.message); } }); client.on("room.message", async (roomId, event) => { if (roomId !== CONTROL_ROOM_ID) return; if (event.sender === BOT_USER_ID) return; const body = event.content?.body; if (!body) return; try { const decision = await routeMessage(body, KNOWN_REPOS); if (decision.type === "code_task") { const [owner, repo] = decision.repo.split("/"); await client.sendText(roomId, `Working on it: ${decision.repo} — ${decision.instruction}`); const cloneUrl = `${GITEA_URL}/${owner}/${repo}.git`; const pr = await runChatTask({ owner, repo, cloneUrl, instruction: decision.instruction }); await client.sendText(roomId, `Opened PR: ${pr.html_url}`); return; } const reply = await chatReply(body); await client.sendText(roomId, truncate(reply)); } catch (err) { console.error("message handling failed", err); await client.sendText(roomId, `Failed: ${err.message}`); } }); await client.start(); console.log("Matrix bot started, room:", CONTROL_ROOM_ID); }