// Pure /goal command parsing. No I/O, no pi imports. // // Verbs (Q1, locked with Jason 2026-08-28): // bare -> status // stop -> pause the loop, goal retained // clear -> remove the goal entirely // resume -> continue a paused goal // -> set (or replace) the active goal // --max N / --max=N run-limit option on set (Q4); remainder is the goal text // // Only the exact words stop/clear/resume are verbs; anything else is goal text. export type GoalCommand = | { kind: "status" } | { kind: "stop" } | { kind: "clear" } | { kind: "resume" } | { kind: "set"; text: string; max?: number; waitTimeoutSeconds?: number } | { kind: "error"; message: string }; export function parseGoalCommand(raw: string): GoalCommand { const input = (raw ?? "").trim(); if (input === "") return { kind: "status" }; const lower = input.toLowerCase(); if (lower === "stop") return { kind: "stop" }; if (lower === "clear") return { kind: "clear" }; if (lower === "resume") return { kind: "resume" }; let max: number | undefined; let text = input; let waitTimeoutSeconds: number | undefined; const waitOption = text.match(/(?:^|\s)--wait-timeout(?:=|\s+)([^\s]+)(?=\s|$)/); if (waitOption) { const n = Number(waitOption[1]); if (!Number.isInteger(n) || n < 10 || n > 86400 || String(n) !== waitOption[1]) { return { kind: "error", message: "--wait-timeout must be an integer from 10 to 86400 seconds" }; } waitTimeoutSeconds = n; text = text.replace(waitOption[0], " ").trim(); } if (/(?:^|\s)--wait-timeout(?:=|\s|$)/.test(text)) { return { kind: "error", message: "--wait-timeout requires one value from 10 to 86400 seconds" }; } const inline = text.match(/(?:^|\s)--max=([^\s]+)(?:\s|$)/); if (inline) { const parsed = parseMax(inline[1]); if (typeof parsed === "string") return { kind: "error", message: parsed }; max = parsed; text = text.replace(inline[0], " ").trim(); } else { const spaced = text.match(/(?:^|\s)--max\s+([^\s]+)(?:\s|$)/); if (spaced) { const parsed = parseMax(spaced[1]); if (typeof parsed === "string") return { kind: "error", message: parsed }; max = parsed; text = text.replace(spaced[0], " ").trim(); } else if (/(?:^|\s)--max(?:\s|$)/.test(text)) { return { kind: "error", message: '--max requires a positive integer (e.g. "--max 40")' }; } } text = text.trim(); if (text === "") { return { kind: "error", message: "goal text required (verbs: stop, clear, resume)" }; } return { kind: "set", text, max, ...(waitTimeoutSeconds === undefined ? {} : { waitTimeoutSeconds }) }; } function parseMax(raw: string): number | string { const n = Number.parseInt(raw, 10); if (!Number.isInteger(n) || n < 1 || String(n) !== raw) { return `--max must be a positive integer, got "${raw}"`; } return n; }