yaml-flow 6.0.0 → 7.1.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 (166) 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 +264 -151
  13. package/card-store.js +4 -4
  14. package/dist/{board-live-cards-public-CltXYgaY.d.cts → board-live-cards-public-5n1-syA3.d.cts} +8 -5
  15. package/dist/{board-live-cards-public-f-E-FAyp.d.ts → board-live-cards-public-CK_J8uv0.d.ts} +8 -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 +55 -12
  78. package/dist/execution-refs.d.ts +55 -12
  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 +3 -0
  94. package/dist/step-machine-public/index.cjs.map +1 -0
  95. package/dist/step-machine-public/index.d.cts +166 -0
  96. package/dist/step-machine-public/index.d.ts +166 -0
  97. package/dist/step-machine-public/index.js +3 -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 +6 -6
  103. package/dist/storage-refs.d.ts +6 -6
  104. package/dist/storage-refs.js +2 -2
  105. package/dist/storage-refs.js.map +1 -1
  106. package/dist/types-CU3DjTKL.d.cts +147 -0
  107. package/dist/types-HGDTWIun.d.ts +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 +370 -0
  110. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.py +398 -0
  111. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +9 -10
  112. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +300 -0
  113. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +617 -0
  114. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-sse-worker.js +48 -0
  115. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +11 -10
  116. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +19 -4
  117. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +4 -8
  118. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -10
  119. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +8 -16
  120. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +2 -6
  121. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +4 -8
  122. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +3 -7
  123. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +4 -8
  124. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +7 -16
  125. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +2 -6
  126. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +13 -3
  127. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +2 -1
  128. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +2 -1
  129. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +2 -1
  130. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +20 -24
  131. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +0 -3
  132. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +8 -13
  133. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +33 -9
  134. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +3 -1
  135. package/examples/cli/step-machine-demo/step2-double-cli.js +6 -12
  136. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +66 -4
  137. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +13 -5
  138. package/examples/example-board/agent-instructions.md +1 -1
  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-rebalance-impact.json +22 -6
  144. package/examples/example-board/cards/card-rebalance-sim.json +66 -15
  145. package/examples/example-board/cards/cardT-market-prices.json +80 -0
  146. package/examples/example-board/cards/{card-portfolio-value.json → cardT-portfolio-value.json} +38 -10
  147. package/examples/example-board/cards/cardT-portfolio.json +78 -0
  148. package/examples/example-board/demo-server-config.json +1 -1
  149. package/examples/example-board/demo-server.js +383 -69
  150. package/examples/example-board/demo-shell-localstorage.html +774 -0
  151. package/examples/example-board/demo-shell-with-server.html +18 -36
  152. package/examples/example-board/demo-shell.html +5 -4
  153. package/examples/example-board/demo-task-executor.js +213 -265
  154. package/package.json +15 -13
  155. package/step-machine-cli.js +43 -310
  156. package/board-livecards-server-runtime.js +0 -1513
  157. package/browser/board-livecards-runtime-client.js +0 -263
  158. package/dist/pycli/quickjs-board-runtime.global.js +0 -9
  159. package/dist/pycli/quickjs-board-runtime.global.js.map +0 -1
  160. package/dist/pycli/quickjs-step-machine-runtime.global.js +0 -5
  161. package/dist/pycli/quickjs-step-machine-runtime.global.js.map +0 -1
  162. package/examples/cli/step-machine-demo/two-step-math-handlers.js +0 -32
  163. package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +0 -24
  164. package/examples/example-board/cards/card-market-prices.json +0 -56
  165. package/examples/example-board/cards/card-portfolio.json +0 -44
  166. package/examples/example-board/demo-shell-browser.html +0 -675
@@ -4,15 +4,22 @@ import http from 'node:http';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import os from 'node:os';
7
+ import net from 'node:net';
7
8
  import { spawnSync, spawn } from 'node:child_process';
8
9
  import { fileURLToPath } from 'node:url';
9
10
  import { createRequire } from 'node:module';
10
11
 
11
12
  import {
12
13
  createMultiBoardServerRuntime,
13
- createRuntimeRequestDispatcher,
14
- isRuntimeRoute,
15
- } from 'yaml-flow/board-livecards-server-runtime';
14
+ createSingleBoardServerRuntime,
15
+ } from 'yaml-flow/server-runtime';
16
+
17
+ import {
18
+ createFsBoardPlatformAdapter,
19
+ createArtifactsStore,
20
+ parseRef,
21
+ serializeRef,
22
+ } from 'yaml-flow/board-live-cards-node';
16
23
 
17
24
  const __filename = fileURLToPath(import.meta.url);
18
25
  const __dirname = path.dirname(__filename);
@@ -27,6 +34,11 @@ function resolveYamlFlowDir() {
27
34
  }
28
35
 
29
36
  const _yamlFlowDir = resolveYamlFlowDir();
37
+
38
+ // cliDir must point to the yaml-flow root so buildBoardCliInvocation finds
39
+ // board-live-cards-cli.js for task-executor completion callbacks.
40
+ // demo-src/example-board is 2 levels below the yaml-flow root.
41
+ const YAML_FLOW_CLI_DIR = _yamlFlowDir || path.resolve(__dirname, '..', '..');
30
42
  const _pkgStepMachineCli = _yamlFlowDir ? path.join(_yamlFlowDir, 'step-machine-cli.js') : null;
31
43
 
32
44
  function loadServerConfig() {
@@ -49,11 +61,17 @@ function resolveFromConfig(configValue) {
49
61
  function resolveKindRefFromConfig(configValue) {
50
62
  if (typeof configValue !== 'string' || !configValue.trim()) return null;
51
63
  const trimmed = configValue.trim();
52
- if (!trimmed.startsWith('::fs-path::')) return trimmed;
53
- const rawPath = trimmed.slice('::fs-path::'.length).trim();
54
- if (!rawPath) return null;
55
- const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(__dirname, rawPath);
56
- return `::fs-path::${resolved}`;
64
+ if (!trimmed.startsWith('b64:')) return trimmed;
65
+ try {
66
+ const parsed = parseRef(trimmed);
67
+ if (parsed.kind !== 'fs-path') return trimmed;
68
+ const rawPath = parsed.value.trim();
69
+ if (!rawPath) return null;
70
+ const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(__dirname, rawPath);
71
+ return serializeRef({ kind: 'fs-path', value: resolved });
72
+ } catch {
73
+ return trimmed;
74
+ }
57
75
  }
58
76
 
59
77
  const serverConfig = loadServerConfig();
@@ -87,23 +105,316 @@ const CORS_HEADERS = {
87
105
  'Access-Control-Allow-Methods': 'GET,POST,PATCH,OPTIONS',
88
106
  };
89
107
 
108
+ // ---------------------------------------------------------------------------
109
+ // Setup directory & defaults
110
+ // ---------------------------------------------------------------------------
111
+
112
+ const setupDir = path.resolve(
113
+ process.env.DEMO_SETUP_DIR || path.join(__dirname, '.demo-setup'),
114
+ );
115
+ fs.mkdirSync(setupDir, { recursive: true });
116
+
117
+ const defaultCardsDir = path.resolve(
118
+ process.env.DEMO_CARDS_DIR || configuredCardsDir || path.join(__dirname, 'cards'),
119
+ );
120
+
121
+ const defaultTaskExecutorPath = process.env.DEMO_TASK_EXECUTOR_PATH || configuredTaskExecutorPath || null;
122
+ const defaultChatHandlerPath = process.env.DEMO_CHAT_HANDLER_PATH || configuredChatHandlerPath || null;
123
+ const defaultInferenceAdapterPath = process.env.DEMO_INFERENCE_ADAPTER_PATH || configuredInferenceAdapterPath || null;
124
+ const defaultStepMachineCliPath = process.env.DEMO_STEP_MACHINE_CLI_PATH || configuredStepMachineCliPath || null;
125
+ const defaultGandalfCardsDir = process.env.DEMO_GANDALF_CARDS_DIR || configuredGandalfCardsDir || null;
126
+ const defaultGandalfTaskExecutorPath = process.env.DEMO_GANDALF_TASK_EXECUTOR_PATH || configuredGandalfTaskExecutorPath || null;
127
+ const defaultGandalfChatHandlerPath = process.env.DEMO_GANDALF_CHAT_HANDLER_PATH || configuredGandalfChatHandlerPath || null;
128
+ const defaultGandalfInferenceAdapterPath = process.env.DEMO_GANDALF_INFERENCE_ADAPTER_PATH || configuredGandalfInferenceAdapterPath || null;
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Host adapter factories — Node-specific implementations injected into the
132
+ // platform-free server runtime.
133
+ // ---------------------------------------------------------------------------
134
+
135
+ function createFsCardSource(cardsDir) {
136
+ return {
137
+ listCards() {
138
+ if (!fs.existsSync(cardsDir)) return [];
139
+ return fs.readdirSync(cardsDir)
140
+ .filter(f => f.endsWith('.json'))
141
+ .map(f => {
142
+ try { return JSON.parse(fs.readFileSync(path.join(cardsDir, f), 'utf-8')); }
143
+ catch { return null; }
144
+ })
145
+ .filter(Boolean);
146
+ },
147
+ };
148
+ }
149
+
150
+ function namedPipePath(pipeName) {
151
+ if (process.platform === 'win32') return `\\\\.\\pipe\\${pipeName}`;
152
+ return path.join(os.tmpdir(), `${pipeName}.sock`);
153
+ }
154
+
155
+ function makeExecutionRef(scriptPath, meta) {
156
+ if (!scriptPath) return undefined;
157
+ const resolved = path.isAbsolute(scriptPath) ? scriptPath : path.resolve(process.cwd(), scriptPath);
158
+ return { howToRun: 'local-node', whatToRun: serializeRef({ kind: 'fs-path', value: resolved }), meta };
159
+ }
160
+
161
+ function createNodeSpawnInvocationAdapter() {
162
+ return {
163
+ async invoke(ref, args) {
164
+ if (ref.howToRun !== 'local-node') {
165
+ return { dispatched: false, error: `unsupported howToRun: ${ref.howToRun}` };
166
+ }
167
+ const whatToRun = ref.whatToRun;
168
+ let scriptPath = '';
169
+ if (whatToRun && typeof whatToRun === 'object') {
170
+ if (whatToRun.kind === 'fs-path') scriptPath = whatToRun.value;
171
+ } else if (typeof whatToRun === 'string' && whatToRun.startsWith('b64:')) {
172
+ try {
173
+ const parsed = parseRef(whatToRun);
174
+ if (parsed.kind === 'fs-path') scriptPath = parsed.value;
175
+ } catch {
176
+ scriptPath = '';
177
+ }
178
+ }
179
+ if (!scriptPath) {
180
+ return { dispatched: false, error: `no fs-path in whatToRun: ${JSON.stringify(whatToRun)}` };
181
+ }
182
+ // Resolve chatsKeyPrefix (blob key prefix) to absolute FS chatDir for handlers
183
+ const finalArgs = { ...args };
184
+ if (finalArgs.chatsKeyPrefix && finalArgs.chatsBlobBasePath) {
185
+ const cardPart = String(finalArgs.chatsKeyPrefix).split('/')[0];
186
+ finalArgs.chatDir = path.join(String(finalArgs.chatsBlobBasePath), cardPart);
187
+ }
188
+ delete finalArgs.chatsKeyPrefix;
189
+ delete finalArgs.chatsBlobBasePath;
190
+ const extra = Buffer.from(JSON.stringify(finalArgs)).toString('base64');
191
+ try {
192
+ const proc = spawn(process.execPath, [
193
+ scriptPath,
194
+ '--boardId', String(args.boardId || ''),
195
+ '--cardId', String(args.cardId || ''),
196
+ '--extraEncJson', extra,
197
+ ], { stdio: 'ignore', windowsHide: true });
198
+ proc.unref();
199
+ return { dispatched: true };
200
+ } catch (err) {
201
+ return { dispatched: false, error: err?.message || String(err) };
202
+ }
203
+ },
204
+ async describe(ref) {
205
+ if (ref.howToRun !== 'local-node') return null;
206
+ const whatToRun = ref.whatToRun;
207
+ let scriptPath = '';
208
+ if (whatToRun && typeof whatToRun === 'object') {
209
+ if (whatToRun.kind === 'fs-path') scriptPath = whatToRun.value;
210
+ } else if (typeof whatToRun === 'string' && whatToRun.startsWith('b64:')) {
211
+ try {
212
+ const parsed = parseRef(whatToRun);
213
+ if (parsed.kind === 'fs-path') scriptPath = parsed.value;
214
+ } catch {
215
+ scriptPath = '';
216
+ }
217
+ }
218
+ if (!scriptPath) return null;
219
+ try {
220
+ const result = spawnSync(process.execPath, [scriptPath, 'describe'], {
221
+ timeout: 5000, encoding: 'utf-8', windowsHide: true,
222
+ });
223
+ if (result.status !== 0) return null;
224
+ return JSON.parse(String(result.stdout).trim());
225
+ } catch { return null; }
226
+ },
227
+ };
228
+ }
229
+
230
+ function createNamedPipeNotificationTransport() {
231
+ return {
232
+ async subscribe(ref, onEvent) {
233
+ if (ref.kind !== 'named-pipe') {
234
+ console.warn(`[notification] unsupported transport kind: ${ref.kind}`);
235
+ return () => {};
236
+ }
237
+ const pipePath = ref.value;
238
+ if (process.platform !== 'win32' && fs.existsSync(pipePath)) {
239
+ try { fs.rmSync(pipePath, { force: true }); } catch { /* best-effort */ }
240
+ }
241
+ const server = net.createServer((socket) => {
242
+ let buf = '';
243
+ socket.on('data', (chunk) => {
244
+ buf += chunk.toString('utf-8');
245
+ while (true) {
246
+ const i = buf.indexOf('\n');
247
+ if (i < 0) break;
248
+ const line = buf.slice(0, i).trim();
249
+ buf = buf.slice(i + 1);
250
+ if (!line) continue;
251
+ try {
252
+ const msg = JSON.parse(line);
253
+ onEvent(msg?.notification ?? msg);
254
+ } catch { /* ignore malformed lines */ }
255
+ }
256
+ });
257
+ });
258
+ await new Promise((resolve, reject) => {
259
+ server.once('error', reject);
260
+ server.listen(pipePath, () => resolve());
261
+ });
262
+ return () => {
263
+ server.close();
264
+ if (process.platform !== 'win32') {
265
+ try { fs.rmSync(pipePath, { force: true }); } catch { /* best-effort */ }
266
+ }
267
+ };
268
+ },
269
+ };
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Server meta store (multi-board registry)
274
+ // ---------------------------------------------------------------------------
275
+
276
+ const serverMetaRef = process.env.DEMO_SERVER_META_STORE_REF || configuredServerMetaStoreRef || serializeRef({ kind: 'fs-path', value: setupDir });
277
+ const serverMetaAdapter = createFsBoardPlatformAdapter(
278
+ parseRef(serverMetaRef), YAML_FLOW_CLI_DIR, { suppressSpawn: true },
279
+ );
280
+ const serverMetaStore = createArtifactsStore(serverMetaAdapter.blobStorage('server-meta'));
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Build multi-board runtime
284
+ // ---------------------------------------------------------------------------
285
+
286
+ const apiBasePath = '/api/boards';
287
+ const invocationAdapter = createNodeSpawnInvocationAdapter();
288
+ const notificationTransport = createNamedPipeNotificationTransport();
289
+ const logger = { info: console.log, warn: console.warn, error: console.error };
290
+
291
+ // Track per-board host config for demo-setup (FS paths are host concerns, not runtime concerns)
292
+ const boardHostConfig = new Map();
293
+
294
+ function buildBoardContextConfig(label, boardDir, taskExecPath, chatHandlerPath, infAdapterPath, boardId) {
295
+ fs.mkdirSync(boardDir, { recursive: true });
296
+
297
+ // Runtime card store lives inside the board's setup dir, isolated from the source cards dir.
298
+ // Layout: boardDir/cards/store — KV card store
299
+ // boardDir/cards/chats — chat blobs
300
+ // boardDir/cards/files — file uploads
301
+ const runtimeCardsDir = path.join(boardDir, 'cards');
302
+ const runtimeCardStoreDir = path.join(runtimeCardsDir, 'store');
303
+ fs.mkdirSync(runtimeCardStoreDir, { recursive: true });
304
+
305
+ const notifyChannel = `yaml-flow-server-${label}-${boardId}-${process.pid}`;
306
+ const baseRef = parseRef(serializeRef({ kind: 'fs-path', value: boardDir }));
307
+ const boardAdapter = createFsBoardPlatformAdapter(baseRef, YAML_FLOW_CLI_DIR, {
308
+ notifyChannel,
309
+ });
310
+ // In the server context the drain loop is driven in-process; suppress the
311
+ // detached CLI spawn that the FS adapter would otherwise fire as a continuation.
312
+ boardAdapter.requestProcessAccumulated = () => {};
313
+ // Artifacts adapter rooted at runtimeCardsDir so chats/ and files/ are siblings of store/.
314
+ const artifactsRef = parseRef(serializeRef({ kind: 'fs-path', value: runtimeCardsDir }));
315
+ const artifactsAdapter = createFsBoardPlatformAdapter(artifactsRef, YAML_FLOW_CLI_DIR, { suppressSpawn: true });
316
+
317
+ const cardStoreRef = serializeRef({ kind: 'fs-path', value: runtimeCardStoreDir });
318
+
319
+ return {
320
+ label,
321
+ boardAdapter,
322
+ artifactsAdapter,
323
+ baseRef,
324
+ cardStoreRef,
325
+ outputsStoreRef: serializeRef({ kind: 'fs-path', value: path.join(path.dirname(boardDir), 'runtime-out', '.outputs') }),
326
+ notifyRef: { kind: 'named-pipe', value: namedPipePath(notifyChannel) },
327
+ taskExecutorRef: makeExecutionRef(taskExecPath, 'task-executor'),
328
+ chatHandlerRef: makeExecutionRef(chatHandlerPath, 'chat-handler'),
329
+ inferenceAdapterRef: makeExecutionRef(infAdapterPath, 'inference-adapter'),
330
+ };
331
+ }
332
+
90
333
  const runtime = createMultiBoardServerRuntime({
91
- apiBasePath: '/api/boards',
92
- serverUrl: `http://127.0.0.1:${PORT}`,
93
- defaultCardsDir: process.env.DEMO_CARDS_DIR || configuredCardsDir || null,
94
- defaultTaskExecutorPath: process.env.DEMO_TASK_EXECUTOR_PATH || configuredTaskExecutorPath || null,
95
- defaultStepMachineCliPath: process.env.DEMO_STEP_MACHINE_CLI_PATH || configuredStepMachineCliPath,
96
- defaultChatHandlerPath: process.env.DEMO_CHAT_HANDLER_PATH || configuredChatHandlerPath || null,
97
- defaultInferenceAdapterPath: process.env.DEMO_INFERENCE_ADAPTER_PATH || configuredInferenceAdapterPath || null,
98
- defaultGandalfCardsDir: process.env.DEMO_GANDALF_CARDS_DIR || configuredGandalfCardsDir || null,
99
- defaultGandalfTaskExecutorPath: process.env.DEMO_GANDALF_TASK_EXECUTOR_PATH || configuredGandalfTaskExecutorPath || null,
100
- defaultGandalfChatHandlerPath: process.env.DEMO_GANDALF_CHAT_HANDLER_PATH || configuredGandalfChatHandlerPath || null,
101
- defaultGandalfInferenceAdapterPath: process.env.DEMO_GANDALF_INFERENCE_ADAPTER_PATH || configuredGandalfInferenceAdapterPath || null,
102
- serverMetaStoreRef: process.env.DEMO_SERVER_META_STORE_REF || configuredServerMetaStoreRef || null,
334
+ apiBasePath,
335
+ serverMetaStore,
336
+ logger,
337
+ boardRuntimeFactory: (boardId, entry) => {
338
+ // sourceCardsDir: read-only source used only for initial seeding.
339
+ const sourceCardsDir = typeof entry.cardsDir === 'string' ? path.resolve(entry.cardsDir) : defaultCardsDir;
340
+ const boardRoot = path.join(setupDir, `board-${boardId}`);
341
+ const boardDir = path.join(boardRoot, 'runtime');
342
+
343
+ const taskExecPath = typeof entry.taskExecutorPath === 'string' ? entry.taskExecutorPath : defaultTaskExecutorPath;
344
+ const chatHandlerPath_ = typeof entry.chatHandlerPath === 'string' ? entry.chatHandlerPath : defaultChatHandlerPath;
345
+ const infAdapterPath = typeof entry.inferenceAdapterPath === 'string' ? entry.inferenceAdapterPath : defaultInferenceAdapterPath;
346
+ const stepMachinePath = typeof entry.stepMachineCliPath === 'string' ? entry.stepMachineCliPath : defaultStepMachineCliPath;
347
+
348
+ const sourceGandalfCardsDir = typeof entry.gandalfCardsDir === 'string' ? path.resolve(entry.gandalfCardsDir) : defaultGandalfCardsDir;
349
+ const gandalfTaskExecPath = typeof entry.gandalfTaskExecutorPath === 'string' ? entry.gandalfTaskExecutorPath : defaultGandalfTaskExecutorPath;
350
+ const gandalfChatPath = typeof entry.gandalfChatHandlerPath === 'string' ? entry.gandalfChatHandlerPath : defaultGandalfChatHandlerPath;
351
+ const gandalfInfPath = typeof entry.gandalfInferenceAdapterPath === 'string' ? entry.gandalfInferenceAdapterPath : defaultGandalfInferenceAdapterPath;
352
+
353
+ const baseCfg = buildBoardContextConfig('base', boardDir, taskExecPath, chatHandlerPath_, infAdapterPath, boardId);
354
+
355
+ const boards = [baseCfg];
356
+ let gandalfBoardDir = null;
357
+ if (sourceGandalfCardsDir && gandalfTaskExecPath) {
358
+ gandalfBoardDir = path.join(boardRoot, 'gandalf-runtime');
359
+ const gandalfCfg = buildBoardContextConfig('gandalf', gandalfBoardDir, gandalfTaskExecPath, gandalfChatPath, gandalfInfPath, boardId);
360
+ gandalfCfg.outputsStoreRef = serializeRef({ kind: 'fs-path', value: path.join(boardRoot, 'gandalf-runtime-out', '.outputs') });
361
+ boards.push(gandalfCfg);
362
+ }
363
+
364
+ // Store host config for demo-setup (FS paths are host concerns)
365
+ boardHostConfig.set(boardId, { cardsDir: sourceCardsDir, gandalfCardsDir: sourceGandalfCardsDir, boardDir, boardRoot });
366
+
367
+ // Auto-run demo-setup (write copilot-instructions.md) at board init time,
368
+ // so clients no longer need a separate /demo-setup request before bootstrapping.
369
+ demoPrepSetup(boardId);
370
+
371
+ // runtimeCardsDir is where the live card store lives (inside setupDir).
372
+ const runtimeCardsDir = path.join(boardDir, 'cards');
373
+
374
+ const singleBoardRuntime = createSingleBoardServerRuntime({
375
+ apiBasePath: `${apiBasePath}/${boardId}`,
376
+ boardId,
377
+ boards,
378
+ invocationAdapter,
379
+ notificationTransport,
380
+ logger,
381
+ serverUrl: `http://127.0.0.1:${PORT}`,
382
+ executionExtra: {
383
+ boardSetupRoot: boardRoot,
384
+ chatsBlobBasePath: path.join(runtimeCardsDir, 'chats'),
385
+ ...(stepMachinePath ? { stepMachineCliPath: stepMachinePath } : {}),
386
+ },
387
+ });
388
+
389
+ // Host concern: seed card store from source cardsDir only if the runtime store is empty.
390
+ const existing = singleBoardRuntime.cardStore.get({});
391
+ const isEmpty = existing.status !== 'success' || !existing.data?.cards?.length;
392
+ if (isEmpty) {
393
+ const cards = createFsCardSource(sourceCardsDir).listCards();
394
+ if (cards.length) singleBoardRuntime.cardStore.set({ body: cards });
395
+ }
396
+ // Seed gandalf board if present
397
+ if (gandalfBoardDir && sourceGandalfCardsDir) {
398
+ const gandalfRuntime = singleBoardRuntime.getBoardRuntime?.('gandalf');
399
+ if (gandalfRuntime) {
400
+ const gExisting = gandalfRuntime.cardStore.get({});
401
+ const gEmpty = gExisting.status !== 'success' || !gExisting.data?.cards?.length;
402
+ if (gEmpty) {
403
+ const gCards = createFsCardSource(sourceGandalfCardsDir).listCards();
404
+ if (gCards.length) gandalfRuntime.cardStore.set({ body: gCards });
405
+ }
406
+ }
407
+ }
408
+
409
+ return singleBoardRuntime;
410
+ },
103
411
  });
104
412
 
413
+ // ---------------------------------------------------------------------------
414
+ // Reset
415
+ // ---------------------------------------------------------------------------
416
+
105
417
  function resetRuntime() {
106
- const setupDir = runtime.setupDir;
107
418
  if (fs.existsSync(setupDir)) {
108
419
  fs.rmSync(setupDir, { recursive: true, force: true });
109
420
  console.log(`[demo-server] reset: wiped ${setupDir}`);
@@ -121,33 +432,24 @@ if (RESET_ON_START) {
121
432
  resetRuntime();
122
433
  }
123
434
 
124
- const dispatch = createRuntimeRequestDispatcher(runtime);
125
-
126
- // Board-id segment regex: /api/boards/:boardId/...
127
- const BOARD_SEG_RE = /^\/api\/boards\/([^/]+)\/(.+)$/;
128
-
129
- function jsonReply(res, status, payload) {
130
- const body = JSON.stringify(payload);
131
- res.writeHead(status, { ...CORS_HEADERS, 'Content-Type': 'application/json; charset=utf-8' });
132
- res.end(body);
133
- }
134
-
135
435
  // ---------------------------------------------------------------------------
136
- // Card preparation — host-level concern, not a reusable runtime concern.
137
- // Writes the concatenated copilot-instructions.md at the board setup root.
138
- // Cards are served directly from cardsDir (no tmp copy needed).
436
+ // Demo-setup — host-level concern (not a runtime concern).
437
+ // Writes concatenated copilot-instructions.md at the board setup root.
139
438
  // ---------------------------------------------------------------------------
140
439
 
141
- const _demoPrepSetupDone = new Map(); // boardId → true
440
+ const BOARD_SEG_RE = /^\/api\/boards\/([^/]+)\/(.+)$/;
441
+ const _demoPrepSetupDone = new Map();
142
442
 
143
- function isDemoSetupDone(boardId, service) {
144
- return _demoPrepSetupDone.get(boardId) === true && fs.existsSync(service.cardsDir);
443
+ function isDemoSetupDone(boardId) {
444
+ const cfg = boardHostConfig.get(boardId);
445
+ return _demoPrepSetupDone.get(boardId) === true && cfg && fs.existsSync(cfg.cardsDir);
145
446
  }
146
447
 
147
- function demoPrepSetup(boardId, service) {
148
- const { cardsDir, gandalfCardsDir, boardDir } = service;
448
+ function demoPrepSetup(boardId) {
449
+ const cfg = boardHostConfig.get(boardId);
450
+ if (!cfg) return;
451
+ const { cardsDir, gandalfCardsDir, boardDir } = cfg;
149
452
 
150
- // Concatenate agent-instructions*.md into copilot-instructions.md at boardSetupRoot.
151
453
  const boardSetupRoot = path.dirname(boardDir);
152
454
  fs.mkdirSync(boardSetupRoot, { recursive: true });
153
455
  const srcDir = path.dirname(cardsDir);
@@ -164,22 +466,28 @@ function demoPrepSetup(boardId, service) {
164
466
  _demoPrepSetupDone.set(boardId, true);
165
467
  }
166
468
 
469
+ function jsonReply(res, status, payload) {
470
+ const body = JSON.stringify(payload);
471
+ res.writeHead(status, { ...CORS_HEADERS, 'Content-Type': 'application/json; charset=utf-8' });
472
+ res.end(body);
473
+ }
474
+
167
475
  async function handleDemoSetup(req, res, boardId) {
168
476
  try {
169
- const { service } = runtime.requireBoardService(boardId);
170
- let setupPerformed = false;
171
-
172
- if (!isDemoSetupDone(boardId, service)) {
173
- demoPrepSetup(boardId, service);
174
- setupPerformed = true;
175
- }
176
-
177
- jsonReply(res, 200, { ok: true, setupPerformed });
477
+ // requireBoardService triggers the factory which runs demoPrepSetup automatically.
478
+ // This endpoint is kept for backward compatibility but setup is now done at board
479
+ // init time inside boardRuntimeFactory — no extra work needed here.
480
+ runtime.requireBoardService(boardId);
481
+ jsonReply(res, 200, { ok: true, setupPerformed: false });
178
482
  } catch (err) {
179
483
  jsonReply(res, err.statusCode || 500, { error: String((err && err.message) || err) });
180
484
  }
181
485
  }
182
486
 
487
+ // ---------------------------------------------------------------------------
488
+ // WorkIQ proxy — host-level concern
489
+ // ---------------------------------------------------------------------------
490
+
183
491
  async function handleWorkiqAsk(req, res) {
184
492
  let body = '';
185
493
  for await (const chunk of req) body += chunk;
@@ -201,8 +509,6 @@ async function handleWorkiqAsk(req, res) {
201
509
  return jsonReply(res, 503, { error: `WorkIQ CLI not found at: ${workiqJs}` });
202
510
  }
203
511
 
204
- // Server has TTY on stdin — workiq can produce output.
205
- // Use async spawn (not spawnSync) to avoid blocking the event loop during the call.
206
512
  await new Promise((resolve) => {
207
513
  let stdout = '';
208
514
  let stderr = '';
@@ -243,9 +549,14 @@ async function handleWorkiqAsk(req, res) {
243
549
  });
244
550
  }
245
551
 
552
+ // ---------------------------------------------------------------------------
553
+ // HTTP server
554
+ // ---------------------------------------------------------------------------
555
+
246
556
  const server = http.createServer((req, res) => {
247
557
  const method = req.method || 'GET';
248
- const pathname = new URL(req.url || '/', 'http://localhost').pathname;
558
+ const url = new URL(req.url || '/', 'http://localhost');
559
+ const pathname = url.pathname;
249
560
 
250
561
  if (method === 'OPTIONS') {
251
562
  res.writeHead(204, CORS_HEADERS);
@@ -266,26 +577,29 @@ const server = http.createServer((req, res) => {
266
577
  return;
267
578
  }
268
579
 
269
- // All other /api/boards routes are handled by the reusable runtime
270
- void dispatch(req, res);
580
+ // All other /api/boards routes are handled by the platform-free runtime
581
+ runtime.handleApi(req, res, url).then((handled) => {
582
+ if (!handled) {
583
+ jsonReply(res, 404, { error: 'Not found' });
584
+ }
585
+ });
271
586
  });
272
587
 
273
588
  server.listen(PORT, '127.0.0.1', () => {
274
589
  console.log(`[demo-server] listening on http://127.0.0.1:${PORT}`);
275
- console.log(`[demo-server] setup dir: ${runtime.setupDir}`);
276
- console.log(`[demo-server] server-meta store: ${runtime.serverMetaStoreRef}`);
277
- console.log(`[demo-server] boards registry key: server-meta/${runtime.boardsRegistryKey}`);
590
+ console.log(`[demo-server] setup dir: ${setupDir}`);
591
+ console.log(`[demo-server] server-meta store: ${serverMetaRef}`);
278
592
  console.log('[demo-server] endpoints:');
279
- console.log(` GET ${runtime.apiBasePath} <- list boards`);
280
- console.log(` POST ${runtime.apiBasePath} {id, label?} <- register board`);
281
- console.log(` GET ${runtime.apiBasePath}/:boardId/demo-setup`);
282
- console.log(` GET ${runtime.apiBasePath}/:boardId/bootstrap`);
283
- console.log(` GET ${runtime.apiBasePath}/:boardId/sse`);
284
- console.log(` GET ${runtime.apiBasePath}/:boardId/board-status`);
285
- console.log(` PATCH ${runtime.apiBasePath}/:boardId/cards/:id`);
286
- console.log(` POST ${runtime.apiBasePath}/:boardId/cards/:id/actions`);
287
- console.log(` POST ${runtime.apiBasePath}/:boardId/cards/:id/files`);
288
- console.log(` GET ${runtime.apiBasePath}/:boardId/cards/:id/files/:idx`);
289
- console.log(` GET ${runtime.apiBasePath}/:boardId/cards/:id/chats`);
593
+ console.log(` GET ${apiBasePath} <- list boards`);
594
+ console.log(` POST ${apiBasePath} {id, label?} <- register board`);
595
+ console.log(` GET ${apiBasePath}/:boardId/demo-setup (no-op; setup now runs at board init)`);
596
+ console.log(` GET ${apiBasePath}/:boardId/init-board`);
597
+ console.log(` GET ${apiBasePath}/:boardId/sse`);
598
+ console.log(` GET ${apiBasePath}/:boardId/board-status`);
599
+ console.log(` PATCH ${apiBasePath}/:boardId/cards/:id`);
600
+ console.log(` POST ${apiBasePath}/:boardId/cards/:id/actions`);
601
+ console.log(` POST ${apiBasePath}/:boardId/cards/:id/files`);
602
+ console.log(` GET ${apiBasePath}/:boardId/cards/:id/files/:idx`);
603
+ console.log(` GET ${apiBasePath}/:boardId/cards/:id/chats`);
290
604
  console.log(` POST /api/workiq/ask {query} <- WorkIQ (M365 Copilot) proxy`);
291
605
  });