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.

91 lines 4.12 kB
/** * What a Discord message should do (#680). Pure: it takes the text plus a snapshot of the * project's state and returns an action, so every routing rule is unit-testable without a * gateway, a daemon, or a run. The side effects live in `bot.ts`. */ /** The help text, also sent when a message cannot be understood as an answer to a gate. */ export const HELP = [ 'The Framework, at your service:', '· send anything else to start a session, or to message the one already running', '· `!status` — what is running right now', '· `!stop` — stop the running session', '· `!help` — this', 'When a session asks a question, reply with the option number.', ].join('\n'); /** Render a parked gate as a numbered question, so it can be answered by number in chat. */ export function renderGate(gate) { const lines = [`**${gate.title}**`]; gate.options.forEach((option, index) => lines.push(`${index + 1}. ${option.label}`)); lines.push(gate.multi ? '_Reply with the numbers, e.g. `1,3`._' : '_Reply with the number._'); return lines.join('\n'); } /** * Resolve an answer to a gate. Accepts option numbers (`2`, or `1,3` for a multi-select) and an * exact option label or id, case-insensitively. `undefined` when the text is not an answer — * the caller then treats it as an ordinary message rather than guessing, because picking the * wrong option on someone's behalf is worse than asking again. */ export function resolvePick(gate, text) { const trimmed = text.trim(); if (!trimmed) return undefined; const parts = gate.multi ? trimmed.split(',').map(part => part.trim()).filter(Boolean) : [trimmed]; const picked = []; for (const part of parts) { const byNumber = /^\d+$/.test(part) ? gate.options[Number(part) - 1] : undefined; const byName = gate.options.find(option => option.label.toLowerCase() === part.toLowerCase() || option.id.toLowerCase() === part.toLowerCase()); const option = byNumber ?? byName; if (!option) return undefined; picked.push(option.id); } if (picked.length === 0) return undefined; return gate.multi ? picked : picked[0]; } /** Decide what a message means. See the module comment. */ export function decideAction(text, ctx) { const trimmed = text.trim(); const command = trimmed.toLowerCase(); if (command === '!help') return { kind: 'reply', reply: HELP }; if (command === '!status') { if (!ctx.live) return { kind: 'reply', reply: `Nothing running in **${ctx.target.name}**.` }; const running = `Running in **${ctx.target.name}** (\`${ctx.live.runId}\`).`; return { kind: 'reply', reply: ctx.live.gate ? `${running}\n\n${renderGate(ctx.live.gate)}` : running }; } if (command === '!stop') { if (!ctx.live) return { kind: 'reply', reply: 'Nothing to stop.' }; return { kind: 'stop', projectId: ctx.live.projectId, runId: ctx.live.runId, reply: 'Stopping the session.' }; } // A parked run is asking something, so read the message as the answer first. if (ctx.live?.gate) { const gate = ctx.live.gate; const pick = resolvePick(gate, trimmed); if (pick !== undefined) { const shown = Array.isArray(pick) ? pick.join(', ') : pick; return { kind: 'choice', projectId: ctx.live.projectId, runId: ctx.live.runId, gateId: gate.id, pick, reply: `Picked **${shown}**.`, }; } // Not an answer: pass it through as a message rather than guessing at the gate. } if (ctx.live) { return { kind: 'message', projectId: ctx.live.projectId, runId: ctx.live.runId, text: trimmed, reply: 'Sent to the running session.', }; } return { kind: 'start', projectId: ctx.target.id, text: trimmed, reply: `Starting a session in **${ctx.target.name}**.` }; } //# sourceMappingURL=routing.js.map