@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// __CARD_DISPLAY_NAME__ — a NeuroSquad card. Docs for every call: vendor/card-sdk.js is
|
|
2
|
+
// @neurosquad/card-sdk; its types and README describe the whole API.
|
|
3
|
+
import {
|
|
4
|
+
connect,
|
|
5
|
+
createTranslator,
|
|
6
|
+
isCardSdkError,
|
|
7
|
+
permissionForPeer,
|
|
8
|
+
requestPermissions
|
|
9
|
+
} from './vendor/card-sdk.js'
|
|
10
|
+
|
|
11
|
+
// Inside NeuroSquad the app hands us a private channel. Opened on its own
|
|
12
|
+
// (npx serve . → http://localhost:3000) the page runs against a mock host.
|
|
13
|
+
const card = window.parent === window ? await previewHost() : await connect()
|
|
14
|
+
|
|
15
|
+
const t = createTranslator(
|
|
16
|
+
{
|
|
17
|
+
en: {
|
|
18
|
+
agents: 'Agents',
|
|
19
|
+
scratchpad: 'Scratchpad',
|
|
20
|
+
send: 'Send',
|
|
21
|
+
noAgents: 'No agents on this canvas yet',
|
|
22
|
+
working_one: '{{count}} agent working',
|
|
23
|
+
working_other: '{{count}} agents working',
|
|
24
|
+
sent_one: 'Sent to {{count}} card',
|
|
25
|
+
sent_other: 'Sent to {{count}} cards',
|
|
26
|
+
notConnected: 'Draw an arrow from this card to a note to send it there',
|
|
27
|
+
status: {
|
|
28
|
+
working: 'working',
|
|
29
|
+
'needs-input': 'waiting for you',
|
|
30
|
+
finished: 'finished',
|
|
31
|
+
idle: 'idle',
|
|
32
|
+
exited: 'stopped'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
ru: {
|
|
36
|
+
agents: 'Агенты',
|
|
37
|
+
scratchpad: 'Черновик',
|
|
38
|
+
send: 'Отправить',
|
|
39
|
+
noAgents: 'На холсте пока нет агентов',
|
|
40
|
+
working_one: 'Работает {{count}} агент',
|
|
41
|
+
working_few: 'Работают {{count}} агента',
|
|
42
|
+
working_many: 'Работают {{count}} агентов',
|
|
43
|
+
working_other: 'Работают {{count}} агента',
|
|
44
|
+
sent_one: 'Отправлено в {{count}} карточку',
|
|
45
|
+
sent_few: 'Отправлено в {{count}} карточки',
|
|
46
|
+
sent_many: 'Отправлено в {{count}} карточек',
|
|
47
|
+
sent_other: 'Отправлено в {{count}} карточки',
|
|
48
|
+
notConnected: 'Проведите стрелку от этой карточки к заметке, чтобы отправить туда',
|
|
49
|
+
status: {
|
|
50
|
+
working: 'работает',
|
|
51
|
+
'needs-input': 'ждёт вас',
|
|
52
|
+
finished: 'закончил',
|
|
53
|
+
idle: 'свободен',
|
|
54
|
+
exited: 'остановлен'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
zh: {
|
|
58
|
+
agents: '智能体',
|
|
59
|
+
scratchpad: '草稿',
|
|
60
|
+
send: '发送',
|
|
61
|
+
noAgents: '画布上还没有智能体',
|
|
62
|
+
working_other: '{{count}} 个智能体正在工作',
|
|
63
|
+
sent_other: '已发送到 {{count}} 张卡片',
|
|
64
|
+
notConnected: '从此卡片画一条箭头到笔记,即可发送到那里',
|
|
65
|
+
status: {
|
|
66
|
+
working: '工作中',
|
|
67
|
+
'needs-input': '等待你',
|
|
68
|
+
finished: '已完成',
|
|
69
|
+
idle: '空闲',
|
|
70
|
+
exited: '已停止'
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
card
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const $ = (id) => document.getElementById(id)
|
|
78
|
+
const pad = $('pad')
|
|
79
|
+
let agents = []
|
|
80
|
+
|
|
81
|
+
// --- render --------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function renderStatic() {
|
|
84
|
+
document.querySelectorAll('[data-t]').forEach((el) => (el.textContent = t(el.dataset.t)))
|
|
85
|
+
$('workspace').textContent = card.workspace.name
|
|
86
|
+
$('greeting').textContent = card.settings.value('greeting') ?? card.instance.displayName
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TONE = {
|
|
90
|
+
working: 'accent',
|
|
91
|
+
'needs-input': 'warning',
|
|
92
|
+
finished: 'success',
|
|
93
|
+
idle: '',
|
|
94
|
+
exited: 'danger'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderAgents() {
|
|
98
|
+
const list = $('agents')
|
|
99
|
+
list.replaceChildren(
|
|
100
|
+
...agents.map((agent) => {
|
|
101
|
+
const item = document.createElement('li')
|
|
102
|
+
item.className = 'ns-list-item'
|
|
103
|
+
const dot = document.createElement('span')
|
|
104
|
+
dot.className = `ns-dot ${TONE[agent.status] ? `ns-dot--${TONE[agent.status]}` : ''}`
|
|
105
|
+
const name = document.createElement('span')
|
|
106
|
+
name.className = 'ns-truncate'
|
|
107
|
+
name.textContent = agent.name
|
|
108
|
+
const status = document.createElement('span')
|
|
109
|
+
status.className = 'agent-status ns-muted'
|
|
110
|
+
status.textContent = t(`status.${agent.status}`)
|
|
111
|
+
item.append(dot, name, status)
|
|
112
|
+
return item
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
if (agents.length === 0) {
|
|
116
|
+
const empty = document.createElement('li')
|
|
117
|
+
empty.className = 'ns-list-item ns-muted'
|
|
118
|
+
empty.textContent = t('noAgents')
|
|
119
|
+
list.append(empty)
|
|
120
|
+
}
|
|
121
|
+
const working = agents.filter((agent) => agent.status === 'working').length
|
|
122
|
+
$('working').textContent = String(working)
|
|
123
|
+
$('working').title = t('working', { count: working })
|
|
124
|
+
// What the overview tile shows when the canvas is zoomed out.
|
|
125
|
+
card.setOverview({
|
|
126
|
+
primary: t('working', { count: working }),
|
|
127
|
+
tone: working ? 'accent' : 'default',
|
|
128
|
+
icon: 'users'
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- data ------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async function loadAgents() {
|
|
135
|
+
try {
|
|
136
|
+
agents = await card.agents.list()
|
|
137
|
+
} catch (error) {
|
|
138
|
+
card.log.warn('could not list agents', error)
|
|
139
|
+
agents = []
|
|
140
|
+
}
|
|
141
|
+
renderAgents()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
card.agents.onChanged((next) => {
|
|
145
|
+
agents = next
|
|
146
|
+
renderAgents()
|
|
147
|
+
})
|
|
148
|
+
card.agents.onStatus(({ agentId, status }) => {
|
|
149
|
+
agents = agents.map((agent) => (agent.id === agentId ? { ...agent, status } : agent))
|
|
150
|
+
renderAgents()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// The scratchpad is saved in the card's own storage (survives reloads and restarts).
|
|
154
|
+
pad.value = await card.storage.get('scratchpad', '')
|
|
155
|
+
let saveTimer
|
|
156
|
+
pad.addEventListener('input', () => {
|
|
157
|
+
clearTimeout(saveTimer)
|
|
158
|
+
saveTimer = setTimeout(() => card.storage.set('scratchpad', pad.value), 400)
|
|
159
|
+
})
|
|
160
|
+
card.lifecycle.onSuspend(() => card.storage.set('scratchpad', pad.value))
|
|
161
|
+
|
|
162
|
+
// Input port: text from a connected card lands in the scratchpad.
|
|
163
|
+
card.ports.onMessage((text) => {
|
|
164
|
+
pad.value = pad.value ? `${pad.value}\n${text}` : String(text)
|
|
165
|
+
card.storage.set('scratchpad', pad.value)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Output port: Send pushes the scratchpad to connected cards (a note appends it).
|
|
169
|
+
$('send').addEventListener('click', async () => {
|
|
170
|
+
try {
|
|
171
|
+
// A note's input needs the optional cards.connected permission: ask for
|
|
172
|
+
// it (the app shows its own prompt) before the first send.
|
|
173
|
+
const needed = card.ports.peers
|
|
174
|
+
.filter((peer) => peer.direction !== 'upstream')
|
|
175
|
+
.map((peer) => permissionForPeer(peer, { outputType: 'ns:markdown' }))
|
|
176
|
+
.filter((id) => id !== null)
|
|
177
|
+
if (needed.length > 0) await requestPermissions(card, ...new Set(needed))
|
|
178
|
+
const delivered = await card.ports.emit('scratchpad', pad.value)
|
|
179
|
+
$('hint').textContent = delivered > 0 ? t('sent', { count: delivered }) : t('notConnected')
|
|
180
|
+
} catch (error) {
|
|
181
|
+
$('hint').textContent = isCardSdkError(error) ? error.hostMessage : String(error)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// A tool for connected agents: they can read what the user wrote here.
|
|
186
|
+
card.tools.handle('read_scratchpad', () => pad.value || '(the scratchpad is empty)')
|
|
187
|
+
|
|
188
|
+
// Keep the UI in step with the app.
|
|
189
|
+
card.settings.onChange(renderStatic)
|
|
190
|
+
t.onChange(() => {
|
|
191
|
+
renderStatic()
|
|
192
|
+
renderAgents()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
renderStatic()
|
|
196
|
+
await loadAgents()
|
|
197
|
+
document.querySelector('main').removeAttribute('aria-busy')
|
|
198
|
+
|
|
199
|
+
// --- standalone preview -------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
async function previewHost() {
|
|
202
|
+
const { createMockHost } = await import('./vendor/testing.js')
|
|
203
|
+
const manifest = await (await fetch('./neurosquad-card.json')).json()
|
|
204
|
+
const host = createMockHost({
|
|
205
|
+
manifest,
|
|
206
|
+
agents: [
|
|
207
|
+
{
|
|
208
|
+
id: 'a1',
|
|
209
|
+
name: 'Claude Code',
|
|
210
|
+
harness: 'claude-code',
|
|
211
|
+
kind: 'ai',
|
|
212
|
+
status: 'working',
|
|
213
|
+
connected: true
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'a2',
|
|
217
|
+
name: 'Codex',
|
|
218
|
+
harness: 'codex-cli',
|
|
219
|
+
kind: 'ai',
|
|
220
|
+
status: 'needs-input',
|
|
221
|
+
connected: false
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 't1',
|
|
225
|
+
name: 'PowerShell',
|
|
226
|
+
harness: 'shell-powershell',
|
|
227
|
+
kind: 'shell',
|
|
228
|
+
status: 'idle',
|
|
229
|
+
connected: false
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
peers: [
|
|
233
|
+
{
|
|
234
|
+
cardId: 'note-1',
|
|
235
|
+
kind: 'note',
|
|
236
|
+
name: 'Notes',
|
|
237
|
+
direction: 'downstream',
|
|
238
|
+
inputs: [
|
|
239
|
+
{
|
|
240
|
+
id: 'append',
|
|
241
|
+
label: 'Append',
|
|
242
|
+
type: 'ns:markdown',
|
|
243
|
+
mode: 'stream',
|
|
244
|
+
retain: false,
|
|
245
|
+
default: true,
|
|
246
|
+
// Like the app: sending to a note needs cards.connected.
|
|
247
|
+
permission: 'cards.connected'
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
outputs: []
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
})
|
|
254
|
+
window.mockHost = host // play with it from DevTools: mockHost.setAgentStatus('a2', 'working')
|
|
255
|
+
return host.connect()
|
|
256
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./vendor/neurosquad-card.schema.json",
|
|
3
|
+
"manifestVersion": 1,
|
|
4
|
+
"name": "__CARD_NAME__",
|
|
5
|
+
"displayName": "__CARD_DISPLAY_NAME__",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"protocol": 1,
|
|
8
|
+
"description": "A starter NeuroSquad card: live agent statuses, a scratchpad that is saved, a port and a tool for connected agents.",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"icon": "icon.png",
|
|
11
|
+
"entry": "index.html",
|
|
12
|
+
"card": {
|
|
13
|
+
"defaultSize": {
|
|
14
|
+
"w": 420,
|
|
15
|
+
"h": 360
|
|
16
|
+
},
|
|
17
|
+
"minSize": {
|
|
18
|
+
"w": 280,
|
|
19
|
+
"h": 220
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"permissions": [
|
|
23
|
+
{
|
|
24
|
+
"id": "agents.read",
|
|
25
|
+
"reason": {
|
|
26
|
+
"en": "Shows which agents are working",
|
|
27
|
+
"ru": "Показывает, какие агенты работают",
|
|
28
|
+
"zh": "显示哪些智能体正在工作"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "cards.connected",
|
|
33
|
+
"optional": true,
|
|
34
|
+
"reason": {
|
|
35
|
+
"en": "Sends the scratchpad to a note you connect with an arrow",
|
|
36
|
+
"ru": "Отправляет черновик в заметку, соединённую стрелкой",
|
|
37
|
+
"zh": "将草稿发送到用箭头连接的笔记"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"settings": [
|
|
42
|
+
{
|
|
43
|
+
"key": "greeting",
|
|
44
|
+
"type": "string",
|
|
45
|
+
"label": {
|
|
46
|
+
"en": "Greeting",
|
|
47
|
+
"ru": "Приветствие",
|
|
48
|
+
"zh": "问候语"
|
|
49
|
+
},
|
|
50
|
+
"default": "Hello from __CARD_DISPLAY_NAME__",
|
|
51
|
+
"maxLength": 80
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"ports": {
|
|
55
|
+
"inputs": [
|
|
56
|
+
{
|
|
57
|
+
"id": "text",
|
|
58
|
+
"label": {
|
|
59
|
+
"en": "Text in",
|
|
60
|
+
"ru": "Текст",
|
|
61
|
+
"zh": "文本输入"
|
|
62
|
+
},
|
|
63
|
+
"description": "Appended to the scratchpad.",
|
|
64
|
+
"type": "ns:text",
|
|
65
|
+
"default": true
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"outputs": [
|
|
69
|
+
{
|
|
70
|
+
"id": "scratchpad",
|
|
71
|
+
"label": {
|
|
72
|
+
"en": "Scratchpad",
|
|
73
|
+
"ru": "Черновик",
|
|
74
|
+
"zh": "草稿"
|
|
75
|
+
},
|
|
76
|
+
"description": "The scratchpad text, when you press Send.",
|
|
77
|
+
"type": "ns:markdown",
|
|
78
|
+
"retain": true
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"tools": [
|
|
83
|
+
{
|
|
84
|
+
"name": "read_scratchpad",
|
|
85
|
+
"title": "Read the scratchpad",
|
|
86
|
+
"description": "Returns the text the user wrote in this card's scratchpad (may be empty).",
|
|
87
|
+
"inputSchema": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {}
|
|
90
|
+
},
|
|
91
|
+
"readOnly": true
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Your card's own styles. The frame is the card body: fill it and scroll inside. */
|
|
2
|
+
html,
|
|
3
|
+
body {
|
|
4
|
+
height: 100%;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.app {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-rows: auto minmax(64px, 1fr) minmax(96px, 1.2fr) auto;
|
|
11
|
+
grid-template-columns: minmax(0, 1fr);
|
|
12
|
+
gap: 10px;
|
|
13
|
+
height: 100%;
|
|
14
|
+
padding: 12px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.agents {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 6px;
|
|
21
|
+
min-height: 0;
|
|
22
|
+
padding: 10px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.agents .ns-list {
|
|
26
|
+
flex: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.pad {
|
|
30
|
+
min-height: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.pad .ns-textarea {
|
|
34
|
+
height: 100%;
|
|
35
|
+
resize: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.agent-status {
|
|
39
|
+
margin-left: auto;
|
|
40
|
+
font-size: 12px;
|
|
41
|
+
}
|