clawdforge/clients/kotlin
Kayos cc54cfbe6c clients/kotlin: initial Kotlin SDK for clawdforge
Async Kotlin/JVM client built on Ktor + kotlinx.serialization. Every I/O
method is a `suspend` function; the client is `Closeable` for `use { }`.
Sealed `ForgeException` hierarchy enables exhaustive `when` over auth,
run-failure, generic-API, and transport errors. Models use `@SerialName`
to bridge idiomatic camelCase Kotlin properties to the snake_case wire
format. `RunResult.result` is a `JsonElement` so callers can narrow with
the standard `kotlinx.serialization.json` extensions.

- Kotlin 1.9.25 / JVM 17 toolchain
- Ktor 2.3.12 client (CIO engine; pluggable via ForgeOptions.engine)
- kotlinx-serialization 1.6.3, kotlinx-coroutines 1.8.1
- 14 tests (JUnit 5 + Ktor MockEngine), all green
- `./gradlew build` clean, `publishToMavenLocal` works
- MIT license declared in publishing block

Mirrors the surface of the Go and Rust SDKs (healthz, run, uploadFile,
admin tokens CRUD).
2026-04-28 23:04:24 -07:00
..
examples/main/kotlin clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
gradle/wrapper clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
src clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
.gitignore clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
build.gradle.kts clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
gradle.properties clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
gradlew clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
gradlew.bat clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
README.md clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00
settings.gradle.kts clients/kotlin: initial Kotlin SDK for clawdforge 2026-04-28 23:04:24 -07:00

clawdforge-kotlin

Async Kotlin client for the clawdforge HTTP service — a LAN-only bearer-token-gated REST wrapper around claude -p subprocess calls.

  • Kotlin 1.9+, JVM 17 target
  • Coroutines — every I/O method is suspend. No blocking variants.
  • Ktor Client (CIO engine) for HTTP, kotlinx.serialization for JSON
  • ForgeClient is Closeable and works inside use { }

Install

The jar is published to your local Maven repo; consume it from any JVM build.

./gradlew publishToMavenLocal

Then in the consumer's build.gradle.kts:

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    implementation("com.clawdforge:clawdforge:0.1.0")
}

You'll also pull Ktor + kotlinx-serialization transitively (they're on the public API surface — RunResult.result is a JsonElement).

Quickstart

import com.clawdforge.ForgeClient
import com.clawdforge.RunRequest
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.nio.file.Path

suspend fun main() {
    ForgeClient(
        baseUrl = "http://localhost:8800",
        token   = System.getenv("CLAWDFORGE_TOKEN"),
    ).use { client ->

        val h = client.healthz()
        println(h.claudeVersion)

        val res = client.run(RunRequest(
            prompt      = """Reply with JSON: {"hello": "world"}""",
            model       = "sonnet",
            timeoutSecs = 60,
        ))
        println("duration: ${res.durationMs}ms")
        println(res.result.jsonObject["hello"]?.jsonPrimitive?.content)

        // Upload + reference a file
        val ft = client.uploadFile(Path.of("./recipe.png"), ttlSecs = 3600)
        client.run(RunRequest(
            prompt = "Extract recipe data from the attached image.",
            files  = listOf(ft.fileToken),
        ))
    }
}

Public API

All methods are suspend — call them from a coroutine scope.

Method HTTP Notes
healthz() GET /healthz No bearer required; IP allowlist still applies.
run(RunRequest) POST /run 200 → RunResult; 502 → ForgeRunFailureException.
uploadFile(Path, ttlSecs?) POST /files Streams via Ktor's submitFormWithBinaryData.
createToken(CreateTokenRequest) POST /admin/tokens Admin bootstrap token only.
listTokens() GET /admin/tokens Admin only.
revokeToken(name) DELETE /admin/tokens/{name} Admin only.
close() Disposes the underlying Ktor client.

Constructor

ForgeClient(
    baseUrl: String,
    token:   String,
    options: ForgeOptions = ForgeOptions(),
)

ForgeOptions knobs:

data class ForgeOptions(
    val defaultModel:   String  = "sonnet",
    val defaultTimeout: Duration = 60.seconds,
    val requestMargin:  Duration = 30.seconds,
    val engine:         HttpClientEngine? = null, // override (e.g. MockEngine in tests)
    val configure:     (HttpClientConfig<*>.() -> Unit)? = null,
)

JSON wire format

The server speaks snake_case (timeout_secs, claude_version, file_token, ip_cidrs). The Kotlin SDK exposes idiomatic camelCase properties and uses @SerialName to bridge — callers never see the snake_case names:

@Serializable
data class RunRequest(
    val prompt: String,
    val model: String? = null,
    val system: String? = null,
    val files: List<String>? = null,
    @SerialName("timeout_secs") val timeoutSecs: Int? = null,
)

RunResult.result is a kotlinx.serialization.json.JsonElement — the server may return either a parsed JSON object/array (when the prompt asked for JSON) or a plain string. Narrow it at the call site:

val obj = res.result.jsonObject                       // throws if not object
val txt = res.result.jsonPrimitive.contentOrNull      // null if not primitive

Errors

A sealed exception hierarchy makes when exhaustive:

try {
    client.run(req)
} catch (e: ForgeException) {
    when (e) {
        is ForgeAuthException        -> rotateToken()                            // 401/403
        is ForgeRunFailureException  -> log("claude died: ${e.errorMessage}")    // 502
        is ForgeApiException         -> log("api ${e.statusCode}: ${e.body}")    // other 4xx/5xx
        is ForgeTransportException   -> backoffAndRetry()                        // network/decode
    }
}

Cancellation propagates correctly: CancellationException is rethrown unmodified, so structured concurrency (timeouts, parent-cancel) keep working.

File uploads stream

uploadFile(path, ttlSecs) uses Ktor's submitFormWithBinaryData with an InputProvider backed by the file's InputStream. A 100 MB file uses roughly the buffer size of heap, not 100 MB.

Builds & tests

./gradlew build              # compile + tests + jar
./gradlew test               # tests only
./gradlew publishToMavenLocal

Tests use Ktor's MockEngine — no network, no extra deps.

Compatibility

  • JVM 17+ (toolchain pinned in build.gradle.kts)
  • Kotlin 1.9.25
  • Ktor 2.3.x
  • kotlinx-serialization 1.6.x
  • kotlinx-coroutines 1.8.x

Multiplatform isn't enabled out of the box, but the only JVM-only choices in the SDK are the CIO engine and java.nio.file.Path for uploadFile. A KMP fork would replace those with okio.Path and a per-target engine.

License

MIT.