UNPKG

@phren/cli

Version:

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

73 lines (72 loc) • 3.13 kB
import { readFile } from "node:fs/promises"; import path from "node:path"; import { bridgeRoot, object, serverName, sessionId } from "./protocol.js"; /** Small stores the agent callback socket keeps: the recorded pane binding * files, push-notification answer bindings, and overview watch leases. */ export const localSocket = () => path.join(bridgeRoot(), "agent.sock"); export const bindingPath = (server, pane) => path.join(bridgeRoot(), "bindings", encodeURIComponent(serverName.parse(server)), encodeURIComponent(pane) + ".json"); export async function recordedSession(server, pane, pids) { try { const value = object(JSON.parse(await readFile(bindingPath(server, String(pane.pane_id)), "utf8"))); if (value.terminal !== pane.terminal_id || value.source !== pane.agent || !Array.isArray(value.pids) || !value.pids.some(p => pids.includes(Number(p)))) return undefined; return sessionId.parse(value.session); } catch { return undefined; } } export class PushBindingStore { now; limit; values = new Map(); constructor(now = Date.now, limit = 128) { this.now = now; this.limit = limit; } add(binding, value) { for (const [key, item] of this.values) if (item.expiresAt <= this.now()) this.values.delete(key); while (this.values.size >= this.limit) this.values.delete(this.values.keys().next().value); this.values.set(binding, value); } consume(binding) { const value = this.values.get(binding); this.values.delete(binding); return value && value.expiresAt > this.now() ? value : undefined; } /** The binding's action without spending it: a tap that opens the app. */ peek(binding) { const value = this.values.get(binding); return value && value.expiresAt > this.now() ? value : undefined; } /** Keeps an action's bindings answerable until `expiresAt`: an ask its * agent still lists must stay answerable from its notification. */ extendAction(action, expiresAt) { for (const value of this.values.values()) if (value.action === action && value.expiresAt < expiresAt) value.expiresAt = expiresAt; } dropAction(action) { for (const [key, value] of this.values) if (value.action === action) this.values.delete(key); } clear() { this.values.clear(); } get size() { return this.values.size; } } /** An explicit foreground overview poll renews interest for a bounded interval. * A disconnected phone never leaves future terminal prompts waiting forever. */ export class ApprovalWatchLeases { now; servers = new Map(); constructor(now = Date.now) { this.now = now; } renew(server) { for (const [key, expiry] of this.servers) if (expiry <= this.now()) this.servers.delete(key); if (this.servers.has(server) || this.servers.size < 64) this.servers.set(server, this.now() + 25_000); } has(server) { return (this.servers.get(server) || 0) > this.now(); } }