# DO NOT EDIT BY HAND. Generated from server/openapi.js via `npm run openapi:yaml`.
# This YAML mirrors the live spec served at GET /api/openapi.json.
openapi: 3.0.3
info:
  title: Agent Dashboard for Claude Code API
  version: 1.4.6
  description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
  contact:
    name: Nguyễn Ngọc Trí Vĩ
    email: vinnt@smartgift.vn
    url: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor
externalDocs:
  description: Project documentation
  url: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor#readme
servers:
  - url: http://localhost:4820
    description: Local dashboard server (default)
  - url: http://127.0.0.1:4820
    description: Local loopback endpoint used by hook-handler
tags:
  - name: Health
    description: Service liveness checks
  - name: Metrics
    description: Prometheus / OpenMetrics scrape endpoint
  - name: Sessions
    description: Claude Code session lifecycle
  - name: Agents
    description: Main/subagent records and status
  - name: Events
    description: Event stream persistence
  - name: Stats
    description: High-level dashboard counters
  - name: Analytics
    description: Aggregated analytics views
  - name: Hooks
    description: Claude hook ingestion endpoint
  - name: Pricing
    description: Model pricing and token cost calculations
  - name: Workflows
    description: Workflow intelligence and session drill-in
  - name: Settings
    description: Operational maintenance endpoints
  - name: Updates
    description: Detect upstream git changes so users can pull and restart manually (local dashboard installs)
  - name: Alerts
    description: 'Rules-based alerting: rule CRUD, fired-alert feed, acknowledgement'
  - name: Webhooks
    description: 'Universal webhook delivery for fired alerts: target CRUD (Slack/Discord/Teams/generic), test probe, and delivery log. Secrets are never returned.'
  - name: Remote Sources
    description: Config for remote SSH machines the dashboard pulls Claude Code history from. No secrets are stored — SSH auth defers to the host's SSH stack (agent, ~/.ssh/config, keys).
  - name: Documentation
    description: OpenAPI/Swagger endpoints
  - name: CcConfig
    description: 'Claude Code configuration explorer: discover and (for low-risk artifacts) edit skills, agents, commands, output styles, MCP servers, hooks, settings, memory, plugins, marketplaces, keybindings, statusline'
  - name: Run
    description: Spawn and supervise Claude Code runs from the dashboard (headless or interactive conversation), stream output over WebSocket, and manage run history
  - name: Lanes
    description: Durable parallel-work lanes and dashboard-managed git worktrees
  - name: Push
    description: Web Push notification subscriptions and broadcast (VAPID); also fires native Electron notifications when hosted in the desktop app
components:
  parameters:
    SessionIdPath:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Session ID
    AgentIdPath:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Agent ID
    PatternPath:
      name: pattern
      in: path
      required: true
      schema:
        type: string
      description: Model pattern (URL-encoded)
    LimitQuery:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
      description: Page size
    OffsetQuery:
      name: offset
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
      description: Pagination offset
    SessionStatusQuery:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - active
          - completed
          - error
          - abandoned
      description: Filter by session status
    AgentStatusQuery:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - working
          - waiting
          - completed
          - error
      description: Filter by agent status
    SessionFilterQuery:
      name: session_id
      in: query
      required: false
      schema:
        type: string
      description: Filter by session ID
    WorkflowStatusQuery:
      name: status
      in: query
      required: false
      schema:
        type: string
        enum:
          - all
          - active
          - completed
          - error
          - abandoned
      description: Filter workflow aggregates by session status
    SourcesQuery:
      name: sources
      in: query
      required: false
      schema:
        type: string
      description: Comma-separated list of data-source ids to include. The built-in local history is the id `local`; remote SSH machines use their `remote_sources.id`. Omit to include every source. Filters on the `sessions.source` column.
  schemas:
    ErrorObject:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorObject'
    MessageErrorObject:
      type: object
      required:
        - message
      properties:
        message:
          type: string
    MessageErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/MessageErrorObject'
    CountMap:
      type: object
      additionalProperties:
        type: integer
    Session:
      type: object
      required:
        - id
        - status
        - started_at
        - updated_at
      properties:
        id:
          type: string
        name:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - active
            - completed
            - error
            - abandoned
        cwd:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
        ended_at:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: string
          nullable: true
          description: JSON-encoded session metadata
        updated_at:
          type: string
          format: date-time
        agent_count:
          type: integer
          nullable: true
        last_activity:
          type: string
          format: date-time
          nullable: true
        cost:
          type: number
          nullable: true
        awaiting_input_since:
          type: string
          format: date-time
          nullable: true
          description: ISO timestamp set when Claude Code is blocked waiting for the user (permission prompt or input request). Null when not waiting; cleared on the next non-Notification hook event.
        awaiting_reason:
          type: string
          enum:
            - notification
            - stop
            - session_start
            - interrupted
          nullable: true
          description: 'Why the session is awaiting input: ''notification'' (Claude asked the user something), ''stop'' (turn completed, waiting for the next prompt), ''session_start'' (new/resumed session waiting for the first prompt), or ''interrupted'' (watchdog/Esc recovery). Null when not waiting; cleared alongside awaiting_input_since.'
    Agent:
      type: object
      required:
        - id
        - session_id
        - name
        - type
        - status
        - started_at
        - updated_at
      properties:
        id:
          type: string
        session_id:
          type: string
        name:
          type: string
        type:
          type: string
          enum:
            - main
            - subagent
        subagent_type:
          type: string
          nullable: true
        status:
          type: string
          enum:
            - working
            - waiting
            - completed
            - error
        task:
          type: string
          nullable: true
        current_tool:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
        ended_at:
          type: string
          format: date-time
          nullable: true
        parent_agent_id:
          type: string
          nullable: true
        metadata:
          type: string
          nullable: true
          description: JSON-encoded agent metadata
        updated_at:
          type: string
          format: date-time
        awaiting_input_since:
          type: string
          format: date-time
          nullable: true
          description: ISO timestamp set when this agent is blocked waiting for user input. Cleared on the next non-Notification hook event for the session.
        awaiting_reason:
          type: string
          enum:
            - notification
            - stop
            - session_start
            - interrupted
          nullable: true
          description: 'Why this agent is awaiting input: ''notification'' (Claude asked the user something), ''stop'' (turn completed, waiting for the next prompt), ''session_start'' (new/resumed session waiting for the first prompt), or ''interrupted'' (watchdog/Esc recovery). Null when not waiting; cleared alongside awaiting_input_since.'
    DashboardEvent:
      type: object
      required:
        - session_id
        - event_type
        - created_at
      properties:
        id:
          type: integer
          nullable: true
        session_id:
          type: string
        agent_id:
          type: string
          nullable: true
        event_type:
          type: string
        tool_name:
          type: string
          nullable: true
        summary:
          type: string
          nullable: true
        data:
          type: string
          nullable: true
          description: JSON-encoded event payload
        created_at:
          type: string
          format: date-time
    HealthResponse:
      type: object
      required:
        - status
        - timestamp
        - version
      properties:
        status:
          type: string
          enum:
            - ok
        version:
          type: string
          description: Dashboard release version from package.json
          example: 1.4.6
        timestamp:
          type: string
          format: date-time
    SessionsListResponse:
      type: object
      required:
        - sessions
        - limit
        - offset
        - total
      properties:
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/Session'
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
          description: Total sessions matching the filters (independent of limit/offset). Used by paginators.
    SessionCreateRequest:
      type: object
      required:
        - id
      properties:
        id:
          type: string
        name:
          type: string
        cwd:
          type: string
        model:
          type: string
        metadata:
          type: object
          additionalProperties: true
    SessionCreateResponse:
      type: object
      required:
        - session
        - created
      properties:
        session:
          $ref: '#/components/schemas/Session'
        created:
          type: boolean
    SessionDetailResponse:
      type: object
      required:
        - session
        - agents
        - events
      properties:
        session:
          $ref: '#/components/schemas/Session'
        agents:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        events:
          type: array
          items:
            $ref: '#/components/schemas/DashboardEvent'
    TranscriptInfo:
      type: object
      required:
        - id
        - name
        - type
        - has_transcript
      properties:
        id:
          type: string
          description: '''main'' for the session''s main transcript, otherwise a subagent or compaction id.'
        name:
          type: string
        type:
          type: string
          enum:
            - main
            - subagent
            - compaction
        subagent_type:
          type: string
          nullable: true
        has_transcript:
          type: boolean
        db_agent_id:
          type: string
          nullable: true
          description: Foreign key into agents.id when this transcript belongs to a tracked subagent.
    TranscriptListResponse:
      type: object
      required:
        - transcripts
      properties:
        transcripts:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptInfo'
    TranscriptContent:
      type: object
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - text
            - tool_use
            - tool_result
            - thinking
        text:
          type: string
        name:
          type: string
          description: Tool name when type === tool_use.
        id:
          type: string
          description: Tool-use id used to pair tool_use with tool_result.
        input:
          description: 'Tool input payload (object) or { _truncated: string } when oversized.'
          oneOf:
            - type: object
              additionalProperties: true
            - type: string
        output:
          type: string
          description: Tool output text when type === tool_result.
        is_error:
          type: boolean
    TranscriptMessage:
      type: object
      required:
        - type
        - content
      properties:
        type:
          type: string
          enum:
            - user
            - assistant
            - session_event
          description: Raw line type. session_event is a synthetic marker (e.g. a /rename) injected by the server, not a real transcript line. Mid-turn queued messages (queued_command attachments) surface as type user.
        sender:
          type: string
          enum:
            - user
            - assistant
            - orchestrator
            - system
            - tool
          description: 'TRUE sender of the line: the human (user), the agent (assistant), a subagent''s task assigned by its parent (orchestrator), a harness injection such as a task-notification or [SYSTEM NOTIFICATION …] banner (system), or a tool_result echoed on a user line (tool).'
        timestamp:
          type: string
          format: date-time
          nullable: true
        content:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptContent'
        line:
          type: integer
          minimum: 1
          description: JSONL line number the message was parsed from (pagination cursor).
        model:
          type: string
        usage:
          type: object
          properties:
            input_tokens:
              type: integer
              minimum: 0
            output_tokens:
              type: integer
              minimum: 0
            cache_read_input_tokens:
              type: integer
              minimum: 0
            cache_creation_input_tokens:
              type: integer
              minimum: 0
        event_kind:
          type: string
          enum:
            - rename
          description: 'For type === session_event: the TUI action this marker represents.'
        title:
          type: string
          description: 'For type === session_event: the new session title.'
    TranscriptResponse:
      type: object
      required:
        - messages
        - total
        - has_more
        - last_line
        - first_line
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/TranscriptMessage'
        total:
          type: integer
          minimum: 0
          description: Total messages available in the transcript.
        has_more:
          type: boolean
          description: True when older messages remain (use `before` to load them).
        last_line:
          type: integer
          minimum: 0
          description: JSONL line number of the newest returned message — pass back as `after` for incremental fetches.
        first_line:
          type: integer
          minimum: 0
          description: JSONL line number of the oldest returned message — pass back as `before` to page backwards.
    SessionStatsResponse:
      type: object
      description: Aggregated counts powering the SessionOverview panel on the Session Detail page. All aggregation runs in SQL.
      required:
        - session_id
        - total_events
        - events_by_type
        - tools_used
        - error_count
        - first_event_at
        - last_event_at
        - agents
        - subagent_types
        - tokens
      properties:
        session_id:
          type: string
        total_events:
          type: integer
          minimum: 0
        events_by_type:
          type: array
          items:
            type: object
            required:
              - event_type
              - count
            properties:
              event_type:
                type: string
              count:
                type: integer
                minimum: 0
        tools_used:
          type: array
          description: Top 15 tools used in this session, sorted by count descending.
          items:
            type: object
            required:
              - tool_name
              - count
            properties:
              tool_name:
                type: string
              count:
                type: integer
                minimum: 0
        error_count:
          type: integer
          minimum: 0
          description: Events whose event_type or summary matches /error/i or /failed/i (case-insensitive).
        first_event_at:
          type: string
          format: date-time
          nullable: true
        last_event_at:
          type: string
          format: date-time
          nullable: true
        agents:
          type: object
          required:
            - total
            - main
            - subagent
            - compaction
            - by_status
          properties:
            total:
              type: integer
              minimum: 0
            main:
              type: integer
              minimum: 0
            subagent:
              type: integer
              minimum: 0
            compaction:
              type: integer
              minimum: 0
            by_status:
              type: object
              additionalProperties:
                type: integer
                minimum: 0
        subagent_types:
          type: array
          description: Subagent types in this session with counts. Excludes the special 'compaction' type which is surfaced via agents.compaction.
          items:
            type: object
            required:
              - subagent_type
              - count
            properties:
              subagent_type:
                type: string
              count:
                type: integer
                minimum: 0
        tokens:
          type: object
          required:
            - input_tokens
            - output_tokens
            - cache_read_tokens
            - cache_write_tokens
          properties:
            input_tokens:
              type: integer
              minimum: 0
            output_tokens:
              type: integer
              minimum: 0
            cache_read_tokens:
              type: integer
              minimum: 0
            cache_write_tokens:
              type: integer
              minimum: 0
    SessionUpdateRequest:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum:
            - active
            - completed
            - error
            - abandoned
        ended_at:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true
    SessionUpdateResponse:
      type: object
      required:
        - session
      properties:
        session:
          $ref: '#/components/schemas/Session'
    AgentsListResponse:
      type: object
      required:
        - agents
        - limit
        - offset
      properties:
        agents:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        limit:
          type: integer
        offset:
          type: integer
    AgentCreateRequest:
      type: object
      required:
        - id
        - session_id
        - name
      properties:
        id:
          type: string
        session_id:
          type: string
        name:
          type: string
        type:
          type: string
          enum:
            - main
            - subagent
        subagent_type:
          type: string
        status:
          type: string
          enum:
            - working
            - waiting
            - completed
            - error
        task:
          type: string
        parent_agent_id:
          type: string
        metadata:
          type: object
          additionalProperties: true
    AgentCreateResponse:
      type: object
      required:
        - agent
        - created
      properties:
        agent:
          $ref: '#/components/schemas/Agent'
        created:
          type: boolean
    AgentDetailResponse:
      type: object
      required:
        - agent
      properties:
        agent:
          $ref: '#/components/schemas/Agent'
    AgentUpdateRequest:
      type: object
      properties:
        name:
          type: string
        status:
          type: string
          enum:
            - working
            - waiting
            - completed
            - error
        task:
          type: string
        current_tool:
          type: string
          nullable: true
        ended_at:
          type: string
          format: date-time
        metadata:
          type: object
          additionalProperties: true
    AgentUpdateResponse:
      type: object
      required:
        - agent
      properties:
        agent:
          $ref: '#/components/schemas/Agent'
    EventsListResponse:
      type: object
      required:
        - events
        - limit
        - offset
        - total
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/DashboardEvent'
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
          description: Total rows matching the current filter (for UI pagination)
    EventsFacetsResponse:
      type: object
      required:
        - event_types
        - tool_names
      properties:
        event_types:
          type: array
          items:
            type: string
        tool_names:
          type: array
          items:
            type: string
    StatsResponse:
      type: object
      required:
        - total_sessions
        - active_sessions
        - active_agents
        - total_agents
        - total_events
        - events_today
        - ws_connections
        - agents_by_status
        - sessions_by_status
      properties:
        total_sessions:
          type: integer
        active_sessions:
          type: integer
        active_agents:
          type: integer
        total_agents:
          type: integer
        total_events:
          type: integer
        events_today:
          type: integer
        ws_connections:
          type: integer
        agents_by_status:
          $ref: '#/components/schemas/CountMap'
        sessions_by_status:
          $ref: '#/components/schemas/CountMap'
    AnalyticsResponse:
      type: object
      required:
        - tokens
        - tool_usage
        - daily_events
        - daily_sessions
        - agent_types
        - event_types
        - avg_events_per_session
        - total_subagents
        - overview
        - agents_by_status
        - sessions_by_status
      properties:
        tokens:
          type: object
          required:
            - total_input
            - total_output
            - total_cache_read
            - total_cache_write
          properties:
            total_input:
              type: integer
            total_output:
              type: integer
            total_cache_read:
              type: integer
            total_cache_write:
              type: integer
        tool_usage:
          type: array
          items:
            type: object
            required:
              - tool_name
              - count
            properties:
              tool_name:
                type: string
              count:
                type: integer
        daily_events:
          type: array
          items:
            type: object
            required:
              - date
              - count
            properties:
              date:
                type: string
              count:
                type: integer
        daily_sessions:
          type: array
          items:
            type: object
            required:
              - date
              - count
            properties:
              date:
                type: string
              count:
                type: integer
        agent_types:
          type: array
          items:
            type: object
            required:
              - subagent_type
              - count
            properties:
              subagent_type:
                type: string
                nullable: true
              count:
                type: integer
        event_types:
          type: array
          items:
            type: object
            required:
              - event_type
              - count
            properties:
              event_type:
                type: string
              count:
                type: integer
        avg_events_per_session:
          type: number
        total_subagents:
          type: integer
        overview:
          type: object
          required:
            - total_sessions
            - active_sessions
            - active_agents
            - total_agents
            - total_events
          properties:
            total_sessions:
              type: integer
            active_sessions:
              type: integer
            active_agents:
              type: integer
            total_agents:
              type: integer
            total_events:
              type: integer
        agents_by_status:
          $ref: '#/components/schemas/CountMap'
        sessions_by_status:
          $ref: '#/components/schemas/CountMap'
    HookEventRequest:
      type: object
      required:
        - hook_type
        - data
      properties:
        hook_type:
          type: string
          description: 'Hook type from Claude Code (common values: PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd)'
        data:
          type: object
          required:
            - session_id
          properties:
            session_id:
              type: string
            tool_name:
              type: string
            transcript_path:
              type: string
          additionalProperties: true
    HookEventResponse:
      type: object
      required:
        - ok
        - event
      properties:
        ok:
          type: boolean
          enum:
            - true
        event:
          $ref: '#/components/schemas/DashboardEvent'
    PricingRule:
      type: object
      required:
        - model_pattern
        - display_name
        - input_per_mtok
        - output_per_mtok
        - cache_read_per_mtok
        - cache_write_per_mtok
        - cache_write_1h_per_mtok
        - updated_at
      properties:
        model_pattern:
          type: string
        display_name:
          type: string
        input_per_mtok:
          type: number
        output_per_mtok:
          type: number
        cache_read_per_mtok:
          type: number
        cache_write_per_mtok:
          type: number
          description: 5m ephemeral cache-write rate
        cache_write_1h_per_mtok:
          type: number
          description: 1h ephemeral cache-write rate
        fast_input_per_mtok:
          type: number
          description: Fast-mode input rate (0 = none)
        fast_output_per_mtok:
          type: number
          description: Fast-mode output rate (0 = none)
        updated_at:
          type: string
          format: date-time
    PricingUpsertRequest:
      type: object
      required:
        - model_pattern
        - display_name
      properties:
        model_pattern:
          type: string
        display_name:
          type: string
        input_per_mtok:
          type: number
        output_per_mtok:
          type: number
        cache_read_per_mtok:
          type: number
        cache_write_per_mtok:
          type: number
          description: 5m ephemeral cache-write rate
        cache_write_1h_per_mtok:
          type: number
          description: 1h ephemeral cache-write rate
        fast_input_per_mtok:
          type: number
          description: Fast-mode input rate (0 = none)
        fast_output_per_mtok:
          type: number
          description: Fast-mode output rate (0 = none)
    PricingListResponse:
      type: object
      required:
        - pricing
      properties:
        pricing:
          type: array
          items:
            $ref: '#/components/schemas/PricingRule'
    PricingUpsertResponse:
      type: object
      required:
        - pricing
      properties:
        pricing:
          $ref: '#/components/schemas/PricingRule'
    CostBreakdownItem:
      type: object
      required:
        - model
        - input_tokens
        - output_tokens
        - cache_read_tokens
        - cache_write_tokens
        - cost
        - matched_rule
      properties:
        model:
          type: string
        speed:
          type: string
        inference_geo:
          type: string
        service_tier:
          type: string
        input_tokens:
          type: integer
        output_tokens:
          type: integer
        cache_read_tokens:
          type: integer
        cache_write_tokens:
          type: integer
        cache_write_1h_tokens:
          type: integer
        web_search_requests:
          type: integer
        web_fetch_requests:
          type: integer
        code_execution_requests:
          type: integer
        cost:
          type: number
        matched_rule:
          type: string
          nullable: true
    DailyCostItem:
      type: object
      required:
        - date
        - cost
      properties:
        date:
          type: string
          format: date
        cost:
          type: number
    CostResult:
      type: object
      required:
        - total_cost
        - breakdown
        - daily_costs
      properties:
        total_cost:
          type: number
        breakdown:
          type: array
          items:
            $ref: '#/components/schemas/CostBreakdownItem'
        daily_costs:
          type: array
          items:
            $ref: '#/components/schemas/DailyCostItem'
        feature_costs:
          type: object
          description: Server-tool surcharges separate from token cost
          properties:
            web_search_cost:
              type: number
            web_fetch_cost:
              type: number
            code_execution_cost:
              type: number
            code_execution_hours_estimated:
              type: number
            code_execution_free_hours:
              type: number
        unpriced_models:
          type: array
          description: Models with usage but no matching pricing rule (cost not counted)
          items:
            type: object
            properties:
              model:
                type: string
              input_tokens:
                type: integer
              output_tokens:
                type: integer
              cache_read_tokens:
                type: integer
              cache_write_tokens:
                type: integer
    DeleteOkResponse:
      type: object
      required:
        - ok
      properties:
        ok:
          type: boolean
          enum:
            - true
    WorkflowAggregateResponse:
      type: object
      required:
        - stats
        - orchestration
        - toolFlow
        - effectiveness
        - patterns
        - modelDelegation
        - errorPropagation
        - concurrency
        - complexity
        - compaction
        - cooccurrence
      properties:
        stats:
          type: object
          required:
            - totalSessions
            - totalAgents
            - totalSubagents
            - avgSubagents
            - successRate
            - avgDepth
            - avgDurationSec
            - totalCompactions
            - avgCompactions
          properties:
            totalSessions:
              type: integer
            totalAgents:
              type: integer
            totalSubagents:
              type: integer
            avgSubagents:
              type: number
            successRate:
              type: number
            avgDepth:
              type: number
            avgDurationSec:
              type: integer
            totalCompactions:
              type: integer
            avgCompactions:
              type: number
            topFlow:
              type: object
              nullable: true
              properties:
                source:
                  type: string
                target:
                  type: string
                count:
                  type: integer
        orchestration:
          type: object
          additionalProperties: true
        toolFlow:
          type: object
          additionalProperties: true
        effectiveness:
          type: array
          items:
            type: object
            additionalProperties: true
        patterns:
          type: object
          additionalProperties: true
        modelDelegation:
          type: object
          additionalProperties: true
        errorPropagation:
          type: object
          additionalProperties: true
        concurrency:
          type: object
          additionalProperties: true
        complexity:
          type: array
          items:
            type: object
            additionalProperties: true
        compaction:
          type: object
          additionalProperties: true
        cooccurrence:
          type: array
          items:
            type: object
            additionalProperties: true
    AgentTreeNode:
      type: object
      required:
        - id
        - name
        - type
        - status
        - children
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
          enum:
            - main
            - subagent
        subagent_type:
          type: string
          nullable: true
        status:
          type: string
        task:
          type: string
          nullable: true
        started_at:
          type: string
          format: date-time
        ended_at:
          type: string
          format: date-time
          nullable: true
        children:
          type: array
          items:
            $ref: '#/components/schemas/AgentTreeNode'
    WorkflowSessionResponse:
      type: object
      required:
        - session
        - tree
        - toolTimeline
        - swimLanes
        - events
      properties:
        session:
          $ref: '#/components/schemas/Session'
        tree:
          type: array
          items:
            $ref: '#/components/schemas/AgentTreeNode'
        toolTimeline:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              tool_name:
                type: string
              event_type:
                type: string
              agent_id:
                type: string
                nullable: true
              created_at:
                type: string
                format: date-time
              summary:
                type: string
                nullable: true
        swimLanes:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              type:
                type: string
              subagent_type:
                type: string
                nullable: true
              status:
                type: string
              started_at:
                type: string
                format: date-time
              ended_at:
                type: string
                format: date-time
                nullable: true
              parent_agent_id:
                type: string
                nullable: true
        events:
          type: array
          items:
            $ref: '#/components/schemas/DashboardEvent'
    SettingsInfoResponse:
      type: object
      required:
        - db
        - hooks
        - server
        - transcript_cache
      properties:
        db:
          type: object
          required:
            - path
            - size
            - counts
          properties:
            path:
              type: string
            size:
              type: integer
            counts:
              type: object
              additionalProperties:
                type: integer
        hooks:
          type: object
          required:
            - installed
            - path
            - hooks
          properties:
            installed:
              type: boolean
            path:
              type: string
            hooks:
              type: object
              additionalProperties:
                type: boolean
        server:
          type: object
          required:
            - version
            - uptime
            - node_version
            - platform
            - ws_connections
          properties:
            version:
              type: string
              description: Dashboard release version from package.json
            uptime:
              type: number
            node_version:
              type: string
            platform:
              type: string
            ws_connections:
              type: integer
        transcript_cache:
          type: object
          required:
            - entries
            - paths
          properties:
            entries:
              type: integer
            paths:
              type: array
              items:
                type: string
    ClearDataResponse:
      type: object
      required:
        - ok
        - cleared
      properties:
        ok:
          type: boolean
          enum:
            - true
        cleared:
          type: object
          additionalProperties:
            type: integer
    ReimportResponse:
      type: object
      required:
        - ok
        - imported
        - skipped
        - errors
      properties:
        ok:
          type: boolean
          enum:
            - true
        imported:
          type: integer
        skipped:
          type: integer
        errors:
          type: integer
    ImportGuideResponse:
      type: object
      properties:
        platform:
          type: string
        default_projects_dir:
          type: string
        default_projects_dir_display:
          type: string
        default_projects_dir_exists:
          type: boolean
        default_projects_dir_stats:
          type: object
          properties:
            projects:
              type: integer
            jsonl_files:
              type: integer
        archive_command:
          type: string
        supported_extensions:
          type: array
          items:
            type: string
        max_upload_bytes:
          type: integer
        max_upload_files:
          type: integer
        steps:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              title:
                type: string
              body:
                type: string
    ImportResultResponse:
      type: object
      required:
        - ok
        - source
        - imported
        - skipped
        - errors
      properties:
        ok:
          type: boolean
          enum:
            - true
        source:
          type: string
          enum:
            - default
            - path
            - upload
        path:
          type: string
          nullable: true
        imported:
          type: integer
        backfilled:
          type: integer
        skipped:
          type: integer
        errors:
          type: integer
        sessions_seen:
          type: integer
        files_scanned:
          type: integer
        files_received:
          type: integer
        rejected_files:
          type: array
          items:
            type: string
        entries_extracted:
          type: integer
        entries_skipped:
          type: integer
    ReinstallHooksResponse:
      type: object
      required:
        - ok
        - hooks
      properties:
        ok:
          type: boolean
        hooks:
          type: object
          required:
            - installed
            - path
            - hooks
          properties:
            installed:
              type: boolean
            path:
              type: string
            hooks:
              type: object
              additionalProperties:
                type: boolean
    ResetPricingResponse:
      type: object
      required:
        - ok
        - pricing
      properties:
        ok:
          type: boolean
          enum:
            - true
        pricing:
          type: array
          items:
            $ref: '#/components/schemas/PricingRule'
    ExportResponse:
      type: object
      required:
        - exported_at
        - sessions
        - agents
        - events
        - token_usage
        - model_pricing
      properties:
        format:
          type: string
          description: Bundle format marker (always "ccam-export" for exports from this version).
          example: ccam-export
        version:
          type: integer
          description: Bundle schema version.
          example: 1
        exported_at:
          type: string
          format: date-time
        sessions:
          type: array
          items:
            $ref: '#/components/schemas/Session'
        agents:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        events:
          type: array
          items:
            $ref: '#/components/schemas/DashboardEvent'
        token_usage:
          type: array
          items:
            type: object
            additionalProperties: true
        workflows:
          type: array
          items:
            type: object
            additionalProperties: true
        dashboard_runs:
          type: array
          items:
            type: object
            additionalProperties: true
        alert_rules:
          type: array
          items:
            type: object
            additionalProperties: true
        model_pricing:
          type: array
          items:
            $ref: '#/components/schemas/PricingRule'
    ImportResponse:
      type: object
      required:
        - ok
        - sessions_imported
        - sessions_skipped
        - agents
        - events
        - token_usage
        - workflows
        - dashboard_runs
        - alert_rules
        - model_pricing
        - errors
      description: Per-table counts from restoring an export bundle. Session-scoped counts are rows newly inserted; sessions_skipped counts sessions already present (skipped whole to stay idempotent).
      properties:
        ok:
          type: boolean
        source:
          type: string
          description: Uploaded filename or server-side path read.
        format:
          type: string
          nullable: true
          description: Bundle format marker, or null for a legacy export.
        sessions_imported:
          type: integer
        sessions_skipped:
          type: integer
        agents:
          type: integer
        events:
          type: integer
        token_usage:
          type: integer
        workflows:
          type: integer
        dashboard_runs:
          type: integer
        alert_rules:
          type: integer
        model_pricing:
          type: integer
        errors:
          type: integer
    CleanupRequest:
      type: object
      properties:
        abandon_hours:
          type: number
          minimum: 0
          description: Mark active sessions abandoned if stale for this many hours
        purge_days:
          type: number
          minimum: 0
          description: Delete old completed/error/abandoned sessions older than this many days
    CleanupResponse:
      type: object
      required:
        - ok
        - abandoned
        - purged_sessions
        - purged_events
        - purged_agents
      properties:
        ok:
          type: boolean
          enum:
            - true
        abandoned:
          type: integer
        purged_sessions:
          type: integer
        purged_events:
          type: integer
        purged_agents:
          type: integer
    CcConfigScope:
      type: string
      enum:
        - user
        - project
        - all
      description: Discovery scope. `user` = ~/.claude (or CLAUDE_HOME); `project` = <cwd>/.claude (and project CLAUDE.md); `all` = both, merged. Defaults to `all` when omitted or unrecognized.
      example: all
    CcConfigFrontmatter:
      type: object
      additionalProperties:
        type: string
      description: Parsed YAML frontmatter from the artifact's markdown header (key → string value). Empty object when the file has no parseable frontmatter. Multi-line values are preserved as raw strings; quotes are stripped.
      example:
        name: code-reviewer
        description: Reviews diffs for regressions
        model: sonnet
    CcConfigSkill:
      type: object
      description: 'A discovered skill: a directory containing a SKILL.md file under <root>/skills/<name>/.'
      required:
        - scope
        - name
        - path
        - file
        - size
        - mtime
        - truncated
        - frontmatter
        - preview
      properties:
        scope:
          type: string
          enum:
            - user
            - project
          description: Which root this skill was discovered under.
          example: user
        name:
          type: string
          description: Skill directory name.
          example: code-reviewer
        path:
          type: string
          description: Absolute path to the skill directory.
          example: /Users/son/.claude/skills/code-reviewer
        file:
          type: string
          description: Absolute path to the SKILL.md file.
          example: /Users/son/.claude/skills/code-reviewer/SKILL.md
        size:
          type: integer
          description: Size of SKILL.md in bytes.
          example: 2048
        mtime:
          type: number
          description: Last-modified time of SKILL.md in epoch milliseconds.
          example: 1718900000000
        truncated:
          type: boolean
          description: True when SKILL.md exceeds 256 KiB and the read was truncated to the first 256 KiB.
          example: false
        frontmatter:
          $ref: '#/components/schemas/CcConfigFrontmatter'
        preview:
          type: string
          description: First 320 characters of the markdown body (after frontmatter).
          example: Use this skill to review pull requests for correctness and style...
    CcConfigMdItem:
      type: object
      description: A single-file markdown artifact (subagent, slash command, or output style) found under <root>/<subdir>/<name>.md.
      required:
        - scope
        - name
        - file
        - size
        - mtime
        - truncated
        - frontmatter
        - preview
      properties:
        scope:
          type: string
          enum:
            - user
            - project
          description: Which root this artifact was discovered under.
          example: project
        name:
          type: string
          description: Artifact name (filename with the trailing .md stripped).
          example: backend-reviewer
        file:
          type: string
          description: Absolute path to the .md file.
          example: /repo/.claude/agents/backend-reviewer.md
        size:
          type: integer
          description: File size in bytes.
          example: 1536
        mtime:
          type: number
          description: Last-modified time in epoch milliseconds.
          example: 1718900000000
        truncated:
          type: boolean
          description: True when the file exceeded 256 KiB and was truncated.
          example: false
        frontmatter:
          $ref: '#/components/schemas/CcConfigFrontmatter'
        preview:
          type: string
          description: First 320 characters of the markdown body (after frontmatter).
          example: Review backend route and hook logic for regressions...
    CcConfigPluginContributions:
      type: object
      nullable: true
      description: Counts of artifacts a plugin contributes, plus its parsed .claude-plugin/plugin.json. Null when the plugin's installPath does not exist on disk.
      required:
        - skills
        - agents
        - commands
        - outputStyles
        - hooks
        - pluginJson
      properties:
        skills:
          type: integer
          description: Number of skill directories (containing SKILL.md) under the plugin.
          example: 3
        agents:
          type: integer
          description: Number of .md agent files under the plugin.
          example: 2
        commands:
          type: integer
          description: Number of .md command files under the plugin.
          example: 5
        outputStyles:
          type: integer
          description: Number of .md output-style files under the plugin.
          example: 0
        hooks:
          type: integer
          description: Number of files under the plugin's hooks/ directory.
          example: 1
        pluginJson:
          type: object
          nullable: true
          additionalProperties: true
          description: Parsed contents of .claude-plugin/plugin.json, or null when absent/unparseable.
          example:
            name: superpowers
            version: 1.2.0
    CcConfigPlugin:
      type: object
      description: An installed plugin instance derived from the plugins manifest.
      required:
        - key
        - name
        - marketplace
        - scope
        - version
        - installPath
        - installedAt
        - lastUpdated
        - gitCommitSha
        - installPathExists
        - enabled
        - contributes
      properties:
        key:
          type: string
          description: Manifest key for this plugin, either the bare name or `<name>@<marketplace>`.
          example: superpowers@obra
        name:
          type: string
          description: Plugin name (the portion of `key` before any `@`).
          example: superpowers
        marketplace:
          type: string
          nullable: true
          description: Marketplace portion of `key` (after `@`), or null when unscoped.
          example: obra
        scope:
          type: string
          description: Install scope reported by the manifest instance (defaults to `user`).
          example: user
        version:
          type: string
          nullable: true
          description: Installed plugin version, or null.
          example: 1.2.0
        installPath:
          type: string
          nullable: true
          description: Absolute install path, or null.
          example: /Users/son/.claude/plugins/superpowers
        installedAt:
          type: string
          nullable: true
          description: Install timestamp from the manifest, or null.
          example: '2026-05-01T12:00:00.000Z'
        lastUpdated:
          type: string
          nullable: true
          description: Last-updated timestamp from the manifest, or null.
          example: '2026-06-01T09:30:00.000Z'
        gitCommitSha:
          type: string
          nullable: true
          description: Pinned git commit SHA from the manifest, or null.
          example: a1b2c3d4e5f6
        installPathExists:
          type: boolean
          description: True when installPath resolves to an existing directory on disk.
          example: true
        enabled:
          type: boolean
          nullable: true
          description: 'Tri-state enabled flag from settings.json `enabledPlugins`: true, false, or null when the plugin is not listed there.'
          example: true
        contributes:
          $ref: '#/components/schemas/CcConfigPluginContributions'
    CcConfigPluginsResponse:
      type: object
      description: Plugins manifest summary plus the resolved plugin list (user scope only).
      required:
        - manifestPath
        - manifestExists
        - plugins
      properties:
        manifestPath:
          type: string
          description: Absolute path to plugins/installed_plugins.json under the Claude home.
          example: /Users/son/.claude/plugins/installed_plugins.json
        manifestExists:
          type: boolean
          description: True when the manifest file was found and parsed.
          example: true
        plugins:
          type: array
          description: Plugins sorted by `key`.
          items:
            $ref: '#/components/schemas/CcConfigPlugin'
    CcConfigMcpServer:
      type: object
      description: A summarized MCP server definition. Sensitive details (header values, env values) are NOT returned — only their key NAMES are surfaced.
      required:
        - name
        - source
        - kind
      properties:
        name:
          type: string
          description: Server name (object key in the source config).
          example: github
        source:
          type: string
          description: Where the definition was found, e.g. `~/.claude.json (top-level)`, `~/.claude.json (projects[<root>])`, or `~/.claude/settings.json`.
          example: ~/.claude.json (top-level)
        kind:
          type: string
          enum:
            - http
            - stdio
            - unknown
          description: 'Transport kind inferred from the definition: `http` (has url), `stdio` (has command), or `unknown`.'
          example: stdio
        url:
          type: string
          description: Endpoint URL — present only for `http` servers.
          example: https://mcp.example.com/sse
        headers:
          type: array
          items:
            type: string
          description: Header NAMES (values redacted) — present only for `http` servers. Empty array when no headers.
          example:
            - Authorization
        command:
          type: string
          description: Executable — present only for `stdio` servers.
          example: npx
        args:
          type: array
          items:
            type: string
          description: Command arguments — present only for `stdio` servers.
          example:
            - '-y'
            - '@modelcontextprotocol/server-github'
        envNames:
          type: array
          items:
            type: string
          description: Environment-variable NAMES (values redacted) — present only for `stdio` servers.
          example:
            - GITHUB_TOKEN
    CcConfigMcpResponse:
      type: object
      description: 'MCP servers split by where they apply: globally to the user, or scoped to the resolved project (cwd) in ~/.claude.json projects[<root>].'
      required:
        - user
        - projectScoped
      properties:
        user:
          type: array
          description: User/global MCP servers (top-level ~/.claude.json plus ~/.claude/settings.json).
          items:
            $ref: '#/components/schemas/CcConfigMcpServer'
        projectScoped:
          type: array
          description: MCP servers scoped to the current project root in ~/.claude.json.
          items:
            $ref: '#/components/schemas/CcConfigMcpServer'
    CcConfigHookEntry:
      type: object
      description: A single flattened hook command bound to a matcher.
      required:
        - matcher
        - type
        - command
        - timeout
      properties:
        matcher:
          type: string
          description: Tool/event matcher pattern (defaults to `*`).
          example: Bash
        type:
          type: string
          description: Hook type (defaults to `command`).
          example: command
        command:
          type: string
          nullable: true
          description: Shell command to run, or null.
          example: node ~/.claude/hooks/handler.js
        timeout:
          type: integer
          nullable: true
          description: Per-hook timeout in seconds, or null when unset.
          example: 30
    CcConfigHookSource:
      type: object
      description: Hooks read from one settings file. `hooks` is keyed by event name (e.g. PreToolUse, PostToolUse, Stop); each known-event value is an array of flattened hook entries, while unknown events are passed through verbatim.
      required:
        - scope
        - file
        - exists
        - hooks
      properties:
        scope:
          type: string
          enum:
            - user
            - project
            - project-local
          description: Which settings file this hook block came from.
          example: user
        file:
          type: string
          description: Absolute path to the settings file.
          example: /Users/son/.claude/settings.json
        exists:
          type: boolean
          description: True when the settings file was found and parsed.
          example: true
        hooks:
          type: object
          description: Event name → array of hook entries (for the known event types) or the raw matcher array (for unknown events). Only non-empty events are included.
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/CcConfigHookEntry'
          example:
            PreToolUse:
              - matcher: Bash
                type: command
                command: node guard.js
                timeout: null
    CcConfigSettingsSource:
      type: object
      description: One settings file's redacted contents. Secret-like keys (matching /token|secret|password|api[_-]?key|auth/i with string values) are replaced with the literal string `<redacted>`.
      required:
        - scope
        - file
        - exists
      properties:
        scope:
          type: string
          enum:
            - user
            - project
            - project-local
          description: Which settings file this entry represents.
          example: user
        file:
          type: string
          description: Absolute path to the settings file.
          example: /Users/son/.claude/settings.json
        exists:
          type: boolean
          description: True when the file was found and parsed. When false, `data`/`raw_size` are omitted.
          example: true
        data:
          type: object
          additionalProperties: true
          description: Parsed, secret-redacted settings JSON. Present only when `exists` is true.
          example:
            model: sonnet
            apiKey: <redacted>
            statusLine:
              type: command
        raw_size:
          type: integer
          description: Byte length of the raw file contents. Present only when `exists` is true.
          example: 412
    CcConfigMemoryItem:
      type: object
      description: 'A memory artifact: either a primary CLAUDE.md (scope `user`/`project`) or a per-project file-based auto-memory markdown file (scope `auto-memory`). Auto-memory items additionally carry `project`, `name`, `isIndex`, and parsed `frontmatter`.'
      required:
        - scope
        - file
        - size
        - mtime
        - truncated
        - preview
      properties:
        scope:
          type: string
          enum:
            - user
            - project
            - auto-memory
          description: '`user`/`project` = the two primary CLAUDE.md files; `auto-memory` = a *.md file under ~/.claude/projects/<slug>/memory/.'
          example: auto-memory
        project:
          type: string
          description: 'Auto-memory only: the ~/.claude/projects/<slug> directory name the file belongs to.'
          example: '-Users-son-repo'
        name:
          type: string
          description: 'Auto-memory only: the markdown filename.'
          example: MEMORY.md
        isIndex:
          type: boolean
          description: 'Auto-memory only: true for index/manifest files (MEMORY.md, INDEX-*.md), which sort first.'
          example: true
        file:
          type: string
          description: Absolute path to the memory file.
          example: /Users/son/.claude/projects/-Users-son-repo/memory/MEMORY.md
        size:
          type: integer
          description: File size in bytes.
          example: 980
        mtime:
          type: number
          description: Last-modified time in epoch milliseconds.
          example: 1718900000000
        truncated:
          type: boolean
          description: True when the file exceeded 256 KiB and was truncated.
          example: false
        frontmatter:
          $ref: '#/components/schemas/CcConfigFrontmatter'
        preview:
          type: string
          description: First 480 characters of the body (frontmatter stripped for auto-memory files; raw head for CLAUDE.md).
          example: '## Persistent facts\n- Never run destructive ops without confirmation...'
    CcConfigMarketplace:
      type: object
      description: A known plugin marketplace and (best-effort) its parsed manifest summary.
      required:
        - name
        - source
        - installLocation
        - lastUpdated
        - pluginCount
        - marketplaceName
        - marketplaceDescription
        - marketplaceOwner
      properties:
        name:
          type: string
          description: Marketplace key/name.
          example: obra
        source:
          type: object
          nullable: true
          additionalProperties: true
          description: Source descriptor object (e.g. git/github source), or null.
          example:
            source: github
            repo: obra/superpowers-marketplace
        installLocation:
          type: string
          nullable: true
          description: Absolute path where the marketplace is checked out, or null.
          example: /Users/son/.claude/plugins/marketplaces/obra
        lastUpdated:
          type: string
          nullable: true
          description: Last-updated timestamp, or null.
          example: '2026-06-10T00:00:00.000Z'
        pluginCount:
          type: integer
          nullable: true
          description: Number of plugins in the marketplace manifest, or null when no manifest was readable.
          example: 12
        marketplaceName:
          type: string
          nullable: true
          description: '`name` from the marketplace manifest, or null.'
          example: Superpowers
        marketplaceDescription:
          type: string
          nullable: true
          description: '`description` from the marketplace manifest, or null.'
          example: Curated agent skills
        marketplaceOwner:
          description: '`owner` from the marketplace manifest (object or string), or null.'
          nullable: true
          oneOf:
            - type: object
              additionalProperties: true
            - type: string
          example:
            name: obra
    CcConfigMarketplacesResponse:
      type: object
      description: Known-marketplaces manifest summary plus the resolved marketplace list.
      required:
        - knownPath
        - knownExists
        - items
      properties:
        knownPath:
          type: string
          description: Absolute path to plugins/known_marketplaces.json under the Claude home.
          example: /Users/son/.claude/plugins/known_marketplaces.json
        knownExists:
          type: boolean
          description: True when the known-marketplaces file was found and parsed.
          example: true
        items:
          type: array
          description: Marketplaces sorted by name.
          items:
            $ref: '#/components/schemas/CcConfigMarketplace'
    CcConfigKeybinding:
      type: object
      description: A single key → action binding.
      required:
        - key
        - action
      properties:
        key:
          type: string
          description: Key chord.
          example: ctrl+s
        action:
          type: string
          description: Action name (stringified).
          example: submit
    CcConfigKeybindingGroup:
      type: object
      description: A context-scoped group of keybindings.
      required:
        - context
        - bindings
      properties:
        context:
          type: string
          description: The context the bindings apply in (empty string when global).
          example: editor
        bindings:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigKeybinding'
    CcConfigKeybindingsResponse:
      type: object
      description: 'Parsed ~/.claude/keybindings.json. When the file is absent, only `{ file, exists: false }` is returned.'
      required:
        - file
        - exists
      properties:
        file:
          type: string
          description: Absolute path to keybindings.json.
          example: /Users/son/.claude/keybindings.json
        exists:
          type: boolean
          description: True when the file was found and parsed.
          example: true
        schema:
          type: string
          nullable: true
          description: '`$schema` value from the file, or null. Present only when `exists` is true.'
          example: https://json.schemastore.org/claude-keybindings
        docs:
          type: string
          nullable: true
          description: '`$docs` value from the file, or null. Present only when `exists` is true.'
          example: https://docs.claude.com/keybindings
        groups:
          type: array
          description: Binding groups. Present only when `exists` is true.
          items:
            $ref: '#/components/schemas/CcConfigKeybindingGroup'
    CcConfigStatuslineScript:
      type: object
      description: A statusline script (statusline.py or statusline-command.sh) discovered under the Claude home.
      required:
        - file
        - size
        - mtime
        - truncated
        - preview
      properties:
        file:
          type: string
          description: Absolute path to the script.
          example: /Users/son/.claude/statusline.py
        size:
          type: integer
          description: File size in bytes.
          example: 1280
        mtime:
          type: number
          description: Last-modified time in epoch milliseconds.
          example: 1718900000000
        truncated:
          type: boolean
          description: True when the file exceeded 256 KiB and was truncated.
          example: false
        preview:
          type: string
          description: First 4000 characters of the script body.
          example: '#!/usr/bin/env python3\nimport json, sys\n...'
    CcConfigStatuslineResponse:
      type: object
      description: The statusLine config block from user settings.json plus any statusline scripts.
      required:
        - config
        - scripts
      properties:
        config:
          type: object
          nullable: true
          additionalProperties: true
          description: The `statusLine` object from ~/.claude/settings.json, or null when unset.
          example:
            type: command
            command: python3 ~/.claude/statusline.py
        scripts:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigStatuslineScript'
    CcConfigHookScript:
      type: object
      description: A file in the ~/.claude/hooks/ directory.
      required:
        - name
        - file
        - size
        - mtime
      properties:
        name:
          type: string
          description: File name.
          example: post-tool-use.js
        file:
          type: string
          description: Absolute path to the script.
          example: /Users/son/.claude/hooks/post-tool-use.js
        size:
          type: integer
          description: File size in bytes.
          example: 640
        mtime:
          type: number
          description: Last-modified time in epoch milliseconds.
          example: 1718900000000
    CcConfigHookScriptsResponse:
      type: object
      description: Listing of the ~/.claude/hooks/ directory (files only, sorted by name).
      required:
        - dir
        - items
      properties:
        dir:
          type: string
          description: Absolute path to the hooks directory.
          example: /Users/son/.claude/hooks
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigHookScript'
    CcConfigScopeCount:
      type: object
      description: Per-scope counts (user vs project).
      required:
        - user
        - project
      properties:
        user:
          type: integer
          description: Count under the user root.
          example: 12
        project:
          type: integer
          description: Count under the project root.
          example: 3
    CcConfigOverviewResponse:
      type: object
      description: 'Top-level summary for the Claude Config Explorer landing page: resolved roots plus aggregate counts across every surface.'
      required:
        - roots
        - counts
      properties:
        roots:
          type: object
          description: The four filesystem roots the explorer reads from.
          required:
            - claudeHome
            - projectClaudeDir
            - projectRoot
            - claudeJson
          properties:
            claudeHome:
              type: string
              description: Claude home (CLAUDE_HOME or ~/.claude).
              example: /Users/son/.claude
            projectClaudeDir:
              type: string
              description: Project .claude directory under the resolved cwd.
              example: /repo/.claude
            projectRoot:
              type: string
              description: Resolved project root (the cwd).
              example: /repo
            claudeJson:
              type: string
              description: Path to ~/.claude.json (resolved from $HOME, not CLAUDE_HOME).
              example: /Users/son/.claude.json
        counts:
          type: object
          description: Aggregate counts by surface.
          required:
            - skills
            - agents
            - commands
            - outputStyles
            - plugins
            - pluginsEnabled
            - pluginsDisabled
            - marketplaces
            - keybindings
            - mcpServers
            - hooks
            - memory
            - settingsFiles
          properties:
            skills:
              $ref: '#/components/schemas/CcConfigScopeCount'
            agents:
              $ref: '#/components/schemas/CcConfigScopeCount'
            commands:
              $ref: '#/components/schemas/CcConfigScopeCount'
            outputStyles:
              $ref: '#/components/schemas/CcConfigScopeCount'
            plugins:
              type: integer
              description: Total plugins.
              example: 8
            pluginsEnabled:
              type: integer
              description: Plugins explicitly enabled in settings.
              example: 6
            pluginsDisabled:
              type: integer
              description: Plugins explicitly disabled in settings.
              example: 1
            marketplaces:
              type: integer
              description: Known marketplaces.
              example: 2
            keybindings:
              type: integer
              description: Total individual key bindings across all groups.
              example: 24
            mcpServers:
              type: object
              description: MCP server counts by scope.
              required:
                - user
                - project
              properties:
                user:
                  type: integer
                  example: 3
                project:
                  type: integer
                  example: 1
            hooks:
              type: object
              description: Total hook entries per settings scope.
              required:
                - user
                - project
                - project-local
              properties:
                user:
                  type: integer
                  example: 2
                project:
                  type: integer
                  example: 0
                project-local:
                  type: integer
                  example: 1
            memory:
              type: integer
              description: Total memory items.
              example: 5
            settingsFiles:
              type: integer
              description: Number of settings files that exist (of the three scopes).
              example: 2
    CcConfigSkillsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigSkill'
    CcConfigAgentsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigMdItem'
    CcConfigCommandsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigMdItem'
    CcConfigOutputStylesResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigMdItem'
    CcConfigHooksResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigHookSource'
    CcConfigSettingsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigSettingsSource'
    CcConfigMemoryResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CcConfigMemoryItem'
    CcConfigFileReadResponse:
      type: object
      description: Body of a single file resolved under an allowed root. Returned only on success; failures return an ErrorResponse with HTTP 400.
      required:
        - ok
        - file
        - truncated
        - size
        - text
        - mtime
      properties:
        ok:
          type: boolean
          enum:
            - true
          description: Always true on success.
          example: true
        file:
          type: string
          description: Absolute, resolved path that was read.
          example: /Users/son/.claude/skills/code-reviewer/SKILL.md
        truncated:
          type: boolean
          description: True when the file exceeded 256 KiB and only the first 256 KiB are in `text`.
          example: false
        size:
          type: integer
          description: Full file size in bytes.
          example: 2048
        text:
          type: string
          description: File contents (truncated to 256 KiB).
          example: '---\nname: code-reviewer\n---\n# Code Reviewer\n...'
        mtime:
          type: number
          description: Last-modified time in epoch milliseconds.
          example: 1718900000000
    CcConfigFileWriteRequest:
      type: object
      description: Create-or-overwrite request for a low-risk text artifact. `name` is required for every type except `memory` (which targets CLAUDE.md). `project` is required for `auto-memory`. `scope` is ignored for `auto-memory` (always under the Claude home).
      required:
        - scope
        - type
        - content
      properties:
        scope:
          type: string
          enum:
            - user
            - project
          description: 'Target root: `user` (Claude home) or `project` (<cwd>/.claude, or project root for memory). Ignored for `auto-memory`.'
          example: user
        type:
          type: string
          enum:
            - skills
            - agents
            - commands
            - output-styles
            - memory
            - auto-memory
          description: Artifact type. `skills` writes <root>/skills/<name>/SKILL.md; `agents`/`commands`/`output-styles` write <root>/<type>/<name>.md; `memory` writes the scope's CLAUDE.md; `auto-memory` writes ~/.claude/projects/<project>/memory/<name>.
          example: agents
        name:
          type: string
          description: Artifact name. Required for all types except `memory`. For skills/agents/commands/output-styles must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/. For auto-memory must be a flat *.md filename (/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/i, no `..`).
          example: my-reviewer
        content:
          type: string
          description: Full file contents to write. Must be a string and ≤ 256 KiB (UTF-8 bytes).
          example: '---\nname: my-reviewer\n---\n# My Reviewer\nReview the diff...'
        project:
          type: string
          description: 'Required for `auto-memory`: the ~/.claude/projects/<slug> directory name. Validated against /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,255}$/ and rejected if it contains `..`.'
          example: '-Users-son-repo'
    CcConfigFileWriteResponse:
      type: object
      description: Result of a successful create-or-overwrite.
      required:
        - ok
        - file
        - target
        - backupPath
        - created
      properties:
        ok:
          type: boolean
          enum:
            - true
          example: true
        file:
          type: string
          description: Absolute path to the .md file that was written.
          example: /Users/son/.claude/agents/my-reviewer.md
        target:
          type: string
          description: Absolute path to the artifact target (the skill directory for skills; same as `file` otherwise).
          example: /Users/son/.claude/agents/my-reviewer.md
        backupPath:
          type: string
          nullable: true
          description: Absolute path to the timestamped backup created before overwriting, or null when this was a fresh create (nothing to back up).
          example: /Users/son/.claude/cc-config-backups/agents/my-reviewer.md.2026-06-25T12-00-00.000Z.bak
        created:
          type: boolean
          description: True when the artifact did not exist before this write.
          example: true
    CcConfigFileDeleteRequest:
      type: object
      description: Delete request for a low-risk text artifact. Same (scope, type, name, project) semantics as the write request, without `content`.
      required:
        - scope
        - type
      properties:
        scope:
          type: string
          enum:
            - user
            - project
          description: Target root. Ignored for `auto-memory`.
          example: user
        type:
          type: string
          enum:
            - skills
            - agents
            - commands
            - output-styles
            - memory
            - auto-memory
          description: Artifact type to delete.
          example: agents
        name:
          type: string
          description: Artifact name. Required for all types except `memory`.
          example: my-reviewer
        project:
          type: string
          description: 'Required for `auto-memory`: the projects/<slug> directory name.'
          example: '-Users-son-repo'
    CcConfigFileDeleteResponse:
      type: object
      description: Result of a successful delete (a mandatory backup is created first).
      required:
        - ok
        - file
        - target
        - backupPath
      properties:
        ok:
          type: boolean
          enum:
            - true
          example: true
        file:
          type: string
          description: Absolute path to the .md file inside the deleted target.
          example: /Users/son/.claude/agents/my-reviewer.md
        target:
          type: string
          description: Absolute path to the deleted artifact (skill directory for skills).
          example: /Users/son/.claude/agents/my-reviewer.md
        backupPath:
          type: string
          nullable: true
          description: Absolute path to the backup taken before deletion.
          example: /Users/son/.claude/cc-config-backups/agents/my-reviewer.md.2026-06-25T12-00-00.000Z.bak
    CcConfigBackup:
      type: object
      description: A single backup entry under a cc-config-backups directory.
      required:
        - scope
        - type
        - name
        - backupPath
        - isDir
        - mtime
        - size
      properties:
        scope:
          type: string
          enum:
            - user
            - project
            - auto-memory
          description: Scope the backup belongs to. `auto-memory` backups are per-project.
          example: user
        project:
          type: string
          description: 'Auto-memory only: the projects/<slug> directory name.'
          example: '-Users-son-repo'
        type:
          type: string
          enum:
            - skills
            - agents
            - commands
            - output-styles
            - memory
            - auto-memory
          description: Artifact type the backup was taken from.
          example: agents
        name:
          type: string
          description: Backup file/directory name (includes the timestamp and .bak suffix).
          example: my-reviewer.md.2026-06-25T12-00-00.000Z.bak
        backupPath:
          type: string
          description: Absolute path to the backup.
          example: /Users/son/.claude/cc-config-backups/agents/my-reviewer.md.2026-06-25T12-00-00.000Z.bak
        isDir:
          type: boolean
          description: True when the backup is a directory (skill backups).
          example: false
        mtime:
          type: number
          description: Backup modification time in epoch milliseconds (results sort newest first).
          example: 1718900000000
        size:
          type: integer
          nullable: true
          description: Backup size in bytes, or null for directory backups.
          example: 2048
    CcConfigBackupsResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          description: Backups across the requested scope/type buckets, sorted newest first.
          items:
            $ref: '#/components/schemas/CcConfigBackup'
    RunHandle:
      type: object
      description: Live, in-memory view of a spawned `claude` subprocess. Returned by GET /api/run (in `items`), POST /api/run (201), and GET /api/run/{id}. Backed by the spawner's handle map, which reaps each handle 5 minutes after the process exits — after that the run is only visible via GET /api/run/history.
      required:
        - id
        - pid
        - mode
        - cwd
        - model
        - permissionMode
        - effort
        - prompt
        - argv
        - resumeSessionId
        - status
        - startedAt
        - endedAt
        - exitCode
        - signal
        - error
        - sessionId
        - envelopeCount
        - stdoutTail
        - stderrTail
      properties:
        id:
          type: string
          format: uuid
          description: Server-generated run id (UUID v4). Distinct from the Claude session id.
          example: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
        pid:
          type: integer
          nullable: true
          description: OS process id of the spawned `claude` child, or null if the spawn failed.
          example: 48213
        mode:
          type: string
          enum:
            - headless
            - conversation
          description: '`headless`: single-shot — prompt is passed via argv `-p`, stdin is closed, the process exits after one turn. `conversation`: multi-turn — stdin stays open (`--input-format stream-json`) and follow-up turns are delivered via POST /api/run/{id}/message.'
          example: conversation
        cwd:
          type: string
          description: Absolute working directory the child was spawned in (sanitised at request time — must be an existing absolute directory).
          example: /Users/dev/projects/my-app
        model:
          type: string
          nullable: true
          description: Model alias/id passed via `--model`, or null to inherit the user's default.
          example: claude-opus-4-8
        permissionMode:
          type: string
          enum:
            - acceptEdits
            - default
            - plan
            - bypassPermissions
          description: Permission mode passed via `--permission-mode`. Defaults to `acceptEdits` when omitted or invalid.
          example: acceptEdits
        effort:
          type: string
          enum:
            - low
            - medium
            - high
            - xhigh
            - max
          nullable: true
          description: Thinking-effort level passed via `--effort` (higher = more reasoning tokens before the assistant turn). Null inherits the model default.
          example: high
        prompt:
          type: string
          description: The initial user prompt. May be empty only when resuming a conversation (the child idles on the resumed transcript until a follow-up arrives).
          example: Refactor the auth module and add tests.
        argv:
          type: array
          items:
            type: string
          description: Exact argv vector passed to the `claude` binary (always includes `--output-format stream-json --verbose --include-partial-messages` plus the resolved flags).
          example:
            - '--output-format'
            - stream-json
            - '--verbose'
            - '--include-partial-messages'
            - '--permission-mode'
            - acceptEdits
            - '--input-format'
            - stream-json
            - '--model'
            - claude-opus-4-8
        resumeSessionId:
          type: string
          nullable: true
          description: Claude session id this run resumed (`--resume`), or null for a fresh run. Conversation mode only.
          example: null
        status:
          type: string
          enum:
            - spawning
            - running
            - completed
            - error
            - killed
          description: Lifecycle status. `spawning` -> `running` on the first parsed output envelope; terminal states are `completed` (exit 0), `error` (non-zero exit or spawn error), or `killed` (terminated via DELETE). Each transition is broadcast as a `run_status` WebSocket message.
          example: running
        startedAt:
          type: integer
          format: int64
          description: Spawn time in epoch milliseconds.
          example: 1718000000000
        endedAt:
          type: integer
          format: int64
          nullable: true
          description: Exit time in epoch milliseconds, or null while still live.
          example: null
        exitCode:
          type: integer
          nullable: true
          description: Process exit code once the child exits; null while running or if killed by signal.
          example: null
        signal:
          type: string
          nullable: true
          description: Terminating signal name (e.g. `SIGTERM`/`SIGKILL`) if the child was killed by a signal; null otherwise.
          example: null
        error:
          type: string
          nullable: true
          description: Spawn/runtime error message if the child failed to start or errored; null otherwise.
          example: null
        sessionId:
          type: string
          nullable: true
          description: Claude session id, captured from the `system/init` stream envelope (optimistically pre-seeded with resumeSessionId). Used to deep-link to /api/sessions/{id}. Null until the init envelope arrives.
          example: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
        envelopeCount:
          type: integer
          minimum: 0
          description: Total stream-json envelopes parsed for this run so far (monotonic; not capped by the in-memory replay buffer).
          example: 12
        stdoutTail:
          type: string
          description: Trailing slice of the child's raw stdout (capped at ~4 KiB) for quick diagnostics.
          example: |
            {"type":"result","subtype":"success",...}
        stderrTail:
          type: string
          description: Trailing slice of the child's raw stderr (capped at ~4 KiB); also captures parse errors.
          example: ''
    RunHandleWithEnvelopes:
      allOf:
        - $ref: '#/components/schemas/RunHandle'
        - type: object
          description: RunHandle plus the in-memory envelope replay buffer, returned by GET /api/run/{id}?envelopes=1.
          required:
            - envelopes
          properties:
            envelopes:
              type: array
              description: The most recent stream-json envelopes (capped at 500 per handle) passed through verbatim from the `claude` child — system/init, assistant text + tool_use, user tool_result, result/success, partial stream_event deltas, etc. Lets a late-attaching client replay what it missed. Full transcript is always available via /api/sessions/{id}.
              items:
                type: object
                additionalProperties: true
    RunListResponse:
      type: object
      description: All currently-tracked run handles (newest first) plus concurrency telemetry.
      required:
        - items
        - maxConcurrent
        - activeCount
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RunHandle'
          description: Live + recently-exited handles, sorted by startedAt descending. Handles are reaped 5 minutes after exit.
        maxConcurrent:
          type: integer
          description: Concurrency cap (env `RUN_MAX_CONCURRENT`, default effectively uncapped at 10000). Spawns over this throw ECONCURRENCY (HTTP 429).
          example: 10000
        activeCount:
          type: integer
          minimum: 0
          description: Number of handles currently in `spawning` or `running` status.
          example: 2
    RunHistoryItem:
      type: object
      description: A persisted run record from the `dashboard_runs` sqlite table. Survives the 5-minute in-memory reap so past runs remain visible and resumable. Fields are snake_case (DB column names) and timestamps are ISO-8601 strings, distinct from the camelCase epoch-ms fields on RunHandle.
      required:
        - id
        - session_id
        - mode
        - cwd
        - model
        - permission_mode
        - effort
        - resume_session_id
        - prompt_preview
        - status
        - exit_code
        - started_at
        - ended_at
        - isLive
        - lane_id
      properties:
        id:
          type: string
          format: uuid
          description: Run id (matches RunHandle.id).
          example: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
        session_id:
          type: string
          nullable: true
          description: Claude session id once known (from system/init), else null.
          example: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
        mode:
          type: string
          enum:
            - headless
            - conversation
          description: Run mode at spawn time.
          example: conversation
        cwd:
          type: string
          description: Absolute working directory the run was spawned in.
          example: /Users/dev/projects/my-app
        model:
          type: string
          nullable: true
          description: Model used, or null for the default.
          example: claude-opus-4-8
        permission_mode:
          type: string
          nullable: true
          enum:
            - acceptEdits
            - default
            - plan
            - bypassPermissions
          description: Permission mode recorded at spawn time.
          example: acceptEdits
        effort:
          type: string
          nullable: true
          enum:
            - low
            - medium
            - high
            - xhigh
            - max
          description: Effort level recorded at spawn time, or null.
          example: high
        resume_session_id:
          type: string
          nullable: true
          description: Session id this run resumed, or null.
          example: null
        prompt_preview:
          type: string
          nullable: true
          description: First 500 characters of the initial prompt (truncated preview for the history list), or null.
          example: Refactor the auth module and add tests.
        status:
          type: string
          enum:
            - spawning
            - running
            - completed
            - error
            - killed
            - abandoned
          description: Persisted status. `abandoned` is set on server boot for rows left `running`/`spawning` by a previous process (their in-memory handles were wiped by the restart) so the UI does not show them as live.
          example: completed
        exit_code:
          type: integer
          nullable: true
          description: Recorded process exit code, or null.
          example: 0
        started_at:
          type: string
          format: date-time
          description: Spawn time (ISO-8601).
          example: '2026-06-25T17:00:00.000Z'
        ended_at:
          type: string
          format: date-time
          nullable: true
          description: Exit time (ISO-8601), or null if still running / never recorded.
          example: '2026-06-25T17:01:42.500Z'
        isLive:
          type: boolean
          description: 'Computed at request time: true when a live in-memory handle for this id is currently `running` or `spawning`. Lets the UI mark which history rows are re-attachable.'
          example: false
        lane_id:
          type: integer
          nullable: true
          description: Lane this run was started through (POST /api/lanes/{id}/start), or null for a run spawned straight from POST /api/run. Filterable via ?laneId=.
          example: null
    RunHistoryResponse:
      type: object
      description: Persisted run history, most recent first. Empty `items` when the persistence DB is unavailable.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RunHistoryItem'
    RunCwdSuggestion:
      type: object
      description: A suggested working directory for spawning a run. Only directories that still exist on disk are returned.
      required:
        - kind
        - path
        - label
      properties:
        kind:
          type: string
          enum:
            - dashboard
            - home
            - recent
          description: '`dashboard`: the dashboard server''s own cwd (always first). `home`: $HOME. `recent`: distinct cwds Claude Code has been used in, sourced from the sessions table.'
          example: recent
        path:
          type: string
          description: Absolute, resolved directory path.
          example: /Users/dev/projects/my-app
        label:
          type: string
          description: Human-friendly label (defaults to the directory basename).
          example: my-app
    RunCwdsResponse:
      type: object
      description: Working-directory suggestions for the Run launcher.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RunCwdSuggestion'
    RunFilesResponse:
      type: object
      description: File-path autocomplete results for the prompt editor's `@` references — up to 40 paths relative to the given cwd, shortest first.
      required:
        - items
      properties:
        items:
          type: array
          items:
            type: string
          description: Paths relative to the resolved cwd. Dotfiles (except .env/.gitignore) and heavy build dirs (node_modules, .git, dist, build, etc.) are skipped.
          example:
            - package.json
            - src/index.ts
            - server/routes/run.js
    RunBinaryResponse:
      type: object
      description: Whether the `claude` binary is resolvable on PATH (probed via which/where; the binary is not invoked). Lets the UI warn before the user clicks Run.
      required:
        - found
        - path
      properties:
        found:
          type: boolean
          description: True when `claude` resolves on PATH.
          example: true
        path:
          type: string
          nullable: true
          description: Absolute path to the resolved `claude` binary, or null when not found.
          example: /usr/local/bin/claude
    RunSpawnRequest:
      type: object
      description: 'Request body for spawning a run. SIDE EFFECT: a successful call spawns a real `claude` process in `cwd`, begins streaming `run_stream`/`run_status` WebSocket messages, and persists a row to `dashboard_runs`.'
      required:
        - prompt
      properties:
        prompt:
          type: string
          description: Initial user prompt. Required and non-empty UNLESS mode is `conversation` AND resumeSessionId is set (a resumed conversation may start empty and idle on stdin).
          example: Refactor the auth module and add tests.
        mode:
          type: string
          enum:
            - headless
            - conversation
          default: conversation
          description: '`headless` for a single-shot run; `conversation` for a multi-turn run that accepts follow-ups via POST /api/run/{id}/message. Any value other than `headless` is treated as `conversation`.'
          example: conversation
        cwd:
          type: string
          description: Absolute working directory for the child. Must exist as a directory at request time. Omitted/empty defaults to the dashboard server's cwd. Non-absolute or missing paths are rejected with EBADCWD.
          example: /Users/dev/projects/my-app
        model:
          type: string
          description: Optional model alias/id passed via `--model`. Omit to inherit the user's default.
          example: claude-opus-4-8
        resumeSessionId:
          type: string
          description: Optional Claude session id to resume via `--resume`. Must match /^[A-Za-z0-9-]{8,}$/ and requires conversation mode, else EBADSESSION / EBADMODE.
          example: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
        effort:
          type: string
          enum:
            - low
            - medium
            - high
            - xhigh
            - max
          description: Optional thinking-effort level passed via `--effort`. An invalid value is rejected with EBADEFFORT.
          example: high
        permissionMode:
          type: string
          enum:
            - acceptEdits
            - default
            - plan
            - bypassPermissions
          default: acceptEdits
          description: Permission mode passed via `--permission-mode`. Unknown values silently fall back to `acceptEdits`.
          example: acceptEdits
    RunSpawnConcurrencyResponse:
      type: object
      description: HTTP 429 body when the concurrency cap is reached (ECONCURRENCY). In addition to the standard `error` envelope, this response carries a top-level `running` array listing the runs currently occupying the cap.
      required:
        - error
        - running
      properties:
        error:
          $ref: '#/components/schemas/ErrorObject'
        running:
          type: array
          description: The live runs (status running/spawning) that are holding the concurrency slots.
          items:
            type: object
            required:
              - id
              - pid
              - startedAt
              - mode
            properties:
              id:
                type: string
                format: uuid
                example: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
              pid:
                type: integer
                nullable: true
                example: 48213
              startedAt:
                type: integer
                format: int64
                example: 1718000000000
              mode:
                type: string
                enum:
                  - headless
                  - conversation
                example: conversation
    RunMessageRequest:
      type: object
      description: 'Request body for sending a follow-up turn into a running conversation. SIDE EFFECT: writes a stream-json user envelope to the child''s stdin and broadcasts a `run_input_ack` WebSocket message.'
      required:
        - text
      properties:
        text:
          type: string
          description: The follow-up user message. Required and non-empty (else EBADINPUT).
          example: Also update the README to document the new flags.
    RunMessageResponse:
      type: object
      description: Acknowledgement that the follow-up turn was written to the child's stdin.
      required:
        - messageId
      properties:
        messageId:
          type: string
          format: uuid
          description: Server-generated id for this input turn; echoed in the `run_input_ack` WebSocket message.
          example: 9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f
    RunKillResponse:
      type: object
      description: Acknowledgement that a kill was issued (or that the run had already terminated).
      required:
        - ok
      properties:
        ok:
          type: boolean
          enum:
            - true
          example: true
    LaneWorktreeCreateRequest:
      type: object
      required:
        - sourceRepo
      properties:
        sourceRepo:
          type: string
          description: Existing absolute path to the source git repository.
          example: /Users/me/src/project
        title:
          type: string
          description: Human-readable lane title.
          example: Criteria form
        base:
          type: string
          description: Preferred base branch. Defaults to the LANE_BASE_BRANCH env var, or `main` when that is also unset.
          example: main
        slug:
          type: string
          description: Optional branch/directory slug override.
          example: criteria-form
    LaneEnsureRequest:
      type: object
      required:
        - cwd
      properties:
        cwd:
          type: string
          description: Absolute working directory to find or adopt a lane for. A lane whose own cwd is this path, or the closest path-boundary parent of it, is returned as-is.
          example: /Users/me/src/project/packages/app
        title:
          type: string
          description: Title for the lane if one has to be created; ignored when one already exists.
          example: App package
    LaneDestructiveActionRequest:
      type: object
      required:
        - confirm
        - expect
      properties:
        confirm:
          type: boolean
          enum:
            - true
        force:
          type: boolean
          description: Required by reset/remove when unpushed commits exist.
        expect:
          type: object
          description: 'Required complete facts returned by the preceding preflight: head, dirty, untracked, unpushed for reset/remove; sessions, events, tokenRows for purge. Differences return 409 ESTALE without destructive work.'
          additionalProperties: true
    LaneStageDetectionFields:
      type: object
      description: Fields the server's stage-detection heuristic (server/lib/stage-detect.js) adds to every lane returned by GET /api/lanes and GET /api/lanes/:id. An inferred stage is never evidence and never renders as done — see docs/LANES.md#stage-detection.
      properties:
        detected_stage:
          type: string
          nullable: true
          description: Stage id inferred from ingested tool events, or null if no signal has been seen. Independent of the agent's own declared `stage`.
        detected_signal:
          type: string
          nullable: true
          description: The tool-event signal that produced detected_stage, capped at 120 characters; null when detected_stage is null.
        detected:
          type: boolean
          description: Present on each entry of pipeline_nodes. True for the inferred node and any node before it that carries no declared record; decorates that node's state without ever upgrading it to done.
    PushVapidKeyResponse:
      type: object
      required:
        - publicKey
      description: The server's VAPID public key. The browser passes this base64url-encoded key to `PushManager.subscribe({ applicationServerKey })` so the push service will accept deliveries signed by this server's private key.
      properties:
        publicKey:
          type: string
          description: Base64url-encoded VAPID (P-256 ECDSA) public application server key. Generated once and persisted alongside the SQLite DB so the web app and native apps share one key pair.
          example: BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8
    PushSubscriptionKeys:
      type: object
      required:
        - p256dh
        - auth
      description: Client encryption keys produced by the browser's PushManager subscription. Both are required to encrypt Web Push payloads for the endpoint.
      properties:
        p256dh:
          type: string
          description: Base64url-encoded P-256 ECDH public key from the browser subscription (`subscription.getKey('p256dh')`). Stored verbatim in the `push_subscriptions` table.
          example: BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM
        auth:
          type: string
          description: Base64url-encoded auth secret from the browser subscription (`subscription.getKey('auth')`). Stored verbatim in the `push_subscriptions` table.
          example: tBHItJI5svbpez7KI4CCXg
    PushSubscribeRequest:
      type: object
      required:
        - endpoint
        - keys
      description: A browser PushSubscription serialized for storage. Persisted via `INSERT OR REPLACE` keyed on `endpoint`, so re-subscribing the same endpoint is idempotent (it overwrites the stored keys rather than duplicating the row).
      properties:
        endpoint:
          type: string
          format: uri
          description: The push service delivery URL from `subscription.endpoint`. Acts as the primary key in `push_subscriptions`; sending later POSTs encrypted payloads here. Subscriptions that return HTTP 410 (Gone) during `/send` are pruned automatically.
          example: https://fcm.googleapis.com/fcm/send/dGhpcy1pcy1hLWZha2UtZW5kcG9pbnQ
        keys:
          $ref: '#/components/schemas/PushSubscriptionKeys'
    PushSubscribeResponse:
      type: object
      required:
        - ok
      description: Confirmation that the subscription was stored (or overwritten).
      properties:
        ok:
          type: boolean
          enum:
            - true
          description: Always `true` on success.
          example: true
    PushUnsubscribeRequest:
      type: object
      required:
        - endpoint
      description: 'Identifies the subscription to delete by its push-service endpoint. NOTE: the endpoint is supplied in the request BODY (DELETE with a JSON body), not as a query parameter.'
      properties:
        endpoint:
          type: string
          format: uri
          description: 'The `endpoint` of the subscription to remove from `push_subscriptions`. Deletion is idempotent — removing an endpoint that is not stored still returns `{ ok: true }`.'
          example: https://fcm.googleapis.com/fcm/send/dGhpcy1pcy1hLWZha2UtZW5kcG9pbnQ
    PushOkResponse:
      type: object
      required:
        - ok
      description: Generic success acknowledgement returned by subscribe/unsubscribe.
      properties:
        ok:
          type: boolean
          enum:
            - true
          description: Always `true` on success.
          example: true
    PushSendRequest:
      type: object
      required:
        - title
        - body
      description: Notification content to broadcast. Both fields are mandatory; a missing title or body yields a 400. The same title/body is delivered to every reachable surface (native Electron notification + all stored Web Push subscriptions).
      properties:
        title:
          type: string
          description: Notification title line.
          example: Session completed
        body:
          type: string
          description: Notification body text.
          example: Your Claude Code session finished with 3 subagents.
    PushSendResponse:
      type: object
      required:
        - ok
        - native
        - pushed
        - failed
      description: Reports which delivery surfaces actually fired. This lets the client distinguish a real delivery from a silent no-op (no subscribers AND no Electron host), which would otherwise look like success.
      properties:
        ok:
          type: boolean
          enum:
            - true
          description: Always `true` when dispatch ran without throwing.
          example: true
        native:
          type: boolean
          description: '`true` when a native OS notification was shown via Electron''s main-process Notification API (i.e. the server is hosted inside the desktop app and notifications are supported). `false` under a plain `npm start` host.'
          example: false
        pushed:
          type: integer
          minimum: 0
          description: Count of stored Web Push subscriptions that accepted the encrypted payload (fulfilled `web-push` sends).
          example: 2
        failed:
          type: integer
          minimum: 0
          description: Count of Web Push sends that were rejected. Subscriptions rejected with HTTP 410 (Gone) are deleted from `push_subscriptions` as part of this request.
          example: 1
    SessionsFacetsResponse:
      type: object
      description: 'Facet values for the Sessions page filter UI: the distinct working directories (cwd) and the distinct data-source ids seen across all sessions.'
      required:
        - cwds
        - sources
      properties:
        cwds:
          type: array
          description: Distinct, non-empty session working directories (the `cwd` column), sorted ascending. Powers the cwd filter dropdown.
          items:
            type: string
          example:
            - /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
            - /Users/son/code/another-project
        sources:
          type: array
          description: Distinct data-source ids seen across all sessions (the `sessions.source` column). Always includes the built-in `local` history; each configured remote SSH machine contributes its `remote_sources.id`. Powers the source filter dropdown and the `sources` query param on the list/analytics endpoints.
          items:
            type: string
          example:
            - local
            - 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
    RemoteSource:
      type: object
      description: A configured remote SSH machine the dashboard pulls Claude Code history from. NO secrets are stored on this record — SSH authentication defers entirely to the host's SSH stack (ssh-agent, `~/.ssh/config`, and key files). `host` is an SSH destination (`user@host`) or a `~/.ssh/config` alias.
      required:
        - id
        - label
        - host
        - ssh_port
        - identity_file
        - remote_home
        - enabled
        - status
        - last_error
        - last_sync_at
        - last_sync_counts
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: Primary key — the remote-source id (also used as `sessions.source`).
          example: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
        label:
          type: string
          description: Human-readable name shown in the UI.
          example: Work laptop
        host:
          type: string
          description: SSH destination (`user@host`) or a `~/.ssh/config` alias resolved by the host SSH stack.
          example: son@studio.local
        ssh_port:
          type: integer
          nullable: true
          description: Optional SSH port; null defers to the SSH default / `~/.ssh/config`.
          example: 22
        identity_file:
          type: string
          nullable: true
          description: Optional path to a private-key file passed to ssh (`-i`); null to omit.
          example: ~/.ssh/id_ed25519
        remote_home:
          type: string
          nullable: true
          description: Optional remote Claude home to read transcripts from; null defaults to the remote `~/.claude`.
          example: ~/.claude
        enabled:
          type: boolean
          description: Whether this source is eligible for scheduled/manual syncs.
          example: true
        status:
          type: string
          enum:
            - idle
            - syncing
            - ok
            - error
          description: Last known sync status of the source.
          example: ok
        last_error:
          type: string
          nullable: true
          description: Error message from the last failed sync/test, or null.
          example: null
        last_sync_at:
          type: string
          format: date-time
          nullable: true
          description: ISO-8601 timestamp of the last successful sync, or null.
          example: '2026-07-22T18:41:55.117Z'
        last_sync_counts:
          type: object
          nullable: true
          additionalProperties: true
          description: Counters from the last sync (imported / skipped / backfilled / errors / sessions_seen / sessions_tagged), or null if never synced.
          example:
            imported: 9
            skipped: 41
            backfilled: 0
            errors: 0
            sessions_seen: 50
            sessions_tagged: 50
        created_at:
          type: string
          format: date-time
          description: ISO-8601 creation timestamp.
          example: '2026-07-20T09:15:00.000Z'
        updated_at:
          type: string
          format: date-time
          description: ISO-8601 timestamp of the last edit.
          example: '2026-07-22T18:41:55.117Z'
    RemoteSourceCreateRequest:
      type: object
      description: Request body to register a remote SSH source. `label` and `host` are required; the rest are optional. No credentials are ever accepted or stored — auth defers to the host SSH stack.
      required:
        - label
        - host
      properties:
        label:
          type: string
          description: Human-readable name.
          example: Work laptop
        host:
          type: string
          description: SSH destination (`user@host`) or a `~/.ssh/config` alias.
          example: son@studio.local
        ssh_port:
          type: integer
          description: Optional SSH port.
          example: 22
        identity_file:
          type: string
          description: Optional private-key path passed to ssh (`-i`).
          example: ~/.ssh/id_ed25519
        remote_home:
          type: string
          description: Optional remote Claude home; defaults to the remote `~/.claude`.
          example: ~/.claude
        enabled:
          type: boolean
          description: Whether the source is enabled for syncing (default true).
          example: true
    RemoteSourceUpdateRequest:
      type: object
      description: Partial update for a remote source. Only the keys present in the body are changed; omitted keys are left as-is. Same field set as create; both `label` and `host` are optional here.
      properties:
        label:
          type: string
          example: Studio Mac
        host:
          type: string
          example: son@studio.local
        ssh_port:
          type: integer
          nullable: true
          example: 2222
        identity_file:
          type: string
          nullable: true
          example: ~/.ssh/id_ed25519
        remote_home:
          type: string
          nullable: true
          example: ~/.claude
        enabled:
          type: boolean
          example: false
    RemoteSourceResponse:
      type: object
      required:
        - source
      properties:
        source:
          $ref: '#/components/schemas/RemoteSource'
    RemoteSourceListResponse:
      type: object
      required:
        - sources
      properties:
        sources:
          type: array
          items:
            $ref: '#/components/schemas/RemoteSource'
    RemoteSourceTestResponse:
      type: object
      description: Result of an SSH connectivity probe.
      required:
        - ok
        - message
      properties:
        ok:
          type: boolean
          example: true
        message:
          type: string
          description: Human-readable probe result.
          example: Connected; found 24 project directories under ~/.claude/projects.
        remoteProjects:
          type: array
          description: Optional list of remote project directories discovered during the probe (present on success).
          items:
            type: string
          example:
            - '-Users-son-code-foo'
            - '-Users-son-code-bar'
    RemoteSourceSyncResponse:
      type: object
      description: Counters from a pull-now sync against the remote source.
      required:
        - ok
        - imported
        - skipped
        - backfilled
        - errors
        - sessions_seen
        - sessions_tagged
      properties:
        ok:
          type: boolean
          example: true
        imported:
          type: integer
          example: 9
        skipped:
          type: integer
          example: 41
        backfilled:
          type: integer
          example: 0
        errors:
          type: integer
          example: 0
        sessions_seen:
          type: integer
          example: 50
        sessions_tagged:
          type: integer
          description: Number of imported sessions stamped with this source's id.
          example: 50
    SettingsClaudeHomeResponse:
      type: object
      description: The Claude Code home directory the dashboard reads transcripts and settings from. Defaults to `~/.claude` unless overridden via the CLAUDE_HOME environment variable.
      required:
        - claude_home
      properties:
        claude_home:
          type: string
          description: Absolute path to the active Claude Code home directory (CLAUDE_HOME, or `<homedir>/.claude` when unset).
          example: /Users/son/.claude
    SettingsClaudeHomeUpdateRequest:
      type: object
      description: Request body for changing the Claude Code home directory. A leading `~` is expanded to the user's home directory; the resolved path must be absolute and point to an existing directory.
      required:
        - path
      properties:
        path:
          type: string
          description: New Claude Code home directory. A leading `~/` is expanded to the OS home directory before validation. Must resolve to an absolute path that exists and is a directory.
          example: ~/.codefuse/engine/cc
    SettingsClaudeHomeUpdateResponse:
      type: object
      description: Confirmation that CLAUDE_HOME was updated. The new value is applied to process.env immediately and persisted to the project `.env` file.
      required:
        - ok
        - claude_home
      properties:
        ok:
          type: boolean
          enum:
            - true
        claude_home:
          type: string
          description: The resolved absolute path now in effect (after `~` expansion).
          example: /Users/son/.codefuse/engine/cc
    WorkflowToolRun:
      type: object
      description: 'A Claude Code Workflow-tool run (issue #167): a fleet of sub-agents spawned by the ''Workflow'' tool (or self-paced /loop). These emit no hooks; the source of truth is the on-disk run journal, ingested into the `workflows` table (see server/lib/workflow-ingest.js). Keyed by `run_id` and parented to the launching session. The JSON-blob columns `phases` and `progress` are parsed into arrays before serialization.'
      required:
        - run_id
        - session_id
        - status
        - agent_count
        - total_tokens
        - total_tool_calls
        - phases
        - progress
        - source
        - created_at
        - updated_at
      properties:
        run_id:
          type: string
          description: Primary key — the workflow run id.
          example: wf_a1b2c3d4
        session_id:
          type: string
          description: The session that launched this run (FK into sessions.id).
          example: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
        task_id:
          type: string
          nullable: true
          description: Optional task/issue identifier associated with the run.
          example: ISSUE-167
        name:
          type: string
          nullable: true
          description: Human-readable run name from the journal, if present.
          example: Refactor pricing engine
        status:
          type: string
          description: Open status string (e.g. running | completed | error | failed). Intentionally not constrained to an enum so new harness states never trip a stale constraint.
          example: completed
        default_model:
          type: string
          nullable: true
          description: Default model the run delegated work to, when recorded.
          example: claude-opus-4-8
        started_at:
          type: string
          format: date-time
          nullable: true
          description: When the run started, if known.
          example: '2026-06-25T18:04:11.122Z'
        ended_at:
          type: string
          format: date-time
          nullable: true
          description: When the run finished, if known.
          example: '2026-06-25T18:09:47.530Z'
        duration_ms:
          type: integer
          nullable: true
          description: Total run duration in milliseconds, if known.
          example: 336408
        agent_count:
          type: integer
          minimum: 0
          description: Number of inner agents in this run.
          example: 6
        total_tokens:
          type: integer
          minimum: 0
          description: Aggregate token usage across the run's inner agents.
          example: 1284750
        total_tool_calls:
          type: integer
          minimum: 0
          description: Aggregate tool-call count across the run's inner agents.
          example: 412
        phases:
          type: array
          description: Parsed `phases[]` array from the run journal (verbatim journal payload, opaque to this API). Empty array when absent or unparseable.
          items:
            type: object
            additionalProperties: true
          example:
            - name: plan
              status: completed
            - name: implement
              status: completed
        progress:
          type: array
          description: Parsed `workflowProgress[]` array from the run journal (verbatim journal payload, opaque to this API). Empty array when absent or unparseable.
          items:
            type: object
            additionalProperties: true
          example:
            - step: 1
              label: scaffold
              done: true
        script_path:
          type: string
          nullable: true
          description: Path to the run's driving script, if recorded.
          example: /Users/son/.claude/projects/-Users-son-code/wf_a1b2c3d4.sh
        journal_path:
          type: string
          nullable: true
          description: Path to the on-disk run journal this row was ingested from.
          example: /Users/son/.claude/projects/-Users-son-code/5f3c0e2a/workflows/wf_a1b2c3d4.json
        source:
          type: string
          description: Ingestion source for the row (defaults to 'journal').
          example: journal
        created_at:
          type: string
          format: date-time
          description: Row creation timestamp.
          example: '2026-06-25T18:09:48.001Z'
        updated_at:
          type: string
          format: date-time
          description: Row last-update timestamp.
          example: '2026-06-25T18:09:48.001Z'
    WorkflowRunsListResponse:
      type: object
      description: Paginated list of Workflow-tool runs with status counts. `total` reflects the active filter (status when supplied, otherwise the full table); `counts` is always the whole-table breakdown by status.
      required:
        - runs
        - total
        - counts
        - limit
        - offset
      properties:
        runs:
          type: array
          items:
            $ref: '#/components/schemas/WorkflowToolRun'
        total:
          type: integer
          minimum: 0
          description: 'Total runs matching the current filter (independent of limit/offset). Equals the status-filtered count when `status` is supplied, otherwise the full-table count. Note: not narrowed by `session_id`.'
          example: 42
        counts:
          type: object
          description: Whole-table run counts grouped by status (not affected by filters).
          additionalProperties:
            type: integer
            minimum: 0
          example:
            completed: 30
            error: 5
            running: 7
        limit:
          type: integer
          description: Effective page size used.
          example: 50
        offset:
          type: integer
          description: Effective pagination offset used.
          example: 0
    WorkflowRunDetailResponse:
      type: object
      description: A single Workflow-tool run with its linked inner agents and the events attributed to those agents (chronological, capped at 5000).
      required:
        - workflow
        - agents
        - events
      properties:
        workflow:
          $ref: '#/components/schemas/WorkflowToolRun'
        agents:
          type: array
          description: Inner agents linked to this run via agents.workflow_run_id.
          items:
            $ref: '#/components/schemas/Agent'
        events:
          type: array
          description: Events attributed to this run's inner agents, ordered by created_at then id. Capped at 5000 rows.
          items:
            $ref: '#/components/schemas/DashboardEvent'
paths:
  /api/health:
    get:
      tags:
        - Health
      summary: Health check
      operationId: getHealth
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /api/sessions:
    get:
      tags:
        - Sessions
      summary: List sessions
      description: Returns a paginated list of sessions, newest activity first, each enriched with a SQL `agent_count` (LEFT JOIN onto agents), a `last_activity` alias of `updated_at`, and a `cost` computed from the session's token usage against the current pricing rules. The `status` and `q` filters compose (AND) with each other and with pagination; `q` is a case-insensitive LIKE across `id`, `name`, and `cwd`. `total` reflects all rows matching the filters independent of `limit`/`offset` so paginators stay accurate, while `cost` is only calculated for the rows on the returned page (when `sort_by=price` it is computed across all matching rows so the price sort is correct). The endpoint is read-only with no side effects; `metadata` on each session is returned as a raw JSON-encoded string, not a parsed object.
      operationId: listSessions
      parameters:
        - $ref: '#/components/parameters/SessionStatusQuery'
          example: active
        - name: q
          in: query
          schema:
            type: string
          description: Case-insensitive search across `id` / `name` / `cwd`. Composes with the status filter when both are present.
          example: pricing
        - $ref: '#/components/parameters/SourcesQuery'
          example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
        - $ref: '#/components/parameters/LimitQuery'
          example: 50
        - $ref: '#/components/parameters/OffsetQuery'
          example: 0
      responses:
        '200':
          description: Session list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionsListResponse'
              example:
                sessions:
                  - id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                    name: Refactor pricing route + add cost endpoint
                    status: active
                    cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                    model: claude-opus-4-20250514
                    started_at: '2026-06-25T14:02:11.004Z'
                    ended_at: null
                    metadata: '{"source":"hook","git_branch":"feat/spend-budgets"}'
                    updated_at: '2026-06-25T14:31:50.119Z'
                    agent_count: 4
                    last_activity: '2026-06-25T14:31:50.119Z'
                    cost: 0.8421
                    awaiting_input_since: null
                    awaiting_reason: null
                  - id: 1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9
                    name: Fix flaky transcript pagination test
                    status: completed
                    cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                    model: claude-sonnet-4-20250514
                    started_at: '2026-06-24T09:12:00.000Z'
                    ended_at: '2026-06-24T09:48:32.501Z'
                    metadata: null
                    updated_at: '2026-06-24T09:48:32.501Z'
                    agent_count: 1
                    last_activity: '2026-06-24T09:48:32.501Z'
                    cost: 0.1532
                    awaiting_input_since: null
                    awaiting_reason: null
                limit: 50
                offset: 0
                total: 137
    post:
      tags:
        - Sessions
      summary: Create session (idempotent)
      description: 'Creates a session keyed by `id`. The operation is idempotent: if a session with that `id` already exists it is returned untouched with `created: false` and HTTP 200; only a brand-new row yields `created: true` and HTTP 201. New sessions are inserted with `status: "active"` and any omitted optional fields stored as null. The `metadata` field is accepted as a JSON object in the request but persisted (and returned on the session) as a JSON-encoded string. A successful create broadcasts a `session_created` websocket frame. A missing `id` returns 400 with code `INVALID_INPUT`.'
      operationId: createSession
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionCreateRequest'
            example:
              id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
              name: Refactor pricing route + add cost endpoint
              cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
              model: claude-opus-4-20250514
              metadata:
                source: hook
                git_branch: feat/spend-budgets
      responses:
        '200':
          description: Session already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCreateResponse'
              example:
                session:
                  id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Refactor pricing route + add cost endpoint
                  status: active
                  cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                  model: claude-opus-4-20250514
                  started_at: '2026-06-25T14:02:11.004Z'
                  ended_at: null
                  metadata: '{"source":"hook","git_branch":"feat/spend-budgets"}'
                  updated_at: '2026-06-25T14:31:50.119Z'
                  agent_count: 4
                  last_activity: '2026-06-25T14:31:50.119Z'
                  cost: 0.8421
                  awaiting_input_since: null
                  awaiting_reason: null
                created: false
        '201':
          description: Session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCreateResponse'
              example:
                session:
                  id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Refactor pricing route + add cost endpoint
                  status: active
                  cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                  model: claude-opus-4-20250514
                  started_at: '2026-06-25T14:02:11.004Z'
                  ended_at: null
                  metadata: '{"source":"hook","git_branch":"feat/spend-budgets"}'
                  updated_at: '2026-06-25T14:02:11.004Z'
                  agent_count: 0
                  last_activity: '2026-06-25T14:02:11.004Z'
                  cost: 0
                  awaiting_input_since: null
                  awaiting_reason: null
                created: true
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_INPUT
                  message: id is required
  /api/sessions/{id}:
    get:
      tags:
        - Sessions
      summary: Get session details
      description: Returns a single session together with all of its agents (chronological) and persisted events. Read-only, no side effects. The session's `metadata` and each event's `data` are returned as raw JSON-encoded strings, not parsed objects. Returns 404 with code `NOT_FOUND` when no session matches the path `id`.
      operationId: getSession
      parameters:
        - $ref: '#/components/parameters/SessionIdPath'
          example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
      responses:
        '200':
          description: Session with associated agents/events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionDetailResponse'
              example:
                session:
                  id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Refactor pricing route + add cost endpoint
                  status: active
                  cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                  model: claude-opus-4-20250514
                  started_at: '2026-06-25T14:02:11.004Z'
                  ended_at: null
                  metadata: '{"source":"hook","git_branch":"feat/spend-budgets"}'
                  updated_at: '2026-06-25T14:31:50.119Z'
                  agent_count: 4
                  last_activity: '2026-06-25T14:31:50.119Z'
                  cost: 0.8421
                  awaiting_input_since: null
                  awaiting_reason: null
                agents:
                  - id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                    session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                    name: Main Agent
                    type: main
                    subagent_type: null
                    status: working
                    task: null
                    current_tool: Edit
                    started_at: '2026-06-25T14:02:11.004Z'
                    ended_at: null
                    parent_agent_id: null
                    metadata: '{"model":"claude-opus-4-20250514"}'
                    updated_at: '2026-06-25T14:31:50.119Z'
                    awaiting_input_since: null
                    awaiting_reason: null
                  - id: ad18a79192af10ed1
                    session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                    name: Explore pricing module
                    type: subagent
                    subagent_type: Explore
                    status: completed
                    task: Map every caller of calculateCost() across server/routes
                    current_tool: null
                    started_at: '2026-06-25T14:10:22.310Z'
                    ended_at: '2026-06-25T14:14:09.882Z'
                    parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                    metadata: null
                    updated_at: '2026-06-25T14:14:09.882Z'
                    awaiting_input_since: null
                    awaiting_reason: null
                events:
                  - id: 48213
                    session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                    agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                    event_type: PostToolUse
                    tool_name: Edit
                    summary: Edited server/routes/pricing.js
                    data: '{"tool_input":{"file_path":"server/routes/pricing.js"},"tool_response":{"success":true}}'
                    created_at: '2026-06-25T14:31:50.119Z'
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Session not found
    patch:
      tags:
        - Sessions
      summary: Update session
      description: Partially updates a session by `id`. Only `name`, `status`, `ended_at`, and `metadata` are accepted; any field omitted from the body is passed as null and the underlying UPDATE uses COALESCE, so a null leaves the existing column value unchanged (partial-update semantics) — you cannot clear a field to null through this endpoint. `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts a `session_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the session does not exist.
      operationId: updateSession
      parameters:
        - $ref: '#/components/parameters/SessionIdPath'
          example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionUpdateRequest'
            example:
              status: completed
              ended_at: '2026-06-25T15:07:44.220Z'
              metadata:
                source: hook
                git_branch: feat/spend-budgets
                outcome: merged
      responses:
        '200':
          description: Session updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionUpdateResponse'
              example:
                session:
                  id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Refactor pricing route + add cost endpoint
                  status: completed
                  cwd: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                  model: claude-opus-4-20250514
                  started_at: '2026-06-25T14:02:11.004Z'
                  ended_at: '2026-06-25T15:07:44.220Z'
                  metadata: '{"source":"hook","git_branch":"feat/spend-budgets","outcome":"merged"}'
                  updated_at: '2026-06-25T15:07:44.220Z'
                  agent_count: 4
                  last_activity: '2026-06-25T15:07:44.220Z'
                  cost: 0.8421
                  awaiting_input_since: null
                  awaiting_reason: null
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Session not found
  /api/sessions/{id}/stats:
    get:
      tags:
        - Sessions
      summary: Get aggregated session stats
      description: 'Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.'
      operationId: getSessionStats
      parameters:
        - $ref: '#/components/parameters/SessionIdPath'
          example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
      responses:
        '200':
          description: Aggregated session stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionStatsResponse'
              example:
                session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                total_events: 1284
                events_by_type:
                  - event_type: PostToolUse
                    count: 612
                  - event_type: PreToolUse
                    count: 612
                  - event_type: Notification
                    count: 41
                  - event_type: Stop
                    count: 19
                tools_used:
                  - tool_name: Bash
                    count: 188
                  - tool_name: Edit
                    count: 143
                  - tool_name: Read
                    count: 121
                  - tool_name: Grep
                    count: 77
                error_count: 6
                first_event_at: '2026-06-25T14:02:11.052Z'
                last_event_at: '2026-06-25T14:31:50.119Z'
                agents:
                  total: 4
                  main: 1
                  subagent: 3
                  compaction: 1
                  by_status:
                    working: 1
                    completed: 2
                    error: 1
                subagent_types:
                  - subagent_type: Explore
                    count: 2
                  - subagent_type: general-purpose
                    count: 1
                tokens:
                  input_tokens: 18422
                  output_tokens: 9134
                  cache_read_tokens: 1204880
                  cache_write_tokens: 88210
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Session not found
  /api/sessions/{id}/transcripts:
    get:
      tags:
        - Sessions
      summary: List available transcripts for a session
      description: Lists every JSONL transcript file associated with a session — the main agent's transcript plus any subagent and compaction transcripts — by scanning the on-disk Claude project directory (live files, falling back to import-time snapshots). Read-only, no side effects. Each entry carries a best-effort `db_agent_id` resolved by matching transcripts to tracked agents (exact id first, then positional-by-time within each type group); it may be null when a transcript has no matching agent row. Used by the Conversation tab to populate the transcript switcher. Returns 404 with code `NOT_FOUND` when the session does not exist.
      operationId: listSessionTranscripts
      parameters:
        - $ref: '#/components/parameters/SessionIdPath'
          example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
      responses:
        '200':
          description: List of transcripts available for the session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptListResponse'
              example:
                transcripts:
                  - id: main
                    name: Main Agent
                    type: main
                    has_transcript: true
                    db_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                  - id: ad18a79192af10ed1
                    name: Explore pricing module
                    type: subagent
                    subagent_type: Explore
                    has_transcript: true
                    db_agent_id: ad18a79192af10ed1
                  - id: acompact-7c1e2f90
                    name: Context Compaction
                    type: compaction
                    subagent_type: null
                    has_transcript: true
                    db_agent_id: null
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Session not found
  /api/sessions/{id}/transcript:
    get:
      tags:
        - Sessions
      summary: Stream messages from a specific transcript
      description: 'Returns parsed, renderable messages from a JSONL transcript with cursor-based pagination, reading the live file under ~/.claude/projects and falling back to the durable import-time snapshot. Pass `agent_id` to select a specific subagent or compaction transcript (default is the session''s main transcript). Pagination cursors are mutually exclusive: `after` returns messages strictly newer than a JSONL line number (incremental live updates on `new_event`), `before` returns messages strictly older than a line (load-on-scroll-up), and `offset` is legacy start-offset paging. `last_line`/`first_line` are the JSONL line numbers of the newest/oldest returned message — feed them back as `after`/`before`. When the session, transcript file, or path cannot be found the endpoint degrades gracefully to an empty result (`messages: []`, `total: 0`, `has_more: false`) rather than erroring. Read-only, no side effects.'
      operationId: getSessionTranscript
      parameters:
        - $ref: '#/components/parameters/SessionIdPath'
          example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
        - name: agent_id
          in: query
          schema:
            type: string
          description: Transcript identifier — 'main' for the session's main transcript, or a subagent / compaction id from /transcripts.
          example: main
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 500
          description: Maximum number of messages to return.
          example: 50
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
          description: Offset from the start of the transcript (mutually exclusive with after/before).
          example: 0
        - name: after
          in: query
          schema:
            type: integer
            minimum: 0
          description: Only return messages whose JSONL line number is strictly greater than this value. Used for incremental live updates.
          example: 842
        - name: before
          in: query
          schema:
            type: integer
            minimum: 0
          description: Only return messages whose JSONL line number is strictly less than this value. Used to load older messages on scroll-up.
          example: 200
      responses:
        '200':
          description: Parsed messages with cursor metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptResponse'
              example:
                messages:
                  - type: user
                    timestamp: '2026-06-25T14:02:11.004Z'
                    content:
                      - type: text
                        text: Refactor the pricing route and add a cost endpoint.
                  - type: assistant
                    timestamp: '2026-06-25T14:02:18.771Z'
                    model: claude-opus-4-20250514
                    content:
                      - type: thinking
                        text: I'll start by reading server/routes/pricing.js.
                      - type: tool_use
                        name: Read
                        id: toolu_01A7c2Df9
                        input:
                          file_path: server/routes/pricing.js
                    usage:
                      input_tokens: 412
                      output_tokens: 96
                  - type: user
                    timestamp: '2026-06-25T14:02:19.330Z'
                    content:
                      - type: tool_result
                        id: toolu_01A7c2Df9
                        output: |-
                          const { Router } = require("express");
                          ...
                        is_error: false
                total: 1284
                has_more: true
                last_line: 5310
                first_line: 5301
        '404':
          description: Session or transcript not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Session not found
  /api/agents:
    get:
      tags:
        - Agents
      summary: List agents
      description: 'Returns agents, most recent first. Filters are applied with precedence rather than composition: when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent''s `metadata` is returned as a raw JSON-encoded string, not a parsed object.'
      operationId: listAgents
      parameters:
        - $ref: '#/components/parameters/AgentStatusQuery'
          example: working
        - $ref: '#/components/parameters/SessionFilterQuery'
          example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
        - $ref: '#/components/parameters/SourcesQuery'
          example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
        - $ref: '#/components/parameters/LimitQuery'
          example: 50
        - $ref: '#/components/parameters/OffsetQuery'
          example: 0
      responses:
        '200':
          description: Agent list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentsListResponse'
              example:
                agents:
                  - id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                    session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                    name: Main Agent
                    type: main
                    subagent_type: null
                    status: working
                    task: null
                    current_tool: Edit
                    started_at: '2026-06-25T14:02:11.004Z'
                    ended_at: null
                    parent_agent_id: null
                    metadata: '{"model":"claude-opus-4-20250514"}'
                    updated_at: '2026-06-25T14:31:50.119Z'
                    awaiting_input_since: null
                    awaiting_reason: null
                  - id: ad18a79192af10ed1
                    session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                    name: Explore pricing module
                    type: subagent
                    subagent_type: Explore
                    status: completed
                    task: Map every caller of calculateCost() across server/routes
                    current_tool: null
                    started_at: '2026-06-25T14:10:22.310Z'
                    ended_at: '2026-06-25T14:14:09.882Z'
                    parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                    metadata: null
                    updated_at: '2026-06-25T14:14:09.882Z'
                    awaiting_input_since: null
                    awaiting_reason: null
                limit: 50
                offset: 0
    post:
      tags:
        - Agents
      summary: Create agent (idempotent)
      description: 'Creates an agent keyed by `id`. The operation is idempotent: if an agent with that `id` already exists it is returned untouched with `created: false` and HTTP 200; only a brand-new row yields `created: true` and HTTP 201. Omitted optional fields default server-side — `type` to `"main"`, `status` to `"waiting"` — and other unspecified columns are stored as null. `metadata` is accepted as a JSON object but persisted (and returned) as a JSON-encoded string. A successful create broadcasts an `agent_created` websocket frame. Missing `id`, `session_id`, or `name` returns 400 with code `INVALID_INPUT`.'
      operationId: createAgent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentCreateRequest'
            example:
              id: ad18a79192af10ed1
              session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
              name: Explore pricing module
              type: subagent
              subagent_type: Explore
              status: working
              task: Map every caller of calculateCost() across server/routes
              parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
              metadata:
                spawned_by: Task
      responses:
        '200':
          description: Agent already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentCreateResponse'
              example:
                agent:
                  id: ad18a79192af10ed1
                  session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Explore pricing module
                  type: subagent
                  subagent_type: Explore
                  status: completed
                  task: Map every caller of calculateCost() across server/routes
                  current_tool: null
                  started_at: '2026-06-25T14:10:22.310Z'
                  ended_at: '2026-06-25T14:14:09.882Z'
                  parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                  metadata: null
                  updated_at: '2026-06-25T14:14:09.882Z'
                  awaiting_input_since: null
                  awaiting_reason: null
                created: false
        '201':
          description: Agent created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentCreateResponse'
              example:
                agent:
                  id: ad18a79192af10ed1
                  session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Explore pricing module
                  type: subagent
                  subagent_type: Explore
                  status: working
                  task: Map every caller of calculateCost() across server/routes
                  current_tool: null
                  started_at: '2026-06-25T14:10:22.310Z'
                  ended_at: null
                  parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                  metadata: '{"spawned_by":"Task"}'
                  updated_at: '2026-06-25T14:10:22.310Z'
                  awaiting_input_since: null
                  awaiting_reason: null
                created: true
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_INPUT
                  message: id, session_id, and name are required
  /api/agents/{id}:
    get:
      tags:
        - Agents
      summary: Get agent
      description: Returns a single agent by `id`. Read-only, no side effects. The agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object. Returns 404 with code `NOT_FOUND` when no agent matches the path `id`.
      operationId: getAgent
      parameters:
        - $ref: '#/components/parameters/AgentIdPath'
          example: ad18a79192af10ed1
      responses:
        '200':
          description: Agent details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentDetailResponse'
              example:
                agent:
                  id: ad18a79192af10ed1
                  session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Explore pricing module
                  type: subagent
                  subagent_type: Explore
                  status: completed
                  task: Map every caller of calculateCost() across server/routes
                  current_tool: null
                  started_at: '2026-06-25T14:10:22.310Z'
                  ended_at: '2026-06-25T14:14:09.882Z'
                  parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                  metadata: null
                  updated_at: '2026-06-25T14:14:09.882Z'
                  awaiting_input_since: null
                  awaiting_reason: null
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Agent not found
    patch:
      tags:
        - Agents
      summary: Update agent
      description: 'Partially updates an agent by `id`. Accepts `name`, `status`, `task`, `current_tool`, `ended_at`, and `metadata`. The UPDATE uses COALESCE, so any field omitted (passed as null) leaves the existing column value unchanged — with one deliberate exception: `current_tool` is written through verbatim when present in the body, so it can be explicitly cleared to null (e.g. when a tool call finishes). `metadata` is supplied as a JSON object but stored and returned as a JSON-encoded string. A successful update re-reads the row and broadcasts an `agent_updated` websocket frame. Returns 404 with code `NOT_FOUND` when the agent does not exist.'
      operationId: updateAgent
      parameters:
        - $ref: '#/components/parameters/AgentIdPath'
          example: ad18a79192af10ed1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentUpdateRequest'
            example:
              status: completed
              current_tool: null
              ended_at: '2026-06-25T14:14:09.882Z'
      responses:
        '200':
          description: Agent updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentUpdateResponse'
              example:
                agent:
                  id: ad18a79192af10ed1
                  session_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
                  name: Explore pricing module
                  type: subagent
                  subagent_type: Explore
                  status: completed
                  task: Map every caller of calculateCost() across server/routes
                  current_tool: null
                  started_at: '2026-06-25T14:10:22.310Z'
                  ended_at: '2026-06-25T14:14:09.882Z'
                  parent_agent_id: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d-main
                  metadata: null
                  updated_at: '2026-06-25T14:14:09.882Z'
                  awaiting_input_since: null
                  awaiting_reason: null
        '404':
          description: Agent not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Agent not found
  /api/events:
    get:
      tags:
        - Events
      summary: List events with multi-dimensional filtering
      operationId: listEvents
      description: |-
        Returns a paginated, reverse-chronological slice of the `events` table (ordered by `created_at DESC, id DESC`) together with the total row count matching the active filters, so the UI can drive a paginator without a second request.

        All four entity filters — `event_type`, `tool_name`, `agent_id`, and `session_id` — accept a **comma-separated list (CSV)** of values and match with `IN (...)` semantics: passing `event_type=Stop,PreToolUse` returns rows whose `event_type` is either `Stop` OR `PreToolUse`. Values are trimmed and blank entries are dropped. Filters are combined with one another using AND.

        `q` performs a case-insensitive substring (`LIKE %q%`) search across the `summary`, `tool_name`, and the JSON-encoded `data` columns. `from`/`to` are inclusive ISO-8601 datetime bounds on `created_at`; unparseable values are ignored rather than rejected. `limit` is clamped to 1–500 (default 50) and `offset` is clamped to >= 0 (default 0).

        Note: each returned event's `data` field is a **JSON-encoded string**, not a nested object — callers must `JSON.parse` it to inspect the payload.
      parameters:
        - in: query
          name: event_type
          description: 'Comma-separated (CSV) list of `event_type` values; matched with IN semantics (OR within the list). Common values: PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd.'
          schema:
            type: string
          example: Stop,PreToolUse
        - in: query
          name: tool_name
          description: 'Comma-separated (CSV) list of `tool_name` values; matched with IN semantics (OR within the list). Common values: Bash, Edit, Read, Write, Grep, Glob, Task.'
          schema:
            type: string
          example: Bash,Edit
        - in: query
          name: agent_id
          description: Comma-separated (CSV) list of `agent_id` values; matched with IN semantics. The main agent of a session uses the id `<session_id>-main`.
          schema:
            type: string
          example: 8f3c2a10-1b2c-4d5e-9f80-112233445566-main
        - in: query
          name: session_id
          description: Comma-separated (CSV) list of `session_id` values; matched with IN semantics (OR within the list).
          schema:
            type: string
          example: 8f3c2a10-1b2c-4d5e-9f80-112233445566,2a7d9e44-3c1f-4a6b-bc20-aabbccddeeff
        - in: query
          name: q
          description: Case-insensitive substring search (`LIKE %q%`) applied across the `summary`, `tool_name`, and JSON-encoded `data` columns.
          schema:
            type: string
          example: curl
        - in: query
          name: from
          description: ISO-8601 datetime lower bound (inclusive) on `created_at`. Unparseable values are ignored.
          schema:
            type: string
            format: date-time
          example: '2026-06-25T00:00:00.000Z'
        - in: query
          name: to
          description: ISO-8601 datetime upper bound (inclusive) on `created_at`. Unparseable values are ignored.
          schema:
            type: string
            format: date-time
          example: '2026-06-26T00:00:00.000Z'
        - $ref: '#/components/parameters/SourcesQuery'
          example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
        - in: query
          name: limit
          description: Max rows to return; clamped to 1–500 (default 50).
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
          example: 50
        - $ref: '#/components/parameters/OffsetQuery'
      responses:
        '200':
          description: Event list with total count for pagination
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventsListResponse'
              example:
                events:
                  - id: 48213
                    session_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566
                    agent_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566-main
                    event_type: PreToolUse
                    tool_name: Bash
                    summary: 'Bash: curl -s https://api.example.com/health'
                    data: '{"session_id":"8f3c2a10-1b2c-4d5e-9f80-112233445566","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"curl -s https://api.example.com/health","description":"Check upstream health"},"cwd":"/Users/dev/project"}'
                    created_at: '2026-06-25T18:42:07.512Z'
                  - id: 48212
                    session_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566
                    agent_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566-main
                    event_type: Stop
                    tool_name: null
                    summary: Session finished responding
                    data: '{"session_id":"8f3c2a10-1b2c-4d5e-9f80-112233445566","hook_event_name":"Stop"}'
                    created_at: '2026-06-25T18:41:55.004Z'
                limit: 50
                offset: 0
                total: 1342
  /api/events/facets:
    get:
      tags:
        - Events
      summary: Distinct event_type and tool_name values available in the DB
      operationId: listEventFacets
      description: Returns the distinct, non-null `event_type` and `tool_name` values currently present in the `events` table, each sorted alphabetically. The UI uses this to populate the filter dropdowns on the Events screen without hardcoding the set of tools or hook types — so the lists automatically reflect whatever has actually been ingested. Both arrays are independent and may be empty when the table holds no matching rows.
      responses:
        '200':
          description: Facet values for populating filter dropdowns
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventsFacetsResponse'
              example:
                event_types:
                  - Notification
                  - PostToolUse
                  - PreToolUse
                  - SessionEnd
                  - SessionStart
                  - Stop
                  - SubagentStop
                tool_names:
                  - Bash
                  - Edit
                  - Glob
                  - Grep
                  - Read
                  - Task
                  - Write
  /api/stats:
    get:
      tags:
        - Stats
      summary: Get aggregate dashboard stats
      operationId: getStats
      description: |-
        Returns the headline counters shown across the top of the dashboard: total and active session/agent counts, total event count, today's event count, and the current number of live WebSocket connections.

        The overview counters are spread at the top level of the response object. Two additional maps, `agents_by_status` and `sessions_by_status`, break the counts down by lifecycle status (e.g. agents: working/waiting/completed/error; sessions: active/completed/error/abandoned). **Statuses with a zero count are omitted from these maps**, so callers must not assume every status key is present.

        `events_today` is computed in the caller's local day. Pass `tz_offset` as the minutes value from JavaScript's `Date.prototype.getTimezoneOffset()` (for example `420` for US Pacific Daylight Time, `300` for US Eastern Daylight Time, `0` for UTC). When omitted or non-numeric, the server falls back to UTC (offset 0).
      parameters:
        - in: query
          name: tz_offset
          description: Caller timezone offset in MINUTES, as returned by JS `Date.prototype.getTimezoneOffset()` (e.g. 420 for PDT, 300 for EDT, 0 for UTC). Used to bucket `events_today` into the caller's local day. Defaults to 0 (UTC) when omitted or non-numeric.
          schema:
            type: integer
          example: 420
        - $ref: '#/components/parameters/SourcesQuery'
          example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
      responses:
        '200':
          description: Statistics overview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsResponse'
              example:
                total_sessions: 184
                active_sessions: 3
                active_agents: 5
                total_agents: 372
                total_events: 28451
                events_today: 612
                ws_connections: 2
                agents_by_status:
                  working: 4
                  waiting: 1
                  completed: 360
                  error: 7
                sessions_by_status:
                  active: 3
                  completed: 175
                  error: 6
  /api/metrics:
    get:
      tags:
        - Metrics
      summary: Prometheus metrics exposition
      operationId: getMetrics
      description: Exposes the dashboard's live counters (sessions/agents by status, event and token totals, connected realtime clients, configured remote sources, process uptime/memory, build version) in the Prometheus text-exposition format (v0.0.4) for scraping into Prometheus / Grafana. Read-only. Mounted under `/api`, so it honors the optional `DASHBOARD_TOKEN` guard — point the scrape job's bearer token at it when the server is bound to a LAN.
      responses:
        '200':
          description: Prometheus text exposition (v0.0.4).
          content:
            text/plain:
              schema:
                type: string
              example: |
                # HELP ccam_up 1 when the dashboard API is serving this scrape.
                # TYPE ccam_up gauge
                ccam_up 1
                # HELP ccam_sessions Number of sessions by lifecycle status.
                # TYPE ccam_sessions gauge
                ccam_sessions{status="active"} 3
  /api/analytics:
    get:
      tags:
        - Analytics
      summary: Get analytics aggregates
      operationId: getAnalytics
      description: |-
        Returns the full analytics rollup powering the Analytics screen: aggregate token usage (`tokens`), total estimated spend across all sessions (`total_cost`, in USD, computed from the configured pricing rules), per-tool invocation counts (`tool_usage`), per-day event and session time series (`daily_events`, `daily_sessions`), the distribution of subagent types (`agent_types`), per-type event counts (`event_types`), the mean number of events per session (`avg_events_per_session`), the total subagent count (`total_subagents`), and a nested `overview` object mirroring the headline session/agent/event counters.

        As with `/api/stats`, the top-level `agents_by_status` and `sessions_by_status` maps **omit statuses whose count is zero**. The `agent_types[].subagent_type` field may be `null` for the main agent / untyped subagents.

        The daily time series are bucketed by the caller's local day. Pass `tz_offset` as the minutes value from JS `Date.prototype.getTimezoneOffset()` (e.g. `420` for PDT). When omitted or non-numeric, the server buckets in UTC.
      parameters:
        - in: query
          name: tz_offset
          description: Caller timezone offset in MINUTES, as returned by JS `Date.prototype.getTimezoneOffset()` (e.g. 420 for PDT, 300 for EDT, 0 for UTC). Used to bucket the `daily_events` / `daily_sessions` time series into the caller's local day. Defaults to UTC when omitted or non-numeric.
          schema:
            type: integer
          example: 420
        - $ref: '#/components/parameters/SourcesQuery'
          example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
      responses:
        '200':
          description: Analytics response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsResponse'
              example:
                tokens:
                  total_input: 4821002
                  total_output: 1933517
                  total_cache_read: 19288440
                  total_cache_write: 2044120
                total_cost: 42.7183
                tool_usage:
                  - tool_name: Bash
                    count: 5821
                  - tool_name: Read
                    count: 4310
                  - tool_name: Edit
                    count: 2980
                  - tool_name: Grep
                    count: 1744
                daily_events:
                  - date: '2026-06-23'
                    count: 488
                  - date: '2026-06-24'
                    count: 921
                  - date: '2026-06-25'
                    count: 612
                daily_sessions:
                  - date: '2026-06-23'
                    count: 4
                  - date: '2026-06-24'
                    count: 9
                  - date: '2026-06-25'
                    count: 6
                agent_types:
                  - subagent_type: null
                    count: 184
                  - subagent_type: general-purpose
                    count: 96
                  - subagent_type: Explore
                    count: 71
                  - subagent_type: code-reviewer
                    count: 21
                event_types:
                  - event_type: PreToolUse
                    count: 14210
                  - event_type: PostToolUse
                    count: 13988
                  - event_type: Stop
                    count: 168
                  - event_type: SubagentStop
                    count: 85
                avg_events_per_session: 154.6
                total_subagents: 188
                overview:
                  total_sessions: 184
                  active_sessions: 3
                  active_agents: 5
                  total_agents: 372
                  total_events: 28451
                agents_by_status:
                  working: 4
                  waiting: 1
                  completed: 360
                  error: 7
                sessions_by_status:
                  active: 3
                  completed: 175
                  error: 6
  /api/hooks/event:
    post:
      tags:
        - Hooks
      summary: Ingest Claude Code hook event
      operationId: ingestHookEvent
      description: |-
        Primary ingestion endpoint for Claude Code lifecycle hooks. The hook handler posts an envelope of the form `{ hook_type, data }`, where `hook_type` is the Claude Code hook name (PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd) and `data` carries the raw hook payload — at minimum a `session_id`. The server upserts the session and its main agent on first sight, applies the appropriate lifecycle state transition, extracts token usage and compaction signals from the transcript when present, persists an `events` row (storing `data` as a JSON-encoded string), and broadcasts a `new_event` message over the WebSocket.

        On success the response is `{ ok: true, event: { ... } }`, where `event` echoes the normalized row that was just inserted (`session_id`, `agent_id`, `event_type`, `tool_name`, `summary`, `created_at`). Ingestion is designed to be fail-safe and non-blocking for the hook caller.

        Validation failures return HTTP 400 with an `ErrorResponse` body (`{ error: { code, message } }`): `INVALID_INPUT` when `hook_type` or `data` is missing, and `MISSING_SESSION` when `data.session_id` is absent.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HookEventRequest'
            example:
              hook_type: PreToolUse
              data:
                session_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566
                hook_event_name: PreToolUse
                tool_name: Bash
                tool_input:
                  command: curl -s https://api.example.com/health
                  description: Check upstream health
                cwd: /Users/dev/project
                transcript_path: /Users/dev/.claude/projects/-Users-dev-project/8f3c2a10-1b2c-4d5e-9f80-112233445566.jsonl
      responses:
        '200':
          description: Event processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HookEventResponse'
              example:
                ok: true
                event:
                  session_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566
                  agent_id: 8f3c2a10-1b2c-4d5e-9f80-112233445566-main
                  event_type: PreToolUse
                  tool_name: Bash
                  summary: 'Bash: curl -s https://api.example.com/health'
                  created_at: '2026-06-25T18:42:07.512Z'
        '400':
          description: Invalid hook payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: MISSING_SESSION
                  message: session_id is required in data
  /api/pricing:
    get:
      tags:
        - Pricing
      summary: List pricing rules
      operationId: listPricingRules
      description: 'Returns every stored pricing rule, wrapped as `{ pricing: [ ... ] }`. Each rule carries per-MTok (per-million-token) rates for input, output, cache reads, and the two cache-write tiers (5-minute and 1-hour ephemeral), plus optional fast-mode input/output rates (0 = not configured). Rules are matched against model ids by treating the SQL `%` wildcard in `model_pattern` as `.*`; when several rules match, the longest (most specific) pattern wins. Rates here feed the cost calculations under `/api/pricing/cost`.'
      responses:
        '200':
          description: Pricing rules
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PricingListResponse'
              example:
                pricing:
                  - model_pattern: claude-opus-4%
                    display_name: Claude Opus 4 (family)
                    input_per_mtok: 15
                    output_per_mtok: 75
                    cache_read_per_mtok: 1.5
                    cache_write_per_mtok: 18.75
                    cache_write_1h_per_mtok: 30
                    fast_input_per_mtok: 0
                    fast_output_per_mtok: 0
                    updated_at: '2026-06-25T18:42:11.000Z'
                  - model_pattern: claude-haiku%
                    display_name: Claude Haiku (family)
                    input_per_mtok: 0.8
                    output_per_mtok: 4
                    cache_read_per_mtok: 0.08
                    cache_write_per_mtok: 1
                    cache_write_1h_per_mtok: 1.6
                    fast_input_per_mtok: 0
                    fast_output_per_mtok: 0
                    updated_at: '2026-06-20T09:15:00.000Z'
    put:
      tags:
        - Pricing
      summary: Create/update pricing rule
      operationId: upsertPricingRule
      description: 'Creates a pricing rule or updates the existing one with the same `model_pattern` (upsert keyed on `model_pattern`). `model_pattern` and `display_name` are required; every `*_per_mtok` rate is optional and defaults to 0 when omitted. Use the SQL `%` wildcard in `model_pattern` to match a model family (e.g. `claude-opus-4%`). Set `fast_input_per_mtok` / `fast_output_per_mtok` only if the model bills fast-mode usage at a premium; leave them 0 otherwise. Note the asymmetry with the list endpoint: the response wraps a SINGLE stored rule as `{ pricing: <rule> }` (not an array). A missing `model_pattern` or `display_name` returns 400 `INVALID_INPUT`, and so does any `*_per_mtok` rate that is not a non-negative finite number (numeric strings are coerced; NaN and negative rates are rejected before anything is written).'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PricingUpsertRequest'
            example:
              model_pattern: claude-opus-4%
              display_name: Claude Opus 4 (family)
              input_per_mtok: 15
              output_per_mtok: 75
              cache_read_per_mtok: 1.5
              cache_write_per_mtok: 18.75
              cache_write_1h_per_mtok: 30
              fast_input_per_mtok: 0
              fast_output_per_mtok: 0
      responses:
        '200':
          description: Pricing rule stored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PricingUpsertResponse'
              example:
                pricing:
                  model_pattern: claude-opus-4%
                  display_name: Claude Opus 4 (family)
                  input_per_mtok: 15
                  output_per_mtok: 75
                  cache_read_per_mtok: 1.5
                  cache_write_per_mtok: 18.75
                  cache_write_1h_per_mtok: 30
                  fast_input_per_mtok: 0
                  fast_output_per_mtok: 0
                  updated_at: '2026-06-25T18:42:11.000Z'
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_INPUT
                  message: model_pattern and display_name are required
  /api/pricing/{pattern}:
    delete:
      tags:
        - Pricing
      summary: Delete pricing rule
      operationId: deletePricingRule
      description: 'Deletes the pricing rule whose `model_pattern` exactly matches the `pattern` path segment. The pattern is URL-ENCODED: the SQL `%` wildcard must be sent as `%25` (so `claude-opus-4%` becomes `claude-opus-4%25`). The server decodes it before lookup. Returns `{ ok: true }` on success, or 404 `NOT_FOUND` if no rule matches.'
      parameters:
        - name: pattern
          in: path
          required: true
          schema:
            type: string
          description: Model pattern (URL-encoded). The SQL `%` wildcard must be escaped as `%25` (e.g. `claude-opus-4%25` for the rule `claude-opus-4%`).
          example: claude-opus-4%25
      responses:
        '200':
          description: Rule deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteOkResponse'
              example:
                ok: true
        '404':
          description: Pricing rule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Pricing rule not found
  /api/pricing/cost:
    get:
      tags:
        - Pricing
      summary: Get total token cost across all sessions
      operationId: getTotalCost
      description: Computes the aggregate token cost across EVERY session by matching each (model, speed, inference_geo, service_tier) usage bucket against the most specific pricing rule. Returns `total_cost`, a per-bucket `breakdown`, `feature_costs` (web-search surcharge, code-execution container time with the org free-hours allowance applied), `unpriced_models` (usage with no matching rule, contributing $0 so the total stays honest), and `daily_costs` bucketed by local calendar day. Pass `tz_offset` (minutes; the JS `Date.getTimezoneOffset()` value, e.g. 300 for US Eastern, -120 for CEST) so day boundaries align with the viewer's timezone; omitted/invalid offsets fall back to UTC. Honors the `sources` data-scope filter, like the sessions / stats / analytics endpoints, so the reported cost matches the active scope.
      parameters:
        - name: tz_offset
          in: query
          required: false
          schema:
            type: integer
          description: Viewer timezone offset in minutes, as returned by `Date.getTimezoneOffset()` (positive for zones behind UTC, e.g. 300 = US Eastern, -120 = CEST). Shifts the `daily_costs` day boundaries; invalid or omitted values default to UTC.
          example: 300
        - name: sources
          in: query
          required: false
          schema:
            type: string
          description: Comma-separated data-source ids to include (local history is `local`; remote SSH machines use their `remote_sources.id`). Omit for all sources. Narrows the aggregate cost to the given origins.
          example: local
      responses:
        '200':
          description: Cost result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostResult'
              example:
                total_cost: 12.8431
                breakdown:
                  - model: claude-opus-4-8
                    speed: standard
                    inference_geo: global
                    service_tier: standard
                    input_tokens: 184320
                    output_tokens: 51200
                    cache_read_tokens: 920000
                    cache_write_tokens: 64000
                    cache_write_1h_tokens: 12000
                    web_search_requests: 8
                    web_fetch_requests: 3
                    code_execution_requests: 2
                    cost: 8.4127
                    matched_rule: claude-opus-4%
                  - model: claude-haiku-4-5
                    speed: fast
                    inference_geo: us
                    service_tier: standard
                    input_tokens: 512000
                    output_tokens: 128000
                    cache_read_tokens: 64000
                    cache_write_tokens: 8000
                    cache_write_1h_tokens: 0
                    web_search_requests: 0
                    web_fetch_requests: 0
                    code_execution_requests: 0
                    cost: 1.5904
                    matched_rule: claude-haiku%
                feature_costs:
                  web_search_cost: 0.08
                  web_fetch_cost: 0
                  code_execution_cost: 0
                  code_execution_hours_estimated: 0.1667
                  code_execution_free_hours: 50
                unpriced_models:
                  - model: claude-experimental-preview
                    input_tokens: 4096
                    output_tokens: 2048
                    cache_read_tokens: 0
                    cache_write_tokens: 0
                daily_costs:
                  - date: '2026-06-23'
                    cost: 3.1102
                  - date: '2026-06-24'
                    cost: 5.7421
                  - date: '2026-06-25'
                    cost: 3.9908
  /api/pricing/cost/{sessionId}:
    get:
      tags:
        - Pricing
      summary: Get token cost for one session
      operationId: getSessionCost
      description: Same cost computation as `/api/pricing/cost`, but scoped to a single session's token usage. Returns the identical `CostResult` shape (`total_cost`, `breakdown`, `feature_costs`, `unpriced_models`, `daily_costs`); `daily_costs` holds at most one entry — the session's start date in the viewer's local day, or an empty array if the session id is unknown. Pass `tz_offset` (minutes, `Date.getTimezoneOffset()`) to place that start date in the viewer's timezone; defaults to UTC.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
          description: Session ID to price.
          example: sess_8f2a3b1c
        - name: tz_offset
          in: query
          required: false
          schema:
            type: integer
          description: Viewer timezone offset in minutes (`Date.getTimezoneOffset()`; 300 = US Eastern, -120 = CEST). Places the session start date in the viewer's local day; defaults to UTC when omitted or invalid.
          example: 300
      responses:
        '200':
          description: Session cost result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostResult'
              example:
                total_cost: 8.4127
                breakdown:
                  - model: claude-opus-4-8
                    speed: standard
                    inference_geo: global
                    service_tier: standard
                    input_tokens: 184320
                    output_tokens: 51200
                    cache_read_tokens: 920000
                    cache_write_tokens: 64000
                    cache_write_1h_tokens: 12000
                    web_search_requests: 8
                    web_fetch_requests: 3
                    code_execution_requests: 2
                    cost: 8.4127
                    matched_rule: claude-opus-4%
                  - model: claude-haiku-4-5
                    speed: fast
                    inference_geo: us
                    service_tier: standard
                    input_tokens: 512000
                    output_tokens: 128000
                    cache_read_tokens: 64000
                    cache_write_tokens: 8000
                    cache_write_1h_tokens: 0
                    web_search_requests: 0
                    web_fetch_requests: 0
                    code_execution_requests: 0
                    cost: 1.5904
                    matched_rule: claude-haiku%
                feature_costs:
                  web_search_cost: 0.08
                  web_fetch_cost: 0
                  code_execution_cost: 0
                  code_execution_hours_estimated: 0.1667
                  code_execution_free_hours: 50
                unpriced_models:
                  - model: claude-experimental-preview
                    input_tokens: 4096
                    output_tokens: 2048
                    cache_read_tokens: 0
                    cache_write_tokens: 0
                daily_costs:
                  - date: '2026-06-25'
                    cost: 8.4127
  /api/workflows:
    get:
      tags:
        - Workflows
      summary: Get workflow intelligence aggregates
      description: 'Returns the full workflow-intelligence aggregate powering the Workflows analytics page — 11 sections in one payload: `stats` (headline counters: sessions, agents, subagents, success rate, avg depth/duration, compactions, top tool flow), `orchestration` (subagent-type breakdown + delegation edges + outcomes), `toolFlow` (tool-to-tool transitions + tool counts), `effectiveness` (per-subagent-type success rate, avg duration, weekly trend), `patterns` (frequent subagent sequences + solo-session share), `modelDelegation` (model usage for main/sub agents + tokens by model), `errorPropagation` (errors by depth/type + error rate), `concurrency` (averaged agent swim-lane start/end), `complexity` (per-session agent/token/duration rows), `compaction` (compaction counts + tokens recovered), and `cooccurrence` (directed subagent-after-subagent pairs). The optional `status` query filter scopes every section to sessions of one status. Errors use the SHORT `{ error: { message } }` shape.'
      operationId: getWorkflowIntelligence
      parameters:
        - $ref: '#/components/parameters/WorkflowStatusQuery'
      responses:
        '200':
          description: Workflow aggregate data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowAggregateResponse'
              example:
                stats:
                  totalSessions: 412
                  totalAgents: 1893
                  totalSubagents: 1481
                  avgSubagents: 3.6
                  successRate: 94.2
                  avgDepth: 1.4
                  avgDurationSec: 1187
                  totalCompactions: 96
                  avgCompactions: 0.2
                  topFlow:
                    source: Read
                    target: Edit
                    count: 3120
                orchestration:
                  sessionCount: 412
                  mainCount: 412
                  subagentTypes:
                    - subagent_type: general-purpose
                      count: 612
                      completed: 590
                      errors: 8
                    - subagent_type: Explore
                      count: 388
                      completed: 380
                      errors: 2
                  edges:
                    - source: main
                      target: general-purpose
                      weight: 612
                  outcomes:
                    - status: completed
                      count: 1402
                    - status: error
                      count: 79
                  compactions:
                    total: 96
                    sessions: 71
                toolFlow:
                  transitions:
                    - source: Read
                      target: Edit
                      value: 3120
                  toolCounts:
                    - tool_name: Read
                      count: 9821
                    - tool_name: Edit
                      count: 5402
                    - tool_name: Bash
                      count: 4810
                effectiveness:
                  - subagent_type: general-purpose
                    total: 612
                    completed: 590
                    errors: 8
                    sessions: 281
                    successRate: 98.7
                    avgDuration: 142
                    trend:
                      - 88
                      - 91
                      - 102
                      - 97
                      - 110
                      - 24
                      - 12
                patterns:
                  patterns:
                    - steps:
                        - Explore
                        - general-purpose
                      count: 142
                      percentage: 34.5
                  soloSessionCount: 120
                  soloPercentage: 29.1
                modelDelegation:
                  mainModels:
                    - model: claude-opus-4-8
                      agent_count: 290
                      session_count: 290
                  subagentModels:
                    - model: claude-opus-4-8
                      agent_count: 980
                  tokensByModel:
                    - model: claude-opus-4-8
                      input_tokens: 4820112
                      output_tokens: 1920334
                      cache_read_tokens: 88201442
                      cache_write_tokens: 12044210
                errorPropagation:
                  byDepth:
                    - depth: 0
                      count: 41
                    - depth: 1
                      count: 38
                  byType:
                    - subagent_type: general-purpose
                      count: 8
                  eventErrors:
                    - summary: 'Error in Bash: command not found'
                      count: 5
                  sessionsWithErrors: 33
                  totalSessions: 412
                  errorRate: 8
                concurrency:
                  aggregateLanes:
                    - name: Main Agent
                      avgStart: 0
                      avgEnd: 1
                      count: 412
                    - name: Explore
                      avgStart: 0.12
                      avgEnd: 0.31
                      count: 388
                complexity:
                  - id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    name: Refactor auth flow
                    status: completed
                    duration: 2538
                    agentCount: 6
                    subagentCount: 5
                    totalTokens: 940212
                    model: claude-opus-4-8
                compaction:
                  totalCompactions: 96
                  tokensRecovered: 18402991
                  perSession:
                    - session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                      compactions: 3
                  sessionsWithCompactions: 71
                  totalSessions: 412
                cooccurrence:
                  - source: Explore
                    target: general-purpose
                    weight: 142
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: 'no such table: agents'
  /api/workflows/session/{id}:
    get:
      tags:
        - Workflows
      summary: Get workflow drill-in for one session
      description: 'Returns the workflow drill-in for a single session, used by the session-level Workflow view: `session` (the session row), `tree` (the recursive parent→child agent tree rooted at the main agent), `toolTimeline` (chronological tool events with tool_name/event_type/agent_id/summary), `swimLanes` (a flat per-agent start/end lane list for the Gantt-style view), and `events` (the chronological event stream, capped at the first 500 rows). Returns 404 with the SHORT `{ error: { message } }` shape when the session id is unknown.'
      operationId: getWorkflowSession
      parameters:
        - $ref: '#/components/parameters/SessionIdPath'
      responses:
        '200':
          description: Workflow session detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowSessionResponse'
              example:
                session:
                  id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                  name: Refactor auth flow
                  status: completed
                  cwd: /Users/son/code/foo
                  model: claude-opus-4-8
                  started_at: '2026-06-25T13:00:00.000Z'
                  ended_at: '2026-06-25T13:42:18.220Z'
                  metadata: null
                  updated_at: '2026-06-25T13:42:18.220Z'
                tree:
                  - id: agent_main_5f3c0e2a
                    name: Main agent
                    type: main
                    subagent_type: null
                    status: completed
                    task: null
                    started_at: '2026-06-25T13:00:00.000Z'
                    ended_at: '2026-06-25T13:42:18.220Z'
                    children:
                      - id: agent_sub_a1
                        name: Explore the auth module
                        type: subagent
                        subagent_type: Explore
                        status: completed
                        task: Map auth call sites
                        started_at: '2026-06-25T13:02:10.000Z'
                        ended_at: '2026-06-25T13:06:55.000Z'
                        children: []
                toolTimeline:
                  - id: 91021
                    tool_name: Read
                    event_type: PreToolUse
                    agent_id: agent_main_5f3c0e2a
                    created_at: '2026-06-25T13:01:02.500Z'
                    summary: Read server/auth.js
                swimLanes:
                  - id: agent_main_5f3c0e2a
                    name: Main agent
                    type: main
                    subagent_type: null
                    status: completed
                    started_at: '2026-06-25T13:00:00.000Z'
                    ended_at: '2026-06-25T13:42:18.220Z'
                    parent_agent_id: null
                events:
                  - id: 91021
                    session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    agent_id: agent_main_5f3c0e2a
                    event_type: PreToolUse
                    tool_name: Read
                    summary: Read server/auth.js
                    data: null
                    created_at: '2026-06-25T13:01:02.500Z'
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: Session not found
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: 'no such table: agents'
  /api/settings/info:
    get:
      tags:
        - Settings
      summary: Get system/database/hook diagnostics
      description: 'Returns a diagnostics snapshot used by the Settings page: `db` (database file path, on-disk size in bytes, per-table row counts, SQLite pragmas, and recent event load over the last 5/15/60 minutes), `hooks` (whether the Claude Code hook-handler is installed in `settings.json`, the settings path, and a per-hook-type installed map), `server` (process uptime, Node version, platform, live WebSocket connection count, memory/CPU/host stats), and `transcript_cache` (number of cached transcript entries and the cached paths). Read-only and cheap to poll.'
      operationId: getSettingsInfo
      responses:
        '200':
          description: Settings and diagnostics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SettingsInfoResponse'
              example:
                db:
                  path: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor/server/data/agent-monitor.db
                  size: 48922624
                  counts:
                    sessions: 412
                    agents: 1893
                    events: 58117
                    model_pricing: 11
                    token_usage: 401
                  pragmas:
                    journal_mode: wal
                    synchronous: 1
                    auto_vacuum: 0
                    encoding: UTF-8
                    foreign_keys: 1
                    busy_timeout: 5000
                  load_stats:
                    m5: 23
                    m15: 88
                    h1: 511
                hooks:
                  installed: true
                  path: /Users/son/.claude/settings.json
                  hooks:
                    PreToolUse: true
                    PostToolUse: true
                    Stop: true
                    SubagentStop: true
                    Notification: true
                    SessionStart: true
                    SessionEnd: true
                server:
                  uptime: 14523.91
                  node_version: v22.14.0
                  platform: darwin
                  ws_connections: 2
                  memory:
                    rss: 142802944
                    heapTotal: 71303168
                    heapUsed: 58392104
                    external: 3211884
                    arrayBuffers: 1048576
                  cpu_load:
                    - 2.13
                    - 2.45
                    - 2.31
                  arch: arm64
                  total_mem: 17179869184
                  free_mem: 2147483648
                  cpus: 10
                transcript_cache:
                  entries: 37
                  paths:
                    - /Users/son/.claude/projects/-Users-son-code-foo/abc123.jsonl
                    - /Users/son/.claude/projects/-Users-son-code-bar/def456.jsonl
  /api/settings/clear-data:
    post:
      tags:
        - Settings
      summary: Delete all dashboard data
      description: '⚠ DESTRUCTIVE — IRREVERSIBLE. Deletes ALL sessions, agents, events, token_usage rows, the fired-alert feed (alert_events), and the webhook delivery log. There is no confirmation step and no undo — export first via GET /api/settings/export if you need a backup. User CONFIGURATION survives: alert *rules*, webhook *targets*, and model_pricing are preserved (they''re settings, not captured data). The response echoes the row counts that existed BEFORE the wipe so the UI can report what was removed.'
      operationId: clearData
      responses:
        '200':
          description: Data cleared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClearDataResponse'
              example:
                ok: true
                cleared:
                  sessions: 412
                  agents: 1893
                  events: 58117
                  model_pricing: 11
                  token_usage: 401
  /api/settings/reimport:
    post:
      tags:
        - Settings
      summary: Re-import legacy sessions from ~/.claude
      description: Re-runs the legacy history importer against the default `~/.claude` projects directory, funneling every transcript through the same parser + importSession pipeline the live server uses. This is IDEMPOTENT and ADDITIVE — already-imported sessions are deduplicated (counted under `skipped`), token counts and compaction baselines are preserved so cost never double-counts, and nothing existing is deleted. The response reports how many sessions were `imported` vs `skipped`, plus an `errors` count for transcripts that failed to parse.
      operationId: reimportLegacySessions
      responses:
        '200':
          description: Import completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReimportResponse'
              example:
                ok: true
                imported: 38
                skipped: 374
                errors: 0
        '500':
          description: Import failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: IMPORT_FAILED
                  message: 'ENOENT: no such file or directory, scandir ''/Users/son/.claude/projects'''
  /api/settings/reinstall-hooks:
    post:
      tags:
        - Settings
      summary: Reinstall Claude Code hooks
      description: 'Re-runs the hook installer to (re)wire the dashboard''s hook-handler into Claude Code''s `settings.json` for all seven hook types (PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd). Safe to re-run — it overwrites/repairs the dashboard''s own hook entries without touching unrelated user hooks. The response returns the post-install hook status so the UI can confirm every hook type is now `installed: true`.'
      operationId: reinstallHooks
      responses:
        '200':
          description: Hooks reinstall result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReinstallHooksResponse'
              example:
                ok: true
                hooks:
                  installed: true
                  path: /Users/son/.claude/settings.json
                  hooks:
                    PreToolUse: true
                    PostToolUse: true
                    Stop: true
                    SubagentStop: true
                    Notification: true
                    SessionStart: true
                    SessionEnd: true
        '500':
          description: Hook installation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: HOOK_INSTALL_FAILED
                  message: 'EACCES: permission denied, open ''/Users/son/.claude/settings.json'''
  /api/settings/reset-pricing:
    post:
      tags:
        - Settings
      summary: Reset pricing table to defaults
      description: ⚠ DESTRUCTIVE to pricing customizations. Deletes EVERY row in the model_pricing table and re-seeds it from the dashboard's built-in DEFAULT_PRICING list. Any custom rates or custom model patterns you added are permanently lost — there is no undo. Captured session/token data is untouched (only the pricing rules used to *compute* cost change). The response returns the full freshly-seeded pricing table.
      operationId: resetPricing
      responses:
        '200':
          description: Pricing defaults restored
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResetPricingResponse'
              example:
                ok: true
                pricing:
                  - model_pattern: claude-opus-4*
                    display_name: Claude Opus 4
                    input_per_mtok: 15
                    output_per_mtok: 75
                    cache_read_per_mtok: 1.5
                    cache_write_per_mtok: 18.75
                    cache_write_1h_per_mtok: 30
                    fast_input_per_mtok: 0
                    fast_output_per_mtok: 0
                    updated_at: '2026-06-26T00:00:00.000Z'
                  - model_pattern: claude-sonnet-4*
                    display_name: Claude Sonnet 4
                    input_per_mtok: 3
                    output_per_mtok: 15
                    cache_read_per_mtok: 0.3
                    cache_write_per_mtok: 3.75
                    cache_write_1h_per_mtok: 6
                    fast_input_per_mtok: 0
                    fast_output_per_mtok: 0
                    updated_at: '2026-06-26T00:00:00.000Z'
  /api/settings/export:
    get:
      tags:
        - Settings
      summary: Export all dashboard data as JSON
      description: 'Exports the entire dataset as a single versioned JSON document — all sessions, agents, events, token_usage rows, workflows, dashboard_runs, alert_rules, and model_pricing — stamped with `format: "ccam-export"`, `version`, and `exported_at`. Served with a `Content-Disposition: attachment` header (filename `agent-monitor-export-YYYY-MM-DD.json`) so browsers download it. Use it to back up before a destructive operation (clear-data / cleanup with purge_days) or to migrate/consolidate data across machines — the bundle is re-importable via POST /api/settings/import. Read-only; nothing is modified.'
      operationId: exportData
      responses:
        '200':
          description: Export payload (served as attachment)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportResponse'
              example:
                format: ccam-export
                version: 1
                exported_at: '2026-06-26T01:12:44.913Z'
                sessions:
                  - id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    name: Refactor auth flow
                    status: completed
                    cwd: /Users/son/code/foo
                    model: claude-opus-4-8
                    started_at: '2026-06-25T13:00:00.000Z'
                    ended_at: '2026-06-25T13:42:18.220Z'
                    metadata: null
                    updated_at: '2026-06-25T13:42:18.220Z'
                agents:
                  - id: agent_main_5f3c0e2a
                    session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    name: Main agent
                    type: main
                    subagent_type: null
                    status: completed
                    task: null
                    current_tool: null
                    started_at: '2026-06-25T13:00:00.000Z'
                    ended_at: '2026-06-25T13:42:18.220Z'
                    parent_agent_id: null
                    metadata: null
                    updated_at: '2026-06-25T13:42:18.220Z'
                events:
                  - id: 91021
                    session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    agent_id: agent_main_5f3c0e2a
                    event_type: PreToolUse
                    tool_name: Edit
                    summary: Edit server/auth.js
                    data: null
                    created_at: '2026-06-25T13:05:42.001Z'
                token_usage:
                  - session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    model: claude-opus-4-8
                    input_tokens: 18422
                    output_tokens: 9120
                    cache_read_tokens: 220184
                    cache_write_tokens: 41002
                model_pricing:
                  - model_pattern: claude-opus-4*
                    display_name: Claude Opus 4
                    input_per_mtok: 15
                    output_per_mtok: 75
                    cache_read_per_mtok: 1.5
                    cache_write_per_mtok: 18.75
                    cache_write_1h_per_mtok: 30
                    fast_input_per_mtok: 0
                    fast_output_per_mtok: 0
                    updated_at: '2026-06-26T00:00:00.000Z'
                  - model_pattern: claude-sonnet-4*
                    display_name: Claude Sonnet 4
                    input_per_mtok: 3
                    output_per_mtok: 15
                    cache_read_per_mtok: 0.3
                    cache_write_per_mtok: 3.75
                    cache_write_1h_per_mtok: 6
                    fast_input_per_mtok: 0
                    fast_output_per_mtok: 0
                    updated_at: '2026-06-26T00:00:00.000Z'
  /api/settings/cleanup:
    post:
      tags:
        - Settings
      summary: Abandon stale sessions and optionally purge old history
      description: 'Two-phase maintenance. Phase 1 (`abandon_hours`, non-destructive): marks any still-`active` session with no events newer than that many hours as `abandoned`, and completes its lingering agents — a tidy-up of crashed/orphaned sessions. Phase 2 (`purge_days`) is ⚠ DESTRUCTIVE and IRREVERSIBLE: it permanently DELETES completed/error/abandoned sessions (and their events, agents, and token_usage) whose `started_at` is older than that many days. Active sessions are NEVER purged. Both fields are optional and independent — send only `abandon_hours` for a safe tidy-up, or include `purge_days` to also reclaim disk. Export first if the purged history matters. The response reports counts for each phase.'
      operationId: cleanupData
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CleanupRequest'
            examples:
              tidyOnly:
                summary: 'Safe: abandon stale sessions only (no deletion)'
                value:
                  abandon_hours: 12
              abandonAndPurge:
                summary: Abandon stale sessions, then purge history older than 90 days (destructive)
                value:
                  abandon_hours: 12
                  purge_days: 90
      responses:
        '200':
          description: Cleanup result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CleanupResponse'
              example:
                ok: true
                abandoned: 3
                purged_sessions: 57
                purged_events: 14820
                purged_agents: 241
  /api/import/guide:
    get:
      tags:
        - Import
      summary: Import guide with OS-aware defaults and step-by-step instructions
      description: 'Returns the OS-aware import guide the Import page renders verbatim: the detected `platform`, the default `~/.claude/projects` directory (raw + display form), whether it exists and how many projects/JSONL files it holds, an OS-specific `archive_command` for bundling history off another machine, the supported file extensions, the upload size/count limits, and four ordered `steps` (locate → archive → choose mode → verify). Read-only; performs no import.'
      operationId: importGuide
      responses:
        '200':
          description: Guide payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportGuideResponse'
              example:
                platform: darwin
                default_projects_dir: /Users/son/.claude/projects
                default_projects_dir_display: ~/.claude/projects
                default_projects_dir_exists: true
                default_projects_dir_stats:
                  projects: 24
                  jsonl_files: 312
                archive_command: tar -czf claude-history.tar.gz -C ~/.claude projects
                supported_extensions:
                  - .jsonl
                  - .meta.json
                  - .zip
                  - .tar
                  - .tar.gz
                  - .tgz
                  - .gz
                max_upload_bytes: 1073741824
                max_upload_files: 2000
                steps:
                  - id: locate
                    title: Locate your Claude Code history
                    body: Claude Code stores every session as a JSONL transcript under ~/.claude/projects. Each subdirectory is named after the working directory where the session started (with slashes replaced by dashes).
                  - id: archive
                    title: Bundle it for transfer (optional)
                    body: |-
                      If you're importing from another machine, archive the whole projects folder first:

                          tar -czf claude-history.tar.gz -C ~/.claude projects

                      Move claude-history.tar.gz to this machine however you like (AirDrop, scp, USB, cloud storage).
                  - id: choose
                    title: Pick an import mode
                    body: 'Rescan default: re-read ~/.claude/projects on this machine and import anything new. From folder: point the dashboard at any directory you''ve extracted history into. Upload: drag-drop JSONL files or an archive directly into the browser.'
                  - id: verify
                    title: Verify tokens and cost
                    body: 'Imports are idempotent: re-running is always safe. Token counts are deduplicated per session ID, with compaction baselines preserved so cost never double-counts. After import, open Analytics → Cost to confirm the breakdown.'
  /api/import/rescan:
    post:
      tags:
        - Import
      summary: Rescan the default ~/.claude/projects directory
      description: 'Re-scans the default `~/.claude/projects` directory and imports anything new through the live ingestion pipeline. IDEMPOTENT and ADDITIVE — re-running is always safe, already-imported sessions are deduplicated (`skipped`), and token/compaction baselines are preserved so cost never double-counts. Progress is broadcast over the WebSocket as `import.progress` frames while it runs. The response reports `imported` / `skipped` / `backfilled` / `errors` plus `sessions_seen` and `files_scanned`, with `source: "default"`.'
      operationId: importRescan
      responses:
        '200':
          description: Import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResultResponse'
              example:
                ok: true
                source: default
                imported: 14
                skipped: 298
                backfilled: 2
                errors: 0
                sessions_seen: 312
                files_scanned: 312
        '500':
          description: Import failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: IMPORT_FAILED
                  message: Failed to read projects directory
  /api/import/scan-path:
    post:
      tags:
        - Import
      summary: Import transcripts from an arbitrary absolute directory
      description: 'Imports transcripts from an arbitrary directory you point the dashboard at (e.g. history extracted from another machine). The `path` must resolve to an existing directory: a leading `~` is expanded to the home directory, the result must be absolute, and subdirectories are walked recursively for `.jsonl` files. Same idempotent, baseline-preserving pipeline as the default rescan; progress is broadcast as `import.progress`. The response echoes the resolved `path` and the per-run counters with `source: "path"`.'
      operationId: importScanPath
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - path
              properties:
                path:
                  type: string
                  description: Absolute directory path. Tilde (~) is expanded. Walks subdirectories recursively.
            examples:
              absolute:
                summary: Absolute directory
                value:
                  path: /Users/son/Downloads/claude-history/projects
              tilde:
                summary: Tilde expanded to home directory
                value:
                  path: ~/Downloads/claude-history/projects
      responses:
        '200':
          description: Import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResultResponse'
              example:
                ok: true
                source: path
                path: /Users/son/Downloads/claude-history/projects
                imported: 9
                skipped: 41
                backfilled: 0
                errors: 0
                sessions_seen: 50
                files_scanned: 50
        '400':
          description: Path validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missing:
                  summary: path omitted
                  value:
                    error:
                      code: INVALID_INPUT
                      message: '`path` is required'
                notAbsolute:
                  summary: path is not absolute
                  value:
                    error:
                      code: INVALID_INPUT
                      message: '`path` must be an absolute path'
                notFound:
                  summary: path does not exist
                  value:
                    error:
                      code: PATH_NOT_FOUND
                      message: 'Path does not exist: /tmp/nope'
                notDir:
                  summary: path is a file, not a directory
                  value:
                    error:
                      code: NOT_A_DIRECTORY
                      message: 'Path is not a directory: /tmp/foo.jsonl'
        '500':
          description: Import failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: IMPORT_FAILED
                  message: Unexpected end of JSON input
  /api/import/upload:
    post:
      tags:
        - Import
      summary: Upload JSONL files or archives (.zip, .tar, .tar.gz, .tgz, .gz)
      description: 'Imports history uploaded directly from the browser as `multipart/form-data` under the `files` field. Accepts raw `.jsonl` / `.meta.json` transcripts and/or archives (`.zip`, `.tar`, `.tar.gz`, `.tgz`, `.gz`), which are extracted into a temp dir and walked for JSONL content. Unsupported extensions are silently rejected and reported in `rejected_files`. Extraction is bounded to defend against zip bombs — exceeding the limit returns 413. Same idempotent import pipeline; progress is broadcast as `import.progress`. On success the response carries `source: "upload"` plus `files_received`, `rejected_files`, `entries_extracted`, `entries_skipped`, and the standard import counters. (Requires the optional `multer` dependency; a missing install yields a 500.)'
      operationId: importUpload
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: Files to import. Supports .jsonl, .meta.json, .zip, .tar, .tar.gz, .tgz, .gz.
            example:
              files:
                - claude-history.tar.gz
                - session-extra.jsonl
      responses:
        '200':
          description: Import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResultResponse'
              example:
                ok: true
                source: upload
                files_received: 3
                rejected_files:
                  - notes.txt
                entries_extracted: 128
                entries_skipped: 2
                imported: 12
                skipped: 116
                backfilled: 0
                errors: 0
                sessions_seen: 128
                files_scanned: 128
        '400':
          description: No files or no JSONL content
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                noFiles:
                  summary: No supported files in the upload
                  value:
                    error:
                      code: NO_FILES
                      message: No supported files in upload. 1 file(s) rejected (unsupported extension).
                noJsonl:
                  summary: Archive extracted but contained no .jsonl
                  value:
                    error:
                      code: NO_JSONL
                      message: 'No .jsonl files were found in the uploaded content. Supported inputs: .jsonl, .meta.json, .zip, .tar, .tar.gz, .tgz, .gz.'
        '413':
          description: Extraction limit exceeded (possible zip bomb)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EXTRACTION_LIMIT_EXCEEDED
                  message: Archive exceeded the extraction size limit (possible zip bomb).
        '500':
          description: Upload or import failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                noMulter:
                  summary: Optional multer dependency not installed
                  value:
                    error:
                      code: UPLOADER_UNAVAILABLE
                      message: File upload requires `multer`. Run `npm install` to pick up new deps.
                importFailed:
                  summary: Import pipeline error
                  value:
                    error:
                      code: IMPORT_FAILED
                      message: Unexpected end of JSON input
  /api/updates/status:
    get:
      tags:
        - Updates
      summary: Check whether the dashboard git checkout is behind origin
      description: 'Reports whether the dashboard''s own git checkout is behind its upstream remote, so a user can pull and restart manually (the dashboard never self-restarts). The response is a variant object: when it IS a git checkout it includes `git_repo: true`, `repo_root`, `update_available`, `commits_behind`, `remote_ref`, `local_sha`, `remote_sha`, and a copy-pasteable `manual_command`; when it is NOT a git checkout (e.g. an npm/tarball install) it returns `git_repo: false` with a `message` and no diff fields. Read-only — this only inspects git, it does not fetch destructively or modify the working tree.'
      operationId: getUpdatesStatus
      responses:
        '200':
          description: Update check result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: Includes git_repo, update_available, commits_behind, remote_ref, local_sha, remote_sha, manual_command, and optional error/message fields.
              examples:
                updateAvailable:
                  summary: Behind origin — update available
                  value:
                    git_repo: true
                    repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                    update_available: true
                    commits_behind: 4
                    remote_ref: origin/master
                    local_sha: de5d0891a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7
                    remote_sha: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                    manual_command: npm run self-update
                    checked_at: '2026-06-26T01:20:09.553Z'
                upToDate:
                  summary: Up to date with origin
                  value:
                    git_repo: true
                    repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                    update_available: false
                    commits_behind: 0
                    remote_ref: origin/master
                    local_sha: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                    remote_sha: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                    manual_command: npm run self-update
                    checked_at: '2026-06-26T01:25:41.002Z'
                notARepo:
                  summary: Not a git checkout — detection unavailable
                  value:
                    git_repo: false
                    update_available: false
                    message: Not a git checkout — update detection is unavailable for this install.
        '500':
          description: Update status query failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: UPDATE_STATUS_FAILED
                  message: 'git: command not found'
  /api/updates/check:
    post:
      tags:
        - Updates
      summary: Run an update check immediately and broadcast the result
      description: Runs the same upstream check as GET /api/updates/status immediately and, in addition to returning the result, broadcasts it to every connected client as an `update_status` WebSocket message — so all open dashboard tabs refresh their update banner at once. The response shape is identical to GET /api/updates/status (the same git_repo / update_available / commits_behind / manual_command variant object). Read-only with respect to the working tree.
      operationId: triggerUpdatesCheck
      responses:
        '200':
          description: Fresh update status payload (also broadcast over WebSocket)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: Same shape as GET /api/updates/status. Also sent as an update_status WebSocket message to all connected clients.
              examples:
                updateAvailable:
                  summary: Behind origin — update available
                  value:
                    git_repo: true
                    repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                    update_available: true
                    commits_behind: 4
                    remote_ref: origin/master
                    local_sha: de5d0891a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7
                    remote_sha: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                    manual_command: npm run self-update
                    checked_at: '2026-06-26T01:20:09.553Z'
                upToDate:
                  summary: Up to date with origin
                  value:
                    git_repo: true
                    repo_root: /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                    update_available: false
                    commits_behind: 0
                    remote_ref: origin/master
                    local_sha: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                    remote_sha: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                    manual_command: npm run self-update
                    checked_at: '2026-06-26T01:25:41.002Z'
        '500':
          description: Update check failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: UPDATE_CHECK_FAILED
                  message: 'git fetch failed: network unreachable'
  /api/alerts:
    get:
      tags:
        - Alerts
      summary: List fired alerts, newest first
      operationId: listAlerts
      description: Returns the fired-alert feed, newest first, as `{ alerts, total, unacked, limit, offset }`. Each alert event carries the originating rule's id/name/type, a human-readable `message`, an `acknowledged` flag (0/1), `created_at`, and `details` — which is a JSON STRING (not an object) that callers must `JSON.parse`. `limit` is clamped to 1–200 (default 50) and negative `offset` is clamped to 0. Set `unacked=true` to return only unacknowledged alerts; `total` then counts only unacked rows, while `unacked` always reports the global unacknowledged count.
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
          description: Page size, clamped to the 1–200 range (default 50). Values outside the range are clamped, not rejected.
          example: 50
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
          description: Pagination offset; negative values are clamped to 0.
          example: 0
        - name: unacked
          in: query
          required: false
          schema:
            type: boolean
          description: When the literal string "true", return only unacknowledged alerts (and scope `total` to that subset).
          example: 'true'
      responses:
        '200':
          description: Paginated alert feed with total and unacked counts
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: Includes alerts[], total, unacked, limit, offset.
              example:
                alerts:
                  - id: 42
                    rule_id: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
                    rule_name: Idle session watchdog
                    rule_type: inactivity
                    message: Session sess_8f2a has been inactive for 35 minutes
                    details: '{"session_id":"sess_8f2a","minutes":35,"threshold":30}'
                    acknowledged: 0
                    created_at: '2026-06-25T17:05:44.000Z'
                  - id: 41
                    rule_id: 1a2b3c4d-5e6f-7081-9201-aabbccddeeff
                    rule_name: Heavy token burn
                    rule_type: token_threshold
                    message: Session sess_3c1d crossed 5,000,000 total tokens
                    details: '{"session_id":"sess_3c1d","total_tokens":5120000,"threshold":5000000}'
                    acknowledged: 1
                    created_at: '2026-06-25T16:40:02.000Z'
                total: 2
                unacked: 1
                limit: 50
                offset: 0
  /api/alerts/rules:
    get:
      tags:
        - Alerts
      summary: List alert rules
      operationId: listAlertRules
      description: 'Returns all alert rules as `{ rules: [ ... ] }`. Each rule''s `config` is returned as a PARSED object (the column is stored as JSON text), and `enabled` is coerced to a boolean. The `config` shape depends on `rule_type`: `event_pattern` uses event_type / tool_name / summary_contains plus optional count + window_minutes; `inactivity` uses `minutes`; `status_duration` uses `status` + `minutes`; `token_threshold` uses `total_tokens`.'
      responses:
        '200':
          description: All alert rules with parsed config objects
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                rules:
                  - id: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
                    name: Idle session watchdog
                    rule_type: inactivity
                    config:
                      minutes: 30
                    enabled: true
                    cooldown_seconds: 300
                    created_at: '2026-06-10T12:00:00.000Z'
                    updated_at: '2026-06-24T08:30:00.000Z'
                  - id: 1a2b3c4d-5e6f-7081-9201-aabbccddeeff
                    name: Heavy token burn
                    rule_type: token_threshold
                    config:
                      total_tokens: 5000000
                    enabled: true
                    cooldown_seconds: 600
                    created_at: '2026-06-12T14:20:00.000Z'
                    updated_at: '2026-06-12T14:20:00.000Z'
    post:
      tags:
        - Alerts
      summary: Create an alert rule
      operationId: createAlertRule
      description: |-
        Creates an alert rule and returns it serialized as `{ rule: { ... } }` with HTTP 201. `name`, `rule_type`, and `config` are required; the `config` shape is validated per `rule_type`:
        - `event_pattern`: `{ event_type?, tool_name?, summary_contains?, count?, window_minutes? }` — fires when matching events accumulate.
        - `inactivity`: `{ minutes }` — fires when a session goes idle.
        - `status_duration`: `{ status, minutes }` — fires when a session holds a status too long.
        - `token_threshold`: `{ total_tokens }` — fires when usage crosses a ceiling.
        `enabled` defaults to true and `cooldown_seconds` defaults to 300 (must be a non-negative integer). A bad name, unknown `rule_type`, invalid `config`, or negative cooldown returns 400 `INVALID_INPUT`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - rule_type
                - config
              properties:
                name:
                  type: string
                rule_type:
                  type: string
                  enum:
                    - event_pattern
                    - inactivity
                    - status_duration
                    - token_threshold
                config:
                  type: object
                  additionalProperties: true
                  description: 'Type-specific config. event_pattern: event_type/tool_name/summary_contains + optional count/window_minutes. inactivity: minutes. status_duration: status + minutes. token_threshold: total_tokens.'
                enabled:
                  type: boolean
                  default: true
                cooldown_seconds:
                  type: integer
                  default: 300
            examples:
              inactivity:
                summary: Inactivity rule
                value:
                  name: Idle session watchdog
                  rule_type: inactivity
                  config:
                    minutes: 30
                  enabled: true
                  cooldown_seconds: 300
              event_pattern:
                summary: Event-pattern rule (repeated tool errors)
                value:
                  name: Repeated Bash failures
                  rule_type: event_pattern
                  config:
                    event_type: PostToolUse
                    tool_name: Bash
                    summary_contains: error
                    count: 3
                    window_minutes: 10
                  enabled: true
                  cooldown_seconds: 600
              status_duration:
                summary: Status-duration rule
                value:
                  name: Stuck waiting too long
                  rule_type: status_duration
                  config:
                    status: waiting
                    minutes: 15
                  enabled: true
                  cooldown_seconds: 300
              token_threshold:
                summary: Token-threshold rule
                value:
                  name: Heavy token burn
                  rule_type: token_threshold
                  config:
                    total_tokens: 5000000
                  enabled: true
                  cooldown_seconds: 600
      responses:
        '201':
          description: Created rule
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                rule:
                  id: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
                  name: Idle session watchdog
                  rule_type: inactivity
                  config:
                    minutes: 30
                  enabled: true
                  cooldown_seconds: 300
                  created_at: '2026-06-10T12:00:00.000Z'
                  updated_at: '2026-06-24T08:30:00.000Z'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_INPUT
                  message: name is required
  /api/alerts/rules/{id}:
    patch:
      tags:
        - Alerts
      summary: Update an alert rule (partial; rule_type is immutable)
      operationId: updateAlertRule
      description: 'Partially updates an alert rule and returns it serialized as `{ rule: { ... } }`. Only the fields present in the body change; `rule_type` CANNOT be changed and any supplied `config` is validated against the rule''s STORED type. `name` (if present) must be a non-empty string and `cooldown_seconds` (if present) must be a non-negative integer. Returns 404 `NOT_FOUND` for an unknown id, or 400 `INVALID_INPUT` for a bad name, invalid config, or negative cooldown.'
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Alert rule ID (UUID).
          example: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                config:
                  type: object
                  additionalProperties: true
                enabled:
                  type: boolean
                cooldown_seconds:
                  type: integer
            examples:
              disableRule:
                summary: Disable a rule without touching its config
                value:
                  enabled: false
              retuneInactivity:
                summary: Re-tune an inactivity rule's threshold + cooldown
                value:
                  config:
                    minutes: 45
                  cooldown_seconds: 900
              rename:
                summary: Rename a rule
                value:
                  name: Idle session watchdog (prod)
      responses:
        '200':
          description: Updated rule
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                rule:
                  id: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
                  name: Idle session watchdog
                  rule_type: inactivity
                  config:
                    minutes: 30
                  enabled: false
                  cooldown_seconds: 900
                  created_at: '2026-06-10T12:00:00.000Z'
                  updated_at: '2026-06-24T08:30:00.000Z'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_INPUT
                  message: cooldown_seconds must be a non-negative integer
        '404':
          description: Rule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Alert rule not found
    delete:
      tags:
        - Alerts
      summary: Delete an alert rule and its fired-alert history
      operationId: deleteAlertRule
      description: 'Deletes the alert rule with the given id. Its fired-alert history cascades away with it (the foreign key is ON DELETE CASCADE), so any alerts previously raised by this rule are also removed from the feed. Returns `{ ok: true }` on success or 404 `NOT_FOUND` for an unknown id.'
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Alert rule ID (UUID).
          example: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
      responses:
        '200':
          description: Deletion confirmation
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                ok: true
        '404':
          description: Rule not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Alert rule not found
  /api/alerts/{id}/ack:
    post:
      tags:
        - Alerts
      summary: Acknowledge one fired alert
      operationId: ackAlert
      description: 'Marks a single fired alert (by its integer event id) as acknowledged and returns the updated row as `{ alert: { ... } }` (with `acknowledged: 1`). Acknowledging also broadcasts an `alert_updated` WebSocket message so connected dashboards refresh their unacked badge. The id must be numeric; an unknown id returns 404 `NOT_FOUND`.'
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Alert event ID (numeric).
          example: 42
      responses:
        '200':
          description: Acknowledged alert row
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                alert:
                  id: 42
                  rule_id: 7c1d8e2a-9b34-4f50-a1c2-6d8e0f3b5a91
                  rule_name: Idle session watchdog
                  rule_type: inactivity
                  message: Session sess_8f2a has been inactive for 35 minutes
                  details: '{"session_id":"sess_8f2a","minutes":35,"threshold":30}'
                  acknowledged: 1
                  created_at: '2026-06-25T17:05:44.000Z'
        '404':
          description: Alert not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Alert not found
  /api/alerts/ack-all:
    post:
      tags:
        - Alerts
      summary: Acknowledge all unacked alerts
      operationId: ackAllAlerts
      description: 'Acknowledges every currently unacknowledged alert in one call and returns `{ ok: true, acknowledged: <count> }` where `acknowledged` is the number of rows actually updated. When at least one alert is acknowledged, an `alert_updated` WebSocket message (`{ acked_all: true }`) is broadcast so dashboards clear their unacked badge. Calling this when nothing is unacked returns `acknowledged: 0`.'
      responses:
        '200':
          description: Count of acknowledged alerts
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                ok: true
                acknowledged: 3
  /api/webhooks/providers:
    get:
      tags:
        - Webhooks
      summary: List supported providers + their config fields (for the UI)
      description: 'Returns the redacted provider catalog the webhook-target editor renders. For each of the 14 supported providers (Slack, Discord, Teams, Google Chat, Mattermost, Rocket.Chat, Telegram, PagerDuty, Opsgenie, Splunk On-Call, Zapier, Make, n8n, Pipedream) plus the `generic` family, it lists: a human label, the provider family, whether the URL must be https / is user-supplied, and the per-provider config field definitions (key, label, type, required, options, and whether the field is secret). No secret values are ever included — this is purely the *shape* of the form, not stored credentials.'
      operationId: listWebhookProviders
      responses:
        '200':
          description: 'Provider catalog: label, family, url requirements, fields'
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                providers:
                  slack:
                    label: Slack
                    family: chat
                    https: true
                    urlRequired: true
                    fields: []
                  telegram:
                    label: Telegram
                    family: chat
                    https: true
                    urlRequired: false
                    fields:
                      - key: bot_token
                        label: Bot token
                        type: string
                        required: true
                        secret: true
                      - key: chat_id
                        label: Chat ID
                        type: string
                        required: true
                        secret: false
                  pagerduty:
                    label: PagerDuty
                    family: incident
                    https: true
                    urlRequired: false
                    fields:
                      - key: routing_key
                        label: Routing key
                        type: string
                        required: true
                        secret: true
                      - key: severity
                        label: Severity
                        type: enum
                        required: false
                        options:
                          - critical
                          - error
                          - warning
                          - info
                        secret: false
                  generic:
                    label: Generic webhook
                    family: generic
                    https: false
                    urlRequired: true
                    fields: []
  /api/webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhook targets (URLs masked, secrets redacted)
      description: 'Lists every configured webhook target. Responses are always redacted: the full URL is never returned — only `url_preview` (protocol + host + a `…` + the last 4 chars), `has_secret` is a boolean (never the secret itself), and any secret-flagged provider config fields (routing keys, API keys, bot tokens) and custom header values are masked to `••••`. Each target also carries its most recent delivery outcome in `last_delivery` (or null if it has never fired).'
      operationId: listWebhooks
      responses:
        '200':
          description: All configured webhook targets
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                targets:
                  - id: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
                    name: Eng on-call (Slack)
                    type: slack
                    enabled: true
                    url_preview: https://hooks.slack.com/…BXqZ
                    has_secret: false
                    headers: null
                    config: null
                    rule_ids:
                      - rule_inactivity_30m
                    created_at: '2026-06-25T14:03:11.482Z'
                    updated_at: '2026-06-25T14:03:11.482Z'
                    last_delivery:
                      status: success
                      status_code: 200
                      attempts: 1
                      error: null
                      created_at: '2026-06-25T18:41:55.117Z'
                  - id: 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                    name: Internal alert sink
                    type: generic
                    enabled: true
                    url_preview: https://alerts.internal.example.com/…hook
                    has_secret: true
                    headers:
                      X-Api-Key: ••••
                      Authorization: ••••
                    config: null
                    rule_ids: null
                    created_at: '2026-06-20T09:15:00.000Z'
                    updated_at: '2026-06-24T22:48:30.901Z'
                    last_delivery:
                      status: failed
                      status_code: 503
                      attempts: 3
                      error: Service Unavailable
                      created_at: '2026-06-24T22:48:30.901Z'
    post:
      tags:
        - Webhooks
      summary: Create a webhook target
      description: Creates a webhook target that fires when alerts match. `name` and `type` are required. `url` is required for most providers but is derived or defaulted for a few (Telegram and Opsgenie derive it from config; PagerDuty defaults it) — consult GET /api/webhooks/providers for which fields each provider needs. `config` carries provider-specific params (e.g. `{ chat_id }` for Telegram, `{ routing_key, severity }` for PagerDuty, `{ api_key, region }` for Opsgenie). `secret` (HMAC-SHA256 signing) and custom `headers` apply only to the generic family and are silently ignored for other providers. `rule_ids` optionally scopes the target to specific alert rules; omit it to fire for all rules. The response is the created target, REDACTED the same way as the list endpoint (URL masked, secrets shown only as `has_secret`/`••••`).
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - type
              properties:
                name:
                  type: string
                type:
                  type: string
                  enum:
                    - slack
                    - discord
                    - teams
                    - google_chat
                    - mattermost
                    - rocketchat
                    - telegram
                    - pagerduty
                    - opsgenie
                    - splunk_oncall
                    - zapier
                    - make
                    - n8n
                    - pipedream
                    - generic
                url:
                  type: string
                  format: uri
                  description: Required for most providers; omit for those that derive their URL (Telegram, Opsgenie) or default it (PagerDuty). See GET /api/webhooks/providers.
                enabled:
                  type: boolean
                  default: true
                config:
                  type: object
                  additionalProperties: true
                  description: Provider-specific params, e.g. { chat_id } (Telegram), { routing_key, severity } (PagerDuty), { api_key, region } (Opsgenie).
                secret:
                  type: string
                  description: 'Generic family only: HMAC-SHA256 signing secret'
                headers:
                  type: object
                  additionalProperties:
                    type: string
                  description: 'Generic family only: extra request headers'
                rule_ids:
                  type: array
                  items:
                    type: string
                  description: 'Optional: scope to specific alert rules (omit for all)'
            examples:
              slack:
                summary: Slack incoming webhook scoped to one rule
                value:
                  name: Eng on-call (Slack)
                  type: slack
                  url: https://hooks.slack.com/services/T0000/B0000/XXXXXXXXXXXXXXXXXXXXBXqZ
                  enabled: true
                  rule_ids:
                    - rule_inactivity_30m
              pagerduty:
                summary: PagerDuty (URL defaulted, routing key in config)
                value:
                  name: Sev1 pager
                  type: pagerduty
                  config:
                    routing_key: R0ABCDEF0123456789ABCDEF01
                    severity: critical
              generic:
                summary: Generic webhook with HMAC secret and custom headers
                value:
                  name: Internal alert sink
                  type: generic
                  url: https://alerts.internal.example.com/cc-monitor/hook
                  secret: whsec_4f8c2a9e1b6d7f0a3c5e8b1d2f4a6c8e
                  headers:
                    X-Api-Key: k_live_abc123
                    Authorization: Bearer t0ken
      responses:
        '201':
          description: Created target (redacted)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                target:
                  id: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
                  name: Eng on-call (Slack)
                  type: slack
                  enabled: true
                  url_preview: https://hooks.slack.com/…BXqZ
                  has_secret: false
                  headers: null
                  config: null
                  rule_ids:
                    - rule_inactivity_30m
                  created_at: '2026-06-25T14:03:11.482Z'
                  updated_at: '2026-06-25T14:03:11.482Z'
                  last_delivery:
                    status: success
                    status_code: 200
                    attempts: 1
                    error: null
                    created_at: '2026-06-25T18:41:55.117Z'
        '400':
          description: Validation error
  /api/webhooks/{id}:
    patch:
      tags:
        - Webhooks
      summary: Update a webhook target (partial; type is immutable)
      description: Partially updates a webhook target. Only the keys present in the body are changed; omitted keys are left as-is. The provider `type` is immutable. `config` is MERGED over the existing config and re-validated, so a single field (e.g. `severity`, `region`) can change without re-sending the secret fields. For the generic family, `secret` may be omitted (keep current) or sent as `null` (clear it); `headers` and `rule_ids` replace their stored value when present. The response is the updated target, REDACTED (URL masked, secrets shown only as `has_secret`/`••••`).
      operationId: updateWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                url:
                  type: string
                  format: uri
                  description: Omit to keep current
                enabled:
                  type: boolean
                config:
                  type: object
                  additionalProperties: true
                  description: Provider params; merged over existing (secrets kept if omitted)
                secret:
                  type:
                    - string
                    - 'null'
                  description: 'Generic family only: omit to keep, null to clear'
                headers:
                  type: object
                  additionalProperties:
                    type: string
                rule_ids:
                  type: array
                  items:
                    type: string
            examples:
              disable:
                summary: Disable a target without touching anything else
                value:
                  enabled: false
              renameAndRescope:
                summary: Rename and re-scope to different rules
                value:
                  name: Eng on-call (Slack) — muted weekends
                  rule_ids:
                    - rule_token_threshold_5m
              mergeConfig:
                summary: Change one PagerDuty config field (routing key kept)
                value:
                  config:
                    severity: warning
              clearSecret:
                summary: 'Generic family: clear the signing secret'
                value:
                  secret: null
      responses:
        '200':
          description: Updated target (redacted)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                target:
                  id: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
                  name: Eng on-call (Slack) — muted weekends
                  type: slack
                  enabled: true
                  url_preview: https://hooks.slack.com/…BXqZ
                  has_secret: false
                  headers: null
                  config: null
                  rule_ids:
                    - rule_inactivity_30m
                  created_at: '2026-06-25T14:03:11.482Z'
                  updated_at: '2026-06-25T14:03:11.482Z'
                  last_delivery:
                    status: success
                    status_code: 200
                    attempts: 1
                    error: null
                    created_at: '2026-06-25T18:41:55.117Z'
        '400':
          description: Validation error
        '404':
          description: Target not found
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook target and its delivery log
      description: 'Permanently deletes a webhook target. Its delivery-log history cascades away with it. Returns `{ ok: true }` on success. This only removes the delivery channel — alert *rules* and the fired-alert feed are untouched.'
      operationId: deleteWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
      responses:
        '200':
          description: Deleted
        '404':
          description: Target not found
  /api/webhooks/{id}/test:
    post:
      tags:
        - Webhooks
      summary: Send a synthetic test alert to a target
      description: Sends a synthetic test alert to the target and reports the delivery outcome synchronously. The HTTP status is always 200 when the target exists — the *request itself* succeeded — and the `ok` flag carries the downstream delivery result. `status` is the HTTP status code returned by the provider (or null if the request never completed), `attempts` is how many tries were made (the delivery layer retries transient failures), and `error` is a human-readable failure reason or null on success.
      operationId: testWebhook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
      responses:
        '200':
          description: Delivery result (ok flag carries the downstream outcome)
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              examples:
                success:
                  summary: Provider accepted the test payload
                  value:
                    ok: true
                    status: 200
                    attempts: 1
                    error: null
                failure:
                  summary: Provider rejected or was unreachable after retries
                  value:
                    ok: false
                    status: 503
                    attempts: 3
                    error: Service Unavailable
        '404':
          description: Target not found
  /api/webhooks/{id}/deliveries:
    get:
      tags:
        - Webhooks
      summary: Recent delivery log for a target
      description: Returns the recent delivery log for a target, newest first. Each row records the alert that fired, the resulting HTTP status, the number of attempts, any error text, and a timestamp. `limit` is clamped to 1–200 (default 20) and `offset` to ≥0 (default 0). This is an audit trail of past sends — it does not trigger a new delivery (use POST /{id}/test for that).
      operationId: listWebhookDeliveries
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          example: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
          description: Max rows to return (clamped to 1–200).
          example: 20
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: Pagination offset (clamped to ≥0).
          example: 0
      responses:
        '200':
          description: Delivery rows, newest first
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                deliveries:
                  - id: 5012
                    target_id: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
                    alert_event_id: 8841
                    status: success
                    status_code: 200
                    attempts: 1
                    error: null
                    created_at: '2026-06-25T18:41:55.117Z'
                  - id: 5008
                    target_id: 9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
                    alert_event_id: 8839
                    status: failed
                    status_code: 429
                    attempts: 3
                    error: Too Many Requests
                    created_at: '2026-06-25T18:30:02.004Z'
                limit: 20
                offset: 0
        '404':
          description: Target not found
  /api/openapi.json:
    get:
      tags:
        - Documentation
      summary: Get OpenAPI specification JSON
      description: 'Returns this OpenAPI 3.0 document as JSON. Both API explorers consume it: Swagger UI (`/api/docs`) for interactive try-it-out requests, and ReDoc (`/api/redoc`) for a read-optimized reference.'
      operationId: getOpenApiJson
      responses:
        '200':
          description: OpenAPI document
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
  /api/docs:
    get:
      tags:
        - Documentation
      summary: Swagger UI explorer
      description: Interactive Swagger UI rendering of this specification, with try-it-out request execution against the live local server.
      operationId: getSwaggerUi
      responses:
        '200':
          description: Swagger UI HTML page
          content:
            text/html:
              schema:
                type: string
  /api/redoc:
    get:
      tags:
        - Documentation
      summary: ReDoc API reference
      description: Read-optimized, three-panel ReDoc rendering of this specification. The ReDoc bundle is served locally from `/api/redoc/redoc.standalone.js` (bundled with the server, never fetched from a CDN), so the reference works fully offline.
      operationId: getRedoc
      responses:
        '200':
          description: ReDoc HTML page
          content:
            text/html:
              schema:
                type: string
  /api/cc-config/overview:
    get:
      tags:
        - CcConfig
      summary: Config explorer overview
      description: Read-only. Returns the four resolved filesystem roots plus aggregate counts across every surface (skills, agents, commands, output styles, plugins, marketplaces, keybindings, MCP servers, hooks, memory, settings files). All sub-reads degrade to empty on any I/O error, so this endpoint never errors.
      operationId: ccConfigGetOverview
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Overview of roots and counts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigOverviewResponse'
              example:
                roots:
                  claudeHome: /Users/son/.claude
                  projectClaudeDir: /repo/.claude
                  projectRoot: /repo
                  claudeJson: /Users/son/.claude.json
                counts:
                  skills:
                    user: 12
                    project: 3
                  agents:
                    user: 5
                    project: 2
                  commands:
                    user: 8
                    project: 0
                  outputStyles:
                    user: 1
                    project: 0
                  plugins: 8
                  pluginsEnabled: 6
                  pluginsDisabled: 1
                  marketplaces: 2
                  keybindings: 24
                  mcpServers:
                    user: 3
                    project: 1
                  hooks:
                    user: 2
                    project: 0
                    project-local: 1
                  memory: 5
                  settingsFiles: 2
  /api/cc-config/skills:
    get:
      tags:
        - CcConfig
      summary: List discovered skills
      description: Read-only. Lists skill directories (each containing a SKILL.md) under the user and/or project roots, with parsed frontmatter and a 320-char body preview. Files over 256 KiB are read truncated. Degrades to an empty list on I/O errors.
      operationId: ccConfigGetSkills
      parameters:
        - name: scope
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - project
              - all
            default: all
          description: 'Discovery scope: `user`, `project`, or `all` (default). Unrecognized values fall back to `all`.'
          example: all
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Skills list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigSkillsResponse'
              example:
                items:
                  - scope: user
                    name: code-reviewer
                    path: /Users/son/.claude/skills/code-reviewer
                    file: /Users/son/.claude/skills/code-reviewer/SKILL.md
                    size: 2048
                    mtime: 1718900000000
                    truncated: false
                    frontmatter:
                      name: code-reviewer
                      description: Reviews diffs
                    preview: Use this skill to review pull requests...
  /api/cc-config/agents:
    get:
      tags:
        - CcConfig
      summary: List subagents
      description: Read-only. Lists single-file subagent definitions (<root>/agents/*.md) under the user and/or project roots, with parsed frontmatter and a 320-char preview. Truncated above 256 KiB; degrades to empty on errors.
      operationId: ccConfigGetAgents
      parameters:
        - name: scope
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - project
              - all
            default: all
          description: 'Discovery scope: `user`, `project`, or `all` (default). Unrecognized values fall back to `all`.'
          example: all
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Agents list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigAgentsResponse'
              example:
                items:
                  - scope: project
                    name: backend-reviewer
                    file: /repo/.claude/agents/backend-reviewer.md
                    size: 1536
                    mtime: 1718900000000
                    truncated: false
                    frontmatter:
                      name: backend-reviewer
                      model: sonnet
                    preview: Review backend route and hook logic for regressions...
  /api/cc-config/commands:
    get:
      tags:
        - CcConfig
      summary: List slash commands
      description: Read-only. Lists slash-command definitions (<root>/commands/*.md) under the user and/or project roots, with parsed frontmatter and a 320-char preview. Truncated above 256 KiB; degrades to empty on errors.
      operationId: ccConfigGetCommands
      parameters:
        - name: scope
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - project
              - all
            default: all
          description: 'Discovery scope: `user`, `project`, or `all` (default). Unrecognized values fall back to `all`.'
          example: all
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Commands list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigCommandsResponse'
              example:
                items:
                  - scope: user
                    name: commit
                    file: /Users/son/.claude/commands/commit.md
                    size: 800
                    mtime: 1718900000000
                    truncated: false
                    frontmatter:
                      description: Create a commit
                    preview: Commit the staged changes with a descriptive message...
  /api/cc-config/output-styles:
    get:
      tags:
        - CcConfig
      summary: List output styles
      description: Read-only. Lists output-style definitions (<root>/output-styles/*.md) under the user and/or project roots, with parsed frontmatter and a 320-char preview. Truncated above 256 KiB; degrades to empty on errors.
      operationId: ccConfigGetOutputStyles
      parameters:
        - name: scope
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - project
              - all
            default: all
          description: 'Discovery scope: `user`, `project`, or `all` (default). Unrecognized values fall back to `all`.'
          example: all
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Output styles list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigOutputStylesResponse'
              example:
                items:
                  - scope: user
                    name: concise
                    file: /Users/son/.claude/output-styles/concise.md
                    size: 420
                    mtime: 1718900000000
                    truncated: false
                    frontmatter:
                      name: concise
                    preview: Respond tersely, no preamble...
  /api/cc-config/plugins:
    get:
      tags:
        - CcConfig
      summary: List installed plugins
      description: 'Read-only. Reads the user-scope plugins manifest (~/.claude/plugins/installed_plugins.json), resolves each instance''s on-disk contributions, and reports its tri-state enabled flag from settings.json. Read-only by design: plugins are written concurrently by the running Claude Code CLI. No scope/cwd parameters. Degrades to an empty plugin list when the manifest is missing.'
      operationId: ccConfigGetPlugins
      responses:
        '200':
          description: Plugins manifest summary and list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigPluginsResponse'
              example:
                manifestPath: /Users/son/.claude/plugins/installed_plugins.json
                manifestExists: true
                plugins:
                  - key: superpowers@obra
                    name: superpowers
                    marketplace: obra
                    scope: user
                    version: 1.2.0
                    installPath: /Users/son/.claude/plugins/superpowers
                    installedAt: '2026-05-01T12:00:00.000Z'
                    lastUpdated: '2026-06-01T09:30:00.000Z'
                    gitCommitSha: a1b2c3d4e5f6
                    installPathExists: true
                    enabled: true
                    contributes:
                      skills: 3
                      agents: 2
                      commands: 5
                      outputStyles: 0
                      hooks: 1
                      pluginJson:
                        name: superpowers
                        version: 1.2.0
  /api/cc-config/mcp:
    get:
      tags:
        - CcConfig
      summary: List MCP servers
      description: Read-only. Discovers MCP servers from ~/.claude.json (top-level and projects[<root>]) and ~/.claude/settings.json, split into `user` and `projectScoped`. Header and env VALUES are never returned — only their key NAMES. Read-only by design (the CLI writes these files concurrently). Degrades to empty arrays on errors.
      operationId: ccConfigGetMcp
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: MCP servers grouped by scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigMcpResponse'
              example:
                user:
                  - name: github
                    source: ~/.claude.json (top-level)
                    kind: stdio
                    command: npx
                    args:
                      - '-y'
                      - '@modelcontextprotocol/server-github'
                    envNames:
                      - GITHUB_TOKEN
                projectScoped:
                  - name: internal
                    source: ~/.claude.json (projects[/repo])
                    kind: http
                    url: https://mcp.example.com/sse
                    headers:
                      - Authorization
  /api/cc-config/hooks:
    get:
      tags:
        - CcConfig
      summary: List configured hooks
      description: Read-only. Reads hooks from the user, project, and project-local settings files. Known event types (SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStop, Notification, PreCompact) are flattened into matcher/type/command/timeout entries; unknown events are passed through verbatim. Hooks in settings are read-only here. Degrades to per-file `exists:false` on errors.
      operationId: ccConfigGetHooks
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Hook sources by scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigHooksResponse'
              example:
                items:
                  - scope: user
                    file: /Users/son/.claude/settings.json
                    exists: true
                    hooks:
                      PostToolUse:
                        - matcher: '*'
                          type: command
                          command: node ~/.claude/hooks/post-tool-use.js
                          timeout: null
                  - scope: project
                    file: /repo/.claude/settings.json
                    exists: false
                    hooks: {}
  /api/cc-config/settings:
    get:
      tags:
        - CcConfig
      summary: List settings files
      description: Read-only. Returns the user, project, and project-local settings.json contents with secret-like keys redacted to the literal `<redacted>` (keys matching /token|secret|password|api[_-]?key|auth/i with string values). Missing files report `exists:false`. Settings files are read-only here (concurrent CLI writes).
      operationId: ccConfigGetSettings
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Settings sources by scope (secrets redacted).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigSettingsResponse'
              example:
                items:
                  - scope: user
                    file: /Users/son/.claude/settings.json
                    exists: true
                    data:
                      model: sonnet
                      apiKey: <redacted>
                    raw_size: 412
                  - scope: project
                    file: /repo/.claude/settings.json
                    exists: false
                  - scope: project-local
                    file: /repo/.claude/settings.local.json
                    exists: false
  /api/cc-config/memory:
    get:
      tags:
        - CcConfig
      summary: List memory files
      description: Read-only. Returns the two primary CLAUDE.md files (scopes `user`/`project`) plus every per-project file-based auto-memory markdown file under ~/.claude/projects/<slug>/memory/ (scope `auto-memory`, sorted index files first). Previews are 480 chars; files over 256 KiB are truncated. Degrades to fewer files on errors.
      operationId: ccConfigGetMemory
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Memory items.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigMemoryResponse'
              example:
                items:
                  - scope: user
                    file: /Users/son/.claude/CLAUDE.md
                    size: 1200
                    mtime: 1718900000000
                    truncated: false
                    preview: '# Workspace & Productivity System...'
                  - scope: auto-memory
                    project: '-Users-son-repo'
                    name: MEMORY.md
                    isIndex: true
                    file: /Users/son/.claude/projects/-Users-son-repo/memory/MEMORY.md
                    size: 980
                    mtime: 1718900000000
                    truncated: false
                    frontmatter: {}
                    preview: '## Persistent facts\n- Never run destructive ops without confirmation...'
  /api/cc-config/marketplaces:
    get:
      tags:
        - CcConfig
      summary: List plugin marketplaces
      description: Read-only. Reads ~/.claude/plugins/known_marketplaces.json and, best-effort, each marketplace's .claude-plugin/marketplace.json for plugin count and metadata. No scope/cwd parameters. Degrades to an empty list when the file is missing.
      operationId: ccConfigGetMarketplaces
      responses:
        '200':
          description: Known marketplaces summary and list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigMarketplacesResponse'
              example:
                knownPath: /Users/son/.claude/plugins/known_marketplaces.json
                knownExists: true
                items:
                  - name: obra
                    source:
                      source: github
                      repo: obra/superpowers-marketplace
                    installLocation: /Users/son/.claude/plugins/marketplaces/obra
                    lastUpdated: '2026-06-10T00:00:00.000Z'
                    pluginCount: 12
                    marketplaceName: Superpowers
                    marketplaceDescription: Curated agent skills
                    marketplaceOwner:
                      name: obra
  /api/cc-config/keybindings:
    get:
      tags:
        - CcConfig
      summary: Get keybindings
      description: Read-only. Parses ~/.claude/keybindings.json into context-scoped groups of key → action bindings. When the file is absent, returns only `{ file, exists:false }`. No scope/cwd parameters.
      operationId: ccConfigGetKeybindings
      responses:
        '200':
          description: Parsed keybindings (or exists:false).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigKeybindingsResponse'
              example:
                file: /Users/son/.claude/keybindings.json
                exists: true
                schema: null
                docs: null
                groups:
                  - context: editor
                    bindings:
                      - key: ctrl+s
                        action: submit
    put:
      tags:
        - CcConfig
      summary: Overwrite keybindings
      description: 'Mutating. Overwrites ~/.claude/keybindings.json from a structured list of context groups (`{ context, bindings: [{ key, action }] }`); on disk the bindings become an object keyed by `key`. The existing file is backed up first (under cc-config-backups/keybindings/) and any top-level metadata (`$schema`, `$docs`, unknown keys) is preserved — only the `bindings` array is replaced. Rejects duplicate contexts and duplicate keys within a context. Writes are atomic. Unlike settings.json, keybindings.json is not rewritten mid-session by the live CLI, so this edit is safe. Emits a `cc_config_changed` websocket event on success.'
      operationId: ccConfigPutKeybindings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - groups
              properties:
                groups:
                  type: array
                  description: Full set of context-scoped keybinding groups to persist.
                  items:
                    type: object
                    required:
                      - context
                      - bindings
                    properties:
                      context:
                        type: string
                        example: Global
                      bindings:
                        type: array
                        items:
                          type: object
                          required:
                            - key
                            - action
                          properties:
                            key:
                              type: string
                              example: ctrl+t
                            action:
                              type: string
                              example: toggleTodos
            example:
              groups:
                - context: Global
                  bindings:
                    - key: ctrl+t
                      action: toggleTodos
                - context: Chat
                  bindings:
                    - key: escape
                      action: cancel
      responses:
        '200':
          description: keybindings.json written (backup taken when it already existed).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigFileWriteResponse'
              example:
                ok: true
                file: /Users/son/.claude/keybindings.json
                target: /Users/son/.claude/keybindings.json
                backupPath: /Users/son/.claude/cc-config-backups/keybindings/keybindings.json.2026-06-25T12-00-00.000Z.bak
                created: false
        '400':
          description: Bad request. `EBADREQ` — `groups` is missing or not an array. `EBADCONTENT` — a group/binding is malformed, a context is empty, or there is a duplicate context or duplicate key within a context.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADCONTENT
                  message: duplicate key "ctrl+t" in context Global
        '413':
          description: '`ETOOLARGE` — the serialized keybindings exceed the 256 KiB limit.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ETOOLARGE
                  message: content exceeds 262144 bytes
  /api/cc-config/statusline:
    get:
      tags:
        - CcConfig
      summary: Get statusline config and scripts
      description: Read-only. Returns the `statusLine` block from ~/.claude/settings.json plus any statusline scripts (statusline.py, statusline-command.sh) with a 4000-char preview. Scripts over 256 KiB are truncated. No scope/cwd parameters.
      operationId: ccConfigGetStatusline
      responses:
        '200':
          description: Statusline config and scripts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigStatuslineResponse'
              example:
                config:
                  type: command
                  command: python3 ~/.claude/statusline.py
                scripts:
                  - file: /Users/son/.claude/statusline.py
                    size: 1280
                    mtime: 1718900000000
                    truncated: false
                    preview: '#!/usr/bin/env python3\nimport json, sys\n...'
  /api/cc-config/hook-scripts:
    get:
      tags:
        - CcConfig
      summary: List hook scripts
      description: Read-only. Lists files in the ~/.claude/hooks/ directory (name, path, size, mtime), sorted by name. Returns metadata only — no file contents. Use GET /file to read an individual script. No scope/cwd parameters.
      operationId: ccConfigGetHookScripts
      responses:
        '200':
          description: Hook scripts directory listing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigHookScriptsResponse'
              example:
                dir: /Users/son/.claude/hooks
                items:
                  - name: post-tool-use.js
                    file: /Users/son/.claude/hooks/post-tool-use.js
                    size: 640
                    mtime: 1718900000000
  /api/cc-config/file:
    get:
      tags:
        - CcConfig
      summary: Read a single file body
      description: 'Read-only. Returns the body of one file. Strict path containment: the resolved absolute path MUST live under the Claude home, the project .claude dir, or (only for a file literally named CLAUDE.md) the project root. Bodies over 256 KiB are truncated. NOTE: there is no 404 path — a missing, unreadable, or out-of-root file all return 400.'
      operationId: ccConfigGetFile
      parameters:
        - name: path
          in: query
          required: true
          schema:
            type: string
          description: Absolute path to read. Must resolve under an allowed root; under the project root only CLAUDE.md is permitted.
          example: /Users/son/.claude/skills/code-reviewer/SKILL.md
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: File body with metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigFileReadResponse'
              example:
                ok: true
                file: /Users/son/.claude/skills/code-reviewer/SKILL.md
                truncated: false
                size: 2048
                text: '---\nname: code-reviewer\n---\n# Code Reviewer\n...'
                mtime: 1718900000000
        '400':
          description: Bad or denied path. `BAD_PATH` — `path` query is missing or empty. `READ_DENIED` — path is outside the allowed roots, only CLAUDE.md is readable from the project root, or the file is missing/unreadable (there is no 404).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: READ_DENIED
                  message: path is outside allowed roots
    put:
      tags:
        - CcConfig
      summary: Create or overwrite a text artifact
      description: Mutating. Creates or overwrites a low-risk text artifact (skills, agents, commands, output-styles, memory, auto-memory). Before overwriting an existing artifact a timestamped backup is created under a cc-config-backups directory (well outside the dirs Claude Code scans); fresh creates have no backup (backupPath:null). Writes are atomic (temp file + rename). Names are validated against a strict allowlist and the resolved path is re-checked to be inside its containment root. Content must be a string ≤ 256 KiB (UTF-8). Plugins, MCP servers, hooks-in-settings, and settings.json are intentionally NOT writable. Emits a `cc_config_changed` websocket event on success.
      operationId: ccConfigPutFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CcConfigFileWriteRequest'
            example:
              scope: user
              type: agents
              name: my-reviewer
              content: '---\nname: my-reviewer\n---\n# My Reviewer\nReview the diff...'
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Artifact created or overwritten.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigFileWriteResponse'
              example:
                ok: true
                file: /Users/son/.claude/agents/my-reviewer.md
                target: /Users/son/.claude/agents/my-reviewer.md
                backupPath: null
                created: true
        '400':
          description: Bad request. `EBADREQ` — scope/type missing or not strings. `EBADTYPE` — unknown type. `EBADSCOPE` — scope not user/project. `EBADNAME` — name fails the allowlist (or auto-memory name is not a flat *.md). `EBADPROJECT` — invalid auto-memory project slug. `EBADCONTENT` — content is not a string. `EOUTOFROOT` — resolved path escapes its containment root.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADNAME
                  message: name must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
        '413':
          description: '`ETOOLARGE` — content exceeds the 256 KiB (262144-byte) limit.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ETOOLARGE
                  message: content exceeds 262144 bytes
        '500':
          description: '`EINTERNAL` (or any unmapped error code) — unexpected filesystem error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EINTERNAL
                  message: 'EACCES: permission denied'
    delete:
      tags:
        - CcConfig
      summary: Delete a text artifact
      description: Mutating. Deletes a low-risk text artifact (same type/scope/name/project semantics as PUT, no content). A mandatory backup is created BEFORE deletion — if the backup fails the original is left intact. Skill deletes remove the whole skill directory. Emits a `cc_config_changed` websocket event on success. Plugins, MCP servers, hooks-in-settings, and settings.json are NOT deletable.
      operationId: ccConfigDeleteFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CcConfigFileDeleteRequest'
            example:
              scope: user
              type: agents
              name: my-reviewer
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Artifact deleted (backup taken first).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigFileDeleteResponse'
              example:
                ok: true
                file: /Users/son/.claude/agents/my-reviewer.md
                target: /Users/son/.claude/agents/my-reviewer.md
                backupPath: /Users/son/.claude/cc-config-backups/agents/my-reviewer.md.2026-06-25T12-00-00.000Z.bak
        '400':
          description: Bad request. `EBADREQ` — scope/type missing or not strings. `EBADTYPE` — unknown type. `EBADSCOPE` — scope not user/project. `EBADNAME` — invalid name. `EBADPROJECT` — invalid auto-memory project slug. `EOUTOFROOT` — resolved path escapes its containment root.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADTYPE
                  message: 'unknown type: plugins'
        '404':
          description: '`ENOTFOUND` — the target artifact does not exist.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ENOTFOUND
                  message: agents/my-reviewer does not exist
        '500':
          description: '`EINTERNAL` (or any unmapped error code) — unexpected filesystem error.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EINTERNAL
                  message: 'EACCES: permission denied'
  /api/cc-config/backups:
    get:
      tags:
        - CcConfig
      summary: List artifact backups
      description: 'Read-only. Lists backups taken by the mutation endpoints, across the user and project roots plus per-project auto-memory backup dirs. Optional `scope`/`type` narrow the search. Results are sorted newest first. Best-effort: unreadable backup dirs are skipped, never erroring.'
      operationId: ccConfigGetBackups
      parameters:
        - name: scope
          in: query
          required: false
          schema:
            type: string
            enum:
              - user
              - project
          description: Narrow to a single root scope. Only `user` or `project` are honored; any other value is ignored (both scopes scanned).
          example: user
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum:
              - skills
              - agents
              - commands
              - output-styles
              - memory
              - auto-memory
          description: Narrow to a single artifact type. When omitted, all types are scanned.
          example: agents
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Override the working directory used to resolve the project root and project .claude dir. Defaults to the dashboard server's own cwd.
          example: /repo
      responses:
        '200':
          description: Backups sorted newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CcConfigBackupsResponse'
              example:
                items:
                  - scope: user
                    type: agents
                    name: my-reviewer.md.2026-06-25T12-00-00.000Z.bak
                    backupPath: /Users/son/.claude/cc-config-backups/agents/my-reviewer.md.2026-06-25T12-00-00.000Z.bak
                    isDir: false
                    mtime: 1718900000000
                    size: 2048
  /api/run:
    get:
      tags:
        - Run
      summary: List live + recently-exited runs
      description: Returns every run handle currently held in memory (newest first) plus concurrency telemetry (`maxConcurrent`, `activeCount`). Handles are reaped 5 minutes after the process exits, so terminal runs disappear from here but remain in GET /api/run/history. Read-only — no process is spawned. The loopback same-origin guard applies.
      operationId: runList
      responses:
        '200':
          description: Tracked runs and concurrency telemetry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunListResponse'
              example:
                items:
                  - id: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
                    pid: 48213
                    mode: conversation
                    cwd: /Users/dev/projects/my-app
                    model: claude-opus-4-8
                    permissionMode: acceptEdits
                    effort: high
                    prompt: Refactor the auth module and add tests.
                    argv:
                      - '--output-format'
                      - stream-json
                      - '--verbose'
                      - '--include-partial-messages'
                      - '--permission-mode'
                      - acceptEdits
                      - '--input-format'
                      - stream-json
                    resumeSessionId: null
                    status: running
                    startedAt: 1718000000000
                    endedAt: null
                    exitCode: null
                    signal: null
                    error: null
                    sessionId: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                    envelopeCount: 12
                    stdoutTail: |
                      {"type":"assistant",...}
                    stderrTail: ''
                maxConcurrent: 10000
                activeCount: 1
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
    post:
      tags:
        - Run
      summary: Spawn a new run
      description: Spawns a real `claude` Code subprocess in the sanitised `cwd`. In `headless` mode the prompt is passed via argv and stdin is closed (one turn, then exit); in `conversation` mode stdin stays open for follow-ups via POST /api/run/{id}/message, and an existing session may be resumed with `resumeSessionId`. The child is always run with `--output-format stream-json --verbose --include-partial-messages`; parsed envelopes are broadcast as `run_stream` WebSocket messages and lifecycle transitions as `run_status`. A row is persisted to `dashboard_runs`, and the in-memory handle is reaped 5 minutes after exit. Concurrency is capped (ECONCURRENCY -> 429). The loopback same-origin guard applies. Returns the freshly-created RunHandle.
      operationId: runSpawn
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunSpawnRequest'
            example:
              prompt: Refactor the auth module and add tests.
              mode: conversation
              cwd: /Users/dev/projects/my-app
              model: claude-opus-4-8
              effort: high
              permissionMode: acceptEdits
      responses:
        '201':
          description: Run spawned; returns the new run handle (status `spawning`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunHandle'
              example:
                id: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
                pid: 48213
                mode: conversation
                cwd: /Users/dev/projects/my-app
                model: claude-opus-4-8
                permissionMode: acceptEdits
                effort: high
                prompt: Refactor the auth module and add tests.
                argv:
                  - '--output-format'
                  - stream-json
                  - '--verbose'
                  - '--include-partial-messages'
                  - '--permission-mode'
                  - acceptEdits
                  - '--input-format'
                  - stream-json
                  - '--model'
                  - claude-opus-4-8
                  - '--effort'
                  - high
                resumeSessionId: null
                status: spawning
                startedAt: 1718000000000
                endedAt: null
                exitCode: null
                signal: null
                error: null
                sessionId: null
                envelopeCount: 0
                stdoutTail: ''
                stderrTail: ''
        '400':
          description: Invalid spawn request — EBADPROMPT (prompt required), EBADCWD (cwd not an existing absolute directory), EBADMODE (bad mode, or resumeSessionId outside conversation mode), EBADEFFORT (effort not low/medium/high/xhigh/max), or EBADSESSION (resumeSessionId not a valid session id).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                badPrompt:
                  value:
                    error:
                      code: EBADPROMPT
                      message: prompt is required
                badCwd:
                  value:
                    error:
                      code: EBADCWD
                      message: cwd must be an absolute path
                badMode:
                  value:
                    error:
                      code: EBADMODE
                      message: mode must be "headless" or "conversation"
                badEffort:
                  value:
                    error:
                      code: EBADEFFORT
                      message: 'effort must be one of: low, medium, high, xhigh, max'
                badSession:
                  value:
                    error:
                      code: EBADSESSION
                      message: resumeSessionId is not a valid session id
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
        '429':
          description: Concurrency cap reached (ECONCURRENCY). The body additionally carries a top-level `running` array listing the runs currently holding the cap.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunSpawnConcurrencyResponse'
              example:
                error:
                  code: ECONCURRENCY
                  message: concurrency limit 10000 reached
                running:
                  - id: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
                    pid: 48213
                    startedAt: 1718000000000
                    mode: conversation
        '500':
          description: Unexpected spawn failure (EINTERNAL).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EINTERNAL
                  message: spawn claude ENOENT
  /api/run/history:
    get:
      tags:
        - Run
      summary: List persisted run history
      description: 'Returns the persistent history of runs spawned via the dashboard, sourced from the `dashboard_runs` sqlite table (newest first). Unlike GET /api/run, this survives the 5-minute in-memory reap so past runs stay visible and resumable for days. Each item is cross-referenced against live handles to set `isLive`. Returns `{ items: [] }` if the persistence DB is unavailable. Read-only — no process is spawned. The loopback same-origin guard applies.'
      operationId: runHistory
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 50
          description: Maximum number of history rows to return (default 50, clamped to 1–500).
          example: 50
        - name: laneId
          in: query
          required: false
          schema:
            type: integer
          description: Return only the runs started through this lane (POST /api/lanes/{id}/start). Omit for every run; runs spawned from POST /api/run carry no lane and are excluded whenever this is set.
          example: 5
      responses:
        '200':
          description: Run history, most recent first
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunHistoryResponse'
              example:
                items:
                  - id: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
                    session_id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                    mode: conversation
                    cwd: /Users/dev/projects/my-app
                    model: claude-opus-4-8
                    permission_mode: acceptEdits
                    effort: high
                    resume_session_id: null
                    prompt_preview: Refactor the auth module and add tests.
                    status: completed
                    exit_code: 0
                    started_at: '2026-06-25T17:00:00.000Z'
                    ended_at: '2026-06-25T17:01:42.500Z'
                    isLive: false
                    lane_id: null
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
  /api/run/cwds:
    get:
      tags:
        - Run
      summary: Suggest working directories
      description: 'Suggests plausible working directories for the Run launcher: the dashboard server''s cwd (always first), $HOME, and distinct recent cwds Claude Code has been used in (from the sessions table). Only directories that still exist on disk are returned; the DB lookup is best-effort. Read-only — no process is spawned. The loopback same-origin guard applies.'
      operationId: runCwds
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: Optional case-insensitive substring filter (also applied client-side).
          example: my-app
      responses:
        '200':
          description: Working-directory suggestions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunCwdsResponse'
              example:
                items:
                  - kind: dashboard
                    path: /Users/dev/Claude-Code-Agent-Monitor
                    label: Dashboard server
                  - kind: home
                    path: /Users/dev
                    label: Home
                  - kind: recent
                    path: /Users/dev/projects/my-app
                    label: my-app
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
  /api/run/files:
    get:
      tags:
        - Run
      summary: Autocomplete files within a cwd
      description: Walks the given `cwd` and returns up to 40 file paths (relative to that cwd, shortest first) for the prompt editor's `@` references. The cwd is validated via the same sanitiser as spawning (must be an existing absolute directory). Dotfiles (except .env/.gitignore) and heavy build dirs (node_modules, .git, dist, build, out, .next, coverage, target, .venv, __pycache__, etc.) are skipped; the walk is bounded (≤5000 entries visited). Read-only — no process is spawned. The loopback same-origin guard applies.
      operationId: runFiles
      parameters:
        - name: cwd
          in: query
          required: false
          schema:
            type: string
          description: Absolute directory to walk. Must exist; defaults to the dashboard server's cwd when omitted. Invalid values yield 400 EBADCWD.
          example: /Users/dev/projects/my-app
        - name: q
          in: query
          required: false
          schema:
            type: string
          description: Optional case-insensitive substring filter applied to the relative path.
          example: route
      responses:
        '200':
          description: Matching file paths relative to cwd
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunFilesResponse'
              example:
                items:
                  - package.json
                  - src/index.ts
                  - server/routes/run.js
        '400':
          description: 'Invalid cwd (EBADCWD): not a string, not absolute, or does not exist.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADCWD
                  message: 'cwd does not exist: /no/such/dir'
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
  /api/run/binary:
    get:
      tags:
        - Run
      summary: Check whether the `claude` binary is on PATH
      description: Probes PATH (via which/where) for the `claude` binary so the UI can warn before the user clicks Run. The binary is NOT invoked — only resolved. Read-only — no process is spawned beyond the lookup. The loopback same-origin guard applies.
      operationId: runBinary
      responses:
        '200':
          description: Binary resolution result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunBinaryResponse'
              example:
                found: true
                path: /usr/local/bin/claude
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
  /api/run/{id}/message:
    post:
      tags:
        - Run
      summary: Send a follow-up turn to a conversation run
      description: 'Writes a stream-json user envelope to a running conversation''s stdin and broadcasts a `run_input_ack` WebSocket message. Only valid for `conversation`-mode runs that are still `running`/`spawning` with writable stdin. SIDE EFFECT: the child resumes processing the new turn and emits further `run_stream`/`run_status` messages. The loopback same-origin guard applies.'
      operationId: runSendMessage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Run id (RunHandle.id).
          example: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunMessageRequest'
            example:
              text: Also update the README to document the new flags.
      responses:
        '200':
          description: Turn written to stdin
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunMessageResponse'
              example:
                messageId: 9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f
        '400':
          description: Cannot deliver input — EBADINPUT (text required), EWRONGMODE (run is not conversation mode), ENOTRUNNING (run is not running/spawning), or ESTDINCLOSED (child stdin not writable).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                badInput:
                  value:
                    error:
                      code: EBADINPUT
                      message: text is required
                wrongMode:
                  value:
                    error:
                      code: EWRONGMODE
                      message: only conversation mode accepts follow-up input
                notRunning:
                  value:
                    error:
                      code: ENOTRUNNING
                      message: run is completed
                stdinClosed:
                  value:
                    error:
                      code: ESTDINCLOSED
                      message: stdin is not writable
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
        '404':
          description: No run with this id (ENOTFOUND) — it never existed or was reaped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ENOTFOUND
                  message: run not found
  /api/run/{id}:
    get:
      tags:
        - Run
      summary: Get a single run
      description: Returns the in-memory handle for one run. Pass `?envelopes=1` to additionally include the bounded (≤500) stream-json envelope replay buffer so a client can re-attach to an in-flight run and see what it missed. Available only while the handle is live or within the 5-minute post-exit reap window — afterwards use GET /api/run/history. Read-only — no process is spawned. The loopback same-origin guard applies.
      operationId: runGet
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Run id (RunHandle.id).
          example: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
        - name: envelopes
          in: query
          required: false
          schema:
            type: string
            enum:
              - '1'
          description: Set to `1` to include the `envelopes` replay buffer in the response.
          example: '1'
      responses:
        '200':
          description: The run handle (with `envelopes` appended when `?envelopes=1`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/RunHandle'
                  - $ref: '#/components/schemas/RunHandleWithEnvelopes'
              example:
                id: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
                pid: 48213
                mode: conversation
                cwd: /Users/dev/projects/my-app
                model: claude-opus-4-8
                permissionMode: acceptEdits
                effort: high
                prompt: Refactor the auth module and add tests.
                argv:
                  - '--output-format'
                  - stream-json
                  - '--verbose'
                  - '--include-partial-messages'
                resumeSessionId: null
                status: running
                startedAt: 1718000000000
                endedAt: null
                exitCode: null
                signal: null
                error: null
                sessionId: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                envelopeCount: 12
                stdoutTail: |
                  {"type":"assistant",...}
                stderrTail: ''
                envelopes:
                  - type: system
                    subtype: init
                    session_id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
                  - type: assistant
                    message:
                      role: assistant
                      content:
                        - type: text
                          text: Working on it...
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
        '404':
          description: No run with this id (ENOTFOUND) — it never existed or was reaped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ENOTFOUND
                  message: run not found
    delete:
      tags:
        - Run
      summary: Kill a run
      description: 'Terminates a live run: sends SIGTERM (escalating to SIGKILL after ~5s if needed), sets the handle status to `killed`, broadcasts a `run_status` WebSocket message, persists the status to `dashboard_runs`, and schedules the handle for the 5-minute reap. Idempotent — returns `{ ok: true }` even if the run had already terminated. The loopback same-origin guard applies.'
      operationId: runKill
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Run id (RunHandle.id).
          example: 3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c
      responses:
        '200':
          description: Kill issued (or run already terminated).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunKillResponse'
              example:
                ok: true
        '403':
          description: 'Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: EBADORIGIN
                  message: cross-origin requests are not allowed
        '404':
          description: No run with this id (ENOTFOUND) — it never existed or was reaped.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: ENOTFOUND
                  message: run not found
  /api/lanes/ensure:
    post:
      tags:
        - Lanes
      summary: Find or adopt the lane owning a working directory
      description: 'Idempotent: returns the lane whose cwd is an exact match or the longest path-boundary parent of `cwd` with `created: false` (200), otherwise creates an `adopted` lane and returns it with `created: true` (201). The Workspace page opens on a directory rather than a lane id, so this is how it gets exactly one lane for that directory. Concurrent calls for the same path resolve to ONE lane: the `lanes.cwd` UNIQUE constraint decides, and the loser re-reads and returns the winner''s lane.'
      operationId: ensureLane
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LaneEnsureRequest'
      responses:
        '200':
          description: An existing lane already owns that cwd.
          content:
            application/json:
              schema:
                type: object
                required:
                  - lane
                  - created
                properties:
                  lane:
                    type: object
                    additionalProperties: true
                  created:
                    type: boolean
                    enum:
                      - false
        '201':
          description: No lane owned that cwd, so an adopted one was created.
          content:
            application/json:
              schema:
                type: object
                required:
                  - lane
                  - created
                properties:
                  lane:
                    type: object
                    additionalProperties: true
                  created:
                    type: boolean
                    enum:
                      - true
        '400':
          description: cwd is missing or not an absolute path (EBADCWD).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The browser request was not same-origin/loopback.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/lanes/worktree:
    post:
      tags:
        - Lanes
      summary: Provision a managed git worktree lane
      description: Validates the absolute source repository, creates a managed lane in `provisioning` state, and returns immediately. Worktree creation continues under the lane lock; the existing `lane_update` broadcast reports either `idle` or `failed` with git stderr in `notes`.
      operationId: createLaneWorktree
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LaneWorktreeCreateRequest'
      responses:
        '202':
          description: Managed lane accepted for background provisioning.
          content:
            application/json:
              schema:
                type: object
                required:
                  - lane
                properties:
                  lane:
                    type: object
                    additionalProperties: true
        '400':
          description: sourceRepo is relative, missing, or not a git repository.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The browser request was not same-origin/loopback.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: The computed worktree directory already belongs to a lane, or no unique directory was available after 50 attempts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/lanes/{id}:
    patch:
      tags:
        - Lanes
      summary: Partially update a lane
      description: Updates lane fields, including run_id; browser requests must pass the loopback same-origin guard. The provisioning-time facts kind, source_repo, slug and base_branch are NOT patchable — kind is check 1 of the destroy guard — and are silently ignored here.
      operationId: updateLane
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Updated lane.
        '400':
          description: kind was not one of adopted|managed (EBADKIND).
        '403':
          description: The browser request was not same-origin/loopback.
        '404':
          description: Lane not found.
  /api/lanes/{id}/preflight:
    get:
      tags:
        - Lanes
      summary: Count facts before a destructive lane action
      operationId: preflightLaneAction
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: action
          in: query
          required: true
          schema:
            type: string
            enum:
              - reset
              - remove
              - purge
      responses:
        '200':
          description: 'Current counted facts for the selected action. reset/remove additionally return blocked[] (hard blockers: adopted, missing, unreadable, unpushed-commits — the only one force overrides) and warnings[] (informational only, e.g. no-remote).'
        '400':
          description: Unknown action.
        '404':
          description: Lane not found.
  /api/lanes/{id}/git:
    get:
      tags:
        - Lanes
      summary: A lane's working-copy facts
      description: Branch, short HEAD, that commit's subject, and the uncommitted counts for the lane's cwd. Read-only, so no same-origin guard. Kept out of GET /api/lanes because it shells out to git three times and that payload is polled and re-broadcast on every lane_update. A cwd that is missing, is not a git repository, or makes git fail returns available:false with HTTP 200 — a lane pointing at a plain directory is a normal state, not a fault.
      operationId: getLaneGitFacts
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: available:true with {branch, head, subject, dirty, untracked}, or available:false alone.
        '404':
          description: Lane not found.
  /api/lanes/{id}/runtime:
    get:
      tags:
        - Lanes
      summary: A lane's own application stack
      description: Slot, ports, per-service liveness, log paths and the last boot error. Recomputed on every call from pid files and port probes rather than cached, because a process can die to OOM or a stray kill without telling anyone. Read-only, so no same-origin guard. Kept out of GET /api/lanes because it opens a socket per declared port and stats every pid file. A lane whose repository declares no .ccam/profile returns available:false with HTTP 200 — most lanes never run a stack, which is a normal state, not a fault.
      operationId: getLaneRuntime
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: available:false (no profile); available:true with provisioned:false (no slot yet); or the full facts with slot, ports, services, up, healthy, steppedAside, logs and lastError.
        '404':
          description: Lane not found.
  /api/lanes/{id}/up:
    post:
      tags:
        - Lanes
      summary: Boot a lane's stack
      description: 'Runs the profile''s boot then health hooks. Returns 202 and finishes in the background because a build can take minutes; progress streams as lane_hook_output and the attempt ends with a lane_runtime message. Does NOT run bootstrap — installing dependencies on every boot would make a routine restart minutes long. A failing health check leaves the processes running, because their logs are what identify the service that never came up. Writes only slot and ports on the lane row: never stage, status or notes.'
      operationId: upLane
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                build:
                  type: boolean
                  default: true
                  description: false passes --no-build to the boot hook, reusing an existing build.
      responses:
        '202':
          description: Accepted; the boot runs in the background.
        '400':
          description: ENOPROFILE — no .ccam/profile, with the paths searched.
        '403':
          description: The browser request was not same-origin/loopback.
        '404':
          description: Lane not found.
        '409':
          description: ESLOTS (every slot taken) or EPORTBUSY (with the occupying pids).
  /api/lanes/{id}/down:
    post:
      tags:
        - Lanes
      summary: Stop a lane's stack
      description: Kills each recorded pid tree bottom-up (killing a parent first reparents its children to init, where nothing knows to look for them), then sweeps listeners on the lane's ports ONLY when a pid file existed — a lane whose stack is already down still owns its port numbers, and an unconditional sweep would kill a server the user started there. Idempotent, and a no-op for a lane that was never up.
      operationId: downLane
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: '{ok, killed: number[], runtime}.'
        '403':
          description: The browser request was not same-origin/loopback.
        '404':
          description: Lane not found.
  /api/lanes/{id}/hook/{name}:
    post:
      tags:
        - Lanes
      summary: Run one of the lane profile's hooks
      description: The surface a driving session uses for ci-gate, e2e, migrate and friends. Returns 202; output streams as lane_hook_output and completion arrives as lane_hook_result with the exit code. The name is checked against a fixed allowlist BEFORE anything is spawned, and args travels as an array of strings straight into argv — neither is ever joined into a command string.
      operationId: runLaneHook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: name
          in: path
          required: true
          schema:
            type: string
            enum:
              - bootstrap
              - boot
              - health
              - migrate
              - seed
              - ci-gate
              - e2e
              - regen
              - db-create
              - db-drop
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                args:
                  type: array
                  items:
                    type: string
      responses:
        '202':
          description: Accepted; the hook runs in the background.
        '400':
          description: ENOHOOK (name outside the allowlist) or ENOPROFILE.
        '403':
          description: The browser request was not same-origin/loopback.
        '404':
          description: Lane not found.
        '409':
          description: ENOSLOT — the lane has no runtime yet; bring it up first.
  /api/lanes/{id}/logs/{svc}:
    get:
      tags:
        - Lanes
      summary: Tail a lane's hook or service log
      description: The resolved path is confined to the lane's log directory after realpath, so a name from the request can never escape it. Read-only, so no same-origin guard.
      operationId: getLaneLog
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: svc
          in: path
          required: true
          schema:
            type: string
        - name: tail
          in: query
          required: false
          schema:
            type: integer
            default: 65536
            maximum: 1048576
          description: Trailing bytes to return; capped at 1 MiB.
      responses:
        '200':
          description: '{available, svc, size, truncated, text}, or {available:false} for a lane with no slot.'
        '404':
          description: Lane not found, or ENOLOG — no such log for this lane.
  /api/lanes/branches:
    get:
      tags:
        - Lanes
      summary: A candidate source repo's local branches
      description: 'Feeds the Add-lane picker: given a repo path, returns its local branches (not origin/* refs — those aren''t checkout-able into a new worktree without a fetch first) plus which one is currently checked out. Same repo validation as POST /api/lanes/worktree; a repo neither endpoint can resolve can''t be provisioned from either. Read-only, no same-origin guard.'
      operationId: listLaneBranches
      parameters:
        - name: repo
          in: query
          required: true
          schema:
            type: string
          description: Absolute path to an existing git repository.
      responses:
        '200':
          description: '{branches: string[], current: string | null}.'
        '400':
          description: repo is missing, relative, does not exist, or is not a git repo.
  /api/lanes/{id}/{action}:
    post:
      tags:
        - Lanes
      summary: Confirm a reset, managed-worktree removal, or session purge
      description: Actions run under the lane lock after waiting for the lane child's actual exit. A failed spawn is already exited because no child started. Reset requires a live managed worktree; remove tears down a managed worktree, prunes git's stale record when the directory was deleted by hand, or only forgets an adopted-lane row without touching its directory. Reset/remove require force when managed work has unpushed commits.
      operationId: runLaneDestructiveAction
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: action
          in: path
          required: true
          schema:
            type: string
            enum:
              - reset
              - remove
              - purge
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LaneDestructiveActionRequest'
      responses:
        '200':
          description: Action completed; purge includes its deleted-row counts.
        '400':
          description: Confirmation or complete expect facts missing, or the managed-worktree guard refused the target.
        '403':
          description: The browser request was not same-origin/loopback.
        '409':
          description: Preflight facts changed (ESTALE) or force is required (EUNPUSHED).
        '500':
          description: Git, run-exit timeout, or internal failure; git failures include error.stderr.
  /api/push/vapid-public-key:
    get:
      tags:
        - Push
      summary: Get the VAPID public key
      description: Returns the server's VAPID public application server key so a browser can register a Web Push subscription via `PushManager.subscribe({ applicationServerKey })`. The key pair is generated once and persisted in the shared data directory alongside the SQLite DB, so the web app and native apps reuse a single key pair across restarts. No authentication — this is a local-first dashboard. Safe to call repeatedly; always returns the same key.
      operationId: pushVapidPublicKey
      responses:
        '200':
          description: The VAPID public key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushVapidKeyResponse'
              example:
                publicKey: BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8
  /api/push/subscribe:
    post:
      tags:
        - Push
      summary: Register a Web Push subscription
      description: 'Stores a browser PushSubscription so future `/api/push/send` broadcasts reach this endpoint. Persisted with `INSERT OR REPLACE INTO push_subscriptions (endpoint, p256dh, auth)`, keyed on `endpoint` — so the operation is idempotent: re-subscribing the same endpoint overwrites its keys instead of creating a duplicate. No authentication (local-first). Requires `endpoint`, `keys.p256dh`, and `keys.auth`; any missing field returns 400.'
      operationId: pushSubscribe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PushSubscribeRequest'
            example:
              endpoint: https://fcm.googleapis.com/fcm/send/dGhpcy1pcy1hLWZha2UtZW5kcG9pbnQ
              keys:
                p256dh: BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM
                auth: tBHItJI5svbpez7KI4CCXg
      responses:
        '200':
          description: Subscription stored (created or overwritten)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushSubscribeResponse'
              example:
                ok: true
        '400':
          description: Missing required fields (one of `endpoint`, `keys.p256dh`, `keys.auth` was absent)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: Missing required fields
    delete:
      tags:
        - Push
      summary: Remove a Web Push subscription
      description: 'Deletes a stored subscription so it stops receiving broadcasts. The endpoint identifier is supplied in the request BODY (a DELETE with a JSON body), NOT as a query parameter. Idempotent — deleting an endpoint that is not stored still returns `{ ok: true }`. No authentication (local-first). A missing `endpoint` returns 400.'
      operationId: pushUnsubscribe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PushUnsubscribeRequest'
            example:
              endpoint: https://fcm.googleapis.com/fcm/send/dGhpcy1pcy1hLWZha2UtZW5kcG9pbnQ
      responses:
        '200':
          description: Subscription removed (or no-op if it was not stored)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushOkResponse'
              example:
                ok: true
        '400':
          description: Missing endpoint in the request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: Missing endpoint
  /api/push/send:
    post:
      tags:
        - Push
      summary: Broadcast a notification to all surfaces
      description: 'Dispatches a notification to every reachable surface at once: it fires a native OS notification via Electron''s main-process Notification API when the server is hosted inside the desktop app, AND sends an encrypted Web Push delivery to every stored subscription. Both legs run unconditionally so whichever surface the user is on receives the alert — under `npm start` the native leg is a no-op, and under the desktop app the Web Push leg is typically a no-op (Electron has no FCM credentials, so `push_subscriptions` is empty). Subscriptions rejected with HTTP 410 (Gone) are pruned from `push_subscriptions` during the request. The response reports `{ native, pushed, failed }` so the caller can tell a real delivery from a silent no-op. No authentication (local-first). A missing `title` or `body` returns 400; an unexpected dispatch error returns 500.'
      operationId: pushSend
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PushSendRequest'
            example:
              title: Session completed
              body: Your Claude Code session finished with 3 subagents.
      responses:
        '200':
          description: Dispatch ran; the body reports which surfaces fired and how many Web Push deliveries succeeded/failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushSendResponse'
              example:
                ok: true
                native: false
                pushed: 2
                failed: 1
        '400':
          description: Missing title or body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: Missing title or body
        '500':
          description: Dispatch error while broadcasting the notification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageErrorResponse'
              example:
                error:
                  message: Push service unavailable
  /api/sessions/facets:
    get:
      tags:
        - Sessions
      summary: List session facet values
      description: 'Returns the distinct facet values for the Sessions page filters: the non-empty working directories (the `cwd` column, sorted ascending) in `cwds`, and the distinct data-source ids (the `sessions.source` column) in `sources`. `sources` always includes the built-in `local` history plus any configured remote SSH machines. Always returns a 200 with (possibly empty) arrays.'
      operationId: listSessionFacets
      responses:
        '200':
          description: Distinct session working directories and data-source ids
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionsFacetsResponse'
              example:
                cwds:
                  - /Users/son/WebstormProjects/Claude-Code-Agent-Monitor
                  - /Users/son/code/another-project
                sources:
                  - local
                  - 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
  /api/remote-sources:
    get:
      tags:
        - Remote Sources
      summary: List remote data sources
      description: Returns every configured remote SSH source the dashboard pulls Claude Code history from. NO secrets are ever returned — these records store none (SSH auth defers to the host SSH stack). Each entry carries its last sync `status`, `last_error`, `last_sync_at`, and `last_sync_counts`. Read-only; always 200.
      operationId: listRemoteSources
      responses:
        '200':
          description: All configured remote sources
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteSourceListResponse'
              example:
                sources:
                  - id: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
                    label: Work laptop
                    host: son@studio.local
                    ssh_port: 22
                    identity_file: ~/.ssh/id_ed25519
                    remote_home: ~/.claude
                    enabled: true
                    status: ok
                    last_error: null
                    last_sync_at: '2026-07-22T18:41:55.117Z'
                    last_sync_counts:
                      imported: 9
                      skipped: 41
                      backfilled: 0
                      errors: 0
                      sessions_seen: 50
                      sessions_tagged: 50
                    created_at: '2026-07-20T09:15:00.000Z'
                    updated_at: '2026-07-22T18:41:55.117Z'
    post:
      tags:
        - Remote Sources
      summary: Register a remote data source
      description: 'Registers a remote SSH source. `label` and `host` are required; `host` is an SSH destination (`user@host`) or a `~/.ssh/config` alias. Optional `ssh_port`, `identity_file`, `remote_home`, and `enabled` fine-tune the connection. No credentials are accepted or stored — auth defers to the host SSH stack. Returns the created source (201). Validation failures return 400 `{ error: { code, message } }` with one of the codes INVALID_LABEL, INVALID_HOST, INVALID_PORT, INVALID_IDENTITY_FILE, INVALID_REMOTE_HOME.'
      operationId: createRemoteSource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoteSourceCreateRequest'
            examples:
              minimal:
                summary: Minimal — label + SSH config alias
                value:
                  label: Work laptop
                  host: studio
              full:
                summary: Full — explicit port, key, and remote home
                value:
                  label: Work laptop
                  host: son@studio.local
                  ssh_port: 22
                  identity_file: ~/.ssh/id_ed25519
                  remote_home: ~/.claude
                  enabled: true
      responses:
        '201':
          description: Remote source created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteSourceResponse'
              example:
                source:
                  id: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
                  label: Work laptop
                  host: son@studio.local
                  ssh_port: 22
                  identity_file: ~/.ssh/id_ed25519
                  remote_home: ~/.claude
                  enabled: true
                  status: idle
                  last_error: null
                  last_sync_at: null
                  last_sync_counts: null
                  created_at: '2026-07-22T18:41:55.117Z'
                  updated_at: '2026-07-22T18:41:55.117Z'
        '400':
          description: 'Validation error (codes: INVALID_LABEL, INVALID_HOST, INVALID_PORT, INVALID_IDENTITY_FILE, INVALID_REMOTE_HOME).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                label:
                  summary: Missing/blank label
                  value:
                    error:
                      code: INVALID_LABEL
                      message: '`label` is required'
                host:
                  summary: Missing/invalid host
                  value:
                    error:
                      code: INVALID_HOST
                      message: '`host` is required'
                port:
                  summary: Port out of range
                  value:
                    error:
                      code: INVALID_PORT
                      message: '`ssh_port` must be 1–65535'
  /api/remote-sources/{id}:
    patch:
      tags:
        - Remote Sources
      summary: Update a remote data source (partial)
      description: Partially updates a remote source. Only the keys present in the body are changed; omitted keys are left as-is. The same validation as create applies to any field that is present. Returns the updated source, or 404 when the id is unknown.
      operationId: updateRemoteSource
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Remote source id.
          example: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoteSourceUpdateRequest'
            examples:
              disable:
                summary: Disable a source without touching anything else
                value:
                  enabled: false
              rename:
                summary: Rename and change port
                value:
                  label: Studio Mac
                  ssh_port: 2222
      responses:
        '200':
          description: Updated remote source
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteSourceResponse'
              example:
                source:
                  id: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
                  label: Studio Mac
                  host: son@studio.local
                  ssh_port: 2222
                  identity_file: ~/.ssh/id_ed25519
                  remote_home: ~/.claude
                  enabled: false
                  status: ok
                  last_error: null
                  last_sync_at: '2026-07-22T18:41:55.117Z'
                  last_sync_counts:
                    imported: 9
                    skipped: 41
                    backfilled: 0
                    errors: 0
                    sessions_seen: 50
                    sessions_tagged: 50
                  created_at: '2026-07-20T09:15:00.000Z'
                  updated_at: '2026-07-22T19:02:10.400Z'
        '400':
          description: 'Validation error (codes: INVALID_LABEL, INVALID_HOST, INVALID_PORT, INVALID_IDENTITY_FILE, INVALID_REMOTE_HOME).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_PORT
                  message: '`ssh_port` must be 1–65535'
        '404':
          description: Remote source not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Remote source not found
    delete:
      tags:
        - Remote Sources
      summary: Delete a remote data source
      description: Deletes a remote source. By default its imported sessions are DETACHED — reassigned to the built-in `local` source — so history is preserved. Pass `?purge=true` to instead permanently DELETE that source's imported sessions along with the source. The response reports whether a purge occurred.
      operationId: deleteRemoteSource
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Remote source id.
          example: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
        - name: purge
          in: query
          required: false
          schema:
            type: boolean
            default: false
          description: When true, also delete this source's imported sessions. When false/omitted, those sessions are reattached to `local`.
          example: true
      responses:
        '200':
          description: Deleted (with the purge outcome)
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                  - purged
                properties:
                  ok:
                    type: boolean
                    example: true
                  purged:
                    type: boolean
                    description: True when the source's imported sessions were deleted (purge=true); false when they were detached to `local`.
                    example: false
              examples:
                detached:
                  summary: Default — sessions detached to local
                  value:
                    ok: true
                    purged: false
                purged:
                  summary: purge=true — sessions deleted
                  value:
                    ok: true
                    purged: true
        '404':
          description: Remote source not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Remote source not found
  /api/remote-sources/{id}/test:
    post:
      tags:
        - Remote Sources
      summary: Probe SSH connectivity to a remote source
      description: Runs an SSH connectivity probe against the source and reports the outcome synchronously. The `ok` flag carries the probe result and `message` is a human-readable summary; on success `remoteProjects` may list the remote project directories discovered under the remote Claude home. This does not import anything — use POST /{id}/sync to pull.
      operationId: testRemoteSource
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Remote source id.
          example: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
      responses:
        '200':
          description: Probe result (ok flag carries the connectivity outcome)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteSourceTestResponse'
              examples:
                success:
                  summary: Reachable
                  value:
                    ok: true
                    message: Connected; found 24 project directories under ~/.claude/projects.
                    remoteProjects:
                      - '-Users-son-code-foo'
                      - '-Users-son-code-bar'
                failure:
                  summary: Unreachable / auth failed
                  value:
                    ok: false
                    message: 'ssh: connect to host studio.local port 22: Connection refused'
        '404':
          description: Remote source not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Remote source not found
  /api/remote-sources/{id}/sync:
    post:
      tags:
        - Remote Sources
      summary: Pull Claude Code history from a remote source now
      description: Triggers an immediate pull of Claude Code history from the remote source over SSH, importing new transcripts through the same idempotent, baseline-preserving pipeline used for local imports and tagging imported sessions with this source's id. The response reports the per-run counters. Sync progress/completion is also broadcast over the WebSocket as `remote_source.status` frames.
      operationId: syncRemoteSource
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Remote source id.
          example: 4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
      responses:
        '200':
          description: Sync result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteSourceSyncResponse'
              example:
                ok: true
                imported: 9
                skipped: 41
                backfilled: 0
                errors: 0
                sessions_seen: 50
                sessions_tagged: 50
        '404':
          description: Remote source not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: NOT_FOUND
                  message: Remote source not found
        '500':
          description: Sync failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: SYNC_FAILED
                  message: ssh exited with code 255
  /api/remote-sources/sync-all:
    post:
      tags:
        - Remote Sources
      summary: Sync all enabled remote sources now
      description: Pulls Claude Code history from every enabled remote source over SSH, sequentially (one connection at a time). Per-source failures are isolated — one unreachable source never aborts the others — and each outcome is returned in `results`. Progress/completion is also broadcast over the WebSocket as `remote_source.status` frames. Always returns 200.
      operationId: syncAllRemoteSources
      responses:
        '200':
          description: Per-source sync outcomes
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  synced:
                    type: integer
                    description: Number of enabled sources that were attempted.
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        ok:
                          type: boolean
                        error:
                          type: string
              example:
                ok: true
                synced: 2
                results:
                  - id: src_a
                    ok: true
  /api/settings/claude-home:
    get:
      tags:
        - Settings
      summary: Get the active Claude Code home directory
      description: Returns the Claude Code home directory the dashboard uses to locate transcripts and settings. Resolves to the CLAUDE_HOME environment variable when set, otherwise `<homedir>/.claude`. Always returns 200.
      operationId: getClaudeHome
      responses:
        '200':
          description: Current Claude Code home directory
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SettingsClaudeHomeResponse'
              example:
                claude_home: /Users/son/.claude
    put:
      tags:
        - Settings
      summary: Update the Claude Code home directory
      description: Changes the Claude Code home directory used for transcript/settings discovery. A leading `~/` in `path` is expanded to the OS home directory; the resolved value must be an absolute path that exists and is a directory. On success the new value is applied to process.env immediately (so subsequent reads use it) and persisted to the project `.env` file. Returns 400 INVALID_PATH when `path` is missing/not a string, or when the resolved path is not absolute, does not exist, or is not a directory.
      operationId: updateClaudeHome
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SettingsClaudeHomeUpdateRequest'
            example:
              path: ~/.codefuse/engine/cc
      responses:
        '200':
          description: Claude Code home updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SettingsClaudeHomeUpdateResponse'
              example:
                ok: true
                claude_home: /Users/son/.codefuse/engine/cc
        '400':
          description: Invalid path — `path` missing or not a string, or the resolved path is not absolute / does not exist / is not a directory (code INVALID_PATH).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: INVALID_PATH
                  message: 'Directory does not exist: /Users/son/.codefuse/engine/cc'
  /api/workflows/runs:
    get:
      tags:
        - Workflows
      summary: List Workflow-tool runs
      description: Returns a paginated list of Workflow-tool runs (fleets of sub-agents spawned by the Claude Code 'Workflow' tool / self-paced /loop), newest first. Filter by `status` (the literal `all` is treated as no filter) or by `session_id`; `session_id` takes precedence over `status` when both are supplied. `counts` is always the whole-table breakdown by status. JSON-blob columns (`phases`, `progress`) are parsed into arrays in each run.
      operationId: listWorkflowRuns
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 50
          description: Page size, clamped to 1–1000 (default 50).
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Pagination offset (clamped to >= 0).
        - name: status
          in: query
          required: false
          schema:
            type: string
          description: Filter by run status (open string, e.g. running | completed | error | failed). The literal value `all` is treated as no filter.
        - name: session_id
          in: query
          required: false
          schema:
            type: string
          description: Filter to runs launched by this session. Takes precedence over `status` when both are provided.
      responses:
        '200':
          description: Paginated list of workflow runs with status counts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowRunsListResponse'
              example:
                runs:
                  - run_id: wf_a1b2c3d4
                    session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    task_id: ISSUE-167
                    name: Refactor pricing engine
                    status: completed
                    default_model: claude-opus-4-8
                    started_at: '2026-06-25T18:04:11.122Z'
                    ended_at: '2026-06-25T18:09:47.530Z'
                    duration_ms: 336408
                    agent_count: 6
                    total_tokens: 1284750
                    total_tool_calls: 412
                    phases:
                      - name: plan
                        status: completed
                    progress:
                      - step: 1
                        label: scaffold
                        done: true
                    script_path: null
                    journal_path: /Users/son/.claude/projects/-Users-son-code/5f3c0e2a/workflows/wf_a1b2c3d4.json
                    source: journal
                    created_at: '2026-06-25T18:09:48.001Z'
                    updated_at: '2026-06-25T18:09:48.001Z'
                total: 42
                counts:
                  completed: 30
                  error: 5
                  running: 7
                limit: 50
                offset: 0
        '500':
          description: Failed to list workflow runs (code WORKFLOW_LIST_FAILED).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: WORKFLOW_LIST_FAILED
                  message: 'no such table: workflows'
  /api/workflows/runs/{runId}:
    get:
      tags:
        - Workflows
      summary: Get a Workflow-tool run with its agents and events
      description: Returns one Workflow-tool run (by `run_id`) together with its linked inner agents and the events attributed to those agents (chronological, capped at 5000 rows). The run's JSON-blob columns (`phases`, `progress`) are parsed into arrays. Returns 404 WORKFLOW_NOT_FOUND when no run matches the id.
      operationId: getWorkflowRun
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
          description: The workflow run id (workflows.run_id).
      responses:
        '200':
          description: Workflow run with inner agents and their events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowRunDetailResponse'
              example:
                workflow:
                  run_id: wf_a1b2c3d4
                  session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                  task_id: ISSUE-167
                  name: Refactor pricing engine
                  status: completed
                  default_model: claude-opus-4-8
                  started_at: '2026-06-25T18:04:11.122Z'
                  ended_at: '2026-06-25T18:09:47.530Z'
                  duration_ms: 336408
                  agent_count: 6
                  total_tokens: 1284750
                  total_tool_calls: 412
                  phases:
                    - name: plan
                      status: completed
                  progress:
                    - step: 1
                      label: scaffold
                      done: true
                  script_path: null
                  journal_path: /Users/son/.claude/projects/-Users-son-code/5f3c0e2a/workflows/wf_a1b2c3d4.json
                  source: journal
                  created_at: '2026-06-25T18:09:48.001Z'
                  updated_at: '2026-06-25T18:09:48.001Z'
                agents:
                  - id: agent-7f1c
                    session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    name: implementer
                    type: subagent
                    subagent_type: general-purpose
                    status: completed
                    task: Implement pricing changes
                    current_tool: null
                    started_at: '2026-06-25T18:04:30.000Z'
                    ended_at: '2026-06-25T18:08:12.000Z'
                    parent_agent_id: null
                    metadata: null
                    updated_at: '2026-06-25T18:08:12.000Z'
                    awaiting_input_since: null
                    awaiting_reason: null
                events:
                  - id: 90211
                    session_id: 5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11
                    agent_id: agent-7f1c
                    event_type: PostToolUse
                    tool_name: Edit
                    summary: Edited server/routes/pricing.js
                    data: null
                    created_at: '2026-06-25T18:05:02.144Z'
        '404':
          description: No workflow run matches the id (code WORKFLOW_NOT_FOUND).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: WORKFLOW_NOT_FOUND
                  message: Workflow run not found
        '500':
          description: Failed to load workflow run detail (code WORKFLOW_DETAIL_FAILED).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: WORKFLOW_DETAIL_FAILED
                  message: database is locked
  /api/settings/import:
    post:
      tags:
        - Settings
      summary: Restore (import) a previously exported data bundle
      description: 'Restores a bundle produced by GET /api/settings/export. Supply it either as `multipart/form-data` with a single `file` field (browser upload) or as a JSON body `{ "path": "<absolute path>" }` (the server reads the file from disk — used by the `ccam import-data` CLI, and it also sidesteps the global 1 MB JSON body cap for large bundles). The restore is idempotent and NON-DESTRUCTIVE: it is session-atomic, so a session already present (matched by its UUID) is skipped whole together with its agents/events/token_usage/workflows, and independent config rows (dashboard_runs, alert_rules, model_pricing) are inserted only when absent. Nothing existing is overwritten — ideal for consolidating several machines into one dashboard. The response reports per-table counts.'
      operationId: importData
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The export .json file.
          application/json:
            schema:
              type: object
              required:
                - path
              properties:
                path:
                  type: string
                  description: Absolute path (server-side) to an export .json file. `~` is expanded.
            example:
              path: /Users/you/ccam-export-2026-06-26.json
      responses:
        '200':
          description: Per-table restore counts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportResponse'
              example:
                ok: true
                source: agent-monitor-export-2026-06-26.json
                format: ccam-export
                sessions_imported: 42
                sessions_skipped: 3
                agents: 96
                events: 5120
                token_usage: 42
                workflows: 4
                dashboard_runs: 11
                alert_rules: 2
                model_pricing: 0
                errors: 0
        '400':
          description: Missing file, invalid JSON, or unrecognized export format
x-issues-url: https://git.smartgift.io.vn/Smartgift-AI/Claude-Code-Monitor/issues
