@newsails/veil-studio 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 (87) hide show
  1. package/README.md +181 -0
  2. package/bin/veil-studio.js +142 -0
  3. package/nuxt-app/.output/public/200.html +13 -0
  4. package/nuxt-app/.output/public/404.html +13 -0
  5. package/nuxt-app/.output/public/_nuxt/builds/latest.json +1 -0
  6. package/nuxt-app/.output/public/_nuxt/builds/meta/6b28df26-54af-4fad-a1f0-38808960d9fe.json +1 -0
  7. package/nuxt-app/.output/public/_nuxt/entry.BrrOeBSX.js +120 -0
  8. package/nuxt-app/.output/public/_nuxt/entry.CYnp7zY5.css +1 -0
  9. package/nuxt-app/.output/public/_nuxt/error-404.BbdzCaXe.js +1 -0
  10. package/nuxt-app/.output/public/_nuxt/error-404.JekaaCis.css +1 -0
  11. package/nuxt-app/.output/public/_nuxt/error-500.CNP9nqm1.css +1 -0
  12. package/nuxt-app/.output/public/_nuxt/error-500.DbOlBIIY.js +1 -0
  13. package/nuxt-app/.output/public/_nuxt/index.BEoXSIOu.css +1 -0
  14. package/nuxt-app/.output/public/_nuxt/index.CNms2yAq.js +1 -0
  15. package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.B7mPwVP_.ttf +0 -0
  16. package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.CSr8KVlo.eot +0 -0
  17. package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.Dp5v-WZN.woff2 +0 -0
  18. package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.PXm3-2wK.woff +0 -0
  19. package/nuxt-app/.output/public/_nuxt/vue.-sixQ7xP.BlWffD__.js +1 -0
  20. package/nuxt-app/.output/public/index.html +13 -0
  21. package/package.json +37 -0
  22. package/server/index.js +184 -0
  23. package/server/routes/files.js +80 -0
  24. package/server/socket.js +507 -0
  25. package/server/utils/board-state.js +357 -0
  26. package/server/utils/config.js +51 -0
  27. package/server/utils/db.js +484 -0
  28. package/server/utils/element-instances.js +104 -0
  29. package/server/utils/element-registry.js +127 -0
  30. package/server/utils/elements/agent-instance.js +62 -0
  31. package/server/utils/elements/agent.js +93 -0
  32. package/server/utils/elements/annotation.js +30 -0
  33. package/server/utils/elements/approval-gate.js +49 -0
  34. package/server/utils/elements/assumption.js +32 -0
  35. package/server/utils/elements/blocker.js +36 -0
  36. package/server/utils/elements/chat-room.js +47 -0
  37. package/server/utils/elements/chat-view.js +33 -0
  38. package/server/utils/elements/code-block.js +39 -0
  39. package/server/utils/elements/collapsible-group.js +37 -0
  40. package/server/utils/elements/comparison-table.js +38 -0
  41. package/server/utils/elements/constraint.js +32 -0
  42. package/server/utils/elements/decision.js +39 -0
  43. package/server/utils/elements/diff-patch.js +38 -0
  44. package/server/utils/elements/divider.js +30 -0
  45. package/server/utils/elements/document-draft.js +35 -0
  46. package/server/utils/elements/fact-claim.js +36 -0
  47. package/server/utils/elements/feedback-request.js +43 -0
  48. package/server/utils/elements/file-reference.js +31 -0
  49. package/server/utils/elements/filter.js +48 -0
  50. package/server/utils/elements/generator.js +51 -0
  51. package/server/utils/elements/goal.js +36 -0
  52. package/server/utils/elements/html.js +35 -0
  53. package/server/utils/elements/idea.js +32 -0
  54. package/server/utils/elements/image-local.js +21 -0
  55. package/server/utils/elements/image.js +34 -0
  56. package/server/utils/elements/json-object.js +47 -0
  57. package/server/utils/elements/label-tag.js +30 -0
  58. package/server/utils/elements/markdown.js +37 -0
  59. package/server/utils/elements/merger.js +36 -0
  60. package/server/utils/elements/message.js +40 -0
  61. package/server/utils/elements/milestone.js +44 -0
  62. package/server/utils/elements/notification.js +34 -0
  63. package/server/utils/elements/outline.js +35 -0
  64. package/server/utils/elements/primitive.js +36 -0
  65. package/server/utils/elements/pro-con-list.js +39 -0
  66. package/server/utils/elements/processor.js +54 -0
  67. package/server/utils/elements/project.js +40 -0
  68. package/server/utils/elements/question.js +42 -0
  69. package/server/utils/elements/queue.js +85 -0
  70. package/server/utils/elements/research-note.js +41 -0
  71. package/server/utils/elements/section.js +35 -0
  72. package/server/utils/elements/source-collection.js +45 -0
  73. package/server/utils/elements/splitter.js +42 -0
  74. package/server/utils/elements/status-update.js +38 -0
  75. package/server/utils/elements/task.js +72 -0
  76. package/server/utils/elements/template.js +46 -0
  77. package/server/utils/elements/test-case.js +42 -0
  78. package/server/utils/elements/text.js +29 -0
  79. package/server/utils/elements/todo-list.js +57 -0
  80. package/server/utils/elements/url-card.js +37 -0
  81. package/server/utils/elements/web-search-query.js +46 -0
  82. package/server/utils/elements/web-snapshot.js +37 -0
  83. package/server/utils/file-utils.js +88 -0
  84. package/server/utils/session-watcher.js +108 -0
  85. package/server/utils/socket-io.js +14 -0
  86. package/server/utils/veil-client.js +185 -0
  87. package/server/utils/veil-ws.js +207 -0
@@ -0,0 +1,80 @@
1
+ 'use strict'
2
+
3
+ const router = require('express').Router()
4
+ const {
5
+ buildFileTree,
6
+ readProjectFile,
7
+ writeProjectFile,
8
+ createProjectDir,
9
+ renameProjectFile,
10
+ deleteProjectPath,
11
+ } = require('../utils/file-utils.js')
12
+
13
+ function handleError(res, err) {
14
+ const status = err.status || 500
15
+ res.status(status).json({ error: err.message })
16
+ }
17
+
18
+ router.get('/tree', (req, res) => {
19
+ try {
20
+ res.json(buildFileTree())
21
+ } catch (err) {
22
+ handleError(res, err)
23
+ }
24
+ })
25
+
26
+ router.get('/content', (req, res) => {
27
+ try {
28
+ const { path: filePath } = req.query
29
+ if (!filePath) return res.status(400).json({ error: 'path query param required' })
30
+ res.json({ content: readProjectFile(filePath) })
31
+ } catch (err) {
32
+ handleError(res, err)
33
+ }
34
+ })
35
+
36
+ router.put('/content', (req, res) => {
37
+ try {
38
+ const { path: filePath, content } = req.body
39
+ if (!filePath) return res.status(400).json({ error: 'path required' })
40
+ writeProjectFile(filePath, content ?? '')
41
+ res.json({ ok: true })
42
+ } catch (err) {
43
+ handleError(res, err)
44
+ }
45
+ })
46
+
47
+ router.post('/mkdir', (req, res) => {
48
+ try {
49
+ const { path: dirPath } = req.body
50
+ if (!dirPath) return res.status(400).json({ error: 'path required' })
51
+ createProjectDir(dirPath)
52
+ res.json({ ok: true })
53
+ } catch (err) {
54
+ handleError(res, err)
55
+ }
56
+ })
57
+
58
+ router.post('/rename', (req, res) => {
59
+ try {
60
+ const { from, to } = req.body
61
+ if (!from || !to) return res.status(400).json({ error: 'from and to required' })
62
+ renameProjectFile(from, to)
63
+ res.json({ ok: true })
64
+ } catch (err) {
65
+ handleError(res, err)
66
+ }
67
+ })
68
+
69
+ router.post('/delete', (req, res) => {
70
+ try {
71
+ const { path: targetPath } = req.body
72
+ if (!targetPath) return res.status(400).json({ error: 'path required' })
73
+ deleteProjectPath(targetPath)
74
+ res.json({ ok: true })
75
+ } catch (err) {
76
+ handleError(res, err)
77
+ }
78
+ })
79
+
80
+ module.exports = router
@@ -0,0 +1,507 @@
1
+ 'use strict'
2
+
3
+ const boardState = require('./utils/board-state.js')
4
+ const { isVeilConnected } = require('./utils/veil-ws.js')
5
+ const { getAllTypes, getElementType, reloadCustomElements } = require('./utils/element-registry.js')
6
+ const { getInstance, invokeAction, initAllInstances, destroyInstance, createInstance, getViewData, getPorts } = require('./utils/element-instances.js')
7
+ const { getIO } = require('./utils/socket-io.js')
8
+ const {
9
+ veilGet, veilPost, veilPut, veilDelete, veilStreamChat,
10
+ } = require('./utils/veil-client.js')
11
+ const { setActiveStreamsRef, setElementStreamingRef } = require('./utils/veil-ws.js')
12
+ const { touchSession } = require('./utils/session-watcher.js')
13
+
14
+ const _activeStreams = new Map() // sessionId → { stopped: boolean } — shared across all socket connections
15
+ setActiveStreamsRef(_activeStreams)
16
+ setElementStreamingRef(_setElementStreaming)
17
+
18
+ function _setElementStreaming(sessionId, streaming) {
19
+ const elements = boardState.getElements().filter(e => e.data?.sessionId === sessionId)
20
+ for (const el of elements) {
21
+ try {
22
+ const instance = getInstance(el.id)
23
+ if (instance?.actions?.setStreaming) {
24
+ instance.actions.setStreaming({ streaming })
25
+ const viewData = getViewData(el.id)
26
+ const ports = getPorts(el.id)
27
+ getIO().to('board').emit('board:delta', { type: 'element:view', id: el.id, viewData: { ...viewData, ports } })
28
+ }
29
+ } catch (err) {
30
+ console.error('[_setElementStreaming] Error:', err.message)
31
+ }
32
+ }
33
+ }
34
+
35
+ function registerSocketHandlers(io) {
36
+ io.on('connection', (socket) => {
37
+ // Join board room + send initial state
38
+ socket.join('board')
39
+ socket.emit('board:init', { snapshot: boardState.getSnapshot() })
40
+ socket.emit('veil:status', { connected: isVeilConnected() })
41
+ socket.emit('element-types:update', { types: getAllTypes() })
42
+
43
+ // Send initial element view data + ports for all elements
44
+ for (const el of boardState.getElements()) {
45
+ const viewData = getViewData(el.id)
46
+ const ports = getPorts(el.id)
47
+ if (Object.keys(viewData).length > 0 || ports.length > 0) {
48
+ socket.emit('board:delta', { type: 'element:view', id: el.id, viewData: { ...viewData, ports } })
49
+ }
50
+ }
51
+
52
+ // ─── Board Actions ──────────────────────────────────────────────────────
53
+
54
+ socket.on('board:action', (payload, ack) => {
55
+ try {
56
+ _handleBoardAction(payload)
57
+ if (typeof ack === 'function') ack({ ok: true })
58
+ } catch (err) {
59
+ console.error('[board:action] Error:', err.message)
60
+ if (typeof ack === 'function') ack({ error: err.message })
61
+ }
62
+ })
63
+
64
+ socket.on('board:get-archive', (_, ack) => {
65
+ if (typeof ack === 'function') ack({ items: boardState.getArchive() })
66
+ })
67
+
68
+ socket.on('board:get-deleted', (_, ack) => {
69
+ if (typeof ack === 'function') ack({ items: boardState.getDeleted() })
70
+ })
71
+
72
+ socket.on('board:restore-archive', ({ id } = {}, ack) => {
73
+ try {
74
+ const el = boardState.restoreFromArchive(id)
75
+ _createInstanceAndBroadcast(el)
76
+ if (typeof ack === 'function') ack({ ok: true, element: el })
77
+ } catch (err) {
78
+ if (typeof ack === 'function') ack({ error: err.message })
79
+ }
80
+ })
81
+
82
+ socket.on('board:add-agent-instance', async ({ agentName, x, y } = {}, ack) => {
83
+ try {
84
+ if (!agentName) throw new Error('agentName is required')
85
+ const res = await veilPost('/sessions', { agentName })
86
+ const sessionId = res.sessionId || res.id || res.session_id
87
+ if (!sessionId) throw new Error('No sessionId returned from Veil API')
88
+ const el = boardState.createElement({ type: 'agent-instance', x: x ?? 200, y: y ?? 200, width: 180, height: 120, data: { agentName, sessionId } })
89
+ _createInstanceAndBroadcast(el)
90
+ if (typeof ack === 'function') ack({ ok: true, elementId: el.id, sessionId })
91
+ } catch (err) {
92
+ console.error('[board:add-agent-instance] Error:', err.message)
93
+ if (typeof ack === 'function') ack({ error: err.message })
94
+ }
95
+ })
96
+
97
+ socket.on('board:restore-deleted', ({ id } = {}, ack) => {
98
+ try {
99
+ const el = boardState.restoreFromDeleted(id)
100
+ _createInstanceAndBroadcast(el)
101
+ if (typeof ack === 'function') ack({ ok: true, element: el })
102
+ } catch (err) {
103
+ if (typeof ack === 'function') ack({ error: err.message })
104
+ }
105
+ })
106
+
107
+ // ─── Element Actions ────────────────────────────────────────────────────
108
+
109
+ socket.on('element:action', async ({ elementId, action, params } = {}, ack) => {
110
+ try {
111
+ const result = await invokeAction(elementId, action, params)
112
+ if (typeof ack === 'function') ack({ ok: true, result })
113
+ } catch (err) {
114
+ console.error('[element:action] Error:', err.message)
115
+ if (typeof ack === 'function') ack({ error: err.message })
116
+ }
117
+ })
118
+
119
+ socket.on('element:reload-custom', async (_, ack) => {
120
+ try {
121
+ // Destroy instances for elements whose type is currently a custom type
122
+ for (const el of boardState.getElements()) {
123
+ const def = getElementType(el.type)
124
+ if (def && !def.isBuiltIn) await destroyInstance(el.id)
125
+ }
126
+ reloadCustomElements()
127
+ initAllInstances()
128
+ const types = getAllTypes()
129
+ io.emit('element-types:update', { types })
130
+ if (typeof ack === 'function') ack({ ok: true, types })
131
+ } catch (err) {
132
+ if (typeof ack === 'function') ack({ error: err.message })
133
+ }
134
+ })
135
+
136
+ // ─── Chat Streaming ─────────────────────────────────────────────────────
137
+
138
+ socket.on('chat:send', async ({ agentName, sessionId, message } = {}) => {
139
+ const ctrl = { stopped: false }
140
+ _activeStreams.set(sessionId, ctrl)
141
+ // Broadcast user message to all clients immediately so every tab sees it
142
+ getIO().to('board').emit('chat:user-message', { sessionId, agentName, message })
143
+ // Mark element as streaming so all clients can reflect the loading state
144
+ _setElementStreaming(sessionId, true)
145
+ try {
146
+ let lastMessage = null
147
+ let tokenUsage = null
148
+ const io = getIO()
149
+ for await (const chunk of veilStreamChat(agentName, message, sessionId)) {
150
+ if (ctrl.stopped) break
151
+ io.to('board').emit('chat:stream', { sessionId, eventType: chunk.eventType, data: chunk.data })
152
+ if (chunk.eventType === 'message') lastMessage = chunk.data
153
+ if (chunk.data && chunk.data.token_usage) tokenUsage = chunk.data.token_usage
154
+ }
155
+ if (!ctrl.stopped) {
156
+ io.to('board').emit('chat:done', { sessionId, message: lastMessage, tokenUsage })
157
+ } else {
158
+ io.to('board').emit('chat:done', { sessionId, message: null, tokenUsage: null })
159
+ }
160
+ } catch (err) {
161
+ console.error('[chat:send] Error:', err.message)
162
+ getIO().to('board').emit('chat:error', { sessionId, error: err.message })
163
+ } finally {
164
+ _activeStreams.delete(sessionId)
165
+ // Mark element as no longer streaming
166
+ _setElementStreaming(sessionId, false)
167
+ // Advance message baseline so the next external syncSession skips these messages
168
+ touchSession(sessionId).catch(() => {})
169
+ }
170
+ })
171
+
172
+ socket.on('chat:stop', ({ sessionId } = {}) => {
173
+ const ctrl = _activeStreams.get(sessionId)
174
+ if (ctrl) ctrl.stopped = true
175
+ })
176
+
177
+ // ─── VeilCLI Proxy Events ─────────────────────────────────────────────
178
+
179
+ // Agents
180
+ socket.on('veil:get-agents', async (_, ack) => {
181
+ try {
182
+ const result = await veilGet('/agents')
183
+ ack({ agents: Array.isArray(result) ? result : (result.agents ?? []) })
184
+ }
185
+ catch (err) { ack({ error: err.message }) }
186
+ })
187
+
188
+ socket.on('veil:create-agent', async ({ data } = {}, ack) => {
189
+ try { ack({ agent: await veilPost('/agents', data) }) }
190
+ catch (err) { ack({ error: err.message }) }
191
+ })
192
+
193
+ socket.on('veil:get-agent', async ({ name } = {}, ack) => {
194
+ try { ack({ agent: await veilGet(`/agents/${encodeURIComponent(name)}`) }) }
195
+ catch (err) { ack({ error: err.message }) }
196
+ })
197
+
198
+ socket.on('veil:update-agent', async ({ name, data } = {}, ack) => {
199
+ try { ack({ agent: await veilPut(`/agents/${encodeURIComponent(name)}`, data) }) }
200
+ catch (err) { ack({ error: err.message }) }
201
+ })
202
+
203
+ socket.on('veil:delete-agent', async ({ name } = {}, ack) => {
204
+ try { ack({ ok: true, result: await veilDelete(`/agents/${encodeURIComponent(name)}`) }) }
205
+ catch (err) { ack({ error: err.message }) }
206
+ })
207
+
208
+ socket.on('veil:reload-agent', async ({ name } = {}, ack) => {
209
+ try { ack({ ok: true, result: await veilPost(`/agents/${encodeURIComponent(name)}/reload`) }) }
210
+ catch (err) { ack({ error: err.message }) }
211
+ })
212
+
213
+ socket.on('veil:get-agent-skills', async ({ name } = {}, ack) => {
214
+ try { ack({ skills: await veilGet(`/agents/${encodeURIComponent(name)}/skills`) }) }
215
+ catch (err) { ack({ error: err.message }) }
216
+ })
217
+
218
+ // Sessions
219
+ socket.on('veil:get-sessions', async ({ agentName } = {}, ack) => {
220
+ try {
221
+ const q = agentName ? { agent: agentName } : {}
222
+ ack({ sessions: await veilGet('/sessions', q) })
223
+ } catch (err) { ack({ error: err.message }) }
224
+ })
225
+
226
+ socket.on('veil:create-session', async ({ agentName } = {}, ack) => {
227
+ try {
228
+ const session = await veilPost('/sessions', { agentName })
229
+ ack({ session })
230
+ } catch (err) { ack({ error: err.message }) }
231
+ })
232
+
233
+ socket.on('veil:get-session', async ({ id } = {}, ack) => {
234
+ try { ack({ session: await veilGet(`/sessions/${encodeURIComponent(id)}`) }) }
235
+ catch (err) { ack({ error: err.message }) }
236
+ })
237
+
238
+ socket.on('veil:delete-session', async ({ id } = {}, ack) => {
239
+ try { ack({ ok: true, result: await veilDelete(`/sessions/${encodeURIComponent(id)}`) }) }
240
+ catch (err) { ack({ error: err.message }) }
241
+ })
242
+
243
+ socket.on('veil:get-session-messages', async ({ id } = {}, ack) => {
244
+ try {
245
+ const [msgRes, sessionRes] = await Promise.all([
246
+ veilGet(`/sessions/${encodeURIComponent(id)}/messages`),
247
+ veilGet(`/sessions/${encodeURIComponent(id)}`).catch(() => null),
248
+ ])
249
+ ack({
250
+ messages: Array.isArray(msgRes) ? msgRes : (msgRes.messages ?? []),
251
+ session: sessionRes,
252
+ })
253
+ }
254
+ catch (err) { ack({ error: err.message }) }
255
+ })
256
+
257
+ socket.on('veil:reset-session', async ({ id } = {}, ack) => {
258
+ try { ack({ ok: true, result: await veilPost(`/sessions/${encodeURIComponent(id)}/reset`) }) }
259
+ catch (err) { ack({ error: err.message }) }
260
+ })
261
+
262
+ // Tasks
263
+ socket.on('veil:create-task', async ({ agentName, prompt } = {}, ack) => {
264
+ try { ack({ task: await veilPost('/tasks', { agent: agentName, prompt }) }) }
265
+ catch (err) { ack({ error: err.message }) }
266
+ })
267
+
268
+ socket.on('veil:get-tasks', async ({ agentName } = {}, ack) => {
269
+ try {
270
+ const q = agentName ? { agent: agentName } : {}
271
+ ack({ tasks: await veilGet('/tasks', q) })
272
+ } catch (err) { ack({ error: err.message }) }
273
+ })
274
+
275
+ socket.on('veil:get-task', async ({ id } = {}, ack) => {
276
+ try { ack({ task: await veilGet(`/tasks/${encodeURIComponent(id)}`) }) }
277
+ catch (err) { ack({ error: err.message }) }
278
+ })
279
+
280
+ socket.on('veil:cancel-task', async ({ id } = {}, ack) => {
281
+ try { ack({ ok: true, result: await veilPost(`/tasks/${encodeURIComponent(id)}/cancel`) }) }
282
+ catch (err) { ack({ error: err.message }) }
283
+ })
284
+
285
+ socket.on('veil:respond-task', async ({ id, message } = {}, ack) => {
286
+ try { ack({ ok: true, result: await veilPost(`/tasks/${encodeURIComponent(id)}/respond`, { message }) }) }
287
+ catch (err) { ack({ error: err.message }) }
288
+ })
289
+
290
+ // Daemons
291
+ socket.on('veil:get-daemons', async (_, ack) => {
292
+ try { ack({ daemons: await veilGet('/daemons') }) }
293
+ catch (err) { ack({ error: err.message }) }
294
+ })
295
+
296
+ socket.on('veil:daemon-start', async ({ agentName } = {}, ack) => {
297
+ try { ack({ ok: true, result: await veilPost(`/daemons/${encodeURIComponent(agentName)}/start`) }) }
298
+ catch (err) { ack({ error: err.message }) }
299
+ })
300
+
301
+ socket.on('veil:daemon-stop', async ({ agentName } = {}, ack) => {
302
+ try { ack({ ok: true, result: await veilPost(`/daemons/${encodeURIComponent(agentName)}/stop`) }) }
303
+ catch (err) { ack({ error: err.message }) }
304
+ })
305
+
306
+ socket.on('veil:daemon-trigger', async ({ agentName } = {}, ack) => {
307
+ try { ack({ ok: true, result: await veilPost(`/daemons/${encodeURIComponent(agentName)}/trigger`) }) }
308
+ catch (err) { ack({ error: err.message }) }
309
+ })
310
+
311
+ // Memory
312
+ socket.on('veil:get-memory', async ({ agentName, level } = {}, ack) => {
313
+ try {
314
+ const path = (level === 'global' || !agentName)
315
+ ? '/memory'
316
+ : `/agents/${encodeURIComponent(agentName)}/memory`
317
+ const res = await veilGet(path)
318
+ ack({ files: res.files ?? [] })
319
+ } catch (err) { ack({ error: err.message }) }
320
+ })
321
+
322
+ socket.on('veil:get-memory-file', async ({ agentName, file, level } = {}, ack) => {
323
+ try {
324
+ const path = (level === 'global' || !agentName)
325
+ ? `/memory/${encodeURIComponent(file)}`
326
+ : `/agents/${encodeURIComponent(agentName)}/memory/${encodeURIComponent(file)}`
327
+ const res = await veilGet(path)
328
+ ack({ content: res.content ?? '' })
329
+ } catch (err) { ack({ error: err.message }) }
330
+ })
331
+
332
+ socket.on('veil:update-memory-file', async ({ agentName, file, content, level } = {}, ack) => {
333
+ try {
334
+ const path = (level === 'global' || !agentName)
335
+ ? `/memory/${encodeURIComponent(file)}`
336
+ : `/agents/${encodeURIComponent(agentName)}/memory/${encodeURIComponent(file)}`
337
+ ack({ ok: true, result: await veilPut(path, { content }) })
338
+ } catch (err) { ack({ error: err.message }) }
339
+ })
340
+
341
+ socket.on('veil:delete-memory-file', async ({ agentName, file, level } = {}, ack) => {
342
+ try {
343
+ const path = (level === 'global' || !agentName)
344
+ ? `/memory/${encodeURIComponent(file)}`
345
+ : `/agents/${encodeURIComponent(agentName)}/memory/${encodeURIComponent(file)}`
346
+ ack({ ok: true, result: await veilDelete(path) })
347
+ } catch (err) { ack({ error: err.message }) }
348
+ })
349
+
350
+ // Models
351
+ socket.on('veil:get-models', async (_, ack) => {
352
+ try { ack({ models: await veilGet('/models') }) }
353
+ catch (err) { ack({ error: err.message }) }
354
+ })
355
+
356
+ // Agent AGENT.md — read from disk via agentFolder returned by GET /agents/:name
357
+ socket.on('veil:get-agent-md', async ({ name } = {}, ack) => {
358
+ try {
359
+ const fs = require('fs')
360
+ const path = require('path')
361
+ const agentRes = await veilGet(`/agents/${encodeURIComponent(name)}`)
362
+ const agentData = agentRes.agent ?? agentRes
363
+ const folder = agentData.agentFolder
364
+ if (!folder) { ack({ content: '' }); return }
365
+ const mdPath = path.join(folder, 'AGENT.md')
366
+ const content = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, 'utf8') : ''
367
+ ack({ content })
368
+ } catch (err) { ack({ error: err.message }) }
369
+ })
370
+
371
+ // Settings
372
+ socket.on('veil:get-settings', async (_, ack) => {
373
+ try { ack({ settings: await veilGet('/settings') }) }
374
+ catch (err) { ack({ error: err.message }) }
375
+ })
376
+
377
+ socket.on('veil:update-settings', async ({ settings } = {}, ack) => {
378
+ try { ack({ ok: true, result: await veilPut('/settings', settings) }) }
379
+ catch (err) { ack({ error: err.message }) }
380
+ })
381
+ })
382
+ }
383
+
384
+ // ─── Board Action Dispatch ──────────────────────────────────────────────────
385
+
386
+ function _handleBoardAction(payload) {
387
+ const { type, id, payload: data = {} } = payload
388
+
389
+ switch (type) {
390
+ case 'element:add': {
391
+ const { type: elType, x, y, data: elData } = data
392
+ const el = boardState.createElement({ type: elType, x, y, data: elData })
393
+ _createInstanceAndBroadcast(el)
394
+ break
395
+ }
396
+ case 'element:move':
397
+ boardState.updateElement(id, { x: data.x, y: data.y })
398
+ break
399
+
400
+ case 'element:resize':
401
+ boardState.updateElement(id, { width: data.width, height: data.height })
402
+ break
403
+
404
+ case 'element:expand':
405
+ boardState.updateElement(id, { expanded: true })
406
+ break
407
+
408
+ case 'element:collapse':
409
+ boardState.updateElement(id, { expanded: false })
410
+ break
411
+
412
+ case 'element:delete':
413
+ boardState.removeElement(id)
414
+ break
415
+
416
+ case 'element:archive':
417
+ boardState.archiveElement(id)
418
+ break
419
+
420
+ case 'element:duplicate': {
421
+ const { offsetX = 40, offsetY = 40 } = data
422
+ const duped = boardState.duplicateElement(id, offsetX, offsetY)
423
+ _createInstanceAndBroadcast(duped)
424
+ break
425
+ }
426
+
427
+ case 'element:update':
428
+ boardState.updateElement(id, data)
429
+ break
430
+
431
+ case 'link:create': {
432
+ const { fromElementId, fromPortKey, toElementId, toPortKey } = data
433
+ boardState.createLink({ fromElementId, fromPortKey, toElementId, toPortKey })
434
+ break
435
+ }
436
+
437
+ case 'link:delete':
438
+ boardState.removeLink(id)
439
+ break
440
+
441
+ case 'zone:create': {
442
+ const { x, y, width, height, name, color } = data
443
+ boardState.createZone({ x, y, width, height, name, color })
444
+ break
445
+ }
446
+
447
+ case 'zone:update':
448
+ boardState.updateZone(id, data)
449
+ break
450
+
451
+ case 'zone:delete':
452
+ boardState.removeZone(id)
453
+ break
454
+
455
+ case 'view:save': {
456
+ const { name, cameraX, cameraY, zoom } = data
457
+ boardState.createView({ name, cameraX, cameraY, zoom })
458
+ break
459
+ }
460
+
461
+ case 'view:delete':
462
+ boardState.removeView(id)
463
+ break
464
+
465
+ case 'meta:set': {
466
+ const { key, value } = data
467
+ boardState.setMeta(key, value)
468
+ break
469
+ }
470
+
471
+ case 'shape:add': {
472
+ const { subtype, x, y, width, height, fillColor, strokeColor, strokeWidth, text, title, textAlign } = data
473
+ boardState.createShape({ subtype, x, y, width, height, fillColor, strokeColor, strokeWidth, text, title, textAlign })
474
+ break
475
+ }
476
+
477
+ case 'shape:update':
478
+ boardState.updateShape(id, data)
479
+ break
480
+
481
+ case 'shape:delete':
482
+ boardState.removeShape(id)
483
+ break
484
+
485
+ default:
486
+ throw new Error(`Unknown board action type: ${type}`)
487
+ }
488
+ }
489
+
490
+ function _createInstanceAndBroadcast(el) {
491
+ try {
492
+ createInstance(el)
493
+ const viewData = getViewData(el.id)
494
+ const ports = getPorts(el.id)
495
+ if (Object.keys(viewData).length > 0 || ports.length > 0) {
496
+ getIO().to('board').emit('board:delta', {
497
+ type: 'element:view',
498
+ id: el.id,
499
+ viewData: { ...viewData, ports },
500
+ })
501
+ }
502
+ } catch (err) {
503
+ console.error('[socket] _createInstanceAndBroadcast error:', err.message)
504
+ }
505
+ }
506
+
507
+ module.exports = { registerSocketHandlers }