import { z } from 'zod' import { AttachmentMetadataSchema, CodexCollaborationModeSchema, DecryptedMessageSchema, MachineSchema, PermissionModeSchema, SessionSchema } from './schemas' import { AgentFlavorSchema } from './modes' import type { DecryptedMessage, Machine, Session } from './schemas' import type { SessionSummary } from './sessionSummary' export const CreateOrLoadMachineRequestSchema = z.object({ id: z.string().min(1), metadata: z.unknown(), runnerState: z.unknown().nullable().optional() }) export type CreateOrLoadMachineRequest = z.infer export const CreateOrLoadSessionRequestSchema = z.object({ id: z.string().uuid().optional(), tag: z.string().min(1), metadata: z.unknown(), agentState: z.unknown().nullable().optional(), model: z.string().optional(), modelReasoningEffort: z.string().optional(), effort: z.string().optional(), machine: CreateOrLoadMachineRequestSchema.optional() }) export type CreateOrLoadSessionRequest = z.infer export const CliMessagesResponseSchema = z.object({ messages: z.array(z.object({ id: z.string(), seq: z.number(), createdAt: z.number(), localId: z.string().nullable().optional(), content: z.unknown() })) }) export type CliMessagesResponse = z.infer export const CreateSessionResponseSchema = z.object({ session: SessionSchema }) export type CreateSessionResponse = z.infer export const CreateMachineResponseSchema = z.object({ machine: MachineSchema }) export type CreateMachineResponse = z.infer export const GetSessionResponseSchema = CreateSessionResponseSchema export type GetSessionResponse = CreateSessionResponse export type AuthResponse = { token: string user: { id: number username?: string firstName?: string lastName?: string } } export type SessionsResponse = { sessions: SessionSummary[] } export type SessionResponse = { session: Session } export type MessagesResponse = { messages: DecryptedMessage[] page: { direction: 'latest' | 'before' | 'after' limit: number epoch: number reset: boolean nextBeforeSeq: number | null nextBeforeAt: number | null nextAfterSeq: number | null nextAfterAt: number | null snapshotHeadSeq: number | null snapshotHeadAt: number | null hasMore: boolean } } export type MachinesResponse = { machines: Machine[] } export type SpawnResponse = | { type: 'success'; sessionId: string } | { type: 'error'; message: string } export const SessionPermissionModeRequestSchema = z.object({ mode: PermissionModeSchema }) export type SessionPermissionModeRequest = z.infer export const ResumeSessionRequestSchema = z.object({ permissionMode: PermissionModeSchema.optional() }) export type ResumeSessionRequest = z.infer export const ReopenSessionResponseSchema = z.object({ ok: z.literal(true), sessionId: z.string(), resumed: z.boolean(), cursorSessionProtocol: z.enum(['acp', 'stream-json']).optional() }) export type ReopenSessionResponse = z.infer export const ReopenSessionMissingMetadataResponseSchema = z.object({ error: z.string(), missing: z.array(z.string()).nonempty() }) export type ReopenSessionMissingMetadataResponse = z.infer export const CursorChatStoreStatusSchema = z.object({ onDisk: z.boolean(), store: z.enum(['legacy', 'acp']).nullable() }) export type CursorChatStoreStatus = z.infer export const CodexImportedMessageSchema = z.union([ z.object({ role: z.literal('user'), content: z.object({ type: z.literal('text'), text: z.string() }), meta: z.object({ sentFrom: z.literal('cli') }) }), z.object({ role: z.literal('agent'), content: z.object({ type: z.literal('codex'), data: z.unknown() }), meta: z.object({ sentFrom: z.literal('cli') }) }) ]) export const CodexLocalSessionSummarySchema = z.object({ id: z.string().min(1), title: z.string(), lastUserMessage: z.string().nullable().optional(), cwd: z.string().nullable().optional(), file: z.string().min(1), modifiedAt: z.number(), originator: z.string().nullable().optional(), cliVersion: z.string().nullable().optional(), source: z.string().nullable().optional(), threadSource: z.string().nullable().optional(), forkedFromId: z.string().nullable().optional() }) export const CodexLocalSessionWithMessagesSchema = CodexLocalSessionSummarySchema.extend({ messages: z.array(CodexImportedMessageSchema) }) export const ListCodexSessionsRpcRequestSchema = z.object({ cwd: z.string().nullable().optional(), sessionIds: z.array(z.string().min(1)).optional() }) export const ListCodexSessionsRpcResponseSchema = z.union([ z.object({ success: z.literal(true), sessions: z.array(z.union([CodexLocalSessionWithMessagesSchema, CodexLocalSessionSummarySchema])) }), z.object({ success: z.literal(false), error: z.string() }) ]) export const ArchiveCodexSessionRpcRequestSchema = z.object({ sessionId: z.string().min(1) }) export const ArchiveCodexSessionRpcResponseSchema = z.union([ z.object({ success: z.literal(true), archivedPath: z.string() }), z.object({ success: z.literal(false), error: z.string() }) ]) export type ListCodexSessionsRpcRequest = z.infer export type ListCodexSessionsRpcResponse = z.infer export type ArchiveCodexSessionRpcRequest = z.infer export type ArchiveCodexSessionRpcResponse = z.infer export const SessionCollaborationModeRequestSchema = z.object({ mode: CodexCollaborationModeSchema }) export type SessionCollaborationModeRequest = z.infer export const SessionModelRequestSchema = z.object({ model: z.union([ z.string().trim().min(1), z.object({ provider: z.string().trim().min(1), modelId: z.string().trim().min(1), }), ]).nullable() }) export type SessionModelRequest = z.infer export const SessionModelReasoningEffortRequestSchema = z.object({ modelReasoningEffort: z.string().trim().min(1).nullable() }) export type SessionModelReasoningEffortRequest = z.infer export const SessionEffortRequestSchema = z.object({ effort: z.string().trim().min(1).nullable() }) export type SessionEffortRequest = z.infer // Fast mode is an explicit two-way choice. `'standard'` (not `null`) is the // stored sentinel for an explicit Fast-off so it stays distinct from // "untouched" and survives restart/resume. Reject anything else so stray tier // strings are never forwarded to the Codex app-server. export const SessionServiceTierRequestSchema = z.object({ serviceTier: z.enum(['fast', 'standard']) }) export type SessionServiceTierRequest = z.infer export const RenameSessionRequestSchema = z.object({ name: z.string().min(1).max(255) }) export type RenameSessionRequest = z.infer /** * An empty string clears the custom name, so unlike session rename there is no * `min(1)`: the machine falls back to its hostname. The length ceiling is * enforced after trimming, so it is not expressed here. */ export const RenameMachineRequestSchema = z.object({ displayName: z.string() }) export type RenameMachineRequest = z.infer export const MACHINE_DISPLAY_NAME_MAX_LENGTH = 64 /** * Scratchlist v2 (tiann/hapi#893) per-entry caps. * * `MAX_ENTRIES` (200) is a per-session ceiling: refuses to create entry * 201 on the hub. Mirrors `SCRATCHLIST_MAX_ENTRIES` in * `web/src/lib/scratchlist.ts` so the hub and web agree on the limit - * the web side has UX for the cap (disabled add button + atCap hint), * the hub side enforces it as a server-side guard against malicious / * runaway clients writing arbitrary amounts. * * `MAX_TEXT_LENGTH` (10_000) is the per-entry text cap. Mirrors * `SCRATCHLIST_MAX_TEXT_LENGTH`. The web side truncates rather than * rejects; the hub-side schema allows up to this length and rejects * anything longer with 400. */ export const SCRATCHLIST_MAX_ENTRIES = 200 export const SCRATCHLIST_MAX_TEXT_LENGTH = 10_000 /** * Hard cap on client-supplied entry id length. The id is persisted as * part of the SQLite primary key, so without a bound an authenticated * client could grow the table and its index with arbitrarily large * keys. 128 chars comfortably fits a UUID (36) plus any prefix scheme * we might layer on later. */ export const SCRATCHLIST_MAX_ENTRY_ID_LENGTH = 128 import { ScratchlistAttachmentsArraySchema } from './scratchlistAttachments' export const ScratchlistEntryCreateRequestSchema = z.object({ /** * Optional client-supplied entry id. Lets the web client preserve its * pre-v2 localStorage entry ids during migration so the optimistic- * update path doesn't have to re-key entries already in the React * tree. New entries created post-v2 can omit this and let the hub * generate one. */ entryId: z.string().min(1).max(SCRATCHLIST_MAX_ENTRY_ID_LENGTH).optional(), text: z.string().max(SCRATCHLIST_MAX_TEXT_LENGTH).default(''), attachments: ScratchlistAttachmentsArraySchema.optional().default([]), /** * Optional client-supplied createdAt. Used by the migration path to * preserve the original timestamps from localStorage. New entries * omit this and let the hub stamp `Date.now()`. */ createdAt: z.number().int().nonnegative().optional() }).refine( (data) => data.text.trim().length > 0 || data.attachments.length > 0, { message: 'Scratchlist entry requires text or attachments', path: ['text'] } ) export type ScratchlistEntryCreateRequest = z.infer export const ScratchlistEntryUpdateRequestSchema = z.object({ text: z.string().max(SCRATCHLIST_MAX_TEXT_LENGTH).optional(), attachments: ScratchlistAttachmentsArraySchema.optional(), }).refine( (data) => data.text !== undefined || data.attachments !== undefined, { message: 'Update requires text and/or attachments', path: ['text'] } ).refine( (data) => { // Attachments-only patch may clear the list (`[]`) while keeping text. if (data.text === undefined && data.attachments !== undefined) { return true } if (data.text !== undefined && data.attachments === undefined) { return data.text.trim().length > 0 } if (data.text !== undefined && data.attachments !== undefined) { return data.text.trim().length > 0 || data.attachments.length > 0 } return true }, { message: 'Scratchlist entry requires non-empty text or attachments', path: ['text'] } ) export type ScratchlistEntryUpdateRequest = z.infer /** Per-session legacy stream-json → ACP migrator request. See tiann/hapi#824. */ export const CursorMigrateToAcpRequestSchema = z.object({ /** Skip removing the legacy ~/.cursor/chats source store.db even after verify passes. */ keepSource: z.boolean().optional(), /** Allow migrating a session whose lifecycleState === 'running' by archiving it first. */ forceArchiveRunning: z.boolean().optional(), /** Skip the verify-by-prompt step (session/load alone is run). */ skipVerify: z.boolean().optional() }) export type CursorMigrateToAcpRequest = z.infer export type CursorMigrateOutcome = | { ok: true; sessionId: string; acpSessionId: string; replayNotifications: number; durationMs: number; lastUsedModelPreserved: string | null; sourceRemoved: boolean } | { ok: false; sessionId: string; reason: CursorMigrateRefusalReason; message: string; durationMs: number } export type CursorMigrateRefusalReason = | 'not_cursor_session' | 'already_acp' | 'running_refused' | 'no_cursor_session_id' | 'no_legacy_store_on_disk' | 'target_already_exists' | 'verify_load_failed' | 'verify_prompt_failed' | 'metadata_write_failed' | 'archive_failed' | 'lock_release_timeout' | 'acp_transport_active' | 'session_resumed_during_migrate' | 'legacy_store_modified_during_migrate' | 'cross_host_session' | 'ambiguous_legacy_store' | 'size_mismatch' | 'internal_error' export const UploadFileRequestSchema = z.object({ filename: z.string().min(1).max(255), content: z.string().min(1), mimeType: z.string().min(1).max(255) }) export type UploadFileRequest = z.infer export const DeleteUploadRequestSchema = z.object({ path: z.string().min(1) }) export type DeleteUploadRequest = z.infer export const MessagesQuerySchema = z.object({ limit: z.coerce.number().int().min(1).max(200).optional(), beforeSeq: z.coerce.number().int().min(1).optional(), beforeAt: z.coerce.number().int().min(0).optional(), afterSeq: z.coerce.number().int().min(1).optional(), afterAt: z.coerce.number().int().min(0).optional(), untilSeq: z.coerce.number().int().min(1).optional(), untilAt: z.coerce.number().int().min(0).optional(), epoch: z.coerce.number().int().min(0).optional(), }) .refine((data) => (data.beforeAt === undefined) === (data.beforeSeq === undefined), { message: 'beforeAt and beforeSeq must be provided together', path: ['beforeAt'], }) .refine((data) => (data.afterAt === undefined) === (data.afterSeq === undefined), { message: 'afterAt and afterSeq must be provided together', path: ['afterAt'], }) .refine((data) => (data.untilAt === undefined) === (data.untilSeq === undefined), { message: 'untilAt and untilSeq must be provided together', path: ['untilAt'], }) .refine((data) => data.beforeAt === undefined || data.afterAt === undefined, { message: 'before and after cursors are mutually exclusive', path: ['afterAt'], }) .refine((data) => data.untilAt === undefined || data.afterAt !== undefined, { message: 'until cursor requires an after cursor', path: ['untilAt'], }) .refine((data) => data.epoch === undefined || data.afterAt !== undefined, { message: 'epoch requires an after cursor', path: ['epoch'], }) export type MessagesQuery = z.infer export const SendMessageRequestSchema = z.object({ text: z.string(), localId: z.string().min(1).optional(), attachments: z.array(AttachmentMetadataSchema).optional(), scheduledAt: z.number().int().positive().nullable().optional() }).refine( (data) => data.scheduledAt == null || typeof data.localId === 'string', { message: 'scheduledAt requires localId', path: ['localId'] } ).refine( (data) => data.scheduledAt == null || data.scheduledAt <= Date.now() + 7 * 24 * 60 * 60 * 1000, { message: 'scheduledAt must be within 7 days from now', path: ['scheduledAt'] } ).refine( (data) => data.scheduledAt == null || !data.attachments?.length, { message: 'scheduled messages with attachments are not supported', path: ['attachments'] } ) export type SendMessageRequest = z.infer export const QueuedStateRequestSchema = z.object({ localIds: z.array(z.string().min(1)).max(1000) }) export type QueuedStateRequest = z.infer export type QueuedStateResponse = { queuedLocalIds: string[] invokedLocalMessages: Array<{ localId: string invokedAt: number }> } export const SpawnSessionRequestSchema = z.object({ directory: z.string().min(1), agent: AgentFlavorSchema.optional(), model: z.string().optional(), effort: z.string().optional(), modelReasoningEffort: z.string().optional(), yolo: z.boolean().optional(), permissionMode: PermissionModeSchema.optional(), sessionType: z.enum(['simple', 'worktree']).optional(), worktreeName: z.string().optional(), serviceTier: z.enum(['fast', 'standard']).optional(), collaborationMode: CodexCollaborationModeSchema.optional() }) export type SpawnSessionRequest = z.infer export const MachineListDirectoryRequestSchema = z.object({ path: z.string().min(1) }) export type MachineListDirectoryRequest = z.infer export const MachinePathsExistsRequestSchema = z.object({ paths: z.array(z.string().min(1)).max(1000) }) export type MachinePathsExistsRequest = z.infer export const AuthRequestSchema = z.union([ z.object({ initData: z.string() }), z.object({ accessToken: z.string() }) ]) export type AuthRequest = z.infer export type CommandResponse = { success: boolean stdout?: string stderr?: string exitCode?: number error?: string } export type GitCommandResponse = CommandResponse export type FileReadResponse = { success: boolean content?: string error?: string } export type GeneratedImageResponse = { success: boolean content?: string mimeType?: string fileName?: string error?: string } export type UploadFileResponse = { success: boolean path?: string error?: string } export type DeleteUploadResponse = { success: boolean error?: string } export type DirectoryEntry = { name: string type: 'file' | 'directory' | 'other' size?: number modified?: number } export type ListDirectoryResponse = { success: boolean entries?: DirectoryEntry[] error?: string } export type RpcListDirectoryResponse = ListDirectoryResponse export type FileMetadataEntry = { path: string size?: number modified?: number } export type StatFilesResponse = { success: boolean entries?: FileMetadataEntry[] error?: string } export type MachineDirectoryEntry = DirectoryEntry & { isGitRepo?: boolean } export type MachineListDirectoryResponse = { success: boolean entries?: MachineDirectoryEntry[] error?: string } export type PathExistsResponse = { exists: Record } export type MachinePathsExistsResponse = PathExistsResponse export type CodexModelSummary = { id: string displayName: string isDefault: boolean defaultReasoningEffort?: string | null defaultServiceTier?: string | null supportedReasoningEfforts?: string[] /** Service tier ids advertised for this model in the current auth/plan context (e.g. 'fast'). */ serviceTiers?: string[] } export type CodexModelsResponse = { success: boolean models?: CodexModelSummary[] error?: string } export type ListCodexModelsResponse = CodexModelsResponse export type OpencodeModelSummary = { modelId: string name?: string } export type OpencodeModelsResponse = { success: boolean availableModels?: OpencodeModelSummary[] /** CLI `agent --list-models` skus grouped under ACP wire bases for variant pickers. */ cliModelSkus?: OpencodeModelSummary[] currentModelId?: string | null error?: string } export type ListOpencodeModelsResponse = OpencodeModelsResponse export type GrokModelSummary = { modelId: string name?: string reasoningEfforts?: GrokReasoningEffortOption[] } export type GrokReasoningEffortOption = { value: string name?: string isDefault?: boolean } export type GrokModelsResponse = { success: boolean availableModels?: GrokModelSummary[] currentModelId?: string | null autoPermissionModeSupported?: boolean error?: string } export type ListGrokModelsResponse = GrokModelsResponse export type GrokReasoningEffortResponse = { success: boolean options?: GrokReasoningEffortOption[] currentValue?: string | null error?: string } export type OpencodeReasoningEffortOption = { value: string name?: string } export type OpencodeReasoningEffortResponse = { success: boolean options?: OpencodeReasoningEffortOption[] currentValue?: string | null error?: string } export type CursorModelSummary = OpencodeModelSummary export type CursorModelsResponse = OpencodeModelsResponse export type ListCursorModelsResponse = CursorModelsResponse /** Maps thinking levels to provider-specific values. null = unsupported. */ export type PiThinkingLevelMap = Partial> export type PiModelSummary = { provider: string modelId: string name?: string contextWindow?: number /** Whether the model supports reasoning/thinking */ reasoning?: boolean /** Maps Pi thinking levels to provider values; null = unsupported level */ thinkingLevelMap?: PiThinkingLevelMap } export type PiModelsResponse = { success: boolean availableModels?: PiModelSummary[] currentModelId?: string | null error?: string } export type ListPiModelsResponse = PiModelsResponse export type PiCommandSummary = { name: string description?: string source: 'extension' | 'prompt' | 'skill' } export type PiCommandsResponse = { success: boolean commands?: PiCommandSummary[] error?: string } export type SlashCommand = { name: string description?: string source: 'builtin' | 'user' | 'plugin' | 'project' content?: string pluginName?: string } export type SlashCommandsResponse = { success: boolean commands?: SlashCommand[] error?: string } export type SqliteStorageUsageResponse = { path: string databaseBytes: number walBytes: number shmBytes: number totalBytes: number }