JSPM

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

TypeScript client SDK for the HeadsDown availability API

Package Exports

  • @headsdown/sdk

Readme

@headsdown/sdk

TypeScript client SDK for the HeadsDown API. Gives AI agents and developer tools typed access to HeadsDown calls, availability boundaries, 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();

// Optional: set default actor context for delegated authorization
const scopedClient = new HeadsDownClient({
  actorContext: { source: "pi", sessionId: "session-123" },
});

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

Read the HeadsDown call

Lead with what HeadsDown recommends for the run, then use availability as supporting context.

import { resolveHeadsDownCallFallback } from "@headsdown/sdk";

const overview = await client.getAgentControlOverview();
const call = resolveHeadsDownCallFallback(overview.headsdownCall);
console.log(`${call.title}: ${call.body}`);
console.log(`HeadsDown call: ${call.primaryActionKey ?? call.primaryActionIntent}`);

Apply canonical HeadsDown actions

Use the built-in action helpers so clients do not hand-roll mutation payloads. Helpers send canonical action keys and derive idempotency keys by default.

const queued = await client.queueForMorning({ runId: "run_123" });
console.log(queued.result?.actionKey); // "queue_for_morning"

await client.continueRun({ runId: "run_123" });
await client.continueWithLimit({ runId: "run_123" });
await client.askUser({ runId: "run_123" });
await client.queueForLater({ runId: "run_123" });

await client.pauseAndSummarize({
  runId: "run_123",
  reason: "Rabbit hole detected. Save the handoff.",
});

await client.allowForDuration({
  runId: "run_123",
  durationMinutes: 15,
  reason: "One targeted validation path",
});

await client.allowOnce({ runId: "run_123" });
await client.createTemporaryException({
  runId: "run_123",
  durationMinutes: 30,
  mode: "limited",
});

await client.keepQueued({ runId: "run_123" });
await client.resumeRun({ runId: "run_123" });
await client.narrowScope({ runId: "run_123" });
await client.stopRun({ runId: "run_123" });

// For a lower-level integration, use the typed generic action surface.
await client.applyHeadsDownAction("queue_for_morning", { runId: "run_123" });

Check Availability

Use availability to understand the user’s current boundary when a workflow still needs the lower-level schedule or mode inputs.

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}`);
}

if (schedule.wrapUpGuidance.active) {
  console.log(`Wrap-Up active: ${schedule.wrapUpGuidance.remainingMinutes} minutes left`);
  console.log(`Guidance source: ${schedule.wrapUpGuidance.source}`);
}

You can also check availability at a specific point in time:

const later = await client.getAvailability({ at: "2025-06-16T09:00:00Z" });

Execution Directive (cold-start model guidance)

Use describeExecutionDirective() to convert availability, Wrap-Up guidance, and verdict context into one canonical instruction payload for LLMs.

import { describeExecutionDirective } from "@headsdown/sdk";

const { contract, schedule } = await client.getAvailability();
const verdict = await client.submitProposal({
  agentRef: "pi",
  framework: "pi",
  description: "Ship the scoped fix",
});

const directive = describeExecutionDirective({ contract, schedule, verdict });
console.log(directive.primaryDirective);

Actor Context (session/workspace-aware authorization)

For endpoints protected by delegation grants, the backend can require actor context metadata. Set it once at client construction, or override it for one call with withActor().

const client = new HeadsDownClient({
  apiKey: "hd_...",
  actorContext: { source: "pi", sessionId: "session-default" },
});

// Per-request override via a derived client
await client
  .withActor({
    source: "pi",
    workspaceRef: "headsdown/headsdown-pi",
    sessionId: "session-override",
  })
  .submitProposal({ agentRef: "pi", description: "Refactor auth resolver" });

Transport format is a single HTTP header:

  • x-headsdown-actor-context: {"source":"...","agentId":"...?","sessionId":"...?","workspaceRef":"...?"}

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", // provenance (ticket/PR/chat reference)
  idempotencyKey: "pi-toolcall-123", // retry safety for this invocation
  deliveryMode: "auto", // optional: "auto" | "wrap_up" | "full_depth"
});

if (verdict.decision === "approved") {
  // Proceed with the task
  if (verdict.wrapUpGuidance?.active) {
    console.log(`Wrap-Up is active until ${verdict.wrapUpGuidance.deadlineAt}`);
  }
} 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 },
});

// Two-axis boundary: createContract is availability status only.
// Wrap-Up fields are rejected here and should be set via updateVerdictSettings or deliveryMode.

Delegation Grants

Delegation grant management mutations now require a session-token auth path in the backend. Because this SDK authenticates with API keys, create/revoke grant calls will raise an auth error with actionable guidance.

try {
  await client.createDelegationGrant({
    scope: "session",
    sessionId: "session-123",
    permissions: ["availability_override_create", "availability_override_cancel"],
    durationMinutes: 30,
    source: "pi",
  });
} catch (error) {
  // AuthError: Delegation grant management requires a session-token auth path.
}

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({
  thresholds: {
    online: { maxFiles: null, maxEstimatedMinutes: null },
    busy: { maxFiles: 5, maxEstimatedMinutes: 30 },
    limited: { maxFiles: 3, maxEstimatedMinutes: 10 },
    offline: { maxFiles: 0, maxEstimatedMinutes: 0 },
  },
  defaultWrapUpMode: "auto",
  wrapUpThresholdMinutes: 30,
});

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,
  HeadsDownActionInvalidStateError,
  HeadsDownActionExpiredError,
  HeadsDownActionFeatureDisabledError,
  HeadsDownActionAuthError,
} from "@headsdown/sdk";

try {
  await client.pauseAndSummarize({
    runId: "run_123",
    reason: "Rabbit hole detected. Save the handoff.",
  });
} catch (error) {
  if (error instanceof HeadsDownActionInvalidStateError) {
    // Action no longer matches the current call state.
  } else if (error instanceof HeadsDownActionExpiredError) {
    // Action request expired before apply.
  } else if (error instanceof HeadsDownActionFeatureDisabledError) {
    // Backend feature is disabled for this workspace/user.
  } else if (error instanceof HeadsDownActionAuthError || error instanceof AuthError) {
    // Auth failed. Re-authenticate or refresh actor context.
  } else if (error instanceof NetworkError) {
    // Connection failed, DNS error, or timeout.
  } else if (error instanceof ValidationError) {
    // Bad input (for example missing runId or invalid durationMinutes).
  } 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}`),
  },
  actorContext: { source: "pi", sessionId: "session-123" },
});
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
actorContext undefined Default actor context sent as x-headsdown-actor-context

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, the specific query/mutation being executed (HeadsDown call reads, canonical action applies, availability checks, task proposals, preset operations, verdict settings, interrupt evaluation), and optional actor context metadata when configured (source, agentId, sessionId, workspaceRef).

What is received: HeadsDown call metadata, availability boundaries, 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

If a ticket only updates SDK runtime normalization or exported TypeScript aliases without changing src/queries.ts or the schema snapshot, regeneration is not required.

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