/** * Shared RPC schema for PTY bridge between Bun process and WebView. * * Bun holds the Unix socket connection to agor-ptyd; the WebView calls * into Bun via requests, and Bun pushes output/close events via messages. */ // ── Requests (WebView → Bun, expects a response) ───────────────────────────── export type PtyRPCRequests = { /** Create a PTY session and subscribe to its output. */ "pty.create": { params: { sessionId: string; cols: number; rows: number; /** Working directory for the shell process. */ cwd?: string; /** Override shell binary (e.g. /usr/bin/ssh). Fix #3: direct spawn, no shell injection. */ shell?: string; /** Arguments for the shell binary (e.g. ['-p', '22', 'user@host']). */ args?: string[]; }; response: { ok: boolean; error?: string }; }; /** * Write input to a PTY session. * `data` is raw UTF-8 text from the user (xterm onData). The pty-client * layer encodes it to base64 before sending to the daemon; this RPC boundary * carries raw text which the Bun handler forwards to PtyClient.writeInput(). */ "pty.write": { params: { sessionId: string; /** Raw UTF-8 text typed by the user (xterm onData delivers this). Encoded to base64 by pty-client before daemon transport. */ data: string; }; response: { ok: boolean }; }; /** Notify the daemon that the terminal dimensions changed. */ "pty.resize": { params: { sessionId: string; cols: number; rows: number }; response: { ok: boolean }; }; /** Unsubscribe from a session's output (session stays alive). */ "pty.unsubscribe": { params: { sessionId: string }; response: { ok: boolean }; }; /** Kill a PTY session. */ "pty.close": { params: { sessionId: string }; response: { ok: boolean }; }; // ── Settings RPC ─────────────────────────────────────────────────────────── /** Get a single setting value by key. Returns null if not set. */ "settings.get": { params: { key: string }; response: { value: string | null }; }; /** Persist a setting key/value pair. */ "settings.set": { params: { key: string; value: string }; response: { ok: boolean }; }; /** Return all settings as a flat object. */ "settings.getAll": { params: Record; response: { settings: Record }; }; /** Return all persisted projects. */ "settings.getProjects": { params: Record; response: { projects: Array<{ id: string; config: string }> }; }; /** Persist a project config (JSON-serialised on the caller side). */ "settings.setProject": { params: { id: string; config: string }; response: { ok: boolean }; }; /** Delete a project by id. */ "settings.deleteProject": { params: { id: string }; response: { ok: boolean }; }; // ── Custom Themes RPC ────────────────────────────────────────────────────── /** Return all user-saved custom themes. */ "themes.getCustom": { params: Record; response: { themes: Array<{ id: string; name: string; palette: Record }> }; }; /** Save (upsert) a custom theme by id. */ "themes.saveCustom": { params: { id: string; name: string; palette: Record }; response: { ok: boolean }; }; /** Delete a custom theme by id. */ "themes.deleteCustom": { params: { id: string }; response: { ok: boolean }; }; // ── File I/O RPC ────────────────────────────────────────────────────────── /** List directory children (files + subdirs). Returns sorted entries. */ "files.list": { params: { path: string }; response: { entries: Array<{ name: string; type: "file" | "dir"; size: number; }>; error?: string; }; }; /** Read a file's content. Returns text for text files, base64 for binary. */ "files.read": { params: { path: string }; response: { content?: string; encoding: "utf8" | "base64"; size: number; error?: string; }; }; /** Get file stat info (mtime, size) for conflict detection. */ "files.stat": { params: { path: string }; response: { mtimeMs: number; size: number; error?: string }; }; /** Extended stat — directory check, git detection, writability. Used by ProjectWizard. */ "files.statEx": { params: { path: string }; response: { exists: boolean; isDirectory: boolean; isGitRepo: boolean; gitBranch?: string; size?: number; writable?: boolean; error?: string; }; }; "files.ensureDir": { params: { path: string }; response: { ok: boolean; path?: string; error?: string }; }; /** Unguarded directory listing for PathBrowser (dirs only, no file content) */ "files.browse": { params: { path: string }; response: { entries: { name: string; type: 'dir'; size: number }[]; error?: string }; }; /** Native folder picker dialog */ "files.pickDirectory": { params: { startingFolder?: string }; response: { path: string | null }; }; /** Get home directory path */ "files.homeDir": { params: {}; response: { path: string }; }; /** Write text content to a file (atomic temp+rename). */ "files.write": { params: { path: string; content: string }; response: { ok: boolean; error?: string }; }; // ── Groups RPC ───────────────────────────────────────────────────────────── /** Return all project groups. */ "groups.list": { params: Record; response: { groups: Array<{ id: string; name: string; icon: string; position: number }> }; }; /** Create a new group. */ "groups.create": { params: { id: string; name: string; icon: string; position: number }; response: { ok: boolean }; }; /** Delete a group by id. */ "groups.delete": { params: { id: string }; response: { ok: boolean }; }; // ── Memora RPC ────────────────────────────────────────────────────────── /** Search memories by query text (FTS5). */ "memora.search": { params: { query: string; limit?: number }; response: { memories: Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string; }>; }; }; /** List recent memories. */ "memora.list": { params: { limit?: number; tag?: string }; response: { memories: Array<{ id: number; content: string; tags: string; metadata: string; createdAt: string; updatedAt: string; }>; }; }; // ── Project clone RPC ────────────────────────────────────────────────────── /** Clone a project into a git worktree. branchName must match /^[a-zA-Z0-9\/_.-]+$/. */ "project.clone": { params: { projectId: string; branchName: string }; response: { ok: boolean; project?: { id: string; config: string }; error?: string }; }; // ── Git RPC ────────────────────────────────────────────────────────────────── /** List branches in a git repository. */ "git.branches": { params: { path: string }; response: { branches: string[]; current: string; error?: string }; }; /** Clone a git repository. */ "git.clone": { params: { url: string; target: string; branch?: string }; response: { ok: boolean; error?: string }; }; /** Probe a git remote URL (ls-remote). Returns branches on success. */ "git.probe": { params: { url: string }; response: { ok: boolean; branches: string[]; defaultBranch: string; error?: string }; }; /** Check if sshfs is installed. */ "ssh.checkSshfs": { params: Record; response: { installed: boolean; path: string | null }; }; /** Detect available shells on this system. */ "system.shells": { params: Record; response: { shells: Array<{ path: string; name: string }>; loginShell: string }; }; /** Detect installed system fonts (uses fc-list). */ "system.fonts": { params: Record; response: { uiFonts: Array<{ family: string; preferred: boolean }>; monoFonts: Array<{ family: string; isNerdFont: boolean }>; }; }; // ── Project templates RPC ─────────────────────────────────────────────────── /** Return available project templates. Optionally pass custom template dir. */ "project.templates": { params: { templateDir?: string }; response: { templates: Array<{ id: string; name: string; description: string; icon: string; }>; }; }; /** Create a project from a template with real scaffold files. */ "project.createFromTemplate": { params: { templateId: string; targetDir: string; projectName: string }; response: { ok: boolean; path: string; error?: string }; }; // ── Provider RPC ────────────────────────────────────────────────────────── /** Scan for available AI providers on this machine. */ "provider.scan": { params: Record; response: { providers: Array<{ id: string; available: boolean; hasApiKey: boolean; hasCli: boolean; cliPath: string | null; version: string | null; }>; }; }; /** Fetch model list for a specific provider. */ "provider.models": { params: { provider: string }; response: { models: Array<{ id: string; name: string; provider: string; }>; }; }; // ── Window control RPC ───────────────────────────────────────────────────── /** Minimize the main window. */ "window.minimize": { params: Record; response: { ok: boolean }; }; /** Toggle maximize/restore on the main window. */ "window.maximize": { params: Record; response: { ok: boolean }; }; /** Close the main window. */ "window.close": { params: Record; response: { ok: boolean }; }; /** Get current window frame (x, y, width, height). */ "window.getFrame": { params: Record; response: { x: number; y: number; width: number; height: number }; }; /** Set the window position. */ "window.setPosition": { params: { x: number; y: number }; response: { ok: boolean }; }; /** Begin native GTK resize drag — delegates to window manager. */ "window.beginResize": { params: { edge: string; button: number; rootX: number; rootY: number }; response: { ok: boolean; error?: string }; }; /** Begin native GTK move drag — delegates to window manager. */ "window.beginMove": { params: { button: number; rootX: number; rootY: number }; response: { ok: boolean }; }; // ── Keybindings RPC ──────────────────────────────────────────────────────── /** Return all persisted custom keybindings (overrides only). */ "keybindings.getAll": { params: Record; response: { keybindings: Record }; }; /** Persist a single keybinding override. */ "keybindings.set": { params: { id: string; chord: string }; response: { ok: boolean }; }; /** Reset a keybinding to default (removes override). */ "keybindings.reset": { params: { id: string }; response: { ok: boolean }; }; // ── Agent RPC ───────────────────────────────────────────────────────────── /** Start an agent session with a given provider. */ "agent.start": { params: { sessionId: string; provider: "claude" | "codex" | "ollama"; prompt: string; cwd?: string; model?: string; systemPrompt?: string; maxTurns?: number; permissionMode?: string; claudeConfigDir?: string; extraEnv?: Record; additionalDirectories?: string[]; worktreeName?: string; }; response: { ok: boolean; error?: string }; }; /** Stop a running agent session. */ "agent.stop": { params: { sessionId: string }; response: { ok: boolean; error?: string }; }; /** Send a follow-up prompt to a running agent session. */ "agent.prompt": { params: { sessionId: string; prompt: string }; response: { ok: boolean; error?: string }; }; /** List all active agent sessions with their state. */ "agent.list": { params: Record; response: { sessions: Array<{ sessionId: string; provider: string; status: string; costUsd: number; inputTokens: number; outputTokens: number; startedAt: number; }>; }; }; // ── Session persistence RPC ──────────────────────────────────────────── /** Save/update a session record. */ "session.save": { params: { projectId: string; sessionId: string; provider: string; status: string; costUsd: number; inputTokens: number; outputTokens: number; model: string; error?: string; createdAt: number; updatedAt: number; }; response: { ok: boolean }; }; /** Load the most recent session for a project. */ "session.load": { params: { projectId: string }; response: { session: { projectId: string; sessionId: string; provider: string; status: string; costUsd: number; inputTokens: number; outputTokens: number; model: string; error?: string; createdAt: number; updatedAt: number; } | null; }; }; /** List sessions for a project (max 20). */ "session.list": { params: { projectId: string }; response: { sessions: Array<{ projectId: string; sessionId: string; provider: string; status: string; costUsd: number; inputTokens: number; outputTokens: number; model: string; error?: string; createdAt: number; updatedAt: number; }>; }; }; /** Save agent messages (batch). */ "session.messages.save": { params: { messages: Array<{ sessionId: string; msgId: string; role: string; content: string; toolName?: string; toolInput?: string; timestamp: number; costUsd?: number; inputTokens?: number; outputTokens?: number; }>; }; response: { ok: boolean }; }; /** Load all messages for a session. */ "session.messages.load": { params: { sessionId: string }; response: { messages: Array<{ sessionId: string; msgId: string; role: string; content: string; toolName?: string; toolInput?: string; timestamp: number; costUsd: number; inputTokens: number; outputTokens: number; }>; }; }; // ── btmsg RPC ────────────────────────────────────────────────────────── /** Register an agent in btmsg. */ "btmsg.registerAgent": { params: { id: string; name: string; role: string; groupId: string; tier: number; model?: string; }; response: { ok: boolean }; }; /** List agents for a group. */ "btmsg.getAgents": { params: { groupId: string }; response: { agents: Array<{ id: string; name: string; role: string; groupId: string; tier: number; model: string | null; status: string; unreadCount: number; }>; }; }; /** Send a direct message between agents. */ "btmsg.sendMessage": { params: { fromAgent: string; toAgent: string; content: string }; response: { ok: boolean; messageId?: string; error?: string }; }; /** Get message history between two agents. */ "btmsg.listMessages": { params: { agentId: string; otherId: string; limit?: number }; response: { messages: Array<{ id: string; fromAgent: string; toAgent: string; content: string; read: boolean; replyTo: string | null; createdAt: string; senderName: string | null; senderRole: string | null; }>; }; }; /** Mark messages as read. */ "btmsg.markRead": { params: { agentId: string; messageIds: string[] }; response: { ok: boolean }; }; /** List channels for a group. */ "btmsg.listChannels": { params: { groupId: string }; response: { channels: Array<{ id: string; name: string; groupId: string; createdBy: string; memberCount: number; createdAt: string; }>; }; }; /** Create a channel. */ "btmsg.createChannel": { params: { name: string; groupId: string; createdBy: string }; response: { ok: boolean; channelId?: string }; }; /** Get channel messages. */ "btmsg.getChannelMessages": { params: { channelId: string; limit?: number }; response: { messages: Array<{ id: string; channelId: string; fromAgent: string; content: string; createdAt: string; senderName: string; senderRole: string; }>; }; }; /** Feature 7: Join a channel. */ "btmsg.joinChannel": { params: { channelId: string; agentId: string }; response: { ok: boolean; error?: string }; }; /** Feature 7: Leave a channel. */ "btmsg.leaveChannel": { params: { channelId: string; agentId: string }; response: { ok: boolean; error?: string }; }; /** Feature 7: Get channel member list. */ "btmsg.getChannelMembers": { params: { channelId: string }; response: { members: Array<{ agentId: string; name: string; role: string }> }; }; /** Send a channel message. */ "btmsg.sendChannelMessage": { params: { channelId: string; fromAgent: string; content: string }; response: { ok: boolean; messageId?: string }; }; /** Record agent heartbeat. */ "btmsg.heartbeat": { params: { agentId: string }; response: { ok: boolean }; }; /** Get dead letter queue entries. */ "btmsg.getDeadLetters": { params: { limit?: number }; response: { letters: Array<{ id: number; fromAgent: string; toAgent: string; content: string; error: string; createdAt: string; }>; }; }; /** Log an audit event. */ "btmsg.logAudit": { params: { agentId: string; eventType: string; detail: string }; response: { ok: boolean }; }; /** Get audit log. */ "btmsg.getAuditLog": { params: { limit?: number }; response: { entries: Array<{ id: number; agentId: string; eventType: string; detail: string; createdAt: string; }>; }; }; // ── bttask RPC ───────────────────────────────────────────────────────── /** List tasks for a group. */ "bttask.listTasks": { params: { groupId: string }; response: { tasks: Array<{ id: string; title: string; description: string; status: string; priority: string; assignedTo: string | null; createdBy: string; groupId: string; parentTaskId: string | null; sortOrder: number; createdAt: string; updatedAt: string; version: number; }>; }; }; /** Create a task. */ "bttask.createTask": { params: { title: string; description: string; priority: string; groupId: string; createdBy: string; assignedTo?: string; }; response: { ok: boolean; taskId?: string; error?: string }; }; /** Update task status with optimistic locking. */ "bttask.updateTaskStatus": { params: { taskId: string; status: string; expectedVersion: number }; response: { ok: boolean; newVersion?: number; error?: string }; }; /** Delete a task. */ "bttask.deleteTask": { params: { taskId: string }; response: { ok: boolean }; }; /** Add a comment to a task. */ "bttask.addComment": { params: { taskId: string; agentId: string; content: string }; response: { ok: boolean; commentId?: string }; }; /** List comments for a task. */ "bttask.listComments": { params: { taskId: string }; response: { comments: Array<{ id: string; taskId: string; agentId: string; content: string; createdAt: string; }>; }; }; /** Count tasks in 'review' status. */ "bttask.reviewQueueCount": { params: { groupId: string }; response: { count: number }; }; // ── Search RPC ────────────────────────────────────────────────────────── /** Full-text search across messages, tasks, and btmsg. Fix #13: typed error for invalid queries. */ "search.query": { params: { query: string; limit?: number }; response: { results: Array<{ resultType: string; id: string; title: string; snippet: string; score: number; }>; /** Set when query is invalid (e.g. FTS5 syntax error). */ error?: string; }; }; /** Index a message for search. */ "search.indexMessage": { params: { sessionId: string; role: string; content: string }; response: { ok: boolean }; }; /** Rebuild the entire search index. */ "search.rebuild": { params: Record; response: { ok: boolean }; }; // ── Plugin RPC ────────────────────────────────────────────────────────── /** Discover plugins from ~/.config/agor/plugins/. */ "plugin.discover": { params: Record; response: { plugins: Array<{ id: string; name: string; version: string; description: string; main: string; permissions: string[]; }>; }; }; /** Read a plugin file (path-traversal-safe). */ "plugin.readFile": { params: { pluginId: string; filePath: string }; response: { ok: boolean; content: string; error?: string }; }; // ── Remote machine (relay) RPC ──────────────────────────────────────────── /** Connect to an agor-relay instance. */ "remote.connect": { params: { url: string; token: string; label?: string }; response: { ok: boolean; machineId?: string; error?: string }; }; /** Disconnect from a relay instance (keeps machine in list for reconnect). */ "remote.disconnect": { params: { machineId: string }; response: { ok: boolean; error?: string }; }; /** Remove a machine entirely — disconnects AND deletes from tracking. */ "remote.remove": { params: { machineId: string }; response: { ok: boolean; error?: string }; }; /** List all known remote machines with connection status. */ "remote.list": { params: Record; response: { machines: Array<{ machineId: string; label: string; url: string; status: "connecting" | "connected" | "disconnected" | "error"; latencyMs: number | null; }>; }; }; /** Send a command to a connected relay. */ "remote.send": { params: { machineId: string; command: string; payload: Record }; response: { ok: boolean; error?: string }; }; /** Feature 3: Get stored relay credentials. */ "remote.getStoredCredentials": { params: Record; response: { credentials: Array<{ url: string; label: string }> }; }; /** Feature 3: Store a relay credential (XOR-obfuscated). */ "remote.storeCredential": { params: { url: string; token: string; label?: string }; response: { ok: boolean }; }; /** Feature 3: Delete a stored relay credential. */ "remote.deleteCredential": { params: { url: string }; response: { ok: boolean }; }; /** Get the status of a specific machine. */ "remote.status": { params: { machineId: string }; response: { status: "connecting" | "connected" | "disconnected" | "error"; latencyMs: number | null; error?: string; }; }; // ── Telemetry RPC ───────────────────────────────────────────────────────── /** Feature 8: Transport diagnostics stats. */ "diagnostics.stats": { params: Record; response: { ptyConnected: boolean; relayConnections: number; activeSidecars: number; rpcCallCount: number; droppedEvents: number; }; }; /** Log a telemetry event from the frontend. */ "telemetry.log": { params: { level: "info" | "warn" | "error"; message: string; attributes?: Record; }; response: { ok: boolean }; }; // ── Updater RPC ────────────────────────────────────────────────────────── /** Check GitHub Releases for a newer version. */ "updater.check": { params: Record; response: { available: boolean; version: string; downloadUrl: string; releaseNotes: string; checkedAt: number; error?: string; }; }; /** Get the current app version and last check timestamp. */ "updater.getVersion": { params: Record; response: { version: string; lastCheck: number }; }; }; // ── Messages (Bun → WebView, fire-and-forget) ──────────────────────────────── export type PtyRPCMessages = { /** PTY output chunk. data is base64-encoded raw bytes from the daemon. */ "pty.output": { sessionId: string; data: string }; /** PTY session exited. */ "pty.closed": { sessionId: string; exitCode: number | null }; // ── Agent events (Bun → WebView) ───────────────────────────────────────── /** Agent message(s) parsed from sidecar NDJSON. */ "agent.message": { sessionId: string; messages: Array<{ id: string; type: string; parentId?: string; content: unknown; timestamp: number; }>; }; /** Agent session status change. */ "agent.status": { sessionId: string; status: string; error?: string; }; /** Agent cost/token update. */ "agent.cost": { sessionId: string; costUsd: number; inputTokens: number; outputTokens: number; }; // ── Remote machine events (Bun → WebView) ──────────────────────────────── /** Remote relay event forwarded from a connected machine. */ "remote.event": { machineId: string; eventType: string; sessionId?: string; payload?: unknown; }; /** Remote machine connection status change. */ "remote.statusChange": { machineId: string; status: "connecting" | "connected" | "disconnected" | "error"; error?: string; }; // Feature 4: Push-based task/relay updates /** Task board data changed (created, moved, deleted). */ "bttask.changed": { groupId: string }; /** New btmsg channel or DM message. */ "btmsg.newMessage": { groupId: string; channelId?: string }; }; // ── Combined schema ─────────────────────────────────────────────────────────── export type PtyRPCSchema = { requests: PtyRPCRequests; messages: PtyRPCMessages; };