# clawdforge-client (Java) Java SDK for [clawdforge](../../README.md) — a LAN-only HTTP service that wraps `claude -p` subprocess calls behind a bearer-token-gated REST API. - **Java 17+** (records, sealed-friendly, switch expressions) - **HTTP**: JDK built-in `java.net.http.HttpClient` — no Apache HttpClient, no OkHttp - **JSON**: Jackson (`jackson-databind` 2.x) - **Build**: Maven - **License**: MIT ## Install This SDK isn't published to Maven Central. Build from source and install to your local Maven repo: ```sh git clone http://192.168.0.5:3001/Sulkta-Coop/clawdforge.git cd clawdforge/clients/java mvn install ``` Then depend on it: ```xml com.clawdforge clawdforge-client 0.1.0 ``` ## Quickstart ```java import com.clawdforge.ForgeClient; import com.clawdforge.RunRequest; import com.clawdforge.RunResult; import com.fasterxml.jackson.databind.JsonNode; ForgeClient client = ForgeClient.builder() .baseUrl("http://192.168.0.5:8800") .token(System.getenv("CLAWDFORGE_TOKEN")) .build(); RunResult res = client.run(RunRequest.builder() .prompt("Reply with JSON: {\"hello\": \"world\"}") .model("sonnet") .timeoutSecs(60) .build()); System.out.println(res.durationMs() + "ms"); JsonNode r = res.result(); if (r.isObject()) { System.out.println(r.get("hello").asText()); } ``` ## Naming & wire format The clawdforge HTTP API uses **`snake_case`** on the wire (`timeout_secs`, `file_token`, `created_at`, ...). This SDK uses idiomatic Java **`camelCase`** everywhere; Jackson `@JsonProperty` annotations on each record handle the mapping. Examples: | Java accessor | Wire field | |---------------------------|--------------------| | `RunRequest#timeoutSecs` | `timeout_secs` | | `RunResult#durationMs` | `duration_ms` | | `RunResult#stopReason` | `stop_reason` | | `FileToken#fileToken` | `file_token` | | `FileToken#ttlSecs` | `ttl_secs` | | `HealthStatus#claudePresent` | `claude_present` | | `HealthStatus#claudeVersion` | `claude_version` | | `AppToken#ipCidrs` | `ip_cidrs` | | `AppToken#createdAt` | `created_at` | ## Public surface ### `ForgeClient` Build via `ForgeClient.builder()`: ```java ForgeClient client = ForgeClient.builder() .baseUrl("http://192.168.0.5:8800") // trailing slash trimmed .token("cf_...") // required bearer token .defaultTimeout(Duration.ofSeconds(120)) // optional, default 120s .httpClient(myCustomHttpClient) // optional, default is JDK default .build(); ``` Methods (all may throw subclasses of `ForgeException`): | Method | Endpoint | Returns | |---|---|---| | `healthz()` | `GET /healthz` | `HealthStatus` | | `run(RunRequest)` | `POST /run` | `RunResult` | | `uploadFile(Path, int ttlSecs)` | `POST /files` | `FileToken` | | `createToken(String name, List ipCidrs)` | `POST /admin/tokens` | `AppToken` | | `listTokens()` | `GET /admin/tokens` | `List` | | `revokeToken(String name)` | `DELETE /admin/tokens/` | `void` | ### `RunRequest` Build via `RunRequest.builder()`: ```java RunRequest req = RunRequest.builder() .prompt("...") // required .model("sonnet") // optional, server default = "sonnet" .system("You are ...") // optional system prompt .files(List.of("ff_...")) // optional file tokens from uploadFile() .timeoutSecs(60) // optional, server clamps to [5, 600] .build(); ``` ### Records Public state is exposed via Java 17 records: - `HealthStatus(boolean ok, boolean claudePresent, String claudeVersion)` - `RunResult(boolean ok, JsonNode result, long durationMs, String stopReason)` - `FileToken(String fileToken, int ttlSecs, long size)` - `AppToken(String name, String token, List ipCidrs, long createdAt)` `RunResult.result()` is a Jackson `JsonNode` because the server may return either a structured JSON value (when the prompt asked for JSON) or a plain JSON string. Narrow with `isObject()` / `isTextual()` / etc. ## Error model — why unchecked? All SDK errors extend **`ForgeException`**, which extends `RuntimeException`. Checked exceptions feel un-modern in Java 17 — they bleed across API boundaries, force `throws` chains in lambdas, and don't pair well with the records / sealed-interface idioms the rest of this SDK leans on. ``` ForgeException (RuntimeException) ├── ApiException (any non-2xx HTTP) │ └── AuthException (401 / 403) └── TransportException (DNS, connect, IO, decode, timeout) ``` Catch order: ```java try { client.run(req); } catch (AuthException e) { // token revoked, IP not allowed } catch (ApiException e) { // 502 from /run failure, 404 from missing token, etc. // e.statusCode() and e.body() are both available } catch (TransportException e) { // network / IO / JSON-decode failures } catch (ForgeException e) { // catch-all for anything else SDK-level } ``` ## File uploads `uploadFile` streams the file from disk in 1 MiB chunks via `HttpRequest.BodyPublishers.ofByteArrays` rather than slurping the whole file into memory: ```java FileToken ft = client.uploadFile(Path.of("./recipe.png"), 3600); client.run(RunRequest.builder() .prompt("Extract recipe data from the attached image.") .files(List.of(ft.fileToken())) .build()); ``` `ttlSecs` is clamped server-side to `[60, 86400]`. Pass `0` to use the server default of 3600. ## Build ```sh mvn package # produces target/clawdforge-client-0.1.0.jar mvn test # runs the JUnit 5 suite mvn javadoc:javadoc ``` The test suite uses the JDK built-in `com.sun.net.httpserver.HttpServer` in-process — no external mock-server dependency. ## Threading `ForgeClient` instances are immutable and safe to share across threads. Construct once, reuse everywhere.