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
JavaScript
/**
* 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