@neurosquad/card-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/dist/card-sdk.js +5266 -0
- package/dist/cli.js +2838 -0
- package/dist/react.js +257 -0
- package/dist/testing.js +1150 -0
- package/dist/types/client/base64.d.ts +7 -0
- package/dist/types/client/card.d.ts +653 -0
- package/dist/types/client/channel.d.ts +49 -0
- package/dist/types/client/coalesce.d.ts +14 -0
- package/dist/types/client/connect.d.ts +39 -0
- package/dist/types/client/errors.d.ts +46 -0
- package/dist/types/client/helpers.d.ts +25 -0
- package/dist/types/client/net.d.ts +111 -0
- package/dist/types/client/theme.d.ts +31 -0
- package/dist/types/client/toolResult.d.ts +16 -0
- package/dist/types/contract/api.d.ts +812 -0
- package/dist/types/contract/index.d.ts +11 -0
- package/dist/types/contract/jsonSchema.d.ts +88 -0
- package/dist/types/contract/localized.d.ts +10 -0
- package/dist/types/contract/manifest.d.ts +179 -0
- package/dist/types/contract/network.d.ts +48 -0
- package/dist/types/contract/permissions.d.ts +180 -0
- package/dist/types/contract/ports.d.ts +237 -0
- package/dist/types/contract/protocol.d.ts +74 -0
- package/dist/types/contract/source.d.ts +111 -0
- package/dist/types/contract/theme.d.ts +21 -0
- package/dist/types/contract/version.d.ts +126 -0
- package/dist/types/i18n.d.ts +50 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/react/index.d.ts +132 -0
- package/dist/types/testing/index.d.ts +7 -0
- package/dist/types/testing/mockHost.d.ts +315 -0
- package/dist/types/version.d.ts +2 -0
- package/dist/ui.css +581 -0
- package/package.json +77 -0
- package/schema/neurosquad-card.v1.json +434 -0
- package/templates/react/README.md +34 -0
- package/templates/react/_gitignore +10 -0
- package/templates/react/icon.png +0 -0
- package/templates/react/index.html +12 -0
- package/templates/react/neurosquad-card.json +94 -0
- package/templates/react/package.json +25 -0
- package/templates/react/src/App.tsx +131 -0
- package/templates/react/src/i18n.ts +61 -0
- package/templates/react/src/main.tsx +77 -0
- package/templates/react/src/styles.css +42 -0
- package/templates/react/tsconfig.json +18 -0
- package/templates/react/vite.config.ts +19 -0
- package/templates/vanilla/README.md +37 -0
- package/templates/vanilla/_gitignore +6 -0
- package/templates/vanilla/icon.png +0 -0
- package/templates/vanilla/index.html +41 -0
- package/templates/vanilla/main.js +256 -0
- package/templates/vanilla/neurosquad-card.json +94 -0
- package/templates/vanilla/style.css +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NeuroSquad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# @neurosquad/card-sdk
|
|
2
|
+
|
|
3
|
+
Build **custom cards** for [NeuroSquad](https://neurosquad.ai) — the desktop
|
|
4
|
+
canvas for CLI coding agents. A card is a small web app that lives on the
|
|
5
|
+
canvas next to agents, terminals and notes. It can watch and prompt the agents
|
|
6
|
+
it is connected to, exchange typed data with other cards over arrows, offer
|
|
7
|
+
tools to agents over MCP, keep its own storage and settings, and reach the
|
|
8
|
+
network through the app's proxy.
|
|
9
|
+
|
|
10
|
+
This package has everything for it:
|
|
11
|
+
|
|
12
|
+
| Import | What |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@neurosquad/card-sdk` | `connect()` and the typed `Card` client, errors, theme, i18n helper, the full contract (types, validators) |
|
|
15
|
+
| `@neurosquad/card-sdk/react` | `CardProvider` and hooks |
|
|
16
|
+
| `@neurosquad/card-sdk/testing` | `createMockHost()` — an in-memory host for unit tests and browser previews |
|
|
17
|
+
| `@neurosquad/card-sdk/ui.css` | optional dark UI kit on the app's live theme |
|
|
18
|
+
| `@neurosquad/card-sdk/schema.json` | JSON Schema of `neurosquad-card.json` |
|
|
19
|
+
| `neurosquad-card` (bin) | `create`, `dev`, `validate`, `pack` |
|
|
20
|
+
|
|
21
|
+
Zero runtime dependencies. React is an optional peer (18.2+ or 19).
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx @neurosquad/card-sdk create my-card # no build step
|
|
27
|
+
npx @neurosquad/card-sdk create my-card --template react
|
|
28
|
+
cd my-card
|
|
29
|
+
npx @neurosquad/card-sdk dev # live in the app
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`dev` needs NeuroSquad running with **Settings → Custom cards → Developer
|
|
33
|
+
mode** on. It links the folder (you confirm in the app), reloads the card when
|
|
34
|
+
you save, and prints the card's log. Add the card from the canvas: **+ →
|
|
35
|
+
Custom card…**.
|
|
36
|
+
|
|
37
|
+
Share a card by pushing its folder to GitHub. Users install it by pasting
|
|
38
|
+
`owner/repo` (or `owner/repo/sub/folder`, `…@tag`) into **Settings → Custom
|
|
39
|
+
cards**; they see the permissions it asks for and approve them. Updates are
|
|
40
|
+
never automatic: every new commit is shown with the permission changes and
|
|
41
|
+
applied by a click.
|
|
42
|
+
|
|
43
|
+
## The card and the manifest
|
|
44
|
+
|
|
45
|
+
A card folder has a `neurosquad-card.json` at its root:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"$schema": "./node_modules/@neurosquad/card-sdk/schema/neurosquad-card.v1.json",
|
|
50
|
+
"manifestVersion": 1,
|
|
51
|
+
"name": "test-radar",
|
|
52
|
+
"displayName": { "en": "Test radar", "ru": "Радар тестов" },
|
|
53
|
+
"version": "1.0.0",
|
|
54
|
+
"protocol": 1,
|
|
55
|
+
"icon": "icon.png",
|
|
56
|
+
"entry": "dist/index.html",
|
|
57
|
+
"card": { "defaultSize": { "w": 460, "h": 340 } },
|
|
58
|
+
"permissions": ["agents.read", "terminals.write", { "id": "network", "hosts": ["api.github.com"] }],
|
|
59
|
+
"settings": [{ "key": "command", "type": "string", "label": "Test command", "default": "npm test" }],
|
|
60
|
+
"ports": {
|
|
61
|
+
"inputs": [{ "id": "run", "label": "Run", "type": "ns:trigger" }],
|
|
62
|
+
"outputs": [{ "id": "failures", "label": "Failures", "type": "ns:tasks", "retain": true }]
|
|
63
|
+
},
|
|
64
|
+
"tools": [
|
|
65
|
+
{
|
|
66
|
+
"name": "run_tests",
|
|
67
|
+
"description": "Runs the project's tests and returns the failing ones.",
|
|
68
|
+
"inputSchema": { "type": "object", "properties": { "filter": { "type": "string" } } }
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`npx @neurosquad/card-sdk validate` checks it with the app's own validators and
|
|
75
|
+
shows the install dialog your users will see.
|
|
76
|
+
|
|
77
|
+
Rules the app enforces on the manifest:
|
|
78
|
+
|
|
79
|
+
- **Text** (names, descriptions, labels, reasons, tool descriptions) may not
|
|
80
|
+
contain control characters (tab and newline only in descriptions), bidi
|
|
81
|
+
overrides or isolates, zero-width characters or Unicode tag characters.
|
|
82
|
+
Only cards from the official organization may call themselves "NeuroSquad",
|
|
83
|
+
"official" or "verified" (`validate` warns; the app refuses).
|
|
84
|
+
- **Reserved names**: `name` may not be a built-in card or tool family
|
|
85
|
+
(`telegram`, `squad`, `ports`, `watch`, `reference`, `timer`, `sticky`,
|
|
86
|
+
`standup`, `image`, `mcp`, `skill`, `canvas`, `browser`, `terminal`,
|
|
87
|
+
`agent`, `note`, `todo`, `kanban`, …). A tool's exposed name
|
|
88
|
+
(`<name with - → _>_<tool>`) may not equal a built-in tool
|
|
89
|
+
(`canvas_spawn_card`, `terminal_send_keys`, …) and may not contain `__`
|
|
90
|
+
(reserved for installed MCP servers).
|
|
91
|
+
- **Schemas** (ports, tools): `enum` at most 256 options and 16 KB in total,
|
|
92
|
+
`const` at most 4 KB.
|
|
93
|
+
- **Updates**: a new version that adds or rewords MCP tools, adds or retypes
|
|
94
|
+
ports, adds secret settings or changes `displayName`/`author`/`homepage`
|
|
95
|
+
asks the user again, like a new permission does.
|
|
96
|
+
|
|
97
|
+
### The sandbox
|
|
98
|
+
|
|
99
|
+
The card runs in a sandboxed frame with its own opaque origin:
|
|
100
|
+
|
|
101
|
+
- no `window.api`, no Node, no access to the app, other cards, `localStorage`
|
|
102
|
+
or cookies — use `card.storage`;
|
|
103
|
+
- **no inline scripts** and **no external resources**: ship scripts, styles,
|
|
104
|
+
fonts and images inside the package (data: and blob: images work);
|
|
105
|
+
- no popups, `alert`/`confirm`, downloads or fullscreen — use `card.ui.confirm`,
|
|
106
|
+
`card.openLink`;
|
|
107
|
+
- `<form>` submit events fire (the frame has `allow-forms`), but submission
|
|
108
|
+
itself never navigates: the host cancels every submission and makes
|
|
109
|
+
`form.submit()` a no-op (CSP `form-action 'none'` backs it up). Call
|
|
110
|
+
`event.preventDefault()` in your submit handler anyway and do the work in JS;
|
|
111
|
+
- **never trust `window` `message` events.** Any other card on the canvas can
|
|
112
|
+
`postMessage` your frame. The SDK's port (handed over by the app from
|
|
113
|
+
`window.parent`) is the only trusted channel;
|
|
114
|
+
- copying: `copy`/`cut` event handlers and `document.execCommand('copy')` do
|
|
115
|
+
nothing (a card could otherwise replace the clipboard on a click). Write
|
|
116
|
+
text with `card.copyText` (needs `clipboard.write`); the user's own copy of
|
|
117
|
+
selected text still works;
|
|
118
|
+
- no nested frames (`iframe`, `object`, `embed` are removed), and only blob
|
|
119
|
+
workers: `new Worker('x.js')` fails in an opaque origin, so fetch the script,
|
|
120
|
+
wrap it in a `Blob` and start the worker from `URL.createObjectURL`;
|
|
121
|
+
- host prompts (`card.ui.confirm`, permission requests, link confirmations)
|
|
122
|
+
are drawn by the app **outside** your card, over a window-wide backdrop. The
|
|
123
|
+
confirm dialog shows your `title`/`message` as quoted card text under the
|
|
124
|
+
app's own heading, and the cancel button is always the app's. **Secrets are
|
|
125
|
+
never typed inside a card**: the user enters them in the app's own settings
|
|
126
|
+
dialog, and your card only sees `settings.secrets[key] === true`. Never
|
|
127
|
+
draw a field that asks for a key — users are told a real prompt dims the
|
|
128
|
+
whole window;
|
|
129
|
+
- network only through `card.net.fetch`, only to hosts in your `network`
|
|
130
|
+
permission (or localhost with `network.local`);
|
|
131
|
+
- the frame is paused and eventually unloaded when nobody sees it — save state
|
|
132
|
+
in `card.lifecycle.onSuspend`.
|
|
133
|
+
|
|
134
|
+
## `connect()`
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { connect } from '@neurosquad/card-sdk'
|
|
138
|
+
|
|
139
|
+
const card = await connect()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Import the SDK from your entry script: it listens for the app's handshake from
|
|
143
|
+
the moment it loads. `connect()` also applies the app theme to `<html>` as
|
|
144
|
+
`--ns-*` CSS variables and keeps `<html lang>` equal to the app language
|
|
145
|
+
(`connect({ theme: false, syncLang: false })` to opt out).
|
|
146
|
+
|
|
147
|
+
Everything the host knows about the card is in `card.context` (kept current):
|
|
148
|
+
`instance`, `workspace`, `visibility`, `expanded`, `theme`, `i18n`,
|
|
149
|
+
`settings`, `permissions`, `ports`, `peers`, `launch`, `spawnInit`, `limits`,
|
|
150
|
+
and `chrome` — the title, status, badge, overview and attention the app still
|
|
151
|
+
shows for this card. Chrome survives frame reloads (dev reload, update,
|
|
152
|
+
resume), so after a reload check `card.context.chrome?.attention` and clear a
|
|
153
|
+
stale pulse with `card.attention('none')`.
|
|
154
|
+
|
|
155
|
+
Chrome updates are throttled by the app (`card.setStatus`/`setBadge`/
|
|
156
|
+
`setOverview` 120 per minute, `setTitle` and `ui.setMenu` 30, `attention`
|
|
157
|
+
once per 10 s, `settings.set` 60, `tools.setEnabled` 30, `ui.confirm` 10,
|
|
158
|
+
`usage.summary` 6). For `setTitle`/`setStatus`/`setBadge`/`setOverview`/
|
|
159
|
+
`attention` the SDK sends one call at a time per method, folds calls made
|
|
160
|
+
meanwhile into one with the latest value, and when the app answers
|
|
161
|
+
`RATE_LIMITED` it retries the latest value after `retryAfterMs` — so calling
|
|
162
|
+
them on every render is fine and never throws for rate. The other methods
|
|
163
|
+
reject with `RATE_LIMITED` (`error.retryAfterMs`).
|
|
164
|
+
|
|
165
|
+
### Any method, any event
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const info = await card.call('card.getInfo') // typed params and result
|
|
169
|
+
const off = card.on('agents.turn', (turn) => console.log(turn)) // typed payload
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Topics (`agents.status`, `agents.turn`, `agents.changed`, `storage.changed`) are
|
|
173
|
+
subscribed with the host automatically while a listener exists.
|
|
174
|
+
|
|
175
|
+
### Namespaces
|
|
176
|
+
|
|
177
|
+
| | |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| Card chrome | `setTitle`, `setStatus(text, { tone, busy })`, `setBadge(3)`, `setOverview({ primary, secondary, progress, tone, icon })`, `attention('needs-input', msg)`, `requestResize`, `openLink`, `focusCard`, `spawn('note')` |
|
|
180
|
+
| `card.ui` | `toast`, `confirm` → boolean, `setMenu([{ id, label, icon, onSelect }])`, `onMenu` |
|
|
181
|
+
| `card.storage` | `get(key, fallback)`, `set`, `delete`, `keys(prefix)`, `clear`, `usage`; `card.storage.package.*` shared by every card of the package; `onChange` |
|
|
182
|
+
| `card.settings` | `values`, `value(key)`, `hasSecret(key)`, `set(values)`, `open()`, `onChange` |
|
|
183
|
+
| `card.agents` | `list`, `get`, `readScreen`, `lastReply`, `prompt(id, text, { whenBusy, submit })`, `onStatus`, `onTurn`, `onChanged`, `onOutput(ids, handler)` |
|
|
184
|
+
| `card.terminals` | `run(id, command)` → `{ exitCode, output }`, `write(id, text, { submit })` |
|
|
185
|
+
| `card.ports` | `inputs`, `outputs`, `peers`, `emit(output, data)`, `send`, `request`, `read`, `onMessage`, `onRequest(input, handler)`, `onPeersChanged` |
|
|
186
|
+
| `card.tools` | `handle(name, handler)`, `setEnabled` |
|
|
187
|
+
| `card.net` | `fetch(url, init)` → `CardResponse` (`ok`, `status`, `headers`, `text()`, `json()`, `bytes()`, `chunks()`, `lines()`) |
|
|
188
|
+
| `card.fs` | `stat`, `list`, `readText`, `readBytes`, `writeText`, `writeBytes`, `mkdir`, `trash`, `watch` — paths relative to the workspace folder |
|
|
189
|
+
| `card.permissions` | `all`, `has(id)`, `request(...ids)` for optional ones, `onChange` |
|
|
190
|
+
| `card.lifecycle` | `onVisibility`, `onSuspend`, `onExpanded`, `onResized` |
|
|
191
|
+
| `card.log` | `debug/info/warn/error` → the package log and the `dev` console |
|
|
192
|
+
| Other | `usage(period)`, `copyText`, `getWorkspace`, `host.capabilities()`, `host.supports(method)`, `waitFor(event)`, `close()` |
|
|
193
|
+
|
|
194
|
+
### Tools for agents
|
|
195
|
+
|
|
196
|
+
Declare the tool in the manifest, implement it in the card. Connected agents
|
|
197
|
+
see it as `<card_name>_<tool>` over the app's MCP server.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
card.tools.handle<{ filter?: string }>('run_tests', async ({ filter }, call) => {
|
|
201
|
+
call.progress('running…') // shown on the arrow
|
|
202
|
+
const result = await card.terminals.run(shellId, `npm test -- ${filter ?? ''}`, { timeoutMs: 60_000 })
|
|
203
|
+
return result.exitCode === 0 ? 'All tests passed' : result.output
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Return a string, any JSON value, or a `ToolResultPayload` (`toolText`,
|
|
208
|
+
`toolImage`, `toolError` help). Throwing sends an error to the agent. Calls
|
|
209
|
+
that arrive before `handle()` is registered wait for it.
|
|
210
|
+
|
|
211
|
+
### Ports
|
|
212
|
+
|
|
213
|
+
An arrow from card A to card B carries A's outputs to B's inputs. Types are
|
|
214
|
+
`ns:*` well-known types (`ns:text`, `ns:markdown`, `ns:tasks`, `ns:table`,
|
|
215
|
+
`ns:event`, `ns:trigger`, …) or `<your-card>/<type>` with a schema. Built-in
|
|
216
|
+
cards take part too: a note accepts `ns:markdown` (append), a checklist
|
|
217
|
+
`ns:tasks`, an agent a prompt, a terminal a command.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
await card.ports.emit('failures', [{ text: 'auth › logs in', status: 'error' }]) // a note gets a Markdown list
|
|
221
|
+
card.ports.onMessage((text) => render(text), { input: 'text' })
|
|
222
|
+
card.ports.onRequest<string>('lookup', async (query) => search(query))
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`ports.message` carries the input's `type` (after conversion) and the
|
|
226
|
+
sender's declared output type in `sourceType`. Generic parameters accept your
|
|
227
|
+
own interfaces (`card.ports.onMessage<Task>(…)`).
|
|
228
|
+
|
|
229
|
+
Sending to a built-in card needs a permission (`PortInfo.permission`, e.g.
|
|
230
|
+
`cards.connected` for a note). Helpers:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { hasDownstreamPeer, permissionForPeer, requestPermissions } from '@neurosquad/card-sdk'
|
|
234
|
+
|
|
235
|
+
if (!hasDownstreamPeer(card.ports.peers)) return hint('Draw an arrow to a note')
|
|
236
|
+
const needed = card.ports.peers.map((p) => permissionForPeer(p, { outputType: 'ns:markdown' }))
|
|
237
|
+
await requestPermissions(card, ...needed.filter((id) => id !== null)) // never throws
|
|
238
|
+
await card.ports.emit('summary', text)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Network
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const res = await card.net.fetch('https://api.github.com/repos/acme/app/issues', {
|
|
245
|
+
headers: { authorization: 'Bearer {{secret:githubToken}}' }, // a "secret" setting, filled in by the user
|
|
246
|
+
responseType: 'json'
|
|
247
|
+
})
|
|
248
|
+
if (!res.ok) throw new Error(`GitHub said ${res.status}`)
|
|
249
|
+
const issues = await res.json<{ title: string }[]>()
|
|
250
|
+
|
|
251
|
+
const stream = await card.net.fetch(url, { responseType: 'stream' })
|
|
252
|
+
for await (const line of stream.lines()) handle(line)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The card never sees secret values: the host puts them into headers for granted
|
|
256
|
+
hosts only.
|
|
257
|
+
|
|
258
|
+
Conditional requests work: `If-None-Match` and `If-Modified-Since` are
|
|
259
|
+
forwarded, and every response header except `set-cookie` comes back
|
|
260
|
+
(`etag`, `last-modified`, `x-ratelimit-*`). A `204`/`304` (any empty body)
|
|
261
|
+
with `responseType: 'json'` gives `json() === null`.
|
|
262
|
+
|
|
263
|
+
### Files
|
|
264
|
+
|
|
265
|
+
`card.fs` (`fs.read`/`fs.write`) reaches only the workspace folder. Writes,
|
|
266
|
+
folders and trash are refused for paths that would let a card run code or
|
|
267
|
+
steer agents: any `.git` path segment (nested repositories, submodule and
|
|
268
|
+
worktree pointer files), `.gitmodules`, `.gitattributes`, `.claude/`,
|
|
269
|
+
`.codex/`, `.cursor/`, `.vscode/`, `.idea/`, `.husky/`, `.github/workflows/`,
|
|
270
|
+
`.gemini/`, `.qwen/`, `.opencode/`, `.mcp.json`, `CLAUDE.md`,
|
|
271
|
+
`CLAUDE.local.md`, `AGENTS.md`, `GEMINI.md`, `QWEN.md`, `.cursorrules`,
|
|
272
|
+
`.windsurfrules`, `opencode.json`, `.envrc`, `.npmrc`, `.yarnrc`, `.yarnrc.yml`
|
|
273
|
+
(`FS_DENIED`). All of `fs.*` is refused when the workspace folder is the home
|
|
274
|
+
folder, a drive root, or contains the app's own data folder.
|
|
275
|
+
|
|
276
|
+
### Errors
|
|
277
|
+
|
|
278
|
+
Every refusal is a `CardSdkError` with a `code` (`PERMISSION_DENIED`,
|
|
279
|
+
`NOT_CONNECTED`, `NOT_VISIBLE`, `RATE_LIMITED`, `HOST_NOT_ALLOWED`,
|
|
280
|
+
`BUDGET_PAUSED`, …), the failing `method`, `data` and a `hint`:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
try {
|
|
284
|
+
await card.agents.prompt(agentId, 'Run the tests')
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (isCardSdkError(error, 'NOT_CONNECTED')) card.ui.toast('Connect me to an agent first')
|
|
287
|
+
else throw error
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Params are checked locally against the same schemas the app uses, so mistakes
|
|
292
|
+
fail fast with `INVALID_PARAMS` and the exact path.
|
|
293
|
+
|
|
294
|
+
## React
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { CardProvider, useAgents, useStorage, useTool, useTranslator, usePaused } from '@neurosquad/card-sdk/react'
|
|
298
|
+
|
|
299
|
+
function App() {
|
|
300
|
+
const { agents } = useAgents()
|
|
301
|
+
const [notes, setNotes] = useStorage('notes', '')
|
|
302
|
+
useTool('read_notes', () => notes)
|
|
303
|
+
return <textarea value={notes} onChange={(e) => setNotes(e.target.value)} />
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
createRoot(root).render(<CardProvider fallback={<p>Connecting…</p>}><App /></CardProvider>)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Hooks: `useCard`, `useCardContext`, `useSettings`, `useStorage`, `useAgents`,
|
|
310
|
+
`useAgentStatus`, `usePort`, `useEmit`, `usePeers`, `useTheme`, `useLanguage`,
|
|
311
|
+
`useVisibility`, `useExpanded`, `usePaused`, `useTool`, `usePortRequest`,
|
|
312
|
+
`useCardEvent`, `useTranslator`.
|
|
313
|
+
|
|
314
|
+
## Languages
|
|
315
|
+
|
|
316
|
+
The app speaks English, Russian and Chinese and switches live.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const t = createTranslator({
|
|
320
|
+
en: { failed_one: '{{count}} test failed', failed_other: '{{count}} tests failed' },
|
|
321
|
+
ru: { failed_one: '{{count}} тест упал', failed_few: '{{count}} теста упало', failed_many: '{{count}} тестов упало', failed_other: '{{count}} теста упало' }
|
|
322
|
+
}, card)
|
|
323
|
+
t('failed', { count: 3 })
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Manifest texts (`displayName`, labels, descriptions) take `"text"` or
|
|
327
|
+
`{ "en": …, "ru": …, "zh": … }`.
|
|
328
|
+
|
|
329
|
+
## Styling
|
|
330
|
+
|
|
331
|
+
Cards style themselves any way they like. For a native look, the optional kit
|
|
332
|
+
reads the app's live theme:
|
|
333
|
+
|
|
334
|
+
```html
|
|
335
|
+
<link rel="stylesheet" href="vendor/ui.css" /> <!-- or import '@neurosquad/card-sdk/ui.css' -->
|
|
336
|
+
<body class="ns-kit">
|
|
337
|
+
<button class="ns-btn ns-btn--primary">Run</button>
|
|
338
|
+
<input class="ns-input" placeholder="Filter" />
|
|
339
|
+
<span class="ns-badge ns-badge--success">passing</span>
|
|
340
|
+
</body>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Classes: `ns-kit`, `ns-stack`, `ns-row`, `ns-spread`, `ns-scroll`, `ns-surface`,
|
|
344
|
+
`ns-title`, `ns-subtitle`, `ns-muted`, `ns-mono`, `ns-btn` (`--primary`,
|
|
345
|
+
`--secondary`, `--outline`, `--ghost`, `--danger`, `--sm`, `--lg`, `--icon`),
|
|
346
|
+
`ns-field`, `ns-label`, `ns-input`, `ns-textarea`, `ns-select`, `ns-switch`,
|
|
347
|
+
`ns-list`, `ns-list-item`, `ns-badge--*`, `ns-dot--*`, `ns-spinner`,
|
|
348
|
+
`ns-progress`, `ns-empty`, `ns-callout`. Variables: `--ns-<token>` for every
|
|
349
|
+
`THEME_TOKENS` entry plus `--ns-font-sans`, `--ns-font-mono`.
|
|
350
|
+
|
|
351
|
+
## Testing
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { createMockHost } from '@neurosquad/card-sdk/testing'
|
|
355
|
+
|
|
356
|
+
const host = createMockHost({ manifest, agents: [...], peers: [...], fetch: (req) => ({ body: { ok: true } }) })
|
|
357
|
+
const card = await host.connect()
|
|
358
|
+
|
|
359
|
+
await card.ports.emit('summary', '# Done')
|
|
360
|
+
expect(host.deliveries).toEqual([{ to: 'note-1', output: 'summary', input: 'append', data: '# Done' }])
|
|
361
|
+
await expect(host.callTool('run_tests', {})).resolves.toEqual({ content: [{ type: 'text', text: 'All tests passed' }] })
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The mock applies the app's checks in the app's order (params schemas,
|
|
365
|
+
permissions, visibility, optionally rate limits) and records everything:
|
|
366
|
+
`calls`, `storage`, `deliveries`, `prompts`, `logs`, `toasts`, `chrome`
|
|
367
|
+
(title/status/badge/overview/menu). Drive the card with `setVisibility`,
|
|
368
|
+
`setLanguage`, `setSettings`, `setSecret`, `sendPortMessage`, `requestPort`,
|
|
369
|
+
`callTool`, `setAgentStatus`, `agentOutput`, `suspend`, `touchFile`. The same
|
|
370
|
+
mock powers the templates' browser preview when the page is opened outside the
|
|
371
|
+
app.
|
|
372
|
+
|
|
373
|
+
It behaves like the app where it matters: `setAgentStatus` also emits
|
|
374
|
+
`agents.turn` (`start` on entering `working`, `end` on leaving it for
|
|
375
|
+
`finished`/`needs-input`); `callTool` returns a rejected promise for bad
|
|
376
|
+
arguments; `sendPortMessage` checks that `from.cardId` is an upstream peer and
|
|
377
|
+
that the value fits the input's schema; `permissions.request` fails with
|
|
378
|
+
`NOT_VISIBLE` off screen; `fs.write` refuses the protected paths above. Options:
|
|
379
|
+
`settings` (initial values), `secrets` (`{ key: value }` for `secret`
|
|
380
|
+
settings — `{{secret:key}}` headers are filled, a missing one is
|
|
381
|
+
`INVALID_PARAMS`), `enforceRateLimits` (per-method throttles; off by default so
|
|
382
|
+
tests stay deterministic).
|
|
383
|
+
|
|
384
|
+
**Test in the app too.** The mock runs in your page, not in the sandboxed
|
|
385
|
+
frame, so CSP and sandbox differences (inline scripts, external resources)
|
|
386
|
+
only show up in NeuroSquad (`neurosquad-card dev`).
|
|
387
|
+
|
|
388
|
+
## CLI
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
neurosquad-card create <folder> [--template vanilla|react] [--name <slug>] [--display-name <text>] [--force]
|
|
392
|
+
neurosquad-card dev [folder] [--user-data-dir <dir>] [--no-build] [--no-logs]
|
|
393
|
+
neurosquad-card validate [folder] [--json] [--app-version <x.y.z>]
|
|
394
|
+
neurosquad-card pack [folder] [--out <file.tgz>] [--dry-run] [--json] [--allow-secret-files]
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- `validate` runs the manifest through the app's validators, applies the
|
|
398
|
+
installer's file rules (paths, sizes, counts, icon format and squareness),
|
|
399
|
+
lints the entry HTML for things the sandbox blocks, and previews the install
|
|
400
|
+
dialog.
|
|
401
|
+
- `pack` lists what would be installed (what git tracks plus untracked files
|
|
402
|
+
that are not ignored — the GitHub tarball), prints the **tree hash** the app
|
|
403
|
+
records, and writes a reproducible `.tgz`. It refuses files that look like
|
|
404
|
+
credentials (`.env*` except `.env.example`, `*.pem`, `*.key`, `*.p12`,
|
|
405
|
+
`*.pfx`, `id_rsa*`, `.npmrc`, `.git-credentials`, `.netrc`) unless you pass
|
|
406
|
+
`--allow-secret-files`, and skips anything reached through a symbolic link
|
|
407
|
+
or junction (it would be read from outside the folder).
|
|
408
|
+
- `validate` and `pack` are safe to run on a folder someone sent you: git
|
|
409
|
+
runs with the folder's own hooks and `core.fsmonitor` disabled, and card
|
|
410
|
+
text is printed with escape sequences and bidi characters removed.
|
|
411
|
+
- `dev` finds the app's data folder (`%APPDATA%/NeuroSquad`,
|
|
412
|
+
`~/Library/Application Support/NeuroSquad`, `~/.config/NeuroSquad`, or
|
|
413
|
+
`--user-data-dir` / `NEUROSQUAD_USER_DATA_DIR`), links the folder, runs
|
|
414
|
+
`npm run watch` (or `npm run build -- --watch`) when present, and streams the
|
|
415
|
+
card's log. Press `r` to reload, `q` to quit. **`dev` runs the folder's own
|
|
416
|
+
npm scripts** — only use it on folders you trust, or pass `--no-build`.
|
|
417
|
+
- Linking a local SDK checkout (`"@neurosquad/card-sdk": "file:…"`) makes a
|
|
418
|
+
symlink, and Vite can then load a second React next to the SDK ("invalid
|
|
419
|
+
hook call"). The React template sets `resolve.dedupe: ['react', 'react-dom']`;
|
|
420
|
+
`install-links=true` in `.npmrc` also works.
|
|
421
|
+
|
|
422
|
+
## Versioning
|
|
423
|
+
|
|
424
|
+
The wire protocol is versioned separately (`CARD_PROTOCOL_VERSION`, now 1).
|
|
425
|
+
New methods and events arrive within a protocol version; check with
|
|
426
|
+
`card.host.supports('method.name')`. A card built for a newer protocol is
|
|
427
|
+
refused by an older app with `PROTOCOL_MISMATCH`.
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
MIT
|