clawdforge/clients/java/examples/Basic.java
Kayos 9866e97977 clients/java: apply audit findings — true streaming upload + token redaction (0d3ee26 → next)
MEDIUM:
- C1: multipart upload now actually streams via SequenceInputStream + Files.newInputStream. Code comment + README + javadoc updated to match reality. Test added uploading 10 MiB file with received-bytes assertion bounding envelope overhead.
- S1: AppToken.toString() override redacts token (was leaking plaintext via record auto-toString).

LOW:
- C2: RunResult.result null/missing-field handling — canonical-constructor coerces null/NullNode to MissingNode, javadoc updated.
- C3: HTTP timeout lower bound: Math.max(5L, n + 30L).
- C4: ForgeClient implements AutoCloseable (no-op on JDK 17, documented).
- S4: javadoc warning on uploadFile path traversal / symlink follow.

Quality:
- Q1: package-info.java added for com.clawdforge.exception (clears pom.xml dead exclude).
- C7: @JsonInclude(NON_DEFAULT) on POST DTOs (drops wire "created_at": 0).

Deps:
- jackson-databind/core/annotations 2.17.2 → 2.18.2 (2.17 EOL'd Aug 2025).

Tests: 14 → 23 (9 added).

Audit: memory/clawdforge-audits/java-0d3ee26.md
2026-04-28 23:20:45 -07:00

82 lines
3.5 KiB
Java

// SPDX-License-Identifier: MIT
//
// Build & run from clients/java/:
// mvn -q package
// javac -cp target/clawdforge-client-0.1.0.jar:$(ls ~/.m2/repository/com/fasterxml/jackson/core/jackson-databind/*/jackson-databind-*.jar | head -1):$(ls ~/.m2/repository/com/fasterxml/jackson/core/jackson-core/*/jackson-core-*.jar | head -1):$(ls ~/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/*/jackson-annotations-*.jar | head -1) -d /tmp/cf-example examples/Basic.java
// java -cp /tmp/cf-example:target/classes:<jackson jars> Basic
//
// Or just import the jar into your own project and run from there.
import com.clawdforge.AppToken;
import com.clawdforge.FileToken;
import com.clawdforge.ForgeClient;
import com.clawdforge.HealthStatus;
import com.clawdforge.RunRequest;
import com.clawdforge.RunResult;
import com.fasterxml.jackson.databind.JsonNode;
import java.nio.file.Path;
import java.util.List;
public class Basic {
public static void main(String[] args) {
String baseUrl = System.getenv().getOrDefault("CLAWDFORGE_URL", "http://localhost:8800");
String token = System.getenv("CLAWDFORGE_TOKEN");
if (token == null || token.isBlank()) {
System.err.println("set CLAWDFORGE_TOKEN");
System.exit(2);
}
try (ForgeClient client = ForgeClient.builder()
.baseUrl(baseUrl)
.token(token)
.build()) {
// 1. health
HealthStatus h = client.healthz();
System.out.printf("ok=%s claude_present=%s version=%s%n",
h.ok(), h.claudePresent(), h.claudeVersion());
// 2. run a prompt asking for JSON
RunResult res = client.run(RunRequest.builder()
.prompt("Reply with JSON: {\"hello\": \"world\"}")
.model("sonnet")
.timeoutSecs(60)
.build());
System.out.printf("duration=%dms stop_reason=%s%n", res.durationMs(), res.stopReason());
JsonNode r = res.result();
if (r.isObject() && r.has("hello")) {
System.out.println("hello -> " + r.get("hello").asText());
} else if (r.isTextual()) {
System.out.println("text -> " + r.asText());
} else if (r.isMissingNode()) {
System.out.println("(no result field in response)");
} else {
System.out.println("raw -> " + r);
}
// 3. (optional) upload + reference a file
String filePath = System.getenv("CLAWDFORGE_FILE");
if (filePath != null && !filePath.isBlank()) {
FileToken ft = client.uploadFile(Path.of(filePath), 3600);
System.out.println("uploaded " + ft.fileToken() + " (" + ft.size() + " bytes)");
RunResult res2 = client.run(RunRequest.builder()
.prompt("Summarize the attached file in one sentence.")
.files(List.of(ft.fileToken()))
.build());
System.out.println("summary -> " + res2.result());
}
// 4. (admin) list tokens — only works with the admin bootstrap token
if (Boolean.parseBoolean(System.getenv().getOrDefault("CLAWDFORGE_ADMIN", "false"))) {
List<AppToken> tokens = client.listTokens();
for (AppToken t : tokens) {
// AppToken.toString() redacts the plaintext bearer.
System.out.println("token " + t);
}
}
}
}
}