clients/cpp: initial C++ SDK for clawdforge

Modern C++20 SDK targeting CMake 3.20+. Library is RAII / move-only,
backed by a libcurl easy handle per Client. Public surface is throwing;
exception hierarchy under clawdforge::Error covers AuthError, APIError
(carries status_code + body), TransportError, and ProtocolError.

Dependencies: libcurl + nlohmann/json (FetchContent or find_package).
Tests use cpp-httplib's in-process server + doctest. 12 test cases /
70 assertions cover healthz, run with JSON / text / 502 / files,
multipart upload, full token CRUD, transport failure, URL normalization,
and bad-input rejection. Clean under -Wall -Wextra -Wpedantic -Werror,
ASan + UBSan clean (no leaks, no UB).

upload_file streams via curl_mime_filedata — no in-memory buffering.

Install path produces clawdforge::clawdforge target consumable via
target_link_libraries; FetchContent path mirrors the existing Rust /
Go SDK ergonomics. MIT licensed.
This commit is contained in:
Kayos 2026-04-28 23:02:37 -07:00
parent a69e924592
commit bae34a7701
12 changed files with 1866 additions and 0 deletions

View file

@ -0,0 +1,78 @@
// SPDX-License-Identifier: MIT
//
// Minimal end-to-end example for the clawdforge C++ SDK. Set CF_BASE_URL and
// CF_TOKEN in your environment, point them at a running clawdforge, build,
// run.
#include <cstdlib>
#include <iostream>
#include <string>
#include <clawdforge/client.hpp>
namespace cf = clawdforge;
namespace {
std::string env_or_default(const char* name, std::string fallback) {
if (const char* v = std::getenv(name); v != nullptr && *v != '\0') {
return std::string{v};
}
return fallback;
}
} // namespace
int main(int argc, char** argv) {
(void)argc;
(void)argv;
const std::string base_url = env_or_default("CF_BASE_URL", "http://localhost:8800");
const std::string token = env_or_default("CF_TOKEN", "");
if (token.empty()) {
std::cerr << "set CF_TOKEN to a clawdforge bearer\n";
return 2;
}
cf::Client client{cf::ClientOptions{
.base_url = base_url,
.token = token,
}};
try {
const auto h = client.healthz();
std::cout << "healthz ok=" << std::boolalpha << h.ok
<< " claude_present=" << h.claude_present
<< " version=" << h.claude_version << "\n";
const auto res = client.run(cf::RunRequest{
.prompt = R"(Reply with JSON: {"hello": "world"})",
.model = "sonnet",
.timeout_secs = 60,
});
std::cout << "duration_ms=" << res.duration_ms << "\n";
if (const auto* j = std::get_if<nlohmann::json>(&res.result)) {
std::cout << "json result: " << j->dump() << "\n";
if (j->contains("hello")) {
std::cout << "hello = " << j->at("hello").get<std::string>() << "\n";
}
} else if (const auto* s = std::get_if<std::string>(&res.result)) {
std::cout << "text result: " << *s << "\n";
}
} catch (const cf::AuthError& e) {
std::cerr << "auth: " << e.what() << "\n";
return 3;
} catch (const cf::APIError& e) {
std::cerr << "api " << e.status_code() << ": " << e.what() << "\n";
std::cerr << "body: " << e.body() << "\n";
return 4;
} catch (const cf::TransportError& e) {
std::cerr << "transport: " << e.what() << "\n";
return 5;
} catch (const cf::Error& e) {
std::cerr << "clawdforge: " << e.what() << "\n";
return 6;
}
return 0;
}