UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

58 lines (57 loc) • 2.63 kB
import { objects } from "./protocol.js"; /** * Codex's own answer to an async question (`request_user_input_async`), the * user message its TUI sends into the running turn: * * <send_user_message_question_reply> * [{"answer":"…","question":"…","questionItemId":"[\"request_user_input_async\",\"call_…\",0]"}] * </send_user_message_question_reply> * * The Hook sends the same message for the phone, reads it back to know which * questions are answered, and shows it in chat as the question and answer. */ const OPEN = "<send_user_message_question_reply>", CLOSE = "</send_user_message_question_reply>"; export function formatQuestionReply(id, entries) { return `${OPEN}\n${JSON.stringify(entries.map((entry, index) => ({ answer: entry.answer, question: entry.question, questionItemId: JSON.stringify(["request_user_input_async", id, index]) })))}\n${CLOSE}`; } /** The entries of a question reply, or undefined for any other message. * Anything after the closing tag (the files attached to the answer, which * Codex joins into the same message) is not part of the entries. */ export function parseQuestionReply(text) { const trimmed = text.trim(), end = trimmed.indexOf(CLOSE); if (!trimmed.startsWith(OPEN) || end < 0) return undefined; let rows; try { rows = JSON.parse(trimmed.slice(OPEN.length, end)); } catch { return undefined; } if (!Array.isArray(rows)) return undefined; const entries = objects(rows).flatMap(row => { if (typeof row.answer !== "string" || typeof row.question !== "string") return []; let id; try { const item = JSON.parse(String(row.questionItemId)); if (Array.isArray(item) && typeof item[1] === "string") id = item[1]; } catch { /* An answer without a readable item id still reads as text. */ } return [{ answer: row.answer, question: row.question, ...(id ? { id } : {}) }]; }); return entries.length ? entries : undefined; } /** A question reply as chat text: each question quoted, then its answer, * then whatever followed the reply in the same text. */ export function readableQuestionReply(text) { const entries = parseQuestionReply(text); if (!entries) return undefined; const trimmed = text.trim(), rest = trimmed.slice(trimmed.indexOf(CLOSE) + CLOSE.length).trim(); const answers = entries.map(entry => entry.question.split("\n").map(line => `> ${line}`).join("\n") + "\n\n" + entry.answer).join("\n\n"); return rest ? `${answers}\n\n${rest}` : answers; }