Dependency-free TypeScript client for the Hawk daemon API
Hawk SDK for TypeScript is the official TypeScript client library for the
Hawk daemon API. It provides a
dependency-free, type-safe client for interacting with Hawk's HTTP API from
Node.js applications (any runtime with a global fetch, Node 18+).
Hawk SDK for TypeScript is part of the hawk-eco mono-ecosystem:
| Component | Purpose |
|---|---|
| hawk | AI-powered coding agent for the terminal |
| hawk-sdk-go | Go SDK for the Hawk daemon API |
| hawk-sdk-python | Python SDK for the Hawk daemon API |
| hawk-sdk-typescript | TypeScript SDK for the Hawk daemon API (this repo) |
| hawk-core-contracts | Shared cross-repo contracts (types, events, tools) |
npm install hawk-sdk
# or
pnpm add hawk-sdkimport { HawkClient, defaultRetryConfig } from "hawk-sdk";
const client = new HawkClient({
baseURL: "http://127.0.0.1:4590", // default
apiKey: process.env.HAWK_API_KEY, // optional
retry: defaultRetryConfig(), // optional: automatic retries w/ backoff
});
const health = await client.health();
console.log(health.status); // "ok"
const reply = await client.chat({ prompt: "Explain closures in JavaScript." });
console.log(reply.response);const reader = await client.chatStream({ prompt: "Write a haiku." });
for await (const event of reader) {
process.stdout.write(event.data);
}const sessions = await client.sessions();
const detail = await client.session(sessions[0].id);
const messages = await client.messages(detail.id, { offset: 0, limit: 50 });
const stats = await client.stats();
await client.deleteSession(detail.id);Agent wraps a client with declarative configuration and tracks the session
automatically for multi-turn conversations:
import { Agent, HawkClient } from "hawk-sdk";
const agent = new Agent(new HawkClient(), {
model: "gpt-4o",
systemPrompt: "You are a concise assistant.",
memory: { enabled: true },
});
const first = await agent.chat("Remember the number 7.");
const second = await agent.chat("What number did I give you?"); // same sessionProvide tools and the SDK runs the full tool-execution loop until the model stops requesting calls:
const agent = new Agent(client, {
tools: [
{
schema: {
name: "get_weather",
description: "Get the weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
run: async (args) => `It is sunny in ${args.city}.`,
},
],
});
const reply = await agent.chat("What's the weather in Tokyo?");All non-2xx responses throw a typed error extending APIError, so you can
branch with instanceof:
import { NotFoundError, RateLimitError } from "hawk-sdk";
try {
await client.session("missing");
} catch (err) {
if (err instanceof NotFoundError) {
console.log("no such session");
} else if (err instanceof RateLimitError) {
console.log(`retry after ${err.retryAfterMs}ms`);
}
}The client performs no retries by default. Pass a retry config to enable
exponential backoff with full jitter and Retry-After support:
import { defaultRetryConfig } from "hawk-sdk";
const client = new HawkClient({ retry: defaultRetryConfig() });Idempotent requests (GET/DELETE) retry on 429/500/502/503/504. Non-idempotent
requests (POST /v1/chat) retry only on 429, since a 5xx may mean the daemon
already began processing the request.
Every request has a whole-request deadline of timeoutMs milliseconds
(default: 30000), so a hung daemon can never block a caller indefinitely.
The deadline is a logical-request deadline: it bounds the entire request,
including every retry attempt and backoff sleep — retries never extend it.
When it elapses, the call rejects with a DOMException whose name is
"TimeoutError" (the APIError hierarchy covers HTTP status errors;
transport-level aborts propagate as-is):
try {
await client.chat({ prompt: "Hello!" });
} catch (err) {
if (err instanceof Error && err.name === "TimeoutError") {
console.log("daemon did not respond in time");
}
}A per-call AbortSignal composes with the deadline: whichever fires first
aborts the request, and the caller's own abort reason propagates unchanged.
For chatStream, the deadline covers obtaining the SSE response; consuming
the returned stream is caller-controlled. Disable the deadline with
timeoutMs: 0:
const client = new HawkClient({ timeoutMs: 5000 });The three Hawk SDKs (Go, TypeScript, Python) share wire behavior but have drifted in transport defaults. Actual current values:
| Default | TypeScript (this SDK) | Go | Python |
|---|---|---|---|
| Retries | Off — opt in with { retry: defaultRetryConfig() } (src/client.ts) |
Off — opt in with WithRetry(DefaultRetryConfig()) (client.go) |
On — retry_config or DEFAULT_RETRY_CONFIG (src/hawk/client.py) |
| Initial backoff | 1s (src/retry.ts, defaultRetryConfig) |
1s (retry.go, DefaultRetryConfig) |
0.5s (src/hawk/retry.py, RetryConfig) |
| Backoff jitter | Full jitter: rand(0, backoff) (src/retry.ts, backoffDurationMs) |
Full jitter: rand(0, backoff) (retry.go, backoffDuration) |
Equal + jitter: backoff + rand(0, backoff/2) (src/hawk/retry.py, _compute_backoff) |
| Request timeout | Whole-request deadline, 30s, includes retries (src/client.ts, timeoutMs) |
ResponseHeaderTimeout: 5s, headers only (client.go) |
httpx timeout, 30s (src/hawk/client.py, DEFAULT_TIMEOUT) |
Max retries (3), max backoff (30s), retryable statuses (429/500/502/503/504),
and the non-idempotent rule (only 429 is retried for POST /v1/chat) are
identical in all three SDKs. This table documents current behavior; it is not
a compatibility contract between the SDKs.
| Method | Description |
|---|---|
health(signal?) |
Daemon connectivity and version |
chat(req, signal?) |
Send a prompt, return the full response |
chatStream(req, signal?) |
Send a prompt, stream SSE events |
chatWithTools(req, tools, maxRounds?, signal?) |
Run the tool-execution loop |
sessions(signal?) |
List active sessions |
session(id, signal?) |
Get a session by ID |
messages(id, opts?, signal?) |
Paginated session messages |
graph(id, opts?, signal?) |
Get and validate a privacy-safe session execution graph |
deleteSession(id, signal?) |
Delete a session |
stats(signal?) |
Aggregated usage statistics |
Every method accepts an optional AbortSignal as its last argument for
cancellation.
GraphExport, GraphNode, GraphEdge, and GraphEvent describe the shared
*.graph/v1 wire format. Use parseGraphExport(value) at an untrusted API
boundary to validate vocabulary, timestamps, unique IDs, provenance, and
self-contained topology. client.graph() retrieves the authenticated
/v1/sessions/{id}/graph projection and applies this parser before returning.
These models are consumers only; the SDK does not own graph facts or storage.
npm install # install dev dependencies (typescript, tsx)
npm run build # compile to dist/
npm run typecheck # type-check without emitting
npm test # run the test suite (node:test via tsx)The runtime build in dist/ has zero dependencies — it relies only on the
global fetch, ReadableStream, and TextDecoder available in Node 18+.
MIT © GrayCode AI