Package Exports
- @headsdown/sdk
Readme
@headsdown/sdk
TypeScript client SDK for the HeadsDown availability API. Gives AI agents and developer tools typed access to availability status, focus modes, and task verdicts.
Zero dependencies. Node 18+ (uses native fetch).
Install
npm install @headsdown/sdkQuick Start
import { HeadsDownClient } from "@headsdown/sdk";
// From an explicit API key
const client = new HeadsDownClient({ apiKey: "hd_..." });
// From saved credentials (~/.config/headsdown/credentials.json)
const client = await HeadsDownClient.fromCredentials();
// Or from the HEADSDOWN_API_KEY environment variable
const client = new HeadsDownClient();Authentication
The SDK uses Device Flow (OAuth 2.0) for authentication. This is the "scan a code" flow designed for CLI tools and agents.
import { HeadsDownClient } from "@headsdown/sdk";
const client = await HeadsDownClient.authenticate(
(auth) => {
console.log(`Open ${auth.verificationUriComplete}`);
console.log(`Or go to ${auth.verificationUri} and enter: ${auth.userCode}`);
},
{ label: "My Tool" },
);
// Credentials are saved automatically to ~/.config/headsdown/credentials.jsonFor lower-level control over the auth flow:
import { DeviceFlow, CredentialStore } from "@headsdown/sdk";
const flow = new DeviceFlow();
const auth = await flow.start("My Tool");
console.log(`Enter code ${auth.userCode} at ${auth.verificationUri}`);
const apiKey = await flow.poll(auth.deviceCode, auth.interval, auth.expiresIn);
const store = new CredentialStore();
await store.save(apiKey, "My Tool");Usage
Check Availability
The primary use case: check whether the user is available before starting work.
const { contract, schedule } = await client.getAvailability();
if (contract?.mode === "busy") {
console.log(`User is in focus mode: ${contract.statusText}`);
console.log(`Expires at: ${contract.expiresAt}`);
}
if (!schedule.inReachableHours) {
console.log(`Not currently reachable. Next transition: ${schedule.nextTransitionAt}`);
}You can also check availability at a specific point in time:
const later = await client.getAvailability({ at: "2025-06-16T09:00:00Z" });Submit a Task Proposal
Ask HeadsDown whether a task should proceed given the user's current availability.
const verdict = await client.submitProposal({
agentRef: "my-agent",
description: "Refactor the auth module to use JWT tokens",
estimatedFiles: 4,
estimatedMinutes: 30,
scopeSummary: "4 files in lib/auth/",
sourceRef: "ticket-142",
});
if (verdict.decision === "approved") {
// Proceed with the task
} else {
// verdict.decision === "deferred"
console.log(`Deferred: ${verdict.reason}`);
}List Past Proposals
// All proposals
const proposals = await client.listProposals();
// Only deferred, last 5
const deferred = await client.listProposals({ verdict: "deferred", latest: 5 });Presets
const presets = await client.listPresets();
const contract = await client.applyPreset(presets[0].id);Create a Contract Directly
const contract = await client.createContract({
mode: "busy",
autoRespond: true,
status: true,
statusText: "Deep work",
statusEmoji: "🔨",
duration: 120, // minutes
ruleSetType: "focus",
ruleSetParams: { maxInterruptions: 0 },
});Verdict and Calibration Utilities
const interrupt = await client.evaluateInterrupt("brezn");
if (!interrupt.allowed) {
console.log(interrupt.autoResponse ?? interrupt.reason);
}
const settings = await client.getVerdictSettings();
const updated = await client.updateVerdictSettings({
online: 60,
busy: 15,
limited: 5,
offline: 0,
});
const profiles = await client.listCalibrationProfiles();User Profile
const profile = await client.getProfile();
console.log(`Authenticated as ${profile.name} (${profile.email})`);Error Handling
The SDK uses a typed error hierarchy. All errors extend HeadsDownError.
import { AuthError, ApiError, NetworkError, ValidationError } from "@headsdown/sdk";
try {
const verdict = await client.submitProposal({ ... });
} catch (error) {
if (error instanceof AuthError) {
// API key missing, invalid, or expired. Re-authenticate.
} else if (error instanceof NetworkError) {
// Connection failed, DNS error, or timeout.
} else if (error instanceof ValidationError) {
// Bad input (e.g., empty description). Check error.field.
} else if (error instanceof ApiError) {
// Server returned an error. Check error.status, error.graphqlErrors.
}
}Configuration
const client = new HeadsDownClient({
apiKey: "hd_...", // API key (or set HEADSDOWN_API_KEY)
baseUrl: "https://headsdown.app", // API base URL
timeout: 30000, // Request timeout in ms
fetch: customFetch, // Custom fetch implementation
retry: { retries: 2, retryDelayMs: 250 }, // Transient failure retries
hooks: {
onRetry: ({ attempt, reason }) => console.log(`retry #${attempt + 1}: ${reason}`),
},
});| Option | Default | Description |
|---|---|---|
apiKey |
HEADSDOWN_API_KEY env var |
HeadsDown API key (hd_ prefix) |
baseUrl |
https://headsdown.app |
API endpoint |
timeout |
30000 |
Request timeout in milliseconds |
fetch |
globalThis.fetch |
Custom fetch (for testing or proxies) |
retry.retries |
2 |
Number of retries for transient failures |
retry.retryDelayMs |
250 |
Base retry delay in ms (exponential backoff) |
hooks |
undefined |
Optional onRequest/onResponse/onRetry hooks |
Data Transparency
This SDK sends requests only to the HeadsDown API (https://headsdown.app/graphql by default). Every request includes your API key as a Bearer token. The exact GraphQL queries are in src/queries.ts, readable in full.
What is sent: Your API key, and the specific query/mutation being executed (availability checks, task proposals, preset operations, verdict settings, interrupt evaluation).
What is received: Your availability status, schedule resolution, task verdicts, calibration data, and preset configurations.
What is stored locally: Your API key at ~/.config/headsdown/credentials.json (file permissions: 0600, user-only read/write).
No telemetry. No analytics. No third-party requests.
Schema Sync
The app repo is the source of truth for the GraphQL schema, and this SDK only consumes a pushed schema file.
When the app exports a new schema, import it here:
npm run schema:sync -- --source /absolute/path/to/schema.json(Equivalent env var form: HEADSDOWN_SCHEMA_SOURCE=/absolute/path/to/schema.json npm run schema:sync.)
Then regenerate operation and variable types:
npm run codegen:typesThe schema compatibility test uses this local snapshot to make drift obvious in CI.
Releases
Releases are tag-driven. Push a tag like v0.1.0 and GitHub Actions will run tests, verify the tag matches package.json, and publish to npm using trusted publishing.
git tag v0.1.0
git push origin v0.1.0Before the first publish, configure npm trusted publishing for this repository and make sure the package version matches the tag.
License
MIT