clawdforge/clients/ruby/README.md

143 lines
4.6 KiB
Markdown

# clawdforge — Ruby SDK
A small, dependency-free Ruby client for the [clawdforge](../../) HTTP service.
clawdforge is a LAN-only HTTP service that wraps `claude -p` subprocess calls
behind a bearer-token-gated REST API. This SDK is a thin idiomatic wrapper
around that API.
- **Stdlib only at runtime** — `net/http` + `json`. No `httparty`, no `faraday`.
- **Ruby 3.0+** — keyword args, frozen string literals.
- **Five public methods** — `healthz`, `run`, `upload_file`, `create_token`,
`list_tokens`, `revoke_token`. That's it.
## Install
From a path checkout (typical inside the clawdforge monorepo):
```ruby
# Gemfile
gem "clawdforge", path: "clients/ruby"
```
Or from a built gem:
```sh
cd clients/ruby
gem build clawdforge.gemspec
gem install ./clawdforge-0.1.0.gem
```
## Quickstart
```ruby
require "clawdforge"
forge = Clawdforge::Client.new(
base_url: "http://localhost:8800",
token: ENV.fetch("CLAWDFORGE_TOKEN"),
)
# 1) liveness
health = forge.healthz
puts health["claude_version"]
# 2) run a prompt
result = forge.run(
prompt: 'Reply with JSON: {"hello": "world"}',
model: "sonnet", # optional, default "sonnet"
system: "Be terse.", # optional
timeout_secs: 60, # optional, default 120, server range 5..600
)
puts result.duration_ms
puts result.stop_reason
# `result.result` is parsed JSON (Hash) when the model returned valid JSON,
# otherwise the raw String.
puts result.result["hello"]
# 3) upload a file, then attach it to a /run call
ft = forge.upload_file("./recipe.png", ttl_secs: 3600)
forge.run(prompt: "extract recipe data", files: [ft.file_token])
```
## Public API
### `Clawdforge::Client.new(base_url:, token:, **opts)`
| keyword | type | default | meaning |
|-----------------------|---------|---------|--------------------------------------------------|
| `base_url:` | String | — | e.g. `"http://localhost:8800"`. Required. |
| `token:` | String | — | bearer token. Required. |
| `default_model:` | String | `"sonnet"` | model used when `run` doesn't supply one. |
| `default_timeout:` | Integer | `120` | `timeout_secs` used when `run` doesn't supply. |
| `http_timeout_margin:`| Integer | `30` | added to subprocess timeout for HTTP timeout. |
| `http_client:` | Net::HTTP | `nil` | inject a pre-built Net::HTTP (mostly for tests). |
### `#healthz`
Returns a Hash: `{"ok" => Bool, "claude_present" => Bool, "claude_version" => String | nil}`.
### `#run(prompt:, model: nil, system: nil, files: nil, timeout_secs: nil) -> RunResult`
- `prompt` (String, required, non-empty)
- `model` (String, optional)
- `system` (String, optional)
- `files` (Array<String>, optional) — file tokens from `upload_file`
- `timeout_secs` (Integer, optional, server range 5..600)
`RunResult` is a Struct with reader methods:
- `ok` (Bool)
- `result` (Hash | String) — parsed JSON if the model returned valid JSON, raw String otherwise
- `duration_ms` (Integer)
- `stop_reason` (String | nil)
### `#upload_file(path, ttl_secs: 3600, filename: nil, content_type: "application/octet-stream") -> FileToken`
Streams the file in 1 MiB chunks via `IO.copy_stream`. `FileToken` carries
`file_token`, `ttl_secs`, `size`.
### Admin methods (admin-bootstrap-token gated)
- `#create_token(name, ip_cidrs: nil) -> AppToken`
- `#list_tokens -> Array<AppToken>`
- `#revoke_token(name) -> true` (raises `APIError(404)` if not found)
`AppToken`'s `token` field holds the plaintext bearer **only** at create
time; on list responses it is `nil` (the server stores only a sha256 hash).
## Errors
```
Clawdforge::Error
├── Clawdforge::TransportError # connection refused, DNS, TCP timeout, TLS, …
└── Clawdforge::APIError # 4xx / 5xx, exposes #status and #body
└── Clawdforge::AuthError # 401 / 403
```
Catch `Clawdforge::Error` to handle the whole family. On `APIError`,
inspect `.status` (Integer) and `.body` (Hash if JSON, String otherwise).
```ruby
begin
forge.run(prompt: "hi", timeout_secs: 5)
rescue Clawdforge::AuthError => e
warn "bad token: #{e.message}"
rescue Clawdforge::APIError => e
warn "server said #{e.status}: #{e.body.inspect}"
rescue Clawdforge::TransportError => e
warn "couldn't reach forge: #{e.message}"
end
```
No retry logic is built in. clawdforge runs are not idempotent (they spawn
`claude -p`), so retry policy belongs with the caller.
## Development
```sh
bundle install
bundle exec rake test # runs Minitest with WebMock
bundle exec rake build # builds clawdforge-X.Y.Z.gem
```
## License
MIT.