packages feed

opencode-0.1.0.0: src/OpenCode/Client.hs

{- |
Module      : OpenCode.Client
Description : HTTP client for OpenCode server API
Copyright   : (c) 2026 Sridhar Ratnakumar
License     : MIT
Maintainer  : srid@srid.ca
Stability   : experimental

This module provides the HTTP client for interacting with the OpenCode server.
-}
module OpenCode.Client (
  OpenCodeClient (..),
  mkClient,
)
where

import Network.HTTP.Client (defaultManagerSettings, newManager)
import Servant.API (Capture, Delete, Get, JSON, Post, QueryParam, ReqBody, (:<|>) (..), type (:>))
import Servant.Client (BaseUrl (..), ClientEnv, ClientError, Scheme (..), client, mkClientEnv, runClientM)

import OpenCode.Types

-- | Health check endpoint.
type HealthAPI = "global" :> "health" :> Get '[JSON] Health

-- | Sub-routes for a specific session ID.
type SessionMemberAPI =
  Get '[JSON] Session
    :<|> Delete '[JSON] Bool
    :<|> "message" :> QueryParam "directory" FilePath :> ReqBody '[JSON] MessageInput :> Post '[JSON] MessageResponse

-- | Grouped Session routes.
type SessionAPI =
  QueryParam "directory" FilePath :> Get '[JSON] [Session]
    :<|> QueryParam "directory" FilePath :> ReqBody '[JSON] SessionCreateInput :> Post '[JSON] Session
    :<|> Capture "sessionID" SessionID :> QueryParam "directory" FilePath :> SessionMemberAPI

-- | Grouped Project routes.
type ProjectAPI =
  Get '[JSON] [Project]
    :<|> "current" :> Get '[JSON] Project

-- | Server configuration endpoint.
type ConfigAPI = "config" :> Get '[JSON] Config

-- | Provider listing endpoint.
type ProviderAPI = "provider" :> Get '[JSON] ProvidersResponse

-- | Combined OpenCode API.
type OpenCodeAPI =
  HealthAPI
    :<|> "session" :> SessionAPI
    :<|> "project" :> ProjectAPI
    :<|> ConfigAPI
    :<|> ProviderAPI

apiProxy :: Proxy OpenCodeAPI
apiProxy = Proxy

{- | A client for interacting with the OpenCode server API.

Create a client using 'mkClient', then use the record fields to make API calls.

>>> client <- mkClient "localhost" 4096
>>> getHealth client
Right (Health {healthy = True})
-}
data OpenCodeClient = OpenCodeClient
  { getHealth :: IO (Either ClientError Health)
  -- ^ Check if the server is healthy. Calls @GET \/global\/health@.
  , listSessions :: Maybe FilePath -> IO (Either ClientError [Session])
  {- ^ List all sessions.

  The @FilePath@ argument filters by working directory context.
  Pass 'Nothing' to list all sessions regardless of directory.
  -}
  , createSession :: Maybe FilePath -> SessionCreateInput -> IO (Either ClientError Session)
  {- ^ Create a new session.

  The @FilePath@ argument sets the working directory context for the session.
  This determines which project/files the LLM will operate on.
  Pass 'Nothing' to use the server's current directory.
  -}
  , getSession :: SessionID -> Maybe FilePath -> IO (Either ClientError Session)
  -- ^ Get a session by ID. The @FilePath@ is the working directory context.
  , deleteSession :: SessionID -> Maybe FilePath -> IO (Either ClientError Bool)
  -- ^ Delete a session by ID. The @FilePath@ is the working directory context.
  , sendMessage :: SessionID -> Maybe FilePath -> MessageInput -> IO (Either ClientError MessageResponse)
  {- ^ Send a message to a session.

  The @FilePath@ argument is the working directory context for the request.
  Pass 'Nothing' to use the session's existing directory.
  -}
  , listProjects :: IO (Either ClientError [Project])
  -- ^ List all projects. Calls @GET \/project@.
  , getCurrentProject :: IO (Either ClientError Project)
  -- ^ Get the current project. Calls @GET \/project\/current@.
  , getConfig :: IO (Either ClientError Config)
  -- ^ Get the server configuration. Calls @GET \/config@.
  , listProviders :: IO (Either ClientError ProvidersResponse)
  -- ^ List all AI providers. Calls @GET \/provider@.
  , clientEnv :: ClientEnv
  -- ^ The underlying servant client environment (for advanced usage).
  }

{- | Create a new OpenCode client.

>>> client <- mkClient "localhost" 4096
>>> getHealth client
Right (Health {healthy = True})

The client uses HTTP (not HTTPS). For HTTPS, you would need to modify
the client to use 'tlsManagerSettings' and 'Https' scheme.
-}
mkClient :: Text -> Int -> IO OpenCodeClient
mkClient host port = do
  manager <- newManager defaultManagerSettings
  let baseUrl = BaseUrl Http (toString host) port ""
      env = mkClientEnv manager baseUrl
      ( healthH
          :<|> ( sessionListH
                   :<|> sessionCreateH
                   :<|> sessionMemberH
                 )
          :<|> ( projectListH
                   :<|> projectCurrentH
                 )
          :<|> configH
          :<|> providerH
        ) = client apiProxy
  pure
    OpenCodeClient
      { getHealth = runClientM healthH env
      , listSessions = \dir -> runClientM (sessionListH dir) env
      , createSession = \dir input -> runClientM (sessionCreateH dir input) env
      , getSession = \sid dir ->
          let (getH :<|> _ :<|> _) = sessionMemberH sid dir
           in runClientM getH env
      , deleteSession = \sid dir ->
          let (_ :<|> deleteH :<|> _) = sessionMemberH sid dir
           in runClientM deleteH env
      , sendMessage = \sid dir input ->
          let (_ :<|> _ :<|> msgH) = sessionMemberH sid dir
           in runClientM (msgH dir input) env
      , listProjects = runClientM projectListH env
      , getCurrentProject = runClientM projectCurrentH env
      , getConfig = runClientM configH env
      , listProviders = runClientM providerH env
      , clientEnv = env
      }