UNPKG

@phren/cli

Version:

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

73 lines (72 loc) • 4.13 kB
import { object, objects } from "./protocol.js"; /** The approval card's tool and input for an app-server request, in the * shapes `approvalSummary` and the phone already read for Codex's hook. * Undefined for questions (`item/tool/requestUserInput`) and MCP * elicitations, which stay in the pane. */ export function appServerApproval(request) { const params = request.params; const reason = typeof params.reason === "string" && params.reason ? { reason: params.reason } : {}; if (request.method === "item/commandExecution/requestApproval") { return { tool: "Bash", input: { ...(typeof params.command === "string" ? { command: params.command } : {}), ...(typeof params.cwd === "string" ? { cwd: params.cwd } : {}), ...reason } }; } if (request.method === "item/fileChange/requestApproval") { const changes = objects(params.changes); const verb = (change) => object(change.kind).type === "add" ? "Add" : object(change.kind).type === "delete" ? "Delete" : "Update"; const patch = changes.filter(change => typeof change.path === "string").map(change => `*** ${verb(change)} File: ${change.path}`).join("\n"); return { tool: "apply_patch", input: { ...(patch ? { patch } : {}), ...(typeof params.grantRoot === "string" ? { grantRoot: params.grantRoot } : {}), ...reason } }; } if (request.method === "item/permissions/requestApproval") return { tool: "Permissions", input: { permissions: params.permissions ?? {}, ...reason } }; return undefined; } /** The app-server's answer for the phone's allow or deny (T3's * CodexSessionRuntime shapes: accept / decline, a permission grant or none). */ export function appServerDecision(request, allow) { if (request.method === "item/permissions/requestApproval") return { permissions: allow ? object(request.params.permissions) : {}, scope: "turn" }; return { decision: allow ? "accept" : "decline" }; } /** A conductor `dispatch` or `hand_off` permission ask the Hook can answer * itself under a standing grant, or offer the phone two grant-writing answers. */ export function conductorCall(tool, input) { // MCP tool names vary by harness: `dispatch`, `phren.dispatch`, // `mcp__phren__dispatch`; phren_admin carries the action in its fields. const tail = /(?:^|[.:/]|__)(dispatch|hand_off|phren_admin)$/.exec(tool.replace(/^mcp_phren_/, ""))?.[1]; const fields = object(input); let action; if (tail === "dispatch") action = "dispatch"; else if (tail === "hand_off") action = "hand_off"; else if (tail === "phren_admin" && (fields.action === "dispatch" || fields.action === "hand_off")) action = fields.action; if (!action) return undefined; const project = typeof fields.project === "string" ? fields.project : undefined; const computer = typeof fields.computer === "string" ? fields.computer : undefined; return { action, ...(project ? { project } : {}), ...(computer ? { computer } : {}) }; } /** The decisions an OpenCode permission ask offers the phone. OpenCode's * `always` covers the current session, so both grant answers reply `always`; * `allow-everywhere` additionally writes an allow rule into the user's * OpenCode config. Advertised as the approval card's `options`, the same * shape the phone already renders for a provider-supplied option list. */ export const OPENCODE_APPROVAL_OPTIONS = [ { label: "Allow once", decision: "approve" }, { label: "Allow for this project", decision: "allow-project" }, { label: "Allow everywhere", decision: "allow-everywhere" }, { label: "Deny", decision: "deny" }, ]; /** The OpenCode permission reply a card decision maps to: both grant scopes * are `always`, a plain approve is `once`, a deny is `reject`. Undefined for * anything else. */ export function opencodePermissionReply(decision) { if (decision === "approve") return "once"; if (decision === "deny") return "reject"; if (decision === "allow-project" || decision === "allow-everywhere") return "always"; return undefined; }