SPB Git

Repository Public

agent.ts 5.0 KB 134 lines typescript

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
Files main
Menu

Theme