Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
49 kB · 1706 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707import { resolve } from "node:path";import { clearLine, createInterface, cursorTo, type Interface } from "node:readline";import { setTimeout as delay } from "node:timers/promises";import type { AgentMessage } from "@earendil-works/pi-agent-core";import chalk from "chalk";import { expandTildePath } from "../config.js";import type { AgentSessionEvent } from "../core/agent-session.js";import type { AgentSessionRuntimeConfig } from "../core/agent-session-config.js";import { type AgentCronJob, formatAgentCronJob } from "../core/cron-jobs.js";import { looksLikeSessionPath } from "../core/session-resolver.js";import { DaemonClient, type DaemonClientMessageListener } from "../modes/daemon/daemon-client.js";import type { DaemonOutbound, DaemonResponse } from "../modes/daemon/daemon-protocol.js";import { matchesSessionIdSuffix } from "../modes/daemon/daemon-session-id.js";import type { SessionSummary } from "../modes/daemon/daemon-session-list.js";import { defaultDaemonSocketPath, normalizeSocketPath } from "../modes/daemon/daemon-socket.js";import { spawnHidden } from "../utils/child-process.js";import { isLocalPath } from "../utils/paths.js";import { isValidThinkingLevel } from "./args.js";import { formatSessionListTable } from "./daemon-list-format.js";import { runPs, runReap } from "./daemon-ps.js";import { runDaemonRestart } from "./daemon-update-restart.js";
interface ParsedDaemonClientCommand { command: string; socketPath: string; json: boolean; positionals: string[];}
const DAEMON_CLIENT_COMMANDS = new Set([ "start", "ps", "list", "create", "attach", "detach", "kill", "rename", "prompt", "send", "agent-messages", "steer", "follow-up", "state", "messages", "stats", "commands", "cron", "retry", "restart", "shutdown",]);
export async function handleDaemonCommand(args: string[]): Promise<boolean> { if (args[0] !== "daemon") { return false; }
try { const parsed = parseDaemonClientCommand(args.slice(1)); await runDaemonClientCommand(parsed); return true; } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: ${message}`)); process.exitCode = 1; return true; }}
function parseDaemonClientCommand(args: string[]): ParsedDaemonClientCommand { let socketPath = defaultDaemonSocketPath(); let json = false; const positionals: string[] = []; let passthrough = false; let command: string | undefined;
for (let index = 0; index < args.length; index++) { const arg = args[index];
if (passthrough) { positionals.push(arg); continue; }
// send/cron parse "--" themselves as an end-of-flags separator if (arg === "--" && (command === "cron" || command === "send")) { positionals.push(arg); passthrough = true; continue; }
if (arg === "--") { passthrough = true; continue; }
if (arg === "--help" || arg === "-h") { if (!command) { command = "help"; } else { positionals.push("help"); } continue; }
if (arg === "--socket" || arg === "--daemon-socket") { const value = args[index + 1]; if (!value) { throw new Error(`${arg} requires a value`); } socketPath = normalizeSocketPath(value); index++; continue; }
if (arg === "--json") { json = true; continue; }
if (!command && DAEMON_CLIENT_COMMANDS.has(arg)) { command = arg; continue; }
positionals.push(arg); }
command = command ?? "open"; return { command, socketPath, json, positionals };}
async function runDaemonClientCommand(parsed: ParsedDaemonClientCommand): Promise<void> { if (parsed.command === "open") { await runOpen(parsed); return; }
if (parsed.command === "start") { await runStart(parsed); return; }
if (parsed.command === "ps") { await runPsCommand(parsed); return; }
if (parsed.command === "restart") { if (parsed.positionals.length !== 0) { throw new Error("Usage: daemon restart"); } await runDaemonRestart({ socketPath: parsed.socketPath, json: parsed.json }); return; }
const client = new DaemonClient(parsed.socketPath); await client.connect();
try { switch (parsed.command) { case "list": await runList(client, parsed.positionals, parsed.json); return; case "create": await runCreate(client, parsed.positionals, parsed.json); return; case "attach": if (parsed.json) { await runJsonAttach(client, requireActiveSessionId(parsed.positionals)); } else { await runAttach(client, requireActiveSessionId(parsed.positionals)); } return; case "detach": await printResponseData( client, parsed.positionals[0] ? { type: "detach", activeSessionId: parsed.positionals[0] } : { type: "detach" }, parsed.json, ); return; case "kill": await printResponseData( client, { type: "kill", activeSessionId: requireActiveSessionId(parsed.positionals) }, parsed.json, ); return; case "rename": await runRename(client, parsed.positionals, parsed.json); return; case "prompt": await runPrompt(client, parsed.positionals); return; case "send": await runSend(client, parsed.positionals, parsed.json); return; case "agent-messages": await runAgentMessages(client, parsed.positionals, parsed.json); return; case "steer": await runMessageCommand(client, "steer", parsed.positionals, parsed.json); return; case "follow-up": await runMessageCommand(client, "follow_up", parsed.positionals, parsed.json); return; case "state": await printResponseData( client, { type: "get_state", activeSessionId: requireActiveSessionId(parsed.positionals) }, true, ); return; case "messages": await printResponseData( client, { type: "get_messages", activeSessionId: requireActiveSessionId(parsed.positionals) }, true, ); return; case "stats": await printResponseData( client, { type: "get_session_stats", activeSessionId: requireActiveSessionId(parsed.positionals) }, true, ); return; case "commands": await printResponseData( client, { type: "get_commands", activeSessionId: requireActiveSessionId(parsed.positionals) }, true, ); return; case "cron": await runCron(client, parsed.positionals, parsed.json); return; case "retry": if (parsed.positionals.length !== 1) { throw new Error("Usage: daemon retry <session>"); } await printResponseData( client, { type: "retry_worker", activeSessionId: parsed.positionals[0]! }, parsed.json, ); return; case "restart": if (parsed.positionals.length !== 0) { throw new Error("Usage: daemon restart"); } await runDaemonRestart({ socketPath: parsed.socketPath, json: parsed.json }); return; case "shutdown": await runShutdown(client, parsed.positionals, parsed.json); return; } } finally { client.close(); }}
async function runShutdown(client: DaemonClient, args: string[], json: boolean): Promise<void> { let force = false; for (const arg of args) { if (arg === "--force" || arg === "-f") { force = true; continue; } throw new Error(`Unknown shutdown option: ${arg}`); } await printResponseData(client, { type: "shutdown", force }, json);}
async function runOpen(parsed: ParsedDaemonClientCommand): Promise<void> { const sessionArgs = parseSessionArgs(parsed.positionals); if (!(await canConnectToDaemon(parsed.socketPath, 250))) { await runStart({ ...parsed, command: "start", positionals: sessionArgs.daemonArgs }); }
const client = new DaemonClient(parsed.socketPath); await client.connect(); try { const autoName = sessionArgs.name === undefined; const sessions = await getLiveSessions(client, autoName); let sessionName = sessionArgs.name ?? nextDefaultSessionName(sessions); let response: DaemonResponse; while (true) { response = await client.request({ type: "create", name: sessionName, config: sessionArgs.config, sessionPath: sessionArgs.sessionPath, continueRecent: sessionArgs.continueRecent, }); if (response.success || !autoName || !response.error.includes(`Agent name "${sessionName}" is unavailable`)) { break; } sessions.push({ sessionName } as SessionSummary); sessionName = nextDefaultSessionName(sessions); } const data = requireSuccess(response); if (!isLiveSessionSummary(data)) { throw new Error("Daemon returned an invalid create response"); } if (parsed.json) { // Machine-readable open has no terminal to attach; match create's --json shape. printJson(data); return; } await runAttach(client, data.activeSessionId); } finally { client.close(); }}
interface ParsedSessionArgs { daemonArgs: string[]; name?: string; config?: AgentSessionRuntimeConfig; sessionPath?: string; continueRecent?: boolean;}
const SESSION_BOOLEAN_FLAGS = new Set([ "--continue", "-c", "--no-session", "--no-tools", "-nt", "--no-builtin-tools", "-nbt", "--no-extensions", "-ne", "--no-skills", "-ns", "--no-prompt-templates", "-np", "--no-themes", "--no-context-files", "-nc", "--verbose", "--offline", "--foreground", "--no-detach", "--background", "-d",]);
function parseSessionArgs(args: string[]): ParsedSessionArgs { const daemonArgs: string[] = []; const nameParts: string[] = []; const config: AgentSessionRuntimeConfig = {}; const pathBaseCwd = findSessionCwdArg(args) ?? process.cwd(); let sessionPath: string | undefined; let continueRecent: boolean | undefined;
for (let index = 0; index < args.length; index++) { const arg = args[index]; if (arg === "--") { nameParts.push(...args.slice(index + 1)); break; }
if (arg === "--name") { const value = args[index + 1]; if (!value) { throw new Error("--name requires a value"); } nameParts.push(value); index++; continue; }
if (arg === "--cwd") { config.cwd = resolve(expandTildePath(requireOptionValue(args, index, arg))); index++; continue; }
if (arg.startsWith("-")) { const parsedOption = parseSessionOption(args, index, config, pathBaseCwd); if (!parsedOption) { nameParts.push(arg); continue; } if (parsedOption.daemonArg) { daemonArgs.push(parsedOption.daemonArg); if (parsedOption.value !== undefined) { daemonArgs.push(parsedOption.value); } } if (parsedOption.sessionPath !== undefined) { sessionPath = parsedOption.sessionPath; } if (parsedOption.continueRecent !== undefined) { continueRecent = parsedOption.continueRecent; } index += parsedOption.consumed; continue; }
nameParts.push(arg); }
const name = nameParts.join(" ").trim(); // Validate: --goal-token-budget without --goal is an error. if (config.initialGoal?.tokenBudget !== undefined && !config.initialGoal.objective) { throw new Error("--goal-token-budget requires --goal"); } return { daemonArgs, name: name || undefined, config: Object.keys(config).length > 0 ? config : undefined, sessionPath, continueRecent, };}
interface ParsedSessionOption { consumed: number; daemonArg?: string; value?: string; sessionPath?: string; continueRecent?: boolean;}
function parseSessionOption( args: string[], index: number, config: AgentSessionRuntimeConfig, pathBaseCwd: string,): ParsedSessionOption | undefined { const arg = args[index]; if (!arg?.startsWith("-")) { return undefined; }
const readValue = (optionName = arg): string => requireOptionValue(args, index, optionName); const withValue = (daemonArg = arg, value = readValue()): ParsedSessionOption => ({ consumed: 1, daemonArg, value }); const withParsedValue = (daemonArg: string, apply: (value: string) => void): ParsedSessionOption => { const value = readValue(daemonArg); apply(value); return withValue(daemonArg, value); }; const withSessionSelector = (value: string, consumed: number): ParsedSessionOption => { if (!value) { throw new Error(`${arg.split("=")[0]} requires a value`); } return { consumed, sessionPath: looksLikeSessionPath(value) ? resolvePathOption(value, config.cwd ?? pathBaseCwd) : value, }; }; const boolean = (daemonArg = arg): ParsedSessionOption => ({ consumed: 0, daemonArg });
if (arg.startsWith("--resume=")) { return withSessionSelector(arg.slice("--resume=".length), 0); }
switch (arg) { case "--continue": case "-c": return { consumed: 0, continueRecent: true }; case "--resume": case "-r": return withSessionSelector(readValue(), 1); case "--session-dir": return withParsedValue(arg, (value) => { config.sessionDir = expandTildePath(value); }); case "--provider": return withParsedValue(arg, (value) => { config.provider = value; }); case "--model": return withParsedValue(arg, (value) => { config.model = value; }); case "--api-key": return withParsedValue(arg, (value) => { config.apiKey = value; }); case "--system-prompt": return withParsedValue(arg, (value) => { config.systemPrompt = value; }); case "--append-system-prompt": return withParsedValue(arg, (value) => { config.appendSystemPrompt = [...(config.appendSystemPrompt ?? []), value]; }); case "--models": { const value = readValue(); config.models = parseCsvValue(value); return withValue(arg, value); } case "--tools": case "-t": { const value = readValue(); config.tools = parseCsvValue(value); return withValue(arg, value); } case "--thinking": { const level = readValue(); if (!isValidThinkingLevel(level)) { throw new Error(`Invalid thinking level "${level}"`); } config.thinking = level; return withValue(arg); } case "--extension": case "-e": return withParsedValue(arg, (value) => { config.extensions = [...(config.extensions ?? []), resolvePathOption(value, config.cwd ?? pathBaseCwd)]; }); case "--skill": return withParsedValue(arg, (value) => { config.skills = [...(config.skills ?? []), resolvePathOption(value, config.cwd ?? pathBaseCwd)]; }); case "--prompt-template": return withParsedValue(arg, (value) => { config.promptTemplates = [ ...(config.promptTemplates ?? []), resolvePathOption(value, config.cwd ?? pathBaseCwd), ]; }); case "--theme": return withParsedValue(arg, (value) => { config.themes = [...(config.themes ?? []), resolvePathOption(value, config.cwd ?? pathBaseCwd)]; }); case "--no-tools": case "-nt": config.noTools = true; return boolean(arg); case "--no-builtin-tools": case "-nbt": config.noBuiltinTools = true; return boolean(arg); case "--no-extensions": case "-ne": config.noExtensions = true; return boolean(arg); case "--no-skills": case "-ns": config.noSkills = true; return boolean(arg); case "--no-prompt-templates": case "-np": config.noPromptTemplates = true; return boolean(arg); case "--no-themes": config.noThemes = true; return boolean(arg); case "--no-context-files": case "-nc": config.noContextFiles = true; return boolean(arg); case "--goal": { const value = readValue(arg); if (!value.trim()) { throw new Error("--goal requires a non-empty objective"); } config.initialGoal = { objective: value, tokenBudget: config.initialGoal?.tokenBudget }; // Session-specific flag: do NOT propagate to daemon startup args. // The goal is sent per-create via the config in the daemon request, // so a later no-goal create is not contaminated. return { consumed: 1 }; } case "--goal-token-budget": { const value = readValue(arg); const budget = Number(value); if (!Number.isInteger(budget) || budget <= 0) { throw new Error("--goal-token-budget must be a positive integer"); } config.initialGoal = { objective: config.initialGoal?.objective ?? "", tokenBudget: budget, }; // Session-specific flag: do NOT propagate to daemon startup args. return { consumed: 1 }; } case "--foreground": case "--no-detach": case "--background": case "-d": case "--verbose": case "--offline": return SESSION_BOOLEAN_FLAGS.has(arg) ? boolean(arg) : undefined; case "--no-session": throw new Error( "--no-session is not supported for daemon sessions; daemon-owned sessions are always persisted", ); default: if (!arg.startsWith("--")) { return undefined; } return parseExtensionFlagOption(args, index, config); }}
function parseCsvValue(value: string): string[] { return value .split(",") .map((part) => part.trim()) .filter((part) => part.length > 0);}
function findSessionCwdArg(args: string[]): string | undefined { for (let index = 0; index < args.length; index++) { if (args[index] === "--") { return undefined; } if (args[index] === "--cwd") { return resolve(expandTildePath(requireOptionValue(args, index, "--cwd"))); } } return undefined;}
function parseExtensionFlagOption( args: string[], index: number, config: AgentSessionRuntimeConfig,): ParsedSessionOption { const arg = args[index]!; const eqIndex = arg.indexOf("="); config.extensionFlagValues = config.extensionFlagValues ?? {}; if (eqIndex !== -1) { config.extensionFlagValues[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1); return { consumed: 0, daemonArg: arg }; }
const name = arg.slice(2); config.extensionFlagValues[name] = true; return { consumed: 0, daemonArg: arg };}
function requireOptionValue(args: string[], index: number, option: string): string { const value = args[index + 1]; if (!value) { throw new Error(`${option} requires a value`); } return value;}
function resolvePathOption(value: string, cwd: string): string { const expanded = expandTildePath(value); return isLocalPath(expanded) ? resolve(cwd, expanded) : expanded;}
async function getLiveSessions(client: DaemonClient, all = false): Promise<SessionSummary[]> { const response = await client.request({ type: "list", ...(all ? { all: true } : {}) }); const data = requireSuccess(response); const sessions = getSessionSummaries(data); if (!sessions) { throw new Error("Daemon returned an invalid list response"); } return sessions;}
function nextDefaultSessionName(sessions: SessionSummary[]): string { const existingNames = new Set( sessions.map((session) => session.sessionName).filter((name): name is string => !!name), ); const numericNames = [...existingNames] .map((name) => Number.parseInt(name, 10)) .filter((value) => Number.isSafeInteger(value) && value > 0); let next = numericNames.length > 0 ? Math.max(...numericNames) + 1 : 1; while (Number.isSafeInteger(next)) { if (!existingNames.has(String(next))) { return String(next); } next++; }
let fallback = "session"; while (existingNames.has(fallback)) { fallback += "-"; } return fallback;}
async function runStart(parsed: ParsedDaemonClientCommand): Promise<void> { if (await canConnectToDaemon(parsed.socketPath, 250)) { console.log(`Daemon already running on ${parsed.socketPath}`); return; }
const entrypoint = process.argv[1]; if (!entrypoint) { throw new Error("Cannot determine current CLI entrypoint for daemon launch"); }
const sessionArgs = parseSessionArgs(parsed.positionals); const daemonArgs = [ ...process.execArgv, entrypoint, "--mode", "daemon", "--daemon-socket", parsed.socketPath, ...sessionArgs.daemonArgs.filter((arg) => arg !== "--background" && arg !== "-d"), ]; const child = spawnHidden(process.execPath, daemonArgs, { cwd: sessionArgs.config?.cwd ?? process.cwd(), detached: true, env: process.env, stdio: "ignore", }); child.unref();
const deadline = Date.now() + 10000; while (Date.now() < deadline) { if (await canConnectToDaemon(parsed.socketPath, 250)) { console.log(`Daemon started on ${parsed.socketPath} (pid ${child.pid})`); return; } await delay(25); }
throw new Error(`Timed out waiting for daemon to start on ${parsed.socketPath}`);}
async function runPsCommand(parsed: ParsedDaemonClientCommand): Promise<void> { let reap = false; let force = false; for (const arg of parsed.positionals) { if (arg === "--reap" || arg === "reap") { reap = true; } else if (arg === "--force" || arg === "-f") { force = true; } else { throw new Error(`Unknown ps option: ${arg}`); } } if (reap) { await runReap(parsed.json, force); return; } await runPs(parsed.json);}
async function canConnectToDaemon(socketPath: string, timeoutMs: number): Promise<boolean> { const client = new DaemonClient(socketPath); try { await client.connect(timeoutMs); return true; } catch { return false; } finally { client.close(); }}
async function runList(client: DaemonClient, args: string[], json: boolean): Promise<void> { const { all } = parseListArgs(args); const response = await client.request({ type: "list", all }); const data = requireSuccess(response); if (json) { printJson(data); return; }
const sessions = getSessionSummaries(data); if (!sessions) { printJson(data); return; }
if (sessions.length === 0) { console.log(all ? "No agents." : "No active agents."); return; }
console.log(formatSessionListTable(sessions));}
function parseListArgs(args: string[]): { all: boolean } { let all = false; for (const arg of args) { if (arg === "-a" || arg === "--all") { all = true; continue; } throw new Error(`Unknown list option: ${arg}`); } return { all };}
async function runCreate(client: DaemonClient, args: string[], json: boolean): Promise<void> { const sessionArgs = parseSessionArgs(args); const response = await client.request({ type: "create", name: sessionArgs.name, config: sessionArgs.config, sessionPath: sessionArgs.sessionPath, continueRecent: sessionArgs.continueRecent, }); const data = requireSuccess(response); if (json) { printJson(data); return; }
if (isLiveSessionSummary(data)) { console.log(`Created ${data.activeSessionId}${data.sessionName ? ` (${data.sessionName})` : ""}`); return; } printJson(data);}
async function runAttach(client: DaemonClient, activeSessionId: string): Promise<void> { if (!process.stdin.isTTY) { // The attach terminal reads stdin; without a TTY it would hang forever. throw new Error("attach requires an interactive terminal (pass --json for machine-readable attach)"); } const terminal = new DaemonAttachTerminal(client, activeSessionId); await terminal.run();}
async function runJsonAttach(client: DaemonClient, activeSessionId: string): Promise<void> { const unsubscribeOutput = client.onMessage(printJsonLine); const sessionClosed = waitForSessionClose(client, activeSessionId); const interrupted = waitUntilInterrupted(); try { await requireSuccessAsync(client.request({ type: "attach", activeSessionId })); await Promise.race([sessionClosed.promise, interrupted.promise]); } finally { unsubscribeOutput(); sessionClosed.cancel(); interrupted.cancel(); }}
async function runRename(client: DaemonClient, args: string[], json: boolean): Promise<void> { const activeSessionId = requireActiveSessionId(args); const name = args.slice(1).join(" ").trim(); if (!name) { throw new Error("Usage: prime-agent rename <agent> <name>"); } const response = await client.request({ type: "rename", activeSessionId, name }); const data = requireSuccess(response); if (json) { printJson(data); return; }
if (isLiveSessionSummary(data)) { console.log(`Renamed ${data.activeSessionId} to ${data.sessionName ?? name}`); return; } printJson(data);}
async function runPrompt(client: DaemonClient, args: string[]): Promise<void> { const activeSessionId = requireActiveSessionId(args); const message = args.slice(1).join(" ").trim(); if (!message) { throw new Error("Usage: daemon prompt <session> <message>"); }
let promptAcknowledged = false; const finished = waitForSessionEnd(client, activeSessionId, () => promptAcknowledged); const unsubscribeOutput = client.onMessage(printJsonLine); try { await requireSuccessAsync(client.request({ type: "attach", activeSessionId })); await requireSuccessAsync(client.request({ type: "prompt", activeSessionId, message })); promptAcknowledged = true; await finished.promise; } finally { unsubscribeOutput(); finished.cancel(); }}
async function runAgentMessages(client: DaemonClient, args: string[], json: boolean): Promise<void> { const subcommand = args[0]; switch (subcommand) { case "status": requireNoExtraArgs(args, "daemon agent-messages status"); await printResponseData(client, { type: "agent_messages_status" }, json); return; case "pause": requireNoExtraArgs(args, "daemon agent-messages pause"); await printResponseData(client, { type: "agent_messages_pause" }, json); return; case "resume": requireNoExtraArgs(args, "daemon agent-messages resume"); await printResponseData(client, { type: "agent_messages_resume" }, json); return; case "clear": { const activeSessionId = args[1]; if (!activeSessionId || args.length !== 2) { throw new Error("Usage: daemon agent-messages clear <session>"); } await printResponseData(client, { type: "agent_messages_clear", activeSessionId }, json); return; } default: throw new Error("Usage: daemon agent-messages <status|pause|resume|clear>"); }}
function requireNoExtraArgs(args: string[], usage: string): void { if (args.length > 1) { throw new Error(`Usage: ${usage}`); }}
async function runSend(client: DaemonClient, args: string[], json: boolean): Promise<void> { const parsed = parseSendArgs(args); const response = await client.request({ type: "send_message", targetActiveSessionId: parsed.targetActiveSessionId, fromActiveSessionId: parsed.fromActiveSessionId, message: parsed.message, }); const data = requireSuccess(response); if (json) { printJson(data); return; } if (isAgentMessageReceipt(data)) { const target = data.target.sessionName ?? data.target.activeSessionId; console.log(data.deliveryStatus === "queued" ? `Queued for ${target}` : `Sent to ${target}`); return; } console.log("ok");}
interface ParsedSendArgs { targetActiveSessionId: string; fromActiveSessionId?: string; message: string;}
function parseSendArgs(args: string[]): ParsedSendArgs { let fromActiveSessionId: string | undefined; let targetActiveSessionId: string | undefined; let explicitMessage: string | undefined; const messageParts: string[] = []; let parseOptions = true;
for (let index = 0; index < args.length; index++) { const arg = args[index]; if (parseOptions && arg === "--") { parseOptions = false; continue; } if (parseOptions && arg === "--from") { const value = args[index + 1]; if (!value) { throw new Error("--from requires a session id or name"); } fromActiveSessionId = value; index++; continue; } if (parseOptions && arg === "--message") { if (!targetActiveSessionId) { throw new Error("--message must appear after the target session"); } const value = args[index + 1]; if (!value) { throw new Error("--message requires message text"); } explicitMessage = value; index++; parseOptions = false; continue; } if (parseOptions && arg.startsWith("--")) { throw new Error(`Unknown option for send: ${arg} (use -- before message text starting with --)`); } if (!targetActiveSessionId) { targetActiveSessionId = arg; continue; } messageParts.push(arg); }
if (explicitMessage !== undefined && messageParts.length > 0) { throw new Error("Usage: prime-agent send [--from <agent>] <agent> [--message <message>|<message>]"); } const message = (explicitMessage ?? messageParts.join(" ")).trim(); if (!targetActiveSessionId || !message) { throw new Error("Usage: prime-agent send [--from <agent>] <agent> [--message <message>|<message>]"); } return { targetActiveSessionId, fromActiveSessionId, message, };}
async function runMessageCommand( client: DaemonClient, type: "steer" | "follow_up", args: string[], json: boolean,): Promise<void> { const activeSessionId = requireActiveSessionId(args); const message = args.slice(1).join(" ").trim(); if (!message) { const command = type === "follow_up" ? "follow-up" : type; throw new Error(`Usage: daemon ${command} <session> <message>`); } await printResponseData(client, { type, activeSessionId, message }, json);}
async function runCron(client: DaemonClient, args: string[], json: boolean): Promise<void> { const subcommand = args[0] ?? "list"; if (subcommand === "list") { const includeInactive = args.includes("--all") || args.includes("-a"); const selector = args.find((arg) => !arg.startsWith("-") && arg !== "list"); const activeSessionId = selector ? await resolveLiveSessionSelector(client, selector) : undefined; const response = await client.request({ type: "cron_list", activeSessionId, includeInactive }); const data = requireSuccess(response); if (json) { printJson(data); return; } const jobs = getCronJobs(data); if (!jobs) { printJson(data); return; } if (jobs.length === 0) { console.log("No scheduled prompts."); return; } for (const job of jobs) { console.log(formatAgentCronJob(job)); } return; }
if (subcommand === "add" || subcommand === "schedule") { const separator = args.indexOf("--"); if (separator < 0) { throw new Error("Usage: prime-agent schedule add <agent> <schedule> -- <message>"); } const activeSessionId = args[1]; if (!activeSessionId) { throw new Error("Usage: prime-agent schedule add <agent> <schedule> -- <message>"); } const schedule = args.slice(2, separator).join(" ").trim(); const message = args .slice(separator + 1) .join(" ") .trim(); if (!schedule || !message) { throw new Error("Usage: prime-agent schedule add <agent> <schedule> -- <message>"); } const response = await client.request({ type: "cron_add", activeSessionId, schedule, prompt: message }); const data = requireSuccess(response); if (json) { printJson(data); return; } const job = getCronJob(data); console.log(job ? `Scheduled ${job.id} next=${job.nextRunAt ?? "-"}` : "Scheduled prompt."); return; }
if (subcommand === "cancel" || subcommand === "delete" || subcommand === "remove") { const jobId = args[1]; if (!jobId) { throw new Error("Usage: prime-agent schedule cancel <job-id>"); } const response = await client.request({ type: "cron_cancel", jobId }); const data = requireSuccess(response); if (json) { printJson(data); return; } const job = getCronJob(data); console.log(job ? `Cancelled ${job.id}` : "Cancelled cron job."); return; }
throw new Error(`Unknown schedule command: ${subcommand}`);}
async function resolveLiveSessionSelector(client: DaemonClient, selector: string): Promise<string> { const sessions = (await getLiveSessions(client)).filter( (session): session is SessionSummary & { activeSessionId: string } => typeof session.activeSessionId === "string", ); const exact = sessions.filter( (session) => session.activeSessionId === selector || session.sessionId === selector || session.sessionName === selector, ); const suffix = sessions.filter( (session) => matchesSessionIdSuffix(session.activeSessionId, selector) || matchesSessionIdSuffix(session.sessionId, selector), ); const matches = exact.length > 0 ? exact : suffix; if (matches.length === 1) { return matches[0]!.activeSessionId; } if (matches.length > 1) { throw new Error(`Ambiguous active session "${selector}"`); } throw new Error(`Unknown active session: ${selector}`);}
async function printResponseData( client: DaemonClient, command: Parameters<DaemonClient["request"]>[0], json: boolean,): Promise<void> { const response = await client.request(command); const data = requireSuccess(response); if (json || data !== undefined) { printJson(data ?? response); return; } console.log("ok");}
function requireActiveSessionId(args: string[]): string { const activeSessionId = args[0]; if (!activeSessionId) { throw new Error("Missing agent id or name"); } return activeSessionId;}
function requireSuccess(response: DaemonResponse): unknown { if (!response.success) { throw new Error(response.error); } return "data" in response ? response.data : undefined;}
async function requireSuccessAsync(responsePromise: Promise<DaemonResponse>): Promise<unknown> { return requireSuccess(await responsePromise);}
interface SessionEndWaiter { promise: Promise<void>; cancel: () => void;}
type DaemonWaiterMessagePredicate = (message: DaemonOutbound) => boolean;
function createDaemonMessageWaiter( client: DaemonClient, shouldResolveOnMessage: DaemonWaiterMessagePredicate,): SessionEndWaiter { let unsubscribeMessages = () => {}; let unsubscribeClose = () => {}; let settled = false; let resolveWait!: () => void; let rejectWait!: (error: Error) => void;
const cleanup = () => { unsubscribeMessages(); unsubscribeClose(); }; const resolveOnce = () => { if (settled) { return; } settled = true; cleanup(); resolveWait(); }; const rejectOnce = (error: Error) => { if (settled) { return; } settled = true; cleanup(); rejectWait(error); };
const promise = new Promise<void>((resolve, reject) => { resolveWait = resolve; rejectWait = reject; }); unsubscribeMessages = client.onMessage((message) => { if (shouldResolveOnMessage(message)) { resolveOnce(); } }); unsubscribeClose = client.onClose((error) => { rejectOnce(error); });
return { promise, cancel: resolveOnce };}
function waitForSessionEnd( client: DaemonClient, activeSessionId: string, isPromptAcknowledged?: () => boolean,): SessionEndWaiter { let observedAgentStart = false; return createDaemonMessageWaiter(client, (message) => { if (message.type === "session_closed" && message.activeSessionId === activeSessionId) { return true; } if (message.type !== "session_event" || message.activeSessionId !== activeSessionId) { return false; } if (message.event.type === "agent_start") { observedAgentStart = true; return false; } // Requiring agent_start guards against replayed agent_end events from the // attach snapshot, but an agent that was already running when we attached // never emits agent_start to this client. Once our prompt is acknowledged, // any later agent_end is live, so accept it to avoid hanging. return message.event.type === "agent_end" && (observedAgentStart || (isPromptAcknowledged?.() ?? false)); });}
function waitForSessionClose(client: DaemonClient, activeSessionId: string): SessionEndWaiter { return createDaemonMessageWaiter( client, (message) => message.type === "session_closed" && message.activeSessionId === activeSessionId, );}
function waitUntilInterrupted(): SessionEndWaiter { let settled = false; let resolveWait!: () => void;
const cleanup = () => { process.off("SIGINT", onSigint); process.off("SIGTERM", onSigterm); }; const resolveOnce = () => { if (settled) { return; } settled = true; cleanup(); resolveWait(); }; const onSigint = () => { resolveOnce(); }; const onSigterm = () => { resolveOnce(); };
const promise = new Promise<void>((resolve) => { resolveWait = resolve; }); process.once("SIGINT", onSigint); process.once("SIGTERM", onSigterm);
return { promise, cancel: resolveOnce };}
function printJson(value: unknown): void { console.log(JSON.stringify(value, null, 2));}
const printJsonLine: DaemonClientMessageListener = (value) => { console.log(JSON.stringify(value));};
class DaemonAttachTerminal { private rl?: Interface; private isStreaming = false; private readonly prompt = chalk.green("prime-agent> ");
constructor( private readonly client: DaemonClient, private readonly activeSessionId: string, ) {}
async run(): Promise<void> { this.rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true, }); this.rl.setPrompt(this.prompt);
const unsubscribe = this.client.onMessage((message) => this.handleMessage(message)); this.rl.on("line", (line) => { void this.handleInput(line).catch((error) => { this.writeLine(chalk.red(error instanceof Error ? error.message : String(error))); }); }); this.rl.on("SIGINT", () => { this.rl?.close(); });
const closePromise = new Promise<void>((resolve) => { this.rl?.once("close", resolve); });
try { await requireSuccessAsync(this.client.request({ type: "attach", activeSessionId: this.activeSessionId })); await closePromise; } finally { unsubscribe(); this.rl?.removeAllListeners(); this.rl = undefined; await this.client.request({ type: "detach", activeSessionId: this.activeSessionId }).catch(() => undefined); process.stdin.pause(); process.stdout.write(`${chalk.dim("Detached.")}\n`); } }
private async handleInput(line: string): Promise<void> { const input = line.trim(); if (!input) { this.rl?.prompt(); return; }
if (input === "/quit" || input === "/exit" || input === "/detach") { this.rl?.close(); return; }
if (input === "/help") { this.printHelp(); this.rl?.prompt(); return; }
if (input === "/abort") { requireSuccess(await this.client.request({ type: "abort", activeSessionId: this.activeSessionId })); this.writeLine(chalk.dim("Abort requested.")); return; }
if (input === "/state") { const response = await this.client.request({ type: "get_state", activeSessionId: this.activeSessionId }); this.writeLine(JSON.stringify(requireSuccess(response), null, 2)); return; }
if (input === "/messages") { const response = await this.client.request({ type: "get_messages", activeSessionId: this.activeSessionId }); const data = requireSuccess(response); if (isMessagesData(data)) { this.writeLine(chalk.bold("Transcript")); this.printTranscript(data.messages); } else { this.writeLine(JSON.stringify(data, null, 2)); } return; }
if (this.isStreaming) { requireSuccess( await this.client.request({ type: "follow_up", activeSessionId: this.activeSessionId, message: input }), ); this.writeLine(chalk.dim("Queued follow-up.")); return; }
requireSuccess( await this.client.request({ type: "prompt", activeSessionId: this.activeSessionId, message: input }), ); }
private handleMessage(message: DaemonOutbound): void { switch (message.type) { case "daemon_hello": return; case "session_attached": this.isStreaming = message.state.isStreaming; this.writeLine(chalk.bold(`Attached to ${message.state.sessionName ?? message.activeSessionId}`)); if (message.state.model) { this.writeLine(chalk.dim(`Model: ${message.state.model.provider}/${message.state.model.id}`)); } if (message.state.sessionFile) { this.writeLine(chalk.dim(`Session: ${message.state.sessionFile}`)); } this.printHelp(); if (message.messages.length > 0) { this.writeLine(chalk.bold("Transcript")); this.printTranscript(message.messages); } this.rl?.prompt(); return; case "session_event": if (message.event.type !== "refine_complete") { this.handleSessionEvent(message.event); } return; case "session_replaced": this.isStreaming = message.state.isStreaming; this.writeLine(chalk.dim(`Session replaced: ${message.state.sessionName ?? message.activeSessionId}`)); if (message.messages.length > 0) { this.writeLine(chalk.bold("Transcript")); this.printTranscript(message.messages); } this.rl?.prompt(); return; case "session_resynced": this.isStreaming = message.snapshot.state.isStreaming; this.writeLine( chalk.dim(`Session resynchronized: ${message.snapshot.state.sessionName ?? message.activeSessionId}`), ); if (message.snapshot.messages.length > 0) { this.writeLine(chalk.bold("Transcript")); this.printTranscript(message.snapshot.messages); } this.rl?.prompt(); return; case "session_detached": return; case "session_closed": this.writeLine(chalk.yellow(`Session closed: ${message.reason}`)); this.rl?.close(); return; case "extension_ui_request": this.writeLine(chalk.dim(`Extension UI request: ${message.method}`)); return; case "extension_error": this.writeLine(chalk.red(`Extension error (${message.extensionPath}, ${message.event}): ${message.error}`)); return; case "response": if (!message.success) { this.writeLine(chalk.red(message.error)); } return; } }
private handleSessionEvent(event: AgentSessionEvent): void { switch (event.type) { case "agent_start": this.isStreaming = true; this.writeLine(chalk.dim("Agent started.")); return; case "agent_end": this.isStreaming = false; this.writeLine(chalk.dim("Agent idle.")); return; case "message_end": this.printMessage(event.message); return; case "message_update": if (event.assistantMessageEvent.type === "toolcall_end") { this.writeLine(chalk.dim(`Tool call: ${event.assistantMessageEvent.toolCall.name}`)); } return; case "tool_execution_start": this.writeLine(chalk.dim(`Tool started: ${event.toolName}`)); return; case "tool_execution_end": this.writeLine(chalk.dim(`Tool ${event.isError ? "failed" : "finished"}: ${event.toolName}`)); return; case "session_action_update": if (event.actions.queuedCount > 0) { this.writeLine( chalk.dim( `Queued: ${event.actions.steering.length} steering, ${event.actions.followUps.length} follow-up`, ), ); } return; case "session_info_changed": this.writeLine(chalk.dim(`Session name: ${event.name ?? "(unnamed)"}`)); return; case "thinking_level_changed": this.writeLine(chalk.dim(`Thinking: ${event.level}`)); return; case "compaction_start": this.writeLine(chalk.dim(`Compaction started: ${event.reason}`)); return; case "compaction_end": this.writeLine(chalk.dim(`Compaction ${event.aborted ? "aborted" : "finished"}: ${event.reason}`)); return; case "auto_retry_start": this.writeLine(chalk.dim(`Retry ${event.attempt}/${event.maxAttempts}: ${event.errorMessage}`)); return; case "auto_retry_end": this.writeLine(chalk.dim(event.success ? "Retry succeeded." : `Retry failed: ${event.finalError ?? ""}`)); return; case "rlm_child_update": this.writeLine(chalk.dim(`Subagent ${event.child.label}: ${event.child.status}`)); return; case "goal_update": this.writeLine(chalk.dim(`Goal: ${event.goal.status}`)); return; case "refine_failed": this.writeLine(chalk.red(`Refinement failed: ${event.error}`)); return; case "refine_complete": return; case "turn_start": case "turn_end": case "message_start": case "tool_execution_update": case "auth_stale": return; } }
private printTranscript(messages: AgentMessage[]): void { for (const message of messages) { this.printMessage(message); } }
private printMessage(message: AgentMessage): void { const role = getMessageRole(message); const body = getMessageText(message).trim(); const label = formatRole(role); this.writeLine(body ? `${label}\n${indent(body)}` : `${label} ${chalk.dim("[no text content]")}`); }
private printHelp(): void { this.writeLine(chalk.dim("Type a message and press Enter. Commands: /help /state /messages /abort /detach")); }
private writeLine(text: string): void { if (this.rl && process.stdout.isTTY) { clearLine(process.stdout, 0); cursorTo(process.stdout, 0); } process.stdout.write(`${text}\n`); this.rl?.prompt(true); }}
function isMessagesData(value: unknown): value is { messages: AgentMessage[] } { if (!value || typeof value !== "object") { return false; } return Array.isArray((value as { messages?: unknown }).messages);}
function getMessageRole(message: AgentMessage): string { if (!message || typeof message !== "object" || !("role" in message) || typeof message.role !== "string") { return "message"; } return message.role;}
function getMessageText(message: AgentMessage): string { if (!message || typeof message !== "object" || !("content" in message)) { return JSON.stringify(message); } return formatContent((message as { content: unknown }).content);}
function formatContent(content: unknown): string { if (typeof content === "string") { return content; } if (Array.isArray(content)) { return content .map(formatContentBlock) .filter((text) => text.length > 0) .join("\n"); } if (content === undefined || content === null) { return ""; } return JSON.stringify(content);}
function formatContentBlock(block: unknown): string { if (!block || typeof block !== "object" || !("type" in block) || typeof block.type !== "string") { return JSON.stringify(block); }
switch (block.type) { case "text": return getStringProperty(block, "text"); case "thinking": { const thinking = getStringProperty(block, "thinking"); return thinking ? `[thinking]\n${thinking}` : "[thinking]"; } case "image": { const mimeType = getStringProperty(block, "mimeType"); return mimeType ? `[image: ${mimeType}]` : "[image]"; } case "toolCall": { const name = getStringProperty(block, "name"); const args = getUnknownProperty(block, "arguments"); const argsText = args === undefined ? "" : ` ${JSON.stringify(args)}`; return `[tool call: ${name || "unknown"}${argsText}]`; } default: return JSON.stringify(block); }}
function getStringProperty(value: object, key: string): string { const record = value as Record<string, unknown>; const property = record[key]; return typeof property === "string" ? property : "";}
function getUnknownProperty(value: object, key: string): unknown { return (value as Record<string, unknown>)[key];}
function formatRole(role: string): string { switch (role) { case "user": return chalk.blue.bold("user:"); case "assistant": return chalk.green.bold("assistant:"); case "toolResult": return chalk.magenta.bold("tool:"); default: return chalk.bold(`${role}:`); }}
function indent(text: string): string { return text .split("\n") .map((line) => ` ${line}`) .join("\n");}
function getSessionSummaries(value: unknown): SessionSummary[] | undefined { if (!value || typeof value !== "object") { return undefined; } const sessions = (value as { sessions?: unknown }).sessions; if (!Array.isArray(sessions)) { return undefined; }
const entries: SessionSummary[] = []; for (const session of sessions) { if (isSessionSummary(session)) { entries.push(session); continue; } return undefined; } return entries;}
function isSessionSummary(value: unknown): value is SessionSummary { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial<SessionSummary>; return ( typeof candidate.id === "string" && typeof candidate.sessionId === "string" && typeof candidate.cwd === "string" && typeof candidate.lifecycle === "string" && typeof candidate.activity === "string" && typeof candidate.isSessionActive === "boolean" && typeof candidate.isStreaming === "boolean" && typeof candidate.isCompacting === "boolean" && typeof candidate.attachedClients === "number" && typeof candidate.messageCount === "number" && (candidate.unfinishedActionCount === undefined || typeof candidate.unfinishedActionCount === "number") && typeof candidate.sessionActions === "object" && candidate.sessionActions !== null && typeof candidate.sessionActions.queuedCount === "number" && Array.isArray(candidate.sessionActions.steering) && Array.isArray(candidate.sessionActions.followUps) );}
function isLiveSessionSummary(value: unknown): value is SessionSummary & { activeSessionId: string } { return isSessionSummary(value) && typeof value.activeSessionId === "string";}
function getCronJobs(value: unknown): AgentCronJob[] | undefined { if (!value || typeof value !== "object") { return undefined; } const jobs = (value as { jobs?: unknown }).jobs; return Array.isArray(jobs) ? (jobs as AgentCronJob[]) : undefined;}
function getCronJob(value: unknown): { id: string; nextRunAt?: string } | undefined { if (!value || typeof value !== "object") { return undefined; } const job = (value as { job?: unknown }).job; if (!job || typeof job !== "object" || typeof (job as { id?: unknown }).id !== "string") { return undefined; } const candidate = job as { id: string; nextRunAt?: unknown }; return { id: candidate.id, ...(typeof candidate.nextRunAt === "string" ? { nextRunAt: candidate.nextRunAt } : {}) };}
function isAgentMessageReceipt( value: unknown,): value is { target: { activeSessionId: string; sessionName?: string }; deliveryStatus?: string } { if (!value || typeof value !== "object") { return false; } const target = (value as { target?: unknown }).target; return ( !!target && typeof target === "object" && typeof (target as { activeSessionId?: unknown }).activeSessionId === "string" );}