Skip to Content
Card SDKAPI referenceconnect() and the card

connect() and the card

Everything a card does goes through one object, the card, which you get from connect():

import { connect } from '@neurosquad/card-sdk' const card = await connect() card.setStatus(`Hello, ${card.workspace.name}`, { tone: 'success' })

connect() waits for the app to hand the card its private channel, introduces itself, and resolves with a Card. Calling it again returns the same card.

Import the SDK from your entry script. It starts listening for the app’s handshake the moment it loads. A card that loads the SDK late (a dynamic import() after a timer) can miss it and fail with UNAVAILABLE.

connect(options?)

import { connect } from '@neurosquad/card-sdk' const card = await connect({ theme: true, // apply the app theme as --ns-* CSS variables on <html> (default true) syncLang: true, // keep <html lang> equal to the app language (default true) forwardErrors: true, // send uncaught errors and rejections to the card log (default true) timeoutMs: 10_000 // how long to wait for the app (default 10 000) })
OptionDefaultMeaning
themetruefalse to leave your CSS alone; an element to put the --ns-* variables on it instead of <html>. See Theme.
syncLangtrueKeep <html lang> equal to the app’s language.
forwardErrorstrueForward error and unhandledrejection to the card log.
timeoutMs10000Waiting for the app’s channel, then for its answer.
port—Use this channel instead of waiting for the app — for the mock host.

It rejects with a CardSdkError:

  • UNAVAILABLE — the page is not inside a NeuroSquad card (opened on its own in a browser). For a preview, use the mock host, as the templates do.
  • PROTOCOL_MISMATCH — the app is older than the card protocol your SDK speaks. The card shows “needs a newer NeuroSquad”.

The card object

MemberWhat
card.contextEverything the app told the card, kept current.
card.setTitle / setStatus / setBadge / setOverview / attention / requestResize / openLink / focusCard / spawnCard chrome
card.uiToasts, confirm dialog, the card’s menu
card.storagePer-card and per-package storage
card.settingsThe settings form’s values
card.agentsAgents, their statuses, output and prompts
card.terminalsCommands in connected terminals
card.portsTyped data to and from connected cards
card.toolsTools for connected agents
card.netHTTP through the app’s proxy
card.fsFiles in the workspace folder
card.permissionsGrant state, asking for optional ones
card.lifecycleVisibility, suspend, expand, resize
card.logLines for the card log
card.copyText / usage / getWorkspace / getTheme / getI18nClipboard, usage, fresh snapshots
card.hostFeature detection
card.call / on / once / waitForAny method, any event
card.close() / closedClose the channel; later calls fail with UNAVAILABLE.

The context

card.context is a HostContext: what the app sent when the card connected, updated from every event (visibility, size, theme, language, settings, permissions, peers). The object is replaced, never mutated, on each change — safe to use with React’s useSyncExternalStore.

import type { HostContext } from '@neurosquad/card-sdk' function describe(context: HostContext): string { const { instance, workspace, visibility, i18n, launch } = context return `${instance.displayName} ${instance.version} in "${workspace.name}", ${visibility}, ${i18n.language}, started: ${launch}` } card.onContextChange((context) => render(describe(context)))
FieldTypeMeaning
instanceCardInstanceInfoinstanceId (this card’s id on the canvas), packageId, name, displayName, version, commit (null for a linked folder), title, size, dev.
workspaceWorkspaceInfoid, name, and path (absolute) only with fs.read.
visibility'visible', 'offscreen', 'overview' or 'hidden'See Lifecycle.
expandedbooleanThe card is expanded to fill the canvas.
themeThemeSnapshotThe app’s colours, radii and fonts.
i18n{ language, locale }en, ru or zh, and a locale for Intl.
settingsSettingsSnapshotvalues, and secrets (which secret keys are set).
permissionsPermissionState[]Every declared permission with granted, optional, hosts, reason.
ports{ inputs, outputs }Your own ports, labels resolved.
peersPeerInfo[]Cards connected by arrows.
launch'created', 'opened', 'resumed', 'reloaded' or 'updated'Why this frame started.
spawnInitJSON or absentData from the card that spawned this one, on its first start.
chromeCardChromeState or absentWhat the app is showing for this card right now — title, status, badge, overview, attention — kept across reloads. See Card chrome. Absent on older app versions.
appVersionstringNeuroSquad’s version.
limitsLIMITSEvery limit of the protocol, see Limits.
protocolnumberThe card protocol the app speaks.

Shortcuts on the card: card.instanceId, card.instance, card.workspace, card.visibility, card.expanded, card.size, card.theme, card.i18n, card.language, card.launch, card.spawnInit, card.limits, card.appVersion.

launch tells you what happened: created (just added to the canvas), opened (the workspace was opened), resumed (back after being suspended), reloaded (the user pressed Reload, a file changed in dev mode, or a permission was revoked), updated (a new version of your card was installed).

Any method, any event

The namespaces are sugar over two primitives, both fully typed from the protocol:

// Any method: params and result are typed from the method name. const { keys } = await card.call('storage.keys', { scope: 'instance', prefix: 'draft:' }) const info = await card.call('card.getInfo') // Any event: the payload is typed from the event name. Returns a function that removes the listener. const off = card.on('agents.turn', (turn) => { if (turn.phase === 'end') render(`${turn.agentId} finished a turn`) }) off() // Once, or as a promise. card.once('lifecycle.expanded', ({ expanded }) => render(expanded)) const next = await card.waitFor('agents.status', (e) => e.status === 'needs-input', { timeoutMs: 60_000 }) render(next.agentId)

Subscribable topics (agents.status, agents.turn, agents.changed, storage.changed) are subscribed with the app automatically while at least one listener exists, and unsubscribed when the last goes. agents.output needs agent ids — use card.agents.onOutput(). If a topic needs a permission you do not have, the listener simply gets nothing, and a warning goes to the card log.

Events that could arrive before you attach a listener — ports.message, ui.menu, fs.changed — are kept (up to 100) and delivered to the first listener, so nothing is lost between connect() and your on().

All events

EventPayloadWhen
lifecycle.visibility{ state }Visibility changed.
lifecycle.suspend{ graceMs }The frame is about to be unloaded. Use card.lifecycle.onSuspend.
lifecycle.expanded{ expanded }Expanded or restored.
lifecycle.resized{ w, h }The card was resized.
settings.changedSettingsSnapshotSettings changed (form or settings.set).
theme.changedThemeSnapshotThe app’s theme changed.
i18n.changed{ language, locale }The app’s language changed.
permissions.changedPermissionState[]A grant changed.
ui.menu{ id }A menu item you added was chosen.
ports.messagesee PortsA value arrived on an input.
ports.requestsee PortsA peer asks a request input. Use card.ports.onRequest.
ports.peersChangedPeerInfo[]Arrows or peers changed.
tools.call, tools.cancelsee ToolsAn agent calls a tool. Use card.tools.handle.
net.chunksee NetworkA streamed response chunk. Use response.chunks().
fs.changed{ watchId, path, type }A watched file changed. Use card.fs.watch.
storage.changed{ scope, key, byInstance }Another copy of the card wrote a package key. Topic.
agents.status{ agentId, status, at }An agent’s status changed. Topic, agents.read.
agents.turn{ agentId, phase, at }A turn started or ended. Topic, agents.read.
agents.changed{ agents }Agents added, removed or renamed. Topic, agents.read.
agents.output{ agentId, text, at }Output of a connected agent. agents.output.
host.ping{ seq }Heartbeat — the SDK answers for you.

Feature detection

New methods and events arrive within a protocol version. Check before you use one the user’s app might not have yet:

if (await card.host.supports('usage.summary')) { const usage = await card.usage('today') render(usage.totalCostMicroUsd) } const { protocol, methods, events } = await card.host.capabilities() render(protocol, methods.length, events.length) // A method newer than your SDK's types: const result = await card.callUnchecked('some.newMethod', { any: 'params' }) render(result)

Conventions

  • Ids. Card and agent ids are the canvas ids — the same cardId you get from card.ports.peers, card.agents.list() or card.spawn().
  • Connected means an arrow between the two cards, in either direction. Ports additionally care about the direction (see Ports).
  • Every call is checked twice. The SDK validates params against the same schemas the app uses and fails fast with INVALID_PARAMS and the exact path; the app checks again, plus permissions, visibility, arrows and rate limits.
  • Only JSON crosses. No Blob, no ArrayBuffer: the helpers that take bytes (fs.writeBytes, net.fetch bodies) encode them as base64 for you.
  • Replies can come out of order; events arrive in the order the app sent them.
  • Never trust window message events. Any other card on the canvas can postMessage your frame. The only trusted channel is the one the SDK receives from the app at connect(); do not add your own message listeners that act on what arrives.