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

@ade-dev/sdk

v0.5.1

Published

Typed Node/Electron-main client that owns a slim ADE runtime and exposes chat as durable named threads.

Readme

@ade-dev/sdk

Typed Node / Electron-main client that spawns a slim ADE runtime as a sidecar and exposes chat as durable named threads.

Docs: ADE SDK — install, threads, MCP honesty table, chat UI, runtime, and API reference.

| Page | What it covers | |---|---| | Threads | Lifecycle, attachments, paged history, instructions, working directory, configuration layers, model switching | | MCP servers | Injected tools, credentials that change, resource links, and the strict-mode honesty table | | Permissions | The policy object, per-provider enforcement, answering approvals | | Chat UI | @ade-dev/chat-ui, theming, approval cards, provider cards | | Electron | The main / preload / renderer bridge under sandbox: true | | Bundling | Platform packages, codesign, entitlements, electron-builder | | Runtime | The sidecar, exit events and autoRestart, version compatibility, and doctor().runtime provenance | | Reference | Every option, shape, and error code | | License | What each artifact is licensed under |

npm install @ade-dev/sdk

Requires Node 22. The sidecar is a guest: isolated home, sync off, no machine-brain authority. It dies with your process.

SDK 0.5.1 supports runtime >=1.2.82 <2.0.0 (SUPPORTED_RUNTIME_RANGE). The lower bound did not move in 0.5 — nothing in that line needs a new wire, so a host on runtime 1.2.82 keeps working. An older or newer runtime still connects, logs one warning, and degrades feature by feature. Pass requireCompatibleRuntime: true to refuse it with runtime_incompatible. doctor().runtime.compatibility reports the verdict.

Quickstart

import { createAdeChat } from "@ade-dev/sdk";

const ade = await createAdeChat({ home: "./.ade-embed" });
const thread = await ade.threads.open("support", {
  provider: "claude",
  model: "claude-sonnet-4-5",
});
thread.on("event", (envelope) => console.log(envelope.event.type));
await thread.send("Summarise today's incidents");

Reopening "support" after a restart resumes the same conversation. Call ade.dispose() when the host shuts down.

Provider auth

The sidecar reuses the provider CLIs and logins already on the machine (claude, codex, cursor, …). ADE state under home is isolated; Claude / Codex / Cursor credentials still live in those tools' own config homes (~/.claude, ~/.codex, ~/.cursor). If the host user can already run that provider in a terminal, the sidecar can too.

ade.providers.status() reports, per provider, whether a binary was found, at which path and version, and whether credentials are present. Read source first: "probed" means the runtime looked at the filesystem, "derived" means it is an older runtime inferring from the model catalog. ade.providers.refresh() bypasses the runtime's 60-second probe cache.

ade.doctor() is the structured health check: binary path and provenance, socket, event transport, providers, and known threads.

const report = await ade.doctor();
if (!report.ok) console.error(report);

Pin a specific ADE build with binaryPath, or let the client download a release. Downloads are fail-closed against that release's SHA256SUMS.

Threads

const thread = await ade.threads.open("support", {
  provider: "claude",
  model: "claude-sonnet-4-5",
  permissions: "always-allow",
});

await thread.send("What changed?");
await thread.steer("Focus on the outage");   // mid-turn follow-up
await thread.steer("", { attachments: [{ path: "/abs/path/log.txt" }] });
await thread.interrupt();
await thread.setModel("claude-opus-4");      // see setModel below
await thread.update({ title: "Outage review" });

const page = await thread.historyPage({ limit: 200 });   // newest page
const rows = await ade.threads.list();                   // title, archived, updatedAt, modelSelection
await ade.threads.archive("support");                    // or unarchive / delete

threads.open(key) is open-or-resume by key stored under home. Keys survive host restarts. exportThread(key) returns the transcript as one JSON envelope per line. thread.title and thread.model (with displayName) are on the handle.

threads.delete(key) removes the runtime session, the transcript and the key. archive keeps both. Each interrupts a running turn first.

thread.update({ title, reasoningEffort, fastMode }, { force }) renames at any time. The other two fields are refused mid-turn unless { force: true }.

Attachments are { path, name?, mimeType?, type?, hydrate? } refs. type ("file" or "image") is inferred from the MIME type or extension when absent, so an image reaches the model as an image. hydrate: false sends the path only and the runtime never reads the bytes. An absolute path must be inside the chat's cwd, <home>/personal-chats/state, or a directory in the thread's attachmentRoots (0.4, runtime 1.2.82). See Attachments for the size limits.

One refused attachmentRoots entry fails the whole open, and a host normally sends the same roots every time. Test a candidate with checkAttachmentRoot, or prepare the whole list with filterAttachmentRoots, which drops a bad entry and reports why (both 0.5). A list identical to the one already in use is not sent again, so a repeated list costs no update call.

thread.retry() and thread.editLast(text) (0.4, runtime 1.2.82) roll the provider back and run the last user message again, and the transcript shows it once. Claude and Codex only; another provider throws unsupported, and a running turn throws turn_in_flight. The rollback covers the conversation, not the disk: files the old turn wrote stay, and the result does not report them. If you allow writes, inspect the events at or after retractedFromSequence in your own history copy and warn the user.

models.defaultModel({ prefer }) (0.5) takes your own ranking inside one provider's models. Return a number above zero to prefer a model — the highest score wins — so a host that wants the newest Sonnet, not the provider's default, states it here instead of keeping its own copy of the rule. A model nobody scores falls back to isDefault, then to catalog order.

Host configuration applies on create only. A resume re-applies what the key was created with and ignores the cwd, instructions, settingSources, permissions, mcpServers and loadUserMcpServers passed to that call, logging one line per option it ignored. Open a different key to run under different configuration — do not assume a tighter policy took effect because you passed one. The one exception is refresh.mcpServers (see below).

When the runtime lost a key's session, the SDK recreates it from the stored record. The record wins for provider, model, cwd, instructions, settingSources, permissions, mcpServers and loadUserMcpServers; the call's options only fill a field the record lacks. Before 0.3 the call's options won. A recreate changes thread.id, so key your state on key.

setModel returns the resolved model with displayName and the four capability reports on the new provider, and emits capabilities_changed on the status channel.

setModel refuses while a turn is in flight — call interrupt() first, or pass { force: true } to accept losing the turn. dispose() is not guarded that way: a shutdown that can refuse is worse than a truncated reply. The transcript is durable either way.

MCP servers and strict mode

const thread = await ade.threads.open("ops", {
  provider: "claude",
  model: "claude-sonnet-4-5",
  mcpServers: {
    docs: { type: "http", url: "https://mcp.example/mcp" },
  },
  // default: withhold the user's own MCP config
  // loadUserMcpServers: true  // opt back in
});

if (thread.mcpCapability?.strictRequested && thread.mcpCapability.level !== "enforced") {
  console.warn(thread.mcpCapability.residual);
}

Supplying mcpServers turns on strict mode unless you set loadUserMcpServers: true. False is not a uniform guarantee. Only Claude can enforce it. Everywhere else ADE applies the strongest mechanism that provider exposes, and Pi has no MCP surface at all (injected servers are refused rather than opening a tool-less thread):

| Provider | Strict mode | What still loads under strict | |---|---|---| | claude | enforced | nothing MCP-wise (user rules/commands/output styles still load — they are not MCP) | | codex | best-effort | servers contributed by a Codex plugin | | cursor | best-effort | user-layer servers (~/.cursor; ADE's own preToolUse hook lives there) | | droid | best-effort | tools that appear only after the first disable pass | | opencode | best-effort | the global OpenCode config directory (for auth) | | pi | unsupported | n/a — create refuses injected servers |

The server shape is checked against the selected provider before launch. Codex does not support sse, so a Codex request containing an SSE entry is rejected. Server names computer_use and ade-cto are reserved, and a request may contain at most 32 servers.

Read thread.mcpCapability after open. strictRequested first, then level. "enforced" is the only value that means "nothing but the servers I supplied". Do not tell your users "only your tools are loaded" without checking that.

Credentials that change

The SDK never writes MCP header values to disk. The thread store keeps header names only, and a pre-0.3 store is migrated on first read. The runtime does the same. So supply current values in one of three ways:

const ade = await createAdeChat({
  home,
  mcpHeaders: (key, server) => (server === "app" ? { Authorization: `Bearer ${token()}` } : undefined),
});
await ade.threads.open("support", { refresh: { mcpServers: { app: { type: "http", url, headers } } } });
await thread.updateMcpServers({ app: { type: "http", url: newUrl, headers: newHeaders } });

refresh.mcpServers replaces the servers on a resume or a recreate. thread.updateMcpServers() replaces them on a live thread, for the next turn; it is refused mid-turn and on a runtime before 1.2.81. Without any of the three, a server connects without its headers and the SDK logs a line. URL query strings and stdio env values are still stored; do not put secrets there.

Tool results carry MCP resource_link items as event.resourceLinks on Codex and on Claude (MCP tools only). Other providers do not pass them.

Shaping the session

threads.open takes four options beyond the provider and model. Each one is optional, each persists on the thread record, and omitting all four reproduces 0.1.x behavior exactly.

const thread = await ade.threads.open("assistant", {
  provider: "claude",
  model: "claude-sonnet-4-5",
  instructions: { mode: "replace", text: "You are Ada, MyApp's assistant." },
  cwd: "/Users/me/Library/Application Support/MyApp/work",
  settingSources: "project",
  permissions: {
    allowedTools: ["mcp:catalog:*", "Read"],
    deniedTools: ["Bash"],
    fallback: "deny",
  },
});
  • instructions — your own system instructions, never a hidden first message. A bare string means append. See Threads.
  • cwd — the absolute directory the provider runs in. Defaults to a scratch workspace under home. Relative paths and ~ are refused.
  • settingSources — which on-disk config the provider loads. "none" is the default and is what every 0.1.x thread got.
  • permissions — a policy object is the third form, alongside "default" and "always-allow". fallback is required. See Permissions.

Each one reports back what the provider actually did, the same way mcpCapability does. Read thread.instructionsCapability, thread.settingSourcesCapability and thread.permissionCapability, and branch on level — the presence of a report is never itself a guarantee.

An approval blocks the turn until it is answered. Handle approval_request with thread.approve(itemId, decision), restore cards after a reload with thread.pendingApprovals(), or pass fallback: "deny" so nothing ever asks. approvalTimeoutMs on the policy (opt-in, enforced by the SDK) declines an approval-shaped request that this client saw and nobody answered in time.

On Codex, every MCP tool call raises an approval, and the policy judges it as mcp:<server>:<tool>: deniedTools, then allowedTools / autoApproveMcpServers, then fallback. accept_always remembers the answer for the session only. On Codex, sandboxRoot still decides command and file escapes, and the tool fields do not.

On Claude (Agent SDK 0.3.280), canUseTool fires for MCP tools and for commands that change things, not for commands Claude Code classifies as read-only. fallback: "ask" stays best-effort for that reason.

On Claude a deny fallback does more than skip the prompt. It removes every mutating built-in the policy does not name from the model's catalog, and scopes MCP to the servers the policy names — including servers you injected yourself, so name those in the policy too. sandboxRoot is not applied in that mode: an unnamed tool is refused outright rather than contained. Read thread.permissionCapability.residual, and see Permissions.

Electron

Run this from the main process, not the renderer. Spawn owns a child process and a socket / named pipe.

const ade = await createAdeChat({
  home: path.join(app.getPath("userData"), "ade"),
});

Do not write the bridge yourself. @ade-dev/sdk/electron, @ade-dev/sdk/electron/preload and @ade-dev/sdk/electron/renderer ship one function per process, with listener teardown on reload and the history-and-live ordering rule already in code. None of the three depends on electron. @ade-dev/sdk/electron/global types window.ade. For a sandbox: true preload, point webPreferences.preload at require.resolve("@ade-dev/sdk/electron/preload-auto") (default key and prefix), copy the ten-line preload from the docs, or bundle exposeAdeBridge into one file. @ade-dev/sdk/electron/preload alone only exports the function and exposes nothing. See Electron.

The renderer does not configure threads. Pass openOptions: (key, rendererOptions) => ThreadOpenOptions to registerAdeIpc and main decides; without it, only provider, model, title and reasoningEffort cross (breaking in 0.3). allowModel gates model choice. registerAdeIpc also accepts () => client, for a host that replaces its client per account.

onThreadRemoved(key, kind) (0.5) fires for every delete and archive the served client reports, whether the renderer or your own main-process code made it, so your chat list stays exact. It follows a client swapped per account as soon as the bridge serves a call. Refuse threads.delete or threads.archive in authorize to stop the renderer from making them at all — there is no separate deny list.

To ship the runtime inside a signed app instead of downloading it, install @ade-dev/runtime, copy it into your resources, and pass ...resolvePackagedRuntime(process.resourcesPath) with allowDownload: false. doctor().runtime.source then reports "packaged". See Bundling.

Runtime exits

const ade = await createAdeChat({ home, autoRestart: true });
ade.on("exit", ({ code, signal, error }) => {});
ade.on("transport", ({ state }) => {});          // "closed" | "reconnected"
ade.on("restart", ({ attempt, ok }) => {});

When the runtime exits, each live thread gets a synthetic { type: "status", turnStatus: "failed", message, synthetic: true } envelope. autoRestart (off by default; true = 5 attempts, 1 s doubling backoff) respawns the runtime and re-opens every live thread. The client object stays the same. A turn in flight is not resumed. See Runtime.

The live seam test

npm test runs against an in-process fake runtime, so it can only prove the SDK agrees with itself. test/live.integration.test.ts boots a real ADE runtime and checks that both sides agree on the wire. It is opt-in and skips itself when the binary is not named.

Build the runtime first, then point the test at it:

cd apps/ade-cli && npm run build
cd packages/sdk
ADE_SDK_LIVE_BINARY=../../apps/ade-cli/dist/cli.cjs npm run test:live

That run spends no provider tokens. It covers the advertised capabilities, the providers.status probe, the host configuration on the raw session summary, the cwd refusals on both sides, pendingApprovals(), and a cold resume.

Add ADE_SDK_LIVE_SPEND=1 to also run the cases that need a model turn. They need a provider that is installed and logged in, and they cost a few cents on the cheapest model in the catalog:

ADE_SDK_LIVE_BINARY=../../apps/ade-cli/dist/cli.cjs ADE_SDK_LIVE_SPEND=1 npm run test:live

Read the header of test/live.integration.test.ts before you trust a green spending run. Claude's own permission default can accept every tool without asking, and on a machine configured that way the approval cases record that fact instead of proving the round trip.

License

MIT. See LICENSE. ADE itself remains AGPL-3.0-only; see the ADE Runtime Embedding Exception for shipping the runtime binary in your app.