UNPKG

@phren/cli

Version:

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

135 lines (134 loc) • 7.93 kB
import { type Json, type Provider, type Target } from "./protocol.js"; export type DeliveryOutcome = "delivered" | "pending"; /** What a phone message is known to have become. `queued`: typed into the * pane and held by the agent until its turn ends. `failed` carries a reason * and may still turn `delivered` if the agent takes it late. */ export type DeliveryState = "queued" | "delivered" | "failed"; /** A transcript row carrying a message's words: its byte offset, and whether the agent fires UserPromptSubmit for it (a user row does; a mid-turn queue removal does not). */ export interface TranscriptEntry { offset: number; hook: boolean; } export type TrackedState = DeliveryState | "unknown"; export interface DeliveryStatus { state: TrackedState; reason?: string; session?: string; } /** A message is kept this long, by its id, so a late status or a retry still * finds it: a busy Codex turn can run far past the old ten minutes. */ export declare const DELIVERY_RETENTION_MS: number; /** An agent that ended its turn after the message was typed and then stayed * idle this long without taking it has dropped it. */ export declare const DELIVERY_GIVE_UP_MS = 30000; /** What the agent hands its UserPromptSubmit hook is the terminal's pasted * form of what Phren typed; compare the words, not the wrapping. Claude Code * also takes each attached picture's path line out of the text and puts an * "[Image #N]" label at the front, so neither side keeps picture paths or * labels. */ export declare function promptKey(text: string): string; export declare const promptHash: (text: string) => string; /** One message per record, bound to the pane it was typed into (server, pane, * agent kind), not to the conversation: a Codex thread or a Claude /clear can * replace the conversation while the agent still holds the typed text, and * the conversation that submits it is the one it reached. Records hold a hash * of the words, never the text, and those with a phone id are written to * `file` so a Hook restart keeps their state. */ export declare class PromptDeliveries { private file?; private now; private landed; private records; private waiters; private writing; /** Panes (server, pane, agent) that sent a UserPromptSubmit or Stop since the Hook started. */ private hookPanes; private paneKey; private static open; /** Open, or failed with its own hook's claim still waiting for its row (a late delivery). */ private static claimable; constructor(file?: string | undefined, now?: () => number, landed?: (record: { source: Provider; session: string; hash: string; at: number; }) => Promise<TranscriptEntry[]>); private samePane; private byId; private prune; private save; /** Settled writes, for tests and an orderly shutdown. */ flush(): Promise<void>; /** Register a message about to be typed into `target`'s pane. Settles * "delivered" once a conversation in that pane submits the words, or * "pending" after `waitMs`; the record outlives the wait. A retry of a * failed id replaces its record. Aborting (the terminal refused before * writing) drops it. */ expect(target: Target, text: string, waitMs: number, signal?: AbortSignal, id?: string, terminal?: string): Promise<DeliveryOutcome>; private release; private open; /** A message Phren typed into `target`'s pane that no conversation there has submitted yet. */ pending(target: Target, text: string): boolean; /** Wait again for a message `expect` reported pending, after a second Enter. */ awaitLate(target: Target, text: string, waitMs?: number): Promise<DeliveryOutcome>; private find; /** The Hook answered `id` as typed and held by the agent. */ queue(id: string, target: Target): void; /** Delivered without a hook: an app-server turn or a served OpenCode prompt. */ delivered(id: string | undefined, target: Target, text: string): void; /** The message did not reach the agent, and why. It stays matchable, so a * late submission still turns it delivered. */ fail(id: string | undefined, target: Target, reason: string): void; /** The pane's Codex server moved to another thread (/new, /resume): * input still pending on the old one stays there, out of the pane's view. * Fails loudly; a late UserPromptSubmit from the old thread still settles it. */ threadChanged(previous: Target): void; /** False when `id` names another message: other words or another pane. */ sameMessage(id: string, target: Pick<Target, "server" | "pane" | "source">, text: string): boolean; /** What became of phone message `id` in `target`'s pane. `unknown` while the * Hook has not answered it, for another pane, or past retention. */ status(id: string, target: Pick<Target, "server" | "pane" | "source">): DeliveryStatus; /** Every known phone message in `target`'s pane, for its conversation's stream. */ forPane(target: Pick<Target, "server" | "pane" | "source">): ({ deliveryId: string; } & DeliveryStatus)[]; /** Conversation `target` just submitted `prompt`. Its own record wins, then * the oldest open one in the same pane: the agent kept the text across a * conversation change, and the conversation now in the pane is where it * went. A prompt nothing matches was typed locally. Never refused. * For Claude the transcript is consulted first and the submission claims * the earliest unclaimed user row with these words, through the same * `settle` and the same table of claimed entries the idle check uses: a * callback for a row already claimed (the idle check settled it, the hook * came late) settles nothing more. */ submitted(target: Target, prompt: string): Promise<void>; /** Marks `record` delivered, with the transcript entry that settled it when known. */ private settle; /** Whether the transcript shows the agent took `record`'s message, claiming the entry it settled with. * An entry settles one message: the table of records' `entry` offsets (per conversation) is the only * state, so neither this path nor the hook's can settle two messages with one turn. A user row is a * turn start whose UserPromptSubmit hook settles the message itself, so while the agent works only a * mid-turn queue removal (no hook fires for it) settles from the transcript; once the agent is idle * (`working` false) a user row settles too, for a hook that never came. Of several identical open * messages the oldest take the entries. `pass` carries the entries still unclaimed within one observe. */ private taken; /** A turn in `target`'s pane ended: the agent now takes held input or never will. */ stopped(target: Target): void; /** Called on the Hook's activity tick with a server's panes. A message * still held fails loudly when its pane closed, its agent restarted, or the * agent ended a turn and stayed idle without taking it (unless its * transcript shows it after all). */ observe(server: string, panes: Json[]): Promise<void>; } /** The byte offsets of the user rows with these words that reached the conversation's transcript * after the message was typed: the agent took it without its hook saying so * (a mid-turn steer). Reads at most the last 512 KiB. */ /** The byte offsets (in the file, `start` being the buffer's own offset) of the rows in `buffer` * whose user text has `record`'s words, newest first. One offset per row. */ export declare function matchingOffsets(buffer: Buffer, start: number, record: { hash: string; at: number; }): TranscriptEntry[]; /** A transcript row's user-typed text: Claude's user rows, Codex's * user_message events and user response items. */ export declare function userTexts(row: Json): string[];