@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
135 lines (134 loc) • 7.93 kB
TypeScript
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[];