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:
parent
a69e924592
commit
bae34a7701
12 changed files with 1866 additions and 0 deletions
160
clients/cpp/README.md
Normal file
160
clients/cpp/README.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# clawdforge — C++ SDK
|
||||
|
||||
Modern C++ client for the [clawdforge](https://github.com/Sulkta-Coop/clawdforge)
|
||||
HTTP API. Wraps `claude -p` subprocess calls behind a bearer-token-gated REST
|
||||
service.
|
||||
|
||||
- C++17 minimum (C++20 lets you use designated initializers in examples).
|
||||
- libcurl + nlohmann/json. No Boost.
|
||||
- RAII, move-only `Client` (one libcurl handle per instance).
|
||||
- Throwing API; full exception hierarchy under `clawdforge::Error`.
|
||||
|
||||
## Install
|
||||
|
||||
### Option A — FetchContent (drop-in for existing CMake projects)
|
||||
|
||||
```cmake
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(clawdforge
|
||||
GIT_REPOSITORY https://github.com/Sulkta-Coop/clawdforge.git
|
||||
GIT_TAG main
|
||||
SOURCE_SUBDIR clients/cpp
|
||||
)
|
||||
FetchContent_MakeAvailable(clawdforge)
|
||||
|
||||
target_link_libraries(my_app PRIVATE clawdforge::clawdforge)
|
||||
```
|
||||
|
||||
`CURL` must be available via `find_package(CURL REQUIRED)`. On Debian/Ubuntu:
|
||||
`sudo apt install libcurl4-openssl-dev`.
|
||||
|
||||
### Option B — install + find_package
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Sulkta-Coop/clawdforge.git
|
||||
cd clawdforge/clients/cpp
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
||||
Then in your project:
|
||||
|
||||
```cmake
|
||||
find_package(clawdforge CONFIG REQUIRED)
|
||||
target_link_libraries(my_app PRIVATE clawdforge::clawdforge)
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```cpp
|
||||
#include <clawdforge/client.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace cf = clawdforge;
|
||||
|
||||
int main() {
|
||||
cf::Client client{cf::ClientOptions{
|
||||
.base_url = "http://localhost:8800",
|
||||
.token = "cf_...",
|
||||
}};
|
||||
|
||||
auto h = client.healthz();
|
||||
std::cout << "claude_version=" << h.claude_version << "\n";
|
||||
|
||||
auto r = client.run(cf::RunRequest{
|
||||
.prompt = R"(Reply with JSON: {"hello":"world"})",
|
||||
.model = "sonnet",
|
||||
.timeout_secs = 60,
|
||||
});
|
||||
|
||||
if (auto* j = std::get_if<nlohmann::json>(&r.result)) {
|
||||
std::cout << j->at("hello").get<std::string>() << "\n";
|
||||
} else if (auto* s = std::get_if<std::string>(&r.result)) {
|
||||
std::cout << *s << "\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
`clawdforge::Client` (in `<clawdforge/client.hpp>`):
|
||||
|
||||
| Method | HTTP | Returns |
|
||||
|---|---|---|
|
||||
| `healthz()` | `GET /healthz` | `HealthzResponse` |
|
||||
| `run(req)` | `POST /run` | `RunResult` |
|
||||
| `upload_file(path, ttl_secs=0)` | `POST /files` | `FileToken` |
|
||||
| `create_token(req)` | `POST /admin/tokens` | `AppToken` |
|
||||
| `list_tokens()` | `GET /admin/tokens` | `std::vector<AppTokenInfo>` |
|
||||
| `revoke_token(name)` | `DELETE /admin/tokens/<name>` | `void` |
|
||||
|
||||
All methods throw on failure. `ClientOptions` lets you set `base_url`, `token`,
|
||||
`admin_token`, `timeout`, `connect_timeout`, `user_agent`, and `insecure_tls`.
|
||||
|
||||
## Errors
|
||||
|
||||
```text
|
||||
clawdforge::Error // abstract base, derives from std::runtime_error
|
||||
├── clawdforge::AuthError // 401 / 403, or missing token on the client
|
||||
├── clawdforge::APIError // any other non-2xx — has status_code() + body()
|
||||
├── clawdforge::TransportError // libcurl-level failures
|
||||
└── clawdforge::ProtocolError // bad URL, missing fields, malformed JSON
|
||||
```
|
||||
|
||||
Catch broadly:
|
||||
|
||||
```cpp
|
||||
try {
|
||||
client.run(req);
|
||||
} catch (const cf::AuthError& e) { /* refresh / abort */ }
|
||||
catch (const cf::APIError& e) { std::cerr << e.status_code() << " " << e.body(); }
|
||||
catch (const cf::TransportError& e) { /* retry */ }
|
||||
catch (const cf::Error& e) { /* anything else */ }
|
||||
```
|
||||
|
||||
`POST /run` returns HTTP 502 on subprocess failure. Surfaced as `APIError`
|
||||
with `status_code() == 502`; `body()` parses as the documented `RunFailure`
|
||||
shape.
|
||||
|
||||
## File uploads
|
||||
|
||||
```cpp
|
||||
auto ft = client.upload_file("/path/to/recipe.png", /*ttl_secs=*/3600);
|
||||
auto res = client.run(cf::RunRequest{
|
||||
.prompt = "extract recipe data",
|
||||
.files = {ft.file_token},
|
||||
});
|
||||
```
|
||||
|
||||
The file is streamed off disk via libcurl's `curl_mime_filedata` — no
|
||||
in-memory buffering of the payload.
|
||||
|
||||
## Threading
|
||||
|
||||
`Client` is move-only and **not** thread-safe. Construct one per worker thread
|
||||
or wrap external accesses in a mutex. Multiple `Client` instances share the
|
||||
process-wide libcurl global state safely (refcounted internally).
|
||||
|
||||
## Build options
|
||||
|
||||
| Option | Default | Effect |
|
||||
|---|---|---|
|
||||
| `CLAWDFORGE_BUILD_TESTS` | `ON` (top-level), `OFF` (subdirectory) | doctest-based suite |
|
||||
| `CLAWDFORGE_BUILD_EXAMPLES` | `ON` (top-level), `OFF` (subdirectory) | builds `clawdforge_basic_example` |
|
||||
| `CLAWDFORGE_WARNINGS_AS_ERRORS` | `ON` | `-Werror` / `/WX` on the library target |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
The test suite spins up a `cpp-httplib` mock server in-process — no real
|
||||
clawdforge needed.
|
||||
|
||||
## License
|
||||
|
||||
MIT. See `LICENSE` at the repo root.
|
||||
Loading…
Add table
Add a link
Reference in a new issue