@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.
- package/README.md +181 -0
- package/bin/veil-studio.js +142 -0
- package/nuxt-app/.output/public/200.html +13 -0
- package/nuxt-app/.output/public/404.html +13 -0
- package/nuxt-app/.output/public/_nuxt/builds/latest.json +1 -0
- package/nuxt-app/.output/public/_nuxt/builds/meta/6b28df26-54af-4fad-a1f0-38808960d9fe.json +1 -0
- package/nuxt-app/.output/public/_nuxt/entry.BrrOeBSX.js +120 -0
- package/nuxt-app/.output/public/_nuxt/entry.CYnp7zY5.css +1 -0
- package/nuxt-app/.output/public/_nuxt/error-404.BbdzCaXe.js +1 -0
- package/nuxt-app/.output/public/_nuxt/error-404.JekaaCis.css +1 -0
- package/nuxt-app/.output/public/_nuxt/error-500.CNP9nqm1.css +1 -0
- package/nuxt-app/.output/public/_nuxt/error-500.DbOlBIIY.js +1 -0
- package/nuxt-app/.output/public/_nuxt/index.BEoXSIOu.css +1 -0
- package/nuxt-app/.output/public/_nuxt/index.CNms2yAq.js +1 -0
- package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.B7mPwVP_.ttf +0 -0
- package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.CSr8KVlo.eot +0 -0
- package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.Dp5v-WZN.woff2 +0 -0
- package/nuxt-app/.output/public/_nuxt/materialdesignicons-webfont.PXm3-2wK.woff +0 -0
- package/nuxt-app/.output/public/_nuxt/vue.-sixQ7xP.BlWffD__.js +1 -0
- package/nuxt-app/.output/public/index.html +13 -0
- package/package.json +37 -0
- package/server/index.js +184 -0
- package/server/routes/files.js +80 -0
- package/server/socket.js +507 -0
- package/server/utils/board-state.js +357 -0
- package/server/utils/config.js +51 -0
- package/server/utils/db.js +484 -0
- package/server/utils/element-instances.js +104 -0
- package/server/utils/element-registry.js +127 -0
- package/server/utils/elements/agent-instance.js +62 -0
- package/server/utils/elements/agent.js +93 -0
- package/server/utils/elements/annotation.js +30 -0
- package/server/utils/elements/approval-gate.js +49 -0
- package/server/utils/elements/assumption.js +32 -0
- package/server/utils/elements/blocker.js +36 -0
- package/server/utils/elements/chat-room.js +47 -0
- package/server/utils/elements/chat-view.js +33 -0
- package/server/utils/elements/code-block.js +39 -0
- package/server/utils/elements/collapsible-group.js +37 -0
- package/server/utils/elements/comparison-table.js +38 -0
- package/server/utils/elements/constraint.js +32 -0
- package/server/utils/elements/decision.js +39 -0
- package/server/utils/elements/diff-patch.js +38 -0
- package/server/utils/elements/divider.js +30 -0
- package/server/utils/elements/document-draft.js +35 -0
- package/server/utils/elements/fact-claim.js +36 -0
- package/server/utils/elements/feedback-request.js +43 -0
- package/server/utils/elements/file-reference.js +31 -0
- package/server/utils/elements/filter.js +48 -0
- package/server/utils/elements/generator.js +51 -0
- package/server/utils/elements/goal.js +36 -0
- package/server/utils/elements/html.js +35 -0
- package/server/utils/elements/idea.js +32 -0
- package/server/utils/elements/image-local.js +21 -0
- package/server/utils/elements/image.js +34 -0
- package/server/utils/elements/json-object.js +47 -0
- package/server/utils/elements/label-tag.js +30 -0
- package/server/utils/elements/markdown.js +37 -0
- package/server/utils/elements/merger.js +36 -0
- package/server/utils/elements/message.js +40 -0
- package/server/utils/elements/milestone.js +44 -0
- package/server/utils/elements/notification.js +34 -0
- package/server/utils/elements/outline.js +35 -0
- package/server/utils/elements/primitive.js +36 -0
- package/server/utils/elements/pro-con-list.js +39 -0
- package/server/utils/elements/processor.js +54 -0
- package/server/utils/elements/project.js +40 -0
- package/server/utils/elements/question.js +42 -0
- package/server/utils/elements/queue.js +85 -0
- package/server/utils/elements/research-note.js +41 -0
- package/server/utils/elements/section.js +35 -0
- package/server/utils/elements/source-collection.js +45 -0
- package/server/utils/elements/splitter.js +42 -0
- package/server/utils/elements/status-update.js +38 -0
- package/server/utils/elements/task.js +72 -0
- package/server/utils/elements/template.js +46 -0
- package/server/utils/elements/test-case.js +42 -0
- package/server/utils/elements/text.js +29 -0
- package/server/utils/elements/todo-list.js +57 -0
- package/server/utils/elements/url-card.js +37 -0
- package/server/utils/elements/web-search-query.js +46 -0
- package/server/utils/elements/web-snapshot.js +37 -0
- package/server/utils/file-utils.js +88 -0
- package/server/utils/session-watcher.js +108 -0
- package/server/utils/socket-io.js +14 -0
- package/server/utils/veil-client.js +185 -0
- package/server/utils/veil-ws.js +207 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: 'text',
|
|
5
|
+
name: 'Text',
|
|
6
|
+
description: 'A plain text note.',
|
|
7
|
+
icon: 'type',
|
|
8
|
+
defaultWidth: 200,
|
|
9
|
+
defaultHeight: 80,
|
|
10
|
+
expansionMode: 'inline',
|
|
11
|
+
tags: ['data', 'content'],
|
|
12
|
+
isBuiltIn: true,
|
|
13
|
+
|
|
14
|
+
createInstance(context) {
|
|
15
|
+
const { element } = context
|
|
16
|
+
return {
|
|
17
|
+
getViewData() { return { content: element.data.content || '' } },
|
|
18
|
+
getPorts() {
|
|
19
|
+
return [{ key: 'text-out', direction: 'output', dataType: 'text', label: 'Text Out' }]
|
|
20
|
+
},
|
|
21
|
+
actions: {
|
|
22
|
+
update({ content } = {}) {
|
|
23
|
+
const { updateElement } = require('../board-state.js')
|
|
24
|
+
updateElement(element.id, { data: { ...element.data, content } })
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: 'todo-list',
|
|
5
|
+
name: 'Todo List',
|
|
6
|
+
description: 'A simple todo list linked to an agent session.',
|
|
7
|
+
icon: 'check-square',
|
|
8
|
+
defaultWidth: 200,
|
|
9
|
+
defaultHeight: 120,
|
|
10
|
+
expansionMode: 'inline',
|
|
11
|
+
tags: ['cli', 'todo'],
|
|
12
|
+
isBuiltIn: true,
|
|
13
|
+
|
|
14
|
+
createInstance(context) {
|
|
15
|
+
const { element } = context
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
getViewData() {
|
|
19
|
+
return {
|
|
20
|
+
sessionId: element.data.sessionId || null,
|
|
21
|
+
agentName: element.data.agentName || null,
|
|
22
|
+
items: element.data.items || [],
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
getPorts() {
|
|
27
|
+
return [
|
|
28
|
+
{ key: 'items-in', direction: 'input', dataType: 'any', label: 'Items In' },
|
|
29
|
+
{ key: 'items-out', direction: 'output', dataType: 'any', label: 'Items Out' },
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
actions: {
|
|
34
|
+
addItem({ text } = {}) {
|
|
35
|
+
if (!text) throw new Error('text required')
|
|
36
|
+
const { updateElement } = require('../board-state.js')
|
|
37
|
+
const items = [...(element.data.items || []), { id: Date.now().toString(), text, done: false }]
|
|
38
|
+
updateElement(element.id, { data: { ...element.data, items } })
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
toggleItem({ id } = {}) {
|
|
42
|
+
if (!id) throw new Error('id required')
|
|
43
|
+
const { updateElement } = require('../board-state.js')
|
|
44
|
+
const items = (element.data.items || []).map(i => i.id === id ? { ...i, done: !i.done } : i)
|
|
45
|
+
updateElement(element.id, { data: { ...element.data, items } })
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
removeItem({ id } = {}) {
|
|
49
|
+
if (!id) throw new Error('id required')
|
|
50
|
+
const { updateElement } = require('../board-state.js')
|
|
51
|
+
const items = (element.data.items || []).filter(i => i.id !== id)
|
|
52
|
+
updateElement(element.id, { data: { ...element.data, items } })
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: 'url-card',
|
|
5
|
+
name: 'URL Card',
|
|
6
|
+
description: 'Display a URL with title, description, and favicon.',
|
|
7
|
+
icon: 'link',
|
|
8
|
+
defaultWidth: 260,
|
|
9
|
+
defaultHeight: 90,
|
|
10
|
+
expansionMode: 'inline',
|
|
11
|
+
tags: ['data', 'web'],
|
|
12
|
+
isBuiltIn: true,
|
|
13
|
+
hiddenFromPalette: true,
|
|
14
|
+
|
|
15
|
+
createInstance(context) {
|
|
16
|
+
const { element } = context
|
|
17
|
+
return {
|
|
18
|
+
getViewData() {
|
|
19
|
+
return {
|
|
20
|
+
url: element.data.url || '',
|
|
21
|
+
title: element.data.title || '',
|
|
22
|
+
description: element.data.description || '',
|
|
23
|
+
favicon: element.data.favicon || null,
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
getPorts() {
|
|
27
|
+
return [{ key: 'url-out', direction: 'output', dataType: 'text', label: 'URL Out' }]
|
|
28
|
+
},
|
|
29
|
+
actions: {
|
|
30
|
+
update(data = {}) {
|
|
31
|
+
const { updateElement } = require('../board-state.js')
|
|
32
|
+
updateElement(element.id, { data: { ...element.data, ...data } })
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: 'web-search-query',
|
|
5
|
+
name: 'Web Search Query',
|
|
6
|
+
description: 'A web search query to send to an agent or run directly.',
|
|
7
|
+
icon: 'search',
|
|
8
|
+
defaultWidth: 220,
|
|
9
|
+
defaultHeight: 80,
|
|
10
|
+
expansionMode: 'inline',
|
|
11
|
+
tags: ['research', 'web'],
|
|
12
|
+
isBuiltIn: true,
|
|
13
|
+
hiddenFromPalette: true,
|
|
14
|
+
|
|
15
|
+
createInstance(context) {
|
|
16
|
+
const { element } = context
|
|
17
|
+
return {
|
|
18
|
+
getViewData() {
|
|
19
|
+
return {
|
|
20
|
+
query: element.data.query || '',
|
|
21
|
+
engine: element.data.engine || null,
|
|
22
|
+
filters: element.data.filters || null,
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
getPorts() {
|
|
26
|
+
return [{ key: 'query-out', direction: 'output', dataType: 'text', label: 'Query Out' }]
|
|
27
|
+
},
|
|
28
|
+
actions: {
|
|
29
|
+
sendToAgent({ agentName } = {}) {
|
|
30
|
+
if (!agentName) throw new Error('agentName required')
|
|
31
|
+
const { createElement } = require('../board-state.js')
|
|
32
|
+
createElement({
|
|
33
|
+
type: 'task',
|
|
34
|
+
x: element.x + 240,
|
|
35
|
+
y: element.y,
|
|
36
|
+
data: { agentName, prompt: element.data.query },
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
update(data = {}) {
|
|
40
|
+
const { updateElement } = require('../board-state.js')
|
|
41
|
+
updateElement(element.id, { data: { ...element.data, ...data } })
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: 'web-snapshot',
|
|
5
|
+
name: 'Web Snapshot',
|
|
6
|
+
description: 'A captured snapshot of a web page.',
|
|
7
|
+
icon: 'globe',
|
|
8
|
+
defaultWidth: 240,
|
|
9
|
+
defaultHeight: 100,
|
|
10
|
+
expansionMode: 'popup',
|
|
11
|
+
tags: ['data', 'web', 'research'],
|
|
12
|
+
isBuiltIn: true,
|
|
13
|
+
hiddenFromPalette: true,
|
|
14
|
+
|
|
15
|
+
createInstance(context) {
|
|
16
|
+
const { element } = context
|
|
17
|
+
return {
|
|
18
|
+
getViewData() {
|
|
19
|
+
return {
|
|
20
|
+
url: element.data.url || '',
|
|
21
|
+
title: element.data.title || '',
|
|
22
|
+
content: element.data.content || '',
|
|
23
|
+
capturedAt: element.data.capturedAt || null,
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
getPorts() {
|
|
27
|
+
return [{ key: 'text-out', direction: 'output', dataType: 'text', label: 'Content Out' }]
|
|
28
|
+
},
|
|
29
|
+
actions: {
|
|
30
|
+
update(data = {}) {
|
|
31
|
+
const { updateElement } = require('../board-state.js')
|
|
32
|
+
updateElement(element.id, { data: { ...element.data, ...data } })
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const { PROJECT_DIR } = require('./config.js')
|
|
6
|
+
|
|
7
|
+
const EXCLUDED_NAMES = new Set(['node_modules', '.git', '.nuxt', '.output', '.cache'])
|
|
8
|
+
const MAX_DEPTH = 6
|
|
9
|
+
|
|
10
|
+
function _safePath(relativePath) {
|
|
11
|
+
const resolved = path.resolve(PROJECT_DIR, relativePath)
|
|
12
|
+
if (!resolved.startsWith(PROJECT_DIR)) {
|
|
13
|
+
const err = new Error(`Access denied: path outside PROJECT_DIR`)
|
|
14
|
+
err.status = 403
|
|
15
|
+
throw err
|
|
16
|
+
}
|
|
17
|
+
return resolved
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildFileTree(dir = PROJECT_DIR, depth = 0) {
|
|
21
|
+
if (depth >= MAX_DEPTH) return []
|
|
22
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
23
|
+
const result = []
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (EXCLUDED_NAMES.has(entry.name)) continue
|
|
26
|
+
const fullPath = path.join(dir, entry.name)
|
|
27
|
+
const relPath = path.relative(PROJECT_DIR, fullPath)
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
result.push({
|
|
30
|
+
name: entry.name,
|
|
31
|
+
path: relPath,
|
|
32
|
+
type: 'directory',
|
|
33
|
+
children: buildFileTree(fullPath, depth + 1),
|
|
34
|
+
})
|
|
35
|
+
} else {
|
|
36
|
+
const stat = fs.statSync(fullPath)
|
|
37
|
+
result.push({
|
|
38
|
+
name: entry.name,
|
|
39
|
+
path: relPath,
|
|
40
|
+
type: 'file',
|
|
41
|
+
size: stat.size,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readProjectFile(relativePath) {
|
|
49
|
+
const abs = _safePath(relativePath)
|
|
50
|
+
return fs.readFileSync(abs, 'utf8')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeProjectFile(relativePath, content) {
|
|
54
|
+
const abs = _safePath(relativePath)
|
|
55
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true })
|
|
56
|
+
fs.writeFileSync(abs, content, 'utf8')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createProjectDir(relativePath) {
|
|
60
|
+
const abs = _safePath(relativePath)
|
|
61
|
+
fs.mkdirSync(abs, { recursive: true })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renameProjectFile(fromRel, toRel) {
|
|
65
|
+
const from = _safePath(fromRel)
|
|
66
|
+
const to = _safePath(toRel)
|
|
67
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
68
|
+
fs.renameSync(from, to)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deleteProjectPath(relativePath) {
|
|
72
|
+
const abs = _safePath(relativePath)
|
|
73
|
+
const stat = fs.statSync(abs)
|
|
74
|
+
if (stat.isDirectory()) {
|
|
75
|
+
fs.rmSync(abs, { recursive: true, force: true })
|
|
76
|
+
} else {
|
|
77
|
+
fs.unlinkSync(abs)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
buildFileTree,
|
|
83
|
+
readProjectFile,
|
|
84
|
+
writeProjectFile,
|
|
85
|
+
createProjectDir,
|
|
86
|
+
renameProjectFile,
|
|
87
|
+
deleteProjectPath,
|
|
88
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { veilGet } = require('./veil-client.js')
|
|
4
|
+
const { getIO } = require('./socket-io.js')
|
|
5
|
+
|
|
6
|
+
// Map<sessionId, number> — message count baseline (messages already seen by Studio)
|
|
7
|
+
// undefined = not initialized → first syncSession will set baseline without emitting
|
|
8
|
+
const _sessionState = new Map()
|
|
9
|
+
|
|
10
|
+
// Prevent concurrent syncs for the same session
|
|
11
|
+
const _syncing = new Set()
|
|
12
|
+
|
|
13
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
async function syncSession(sessionId) {
|
|
16
|
+
if (_syncing.has(sessionId)) return
|
|
17
|
+
_syncing.add(sessionId)
|
|
18
|
+
try {
|
|
19
|
+
await _doSync(sessionId)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.warn(`[session-sync] syncSession ${sessionId} failed: ${err.message}`)
|
|
22
|
+
} finally {
|
|
23
|
+
_syncing.delete(sessionId)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function touchSession(sessionId) {
|
|
28
|
+
// Refresh baseline without emitting — call after chat:send completes
|
|
29
|
+
// so the Studio-sent messages are not re-emitted on the next external sync
|
|
30
|
+
try {
|
|
31
|
+
const res = await veilGet(`/sessions/${encodeURIComponent(sessionId)}/messages`)
|
|
32
|
+
const messages = Array.isArray(res) ? res : (res.messages ?? [])
|
|
33
|
+
_sessionState.set(sessionId, messages.length)
|
|
34
|
+
} catch { /* best-effort, ignore errors */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function initSessionStates() {
|
|
38
|
+
// Pre-populate message-count baselines for all active sessions so that
|
|
39
|
+
// the first syncSession call only emits truly NEW messages
|
|
40
|
+
try {
|
|
41
|
+
const res = await veilGet('/sessions')
|
|
42
|
+
const sessions = Array.isArray(res) ? res : (res.sessions ?? [])
|
|
43
|
+
const active = sessions.filter(s => s.status !== 'closed' && s.status !== 'deleted')
|
|
44
|
+
|
|
45
|
+
await Promise.all(active.map(async (s) => {
|
|
46
|
+
const id = s.id || s.sessionId
|
|
47
|
+
if (!id || _sessionState.has(id)) return
|
|
48
|
+
try {
|
|
49
|
+
const msgRes = await veilGet(`/sessions/${encodeURIComponent(id)}/messages`)
|
|
50
|
+
const msgs = Array.isArray(msgRes) ? msgRes : (msgRes.messages ?? [])
|
|
51
|
+
_sessionState.set(id, msgs.length)
|
|
52
|
+
} catch {
|
|
53
|
+
_sessionState.set(id, 0)
|
|
54
|
+
}
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
if (active.length > 0)
|
|
58
|
+
console.log(`[session-sync] Baselines initialized for ${active.length} session(s)`)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.warn(`[session-sync] initSessionStates failed: ${err.message}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
async function _doSync(sessionId) {
|
|
67
|
+
const res = await veilGet(`/sessions/${encodeURIComponent(sessionId)}/messages`)
|
|
68
|
+
const messages = Array.isArray(res) ? res : (res.messages ?? [])
|
|
69
|
+
|
|
70
|
+
const baseline = _sessionState.get(sessionId)
|
|
71
|
+
|
|
72
|
+
if (baseline === undefined) {
|
|
73
|
+
// First contact with this session — set baseline without emitting historical messages
|
|
74
|
+
_sessionState.set(sessionId, messages.length)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const newMessages = messages.slice(baseline)
|
|
79
|
+
_sessionState.set(sessionId, messages.length)
|
|
80
|
+
|
|
81
|
+
if (newMessages.length === 0) return
|
|
82
|
+
|
|
83
|
+
const io = getIO()
|
|
84
|
+
|
|
85
|
+
for (const msg of newMessages) {
|
|
86
|
+
if (msg.role === 'user') {
|
|
87
|
+
const content = typeof msg.content === 'string'
|
|
88
|
+
? msg.content
|
|
89
|
+
: (msg.content?.[0]?.text ?? '')
|
|
90
|
+
io.to('board').emit('chat:user-message', {
|
|
91
|
+
sessionId,
|
|
92
|
+
agentName: msg.agentName || '',
|
|
93
|
+
message: content,
|
|
94
|
+
})
|
|
95
|
+
} else if (msg.role === 'assistant') {
|
|
96
|
+
io.to('board').emit('chat:stream', {
|
|
97
|
+
sessionId,
|
|
98
|
+
eventType: 'message',
|
|
99
|
+
data: msg,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Signal end of streaming after the batch
|
|
105
|
+
io.to('board').emit('chat:done', { sessionId, message: null, tokenUsage: null })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { syncSession, touchSession, initSessionStates }
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { VEIL_API, VEIL_SECRET } = require('./config.js')
|
|
4
|
+
|
|
5
|
+
function _headers(extra = {}) {
|
|
6
|
+
const h = { 'Content-Type': 'application/json', ...extra }
|
|
7
|
+
if (VEIL_SECRET) h['X-Veil-Secret'] = VEIL_SECRET
|
|
8
|
+
return h
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function _checkResponse(res) {
|
|
12
|
+
if (res.ok) return res
|
|
13
|
+
let msg = `VeilCLI error ${res.status}`
|
|
14
|
+
try {
|
|
15
|
+
const body = await res.json()
|
|
16
|
+
if (body && body.error) {
|
|
17
|
+
msg = body.error.message || body.error.code || msg
|
|
18
|
+
}
|
|
19
|
+
} catch (_) {}
|
|
20
|
+
throw new Error(msg)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function veilGet(apiPath, query = {}) {
|
|
24
|
+
const url = new URL(`${VEIL_API}${apiPath}`)
|
|
25
|
+
for (const [k, v] of Object.entries(query)) {
|
|
26
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, v)
|
|
27
|
+
}
|
|
28
|
+
const res = await fetch(url.toString(), { headers: _headers() })
|
|
29
|
+
await _checkResponse(res)
|
|
30
|
+
return res.json()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function veilPost(apiPath, body = {}) {
|
|
34
|
+
const res = await fetch(`${VEIL_API}${apiPath}`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: _headers(),
|
|
37
|
+
body: JSON.stringify(body),
|
|
38
|
+
})
|
|
39
|
+
await _checkResponse(res)
|
|
40
|
+
return res.json()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function veilPut(apiPath, body = {}) {
|
|
44
|
+
const res = await fetch(`${VEIL_API}${apiPath}`, {
|
|
45
|
+
method: 'PUT',
|
|
46
|
+
headers: _headers(),
|
|
47
|
+
body: JSON.stringify(body),
|
|
48
|
+
})
|
|
49
|
+
await _checkResponse(res)
|
|
50
|
+
return res.json()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function veilDelete(apiPath, query = {}) {
|
|
54
|
+
const url = new URL(`${VEIL_API}${apiPath}`)
|
|
55
|
+
for (const [k, v] of Object.entries(query)) {
|
|
56
|
+
if (v !== undefined && v !== null) url.searchParams.set(k, v)
|
|
57
|
+
}
|
|
58
|
+
const res = await fetch(url.toString(), {
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
headers: _headers(),
|
|
61
|
+
})
|
|
62
|
+
await _checkResponse(res)
|
|
63
|
+
return res.json()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function* veilStreamChat(agentName, message, sessionId) {
|
|
67
|
+
const body = { message, sse: true }
|
|
68
|
+
if (sessionId) body.sessionId = sessionId
|
|
69
|
+
|
|
70
|
+
const res = await fetch(`${VEIL_API}/agents/${encodeURIComponent(agentName)}/chat`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: _headers(),
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
})
|
|
75
|
+
await _checkResponse(res)
|
|
76
|
+
|
|
77
|
+
const reader = res.body.getReader()
|
|
78
|
+
const decoder = new TextDecoder()
|
|
79
|
+
let buffer = ''
|
|
80
|
+
|
|
81
|
+
while (true) {
|
|
82
|
+
const { done, value } = await reader.read()
|
|
83
|
+
if (done) break
|
|
84
|
+
buffer += decoder.decode(value, { stream: true })
|
|
85
|
+
|
|
86
|
+
const parts = buffer.split('\n\n')
|
|
87
|
+
buffer = parts.pop()
|
|
88
|
+
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
const lines = part.trim().split('\n')
|
|
91
|
+
let eventType = null
|
|
92
|
+
let dataLine = null
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.startsWith('event:')) eventType = line.slice(6).trim()
|
|
95
|
+
if (line.startsWith('data:')) dataLine = line.slice(5).trim()
|
|
96
|
+
}
|
|
97
|
+
if (!dataLine) continue
|
|
98
|
+
try {
|
|
99
|
+
const data = JSON.parse(dataLine)
|
|
100
|
+
yield { eventType: eventType || 'message', data }
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function* veilStreamSessionEvents(sessionId, signal) {
|
|
107
|
+
const res = await fetch(`${VEIL_API}/sessions/${encodeURIComponent(sessionId)}/events`, {
|
|
108
|
+
headers: _headers({ Accept: 'text/event-stream' }),
|
|
109
|
+
signal,
|
|
110
|
+
})
|
|
111
|
+
await _checkResponse(res)
|
|
112
|
+
|
|
113
|
+
const reader = res.body.getReader()
|
|
114
|
+
const decoder = new TextDecoder()
|
|
115
|
+
let buffer = ''
|
|
116
|
+
|
|
117
|
+
while (true) {
|
|
118
|
+
const { done, value } = await reader.read()
|
|
119
|
+
if (done) break
|
|
120
|
+
buffer += decoder.decode(value, { stream: true })
|
|
121
|
+
|
|
122
|
+
const parts = buffer.split('\n\n')
|
|
123
|
+
buffer = parts.pop()
|
|
124
|
+
|
|
125
|
+
for (const part of parts) {
|
|
126
|
+
const lines = part.trim().split('\n')
|
|
127
|
+
let eventType = null
|
|
128
|
+
let dataLine = null
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
if (line.startsWith('event:')) eventType = line.slice(6).trim()
|
|
131
|
+
if (line.startsWith('data:')) dataLine = line.slice(5).trim()
|
|
132
|
+
}
|
|
133
|
+
if (!dataLine) continue
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(dataLine)
|
|
136
|
+
yield { eventType: eventType || 'message', data }
|
|
137
|
+
} catch (_) {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function* veilStreamTaskEvents(taskId) {
|
|
143
|
+
const res = await fetch(`${VEIL_API}/tasks/${encodeURIComponent(taskId)}/events`, {
|
|
144
|
+
headers: _headers({ Accept: 'text/event-stream' }),
|
|
145
|
+
})
|
|
146
|
+
await _checkResponse(res)
|
|
147
|
+
|
|
148
|
+
const reader = res.body.getReader()
|
|
149
|
+
const decoder = new TextDecoder()
|
|
150
|
+
let buffer = ''
|
|
151
|
+
|
|
152
|
+
while (true) {
|
|
153
|
+
const { done, value } = await reader.read()
|
|
154
|
+
if (done) break
|
|
155
|
+
buffer += decoder.decode(value, { stream: true })
|
|
156
|
+
|
|
157
|
+
const parts = buffer.split('\n\n')
|
|
158
|
+
buffer = parts.pop()
|
|
159
|
+
|
|
160
|
+
for (const part of parts) {
|
|
161
|
+
const lines = part.trim().split('\n')
|
|
162
|
+
let eventType = null
|
|
163
|
+
let dataLine = null
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
if (line.startsWith('event:')) eventType = line.slice(6).trim()
|
|
166
|
+
if (line.startsWith('data:')) dataLine = line.slice(5).trim()
|
|
167
|
+
}
|
|
168
|
+
if (!dataLine) continue
|
|
169
|
+
try {
|
|
170
|
+
const data = JSON.parse(dataLine)
|
|
171
|
+
yield { eventType: eventType || 'task.event', data }
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
veilGet,
|
|
179
|
+
veilPost,
|
|
180
|
+
veilPut,
|
|
181
|
+
veilDelete,
|
|
182
|
+
veilStreamChat,
|
|
183
|
+
veilStreamSessionEvents,
|
|
184
|
+
veilStreamTaskEvents,
|
|
185
|
+
}
|