npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cavi-ai/api-client

v0.6.0

Published

Provider-agnostic TypeScript client for AI agent runtimes — one RuntimeClient contract spanning Claude (incl. Claude Managed Agents, beta), Codex via OpenAI Responses, Hermes, and OpenClaw, with HTTP + WebSocket RPC, SSE, manifest-driven routing, and grac

Readme

License: MIT CI Claude Managed Agents Node Types ESM

npm install @cavi-ai/api-client

Contents

Why This Package Exists

Building on top of agent runtimes means writing the same plumbing over and over: authenticated requests, WebSocket RPC, run-event streams, capability snapshots, route contracts, typed errors, and fallback behavior for when the backend inevitably hiccups. @cavi-ai/api-client keeps all of it in one reusable, provider-agnostic package — so your app code focuses on the workflow, not the transport.

🤔 This package is for you if…

  1. 🛰️ You run multiple gateways and runtimes across different providers, and you'd rather have one client than a pile of one-off integrations.
  2. 🎛️ You're building interactive, agentic UI — live runs, streaming events, capability-aware panels — and don't want to rebuild the transport layer every single project.
  3. You don't love the timing of upstream bugs (always mid-demo, never a quiet Tuesday) and want your UI to shrug them off instead of faceplanting.
  4. 🤷 You've made peace with the one universal constant: humans and agents ship mistakes. So here, degradation is a contract — a backend gap returns typed fallback data with a structured contractGap, not a white screen of death.
  5. 🔀 You need to switch providers without a rewrite — Hermes today, OpenClaw tomorrow, Claude on Friday — all behind the exact same calls.
  6. 📐 You want stable, schema-correct endpoints that hand back the same shape no matter which provider answered, so per-provider if/else spaghetti never leaks into your components.
  7. 🎉 You want in on a genuinely fun open-source project (MIT, strict-typed to the teeth, conformance-tested — and yes, PRs are actually welcome).
  8. 🐶🐱 You like puppies and kittens. …alright, we're reaching. But you're still reading, so maybe we're onto something. 😄

🧩 How it holds together

The core is provider-agnostic. Every provider implements one universal RuntimeClient contract (capabilities · runs · streaming); gateway-style providers (Hermes, OpenClaw) extend it with GatewayClient (teams, kanban, workspace, operator), while non-gateway providers (Claude / Anthropic) implement the runtime tier only. Provider modules customize only what's actually different — endpoint maps, headers, auth scheme, method transport. The shared transports, error handling, stream parsing, and trace behavior stay in one place, and an executable conformance kit keeps every provider honest.

Runtime

  • Pure TypeScript ESM with generated .d.ts files.
  • Node.js >=20, or any modern runtime with fetch and WebSocket.
  • Zero runtime dependencies.
  • React is an optional peer dependency used only by @cavi-ai/api-client/frameworks/react.
  • fetchImpl can be supplied anywhere a runtime needs an explicit fetch implementation.

Exports

The root export is a curated stable API: the unified client + provider registry, the universal RuntimeClient/GatewayClient types, errors and guards, the graceful-degradation envelope, the auth-seam credential helpers, the team manifest interface + resolver, and the run-stream contract. Everything else lives behind a subpath so consumers import only the slice they need:

  • @cavi-ai/api-client/core/httpBaseHttpApiClient, raw/JSON clients, redaction
  • @cavi-ai/api-client/core/data
  • @cavi-ai/api-client/core/errors
  • @cavi-ai/api-client/core/runtimeRuntimeClient, run-stream contract
  • @cavi-ai/api-client/core/sse
  • @cavi-ai/api-client/core/ws
  • @cavi-ai/api-client/core/gateway — gateway resource clients (media, wiki, agent-config, jobs)
  • @cavi-ai/api-client/core/env
  • @cavi-ai/api-client/contracts
  • @cavi-ai/api-client/extensions/caviCaviControlApiClient, registry, portal, library, adapters
  • @cavi-ai/api-client/providers/hermes
  • @cavi-ai/api-client/providers/openclaw
  • @cavi-ai/api-client/providers/claude — Claude (Anthropic): the stateless Messages-API runtime provider and the Managed Agents (beta) surface
  • @cavi-ai/api-client/providers/codex — Codex-flavored OpenAI Responses runtime provider (gpt-5-codex, background runs, polling, cancellation, SSE streaming)
  • @cavi-ai/api-client/frameworks/react

Upgrading from a flat-import version? Provider modules, the CAVI extension, and low-level primitives moved off the root entry to their subpaths. See MIGRATION.md for the import map.

Quick Start

import { createGatewayApiClient } from "@cavi-ai/api-client";

const gateway = createGatewayApiClient({
  baseUrl: process.env.GATEWAY_API_BASE_URL!,
  auth: {
    bearerToken: process.env.GATEWAY_API_AUTH_TOKEN,
    clientId: "dashboard",
  },
});

const run = await gateway.startRun({
  input: "Summarize the current workspace state.",
  session_id: "dashboard",
});
// run.run_id / run.status are your handle for polling, streaming, or UI state.
// Failures are typed — see Typed Errors below for how to branch on them.

🤖 Claude Managed Agents (Beta)

Anthropic ships a second, far less famous way to run Claude: Managed Agents (beta). Instead of you running the agent loop, Anthropic does — a persisted, versioned agent config spawns stateful sessions, each in its own containerized environment where Claude executes tools (bash, file ops, code), streaming every step over SSE. Skills, MCP servers, file mounts, multiagent threads, and rubric-graded outcomes are all part of it. Most people don't even know it exists.

This package is meant to be the easy on-ramp. It's runtime-only and additive: the same RuntimeClient you already use, one extra import, no app rewrite, and nothing about your existing setup has to change. Already have a harness you like? Keep it — try Managed Agents alongside it and see which fits which job.

import {
  ClaudeManagedAgentClient,
  driveManagedAgentSession,
} from "@cavi-ai/api-client/providers/claude";

const claude = new ClaudeManagedAgentClient({
  apiKey: process.env.ANTHROPIC_API_KEY!, // or `authToken` for an OAuth bearer
});

// Agents + environments are persisted — create them once, reference them by id.
const agent = await claude.createAgent({
  name: "researcher",
  model: "claude-opus-4-8",
  system: "You are a meticulous research assistant.",
});
const env = await claude.createEnvironment({ name: "research-env" });

// A session is one stateful run against that agent in that environment.
const session = await claude.createSession({
  agentId: agent.id,
  environmentId: env.id,
});
await claude.sendMessage(session.id, { input: "Summarize today's commits." });

// Tail the SSE stream, answer tool confirmations + custom tools, survive drops.
await driveManagedAgentSession(claude, session.id, {
  onMessage: (text) => appendAgentText(text),
  onToolConfirmation: () => ({ result: "allow" }), // omit → deny
  onComplete: () => markRunComplete(),
});

Everything is exported from @cavi-ai/api-client/providers/claude and verified against the live beta API (managed-agents-2026-04-01):

  • Sessions & steeringcreateSession, sendMessage/sendEvents, interruptSession, confirmTool, respondCustomTool, openEventStream, and driveManagedAgentSession (a deadlock-safe stream driver with lossless reconnect and dedupe).
  • Agents & environments — persisted, versioned configs and per-session containers (createAgent, updateAgent, createEnvironment).
  • Typed eventsparseSessionEvent + a discriminated ManagedAgentSessionEvent union (messages, tool calls, status, errors, outcomes, threads).
  • OutcomesdefineOutcome for rubric-graded session loops.
  • Multiagent threadslistThreads, openThreadEventStream, and friends.
  • Memory stores — full CRUD over stores, memories, and memory versions.
  • Vaults & MCP credentials — vault/credential CRUD plus validateMcpOauthCredential for static_bearer and mcp_oauth auth.
  • TeamsbuildManagedAgentTeamsPlan maps a TeamManifest to a coordinator + roster.
  • Webhook verificationverifyManagedAgentWebhook implements the Standard Webhooks signing scheme via Web Crypto (verified against the scheme; live delivery is out of scope for this release).
  • Self-hosted environmentsgetWorkQueueStats / stopWork observe and control the work queue (queue monitoring only — the tool-executing worker stays with the host).

The stateless Claude Messages-API client (ClaudeApiClient, below) is unchanged — Managed Agents is an additive sibling under the same subpath.

Core Concepts

One Client Shape

Every provider implements the universal RuntimeClient contract — capability profile, runs (startRun/optional getRun/cancelRun), and optional streamRun. GatewayClient extends RuntimeClient for gateway-style backends, adding teams, kanban, workspace, and operator surfaces. GatewayApiClient implements both tiers; a non-gateway provider (e.g. Claude) implements RuntimeClient only and declares its capability profile so unsupported surfaces fail with a typed EndpointNotFound rather than a hard crash.

import {
  GatewayApiClient,
  createGatewayApiClient,
  createGatewayProviderRegistry,
} from "@cavi-ai/api-client";
import type { GatewayProviderModule } from "@cavi-ai/api-client/core/gateway";

const provider: GatewayProviderModule = {
  kind: "acme",
  aliases: ["acme-gateway"],
  createApiClient: (options) => new GatewayApiClient(options, "acme-api"),
};

const registry = createGatewayProviderRegistry({ modules: [provider] });
const client = createGatewayApiClient(config.gateway, {
  provider: "acme-gateway",
  registry,
});

Provider modules should reuse core transports. They should not fork JSON request handling, RPC flow, SSE parsing, trace redaction, or error normalization.

Providers

Built-in providers register through the same registry; createRuntimeProviderRegistry accepts runtime-only modules (no gateway factories required):

import { createRuntimeProviderRegistry } from "@cavi-ai/api-client";
import { HERMES_PROVIDER_MODULE } from "@cavi-ai/api-client/providers/hermes";
import { OPENCLAW_PROVIDER_MODULE } from "@cavi-ai/api-client/providers/openclaw";
import { createClaudeProviderModule } from "@cavi-ai/api-client/providers/claude";
import { createCodexProviderModule } from "@cavi-ai/api-client/providers/codex";

const registry = createRuntimeProviderRegistry({
  modules: [
    HERMES_PROVIDER_MODULE,
    OPENCLAW_PROVIDER_MODULE,
    createClaudeProviderModule({ apiKey: process.env.ANTHROPIC_API_KEY! }),
    createCodexProviderModule({ apiKey: process.env.OPENAI_API_KEY! }),
  ],
});

registry.resolveProvider("claude-sdk"); // -> the Claude module
registry.resolveProvider("codex"); // -> the Codex Responses module

The Claude (Anthropic) provider is runtime-only — it maps startRun to POST /v1/messages and streams the Messages SSE into the canonical RunStreamEvent:

import { ClaudeApiClient } from "@cavi-ai/api-client/providers/claude";

const claude = new ClaudeApiClient({ apiKey: process.env.ANTHROPIC_API_KEY! });

const run = await claude.startRun({
  input: "Summarize the workspace state.",
  model: "claude-opus-4-8",
  instructions: "Be concise.",
});

await claude.streamRun(
  { input: "Stream a haiku.", model: "claude-opus-4-8" },
  {
    onEvent: (event) => appendRunEvent(event), // canonical RunStreamEvent
    onComplete: () => markRunComplete(),
  },
);

For Claude's stateful, server-run mode — persisted agents, containerized sessions, and SSE steering — see 🤖 Claude Managed Agents (Beta) above; it ships from the same @cavi-ai/api-client/providers/claude subpath.

The Codex provider is also runtime-only. It uses the OpenAI Responses API with gpt-5-codex by default, starts background responses so UIs can poll or cancel by response id, and streams Responses SSE into canonical RunStreamEvents:

import { CodexApiClient } from "@cavi-ai/api-client/providers/codex";

const codex = new CodexApiClient({ apiKey: process.env.OPENAI_API_KEY! });

const run = await codex.startRun({
  input: "Review this component plan for risks.",
  instructions: "Return concise engineering guidance.",
});

await codex.streamRun(
  { input: "Draft the implementation checklist." },
  { onEvent: (event) => appendRunEvent(event) },
);

Keep OpenAI API keys backend-owned. Browser and mobile apps should call your backend, which can instantiate CodexApiClient; they should not embed raw OpenAI credentials.

Writing your own provider? Point the shared conformance kit (src/__tests__/support/runtime-conformance.ts) at your client and it must pass the same contract every built-in provider does.

Credential Schemes

A provider declares its auth scheme through auth.resolveHeaders instead of the core hardcoding a bearer token. Ready-made resolvers cover the common cases:

import { bearerCredentials, apiKeyCredentials } from "@cavi-ai/api-client";

// Gateway bearer token (the default if you pass auth.bearerToken)
const gatewayAuth = { resolveHeaders: bearerCredentials(process.env.GATEWAY_API_AUTH_TOKEN) };

// Anthropic api-key + version header
const anthropicAuth = {
  resolveHeaders: apiKeyCredentials(process.env.ANTHROPIC_API_KEY!, {
    header: "x-api-key",
    extra: { "anthropic-version": "2023-06-01" },
  }),
};

A provider can also report a protocol version; assertProtocolVersion(caps, expected) turns a backend version mismatch into a typed ProtocolMismatch error instead of a confusing downstream failure.

Typed Errors

Every failure is one of the package's typed classes — HttpApiError (non-2xx, network failure, abort, invalid JSON), GatewayHttpError (gateway HTTP detail), GatewayRpcError (WebSocket RPC rejection), or ApiClientError (synthesized config/validation/transport). Branch on a guard, never on error.message, and never re-wrap a failure in a generic Error.

import {
  getErrorStatus,
  isAbortError,
  isAuthError,
  isHttpApiError,
} from "@cavi-ai/api-client";

try {
  await cavi.getOperatorSnapshot();
} catch (error) {
  if (isAbortError(error)) return;          // request was cancelled
  if (isAuthError(error)) return signOut(); // 401/403 across HTTP error classes
  if (getErrorStatus(error) === 404) return markUnavailable();
  if (isHttpApiError(error)) {
    reportError({ status: error.status, path: error.path, body: error.body });
  }
  throw error; // unknown shape: never swallow it
}

isAuthError covers both HttpApiError and GatewayHttpError; getErrorStatus returns the numeric HTTP status or undefined. Lower-level helpers (getErrorMessage, serializeError, toError, and the ApiClientErrorType / ApiClientErrorCode enums) remain available from core/errors.

Antipattern:

// ❌ Message matching breaks on rewording/localization and swallows real failures.
catch (e: any) {
  if (e.message.includes("401")) signOut();
  else console.log("request failed");
}
// ❌ Re-wrapping erases status, path, body, and the cause chain.
throw new Error("snapshot failed");

Graceful Degradation

Gateway loaders can return a typed DataEnvelope<T>:

type DataEnvelope<T> = {
  data: T;
  source: "gateway" | "mock";
  fetchedAt: number;
  contractGaps: ContractGap[];
};

withFallback and withMutationResult keep expected backend gaps visible without crashing an entire dashboard. Auth failures and unknown errors still throw.

Route Mirrors

OpenClaw, Caviclaw plugins, and gateway hosts own their runtime API routes. This package only mirrors those routes so client code has one import path and does not drift into hand-built strings. Mirrored route literals belong in:

  • Global contracts: src/contracts/paths.ts and src/contracts/surfaces.ts.
  • CAVI extension contracts: src/extensions/cavi/contracts/paths.ts and src/extensions/cavi/contracts/surfaces.ts.
  • OpenClaw provider mirrors: src/providers/openclaw/manifest.ts and src/providers/openclaw/workboard.ts.

Consumers should use exported constants and resolvers such as resolvePath and appendHttpQuery (root), and resolveCaviPath plus the CAVI contract helpers (@cavi-ai/api-client/extensions/cavi). Avoid assembling paths by hand in clients, components, or adapters. New or changed paths must come from the upstream gateway/plugin contract first.

Team Manifest

The team manifest is an interface, not data the package owns. The package ships the manifest shape (types + normalization + lookup validation), a TeamRouteResolver, and a TeamManifestSource "bring-your-own-manifest" seam — the host supplies the actual team/member/workspace/action data at runtime. CAVI identity specifics live in identity.metadata, keeping the contract agnostic.

import {
  createStaticManifestSource,
  createTeamRouteResolver,
  normalizeTeamManifest,
  resolveTeamWorkspaceApiPath,
  type TeamManifest,
} from "@cavi-ai/api-client";

const source = createStaticManifestSource({
  version: 1,
  teams: [
    {
      id: "research",
      identity: { displayName: "Research", slug: "research", code: "RND" },
      workspace: {
        rootPath: "/teams/research/workspace",
        paths: ["reports", { key: "media.images", path: "media/images" }],
      },
      members: [{ id: "analyst", capabilities: ["research.read"] }],
    },
  ],
} satisfies TeamManifest);

const manifest = await source.getManifest();
const resolver = createTeamRouteResolver();
const path = resolver.resolveWorkspaceApiPath(manifest, "research", "media.images", {
  memberId: "analyst",
});

The CAVI team registry (which overlays operator-dispatch data on top of the generic manifest) lives in @cavi-ai/api-client/extensions/cavicreateTeamRegistry, configureTeamRegistryConfig, TEAM_REGISTRY_CONFIG.

See docs/team-manifest.md and docs/team-manifest.consumer.template.ts for consumer-owned manifest examples.

Common Surfaces

HTTP

import {
  CaviControlApiClient,
  resolveHttpApiConfigFromEnv,
} from "@cavi-ai/api-client/extensions/cavi";

const config = resolveHttpApiConfigFromEnv(process.env);
const cavi = new CaviControlApiClient({
  baseUrl: config.cavi.baseUrl,
  auth: {
    bearerToken: config.cavi.authToken,
    clientId: config.cavi.clientId,
  },
});

Canonical environment keys:

  • CAVI_API_BASE_URL, CAVI_API_AUTH_TOKEN, CAVI_API_CLIENT_ID
  • GATEWAY_API_BASE_URL, GATEWAY_API_AUTH_TOKEN, GATEWAY_API_CLIENT_ID
  • LIBRARY_API_BASE_URL, LIBRARY_API_AUTH_TOKEN, LIBRARY_API_CLIENT_ID

Alias keys for common web and mobile app runtimes are supported by default. Pass { includeAliases: false } to disable aliases.

WebSocket RPC

import { createGatewayWebSocketClient } from "@cavi-ai/api-client";

const ws = createGatewayWebSocketClient(target.wsUrl, authToken, {
  clientId: "dashboard",
  requestedScopes: ["operator.read"],
});

await ws.connect();
const sessions = await ws.request<{ sessions: unknown[] }>("sessions.list", {
  limit: 20,
});

Run Event Streams

import { createGatewaySseRunEventProvider } from "@cavi-ai/api-client";

const stream = createGatewaySseRunEventProvider({
  httpBase: config.gateway.baseUrl,
  authToken: config.gateway.authToken,
  clientId: config.gateway.clientId,
  sessionKey: "dashboard",
});

// Keep the subscription handle so you can `subscription.dispose()` on unmount.
const subscription = await stream.subscribe(
  { runId: "run-123" },
  {
    onEvent: (event) => appendRunEvent(event),
    onError: (error) => reportError(error),
    onComplete: () => markRunComplete(),
  },
);

Media And Wiki

import {
  createGatewayMediaClient,
  createGatewayWikiClient,
} from "@cavi-ai/api-client";

const media = createGatewayMediaClient({ baseUrl, auth });
const image = await media.generateImage({
  input: "diagram of a workflow",
  format: "png",
});

const wiki = createGatewayWikiClient({ baseUrl, auth });
const page = await wiki.readWikiPage("default", "index.qmd");

React

import {
  GatewayClientProvider,
  useGatewayClientContext,
} from "@cavi-ai/api-client/frameworks/react";

function App() {
  return (
    <GatewayClientProvider
      gatewayBaseUrl={gatewayBaseUrl}
      authToken={authToken}
      clientId="portal"
      autoReconnect
    >
      <Panel />
    </GatewayClientProvider>
  );
}

function Panel() {
  const { client, state, connect } = useGatewayClientContext();
  return <button onClick={() => void connect()}>{state}</button>;
}

CAVI Extension Adapters

extensions/cavi contains product-shaped composition over the generic core. The extension mirrors CAVI-specific DTOs, adapters, fallback providers, and plugin route contracts while still using shared transports and error handling.

import { createCaviControlAdapters } from "@cavi-ai/api-client/extensions/cavi";

const adapters = createCaviControlAdapters({
  gatewayBaseUrl,
  apiBaseUrl,
  authToken,
  client: gatewayRpcClient,
  fallbackMode: "empty",
});

const overview = await adapters.loadOverview();

Secure Credential Handling

The client never persists credentials — applications pass auth.bearerToken and own storage. On devices that means the OS keychain/keystore or an encrypted store, never plaintext AsyncStorage or localStorage. Keep the client storage-agnostic behind one interface:

export interface TokenStore {
  get(): Promise<string | null>;
  set(token: string): Promise<void>;
  clear(): Promise<void>;
}

expo-secure-store (Keychain / Keystore) and react-native-mmkv (encrypted instance) are drop-in backends:

import * as SecureStore from "expo-secure-store";
import { MMKV } from "react-native-mmkv";

const KEY = "cavi.gateway.bearer";

export const secureStoreTokens: TokenStore = {
  get: () => SecureStore.getItemAsync(KEY),
  set: (token) =>
    SecureStore.setItemAsync(KEY, token, {
      keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
    }),
  clear: () => SecureStore.deleteItemAsync(KEY),
};

const storage = new MMKV({ id: "cavi-auth", encryptionKey: deviceEncryptionKey });
export const mmkvTokens: TokenStore = {
  get: async () => storage.getString(KEY) ?? null,
  set: async (token) => storage.set(KEY, token),
  clear: async () => storage.delete(KEY),
};

Build the client from the stored token and refresh only on a typed auth failure — isAuthError covers HttpApiError and GatewayHttpError, so you never re-check .status by hand:

import { isAuthError } from "@cavi-ai/api-client";
import { CaviControlApiClient } from "@cavi-ai/api-client/extensions/cavi";

async function withFreshAuth<T>(
  tokens: TokenStore,
  call: (client: CaviControlApiClient) => Promise<T>,
): Promise<T> {
  const build = async () =>
    new CaviControlApiClient({
      baseUrl: config.cavi.baseUrl,
      auth: { bearerToken: (await tokens.get()) ?? undefined, clientId: "cavi-mobile" },
    });
  try {
    return await call(await build());
  } catch (error) {
    if (!isAuthError(error)) throw error; // only refresh on 401/403
    await tokens.set(await refreshAccessToken());
    return call(await build());
  }
}

Avoid logging raw request headers, auth tokens, or full error bodies; trace helpers redact sensitive values before emitting previews.

Antipattern:

// ❌ Plaintext token on disk — readable by backups, other apps, rooted devices.
await AsyncStorage.setItem("token", token);
localStorage.setItem("token", token);

Architecture

See ARCHITECTURE.md for the layer map, provider boundary, route mirror rules, and extension/plugin split.

Development

pnpm install
pnpm test
pnpm run build
pnpm run lint:md
pnpm run verify

Strict TypeScript is the lint gate. Relative source imports use .js extensions. Tests live under src/__tests__/**.

The hardening tests in src/__tests__/package-hardening.test.ts enforce package boundaries, path ownership, forbidden imports, and output shape. Update them only when the package boundary intentionally changes.

Contributing

See CONTRIBUTING.md for workflow, provider-author guidance, and boundary rules.

Code of Conduct

Participation in this project is covered by CODE_OF_CONDUCT.md.

Security

See SECURITY.md for vulnerability reporting.

License

MIT