@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +431 -0
  3. package/dist/card-sdk.js +5266 -0
  4. package/dist/cli.js +2838 -0
  5. package/dist/react.js +257 -0
  6. package/dist/testing.js +1150 -0
  7. package/dist/types/client/base64.d.ts +7 -0
  8. package/dist/types/client/card.d.ts +653 -0
  9. package/dist/types/client/channel.d.ts +49 -0
  10. package/dist/types/client/coalesce.d.ts +14 -0
  11. package/dist/types/client/connect.d.ts +39 -0
  12. package/dist/types/client/errors.d.ts +46 -0
  13. package/dist/types/client/helpers.d.ts +25 -0
  14. package/dist/types/client/net.d.ts +111 -0
  15. package/dist/types/client/theme.d.ts +31 -0
  16. package/dist/types/client/toolResult.d.ts +16 -0
  17. package/dist/types/contract/api.d.ts +812 -0
  18. package/dist/types/contract/index.d.ts +11 -0
  19. package/dist/types/contract/jsonSchema.d.ts +88 -0
  20. package/dist/types/contract/localized.d.ts +10 -0
  21. package/dist/types/contract/manifest.d.ts +179 -0
  22. package/dist/types/contract/network.d.ts +48 -0
  23. package/dist/types/contract/permissions.d.ts +180 -0
  24. package/dist/types/contract/ports.d.ts +237 -0
  25. package/dist/types/contract/protocol.d.ts +74 -0
  26. package/dist/types/contract/source.d.ts +111 -0
  27. package/dist/types/contract/theme.d.ts +21 -0
  28. package/dist/types/contract/version.d.ts +126 -0
  29. package/dist/types/i18n.d.ts +50 -0
  30. package/dist/types/index.d.ts +28 -0
  31. package/dist/types/react/index.d.ts +132 -0
  32. package/dist/types/testing/index.d.ts +7 -0
  33. package/dist/types/testing/mockHost.d.ts +315 -0
  34. package/dist/types/version.d.ts +2 -0
  35. package/dist/ui.css +581 -0
  36. package/package.json +77 -0
  37. package/schema/neurosquad-card.v1.json +434 -0
  38. package/templates/react/README.md +34 -0
  39. package/templates/react/_gitignore +10 -0
  40. package/templates/react/icon.png +0 -0
  41. package/templates/react/index.html +12 -0
  42. package/templates/react/neurosquad-card.json +94 -0
  43. package/templates/react/package.json +25 -0
  44. package/templates/react/src/App.tsx +131 -0
  45. package/templates/react/src/i18n.ts +61 -0
  46. package/templates/react/src/main.tsx +77 -0
  47. package/templates/react/src/styles.css +42 -0
  48. package/templates/react/tsconfig.json +18 -0
  49. package/templates/react/vite.config.ts +19 -0
  50. package/templates/vanilla/README.md +37 -0
  51. package/templates/vanilla/_gitignore +6 -0
  52. package/templates/vanilla/icon.png +0 -0
  53. package/templates/vanilla/index.html +41 -0
  54. package/templates/vanilla/main.js +256 -0
  55. package/templates/vanilla/neurosquad-card.json +94 -0
  56. package/templates/vanilla/style.css +41 -0
@@ -0,0 +1,131 @@
1
+ import {
2
+ permissionForPeer,
3
+ requestPermissions,
4
+ type AgentStatus,
5
+ type PermissionId
6
+ } from '@neurosquad/card-sdk'
7
+ import {
8
+ useAgents,
9
+ useCard,
10
+ useCardContext,
11
+ useEmit,
12
+ usePort,
13
+ useSettings,
14
+ useStorage,
15
+ useTool,
16
+ useTranslator
17
+ } from '@neurosquad/card-sdk/react'
18
+ import { useEffect, useState } from 'react'
19
+ import { catalog } from './i18n'
20
+
21
+ const TONE: Record<AgentStatus, string> = {
22
+ working: 'ns-dot--accent',
23
+ 'needs-input': 'ns-dot--warning',
24
+ finished: 'ns-dot--success',
25
+ idle: '',
26
+ exited: 'ns-dot--danger'
27
+ }
28
+
29
+ export function App(): React.JSX.Element {
30
+ const card = useCard()
31
+ const { workspace, instance } = useCardContext()
32
+ const [settings] = useSettings()
33
+ const t = useTranslator(catalog)
34
+ const { agents } = useAgents()
35
+ const [pad, setPad] = useStorage('scratchpad', '')
36
+ const [hint, setHint] = useState('')
37
+ const emitScratchpad = useEmit<string>('scratchpad')
38
+
39
+ const working = agents.filter((agent) => agent.status === 'working').length
40
+
41
+ // What the overview tile shows when the canvas is zoomed out.
42
+ useEffect(() => {
43
+ void card.setOverview({
44
+ primary: t('working', { count: working }),
45
+ tone: working ? 'accent' : 'default',
46
+ icon: 'users'
47
+ })
48
+ }, [card, t, working])
49
+
50
+ // Text arriving on the "text" input port is appended to the scratchpad.
51
+ const { message } = usePort<string>('text')
52
+ useEffect(() => {
53
+ if (message)
54
+ setPad((previous) =>
55
+ previous ? `${previous}\n${message.data as string}` : (message.data as string)
56
+ )
57
+ }, [message, setPad])
58
+
59
+ // A tool for connected agents: they can read what the user wrote here.
60
+ useTool('read_scratchpad', () => pad || '(the scratchpad is empty)')
61
+
62
+ const send = async (): Promise<void> => {
63
+ try {
64
+ // A note's input needs the optional cards.connected permission: ask for
65
+ // it (the app shows its own prompt) before the first send.
66
+ const needed = card.ports.peers
67
+ .filter((peer) => peer.direction !== 'upstream')
68
+ .map((peer) => permissionForPeer(peer, { outputType: 'ns:markdown' }))
69
+ .filter((id): id is PermissionId => id !== null)
70
+ if (needed.length > 0) await requestPermissions(card, ...new Set(needed))
71
+ const delivered = await emitScratchpad(pad)
72
+ setHint(delivered > 0 ? t('sent', { count: delivered }) : t('notConnected'))
73
+ } catch (error) {
74
+ setHint(error instanceof Error ? error.message : String(error))
75
+ }
76
+ }
77
+
78
+ return (
79
+ <main className="app">
80
+ <header className="ns-spread">
81
+ <div className="ns-grow">
82
+ <p className="ns-subtitle ns-truncate">{workspace.name}</p>
83
+ <h1 className="ns-title ns-truncate">
84
+ {String(settings.values['greeting'] ?? instance.displayName)}
85
+ </h1>
86
+ </div>
87
+ <span className="ns-badge ns-badge--accent" title={t('working', { count: working })}>
88
+ {working}
89
+ </span>
90
+ </header>
91
+
92
+ <section className="ns-surface ns-surface--inset agents">
93
+ <p className="ns-subtitle">{t('agents')}</p>
94
+ <ul className="ns-list ns-scroll">
95
+ {agents.length === 0 && <li className="ns-list-item ns-muted">{t('noAgents')}</li>}
96
+ {agents.map((agent) => (
97
+ <li key={agent.id} className="ns-list-item">
98
+ <span className={`ns-dot ${TONE[agent.status]}`} />
99
+ <span className="ns-truncate">{agent.name}</span>
100
+ <span className="agent-status ns-muted">{t(`status.${agent.status}`)}</span>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ </section>
105
+
106
+ <section className="ns-field pad">
107
+ <label className="ns-label" htmlFor="pad">
108
+ {t('scratchpad')}
109
+ </label>
110
+ <textarea
111
+ id="pad"
112
+ className="ns-textarea"
113
+ spellCheck={false}
114
+ value={pad}
115
+ onChange={(event) => setPad(event.target.value)}
116
+ />
117
+ </section>
118
+
119
+ <footer className="ns-spread">
120
+ <span className="ns-small ns-muted ns-truncate">{hint}</span>
121
+ <button
122
+ className="ns-btn ns-btn--primary ns-btn--sm"
123
+ type="button"
124
+ onClick={() => void send()}
125
+ >
126
+ {t('send')}
127
+ </button>
128
+ </footer>
129
+ </main>
130
+ )
131
+ }
@@ -0,0 +1,61 @@
1
+ import type { Catalog } from '@neurosquad/card-sdk'
2
+
3
+ /** The card's own strings in the app's three languages (en is the fallback). */
4
+ export const catalog: Catalog = {
5
+ en: {
6
+ agents: 'Agents',
7
+ scratchpad: 'Scratchpad',
8
+ send: 'Send',
9
+ noAgents: 'No agents on this canvas yet',
10
+ working_one: '{{count}} agent working',
11
+ working_other: '{{count}} agents working',
12
+ sent_one: 'Sent to {{count}} card',
13
+ sent_other: 'Sent to {{count}} cards',
14
+ notConnected: 'Draw an arrow from this card to a note to send it there',
15
+ status: {
16
+ working: 'working',
17
+ 'needs-input': 'waiting for you',
18
+ finished: 'finished',
19
+ idle: 'idle',
20
+ exited: 'stopped'
21
+ }
22
+ },
23
+ ru: {
24
+ agents: 'Агенты',
25
+ scratchpad: 'Черновик',
26
+ send: 'Отправить',
27
+ noAgents: 'На холсте пока нет агентов',
28
+ working_one: 'Работает {{count}} агент',
29
+ working_few: 'Работают {{count}} агента',
30
+ working_many: 'Работают {{count}} агентов',
31
+ working_other: 'Работают {{count}} агента',
32
+ sent_one: 'Отправлено в {{count}} карточку',
33
+ sent_few: 'Отправлено в {{count}} карточки',
34
+ sent_many: 'Отправлено в {{count}} карточек',
35
+ sent_other: 'Отправлено в {{count}} карточки',
36
+ notConnected: 'Проведите стрелку от этой карточки к заметке, чтобы отправить туда',
37
+ status: {
38
+ working: 'работает',
39
+ 'needs-input': 'ждёт вас',
40
+ finished: 'закончил',
41
+ idle: 'свободен',
42
+ exited: 'остановлен'
43
+ }
44
+ },
45
+ zh: {
46
+ agents: '智能体',
47
+ scratchpad: '草稿',
48
+ send: '发送',
49
+ noAgents: '画布上还没有智能体',
50
+ working_other: '{{count}} 个智能体正在工作',
51
+ sent_other: '已发送到 {{count}} 张卡片',
52
+ notConnected: '从此卡片画一条箭头到笔记,即可发送到那里',
53
+ status: {
54
+ working: '工作中',
55
+ 'needs-input': '等待你',
56
+ finished: '已完成',
57
+ idle: '空闲',
58
+ exited: '已停止'
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,77 @@
1
+ // Import the SDK first, from the entry module: it starts listening for the
2
+ // app's handshake as soon as it loads.
3
+ import { connect, type Card, type CardManifest } from '@neurosquad/card-sdk'
4
+ import { CardProvider } from '@neurosquad/card-sdk/react'
5
+ import '@neurosquad/card-sdk/ui.css'
6
+ import { StrictMode } from 'react'
7
+ import { createRoot } from 'react-dom/client'
8
+ import manifest from '../neurosquad-card.json'
9
+ import { App } from './App'
10
+ import './styles.css'
11
+
12
+ /** Opened on its own (`npm run dev`), the card runs against a mock host with sample data. */
13
+ async function previewCard(): Promise<Card> {
14
+ const { createMockHost } = await import('@neurosquad/card-sdk/testing')
15
+ const host = createMockHost({
16
+ manifest: manifest as unknown as CardManifest,
17
+ agents: [
18
+ {
19
+ id: 'a1',
20
+ name: 'Claude Code',
21
+ harness: 'claude-code',
22
+ kind: 'ai',
23
+ status: 'working',
24
+ connected: true
25
+ },
26
+ {
27
+ id: 'a2',
28
+ name: 'Codex',
29
+ harness: 'codex-cli',
30
+ kind: 'ai',
31
+ status: 'needs-input',
32
+ connected: false
33
+ },
34
+ {
35
+ id: 't1',
36
+ name: 'PowerShell',
37
+ harness: 'shell-powershell',
38
+ kind: 'shell',
39
+ status: 'idle',
40
+ connected: false
41
+ }
42
+ ],
43
+ peers: [
44
+ {
45
+ cardId: 'note-1',
46
+ kind: 'note',
47
+ name: 'Notes',
48
+ direction: 'downstream',
49
+ inputs: [
50
+ {
51
+ id: 'append',
52
+ label: 'Append',
53
+ type: 'ns:markdown',
54
+ mode: 'stream',
55
+ retain: false,
56
+ default: true,
57
+ // Like the app: sending to a note needs cards.connected.
58
+ permission: 'cards.connected'
59
+ }
60
+ ],
61
+ outputs: []
62
+ }
63
+ ]
64
+ })
65
+ Object.assign(window, { mockHost: host }) // try mockHost.setAgentStatus('a2', 'working') in DevTools
66
+ return host.connect()
67
+ }
68
+
69
+ const card = window.parent === window ? await previewCard() : await connect()
70
+
71
+ createRoot(document.getElementById('root')!).render(
72
+ <StrictMode>
73
+ <CardProvider card={card}>
74
+ <App />
75
+ </CardProvider>
76
+ </StrictMode>
77
+ )
@@ -0,0 +1,42 @@
1
+ /* Your card's own styles, on top of the optional kit (@neurosquad/card-sdk/ui.css). */
2
+ html,
3
+ body,
4
+ #root {
5
+ height: 100%;
6
+ overflow: hidden;
7
+ }
8
+
9
+ .app {
10
+ display: grid;
11
+ grid-template-rows: auto minmax(64px, 1fr) minmax(96px, 1.2fr) auto;
12
+ grid-template-columns: minmax(0, 1fr);
13
+ gap: 10px;
14
+ height: 100%;
15
+ padding: 12px;
16
+ }
17
+
18
+ .agents {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 6px;
22
+ min-height: 0;
23
+ padding: 10px;
24
+ }
25
+
26
+ .agents .ns-list {
27
+ flex: 1;
28
+ }
29
+
30
+ .pad {
31
+ min-height: 0;
32
+ }
33
+
34
+ .pad .ns-textarea {
35
+ height: 100%;
36
+ resize: none;
37
+ }
38
+
39
+ .agent-status {
40
+ margin-left: auto;
41
+ font-size: 12px;
42
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "skipLibCheck": true,
14
+ "types": ["vite/client"],
15
+ "noEmit": true
16
+ },
17
+ "include": ["src", "vite.config.ts"]
18
+ }
@@ -0,0 +1,19 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import { defineConfig } from 'vite'
3
+
4
+ // The app serves the built card from nscard://<id>/dist/…, so every URL must be
5
+ // relative (base './'). Commit dist/: installs come straight from the repository.
6
+ export default defineConfig({
7
+ base: './',
8
+ plugins: [react()],
9
+ // One React even when the SDK is linked from a local checkout (file: deps are
10
+ // symlinks, and Vite would resolve react next to the SDK's real path).
11
+ resolve: { dedupe: ['react', 'react-dom'] },
12
+ build: {
13
+ outDir: 'dist',
14
+ emptyOutDir: true,
15
+ // The card CSP allows scripts only from the package; no inline preload polyfill.
16
+ modulePreload: { polyfill: false },
17
+ target: 'es2022'
18
+ }
19
+ })
@@ -0,0 +1,37 @@
1
+ # __CARD_DISPLAY_NAME__
2
+
3
+ A custom card for [NeuroSquad](https://neurosquad.ai), made with `@neurosquad/card-sdk`
4
+ (vanilla template — no build step).
5
+
6
+ ## Files
7
+
8
+ | File | What it is |
9
+ | --- | --- |
10
+ | `neurosquad-card.json` | The manifest: name, size, permissions, settings, ports, tools |
11
+ | `index.html`, `main.js`, `style.css` | The card itself — a small web page |
12
+ | `icon.png` | Square PNG/WebP, ≤ 128 KB |
13
+ | `vendor/` | The SDK (`card-sdk.js`), the optional UI kit (`ui.css`), the mock host for previews and the manifest schema |
14
+
15
+ ## Develop
16
+
17
+ 1. In NeuroSquad: **Settings → Custom cards → Developer mode**.
18
+ 2. In this folder: `npx @neurosquad/card-sdk dev` — confirm the link in the app,
19
+ then add the card from the canvas (**+ → Custom card…**). Saving a file reloads it;
20
+ the card's log prints in your terminal.
21
+
22
+ Preview in a browser without the app: `npx serve .` and open the page — it runs
23
+ against a mock host with sample agents.
24
+
25
+ ## Check and share
26
+
27
+ ```sh
28
+ npx @neurosquad/card-sdk validate # the app's own checks + the install dialog preview
29
+ npx @neurosquad/card-sdk pack # what would be installed, and the tree hash
30
+ ```
31
+
32
+ Push this folder to GitHub. People install it by pasting `owner/repo` (or
33
+ `owner/repo/path/to/folder`) into **Settings → Custom cards**.
34
+
35
+ Card code runs in a sandbox: no inline scripts, no external resources (bundle
36
+ everything into the folder), network only through `card.net.fetch` to hosts you
37
+ declare in the `network` permission.
@@ -0,0 +1,6 @@
1
+ # packed archives (neurosquad-card pack)
2
+ *.tgz
3
+ .DS_Store
4
+ # secrets never go into a card package (pack refuses them anyway)
5
+ .env*
6
+ !.env.example
Binary file
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__CARD_DISPLAY_NAME__</title>
7
+ <!-- Optional dark kit that follows the app theme (--ns-* variables). Delete it to style from scratch. -->
8
+ <link rel="stylesheet" href="vendor/ui.css" />
9
+ <link rel="stylesheet" href="style.css" />
10
+ <!-- Card code must live in files: the card's CSP blocks inline scripts. -->
11
+ <script type="module" src="main.js"></script>
12
+ </head>
13
+ <body class="ns-kit">
14
+ <main class="app" aria-busy="true">
15
+ <header class="ns-spread">
16
+ <div class="ns-grow">
17
+ <p class="ns-subtitle ns-truncate" id="workspace">&nbsp;</p>
18
+ <h1 class="ns-title ns-truncate" id="greeting">&nbsp;</h1>
19
+ </div>
20
+ <span class="ns-badge ns-badge--accent" id="working" title="">0</span>
21
+ </header>
22
+
23
+ <section class="ns-surface ns-surface--inset agents">
24
+ <p class="ns-subtitle" data-t="agents">Agents</p>
25
+ <ul class="ns-list ns-scroll" id="agents"></ul>
26
+ </section>
27
+
28
+ <section class="ns-field pad">
29
+ <label class="ns-label" for="pad" data-t="scratchpad">Scratchpad</label>
30
+ <textarea class="ns-textarea ns-grow" id="pad" spellcheck="false"></textarea>
31
+ </section>
32
+
33
+ <footer class="ns-spread">
34
+ <span class="ns-small ns-muted ns-truncate" id="hint"></span>
35
+ <button class="ns-btn ns-btn--primary ns-btn--sm" id="send" type="button" data-t="send">
36
+ Send
37
+ </button>
38
+ </footer>
39
+ </main>
40
+ </body>
41
+ </html>