UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

94 lines 4.21 kB
import { DiscordGateway } from './gateway.js'; import { postMessage } from './rest.js'; import { decideAction } from './routing.js'; import { errorMessage } from '../error-message.js'; /** * The Discord chatbot (#680): chat to The Framework from Discord instead of the dashboard. * * Every effect is an injected function, so the whole bot is testable by handing it a fake * message — the routing decisions live in `routing.ts` and are pure, and this module is only * the wiring. Same seam-and-`stop()` shape as the intervention/activity watchers. * * Chat history is not written here: a message routed into a run reaches that run's conversation * through the control channel, and the run commits it to `.the-framework/conversations/` (#908). * That is the answer to the question #680 asked — the Git repo, via the run that received it. */ /** * How a turn that arrived here is attributed in the committed conversation (#917). Named once, * here, so the surface owns its own name rather than the daemon spelling it inline twice. */ export const DISCORD_VIA = 'discord'; /** * Connect the bot and route messages. Never throws: a chat integration that can take the daemon * down is worse than one that is quiet. */ export function startDiscordBot(opts) { const log = (message) => opts.onLog?.(message); const handleMessage = async (message) => { try { if (opts.channelId && message.channelId !== opts.channelId) return; if (opts.enabled && !(await opts.enabled())) return; const target = await opts.target(); if (!target) { await reply(message, 'No project is registered with The Framework yet.'); return; } const live = await opts.liveRun(target.id).catch(() => undefined); const action = decideAction(message.content, { live, target }); switch (action.kind) { case 'choice': await opts.sendChoice(action.projectId, action.gateId, action.pick, action.runId); break; case 'message': // Bind before sending (#932): the answer to this message must land after the baseline. await opts.onRunBound?.(action.runId, message.channelId).catch(() => { }); await opts.sendMessage(action.projectId, action.text, action.runId); break; case 'stop': await opts.sendStop(action.projectId, action.runId); break; case 'start': { const runId = await opts.start(action.projectId, action.text); if (!runId) { await reply(message, 'Could not start a session. It may already be busy.'); return; } // A run that did not exist a moment ago has nothing to adopt, so binding after the start // is still a clean baseline, and it is the first point its id is known. await opts.onRunBound?.(runId, message.channelId).catch(() => { }); break; } case 'reply': break; } await reply(message, action.reply); } catch (err) { log(`Discord bot could not handle a message: ${errorMessage(err)}`); } }; const reply = async (to, content) => { const sent = await postMessage(opts.token, to.channelId, content, { replyToId: to.id, ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}), }); if (!sent) log('Discord bot could not post a reply'); }; const gateway = new DiscordGateway(opts.token, { onMessage: message => void handleMessage(message), onReady: id => log(`Discord bot online (${id})`), onLog: log, }, opts.gateway ?? {}); gateway.connect(); return { stop: () => { gateway.stop(); log('Discord bot offline'); }, handleMessage, }; } //# sourceMappingURL=bot.js.map