SPB Git

Repository Public

dto.ts 14.6 KB 391 lines typescript

1/**2 * Browser-facing DTOs (REST /api/v1). The server validates every request body with these3 * schemas; the web app imports the inferred types. Timestamps are ISO-8601 strings.4 * Filesystem paths exposed to the browser are always RELATIVE to a project root5 * ("" = project root), never absolute host paths, except Project.path which is6 * relative to a workspace root and shown as `<rootLabel>/<rel>`.7 */8import { z } from 'zod';9import { Health, SessionState, TaskState } from './states.js';10import { Branding } from './agent.js';1112const iso = z.string();13export const Id = z.string().uuid();1415// ---------- auth ----------16export const AuthStatus = z.object({17  setupRequired: z.boolean(), // no administrator exists yet -> setup flow with one-time token18  authenticated: z.boolean(),19  user: z.object({ id: Id, username: z.string() }).nullable(),20  methods: z.object({ password: z.boolean(), passkeys: z.number(), totp: z.boolean() }),21  csrfToken: z.string().nullable(), // required as X-CSRF-Token on every mutating request22  sessionExpiresAt: iso.nullable(),23  /** Which product this instance is (Claude Remote / Codex Remote). Optional: older servers omit it. */24  branding: Branding.optional(),25});26export type AuthStatus = z.infer<typeof AuthStatus>;2728export const SetupRequest = z.object({29  setupToken: z.string().min(16),30  username: z.string().min(3).max(32).regex(/^[a-z0-9_.-]+$/i),31  password: z.string().min(12).max(256),32});33export const LoginPasswordRequest = z.object({34  username: z.string().max(64),35  password: z.string().max(256),36  totp: z.string().regex(/^\d{6}$/).optional(),37});38/** Password OK but second factor still needed. */39export const LoginStep = z.object({40  ok: z.boolean(),41  next: z.enum(['done', 'totp', 'passkey', 'enroll_second_factor']),42  retryAfterSec: z.number().optional(),43});44export type LoginStep = z.infer<typeof LoginStep>;45export const TotpEnrollStart = z.object({ secret: z.string(), otpauthUrl: z.string() });46export const TotpEnrollFinish = z.object({ code: z.string().regex(/^\d{6}$/) });47export const Device = z.object({48  id: Id,49  kind: z.enum(['passkey', 'session']),50  label: z.string(),51  createdAt: iso,52  lastUsedAt: iso.nullable(),53  current: z.boolean(),54  userAgent: z.string().nullable(),55  ip: z.string().nullable(),56});57export type Device = z.infer<typeof Device>;5859// ---------- projects ----------60export const Project = z.object({61  id: Id,62  name: z.string(),63  description: z.string(),64  rootLabel: z.string(), // label of the workspace root, e.g. "workspace"65  path: z.string(), // relative to that root66  gitRemote: z.string().nullable(),67  gitBranch: z.string().nullable(),68  gitDirty: z.boolean().nullable(),69  tags: z.array(z.string()),70  pinned: z.boolean(),71  settings: z.record(z.string(), z.unknown()),72  activeSessions: z.number(),73  lastActivityAt: iso.nullable(),74  createdAt: iso,75});76export type Project = z.infer<typeof Project>;77export const CreateProject = z.object({78  name: z.string().min(1).max(80),79  description: z.string().max(2000).default(''),80  mode: z.enum(['create', 'existing', 'clone']),81  rootLabel: z.string().optional(), // default: first workspace root82  path: z.string().max(400).optional(), // relative dir (create/existing); default slug(name)83  gitUrl: z.string().max(500).optional(), // required when mode=clone84  tags: z.array(z.string().max(40)).max(20).default([]),85});86export type CreateProject = z.infer<typeof CreateProject>;87export const UpdateProject = z.object({88  name: z.string().min(1).max(80).optional(),89  description: z.string().max(2000).optional(),90  tags: z.array(z.string().max(40)).max(20).optional(),91  pinned: z.boolean().optional(),92  settings: z.record(z.string(), z.unknown()).optional(),93});9495// ---------- terminal sessions ----------96/**97 * `claude` = the instance's agent CLI (Claude Code, or Codex when the server runs with98 * `CR_AGENT=codex`); the value is historical and kept for wire / database compatibility.99 */100export const SessionKind = z.enum(['claude', 'shell']);101export const TerminalSession = z.object({102  id: Id,103  projectId: Id.nullable(),104  projectName: z.string().nullable(),105  kind: SessionKind,106  title: z.string(),107  state: SessionState,108  cwd: z.string(), // relative to project root ("" = root)109  pid: z.number().nullable(),110  claudeConversationId: z.string().nullable(), // set when started with resume, or learned from hook111  pinned: z.boolean(),112  archived: z.boolean(),113  attachedClients: z.number(),114  exitCode: z.number().nullable(),115  createdAt: iso,116  lastActivityAt: iso.nullable(),117  endedAt: iso.nullable(),118  waitingReason: z.string().nullable(), // short hook message; never terminal content119});120export type TerminalSession = z.infer<typeof TerminalSession>;121export const CreateSession = z.object({122  projectId: Id.nullable().default(null),123  kind: SessionKind.default('claude'),124  title: z.string().max(120).optional(),125  cwd: z.string().max(400).default(''),126  resumeConversationId: z.string().max(100).optional(), // claude --resume <id>127  continueLast: z.boolean().optional(), // claude --continue128  cols: z.number().int().min(20).max(500).default(120),129  rows: z.number().int().min(5).max(200).default(32),130});131export type CreateSession = z.infer<typeof CreateSession>;132export const UpdateSession = z.object({133  title: z.string().min(1).max(120).optional(),134  pinned: z.boolean().optional(),135  archived: z.boolean().optional(),136});137export const ClaudeConversation = z.object({138  id: z.string(),139  summary: z.string(), // first user prompt, truncated to 160 chars140  updatedAt: iso,141  messageCount: z.number(),142});143export type ClaudeConversation = z.infer<typeof ClaudeConversation>;144145// ---------- files ----------146export const FileEntry = z.object({147  name: z.string(),148  path: z.string(), // project-relative149  type: z.enum(['file', 'dir', 'symlink', 'other']),150  size: z.number(),151  mtimeMs: z.number(),152  binary: z.boolean().optional(),153});154export type FileEntry = z.infer<typeof FileEntry>;155export const FileContent = z.object({156  path: z.string(),157  size: z.number(),158  mtimeMs: z.number(),159  sha256: z.string(),160  encoding: z.enum(['utf8']),161  content: z.string(),162});163export type FileContent = z.infer<typeof FileContent>;164export const WriteFile = z.object({165  path: z.string(),166  content: z.string(),167  /** sha256 of the content the editor loaded; omitted when creating. Mismatch -> 409 conflict. */168  baseSha256: z.string().optional(),169  create: z.boolean().optional(),170});171export const FsOp = z.discriminatedUnion('op', [172  z.object({ op: z.literal('mkdir'), path: z.string() }),173  z.object({ op: z.literal('rename'), from: z.string(), to: z.string() }),174  z.object({ op: z.literal('delete'), path: z.string(), confirm: z.literal(true) }),175]);176export const SearchHit = z.object({ path: z.string(), line: z.number().nullable(), preview: z.string().nullable() });177export type SearchHit = z.infer<typeof SearchHit>;178export const GitStatus = z.object({179  isRepo: z.boolean(),180  branch: z.string().nullable(),181  ahead: z.number(),182  behind: z.number(),183  files: z.array(z.object({ path: z.string(), index: z.string(), worktree: z.string() })),184});185export type GitStatus = z.infer<typeof GitStatus>;186187// ---------- tasks ----------188export const AgentTask = z.object({189  id: Id,190  projectId: Id.nullable(),191  title: z.string(),192  command: z.string(),193  host: z.string(), // "local" or an allow-listed node alias194  cwd: z.string(),195  state: TaskState,196  exitCode: z.number().nullable(),197  timeoutSec: z.number().nullable(),198  retryOf: Id.nullable(),199  idempotent: z.boolean(), // retry is only offered when true200  createdAt: iso,201  startedAt: iso.nullable(),202  endedAt: iso.nullable(),203  logBytes: z.number(),204  cpuSec: z.number().nullable(),205  maxRssMb: z.number().nullable(),206  artifacts: z.array(z.object({ id: Id, name: z.string(), size: z.number(), createdAt: iso })),207});208export type AgentTask = z.infer<typeof AgentTask>;209export const CreateTask = z.object({210  projectId: Id.nullable().default(null),211  title: z.string().min(1).max(160),212  command: z.string().min(1).max(8000),213  host: z.string().max(40).default('local'),214  cwd: z.string().max(400).default(''),215  timeoutSec: z.number().int().min(1).max(7 * 24 * 3600).nullable().default(null),216  idempotent: z.boolean().default(false),217  artifactGlobs: z.array(z.string().max(200)).max(20).default([]),218});219export type CreateTask = z.infer<typeof CreateTask>;220221// ---------- cluster ----------222export const ClusterNode = z.object({223  id: z.string(), // alias, e.g. "M5M36"224  model: z.string().nullable(),225  chip: z.string().nullable(),226  kind: z.enum(['mac', 'server', 'mobile', 'storage', 'other']),227  role: z.string().nullable(),228  cpuCores: z.number().nullable(),229  gpuCores: z.number().nullable(),230  memoryMb: z.number().nullable(),231  os: z.string().nullable(),232  health: Health,233  metricsAt: iso.nullable(), // when the metrics below were collected; null => never234  stale: z.boolean(),235  cpuPct: z.number().nullable(),236  memUsedMb: z.number().nullable(),237  diskFreeGb: z.number().nullable(),238  diskTotalGb: z.number().nullable(),239  load1: z.number().nullable(),240  apps: z.array(z.string()),241  execAllowed: z.boolean(), // on the remote-execution allowlist AND reachable with the runtime key242  isHost: z.boolean(), // this is the node Claude Remote runs on243});244export type ClusterNode = z.infer<typeof ClusterNode>;245export const ClusterOverview = z.object({246  source: z.enum(['agentd', 'none']),247  collectedAt: iso.nullable(),248  health: Health,249  nodes: z.array(ClusterNode),250  edges: z.array(z.object({ from: z.string(), to: z.string(), kind: z.enum(['lan', 'tunnel', 'service']) })),251  totals: z.object({ nodes: z.number(), online: z.number(), cores: z.number(), memoryGb: z.number() }),252});253export type ClusterOverview = z.infer<typeof ClusterOverview>;254export const LocalModel = z.object({255  id: z.string(),256  name: z.string(),257  host: z.string(),258  engine: z.string().nullable(),259  endpoint: z.string(), // server-side only reachable; shown as label260  health: Health,261  checkedAt: iso.nullable(),262  latencyMs: z.number().nullable(),263  tokensPerSec: z.number().nullable(),264});265export type LocalModel = z.infer<typeof LocalModel>;266267// ---------- skills / MCP ----------268export const SkillInfo = z.object({269  name: z.string(),270  description: z.string(),271  source: z.enum(['user', 'project', 'plugin', 'registry']),272  kind: z.enum(['skill', 'command', 'agent']),273  version: z.string().nullable(),274  location: z.string(), // display label like "~/.claude/skills/foo"275  enabled: z.boolean(),276  updateAvailable: z.boolean().nullable(), // null => registry not connected277});278export type SkillInfo = z.infer<typeof SkillInfo>;279export const McpServerInfo = z.object({280  name: z.string(),281  scope: z.enum(['user', 'project', 'local']),282  transport: z.enum(['stdio', 'http', 'sse']),283  target: z.string(), // command or URL — secrets (headers/env values) are NEVER included284  envKeys: z.array(z.string()), // names only285  status: z.enum(['connected', 'failed', 'needs_auth', 'unknown']),286  lastError: z.string().nullable(),287  checkedAt: iso.nullable(),288});289export type McpServerInfo = z.infer<typeof McpServerInfo>;290export const AddMcpServer = z.object({291  name: z.string().regex(/^[a-zA-Z0-9_-]{1,40}$/),292  transport: z.enum(['stdio', 'http', 'sse']),293  target: z.string().min(1).max(1000), // command (stdio) or URL294  args: z.array(z.string().max(1000)).max(40).default([]),295  env: z.record(z.string(), z.string()).default({}), // write-only; never echoed back296  headers: z.record(z.string(), z.string()).default({}), // write-only297  scope: z.enum(['user', 'project']).default('user'),298  projectId: Id.optional(),299});300301// ---------- observability / notifications / settings ----------302export const ComponentHealth = z.object({303  component: z.string(),304  health: Health,305  detail: z.string().nullable(),306  checkedAt: iso,307});308export const SystemStatus = z.object({309  collectedAt: iso,310  version: z.string(),311  components: z.array(ComponentHealth),312  host: z.object({313    name: z.string(),314    cpuPct: z.number().nullable(),315    load: z.array(z.number()),316    memUsedMb: z.number().nullable(),317    memTotalMb: z.number().nullable(),318    diskFreeGb: z.number().nullable(),319    diskTotalGb: z.number().nullable(),320    uptimeSec: z.number().nullable(),321  }),322  counts: z.object({323    liveSessions: z.number(),324    wsClients: z.number(),325    runningTasks: z.number(),326    queuedTasks: z.number(),327  }),328  /** The instance's agent CLI (historical key). `agent`/`agentName` say which one it is. */329  claude: z.object({330    version: z.string().nullable(),331    authenticated: z.boolean().nullable(),332    agent: z.enum(['claude', 'codex']).optional(),333    agentName: z.string().optional(),334  }),335});336export type SystemStatus = z.infer<typeof SystemStatus>;337export const NotificationKind = z.enum([338  'session.waiting',339  'session.exited',340  'task.succeeded',341  'task.failed',342  'node.offline',343  'storage.low',344  'system',345]);346export const Notification = z.object({347  id: Id,348  kind: NotificationKind,349  title: z.string(),350  body: z.string(), // generic by default; detail level is a user setting351  link: z.string().nullable(),352  readAt: iso.nullable(),353  createdAt: iso,354});355export type Notification = z.infer<typeof Notification>;356export const AuditEvent = z.object({357  id: z.string(),358  at: iso,359  actor: z.string(),360  action: z.string(),361  target: z.string().nullable(),362  ip: z.string().nullable(),363  ok: z.boolean(),364  detail: z.record(z.string(), z.unknown()),365});366export type AuditEvent = z.infer<typeof AuditEvent>;367export const UserSettings = z.object({368  theme: z.enum(['light', 'dark', 'system']).default('light'),369  /** Terminal colours: 'auto' follows the app theme. */370  terminalTheme: z.enum(['auto', 'light', 'dark']).default('auto'),371  terminalFont: z.string().default('JetBrains Mono'),372  terminalFontSize: z.number().min(8).max(28).default(13),373  mobileFontSize: z.number().min(8).max(28).default(12),374  scrollback: z.number().min(500).max(50000).default(5000),375  defaultProjectId: Id.nullable().default(null),376  defaultLayout: z.enum(['terminal', 'terminal+files', 'terminal+editor', 'terminal+monitoring', 'research']).default('terminal+files'),377  notifications: z378    .object({ inApp: z.boolean(), webPush: z.boolean(), detail: z.enum(['generic', 'titles']) })379    .default({ inApp: true, webPush: false, detail: 'generic' }),380  sessionRetentionDays: z.number().min(1).max(3650).default(90),381  mobileToolbar: z.array(z.string()).default(['esc', 'tab', 'ctrl', 'alt', 'shift', 'up', 'down', 'left', 'right', 'enter', '/', '|', '~', '-']),382  autosave: z.boolean().default(false),383  layout: z.record(z.string(), z.unknown()).default({}), // persisted panel sizes etc.384});385export type UserSettings = z.infer<typeof UserSettings>;386387export const ApiError = z.object({388  error: z.object({ code: z.string(), message: z.string(), details: z.unknown().optional() }),389});390export type ApiError = z.infer<typeof ApiError>;391
Files main
Menu

Theme