yaml-flow 6.0.0 → 7.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 (162) hide show
  1. package/board-live-cards-cli.js +4 -4
  2. package/browser/asset-integrity.json +3 -3
  3. package/browser/board-livecards-client.js +2 -0
  4. package/browser/board-livecards-client.js.map +1 -0
  5. package/browser/board-livecards-localstorage.js +10 -0
  6. package/browser/board-livecards-localstorage.js.map +1 -0
  7. package/browser/board-livegraph-engine.js +2 -2
  8. package/browser/board-livegraph-engine.js.map +1 -1
  9. package/browser/card-compute.js +28 -28
  10. package/browser/compute-jsonata.js +5 -0
  11. package/browser/compute-jsonata.js.map +1 -0
  12. package/browser/live-cards.js +261 -150
  13. package/card-store.js +4 -4
  14. package/dist/{board-live-cards-public-CltXYgaY.d.cts → board-live-cards-public-CW5074xr.d.cts} +9 -5
  15. package/dist/{board-live-cards-public-f-E-FAyp.d.ts → board-live-cards-public-hnZo0mAf.d.ts} +9 -5
  16. package/dist/board-livegraph-runtime/index.cjs +2 -2
  17. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  18. package/dist/board-livegraph-runtime/index.d.cts +11 -9
  19. package/dist/board-livegraph-runtime/index.d.ts +11 -9
  20. package/dist/board-livegraph-runtime/index.js +2 -2
  21. package/dist/board-livegraph-runtime/index.js.map +1 -1
  22. package/dist/board-livegraph-runtime/jsonata-sync.cjs +37 -1
  23. package/dist/card-compute/index.cjs +4 -4
  24. package/dist/card-compute/index.cjs.map +1 -1
  25. package/dist/card-compute/index.d.cts +5 -1
  26. package/dist/card-compute/index.d.ts +5 -1
  27. package/dist/card-compute/index.js +4 -4
  28. package/dist/card-compute/index.js.map +1 -1
  29. package/dist/card-compute/jsonata-sync.cjs +37 -1
  30. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -1
  31. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -1
  32. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +27 -14
  33. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +27 -14
  34. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -1
  35. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -1
  36. package/dist/cli/browser-api/card-store-browser-api.cjs +1 -1
  37. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -1
  38. package/dist/cli/browser-api/card-store-browser-api.js +1 -1
  39. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -1
  40. package/dist/cli/browser-api/jsonata-sync.cjs +37 -1
  41. package/dist/cli/node/artifacts-store-cli.cjs +8 -8
  42. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -1
  43. package/dist/cli/node/artifacts-store-cli.js +8 -8
  44. package/dist/cli/node/artifacts-store-cli.js.map +1 -1
  45. package/dist/cli/node/board-live-cards-cli.cjs +7 -7
  46. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -1
  47. package/dist/cli/node/board-live-cards-cli.js +7 -7
  48. package/dist/cli/node/board-live-cards-cli.js.map +1 -1
  49. package/dist/cli/node/card-store-cli.cjs +5 -5
  50. package/dist/cli/node/card-store-cli.cjs.map +1 -1
  51. package/dist/cli/node/card-store-cli.js +5 -5
  52. package/dist/cli/node/card-store-cli.js.map +1 -1
  53. package/dist/cli/node/execution-adapter.cjs +3 -0
  54. package/dist/cli/node/execution-adapter.cjs.map +1 -0
  55. package/dist/cli/node/execution-adapter.d.cts +174 -0
  56. package/dist/cli/node/execution-adapter.d.ts +174 -0
  57. package/dist/cli/node/execution-adapter.js +3 -0
  58. package/dist/cli/node/execution-adapter.js.map +1 -0
  59. package/dist/cli/node/fs-board-adapter.cjs +7 -7
  60. package/dist/cli/node/fs-board-adapter.cjs.map +1 -1
  61. package/dist/cli/node/fs-board-adapter.d.cts +2 -2
  62. package/dist/cli/node/fs-board-adapter.d.ts +2 -2
  63. package/dist/cli/node/fs-board-adapter.js +7 -7
  64. package/dist/cli/node/fs-board-adapter.js.map +1 -1
  65. package/dist/cli/node/jsonata-sync.cjs +37 -1
  66. package/dist/cli/node/source-cli-task-executor.cjs +4 -4
  67. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -1
  68. package/dist/cli/node/source-cli-task-executor.js +4 -4
  69. package/dist/cli/node/source-cli-task-executor.js.map +1 -1
  70. package/dist/continuous-event-graph/index.cjs +2 -2
  71. package/dist/continuous-event-graph/index.cjs.map +1 -1
  72. package/dist/continuous-event-graph/index.js +2 -2
  73. package/dist/continuous-event-graph/index.js.map +1 -1
  74. package/dist/continuous-event-graph/jsonata-sync.cjs +37 -1
  75. package/dist/execution-refs.cjs +2 -1
  76. package/dist/execution-refs.cjs.map +1 -1
  77. package/dist/execution-refs.d.cts +49 -11
  78. package/dist/execution-refs.d.ts +49 -11
  79. package/dist/execution-refs.js +2 -1
  80. package/dist/execution-refs.js.map +1 -1
  81. package/dist/index.cjs +10 -10
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.js +10 -10
  84. package/dist/index.js.map +1 -1
  85. package/dist/jsonata-sync.cjs +37 -1
  86. package/dist/server-runtime/index.cjs +9 -0
  87. package/dist/server-runtime/index.cjs.map +1 -0
  88. package/dist/server-runtime/index.d.cts +31 -0
  89. package/dist/server-runtime/index.d.ts +31 -0
  90. package/dist/server-runtime/index.js +9 -0
  91. package/dist/server-runtime/index.js.map +1 -0
  92. package/dist/server-runtime/jsonata-sync.cjs +7623 -0
  93. package/dist/step-machine-public/index.cjs +2 -0
  94. package/dist/step-machine-public/index.cjs.map +1 -0
  95. package/dist/step-machine-public/index.d.cts +159 -0
  96. package/dist/step-machine-public/index.d.ts +159 -0
  97. package/dist/step-machine-public/index.js +2 -0
  98. package/dist/step-machine-public/index.js.map +1 -0
  99. package/dist/step-machine-public/jsonata-sync.cjs +7623 -0
  100. package/dist/storage-refs.cjs +2 -2
  101. package/dist/storage-refs.cjs.map +1 -1
  102. package/dist/storage-refs.d.cts +7 -6
  103. package/dist/storage-refs.d.ts +7 -6
  104. package/dist/storage-refs.js +2 -2
  105. package/dist/storage-refs.js.map +1 -1
  106. package/dist/types-B1ZRa4aI.d.ts +147 -0
  107. package/dist/types-BxEFcVK9.d.cts +147 -0
  108. package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +9 -10
  109. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +357 -0
  110. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +9 -10
  111. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +300 -0
  112. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +617 -0
  113. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-sse-worker.js +48 -0
  114. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +11 -10
  115. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +19 -4
  116. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +4 -8
  117. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -10
  118. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +8 -16
  119. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +2 -6
  120. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +4 -8
  121. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +3 -7
  122. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +4 -8
  123. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +7 -16
  124. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +2 -6
  125. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +13 -3
  126. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +2 -1
  127. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +2 -1
  128. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +2 -1
  129. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +20 -24
  130. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +0 -3
  131. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +8 -13
  132. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +33 -9
  133. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +3 -1
  134. package/examples/cli/step-machine-demo/step2-double-cli.js +6 -12
  135. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +66 -4
  136. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +13 -5
  137. package/examples/example-board/cards/_index.json +47 -0
  138. package/examples/example-board/cards/card-market-prices.json +33 -9
  139. package/examples/example-board/cards/card-my-identity.json +30 -6
  140. package/examples/example-board/cards/card-portfolio-action.json +24 -6
  141. package/examples/example-board/cards/card-portfolio-intelligence.json +97 -0
  142. package/examples/example-board/cards/card-portfolio-risks.json +24 -6
  143. package/examples/example-board/cards/card-portfolio-value.json +38 -10
  144. package/examples/example-board/cards/card-portfolio.json +57 -13
  145. package/examples/example-board/cards/card-rebalance-impact.json +22 -6
  146. package/examples/example-board/cards/card-rebalance-sim.json +66 -15
  147. package/examples/example-board/demo-server.js +360 -69
  148. package/examples/example-board/demo-shell-localstorage.html +774 -0
  149. package/examples/example-board/demo-shell-with-server.html +18 -36
  150. package/examples/example-board/demo-shell.html +5 -4
  151. package/examples/example-board/demo-task-executor.js +217 -265
  152. package/package.json +15 -13
  153. package/step-machine-cli.js +43 -310
  154. package/board-livecards-server-runtime.js +0 -1513
  155. package/browser/board-livecards-runtime-client.js +0 -263
  156. package/dist/pycli/quickjs-board-runtime.global.js +0 -9
  157. package/dist/pycli/quickjs-board-runtime.global.js.map +0 -1
  158. package/dist/pycli/quickjs-step-machine-runtime.global.js +0 -5
  159. package/dist/pycli/quickjs-step-machine-runtime.global.js.map +0 -1
  160. package/examples/cli/step-machine-demo/two-step-math-handlers.js +0 -32
  161. package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +0 -24
  162. package/examples/example-board/demo-shell-browser.html +0 -675
@@ -1,1513 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- import { fileURLToPath } from 'node:url';
5
- import { createRequire } from 'node:module';
6
- import net from 'node:net';
7
- import { execFileSync, spawn } from 'node:child_process';
8
- import {
9
- createBoardLiveCardsPublic,
10
- createFsBoardPlatformAdapter,
11
- createCardStorePublic,
12
- createCardStore,
13
- createArtifactsStore,
14
- createChatArtifactsStore,
15
- createFileArtifactsStore,
16
- createCardFileMetadataStore,
17
- parseRef,
18
- } from './dist/cli/node/fs-board-adapter.js';
19
-
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = path.dirname(__filename);
22
- const require = createRequire(import.meta.url);
23
-
24
- const DEFAULT_CORS_HEADERS = {
25
- 'Access-Control-Allow-Origin': '*',
26
- 'Access-Control-Allow-Headers': 'content-type,x-file-name',
27
- 'Access-Control-Allow-Methods': 'GET,POST,PATCH,OPTIONS',
28
- };
29
-
30
- const MAX_STORED_FILE_NAME_LEN = 32;
31
-
32
- // Routes handled by the reusable runtime (demo-setup is excluded, handled by host)
33
- export const RUNTIME_ROUTE_PATTERNS = [
34
- /\/init-board$/,
35
- /\/bootstrap-cards$/,
36
- /\/bootstrap$/,
37
- /\/sse$/,
38
- /\/board-status$/,
39
- /\/cards\/[^/]+$/,
40
- /\/cards\/[^/]+\/actions$/,
41
- /\/cards\/[^/]+\/chats$/,
42
- /\/cards\/[^/]+\/files$/,
43
- ];
44
-
45
- export function isRuntimeRoute(pathname) {
46
- return RUNTIME_ROUTE_PATTERNS.some((pattern) => pattern.test(pathname));
47
- }
48
-
49
- function parseUrl(urlString) {
50
- return new URL(urlString, 'http://localhost');
51
- }
52
-
53
- /**
54
- * Merges `extraFields` into the `extra` object inside a `.task-executor` JSON file.
55
- * No-op if the file doesn't exist or isn't valid JSON.
56
- */
57
- function refreshTaskExecutorExtra(runtimeDir, extraFields) {
58
- const taskExecutorFile = path.join(runtimeDir, '.task-executor');
59
- if (!fs.existsSync(taskExecutorFile)) return;
60
- try {
61
- const current = JSON.parse(fs.readFileSync(taskExecutorFile, 'utf-8'));
62
- const merged = { ...current, extra: { ...(current.extra || {}), ...extraFields } };
63
- fs.writeFileSync(taskExecutorFile, JSON.stringify(merged, null, 2), 'utf-8');
64
- } catch {
65
- // Silently ignore — board will still function, extra is best-effort
66
- }
67
- }
68
-
69
- export function createRuntimeRequestDispatcher(runtime) {
70
- if (!runtime || typeof runtime !== 'object') {
71
- throw new Error('runtime is required');
72
- }
73
-
74
- return async function dispatch(req, res, parsedUrl) {
75
- const method = req.method || 'GET';
76
- const url = parsedUrl || runtime.parseUrl(req.url || '/');
77
-
78
- if (method === 'OPTIONS') {
79
- res.writeHead(204, runtime.corsHeaders);
80
- res.end();
81
- return true;
82
- }
83
-
84
- // Multi-board runtime exposes handleApi; single-board exposes handleRuntimeApi.
85
- if (typeof runtime.handleApi === 'function') {
86
- if (await runtime.handleApi(req, res, url)) return true;
87
- } else {
88
- if (await runtime.handleRuntimeApi(req, res, url)) return true;
89
- }
90
-
91
- runtime.json(res, 404, { error: 'Not found' });
92
- return true;
93
- };
94
- }
95
-
96
- /**
97
- * createMultiBoardServerRuntime
98
- *
99
- * Manages multiple boards under a single DEMO_SETUP_DIR.
100
- * Directory layout:
101
- * setupDir/
102
- * board-default/ ← built-in example board
103
- * runtime/ ← board-graph.json, cards-inventory.jsonl
104
- * surface/ ← tmp-cards/
105
- * runtime-out/ ← computed artefacts
106
- * board-<id>/ ← any additional board
107
- * ...same layout...
108
- *
109
- * Routes:
110
- * GET /api/boards list registered boards
111
- * POST /api/boards {id, label?} register a new board
112
- * GET /api/boards/:boardId/demo-setup (host-handled; runtime exposes performDemoSetup)
113
- * GET /api/boards/:boardId/bootstrap
114
- * GET /api/boards/:boardId/sse
115
- * ... (all single-board routes, prefixed with /:boardId/)
116
- */
117
- export function createMultiBoardServerRuntime(options = {}) {
118
- const setupDir = path.resolve(
119
- options.setupDir ||
120
- process.env.DEMO_SETUP_DIR ||
121
- path.join(os.tmpdir(), 'board-live-cards-demo-setup')
122
- );
123
- const apiBasePath = String(options.apiBasePath || '/api/boards').replace(/\/$/, '');
124
- const corsHeaders = { ...DEFAULT_CORS_HEADERS, ...(options.corsHeaders || {}) };
125
-
126
- // Source card templates shared by all boards unless overridden per-board in config.
127
- const defaultCardsDir = path.resolve(
128
- options.defaultCardsDir || path.join(__dirname, 'cards')
129
- );
130
- const configuredServerMetaStoreRef = typeof options.serverMetaStoreRef === 'string'
131
- && options.serverMetaStoreRef.trim()
132
- ? options.serverMetaStoreRef.trim()
133
- : null;
134
- const serverMetaStoreRef = configuredServerMetaStoreRef || `::fs-path::${setupDir}`;
135
- const serverMetaArtifacts = createArtifactsStore(
136
- createFsBoardPlatformAdapter(parseRef(serverMetaStoreRef), __dirname, { suppressSpawn: true })
137
- .blobStorage('server-meta')
138
- );
139
- const boardsRegistryKey = 'boards-config.json';
140
- const boardServiceCache = new Map();
141
-
142
- fs.mkdirSync(setupDir, { recursive: true });
143
-
144
- function readBoardsConfig() {
145
- const raw = serverMetaArtifacts.getText(boardsRegistryKey);
146
- if (!raw) {
147
- return { boards: [{ id: 'default', label: 'Default Board' }] };
148
- }
149
- try {
150
- return JSON.parse(raw);
151
- } catch {
152
- return { boards: [{ id: 'default', label: 'Default Board' }] };
153
- }
154
- }
155
-
156
- function writeBoardsConfig(config) {
157
- serverMetaArtifacts.putText(boardsRegistryKey, JSON.stringify(config, null, 2));
158
- }
159
-
160
- function safeBoardId(raw) {
161
- const sanitized = String(raw || '')
162
- .replace(/[^a-zA-Z0-9_-]/g, '_')
163
- .replace(/^_+|_+$/g, '');
164
- return sanitized.length > 0 && sanitized.length <= 64 ? sanitized : null;
165
- }
166
-
167
- function getBoardService(boardId) {
168
- if (boardServiceCache.has(boardId)) return boardServiceCache.get(boardId);
169
-
170
- const boardRoot = path.join(setupDir, `board-${boardId}`);
171
- const config = readBoardsConfig();
172
- const entry = config.boards.find((b) => b.id === boardId) || {};
173
- const cardsDir = typeof entry.cardsDir === 'string' ? path.resolve(entry.cardsDir) : defaultCardsDir;
174
- const defaultTaskExecutorPath = typeof entry.taskExecutorPath === 'string'
175
- ? entry.taskExecutorPath
176
- : options.defaultTaskExecutorPath;
177
- const defaultStepMachineCliPath = typeof entry.stepMachineCliPath === 'string'
178
- ? entry.stepMachineCliPath
179
- : options.defaultStepMachineCliPath;
180
- const defaultChatHandlerPath = typeof entry.chatHandlerPath === 'string'
181
- ? entry.chatHandlerPath
182
- : options.defaultChatHandlerPath;
183
- const defaultInferenceAdapterPath = typeof entry.inferenceAdapterPath === 'string'
184
- ? entry.inferenceAdapterPath
185
- : options.defaultInferenceAdapterPath;
186
- const gandalfCardsDir = typeof entry.gandalfCardsDir === 'string'
187
- ? entry.gandalfCardsDir
188
- : (options.defaultGandalfCardsDir || null);
189
- const gandalfTaskExecutorPath = typeof entry.gandalfTaskExecutorPath === 'string'
190
- ? entry.gandalfTaskExecutorPath
191
- : (options.defaultGandalfTaskExecutorPath || null);
192
- const gandalfChatHandlerPath = typeof entry.gandalfChatHandlerPath === 'string'
193
- ? entry.gandalfChatHandlerPath
194
- : (options.defaultGandalfChatHandlerPath || null);
195
- const gandalfInferenceAdapterPath = typeof entry.gandalfInferenceAdapterPath === 'string'
196
- ? entry.gandalfInferenceAdapterPath
197
- : (options.defaultGandalfInferenceAdapterPath || null);
198
-
199
- const service = createExampleBoardServerRuntime({
200
- apiBasePath: `${apiBasePath}/${boardId}`,
201
- corsHeaders,
202
- boardId,
203
- boardDir: path.join(boardRoot, 'runtime'),
204
- cardsDir,
205
- tmpSurfaceDir: path.join(boardRoot, 'surface'),
206
- runtimeOutDir: path.join(boardRoot, 'runtime-out'),
207
- defaultTaskExecutorPath,
208
- defaultStepMachineCliPath,
209
- defaultChatHandlerPath,
210
- defaultInferenceAdapterPath,
211
- gandalfCardsDir,
212
- gandalfRuntimeDir: path.join(boardRoot, 'gandalf-runtime'),
213
- gandalfRuntimeOutDir: path.join(boardRoot, 'gandalf-runtime-out'),
214
- gandalfTaskExecutorPath,
215
- gandalfChatHandlerPath,
216
- gandalfInferenceAdapterPath,
217
- boardLiveCardsCliJs: options.boardLiveCardsCliJs,
218
- serverUrl: options.serverUrl || null,
219
- });
220
-
221
- boardServiceCache.set(boardId, service);
222
- return service;
223
- }
224
-
225
- function json(res, status, payload) {
226
- const body = JSON.stringify(payload);
227
- res.writeHead(status, {
228
- ...corsHeaders,
229
- 'Content-Type': 'application/json; charset=utf-8',
230
- 'Content-Length': Buffer.byteLength(body),
231
- });
232
- res.end(body);
233
- }
234
-
235
- async function handleBoardsRegistryApi(req, res, parsedUrl) {
236
- const method = req.method || 'GET';
237
- const p = parsedUrl.pathname;
238
-
239
- // GET /api/boards — list boards
240
- if (method === 'GET' && p === apiBasePath) {
241
- json(res, 200, { ok: true, boards: readBoardsConfig().boards });
242
- return true;
243
- }
244
-
245
- // POST /api/boards {id, label?} — register new board
246
- if (method === 'POST' && p === apiBasePath) {
247
- const chunks = [];
248
- for await (const c of req) chunks.push(c);
249
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
250
- let body = {};
251
- try { body = raw ? JSON.parse(raw) : {}; } catch { body = {}; }
252
-
253
- const id = safeBoardId(body.id);
254
- if (!id) {
255
- json(res, 400, { error: 'board id must be 1-64 alphanumeric/dash/underscore characters' });
256
- return true;
257
- }
258
-
259
- const config = readBoardsConfig();
260
- if (config.boards.some((b) => b.id === id)) {
261
- json(res, 409, { error: `Board "${id}" is already registered` });
262
- return true;
263
- }
264
-
265
- const label = typeof body.label === 'string' && body.label.trim() ? body.label.trim() : id;
266
- const entry = { id, label };
267
- if (typeof body.cardsDir === 'string') entry.cardsDir = body.cardsDir;
268
- if (typeof body.stepMachineCliPath === 'string') entry.stepMachineCliPath = body.stepMachineCliPath;
269
- if (typeof body.taskExecutorPath === 'string') entry.taskExecutorPath = body.taskExecutorPath;
270
- if (typeof body.chatHandlerPath === 'string') entry.chatHandlerPath = body.chatHandlerPath;
271
- if (typeof body.inferenceAdapterPath === 'string') entry.inferenceAdapterPath = body.inferenceAdapterPath;
272
- config.boards.push(entry);
273
- writeBoardsConfig(config);
274
-
275
- json(res, 200, { ok: true, board: entry });
276
- return true;
277
- }
278
-
279
- return false;
280
- }
281
-
282
- async function handleBoardApi(req, res, parsedUrl) {
283
- const p = parsedUrl.pathname;
284
-
285
- // Extract boardId from /:boardId/... or /:boardId (exact)
286
- const boardSegMatch = p.match(new RegExp(`^${apiBasePath}/([^/]+)(/|$)`));
287
- if (!boardSegMatch) return false;
288
-
289
- const boardId = safeBoardId(decodeURIComponent(boardSegMatch[1]));
290
- if (!boardId) {
291
- json(res, 400, { error: 'Invalid board id' });
292
- return true;
293
- }
294
-
295
- const config = readBoardsConfig();
296
- if (!config.boards.some((b) => b.id === boardId)) {
297
- json(res, 404, {
298
- error: `Board "${boardId}" not registered. POST ${apiBasePath} with {id} to register it first.`,
299
- });
300
- return true;
301
- }
302
-
303
- const service = getBoardService(boardId);
304
- if (await service.handleRuntimeApi(req, res, parsedUrl)) return true;
305
- return false;
306
- }
307
-
308
- async function handleApi(req, res, parsedUrl) {
309
- if (await handleBoardsRegistryApi(req, res, parsedUrl)) return true;
310
- if (await handleBoardApi(req, res, parsedUrl)) return true;
311
- return false;
312
- }
313
-
314
- // Exposed so host layers (e.g. demo-server) can reach a board's service and root path.
315
- // Throws a 404 error if the board is not registered.
316
- function requireBoardService(boardId) {
317
- const config = readBoardsConfig();
318
- if (!config.boards.some((b) => b.id === boardId)) {
319
- const err = new Error(`Board "${boardId}" not registered`);
320
- err.statusCode = 404;
321
- throw err;
322
- }
323
- const boardRoot = path.join(setupDir, `board-${boardId}`);
324
- return { service: getBoardService(boardId), boardRoot };
325
- }
326
-
327
- return {
328
- apiBasePath,
329
- corsHeaders,
330
- setupDir,
331
- serverMetaStoreRef,
332
- boardsRegistryKey,
333
- parseUrl,
334
- json,
335
- handleBoardsRegistryApi,
336
- handleBoardApi,
337
- handleApi,
338
- requireBoardService,
339
- };
340
- }
341
-
342
- export function createNodeHttpRuntimeHandler(runtime) {
343
- const dispatch = createRuntimeRequestDispatcher(runtime);
344
- return function nodeHttpHandler(req, res) {
345
- void dispatch(req, res);
346
- };
347
- }
348
-
349
- export function createExampleBoardServerRuntime(options = {}) {
350
- const apiBasePath = String(options.apiBasePath || '/api/example-board/server').replace(/\/$/, '');
351
- const corsHeaders = { ...DEFAULT_CORS_HEADERS, ...(options.corsHeaders || {}) };
352
- const boardId = typeof options.boardId === 'string' && options.boardId ? options.boardId : '';
353
-
354
- const boardDir = path.resolve(
355
- options.boardDir || process.env.DEMO_BOARD_RUNTIME_DIR || path.join(os.tmpdir(), 'board-live-cards-demo-board')
356
- );
357
- const cardsDir = path.resolve(options.cardsDir || path.join(__dirname, 'cards'));
358
- const tmpSurfaceDir = path.resolve(
359
- options.tmpSurfaceDir || process.env.DEMO_SURFACE_DIR || path.join(os.tmpdir(), 'board-live-cards-demo-surface')
360
- );
361
- const tmpCardsDir = cardsDir;
362
- const runtimeOutDir = path.resolve(
363
- options.runtimeOutDir || process.env.DEMO_RUNTIME_OUT_DIR || path.join(os.tmpdir(), 'board-live-cards-demo-runtime-out')
364
- );
365
- const configuredTaskExecutorPath = typeof options.defaultTaskExecutorPath === 'string'
366
- && options.defaultTaskExecutorPath.trim()
367
- ? (path.isAbsolute(options.defaultTaskExecutorPath)
368
- ? options.defaultTaskExecutorPath
369
- : path.resolve(process.cwd(), options.defaultTaskExecutorPath))
370
- : null;
371
- const configuredStepMachineCliPath = typeof options.defaultStepMachineCliPath === 'string'
372
- && options.defaultStepMachineCliPath.trim()
373
- ? (path.isAbsolute(options.defaultStepMachineCliPath)
374
- ? options.defaultStepMachineCliPath
375
- : path.resolve(process.cwd(), options.defaultStepMachineCliPath))
376
- : null;
377
- const configuredBoardLiveCardsCliJs = typeof options.boardLiveCardsCliJs === 'string'
378
- && options.boardLiveCardsCliJs.trim()
379
- ? (path.isAbsolute(options.boardLiveCardsCliJs)
380
- ? options.boardLiveCardsCliJs
381
- : path.resolve(process.cwd(), options.boardLiveCardsCliJs))
382
- : null;
383
- const configuredChatHandlerPath = typeof options.defaultChatHandlerPath === 'string'
384
- && options.defaultChatHandlerPath.trim()
385
- ? (path.isAbsolute(options.defaultChatHandlerPath)
386
- ? options.defaultChatHandlerPath
387
- : path.resolve(process.cwd(), options.defaultChatHandlerPath))
388
- : null;
389
- const configuredInferenceAdapterPath = typeof options.defaultInferenceAdapterPath === 'string'
390
- && options.defaultInferenceAdapterPath.trim()
391
- ? (path.isAbsolute(options.defaultInferenceAdapterPath)
392
- ? options.defaultInferenceAdapterPath
393
- : path.resolve(process.cwd(), options.defaultInferenceAdapterPath))
394
- : null;
395
-
396
- // Board-cards: parallel runtime dirs for the board-manager board.
397
- const gandalfCardsDir = options.gandalfCardsDir ? path.resolve(options.gandalfCardsDir) : null;
398
- const gandalfRuntimeDir = path.resolve(options.gandalfRuntimeDir || path.join(path.dirname(boardDir), 'gandalf-runtime'));
399
- const gandalfRuntimeOutDir = path.resolve(options.gandalfRuntimeOutDir || path.join(path.dirname(boardDir), 'gandalf-runtime-out'));
400
- const tmpGandalfCardsDir = gandalfCardsDir;
401
-
402
- // Explicit gandalf-card executor paths — no fallback to regular-card paths.
403
- const configuredGandalfTaskExecutorPath = typeof options.gandalfTaskExecutorPath === 'string' && options.gandalfTaskExecutorPath.trim()
404
- ? (path.isAbsolute(options.gandalfTaskExecutorPath) ? options.gandalfTaskExecutorPath : path.resolve(process.cwd(), options.gandalfTaskExecutorPath))
405
- : null;
406
- const configuredGandalfChatHandlerPath = typeof options.gandalfChatHandlerPath === 'string' && options.gandalfChatHandlerPath.trim()
407
- ? (path.isAbsolute(options.gandalfChatHandlerPath) ? options.gandalfChatHandlerPath : path.resolve(process.cwd(), options.gandalfChatHandlerPath))
408
- : null;
409
- const configuredGandalfInferenceAdapterPath = typeof options.gandalfInferenceAdapterPath === 'string' && options.gandalfInferenceAdapterPath.trim()
410
- ? (path.isAbsolute(options.gandalfInferenceAdapterPath) ? options.gandalfInferenceAdapterPath : path.resolve(process.cwd(), options.gandalfInferenceAdapterPath))
411
- : null;
412
-
413
- // Server URL passed down from the hosting server (e.g. demo-server) so executors/handlers
414
- // can call back to server-side proxy endpoints (e.g. /api/workiq/ask).
415
- const serverUrl = typeof options.serverUrl === 'string' && options.serverUrl.trim()
416
- ? options.serverUrl.trim().replace(/\/$/, '')
417
- : null;
418
-
419
- const sseClients = new Set();
420
- const cardPathById = new Map();
421
- const gandalfCardPathById = new Map();
422
-
423
- function isGandalfCard(cardId) { return gandalfCardPathById.has(cardId); }
424
-
425
- function namedPipePath(pipeName) {
426
- if (process.platform === 'win32') return `\\\\.\\pipe\\${pipeName}`;
427
- return path.join(os.tmpdir(), `${pipeName}.sock`);
428
- }
429
-
430
- function makeNotificationState() {
431
- return {
432
- status: null,
433
- computedValues: {},
434
- dataObjects: {},
435
- cards: {},
436
- sockets: new Set(),
437
- };
438
- }
439
-
440
- function appendNotification(state, event) {
441
- if (!event || typeof event !== 'object') return;
442
- if (event.kind === 'status') state.status = event.status;
443
- if (event.kind === 'computed_values' && event.cardId) state.computedValues[event.cardId] = event.values;
444
- if (event.kind === 'data_object' && event.key) state.dataObjects[event.key] = event.payload;
445
- if (event.kind === 'card_refreshed' && event.cardId) state.cards[event.cardId] = event.card;
446
- }
447
-
448
- function makeBoardContext(label, runtimeDir, outputsDir, cardsRootDir, taskExecutorPath, chatHandlerPath, inferenceAdapterPath) {
449
- const notifyChannel = `yaml-flow-server-${label}-${boardId || 'default'}-${process.pid}`;
450
- const baseRefStr = `::fs-path::${runtimeDir}`;
451
- const cardStoreRef = `::fs-path::${path.join(cardsRootDir, 'cards')}`;
452
- const outputsStoreRef = `::fs-path::${path.join(outputsDir, '.outputs')}`;
453
- const baseRef = parseRef(baseRefStr);
454
- const adapter = createFsBoardPlatformAdapter(baseRef, __dirname, {
455
- onWarn: (msg) => console.warn(`[server-runtime:${label}] ${msg}`),
456
- suppressSpawn: true,
457
- notifyChannel,
458
- });
459
- const board = createBoardLiveCardsPublic(baseRef, adapter);
460
- const kv = adapter.kvStorageForRef(cardStoreRef);
461
- const cardAdapterObj = {
462
- readIndex: () => kv.read('_index'),
463
- writeIndex: (idx) => kv.write('_index', idx),
464
- readCard: (id) => kv.read(id),
465
- writeCard: (id, card) => { kv.write(id, card); return id; },
466
- cardExists: (id) => kv.read(id) !== null,
467
- defaultCardKey: (id) => id,
468
- };
469
- const cardStore = createCardStorePublic(createCardStore(cardAdapterObj, console.warn));
470
- const artifactsRef = parseRef(`::fs-path::${cardsRootDir}`);
471
- const artifactsAdapter = createFsBoardPlatformAdapter(artifactsRef, __dirname, { suppressSpawn: true });
472
- const filesArtifacts = createArtifactsStore(artifactsAdapter.blobStorage('files'));
473
- const chatsArtifacts = createArtifactsStore(artifactsAdapter.blobStorage('chats'));
474
- return {
475
- label,
476
- runtimeDir,
477
- outputsDir,
478
- cardsRootDir,
479
- notifyChannel,
480
- board,
481
- cardStore,
482
- filesArtifacts,
483
- chatsArtifacts,
484
- cardStoreRef,
485
- outputsStoreRef,
486
- taskExecutorPath,
487
- chatHandlerPath,
488
- inferenceAdapterPath,
489
- notification: makeNotificationState(),
490
- pipeServer: null,
491
- initialized: false,
492
- cardsBootstrapped: false,
493
- };
494
- }
495
-
496
- const baseCtx = makeBoardContext(
497
- 'base',
498
- boardDir,
499
- runtimeOutDir,
500
- tmpCardsDir,
501
- configuredTaskExecutorPath,
502
- configuredChatHandlerPath,
503
- configuredInferenceAdapterPath,
504
- );
505
-
506
- const gandalfCtx = configuredGandalfTaskExecutorPath && tmpGandalfCardsDir
507
- ? makeBoardContext(
508
- 'gandalf',
509
- gandalfRuntimeDir,
510
- gandalfRuntimeOutDir,
511
- tmpGandalfCardsDir,
512
- configuredGandalfTaskExecutorPath,
513
- configuredGandalfChatHandlerPath,
514
- configuredGandalfInferenceAdapterPath,
515
- )
516
- : null;
517
-
518
- function cardFilesFromDir(dirPath, outMap) {
519
- outMap.clear();
520
- if (!dirPath || !fs.existsSync(dirPath)) return [];
521
- const out = [];
522
- for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
523
- if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.json')) continue;
524
- const full = path.join(dirPath, entry.name);
525
- try {
526
- const card = JSON.parse(fs.readFileSync(full, 'utf-8'));
527
- if (!card || typeof card.id !== 'string') continue;
528
- outMap.set(card.id, full);
529
- out.push(card);
530
- } catch {
531
- // ignore malformed files
532
- }
533
- }
534
- return out;
535
- }
536
-
537
- function toExecutionRef(scriptPath, extraObj) {
538
- if (!scriptPath) return null;
539
- return {
540
- howToRun: 'local-node',
541
- whatToRun: `::fs-path::${scriptPath}`,
542
- extra: extraObj,
543
- };
544
- }
545
-
546
- async function ensurePipeConsumer(ctx) {
547
- if (!ctx || ctx.pipeServer) return;
548
- const pipePath = namedPipePath(ctx.notifyChannel);
549
- if (process.platform !== 'win32' && fs.existsSync(pipePath)) {
550
- try { fs.rmSync(pipePath, { force: true }); } catch { /* best-effort */ }
551
- }
552
- const server = net.createServer((socket) => {
553
- ctx.notification.sockets.add(socket);
554
- socket.on('close', () => ctx.notification.sockets.delete(socket));
555
- let buf = '';
556
- socket.on('data', (chunk) => {
557
- buf += chunk.toString('utf-8');
558
- while (true) {
559
- const i = buf.indexOf('\n');
560
- if (i < 0) break;
561
- const line = buf.slice(0, i).trim();
562
- buf = buf.slice(i + 1);
563
- if (!line) continue;
564
- try {
565
- const msg = JSON.parse(line);
566
- const n = msg?.notification ?? msg;
567
- appendNotification(ctx.notification, n);
568
- } catch {
569
- // ignore malformed lines
570
- }
571
- }
572
- broadcastToSseClients();
573
- });
574
- });
575
- await new Promise((resolve, reject) => {
576
- server.once('error', reject);
577
- server.listen(pipePath, () => resolve());
578
- });
579
- ctx.pipeServer = { server, pipePath };
580
- }
581
-
582
- function ensureCardStorageDirs(cardId) {
583
- const safeCardId = String(cardId || '').replace(/[^a-zA-Z0-9_-]/g, '_') || 'unknown-card';
584
- const baseDir = isGandalfCard(cardId) ? tmpGandalfCardsDir : tmpCardsDir;
585
- const filesDir = path.join(baseDir, 'files', safeCardId);
586
- const chatsDir = path.join(baseDir, 'chats', safeCardId);
587
- fs.mkdirSync(filesDir, { recursive: true });
588
- fs.mkdirSync(chatsDir, { recursive: true });
589
- return { filesDir, chatsDir, safeCardId };
590
- }
591
-
592
- function artifactsStores(cardId) {
593
- const ctx = isGandalfCard(cardId) ? gandalfCtx : baseCtx;
594
- return {
595
- files: ctx ? ctx.filesArtifacts : null,
596
- chats: ctx ? ctx.chatsArtifacts : null,
597
- };
598
- }
599
-
600
- function chatArtifactsForCard(cardId) {
601
- const stores = artifactsStores(cardId);
602
- if (!stores.chats) return null;
603
- return createChatArtifactsStore(stores.chats, { indexFileName: '.index.json' });
604
- }
605
-
606
- function fileArtifactsForCard(cardId) {
607
- const stores = artifactsStores(cardId);
608
- if (!stores.files) return null;
609
- return createFileArtifactsStore(stores.files);
610
- }
611
-
612
- function cardFileMetadataStore() {
613
- return createCardFileMetadataStore();
614
- }
615
-
616
- function parseLeadingSerial(fileName) {
617
- const m = String(fileName || '').match(/^(\d+)[-_]/);
618
- return m ? parseInt(m[1], 10) : 0;
619
- }
620
-
621
- function normalizeDisplayFileName(name) {
622
- const input = String(name || '').trim();
623
- if (!input) return 'upload.bin';
624
- const base = path.basename(input);
625
- return base || 'upload.bin';
626
- }
627
-
628
- function shellQuote(s) {
629
- return '"' + String(s).replace(/"/g, '\\"') + '"';
630
- }
631
-
632
- function runCli(_args) {
633
- throw new Error('CLI path is no longer used by server runtime. Use board public APIs.');
634
- }
635
-
636
- function clearDirContents(dirPath) {
637
- if (!fs.existsSync(dirPath)) return;
638
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
639
- for (const entry of entries) {
640
- const target = path.join(dirPath, entry.name);
641
- fs.rmSync(target, { recursive: true, force: true });
642
- }
643
- }
644
-
645
- function readJson(filePath) {
646
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
647
- }
648
-
649
- function readInventory() {
650
- return [...cardPathById.entries()].map(([cardId, cardFilePath]) => ({ cardId, cardFilePath }));
651
- }
652
-
653
- function readGandalfInventory() {
654
- return [...gandalfCardPathById.entries()].map(([cardId, cardFilePath]) => ({ cardId, cardFilePath }));
655
- }
656
-
657
- function readStatusSnapshot() {
658
- const base = baseCtx.notification.status;
659
- const side = gandalfCtx ? gandalfCtx.notification.status : null;
660
- if (!base && !side) return null;
661
- if (!side) return base;
662
- if (!base) return side;
663
-
664
- const baseCards = Array.isArray(base.cards) ? base.cards : [];
665
- const sideCards = Array.isArray(side.cards) ? side.cards : [];
666
- const mergedCards = [...baseCards, ...sideCards];
667
-
668
- const sum = (obj, k) => Number(obj?.summary?.[k] || 0);
669
- return {
670
- ...base,
671
- cards: mergedCards,
672
- summary: {
673
- ...(base.summary || {}),
674
- card_count: mergedCards.length,
675
- completed: sum(base, 'completed') + sum(side, 'completed'),
676
- eligible: sum(base, 'eligible') + sum(side, 'eligible'),
677
- pending: sum(base, 'pending') + sum(side, 'pending'),
678
- blocked: sum(base, 'blocked') + sum(side, 'blocked'),
679
- unresolved: sum(base, 'unresolved') + sum(side, 'unresolved'),
680
- failed: sum(base, 'failed') + sum(side, 'failed'),
681
- in_progress: sum(base, 'in_progress') + sum(side, 'in_progress'),
682
- orphan_cards: sum(base, 'orphan_cards') + sum(side, 'orphan_cards'),
683
- },
684
- };
685
- }
686
-
687
- function readCardDefinitions() {
688
- const fromCtx = (ctx, fallbackDir, fallbackMap) => {
689
- if (!ctx || !ctx.cardStore) return cardFilesFromDir(fallbackDir, fallbackMap);
690
- const result = ctx.cardStore.get({});
691
- if (result.status !== 'success' || !Array.isArray(result.data?.cards)) {
692
- return cardFilesFromDir(fallbackDir, fallbackMap);
693
- }
694
- return result.data.cards;
695
- };
696
-
697
- const base = fromCtx(baseCtx, tmpCardsDir, cardPathById);
698
- const side = gandalfCtx ? fromCtx(gandalfCtx, tmpGandalfCardsDir, gandalfCardPathById) : [];
699
- return [...base, ...side];
700
- }
701
-
702
- function readCardRuntimeArtifacts() {
703
- const out = {};
704
- for (const [cardId, values] of Object.entries(baseCtx.notification.computedValues)) {
705
- const card = baseCtx.notification.cards[cardId];
706
- out[cardId] = {
707
- schema_version: 'v1',
708
- card_id: cardId,
709
- card_data: card?.card_data ?? {},
710
- computed_values: values ?? {},
711
- fetched_sources: {},
712
- requires: {},
713
- };
714
- }
715
- if (gandalfCtx) {
716
- for (const [cardId, values] of Object.entries(gandalfCtx.notification.computedValues)) {
717
- const card = gandalfCtx.notification.cards[cardId];
718
- out[cardId] = {
719
- schema_version: 'v1',
720
- card_id: cardId,
721
- card_data: card?.card_data ?? {},
722
- computed_values: values ?? {},
723
- fetched_sources: {},
724
- requires: {},
725
- };
726
- }
727
- }
728
- return out;
729
- }
730
-
731
- function readSourcePayloads(cardDefinition) {
732
- const out = {};
733
- if (!cardDefinition || !Array.isArray(cardDefinition.source_defs)) return out;
734
-
735
- const ctx = isGandalfCard(cardDefinition.id) ? gandalfCtx : baseCtx;
736
- const dataObjects = ctx ? ctx.notification.dataObjects : {};
737
- for (const sourceDef of cardDefinition.source_defs) {
738
- if (!sourceDef || !sourceDef.bindTo) continue;
739
- if (Object.prototype.hasOwnProperty.call(dataObjects, sourceDef.bindTo)) {
740
- out[sourceDef.bindTo] = dataObjects[sourceDef.bindTo];
741
- }
742
- }
743
-
744
- return out;
745
- }
746
-
747
- function readDataObjectsByToken() {
748
- return {
749
- ...(baseCtx.notification.dataObjects || {}),
750
- ...(gandalfCtx ? gandalfCtx.notification.dataObjects : {}),
751
- };
752
- }
753
-
754
- function readChatSignal(cardId) {
755
- const { safeCardId } = ensureCardStorageDirs(cardId);
756
- const chatStore = chatArtifactsForCard(cardId);
757
- if (!chatStore) return { count: 0, latest_mtime_ms: 0, processing: false };
758
- return chatStore.readSignal(safeCardId);
759
- }
760
-
761
- function buildPublishedRuntimePayload() {
762
- const cardDefinitions = readCardDefinitions();
763
- const rawArtifacts = readCardRuntimeArtifacts();
764
- const dataObjectsByToken = readDataObjectsByToken();
765
- const cardRuntimeById = {};
766
-
767
- for (const cardDefinition of cardDefinitions) {
768
- if (!cardDefinition || !cardDefinition.id) continue;
769
- const rawArtifact = rawArtifacts[cardDefinition.id] || {};
770
- const sourcesFromFiles = readSourcePayloads(cardDefinition);
771
- const chatSignal = readChatSignal(cardDefinition.id);
772
- cardRuntimeById[cardDefinition.id] = {
773
- schema_version: rawArtifact.schema_version || 'v1',
774
- card_id: rawArtifact.card_id || cardDefinition.id,
775
- card_data:
776
- rawArtifact.card_data && typeof rawArtifact.card_data === 'object'
777
- ? rawArtifact.card_data
778
- : cardDefinition.card_data && typeof cardDefinition.card_data === 'object'
779
- ? cardDefinition.card_data
780
- : {},
781
- computed_values:
782
- rawArtifact.computed_values && typeof rawArtifact.computed_values === 'object'
783
- ? rawArtifact.computed_values
784
- : {},
785
- fetched_sources: sourcesFromFiles,
786
- requires:
787
- rawArtifact.requires && typeof rawArtifact.requires === 'object'
788
- ? rawArtifact.requires
789
- : {},
790
- };
791
-
792
- if (!cardRuntimeById[cardDefinition.id].card_data || typeof cardRuntimeById[cardDefinition.id].card_data !== 'object') {
793
- cardRuntimeById[cardDefinition.id].card_data = {};
794
- }
795
- cardRuntimeById[cardDefinition.id].card_data.__chat_signal = chatSignal;
796
- }
797
-
798
- return {
799
- cardDefinitions,
800
- statusSnapshot: readStatusSnapshot(),
801
- dataObjectsByToken,
802
- cardRuntimeById,
803
- };
804
- }
805
-
806
- function resolveTaskExecutorPath(taskExecutorPathParam) {
807
- const raw = typeof taskExecutorPathParam === 'string' ? taskExecutorPathParam.trim() : '';
808
- const resolved = raw
809
- ? (path.isAbsolute(raw) ? raw : path.resolve(__dirname, raw))
810
- : configuredTaskExecutorPath;
811
- if (!resolved) {
812
- const err = new Error('taskExecutorPath is required (query param or runtime defaultTaskExecutorPath option)');
813
- err.statusCode = 400;
814
- throw err;
815
- }
816
- if (!fs.existsSync(resolved)) {
817
- const err = new Error(`Task executor script not found: ${resolved}`);
818
- err.statusCode = 400;
819
- throw err;
820
- }
821
- return resolved;
822
- }
823
-
824
- function resolveChatHandlerPath(chatHandlerPathParam) {
825
- const raw = typeof chatHandlerPathParam === 'string' ? chatHandlerPathParam.trim() : '';
826
- const resolved = raw
827
- ? (path.isAbsolute(raw) ? raw : path.resolve(__dirname, raw))
828
- : configuredChatHandlerPath;
829
- if (!resolved) return null;
830
- if (!fs.existsSync(resolved)) {
831
- const err = new Error(`Chat handler script not found: ${resolved}`);
832
- err.statusCode = 400;
833
- throw err;
834
- }
835
- return resolved;
836
- }
837
-
838
- function resolveInferenceAdapterPath(inferenceAdapterPathParam) {
839
- const raw = typeof inferenceAdapterPathParam === 'string' ? inferenceAdapterPathParam.trim() : '';
840
- const resolved = raw
841
- ? (path.isAbsolute(raw) ? raw : path.resolve(__dirname, raw))
842
- : configuredInferenceAdapterPath;
843
- if (!resolved) return null;
844
- if (!fs.existsSync(resolved)) {
845
- const err = new Error(`Inference adapter script not found: ${resolved}`);
846
- err.statusCode = 400;
847
- throw err;
848
- }
849
- return resolved;
850
- }
851
-
852
- async function initContext(ctx, taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
853
- if (!ctx) return;
854
- if (ctx.initialized) return;
855
-
856
- const te = resolveTaskExecutorPath(taskExecutorPathParam || ctx.taskExecutorPath);
857
- const ch = resolveChatHandlerPath(chatHandlerPathParam || ctx.chatHandlerPath);
858
- const ia = resolveInferenceAdapterPath(inferenceAdapterPathParam || ctx.inferenceAdapterPath);
859
- const boardSetupRoot = path.dirname(boardDir);
860
- const extra = {
861
- boardSetupRoot,
862
- boardId,
863
- boardRuntimeDir: path.relative(boardSetupRoot, ctx.runtimeDir),
864
- runtimeStatusDir: path.relative(boardSetupRoot, ctx.outputsDir),
865
- cardsDir: path.relative(boardSetupRoot, ctx.cardsRootDir),
866
- ...(serverUrl ? { serverUrl } : {}),
867
- ...(configuredBoardLiveCardsCliJs ? { boardLiveCardsCliJs: configuredBoardLiveCardsCliJs } : {}),
868
- ...(configuredStepMachineCliPath ? { stepMachineCliPath: configuredStepMachineCliPath } : {}),
869
- };
870
-
871
- const params = {
872
- cardStoreRef: ctx.cardStoreRef,
873
- outputsStoreRef: ctx.outputsStoreRef,
874
- };
875
- const body = {};
876
- body['task-executor-ref'] = toExecutionRef(te, extra);
877
- if (ch) body['chat-handler-ref'] = toExecutionRef(ch, extra);
878
- if (ia) body['inference-adapter-ref'] = toExecutionRef(ia, extra);
879
-
880
- const initResult = ctx.board.init({ params, body });
881
- if (initResult.status !== 'success') {
882
- const err = new Error(initResult.error || `init failed for ${ctx.label}`);
883
- err.statusCode = 500;
884
- throw err;
885
- }
886
- await ensurePipeConsumer(ctx);
887
- ctx.initialized = true;
888
- }
889
-
890
- async function upsertCardsFromDir(ctx, outMap) {
891
- if (!ctx) return;
892
- if (ctx.cardsBootstrapped) return;
893
- const cards = cardFilesFromDir(ctx.cardsRootDir, outMap);
894
- for (const card of cards) {
895
- const setResult = ctx.cardStore.set({ body: card });
896
- if (setResult.status !== 'success') continue;
897
- ctx.board.upsertCard({ params: { cardId: card.id, restart: true } });
898
- }
899
- await ctx.board.processAccumulatedEvents({});
900
- ctx.cardsBootstrapped = true;
901
- }
902
-
903
- async function initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
904
- await initContext(baseCtx, taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam);
905
- if (gandalfCtx && gandalfCtx.taskExecutorPath) {
906
- await initContext(gandalfCtx, gandalfCtx.taskExecutorPath, gandalfCtx.chatHandlerPath, gandalfCtx.inferenceAdapterPath);
907
- }
908
- }
909
-
910
- async function bootstrapBoard() {
911
- await initBoardAndSetup();
912
- await upsertCardsFromDir(baseCtx, cardPathById);
913
- if (gandalfCtx) await upsertCardsFromDir(gandalfCtx, gandalfCardPathById);
914
- }
915
-
916
- function cardContextForCard(cardId) {
917
- return isGandalfCard(cardId) ? gandalfCtx : baseCtx;
918
- }
919
-
920
- function readCardFromStore(cardId) {
921
- const ctx = cardContextForCard(cardId);
922
- if (!ctx) return null;
923
- const result = ctx.cardStore.get({ params: { id: cardId } });
924
- if (result.status !== 'success') return null;
925
- const cards = Array.isArray(result.data?.cards) ? result.data.cards : [];
926
- return cards.length > 0 ? cards[0] : null;
927
- }
928
-
929
- function mutateCard(cardId, updateFn, opts) {
930
- const options = opts && typeof opts === 'object' ? opts : {};
931
- const syncBoard = options.syncBoard !== false;
932
- const ctx = cardContextForCard(cardId);
933
- if (!ctx) {
934
- const err = new Error(`Card not found: ${cardId}`);
935
- err.statusCode = 404;
936
- throw err;
937
- }
938
-
939
- const card = readCardFromStore(cardId);
940
- if (!card || typeof card !== 'object') {
941
- const err = new Error(`Card not found: ${cardId}`);
942
- err.statusCode = 404;
943
- throw err;
944
- }
945
-
946
- const nextCard = updateFn(card) || card;
947
- const setResult = ctx.cardStore.set({ body: nextCard });
948
- if (setResult.status !== 'success') {
949
- const err = new Error(setResult.error || `Failed to persist card: ${cardId}`);
950
- err.statusCode = 500;
951
- throw err;
952
- }
953
-
954
- if (syncBoard) {
955
- const upsertResult = ctx.board.upsertCard({ params: { cardId, restart: true } });
956
- if (upsertResult.status !== 'success') {
957
- const err = new Error(upsertResult.error || `Failed to upsert card: ${cardId}`);
958
- err.statusCode = 500;
959
- throw err;
960
- }
961
- }
962
- }
963
-
964
- function updateCard(cardId, updateFn) {
965
- mutateCard(cardId, updateFn, { syncBoard: true });
966
- }
967
-
968
- function updateCardLocalOnly(cardId, updateFn) {
969
- mutateCard(cardId, updateFn, { syncBoard: false });
970
- }
971
-
972
- function patchCard(cardId, patch) {
973
- updateCard(cardId, (card) => {
974
- if (!patch || typeof patch !== 'object' || Object.keys(patch).length === 0) {
975
- return card;
976
- }
977
-
978
- function deepSet(obj, dottedPath, value) {
979
- const parts = String(dottedPath || '').split('.').filter(Boolean);
980
- if (!parts.length) return;
981
- let target = obj;
982
- for (let i = 0; i < parts.length - 1; i++) {
983
- const key = parts[i];
984
- if (!target[key] || typeof target[key] !== 'object') target[key] = {};
985
- target = target[key];
986
- }
987
- target[parts[parts.length - 1]] = value;
988
- }
989
-
990
- if (patch.fieldValues && typeof patch.fieldValues === 'object') {
991
- let writeTo = null;
992
- if (card.view && Array.isArray(card.view.elements)) {
993
- for (const elem of card.view.elements) {
994
- if (elem && elem.data && elem.data.writeTo) {
995
- writeTo = elem.data.writeTo;
996
- break;
997
- }
998
- }
999
- }
1000
- if (writeTo) {
1001
- deepSet(card, writeTo, patch.fieldValues);
1002
- } else {
1003
- card.card_data = { ...(card.card_data || {}), ...patch.fieldValues };
1004
- }
1005
- } else if (Array.isArray(patch._stagedFiles) && patch._stagedFiles.length > 0) {
1006
- return card;
1007
- } else {
1008
- for (const [key, value] of Object.entries(patch)) {
1009
- if (key === '_stagedFiles') continue;
1010
- if (
1011
- value !== null &&
1012
- typeof value === 'object' &&
1013
- !Array.isArray(value) &&
1014
- card[key] !== null &&
1015
- typeof card[key] === 'object' &&
1016
- !Array.isArray(card[key])
1017
- ) {
1018
- card[key] = { ...card[key], ...value };
1019
- } else {
1020
- card[key] = value;
1021
- }
1022
- }
1023
- }
1024
-
1025
- return card;
1026
- });
1027
- }
1028
-
1029
- function clearChatRecords(cardId) {
1030
- const { safeCardId } = ensureCardStorageDirs(cardId);
1031
- const chatStore = chatArtifactsForCard(cardId);
1032
- if (!chatStore) return;
1033
- chatStore.clear(safeCardId);
1034
- }
1035
-
1036
- function readCardStoredFileNames(cardId) {
1037
- const names = [];
1038
- try {
1039
- const card = readCardFromStore(cardId);
1040
- if (!card) return names;
1041
- const metadata = cardFileMetadataStore().read(card && card.card_data ? card.card_data : null);
1042
- for (const entry of metadata) names.push(entry.stored_name);
1043
- } catch {
1044
- // ignore malformed card file
1045
- }
1046
- return names;
1047
- }
1048
-
1049
- function nextChatStoredName(cardId, role) {
1050
- const { safeCardId } = ensureCardStorageDirs(cardId);
1051
- const chatStore = chatArtifactsForCard(cardId);
1052
- const serial = chatStore ? chatStore.nextSerial(safeCardId) : 1;
1053
- const safeRole = String(role || 'system').toLowerCase().replace(/[^a-z0-9_-]/g, '_') || 'system';
1054
- return `${String(serial).padStart(3, '0')}_${safeRole}.txt`;
1055
- }
1056
-
1057
- function writeChatRecord(cardId, role, text, files) {
1058
- const now = new Date().toISOString();
1059
- const { safeCardId } = ensureCardStorageDirs(cardId);
1060
- const stores = artifactsStores(cardId);
1061
- const outName = nextChatStoredName(cardId, role || 'system');
1062
- const artifactKey = `${safeCardId}/${outName}`;
1063
-
1064
- const lines = [];
1065
- const msg = typeof text === 'string' ? text.trim() : '';
1066
- if (msg) lines.push(msg);
1067
-
1068
- const fileList = Array.isArray(files) ? files : [];
1069
- if (fileList.length) {
1070
- if (lines.length) lines.push('');
1071
- lines.push('files:');
1072
- for (const file of fileList) {
1073
- if (!file || typeof file !== 'object') continue;
1074
- const display = typeof file.name === 'string' ? file.name : 'file';
1075
- const stored = typeof file.stored_name === 'string' ? file.stored_name : '';
1076
- lines.push(stored ? `- ${display} -> ${stored}` : `- ${display}`);
1077
- }
1078
- }
1079
-
1080
- if (stores.chats) stores.chats.putText(artifactKey, `${lines.join('\n')}\n`);
1081
- const serial = parseLeadingSerial(outName);
1082
- const chatStore = chatArtifactsForCard(cardId);
1083
- if (chatStore) {
1084
- chatStore.appendIndexRecord(safeCardId, {
1085
- serial,
1086
- role: role || 'system',
1087
- stored_name: outName,
1088
- path: `${cardId}/chats/${outName}`,
1089
- updated_at: now,
1090
- });
1091
- }
1092
- return {
1093
- at: now,
1094
- role: role || 'system',
1095
- text: msg,
1096
- files: fileList,
1097
- path: `${cardId}/chats/${outName}`,
1098
- };
1099
- }
1100
-
1101
- function readChatRecords(cardId) {
1102
- const { safeCardId } = ensureCardStorageDirs(cardId);
1103
- const chatStore = chatArtifactsForCard(cardId);
1104
- if (!chatStore) return [];
1105
- return chatStore.readRecords(safeCardId).map((row) => ({
1106
- ...row,
1107
- path: `${cardId}/chats/${row.stored_name}`,
1108
- }));
1109
- }
1110
-
1111
- function persistUploadedFile(cardId, requestedName, contentType, buffer) {
1112
- const { safeCardId } = ensureCardStorageDirs(cardId);
1113
- const stores = artifactsStores(cardId);
1114
- const displayName = normalizeDisplayFileName(requestedName);
1115
- const fileStore = fileArtifactsForCard(cardId);
1116
- const storedName = fileStore
1117
- ? fileStore.allocateStoredName(safeCardId, displayName, {
1118
- seedNames: readCardStoredFileNames(cardId),
1119
- maxLen: MAX_STORED_FILE_NAME_LEN,
1120
- })
1121
- : `${String(Date.now())}-${displayName}`;
1122
-
1123
- if (stores.files) {
1124
- stores.files.putBytes(`${safeCardId}/${storedName}`, new Uint8Array(buffer), contentType || 'application/octet-stream');
1125
- }
1126
-
1127
- return {
1128
- name: displayName,
1129
- stored_name: storedName,
1130
- size: buffer.length,
1131
- mime_type: contentType || 'application/octet-stream',
1132
- path: `${cardId}/files/${storedName}`,
1133
- uploaded_at: new Date().toISOString(),
1134
- };
1135
- }
1136
-
1137
- // Fire-and-forget invocation of .chat-handler after a user chat message is persisted.
1138
- // The handler file lives in the appropriate runtime dir (.chat-handler).
1139
- // Called with: --boardId <id> --cardId <id> --extraEncJson <base64json>
1140
- // extraEncJson decodes to:
1141
- // boardSetupRoot — absolute path to board root (parent of runtime/, surface/, runtime-out/)
1142
- // boardRuntimeDir — relative: 'runtime' (or 'gandalf-runtime' for gandalf cards)
1143
- // runtimeStatusDir — relative: 'runtime-out'
1144
- // cardsDir — relative: 'surface/tmp-cards' (or 'surface/tmp-gandalf-cards')
1145
- // chatDir — relative (from cardsDir): e.g. 'card-portfolio/chats'
1146
- // chatProcessingMarkerKey — relative marker key in chats artifacts store, e.g. 'card-portfolio/.processing'
1147
- // lastChatFile — filename of the just-written user message, e.g. '001_user.txt'
1148
- // boardLiveCardsCliJs — absolute path to board-live-cards-cli.js (if configured)
1149
- // stepMachineCliPath — absolute path to step-machine-cli.js (if configured)
1150
- // Handler failures are logged and silently ignored — chat-send response is never affected.
1151
- function invokeChatHandler(cardId, chatsDir, lastChatFile) {
1152
- const isGandalf = isGandalfCard(cardId);
1153
- const runtimeDir = isGandalf ? gandalfRuntimeDir : boardDir;
1154
- const handlerFile = path.join(runtimeDir, '.chat-handler');
1155
- if (!fs.existsSync(handlerFile)) return;
1156
- const handlerCmd = fs.readFileSync(handlerFile, 'utf-8').trim();
1157
- if (!handlerCmd) return;
1158
- const boardSetupRoot = path.dirname(boardDir);
1159
- const { safeCardId } = ensureCardStorageDirs(cardId);
1160
- const stores = artifactsStores(cardId);
1161
- const processingMarkerKey = `${safeCardId}/.processing`;
1162
- const processingFile = path.join(chatsDir, '.processing');
1163
- try {
1164
- if (stores.chats) {
1165
- stores.chats.putText(processingMarkerKey, '', 'text/plain; charset=utf-8');
1166
- } else {
1167
- fs.mkdirSync(chatsDir, { recursive: true });
1168
- fs.writeFileSync(processingFile, '', 'utf-8');
1169
- }
1170
- } catch {}
1171
- const extra = Buffer.from(JSON.stringify({
1172
- boardSetupRoot,
1173
- boardRuntimeDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeDir : boardDir),
1174
- runtimeStatusDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeOutDir : runtimeOutDir),
1175
- cardsDir: path.relative(boardSetupRoot, isGandalf ? tmpGandalfCardsDir : tmpCardsDir),
1176
- chatDir: chatsDir,
1177
- chatProcessingMarkerKey: processingMarkerKey,
1178
- lastChatFile,
1179
- ...(serverUrl ? { serverUrl } : {}),
1180
- ...(configuredBoardLiveCardsCliJs ? { boardLiveCardsCliJs: configuredBoardLiveCardsCliJs } : {}),
1181
- ...(configuredStepMachineCliPath ? { stepMachineCliPath: configuredStepMachineCliPath } : {}),
1182
- })).toString('base64');
1183
- try {
1184
- const proc = spawn(handlerCmd, [
1185
- '--boardId', boardId, '--cardId', String(cardId),
1186
- '--extraEncJson', extra,
1187
- ], {
1188
- shell: true,
1189
- stdio: 'ignore',
1190
- });
1191
- proc.unref();
1192
- console.log(`[chat-handler] invoked for card "${cardId}" (boardId: "${boardId}")`);
1193
- } catch (err) {
1194
- try {
1195
- if (stores.chats) {
1196
- stores.chats.remove(processingMarkerKey);
1197
- } else {
1198
- fs.unlinkSync(processingFile);
1199
- }
1200
- } catch {}
1201
- console.warn(`[chat-handler] spawn failed for card "${cardId}":`, (err && err.message) || String(err));
1202
- }
1203
- }
1204
-
1205
- function applyCardAction(cardId, actionType, payload) {
1206
- const persistCard = actionType === 'chat-send' ? updateCardLocalOnly : updateCard;
1207
- let chatHandlerArgs = null;
1208
- persistCard(cardId, (card) => {
1209
- const now = new Date().toISOString();
1210
- const cardData = card.card_data && typeof card.card_data === 'object' ? card.card_data : {};
1211
- card.card_data = cardData;
1212
-
1213
- if (actionType === 'chat-send') {
1214
- const text = payload && typeof payload.text === 'string' ? payload.text.trim() : '';
1215
- const files = Array.isArray(payload && payload.files)
1216
- ? payload.files
1217
- .map((f) => {
1218
- if (!f) return null;
1219
- if (typeof f === 'string') return { name: f };
1220
- if (typeof f === 'object' && typeof f.name === 'string') {
1221
- return {
1222
- name: f.name,
1223
- size: f.size || null,
1224
- mime_type: f.mime_type || null,
1225
- path: f.path || null,
1226
- uploaded_at: f.uploaded_at || null,
1227
- stored_name: f.stored_name || null,
1228
- };
1229
- }
1230
- return null;
1231
- })
1232
- .filter(Boolean)
1233
- : [];
1234
-
1235
- if (text || files.length > 0) {
1236
- const { chatsDir } = ensureCardStorageDirs(cardId);
1237
- const userRecord = writeChatRecord(cardId, 'user', text, files);
1238
- chatHandlerArgs = { chatsDir, lastChatFile: path.basename(userRecord.path) };
1239
- for (const file of files) {
1240
- if (!file || typeof file !== 'object') continue;
1241
- const display = typeof file.name === 'string' ? file.name : 'file';
1242
- const stored = typeof file.stored_name === 'string' ? file.stored_name : null;
1243
- if (!stored) continue;
1244
- writeChatRecord(cardId, 'system', `File ${display} uploaded as ${stored}.`, []);
1245
- }
1246
- }
1247
-
1248
- return card;
1249
- }
1250
-
1251
- if (actionType === 'file-upload') {
1252
- const files = cardFileMetadataStore().normalizeIncoming(payload && payload.files, now);
1253
-
1254
- if (files.length > 0) {
1255
- cardFileMetadataStore().merge(cardData, files);
1256
- }
1257
-
1258
- return card;
1259
- }
1260
-
1261
- if (actionType === 'action') {
1262
- const buttonId = payload && typeof payload.buttonId === 'string' ? payload.buttonId : '';
1263
- if (!buttonId) return card;
1264
-
1265
- cardData.lastAction = { buttonId, at: now };
1266
- cardData.lastActionText = `${buttonId} @ ${now}`;
1267
- }
1268
-
1269
- return card;
1270
- });
1271
-
1272
- if (chatHandlerArgs) {
1273
- invokeChatHandler(cardId, chatHandlerArgs.chatsDir, chatHandlerArgs.lastChatFile);
1274
- }
1275
- }
1276
-
1277
- function json(res, status, payload) {
1278
- const body = JSON.stringify(payload);
1279
- res.writeHead(status, {
1280
- ...corsHeaders,
1281
- 'Content-Type': 'application/json; charset=utf-8',
1282
- 'Content-Length': Buffer.byteLength(body),
1283
- });
1284
- res.end(body);
1285
- }
1286
-
1287
- async function readJsonBody(req) {
1288
- const chunks = [];
1289
- for await (const c of req) chunks.push(c);
1290
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
1291
- if (!raw) return {};
1292
- return JSON.parse(raw);
1293
- }
1294
-
1295
- async function readRawBody(req) {
1296
- const chunks = [];
1297
- for await (const c of req) chunks.push(c);
1298
- return Buffer.concat(chunks);
1299
- }
1300
-
1301
- function broadcastToSseClients() {
1302
- const payload = buildPublishedRuntimePayload();
1303
- const data = `data: ${JSON.stringify(payload)}\n\n`;
1304
- for (const client of sseClients) {
1305
- try {
1306
- client.write(data);
1307
- } catch {
1308
- sseClients.delete(client);
1309
- }
1310
- }
1311
- }
1312
-
1313
- function handleSse(req, res) {
1314
- res.writeHead(200, {
1315
- ...corsHeaders,
1316
- 'Content-Type': 'text/event-stream',
1317
- 'Cache-Control': 'no-cache',
1318
- Connection: 'keep-alive',
1319
- });
1320
-
1321
- sseClients.add(res);
1322
- res.write(`data: ${JSON.stringify(buildPublishedRuntimePayload())}\n\n`);
1323
-
1324
- const keepAlive = setInterval(() => {
1325
- try { res.write(': keepalive\n\n'); } catch { /* ignore */ }
1326
- }, 15_000);
1327
-
1328
- req.on('close', () => {
1329
- clearInterval(keepAlive);
1330
- sseClients.delete(res);
1331
- res.end();
1332
- });
1333
- }
1334
-
1335
- async function handleDemoSetupApi(req, res, parsedUrl) {
1336
- return false; // Demo-setup is handled by the host layer.
1337
- }
1338
-
1339
- async function handleRuntimeApi(req, res, parsedUrl) {
1340
- const method = req.method || 'GET';
1341
- const url = parsedUrl || parseUrl(req.url || '/');
1342
- const p = url.pathname;
1343
-
1344
- try {
1345
- if (method === 'GET' && p === `${apiBasePath}/init-board`) {
1346
- const taskExecutorPathParam = url.searchParams.get('taskExecutorPath') || '';
1347
- const chatHandlerPathParam = url.searchParams.get('chatHandlerPath') || '';
1348
- await initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam);
1349
- json(res, 200, buildPublishedRuntimePayload());
1350
- return true;
1351
- }
1352
-
1353
- if (method === 'GET' && p === `${apiBasePath}/bootstrap-cards`) {
1354
- await bootstrapBoard();
1355
- json(res, 200, buildPublishedRuntimePayload());
1356
- return true;
1357
- }
1358
-
1359
- if (method === 'GET' && p === `${apiBasePath}/bootstrap`) {
1360
- await bootstrapBoard();
1361
- json(res, 200, buildPublishedRuntimePayload());
1362
- return true;
1363
- }
1364
-
1365
- if (method === 'GET' && p === `${apiBasePath}/sse`) {
1366
- await bootstrapBoard();
1367
- handleSse(req, res);
1368
- return true;
1369
- }
1370
-
1371
- if (method === 'GET' && p === `${apiBasePath}/board-status`) {
1372
- json(res, 200, buildPublishedRuntimePayload());
1373
- return true;
1374
- }
1375
-
1376
- const cardMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)$`));
1377
- if (method === 'PATCH' && cardMatch) {
1378
- await bootstrapBoard();
1379
- const cardId = decodeURIComponent(cardMatch[1]);
1380
- const body = await readJsonBody(req);
1381
- patchCard(cardId, body);
1382
- broadcastToSseClients();
1383
- json(res, 200, { ok: true });
1384
- return true;
1385
- }
1386
-
1387
- const cardActionMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/actions$`));
1388
- if (method === 'POST' && cardActionMatch) {
1389
- await bootstrapBoard();
1390
- const cardId = decodeURIComponent(cardActionMatch[1]);
1391
- const body = await readJsonBody(req);
1392
- applyCardAction(cardId, body && body.actionType, body && body.payload);
1393
- broadcastToSseClients();
1394
- json(res, 200, { ok: true });
1395
- return true;
1396
- }
1397
-
1398
- const cardChatsMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/chats$`));
1399
- if (method === 'GET' && cardChatsMatch) {
1400
- await bootstrapBoard();
1401
- const cardId = decodeURIComponent(cardChatsMatch[1]);
1402
- json(res, 200, { ok: true, messages: readChatRecords(cardId) });
1403
- return true;
1404
- }
1405
-
1406
- const cardFileMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/files$`));
1407
- if (method === 'POST' && cardFileMatch) {
1408
- await bootstrapBoard();
1409
- const cardId = decodeURIComponent(cardFileMatch[1]);
1410
- const inChat = String(url.searchParams.get('inChat') || '').toLowerCase() === 'true';
1411
- const encodedName = req.headers['x-file-name'];
1412
- const contentType = String(req.headers['content-type'] || 'application/octet-stream');
1413
- const rawName = Array.isArray(encodedName) ? encodedName[0] : encodedName;
1414
- const requestedName = rawName ? decodeURIComponent(String(rawName)) : 'upload.bin';
1415
- const body = await readRawBody(req);
1416
- if (!body.length) {
1417
- json(res, 400, { error: 'Empty upload body' });
1418
- return true;
1419
- }
1420
-
1421
- const file = persistUploadedFile(cardId, requestedName, contentType, body);
1422
- if (inChat) {
1423
- updateCardLocalOnly(cardId, (card) => {
1424
- const now = new Date().toISOString();
1425
- const cardData = card.card_data && typeof card.card_data === 'object' ? card.card_data : {};
1426
- card.card_data = cardData;
1427
- const incoming = cardFileMetadataStore().normalizeIncoming([{
1428
- name: file.name,
1429
- stored_name: file.stored_name,
1430
- size: file.size,
1431
- mime_type: file.mime_type,
1432
- path: file.path,
1433
- uploaded_at: file.uploaded_at || now,
1434
- }], now);
1435
- cardFileMetadataStore().merge(cardData, incoming);
1436
- return card;
1437
- });
1438
- writeChatRecord(cardId, 'system', `file uploaded: ${file.name} as ${file.stored_name}`, []);
1439
- }
1440
- broadcastToSseClients();
1441
- json(res, 200, { ok: true, file });
1442
- return true;
1443
- }
1444
-
1445
- const cardFileDownloadMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/files/(\\d+)$`));
1446
- if (method === 'GET' && cardFileDownloadMatch) {
1447
- const cardId = decodeURIComponent(cardFileDownloadMatch[1]);
1448
- const idx = parseInt(cardFileDownloadMatch[2], 10);
1449
- const expectedStoredName = url.searchParams.get('sn');
1450
-
1451
- const card = readCardFromStore(cardId);
1452
- if (!card || typeof card !== 'object') {
1453
- json(res, 404, { error: 'Card not found' });
1454
- return true;
1455
- }
1456
-
1457
- const resolved = cardFileMetadataStore().resolve(card.card_data, idx, expectedStoredName);
1458
- if (!resolved.ok && resolved.reason === 'stale_reference') {
1459
- json(res, 409, { error: 'File reference is stale. Refresh and try again.' });
1460
- return true;
1461
- }
1462
- if (!resolved.ok) {
1463
- json(res, 404, { error: 'File not found' });
1464
- return true;
1465
- }
1466
-
1467
- const fileRecord = resolved.file;
1468
-
1469
- const { safeCardId } = ensureCardStorageDirs(cardId);
1470
- const stores = artifactsStores(cardId);
1471
- const fileKey = `${safeCardId}/${fileRecord.stored_name}`;
1472
- const bytes = stores.files ? stores.files.getBytes(fileKey) : null;
1473
- if (!bytes) {
1474
- json(res, 404, { error: 'File not found' });
1475
- return true;
1476
- }
1477
-
1478
- const buffer = Buffer.from(bytes);
1479
- const filename = fileRecord.name || fileRecord.stored_name;
1480
- const mimeType = fileRecord.mime_type || 'application/octet-stream';
1481
- res.writeHead(200, {
1482
- 'Content-Type': mimeType,
1483
- 'Content-Disposition': `attachment; filename="${filename}"`,
1484
- 'Content-Length': buffer.length,
1485
- });
1486
- res.end(buffer);
1487
- return true;
1488
- }
1489
-
1490
- return false;
1491
- } catch (err) {
1492
- const statusCode = err && err.statusCode ? err.statusCode : 500;
1493
- json(res, statusCode, { error: String((err && err.message) || err) });
1494
- return true;
1495
- }
1496
- }
1497
-
1498
- return {
1499
- apiBasePath,
1500
- corsHeaders,
1501
- boardDir,
1502
- tmpSurfaceDir,
1503
- runtimeOutDir,
1504
- parseUrl,
1505
- json,
1506
- runCli,
1507
- cardsDir,
1508
- gandalfCardsDir,
1509
- buildPublishedRuntimePayload,
1510
- handleRuntimeApi,
1511
- clearChatRecords,
1512
- };
1513
- }