JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 101
  • Score
    100M100P100Q102303F
  • License MIT

TypeScript client SDK for the HeadsDown availability API

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/sdk

Quick 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.json

For 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:types

The 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.0

Before the first publish, configure npm trusted publishing for this repository and make sure the package version matches the tag.

License

MIT