Repository Public
1/**2 * Agent profiles. One codebase serves two products: "Claude Remote" (Claude Code CLI) and3 * "Codex Remote" (OpenAI Codex CLI). An instance runs exactly ONE agent, selected by the4 * environment variable `CR_AGENT` (`claude`, the default, or `codex`) read by the runtime and the5 * server; the web app learns it at run time from `GET /auth/status` (`branding`).6 *7 * Wire compatibility: the session kind stays `'claude' | 'shell'`. `'claude'` means "the8 * instance's agent" (Claude Code on a Claude instance, Codex on a Codex instance); it is a9 * historical name, kept so that existing rows (`terminal_sessions.kind = 'claude'`) and the10 * database CHECK constraint stay valid without a migration. `AGENT_SESSION_KIND` names it.11 */12import { z } from 'zod';1314export const AgentId = z.enum(['claude', 'codex']);15export type AgentId = z.infer<typeof AgentId>;1617/** Stored / wire value of the session kind that runs the instance's agent CLI. */18export const AGENT_SESSION_KIND = 'claude' as const;1920export interface AgentCapabilities {21 /**22 * The CLI reports "the user submitted a prompt" (Claude Code `UserPromptSubmit` hook). When23 * false, the server leaves WAITING_FOR_INPUT on the next terminal input from a browser.24 */25 promptSubmitHook: boolean;26 /** The CLI reports "needs attention" separately from "turn finished" (Claude `Notification`). */27 attentionHook: boolean;28 /** MCP servers can be added in a project scope from the CLI (`claude mcp add --scope project`). */29 projectMcpScope: boolean;30 /** `mcp list` health-checks the servers (connected / failed). */31 mcpLiveStatus: boolean;32 /** Custom headers can be given to `mcp add` for HTTP servers. */33 mcpHeaders: boolean;34 /** MCP transports accepted by `mcp add`. */35 mcpTransports: readonly ('stdio' | 'http' | 'sse')[];36 /** Kinds of items the Skills page can list for this agent. */37 skillKinds: readonly ('skill' | 'command' | 'agent')[];38}3940export interface AgentProfile {41 id: AgentId;42 /** Name of the CLI product, e.g. "Claude Code". */43 displayName: string;44 /** Short name used in sentences and session titles, e.g. "Claude". */45 shortName: string;46 /** Name of this web application, e.g. "Claude Remote". */47 productName: string;48 /** Machine name of the application: tmux socket, default data dir, PM2 prefix. */49 slug: string;50 /** Default binary name looked up on the login-shell PATH. */51 defaultBinary: string;52 /** What to type in a shell session to authenticate the CLI. */53 loginCommand: string;54 capabilities: AgentCapabilities;55}5657export const AGENT_PROFILES: Record<AgentId, AgentProfile> = {58 claude: {59 id: 'claude',60 displayName: 'Claude Code',61 shortName: 'Claude',62 productName: 'Claude Remote',63 slug: 'claude-remote',64 defaultBinary: 'claude',65 loginCommand: 'claude auth login',66 capabilities: {67 promptSubmitHook: true,68 attentionHook: true,69 projectMcpScope: true,70 mcpLiveStatus: true,71 mcpHeaders: true,72 mcpTransports: ['stdio', 'http', 'sse'],73 skillKinds: ['skill', 'command', 'agent'],74 },75 },76 codex: {77 id: 'codex',78 displayName: 'Codex',79 shortName: 'Codex',80 productName: 'Codex Remote',81 slug: 'codex-remote',82 defaultBinary: 'codex',83 loginCommand: 'codex login',84 capabilities: {85 promptSubmitHook: false,86 attentionHook: false,87 projectMcpScope: false,88 mcpLiveStatus: false,89 mcpHeaders: false,90 mcpTransports: ['stdio', 'http'],91 skillKinds: ['skill', 'agent'],92 },93 },94};9596export const DEFAULT_AGENT: AgentId = 'claude';9798/** `CR_AGENT` → agent id. Unset / empty ⇒ `claude`; anything else unknown is a configuration error. */99export function resolveAgentId(raw: string | undefined | null): AgentId {100 const v = (raw ?? '').trim().toLowerCase();101 if (v === '') return DEFAULT_AGENT;102 const parsed = AgentId.safeParse(v);103 if (!parsed.success) throw new Error(`CR_AGENT must be "claude" or "codex" (got "${v.slice(0, 40)}")`);104 return parsed.data;105}106107export function agentProfile(id: AgentId): AgentProfile {108 return AGENT_PROFILES[id];109}110111/** Public branding returned by `GET /auth/status` (no secret, readable before login). */112export const Branding = z.object({113 agent: AgentId,114 productName: z.string(),115 agentName: z.string(),116});117export type Branding = z.infer<typeof Branding>;118119export function brandingOf(id: AgentId): Branding {120 const p = AGENT_PROFILES[id];121 return { agent: p.id, productName: p.productName, agentName: p.displayName };122}123124/** Conversation / session ids accepted after `--resume` (claude) or `resume` (codex). */125export const CONVERSATION_ID_RE: Record<AgentId, RegExp> = {126 // Claude Code transcript ids (UUIDs today; the looser historical rule is kept).127 claude: /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/,128 // Codex session ids are UUIDs (v7 observed with codex-cli 0.166). Never a session *name*.129 codex: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,130};131export function isConversationId(agent: AgentId, id: string): boolean {132 return CONVERSATION_ID_RE[agent].test(id);133}134