Cat Factory
Home
Get Started
GitHub
Home
Get Started
GitHub
  • Start

    • Introduction
    • Core Concepts
    • Quick Start
    • Tutorial: Your First Task to a Merged Pull Request
  • Guides

    • Recipes

      • Cookbook
    • Plan the work

      • Design Your Board
      • Clarify Requirements
      • Author a Document
      • Plan an Initiative
    • Run pipelines

      • Choose and Edit a Pipeline
      • Run a Pipeline
      • Schedule Recurring Work
      • Review and Merge Pull Requests
      • Control Spend with Budgets
    • Connect

      • Connect a Repository
      • Connect Issue & Document Sources
      • Feed Design Context to Agents
      • Preview and Test a Frontend
    • Models & prompts

      • Connect a Model Provider
      • Apply Standards with Prompt Fragments
      • Run a Claude Skill as a Step
      • Compare Prompts and Models in the Sandbox
    • Collaborate

      • Invite and Manage Your Team
      • Share Services Across Workspaces
      • Register Foundational Services
  • Deploy

    • Run Locally
    • Deploy to Node.js
    • Deploy to Cloudflare
    • Deploy on Kubernetes
    • Lay Out a Kubernetes Cluster
    • Set Up a Local Kubernetes Cluster on Windows
    • Register the GitHub App
    • Set Up Enterprise SSO
    • Set Up Your Deployment Repository
    • Configuration
  • Operate

    • Observability
    • Set Up Notifications
    • Run Jobs on Your Own Runners
    • Provision Ephemeral Environments
    • Debug a Run from Outside the Browser
    • Troubleshooting
    • Upgrades & Data Retention
  • Extend

    • Add a Custom Agent Kind
    • Add a Custom Gate or Judge
    • Add a Custom Provider
    • Extend the App with Frontend Modules
    • Integration Manifests
    • Give Agents External Tools (MCP)
    • Package a Reusable Operation
    • Register an Initiative Preset
    • Public API
    • Official SDKs
    • MCP Server
    • Cloudflare OS Gatekeeper
  • Reference

    • Architecture
    • Agent Isolation Model
    • Security Model & Hardening
    • Packages & Repository Layout
    • GitHub and GitLab Support Matrix
    • Environment Variables
    • API Endpoint Reference
    • Glossary

Official SDKs

Cat Factory publishes clients for the public API in four languages. They cover the same operations, decode the same wire shapes, and are generated from the same OpenAPI spec the deployment validates against, so a client cannot describe a request the server would reject.

LanguagePackageRegistry
TypeScript@cat-factory/sdknpm
Pythoncat-factory-sdkPyPI
Gogithub.com/kibertoad/cat-factory/sdk/gothe Go module proxy
Java and Kotlinai.catfactory:cat-factory-sdkMaven Central

Two projections sit beside them rather than being a fifth client: the MCP server, which exposes the same operations as tools to an MCP host, and the Gatekeeper bindings, a policy-annotated operation table for credential-holding front-ends. The Cloudflare OS Gatekeeper is built on that table.

Getting a key

Every client authenticates with a public-API key minted from the deployment, and the key is what decides what the caller can do. Scopes are a ladder: read ⊂ write ⊂ decide ⊂ admin. Mint the narrowest one that does the job, and remember that every key is scoped to one workspace. See Public API → Setup.

import { CatFactoryClient } from '@cat-factory/sdk'

const client = new CatFactoryClient({
  baseUrl: 'https://cat-factory.example.com',
  apiKey: process.env.CAT_FACTORY_API_KEY,
})

const { services } = await client.services.list()
const task = await client.tasks.create(services[0].serviceId, {
  title: 'Add a health check',
  description: 'Expose /health returning build metadata.',
})
const run = await client.tasks.start(task.taskId)

What every client guarantees

These are invariants each SDK implements in its own idiom. All of them are about being honest with the caller rather than convenient:

  • Absent is not null. A field that may not be sent and a field that is sent holding null are kept apart everywhere. Collapsing them turns "leave this alone" into "clear it" on a request, and "the server had no value" into "the server said nothing" on a response.
  • An unknown value never raises. The API is additive forever, so a deployment will send enum values and object fields an older client has never heard of. Every client decodes them anyway; otherwise each additive server release would be an outage for anyone who has not upgraded.
  • The error class comes from the HTTP status; the cause comes from code. The status picks the exception type, and code is exposed verbatim as a plain string rather than narrowed to a closed enum, because new codes appear without a major version.
  • Only idempotent requests are retried. Starting a run costs real model work, and a transport failure with no response says nothing about whether the server acted.
  • Streams are never auto-reconnected. A reconnect replays the event stream from its start, and only you know which events you have already acted on.
  • The client deadline bounds the response, not a stream. On an ordinary request it covers the whole exchange. On an event stream it stops once the headers arrive, because a run parked on a human decision waits indefinitely by design: a quiet stream is the normal state of a healthy one.
  • Pagination ends on the cursor, not on an empty page. A keyset page may legitimately arrive empty with a cursor still set, so every client follows the cursor and raises if the server hands back the one it was just given.
  • No dependencies you did not ask for. Python and Go have none, TypeScript uses fetch, and Java has exactly one (Jackson) plus compile-time annotations.

Kotlin

There is no separate Kotlin SDK, and that is a decision rather than an omission. One artifact serves both languages: the Java client is annotated so Kotlin reads real nullability off it rather than falling back to platform types, keywords that collide (public) are escaped while keeping the wire name, models are built with builders rather than telescoping constructors, the error hierarchy is sealed so a Kotlin when is exhaustive with no else, and enums tolerate values a newer deployment sends.

val client = CatFactoryClient.builder()
    .baseUrl("https://cat-factory.example.com")
    .apiKey(System.getenv("CAT_FACTORY_API_KEY"))
    .build()

val service = client.services().list().services.first()
val pr: String? = client.tasks().get(taskId).pullRequestUrl  // nullable, and the compiler knows

What a Kotlin caller does not get, stated plainly: the models are Java records, so there is no copy() and no destructuring, and named arguments do not work on Java methods. Both are cosmetic beside null-safety.

Pointing a client at localhost or a mock

baseUrl takes any origin and no client validates the scheme, so a local deployment, a recorded fixture server, or a WireMock/MSW double all work.

LanguageBase URLInjecting your own transport
TypeScriptnew CatFactoryClient({ baseUrl, apiKey })fetch:
PythonCatFactoryClient(base_url=…, api_key=…)opener=
Gocatfactory.New(Options{BaseURL: …})HTTPClient:
JavaCatFactoryClient.builder().baseUrl(…).httpClient(…)

Two things to know when the target is a local mock:

  • The Java client drops to HTTP/1.1 for cleartext origins on purpose. Its default is HTTP/2, which over plain HTTP sends an upgrade header on every request, and a mock that does not speak it is entitled to reject the request. Over HTTPS the negotiation is ALPN, so HTTP/2 is kept.
  • The key is never inspected client-side, so a mock needs no real key: any non-empty string works. Only that baseUrl and apiKey are non-empty is validated, and only to fail early rather than send an unauthenticated request.

How they stay correct

Models and operation methods are generated from the OpenAPI spec, which is itself generated from the route contracts the deployment validates against. Each client's transport, error hierarchy, retry policy, pagination helper, and event-stream reader are hand-written and live beside the generated files, which is what keeps a contract change from rewriting behaviour.

The API Endpoint Reference is rendered from that same spec, so what it says a field is called and what your client calls it are the same fact rendered twice, never two claims that can disagree.

A cross-client smoketest drives the same scenario through all four against one real backend and compares their reports field by field, so one language decoding a field differently, mapping a refusal to the wrong class, or paginating one page short fails a test rather than shipping.

The contributor-facing account of that chain (regenerating, adding an endpoint, the release mechanics) lives in the code repository: sdk/README.md.


Next: the Public API reference for the operations themselves, or the MCP Server to give an MCP host the same surface.

Edit this page on GitHub
Last Updated: 8/10/26, 9:53 AM
Prev
Public API
Next
MCP Server