@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
397 lines (396 loc) • 15.8 kB
TypeScript
import type { ConductorSuccessionService } from "./conductor-succession.js";
import { z } from "zod";
import { moveToSchema } from "./dispatch.js";
import { type HarnessInventory } from "./harnesses.js";
import { type Json, type Provider, type StartingTarget, type Target } from "./protocol.js";
import type { TerminalProvider } from "./terminal.js";
export { moveToSchema };
export type MoveTo = z.infer<typeof moveToSchema>;
export declare const moveRequestSchema: z.ZodObject<{
target: z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
session: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
}, z.core.$strip>;
to: z.ZodObject<{
computer: z.ZodOptional<z.ZodString>;
harness: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
}>;
account: z.ZodOptional<z.ZodString>;
model: z.ZodOptional<z.ZodString>;
effort: z.ZodOptional<z.ZodEnum<{
high: "high";
low: "low";
max: "max";
medium: "medium";
minimal: "minimal";
xhigh: "xhigh";
}>>;
}, z.core.$strict>;
mode: z.ZodDefault<z.ZodEnum<{
"new-chat": "new-chat";
replace: "replace";
}>>;
id: z.ZodOptional<z.ZodString>;
handoffTimeoutMs: z.ZodOptional<z.ZodNumber>;
}, z.core.$strict>;
declare const moveRecordSchema: z.ZodObject<{
id: z.ZodString;
state: z.ZodEnum<{
exiting: "exiting";
failed: "failed";
"handing-off": "handing-off";
launching: "launching";
linking: "linking";
moved: "moved";
uncertain: "uncertain";
}>;
createdAt: z.ZodString;
updatedAt: z.ZodString;
from: z.ZodObject<{
target: z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
session: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
}, z.core.$strip>;
account: z.ZodOptional<z.ZodString>;
label: z.ZodOptional<z.ZodString>;
}, z.core.$strict>;
to: z.ZodObject<{
computer: z.ZodOptional<z.ZodString>;
harness: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
}>;
account: z.ZodOptional<z.ZodString>;
model: z.ZodOptional<z.ZodString>;
effort: z.ZodOptional<z.ZodEnum<{
high: "high";
low: "low";
max: "max";
medium: "medium";
minimal: "minimal";
xhigh: "xhigh";
}>>;
}, z.core.$strict>;
computerFingerprint: z.ZodOptional<z.ZodString>;
computerId: z.ZodOptional<z.ZodString>;
accountChoice: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
mode: z.ZodOptional<z.ZodEnum<{
"new-chat": "new-chat";
replace: "replace";
}>>;
modeForced: z.ZodOptional<z.ZodLiteral<"conductor">>;
handedOffTo: z.ZodOptional<z.ZodObject<{
computer: z.ZodOptional<z.ZodString>;
target: z.ZodUnion<readonly [z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
session: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
}, z.core.$strip>, z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
starting: z.ZodLiteral<true>;
startingToken: z.ZodString;
}, z.core.$strip>]>;
}, z.core.$strict>>;
handedOffFrom: z.ZodOptional<z.ZodObject<{
target: z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
session: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
}, z.core.$strip>;
title: z.ZodString;
}, z.core.$strict>>;
cwd: z.ZodString;
dispatch: z.ZodOptional<z.ZodString>;
workerDispatch: z.ZodOptional<z.ZodString>;
handoff: z.ZodOptional<z.ZodObject<{
path: z.ZodString;
source: z.ZodEnum<{
agent: "agent";
fallback: "fallback";
}>;
reason: z.ZodOptional<z.ZodString>;
chars: z.ZodNumber;
}, z.core.$strict>>;
exit: z.ZodOptional<z.ZodEnum<{
clean: "clean";
interrupted: "interrupted";
killed: "killed";
}>>;
placement: z.ZodOptional<z.ZodEnum<{
"adjacent-pane": "adjacent-pane";
peer: "peer";
"same-pane": "same-pane";
}>>;
target: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
session: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
}, z.core.$strip>, z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
starting: z.ZodLiteral<true>;
startingToken: z.ZodString;
}, z.core.$strip>]>>;
paneTarget: z.ZodOptional<z.ZodObject<{
server: z.ZodString;
workspace: z.ZodString;
tab: z.ZodString;
pane: z.ZodString;
source: z.ZodEnum<{
claude: "claude";
codex: "codex";
copilot: "copilot";
opencode: "opencode";
phren: "phren";
}>;
}, z.core.$strip>>;
linkPending: z.ZodOptional<z.ZodBoolean>;
notice: z.ZodOptional<z.ZodString>;
movedAt: z.ZodOptional<z.ZodString>;
error: z.ZodOptional<z.ZodString>;
recovery: z.ZodOptional<z.ZodObject<{
attempts: z.ZodNumber;
nextAt: z.ZodString;
}, z.core.$strict>>;
}, z.core.$strict>;
export type MoveRecord = z.infer<typeof moveRecordSchema>;
/** What a dispatching Hook records on its receipt when its worker moved. */
export interface MovedWorker {
id: string;
at: string;
handoff?: string;
computerFingerprint?: string;
computerId?: string;
from: {
harness: Provider;
account?: string;
};
to: MoveTo;
target: Target | StartingTarget;
}
export declare function movesRoot(): string;
/** The hand-off sits beside the new agent's brief, in a folder Claude is given read access to. */
export declare const handoffPath: (id: string) => string;
export declare function readMove(id: string): Promise<MoveRecord | undefined>;
/** Recent moves, newest first; old ones are removed on the way. */
export declare function listMoves(now?: number): Promise<MoveRecord[]>;
/** The move that took the agent out of `target`'s pane: a target with a
* conversation matches only that conversation; a starting one, the pane. */
export declare function moveFrom(records: readonly MoveRecord[], target: {
server: string;
pane: string;
source: string;
session?: string;
}): MoveRecord | undefined;
/** Conversation-bound annotations shared by tab and pane overview rows. */
export declare function moveSessionFields(records: readonly MoveRecord[], target: Target, title?: string): Json;
/** The receipt fields for a finished move. */
export declare function movedWorker(record: MoveRecord): MovedWorker | undefined;
/** A harness as people name it, with its account and model: "Claude (work, opus)". */
export declare function harnessName(to: {
harness: string;
account?: string;
model?: string;
effort?: string;
}): string;
/** What the moving agent is asked to write. */
export declare function handoffRequest(id: string, to: MoveTo, mode?: "replace" | "new-chat"): string;
/** The hand-off between the markers in the agent's replies after the request,
* or undefined while its end marker has not arrived. Code fences around the
* block are dropped. */
export declare function extractHandoff(replies: readonly string[], id: string): string | undefined;
/** One message from a conversation's tail. */
export interface TailEntry {
role: "owner" | "agent";
text: string;
}
/** The owner's prompts and the agent's replies in a transcript's lines, oldest first. */
export declare function conversationEntries(lines: readonly string[], source: Provider): TailEntry[];
/** The agent's replies after the move's request: the request names the move, so an older hand-off never counts. */
export declare function repliesAfterRequest(entries: readonly TailEntry[], id: string): string[];
/** What `git` shows in the checkout at the move. */
export interface GitState {
root: string;
branch?: string;
head?: string;
upstream?: string;
ahead?: number;
behind?: number;
/** `git status --porcelain` lines: every change, tracked or not. */
changes: string[];
}
export declare function readGitState(directory: string): Promise<GitState | undefined>;
/** The commands of every process below `pids` (the agent's own children), not the agent itself. */
export declare function processesBelow(pids: readonly number[]): Promise<string[]>;
/** What Phren itself saw at the move, appended to every hand-off. */
export declare function observedState(git: GitState | undefined, processes: readonly string[], processCommands?: boolean): string;
/** A hand-off built without the agent: the conversation's tail, the original brief and the git state. */
export declare function fallbackHandoff(input: {
reason: string;
from: string;
entries: readonly TailEntry[];
original?: string;
dispatch?: string;
label?: string;
}): string;
/** The hand-off file: who moved where and when, the body, then what Phren saw. */
export declare function handoffDocument(record: MoveRecord, body: string, observed: string, at: string, conductor?: boolean): string;
/** The new agent's first prompt: the whole hand-off when it fits the brief, else where to read it. */
export declare function continuePrompt(document: string, file: string): string;
export interface MoverDeps {
succession?: ConductorSuccessionService;
snapshot: (server: string) => Promise<Json>;
/** The conversation the pane runs now. */
identity: (server: string, pane: Json) => Promise<string | undefined>;
terminal: Pick<TerminalProvider, "prompt" | "sendKeys" | "processes" | "closePane">;
/** The `hand_off` delivery path: queued while the agent is busy. */
deliver: (target: Target, text: string, deliveryId: string) => Promise<Json>;
/** Starts the new agent (`startSession`), in `into` when given. */
launch: (server: string, data: Json, options: {
into?: {
workspaceId: string;
tabId: string;
paneId: string;
};
dispatchId?: string;
}) => Promise<Json>;
inventory: () => Promise<HarnessInventory | undefined>;
/** The conversation's transcript lines, its tail is enough. */
transcript: (target: Target) => Promise<string[]>;
git?: (directory: string) => Promise<GitState | undefined>;
processesBelow?: (pids: readonly number[]) => Promise<string[]>;
/** The Claude account the pane runs under, when known. */
paneAccount?: (server: string, pane: Json) => string | undefined;
/** The dispatch the pane's agent was launched for. */
paneDispatch?: (server: string, pane: Json) => Promise<string | undefined>;
/** The text of a brief this computer wrote. */
brief?: (id: string) => Promise<string | undefined>;
/** Whether the pane holds this computer's conductor. */
isConductor?: (server: string, pane: Json) => Promise<boolean>;
/** Herdr gives a pane its variables only when it is created; tmux sets them on every start. */
envAtStart?: (server: string) => boolean;
/** Cleanup after the old agent exited (a Codex pane's app-server). */
afterExit?: (server: string, pane: string) => Promise<void>;
kill?: (pid: number, signal: NodeJS.Signals) => void;
now?: () => number;
sleep?: (ms: number) => Promise<void>;
pollMs?: number;
/** How long a clean exit, then an interrupt, may take before the agent counts as hung. */
exitMs?: number;
}
/** Runs moves on the computer that runs the session. A move outlives the
* request that started it (the Hook answers in seconds); its record says where it is. */
export declare class SessionMover {
private readonly deps;
private readonly cancelled;
private readonly admissions;
private readonly running;
constructor(deps: MoverDeps);
private get now();
private sleep;
private get poll();
/** Checks the move can happen, records it and starts it. Refuses before anything is typed. */
start(input: unknown, caller?: {
owner?: boolean;
}): Promise<MoveRecord>;
private admit;
/** For tests: the move under way in a pane, until it settles. */
settled(server: string, pane: string): Promise<void> | undefined;
private update;
private run;
abort(id: string): Promise<MoveRecord>;
private assertNotAborted;
private continueOnPeer;
/** Startup and tick recovery reuse the receiving Hook's durable move id.
* No fresh id, brief or pane is created after an uncertain launch. */
reconcile(): Promise<void>;
/** An ordinary move never acquires permission to stop a conductor merely
* because the owner promoted its source while the hand-off was running. */
private checkPromotion;
/** Uses the same durable prompt path as the hand-off and its first brief. */
private conductorNotice;
/** The agent's own hand-off, or why there is none. Fails only when the session itself went away. */
private askForHandoff;
private fallback;
/** Ends the old agent with its own exit command; an interrupt, then a kill, only when it hangs. */
private exit;
/** Starts the target in the old pane when its shell has what the target needs, else in a new tab beside it. */
private relaunch;
/** New chats remain open as linking while the harness establishes its conversation. */
private newTarget;
}
/** The last `bytes` of a transcript as lines, the first (cut) one dropped. */
export declare function transcriptTail(file: string, bytes?: number): Promise<string[]>;