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