# connect() and the card

> Connecting a card to NeuroSquad, the typed Card object, its live context, calling any method and listening to any event.

Source: https://docs.neurosquad.ai/en/card-sdk/api

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

```ts
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`](#the-card-object). 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?)`

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

| Option | Default | Meaning |
| --- | --- | --- |
| `theme` | `true` | `false` to leave your CSS alone; an element to put the `--ns-*` variables on it instead of `<html>`. See [Theme](https://docs.neurosquad.ai/en/card-sdk/api/environment#theme). |
| `syncLang` | `true` | Keep `<html lang>` equal to the app's language. |
| `forwardErrors` | `true` | Forward `error` and `unhandledrejection` to the [card log](https://docs.neurosquad.ai/en/card-sdk/api/environment#log). |
| `timeoutMs` | `10000` | Waiting for the app's channel, then for its answer. |
| `port` | — | Use this channel instead of waiting for the app — for the [mock host](https://docs.neurosquad.ai/en/card-sdk/testing). |

It rejects with a [`CardSdkError`](https://docs.neurosquad.ai/en/card-sdk/api/errors):

- `UNAVAILABLE` — the page is not inside a NeuroSquad card (opened on its own in a browser). For a
preview, use the [mock host](https://docs.neurosquad.ai/en/card-sdk/testing), 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

| Member | What |
| --- | --- |
| `card.context` | Everything the app told the card, [kept current](#context). |
| `card.setTitle / setStatus / setBadge / setOverview / attention / requestResize / openLink / focusCard / spawn` | [Card chrome](https://docs.neurosquad.ai/en/card-sdk/api/card-ui) |
| `card.ui` | [Toasts, confirm dialog, the card's menu](https://docs.neurosquad.ai/en/card-sdk/api/card-ui#ui) |
| `card.storage` | [Per-card and per-package storage](https://docs.neurosquad.ai/en/card-sdk/api/storage-settings#storage) |
| `card.settings` | [The settings form's values](https://docs.neurosquad.ai/en/card-sdk/api/storage-settings#settings) |
| `card.agents` | [Agents, their statuses, output and prompts](https://docs.neurosquad.ai/en/card-sdk/api/agents) |
| `card.terminals` | [Commands in connected terminals](https://docs.neurosquad.ai/en/card-sdk/api/agents#terminals) |
| `card.ports` | [Typed data to and from connected cards](https://docs.neurosquad.ai/en/card-sdk/api/ports) |
| `card.tools` | [Tools for connected agents](https://docs.neurosquad.ai/en/card-sdk/api/tools) |
| `card.net` | [HTTP through the app's proxy](https://docs.neurosquad.ai/en/card-sdk/api/network) |
| `card.fs` | [Files in the workspace folder](https://docs.neurosquad.ai/en/card-sdk/api/files) |
| `card.permissions` | [Grant state, asking for optional ones](https://docs.neurosquad.ai/en/card-sdk/api/environment#permissions) |
| `card.lifecycle` | [Visibility, suspend, expand, resize](https://docs.neurosquad.ai/en/card-sdk/api/environment#lifecycle) |
| `card.log` | [Lines for the card log](https://docs.neurosquad.ai/en/card-sdk/api/environment#log) |
| `card.copyText / usage / getWorkspace / getTheme / getI18n` | [Clipboard, usage](https://docs.neurosquad.ai/en/card-sdk/api/files), fresh snapshots |
| `card.host` | [Feature detection](#feature-detection) |
| `card.call / on / once / waitFor` | [Any method, any event](#any-method-any-event) |
| `card.close() / closed` | Close 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`.

```ts
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)))
```

| Field | Type | Meaning |
| --- | --- | --- |
| `instance` | `CardInstanceInfo` | `instanceId` (this card's id on the canvas), `packageId`, `name`, `displayName`, `version`, `commit` (`null` for a linked folder), `title`, `size`, `dev`. |
| `workspace` | `WorkspaceInfo` | `id`, `name`, and `path` (absolute) only with `fs.read`. |
| `visibility` | `'visible'`, `'offscreen'`, `'overview'` or `'hidden'` | See [Lifecycle](https://docs.neurosquad.ai/en/card-sdk/api/environment#lifecycle). |
| `expanded` | `boolean` | The card is expanded to fill the canvas. |
| `theme` | `ThemeSnapshot` | The app's colours, radii and fonts. |
| `i18n` | `{ language, locale }` | `en`, `ru` or `zh`, and a locale for `Intl`. |
| `settings` | `SettingsSnapshot` | `values`, and `secrets` (which secret keys are set). |
| `permissions` | `PermissionState[]` | Every declared permission with `granted`, `optional`, `hosts`, `reason`. |
| `ports` | `{ inputs, outputs }` | Your own ports, labels resolved. |
| `peers` | `PeerInfo[]` | Cards connected by arrows. |
| `launch` | `'created'`, `'opened'`, `'resumed'`, `'reloaded'` or `'updated'` | Why this frame started. |
| `spawnInit` | JSON or absent | Data from the card that [spawned](https://docs.neurosquad.ai/en/card-sdk/api/card-ui#spawn) this one, on its first start. |
| `chrome` | `CardChromeState` or absent | What the app is showing for this card right now — `title`, `status`, `badge`, `overview`, `attention` — kept across reloads. See [Card chrome](https://docs.neurosquad.ai/en/card-sdk/api/card-ui#attention). Absent on older app versions. |
| `appVersion` | `string` | NeuroSquad's version. |
| `limits` | `LIMITS` | Every limit of the protocol, see [Limits](https://docs.neurosquad.ai/en/card-sdk/api/errors#limits). |
| `protocol` | `number` | The 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](https://docs.neurosquad.ai/en/card-sdk/api/environment#lifecycle)),
`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:

```ts
// 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()`](https://docs.neurosquad.ai/en/card-sdk/api/agents#output).
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

| Event | Payload | When |
| --- | --- | --- |
| `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.changed` | `SettingsSnapshot` | Settings changed (form or `settings.set`). |
| `theme.changed` | `ThemeSnapshot` | The app's theme changed. |
| `i18n.changed` | `{ language, locale }` | The app's language changed. |
| `permissions.changed` | `PermissionState[]` | A grant changed. |
| `ui.menu` | `{ id }` | A menu item you added was chosen. |
| `ports.message` | see [Ports](https://docs.neurosquad.ai/en/card-sdk/api/ports#receiving) | A value arrived on an input. |
| `ports.request` | see [Ports](https://docs.neurosquad.ai/en/card-sdk/api/ports#requests) | A peer asks a request input. Use `card.ports.onRequest`. |
| `ports.peersChanged` | `PeerInfo[]` | Arrows or peers changed. |
| `tools.call`, `tools.cancel` | see [Tools](https://docs.neurosquad.ai/en/card-sdk/api/tools) | An agent calls a tool. Use `card.tools.handle`. |
| `net.chunk` | see [Network](https://docs.neurosquad.ai/en/card-sdk/api/network#streaming) | A 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:

```ts
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](https://docs.neurosquad.ai/en/card-sdk/api/ports#direction)).
- **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.
