yaml-flow 5.4.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/board-live-cards-cli.js +2 -2
- package/board-livecards-server-runtime.js +486 -547
- package/browser/asset-integrity.json +10 -0
- package/browser/board-livegraph-engine.js +2 -1676
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/live-cards.js +347 -26
- package/browser/live-cards.schema.json +418 -132
- package/card-store.js +37 -0
- package/dist/batch/index.cjs +1 -108
- package/dist/batch/index.cjs.map +1 -1
- package/dist/batch/index.js +1 -106
- package/dist/batch/index.js.map +1 -1
- package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +136 -0
- package/dist/board-live-cards-lib-jM2uYG1v.d.ts +136 -0
- package/dist/board-live-cards-public-CltXYgaY.d.cts +314 -0
- package/dist/board-live-cards-public-f-E-FAyp.d.ts +314 -0
- package/dist/board-livegraph-runtime/index.cjs +2 -1671
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.d.cts +1 -2
- package/dist/board-livegraph-runtime/index.d.ts +1 -2
- package/dist/board-livegraph-runtime/index.js +2 -1662
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/board-livegraph-runtime/jsonata-sync.cjs +7587 -0
- package/dist/card-compute/index.cjs +9 -7159
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +22 -0
- package/dist/card-compute/index.d.ts +22 -0
- package/dist/card-compute/index.js +9 -7145
- package/dist/card-compute/index.js.map +1 -1
- package/dist/card-compute/jsonata-sync.cjs +7587 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +24 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +24 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -0
- package/dist/cli/browser-api/card-store-browser-api.cjs +2 -0
- package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -0
- package/dist/cli/browser-api/card-store-browser-api.d.cts +26 -0
- package/dist/cli/browser-api/card-store-browser-api.d.ts +26 -0
- package/dist/cli/browser-api/card-store-browser-api.js +2 -0
- package/dist/cli/browser-api/card-store-browser-api.js.map +1 -0
- package/dist/cli/browser-api/jsonata-sync.cjs +7587 -0
- package/dist/cli/node/artifacts-store-cli.cjs +11 -0
- package/dist/cli/node/artifacts-store-cli.cjs.map +1 -0
- package/dist/cli/node/artifacts-store-cli.d.cts +8 -0
- package/dist/cli/node/artifacts-store-cli.d.ts +8 -0
- package/dist/cli/node/artifacts-store-cli.js +11 -0
- package/dist/cli/node/artifacts-store-cli.js.map +1 -0
- package/dist/cli/node/board-live-cards-cli.cjs +15 -0
- package/dist/cli/node/board-live-cards-cli.cjs.map +1 -0
- package/dist/cli/node/board-live-cards-cli.d.cts +20 -0
- package/dist/cli/node/board-live-cards-cli.d.ts +20 -0
- package/dist/cli/node/board-live-cards-cli.js +15 -0
- package/dist/cli/node/board-live-cards-cli.js.map +1 -0
- package/dist/cli/node/card-store-cli.cjs +8 -0
- package/dist/cli/node/card-store-cli.cjs.map +1 -0
- package/dist/cli/node/card-store-cli.d.cts +15 -0
- package/dist/cli/node/card-store-cli.d.ts +15 -0
- package/dist/cli/node/card-store-cli.js +8 -0
- package/dist/cli/node/card-store-cli.js.map +1 -0
- package/dist/cli/node/fs-board-adapter.cjs +14 -0
- package/dist/cli/node/fs-board-adapter.cjs.map +1 -0
- package/dist/cli/node/fs-board-adapter.d.cts +204 -0
- package/dist/cli/node/fs-board-adapter.d.ts +204 -0
- package/dist/cli/node/fs-board-adapter.js +14 -0
- package/dist/cli/node/fs-board-adapter.js.map +1 -0
- package/dist/cli/node/jsonata-sync.cjs +7587 -0
- package/dist/cli/node/source-cli-task-executor.cjs +11 -0
- package/dist/cli/node/source-cli-task-executor.cjs.map +1 -0
- package/dist/cli/node/source-cli-task-executor.d.cts +1 -0
- package/dist/cli/node/source-cli-task-executor.d.ts +1 -0
- package/dist/cli/node/source-cli-task-executor.js +11 -0
- package/dist/cli/node/source-cli-task-executor.js.map +1 -0
- package/dist/config/index.cjs +1 -79
- package/dist/config/index.cjs.map +1 -1
- package/dist/config/index.js +1 -76
- package/dist/config/index.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +2 -2129
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +81 -5
- package/dist/continuous-event-graph/index.d.ts +81 -5
- package/dist/continuous-event-graph/index.js +2 -2088
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/continuous-event-graph/jsonata-sync.cjs +7587 -0
- package/dist/event-graph/index.cjs +22 -8292
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.js +22 -8237
- package/dist/event-graph/index.js.map +1 -1
- package/dist/execution-refs.cjs +2 -0
- package/dist/execution-refs.cjs.map +1 -0
- package/dist/execution-refs.d.cts +222 -0
- package/dist/execution-refs.d.ts +222 -0
- package/dist/execution-refs.js +2 -0
- package/dist/execution-refs.js.map +1 -0
- package/dist/index.cjs +29 -13221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +29 -13112
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +5 -617
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.js +5 -610
- package/dist/inference/index.js.map +1 -1
- package/dist/jsonata-sync.cjs +7587 -0
- package/dist/{live-cards-bridge-x5XREkXm.d.cts → live-cards-bridge-BXbVTsna.d.cts} +27 -4
- package/dist/{live-cards-bridge-EQjytzI_.d.ts → live-cards-bridge-Ds28XR15.d.ts} +27 -4
- package/dist/pycli/quickjs-board-runtime.global.js +9 -0
- package/dist/pycli/quickjs-board-runtime.global.js.map +1 -0
- package/dist/pycli/quickjs-step-machine-runtime.global.js +5 -0
- package/dist/pycli/quickjs-step-machine-runtime.global.js.map +1 -0
- package/dist/step-machine/index.cjs +11 -7129
- package/dist/step-machine/index.cjs.map +1 -1
- package/dist/step-machine/index.js +11 -7113
- package/dist/step-machine/index.js.map +1 -1
- package/dist/storage-refs.cjs +10 -0
- package/dist/storage-refs.cjs.map +1 -0
- package/dist/storage-refs.d.cts +92 -0
- package/dist/storage-refs.d.ts +92 -0
- package/dist/storage-refs.js +10 -0
- package/dist/storage-refs.js.map +1 -0
- package/dist/stores/file.cjs +1 -114
- package/dist/stores/file.cjs.map +1 -1
- package/dist/stores/file.js +1 -112
- package/dist/stores/file.js.map +1 -1
- package/dist/stores/index.cjs +1 -231
- package/dist/stores/index.cjs.map +1 -1
- package/dist/stores/index.js +1 -227
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/localStorage.cjs +1 -76
- package/dist/stores/localStorage.cjs.map +1 -1
- package/dist/stores/localStorage.js +1 -74
- package/dist/stores/localStorage.js.map +1 -1
- package/dist/stores/memory.cjs +1 -47
- package/dist/stores/memory.cjs.map +1 -1
- package/dist/stores/memory.js +1 -45
- package/dist/stores/memory.js.map +1 -1
- package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +292 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.js +218 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +201 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +25 -16
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +553 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +365 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +1 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +32 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +53 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +15 -6
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +57 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +1 -1
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +7 -2
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +6 -2
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +97 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +50 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +44 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +70 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +36 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +26 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +39 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +80 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +25 -172
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +40 -34
- package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +46 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +77 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
- package/examples/example-board/agent-instructions.md +11 -5
- package/examples/example-board/demo-chat-handler.js +14 -4
- package/examples/example-board/demo-server-config.json +1 -0
- package/examples/example-board/demo-server.js +14 -7
- package/examples/example-board/demo-shell-browser.html +5 -4
- package/examples/example-board/demo-shell-with-server.html +6 -5
- package/examples/example-board/demo-task-executor.js +81 -35
- package/examples/index.html +0 -14
- package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -1
- package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
- package/package.json +39 -3
- package/schema/live-cards.schema.json +418 -132
- package/dist/cli/board-live-cards-cli.cjs +0 -10650
- package/dist/cli/board-live-cards-cli.cjs.map +0 -1
- package/dist/cli/board-live-cards-cli.d.cts +0 -179
- package/dist/cli/board-live-cards-cli.d.ts +0 -179
- package/dist/cli/board-live-cards-cli.js +0 -10598
- package/dist/cli/board-live-cards-cli.js.map +0 -1
- package/dist/journal-9HEgs7dU.d.ts +0 -28
- package/dist/journal-B-JCfQnh.d.cts +0 -28
- package/dist/schedule-Cszq9LYY.d.ts +0 -21
- package/dist/schedule-qWNL0RQh.d.cts +0 -21
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +0 -22
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +0 -16
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +0 -28
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +0 -15
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +0 -15
- package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +0 -28
- package/examples/browser/boards/portfolio-tracker/fetch-prices.js +0 -43
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +0 -7
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +0 -351
|
@@ -3,7 +3,19 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import net from 'node:net';
|
|
6
7
|
import { execFileSync, spawn } from 'node:child_process';
|
|
8
|
+
import {
|
|
9
|
+
createBoardLiveCardsPublic,
|
|
10
|
+
createFsBoardPlatformAdapter,
|
|
11
|
+
createCardStorePublic,
|
|
12
|
+
createCardStore,
|
|
13
|
+
createArtifactsStore,
|
|
14
|
+
createChatArtifactsStore,
|
|
15
|
+
createFileArtifactsStore,
|
|
16
|
+
createCardFileMetadataStore,
|
|
17
|
+
parseRef,
|
|
18
|
+
} from './dist/cli/node/fs-board-adapter.js';
|
|
7
19
|
|
|
8
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
21
|
const __dirname = path.dirname(__filename);
|
|
@@ -87,7 +99,6 @@ export function createRuntimeRequestDispatcher(runtime) {
|
|
|
87
99
|
* Manages multiple boards under a single DEMO_SETUP_DIR.
|
|
88
100
|
* Directory layout:
|
|
89
101
|
* setupDir/
|
|
90
|
-
* boards-config.json ← board registry
|
|
91
102
|
* board-default/ ← built-in example board
|
|
92
103
|
* runtime/ ← board-graph.json, cards-inventory.jsonl
|
|
93
104
|
* surface/ ← tmp-cards/
|
|
@@ -116,25 +127,34 @@ export function createMultiBoardServerRuntime(options = {}) {
|
|
|
116
127
|
const defaultCardsDir = path.resolve(
|
|
117
128
|
options.defaultCardsDir || path.join(__dirname, 'cards')
|
|
118
129
|
);
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
const configuredServerMetaStoreRef = typeof options.serverMetaStoreRef === 'string'
|
|
131
|
+
&& options.serverMetaStoreRef.trim()
|
|
132
|
+
? options.serverMetaStoreRef.trim()
|
|
133
|
+
: null;
|
|
134
|
+
const serverMetaStoreRef = configuredServerMetaStoreRef || `::fs-path::${setupDir}`;
|
|
135
|
+
const serverMetaArtifacts = createArtifactsStore(
|
|
136
|
+
createFsBoardPlatformAdapter(parseRef(serverMetaStoreRef), __dirname, { suppressSpawn: true })
|
|
137
|
+
.blobStorage('server-meta')
|
|
138
|
+
);
|
|
139
|
+
const boardsRegistryKey = 'boards-config.json';
|
|
121
140
|
const boardServiceCache = new Map();
|
|
122
141
|
|
|
123
142
|
fs.mkdirSync(setupDir, { recursive: true });
|
|
124
143
|
|
|
125
144
|
function readBoardsConfig() {
|
|
126
|
-
|
|
145
|
+
const raw = serverMetaArtifacts.getText(boardsRegistryKey);
|
|
146
|
+
if (!raw) {
|
|
127
147
|
return { boards: [{ id: 'default', label: 'Default Board' }] };
|
|
128
148
|
}
|
|
129
149
|
try {
|
|
130
|
-
return JSON.parse(
|
|
150
|
+
return JSON.parse(raw);
|
|
131
151
|
} catch {
|
|
132
152
|
return { boards: [{ id: 'default', label: 'Default Board' }] };
|
|
133
153
|
}
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
function writeBoardsConfig(config) {
|
|
137
|
-
|
|
157
|
+
serverMetaArtifacts.putText(boardsRegistryKey, JSON.stringify(config, null, 2));
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
function safeBoardId(raw) {
|
|
@@ -252,12 +272,6 @@ export function createMultiBoardServerRuntime(options = {}) {
|
|
|
252
272
|
config.boards.push(entry);
|
|
253
273
|
writeBoardsConfig(config);
|
|
254
274
|
|
|
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
275
|
json(res, 200, { ok: true, board: entry });
|
|
262
276
|
return true;
|
|
263
277
|
}
|
|
@@ -314,6 +328,8 @@ export function createMultiBoardServerRuntime(options = {}) {
|
|
|
314
328
|
apiBasePath,
|
|
315
329
|
corsHeaders,
|
|
316
330
|
setupDir,
|
|
331
|
+
serverMetaStoreRef,
|
|
332
|
+
boardsRegistryKey,
|
|
317
333
|
parseUrl,
|
|
318
334
|
json,
|
|
319
335
|
handleBoardsRegistryApi,
|
|
@@ -377,18 +393,11 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
377
393
|
: path.resolve(process.cwd(), options.defaultInferenceAdapterPath))
|
|
378
394
|
: null;
|
|
379
395
|
|
|
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
396
|
// Board-cards: parallel runtime dirs for the board-manager board.
|
|
385
397
|
const gandalfCardsDir = options.gandalfCardsDir ? path.resolve(options.gandalfCardsDir) : null;
|
|
386
398
|
const gandalfRuntimeDir = path.resolve(options.gandalfRuntimeDir || path.join(path.dirname(boardDir), 'gandalf-runtime'));
|
|
387
399
|
const gandalfRuntimeOutDir = path.resolve(options.gandalfRuntimeOutDir || path.join(path.dirname(boardDir), 'gandalf-runtime-out'));
|
|
388
400
|
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
401
|
|
|
393
402
|
// Explicit gandalf-card executor paths — no fallback to regular-card paths.
|
|
394
403
|
const configuredGandalfTaskExecutorPath = typeof options.gandalfTaskExecutorPath === 'string' && options.gandalfTaskExecutorPath.trim()
|
|
@@ -407,85 +416,201 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
407
416
|
? options.serverUrl.trim().replace(/\/$/, '')
|
|
408
417
|
: null;
|
|
409
418
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
);
|
|
419
|
+
const sseClients = new Set();
|
|
420
|
+
const cardPathById = new Map();
|
|
421
|
+
const gandalfCardPathById = new Map();
|
|
422
|
+
|
|
423
|
+
function isGandalfCard(cardId) { return gandalfCardPathById.has(cardId); }
|
|
424
|
+
|
|
425
|
+
function namedPipePath(pipeName) {
|
|
426
|
+
if (process.platform === 'win32') return `\\\\.\\pipe\\${pipeName}`;
|
|
427
|
+
return path.join(os.tmpdir(), `${pipeName}.sock`);
|
|
424
428
|
}
|
|
425
|
-
function isGandalfCard(cardId) { return _gandalfCardIds.has(cardId); }
|
|
426
429
|
|
|
427
|
-
function
|
|
428
|
-
|
|
430
|
+
function makeNotificationState() {
|
|
431
|
+
return {
|
|
432
|
+
status: null,
|
|
433
|
+
computedValues: {},
|
|
434
|
+
dataObjects: {},
|
|
435
|
+
cards: {},
|
|
436
|
+
sockets: new Set(),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
429
439
|
|
|
430
|
-
|
|
431
|
-
if (
|
|
440
|
+
function appendNotification(state, event) {
|
|
441
|
+
if (!event || typeof event !== 'object') return;
|
|
442
|
+
if (event.kind === 'status') state.status = event.status;
|
|
443
|
+
if (event.kind === 'computed_values' && event.cardId) state.computedValues[event.cardId] = event.values;
|
|
444
|
+
if (event.kind === 'data_object' && event.key) state.dataObjects[event.key] = event.payload;
|
|
445
|
+
if (event.kind === 'card_refreshed' && event.cardId) state.cards[event.cardId] = event.card;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function makeBoardContext(label, runtimeDir, outputsDir, cardsRootDir, taskExecutorPath, chatHandlerPath, inferenceAdapterPath) {
|
|
449
|
+
const notifyChannel = `yaml-flow-server-${label}-${boardId || 'default'}-${process.pid}`;
|
|
450
|
+
const baseRefStr = `::fs-path::${runtimeDir}`;
|
|
451
|
+
const cardStoreRef = `::fs-path::${path.join(cardsRootDir, 'cards')}`;
|
|
452
|
+
const outputsStoreRef = `::fs-path::${path.join(outputsDir, '.outputs')}`;
|
|
453
|
+
const baseRef = parseRef(baseRefStr);
|
|
454
|
+
const adapter = createFsBoardPlatformAdapter(baseRef, __dirname, {
|
|
455
|
+
onWarn: (msg) => console.warn(`[server-runtime:${label}] ${msg}`),
|
|
456
|
+
suppressSpawn: true,
|
|
457
|
+
notifyChannel,
|
|
458
|
+
});
|
|
459
|
+
const board = createBoardLiveCardsPublic(baseRef, adapter);
|
|
460
|
+
const kv = adapter.kvStorageForRef(cardStoreRef);
|
|
461
|
+
const cardAdapterObj = {
|
|
462
|
+
readIndex: () => kv.read('_index'),
|
|
463
|
+
writeIndex: (idx) => kv.write('_index', idx),
|
|
464
|
+
readCard: (id) => kv.read(id),
|
|
465
|
+
writeCard: (id, card) => { kv.write(id, card); return id; },
|
|
466
|
+
cardExists: (id) => kv.read(id) !== null,
|
|
467
|
+
defaultCardKey: (id) => id,
|
|
468
|
+
};
|
|
469
|
+
const cardStore = createCardStorePublic(createCardStore(cardAdapterObj, console.warn));
|
|
470
|
+
const artifactsRef = parseRef(`::fs-path::${cardsRootDir}`);
|
|
471
|
+
const artifactsAdapter = createFsBoardPlatformAdapter(artifactsRef, __dirname, { suppressSpawn: true });
|
|
472
|
+
const filesArtifacts = createArtifactsStore(artifactsAdapter.blobStorage('files'));
|
|
473
|
+
const chatsArtifacts = createArtifactsStore(artifactsAdapter.blobStorage('chats'));
|
|
474
|
+
return {
|
|
475
|
+
label,
|
|
476
|
+
runtimeDir,
|
|
477
|
+
outputsDir,
|
|
478
|
+
cardsRootDir,
|
|
479
|
+
notifyChannel,
|
|
480
|
+
board,
|
|
481
|
+
cardStore,
|
|
482
|
+
filesArtifacts,
|
|
483
|
+
chatsArtifacts,
|
|
484
|
+
cardStoreRef,
|
|
485
|
+
outputsStoreRef,
|
|
486
|
+
taskExecutorPath,
|
|
487
|
+
chatHandlerPath,
|
|
488
|
+
inferenceAdapterPath,
|
|
489
|
+
notification: makeNotificationState(),
|
|
490
|
+
pipeServer: null,
|
|
491
|
+
initialized: false,
|
|
492
|
+
cardsBootstrapped: false,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
432
495
|
|
|
433
|
-
|
|
434
|
-
|
|
496
|
+
const baseCtx = makeBoardContext(
|
|
497
|
+
'base',
|
|
498
|
+
boardDir,
|
|
499
|
+
runtimeOutDir,
|
|
500
|
+
tmpCardsDir,
|
|
501
|
+
configuredTaskExecutorPath,
|
|
502
|
+
configuredChatHandlerPath,
|
|
503
|
+
configuredInferenceAdapterPath,
|
|
504
|
+
);
|
|
435
505
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
506
|
+
const gandalfCtx = configuredGandalfTaskExecutorPath && tmpGandalfCardsDir
|
|
507
|
+
? makeBoardContext(
|
|
508
|
+
'gandalf',
|
|
509
|
+
gandalfRuntimeDir,
|
|
510
|
+
gandalfRuntimeOutDir,
|
|
511
|
+
tmpGandalfCardsDir,
|
|
512
|
+
configuredGandalfTaskExecutorPath,
|
|
513
|
+
configuredGandalfChatHandlerPath,
|
|
514
|
+
configuredGandalfInferenceAdapterPath,
|
|
515
|
+
)
|
|
516
|
+
: null;
|
|
441
517
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
518
|
+
function cardFilesFromDir(dirPath, outMap) {
|
|
519
|
+
outMap.clear();
|
|
520
|
+
if (!dirPath || !fs.existsSync(dirPath)) return [];
|
|
521
|
+
const out = [];
|
|
522
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
523
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.json')) continue;
|
|
524
|
+
const full = path.join(dirPath, entry.name);
|
|
525
|
+
try {
|
|
526
|
+
const card = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
527
|
+
if (!card || typeof card.id !== 'string') continue;
|
|
528
|
+
outMap.set(card.id, full);
|
|
529
|
+
out.push(card);
|
|
530
|
+
} catch {
|
|
531
|
+
// ignore malformed files
|
|
532
|
+
}
|
|
446
533
|
}
|
|
447
|
-
|
|
448
|
-
return null;
|
|
534
|
+
return out;
|
|
449
535
|
}
|
|
450
536
|
|
|
451
|
-
|
|
537
|
+
function toExecutionRef(scriptPath, extraObj) {
|
|
538
|
+
if (!scriptPath) return null;
|
|
539
|
+
return {
|
|
540
|
+
howToRun: 'local-node',
|
|
541
|
+
whatToRun: `::fs-path::${scriptPath}`,
|
|
542
|
+
extra: extraObj,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
452
545
|
|
|
453
|
-
|
|
454
|
-
|
|
546
|
+
async function ensurePipeConsumer(ctx) {
|
|
547
|
+
if (!ctx || ctx.pipeServer) return;
|
|
548
|
+
const pipePath = namedPipePath(ctx.notifyChannel);
|
|
549
|
+
if (process.platform !== 'win32' && fs.existsSync(pipePath)) {
|
|
550
|
+
try { fs.rmSync(pipePath, { force: true }); } catch { /* best-effort */ }
|
|
551
|
+
}
|
|
552
|
+
const server = net.createServer((socket) => {
|
|
553
|
+
ctx.notification.sockets.add(socket);
|
|
554
|
+
socket.on('close', () => ctx.notification.sockets.delete(socket));
|
|
555
|
+
let buf = '';
|
|
556
|
+
socket.on('data', (chunk) => {
|
|
557
|
+
buf += chunk.toString('utf-8');
|
|
558
|
+
while (true) {
|
|
559
|
+
const i = buf.indexOf('\n');
|
|
560
|
+
if (i < 0) break;
|
|
561
|
+
const line = buf.slice(0, i).trim();
|
|
562
|
+
buf = buf.slice(i + 1);
|
|
563
|
+
if (!line) continue;
|
|
564
|
+
try {
|
|
565
|
+
const msg = JSON.parse(line);
|
|
566
|
+
const n = msg?.notification ?? msg;
|
|
567
|
+
appendNotification(ctx.notification, n);
|
|
568
|
+
} catch {
|
|
569
|
+
// ignore malformed lines
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
broadcastToSseClients();
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
await new Promise((resolve, reject) => {
|
|
576
|
+
server.once('error', reject);
|
|
577
|
+
server.listen(pipePath, () => resolve());
|
|
578
|
+
});
|
|
579
|
+
ctx.pipeServer = { server, pipePath };
|
|
455
580
|
}
|
|
456
581
|
|
|
457
582
|
function ensureCardStorageDirs(cardId) {
|
|
458
583
|
const safeCardId = String(cardId || '').replace(/[^a-zA-Z0-9_-]/g, '_') || 'unknown-card';
|
|
459
584
|
const baseDir = isGandalfCard(cardId) ? tmpGandalfCardsDir : tmpCardsDir;
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
const chatsDir = path.join(cardDir, 'chats');
|
|
585
|
+
const filesDir = path.join(baseDir, 'files', safeCardId);
|
|
586
|
+
const chatsDir = path.join(baseDir, 'chats', safeCardId);
|
|
463
587
|
fs.mkdirSync(filesDir, { recursive: true });
|
|
464
588
|
fs.mkdirSync(chatsDir, { recursive: true });
|
|
465
|
-
return { filesDir, chatsDir };
|
|
589
|
+
return { filesDir, chatsDir, safeCardId };
|
|
466
590
|
}
|
|
467
591
|
|
|
468
|
-
function
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
592
|
+
function artifactsStores(cardId) {
|
|
593
|
+
const ctx = isGandalfCard(cardId) ? gandalfCtx : baseCtx;
|
|
594
|
+
return {
|
|
595
|
+
files: ctx ? ctx.filesArtifacts : null,
|
|
596
|
+
chats: ctx ? ctx.chatsArtifacts : null,
|
|
597
|
+
};
|
|
473
598
|
}
|
|
474
599
|
|
|
475
|
-
function
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
.replace(/[^a-z0-9_-]/g, '_')
|
|
480
|
-
.replace(/_+/g, '_')
|
|
481
|
-
.replace(/^_+|_+$/g, '');
|
|
482
|
-
return normalized || 'file';
|
|
600
|
+
function chatArtifactsForCard(cardId) {
|
|
601
|
+
const stores = artifactsStores(cardId);
|
|
602
|
+
if (!stores.chats) return null;
|
|
603
|
+
return createChatArtifactsStore(stores.chats, { indexFileName: '.index.json' });
|
|
483
604
|
}
|
|
484
605
|
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return
|
|
606
|
+
function fileArtifactsForCard(cardId) {
|
|
607
|
+
const stores = artifactsStores(cardId);
|
|
608
|
+
if (!stores.files) return null;
|
|
609
|
+
return createFileArtifactsStore(stores.files);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function cardFileMetadataStore() {
|
|
613
|
+
return createCardFileMetadataStore();
|
|
489
614
|
}
|
|
490
615
|
|
|
491
616
|
function parseLeadingSerial(fileName) {
|
|
@@ -493,56 +618,19 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
493
618
|
return m ? parseInt(m[1], 10) : 0;
|
|
494
619
|
}
|
|
495
620
|
|
|
496
|
-
function
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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;
|
|
621
|
+
function normalizeDisplayFileName(name) {
|
|
622
|
+
const input = String(name || '').trim();
|
|
623
|
+
if (!input) return 'upload.bin';
|
|
624
|
+
const base = path.basename(input);
|
|
625
|
+
return base || 'upload.bin';
|
|
525
626
|
}
|
|
526
627
|
|
|
527
628
|
function shellQuote(s) {
|
|
528
629
|
return '"' + String(s).replace(/"/g, '\\"') + '"';
|
|
529
630
|
}
|
|
530
631
|
|
|
531
|
-
function
|
|
532
|
-
|
|
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
|
-
});
|
|
632
|
+
function runCli(_args) {
|
|
633
|
+
throw new Error('CLI path is no longer used by server runtime. Use board public APIs.');
|
|
546
634
|
}
|
|
547
635
|
|
|
548
636
|
function clearDirContents(dirPath) {
|
|
@@ -559,78 +647,97 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
559
647
|
}
|
|
560
648
|
|
|
561
649
|
function readInventory() {
|
|
562
|
-
|
|
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));
|
|
650
|
+
return [...cardPathById.entries()].map(([cardId, cardFilePath]) => ({ cardId, cardFilePath }));
|
|
569
651
|
}
|
|
570
652
|
|
|
571
653
|
function readGandalfInventory() {
|
|
572
|
-
|
|
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));
|
|
654
|
+
return [...gandalfCardPathById.entries()].map(([cardId, cardFilePath]) => ({ cardId, cardFilePath }));
|
|
579
655
|
}
|
|
580
656
|
|
|
581
657
|
function readStatusSnapshot() {
|
|
582
|
-
const base =
|
|
583
|
-
const
|
|
584
|
-
if (!base && !
|
|
585
|
-
if (!
|
|
586
|
-
if (!base) return
|
|
587
|
-
|
|
658
|
+
const base = baseCtx.notification.status;
|
|
659
|
+
const side = gandalfCtx ? gandalfCtx.notification.status : null;
|
|
660
|
+
if (!base && !side) return null;
|
|
661
|
+
if (!side) return base;
|
|
662
|
+
if (!base) return side;
|
|
663
|
+
|
|
664
|
+
const baseCards = Array.isArray(base.cards) ? base.cards : [];
|
|
665
|
+
const sideCards = Array.isArray(side.cards) ? side.cards : [];
|
|
666
|
+
const mergedCards = [...baseCards, ...sideCards];
|
|
667
|
+
|
|
668
|
+
const sum = (obj, k) => Number(obj?.summary?.[k] || 0);
|
|
669
|
+
return {
|
|
670
|
+
...base,
|
|
671
|
+
cards: mergedCards,
|
|
672
|
+
summary: {
|
|
673
|
+
...(base.summary || {}),
|
|
674
|
+
card_count: mergedCards.length,
|
|
675
|
+
completed: sum(base, 'completed') + sum(side, 'completed'),
|
|
676
|
+
eligible: sum(base, 'eligible') + sum(side, 'eligible'),
|
|
677
|
+
pending: sum(base, 'pending') + sum(side, 'pending'),
|
|
678
|
+
blocked: sum(base, 'blocked') + sum(side, 'blocked'),
|
|
679
|
+
unresolved: sum(base, 'unresolved') + sum(side, 'unresolved'),
|
|
680
|
+
failed: sum(base, 'failed') + sum(side, 'failed'),
|
|
681
|
+
in_progress: sum(base, 'in_progress') + sum(side, 'in_progress'),
|
|
682
|
+
orphan_cards: sum(base, 'orphan_cards') + sum(side, 'orphan_cards'),
|
|
683
|
+
},
|
|
684
|
+
};
|
|
588
685
|
}
|
|
589
686
|
|
|
590
687
|
function readCardDefinitions() {
|
|
591
|
-
|
|
592
|
-
if (!
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (!entry || !entry.cardId || !entry.cardFilePath) continue;
|
|
597
|
-
if (!fs.existsSync(entry.cardFilePath)) continue;
|
|
598
|
-
out.push(readJson(entry.cardFilePath));
|
|
688
|
+
const fromCtx = (ctx, fallbackDir, fallbackMap) => {
|
|
689
|
+
if (!ctx || !ctx.cardStore) return cardFilesFromDir(fallbackDir, fallbackMap);
|
|
690
|
+
const result = ctx.cardStore.get({});
|
|
691
|
+
if (result.status !== 'success' || !Array.isArray(result.data?.cards)) {
|
|
692
|
+
return cardFilesFromDir(fallbackDir, fallbackMap);
|
|
599
693
|
}
|
|
600
|
-
return
|
|
601
|
-
}
|
|
602
|
-
|
|
694
|
+
return result.data.cards;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const base = fromCtx(baseCtx, tmpCardsDir, cardPathById);
|
|
698
|
+
const side = gandalfCtx ? fromCtx(gandalfCtx, tmpGandalfCardsDir, gandalfCardPathById) : [];
|
|
699
|
+
return [...base, ...side];
|
|
603
700
|
}
|
|
604
701
|
|
|
605
702
|
function readCardRuntimeArtifacts() {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
703
|
+
const out = {};
|
|
704
|
+
for (const [cardId, values] of Object.entries(baseCtx.notification.computedValues)) {
|
|
705
|
+
const card = baseCtx.notification.cards[cardId];
|
|
706
|
+
out[cardId] = {
|
|
707
|
+
schema_version: 'v1',
|
|
708
|
+
card_id: cardId,
|
|
709
|
+
card_data: card?.card_data ?? {},
|
|
710
|
+
computed_values: values ?? {},
|
|
711
|
+
fetched_sources: {},
|
|
712
|
+
requires: {},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
if (gandalfCtx) {
|
|
716
|
+
for (const [cardId, values] of Object.entries(gandalfCtx.notification.computedValues)) {
|
|
717
|
+
const card = gandalfCtx.notification.cards[cardId];
|
|
718
|
+
out[cardId] = {
|
|
719
|
+
schema_version: 'v1',
|
|
720
|
+
card_id: cardId,
|
|
721
|
+
card_data: card?.card_data ?? {},
|
|
722
|
+
computed_values: values ?? {},
|
|
723
|
+
fetched_sources: {},
|
|
724
|
+
requires: {},
|
|
725
|
+
};
|
|
614
726
|
}
|
|
615
|
-
return out;
|
|
616
727
|
}
|
|
617
|
-
return
|
|
728
|
+
return out;
|
|
618
729
|
}
|
|
619
730
|
|
|
620
731
|
function readSourcePayloads(cardDefinition) {
|
|
621
732
|
const out = {};
|
|
622
733
|
if (!cardDefinition || !Array.isArray(cardDefinition.source_defs)) return out;
|
|
623
734
|
|
|
735
|
+
const ctx = isGandalfCard(cardDefinition.id) ? gandalfCtx : baseCtx;
|
|
736
|
+
const dataObjects = ctx ? ctx.notification.dataObjects : {};
|
|
624
737
|
for (const sourceDef of cardDefinition.source_defs) {
|
|
625
|
-
if (!sourceDef || !sourceDef.bindTo
|
|
626
|
-
|
|
627
|
-
|
|
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;
|
|
738
|
+
if (!sourceDef || !sourceDef.bindTo) continue;
|
|
739
|
+
if (Object.prototype.hasOwnProperty.call(dataObjects, sourceDef.bindTo)) {
|
|
740
|
+
out[sourceDef.bindTo] = dataObjects[sourceDef.bindTo];
|
|
634
741
|
}
|
|
635
742
|
}
|
|
636
743
|
|
|
@@ -638,48 +745,17 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
638
745
|
}
|
|
639
746
|
|
|
640
747
|
function readDataObjectsByToken() {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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;
|
|
748
|
+
return {
|
|
749
|
+
...(baseCtx.notification.dataObjects || {}),
|
|
750
|
+
...(gandalfCtx ? gandalfCtx.notification.dataObjects : {}),
|
|
751
|
+
};
|
|
657
752
|
}
|
|
658
753
|
|
|
659
754
|
function readChatSignal(cardId) {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
663
|
-
|
|
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 };
|
|
755
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
756
|
+
const chatStore = chatArtifactsForCard(cardId);
|
|
757
|
+
if (!chatStore) return { count: 0, latest_mtime_ms: 0, processing: false };
|
|
758
|
+
return chatStore.readSignal(safeCardId);
|
|
683
759
|
}
|
|
684
760
|
|
|
685
761
|
function buildPublishedRuntimePayload() {
|
|
@@ -773,192 +849,115 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
773
849
|
return resolved;
|
|
774
850
|
}
|
|
775
851
|
|
|
776
|
-
function
|
|
777
|
-
if (!
|
|
778
|
-
if (
|
|
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
|
+
async function initContext(ctx, taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
|
|
853
|
+
if (!ctx) return;
|
|
854
|
+
if (ctx.initialized) return;
|
|
852
855
|
|
|
856
|
+
const te = resolveTaskExecutorPath(taskExecutorPathParam || ctx.taskExecutorPath);
|
|
857
|
+
const ch = resolveChatHandlerPath(chatHandlerPathParam || ctx.chatHandlerPath);
|
|
858
|
+
const ia = resolveInferenceAdapterPath(inferenceAdapterPathParam || ctx.inferenceAdapterPath);
|
|
853
859
|
const boardSetupRoot = path.dirname(boardDir);
|
|
854
|
-
const
|
|
860
|
+
const extra = {
|
|
855
861
|
boardSetupRoot,
|
|
856
862
|
boardId,
|
|
857
|
-
boardRuntimeDir:
|
|
858
|
-
runtimeStatusDir: path.relative(boardSetupRoot,
|
|
859
|
-
cardsDir:
|
|
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),
|
|
863
|
+
boardRuntimeDir: path.relative(boardSetupRoot, ctx.runtimeDir),
|
|
864
|
+
runtimeStatusDir: path.relative(boardSetupRoot, ctx.outputsDir),
|
|
865
|
+
cardsDir: path.relative(boardSetupRoot, ctx.cardsRootDir),
|
|
905
866
|
...(serverUrl ? { serverUrl } : {}),
|
|
906
867
|
...(configuredBoardLiveCardsCliJs ? { boardLiveCardsCliJs: configuredBoardLiveCardsCliJs } : {}),
|
|
907
868
|
...(configuredStepMachineCliPath ? { stepMachineCliPath: configuredStepMachineCliPath } : {}),
|
|
908
|
-
}
|
|
869
|
+
};
|
|
909
870
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
871
|
+
const params = {
|
|
872
|
+
cardStoreRef: ctx.cardStoreRef,
|
|
873
|
+
outputsStoreRef: ctx.outputsStoreRef,
|
|
874
|
+
};
|
|
875
|
+
const body = {};
|
|
876
|
+
body['task-executor-ref'] = toExecutionRef(te, extra);
|
|
877
|
+
if (ch) body['chat-handler-ref'] = toExecutionRef(ch, extra);
|
|
878
|
+
if (ia) body['inference-adapter-ref'] = toExecutionRef(ia, extra);
|
|
879
|
+
|
|
880
|
+
const initResult = ctx.board.init({ params, body });
|
|
881
|
+
if (initResult.status !== 'success') {
|
|
882
|
+
const err = new Error(initResult.error || `init failed for ${ctx.label}`);
|
|
883
|
+
err.statusCode = 500;
|
|
884
|
+
throw err;
|
|
913
885
|
}
|
|
886
|
+
await ensurePipeConsumer(ctx);
|
|
887
|
+
ctx.initialized = true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function upsertCardsFromDir(ctx, outMap) {
|
|
891
|
+
if (!ctx) return;
|
|
892
|
+
if (ctx.cardsBootstrapped) return;
|
|
893
|
+
const cards = cardFilesFromDir(ctx.cardsRootDir, outMap);
|
|
894
|
+
for (const card of cards) {
|
|
895
|
+
const setResult = ctx.cardStore.set({ body: card });
|
|
896
|
+
if (setResult.status !== 'success') continue;
|
|
897
|
+
ctx.board.upsertCard({ params: { cardId: card.id, restart: true } });
|
|
898
|
+
}
|
|
899
|
+
await ctx.board.processAccumulatedEvents({});
|
|
900
|
+
ctx.cardsBootstrapped = true;
|
|
914
901
|
}
|
|
915
902
|
|
|
916
|
-
function
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
runCli(['upsert-card', '--rg', gandalfRuntimeDir, '--card-glob', path.join(tmpGandalfCardsDir, '*.json')]);
|
|
922
|
-
_refreshGandalfCardCache();
|
|
903
|
+
async function initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam) {
|
|
904
|
+
await initContext(baseCtx, taskExecutorPathParam, chatHandlerPathParam, inferenceAdapterPathParam);
|
|
905
|
+
if (gandalfCtx && gandalfCtx.taskExecutorPath) {
|
|
906
|
+
await initContext(gandalfCtx, gandalfCtx.taskExecutorPath, gandalfCtx.chatHandlerPath, gandalfCtx.inferenceAdapterPath);
|
|
907
|
+
}
|
|
923
908
|
}
|
|
924
909
|
|
|
925
|
-
function
|
|
926
|
-
|
|
910
|
+
async function bootstrapBoard() {
|
|
911
|
+
await initBoardAndSetup();
|
|
912
|
+
await upsertCardsFromDir(baseCtx, cardPathById);
|
|
913
|
+
if (gandalfCtx) await upsertCardsFromDir(gandalfCtx, gandalfCardPathById);
|
|
927
914
|
}
|
|
928
915
|
|
|
929
|
-
function
|
|
930
|
-
|
|
931
|
-
bootstrapCards();
|
|
932
|
-
bootstrapGandalfCards();
|
|
916
|
+
function cardContextForCard(cardId) {
|
|
917
|
+
return isGandalfCard(cardId) ? gandalfCtx : baseCtx;
|
|
933
918
|
}
|
|
934
919
|
|
|
935
|
-
function
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
return found ? found.cardFilePath : null;
|
|
920
|
+
function readCardFromStore(cardId) {
|
|
921
|
+
const ctx = cardContextForCard(cardId);
|
|
922
|
+
if (!ctx) return null;
|
|
923
|
+
const result = ctx.cardStore.get({ params: { id: cardId } });
|
|
924
|
+
if (result.status !== 'success') return null;
|
|
925
|
+
const cards = Array.isArray(result.data?.cards) ? result.data.cards : [];
|
|
926
|
+
return cards.length > 0 ? cards[0] : null;
|
|
943
927
|
}
|
|
944
928
|
|
|
945
929
|
function mutateCard(cardId, updateFn, opts) {
|
|
946
930
|
const options = opts && typeof opts === 'object' ? opts : {};
|
|
947
931
|
const syncBoard = options.syncBoard !== false;
|
|
948
|
-
const
|
|
949
|
-
if (!
|
|
932
|
+
const ctx = cardContextForCard(cardId);
|
|
933
|
+
if (!ctx) {
|
|
934
|
+
const err = new Error(`Card not found: ${cardId}`);
|
|
935
|
+
err.statusCode = 404;
|
|
936
|
+
throw err;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const card = readCardFromStore(cardId);
|
|
940
|
+
if (!card || typeof card !== 'object') {
|
|
950
941
|
const err = new Error(`Card not found: ${cardId}`);
|
|
951
942
|
err.statusCode = 404;
|
|
952
943
|
throw err;
|
|
953
944
|
}
|
|
954
945
|
|
|
955
|
-
const card = readJson(cardPath);
|
|
956
946
|
const nextCard = updateFn(card) || card;
|
|
957
|
-
|
|
947
|
+
const setResult = ctx.cardStore.set({ body: nextCard });
|
|
948
|
+
if (setResult.status !== 'success') {
|
|
949
|
+
const err = new Error(setResult.error || `Failed to persist card: ${cardId}`);
|
|
950
|
+
err.statusCode = 500;
|
|
951
|
+
throw err;
|
|
952
|
+
}
|
|
958
953
|
|
|
959
954
|
if (syncBoard) {
|
|
960
|
-
const
|
|
961
|
-
|
|
955
|
+
const upsertResult = ctx.board.upsertCard({ params: { cardId, restart: true } });
|
|
956
|
+
if (upsertResult.status !== 'success') {
|
|
957
|
+
const err = new Error(upsertResult.error || `Failed to upsert card: ${cardId}`);
|
|
958
|
+
err.statusCode = 500;
|
|
959
|
+
throw err;
|
|
960
|
+
}
|
|
962
961
|
}
|
|
963
962
|
}
|
|
964
963
|
|
|
@@ -1028,52 +1027,39 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1028
1027
|
}
|
|
1029
1028
|
|
|
1030
1029
|
function clearChatRecords(cardId) {
|
|
1031
|
-
const {
|
|
1032
|
-
|
|
1030
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1031
|
+
const chatStore = chatArtifactsForCard(cardId);
|
|
1032
|
+
if (!chatStore) return;
|
|
1033
|
+
chatStore.clear(safeCardId);
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
1035
|
-
function
|
|
1036
|
+
function readCardStoredFileNames(cardId) {
|
|
1036
1037
|
const names = [];
|
|
1037
|
-
|
|
1038
1038
|
try {
|
|
1039
|
-
const
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
for (const entry of files) {
|
|
1044
|
-
if (entry && typeof entry.stored_name === 'string') names.push(entry.stored_name);
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1039
|
+
const card = readCardFromStore(cardId);
|
|
1040
|
+
if (!card) return names;
|
|
1041
|
+
const metadata = cardFileMetadataStore().read(card && card.card_data ? card.card_data : null);
|
|
1042
|
+
for (const entry of metadata) names.push(entry.stored_name);
|
|
1047
1043
|
} catch {
|
|
1048
|
-
// ignore malformed card file
|
|
1044
|
+
// ignore malformed card file
|
|
1049
1045
|
}
|
|
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);
|
|
1046
|
+
return names;
|
|
1060
1047
|
}
|
|
1061
1048
|
|
|
1062
1049
|
function nextChatStoredName(cardId, role) {
|
|
1063
|
-
const {
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
: [];
|
|
1067
|
-
const serial = nextSerialFromNames(names);
|
|
1050
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1051
|
+
const chatStore = chatArtifactsForCard(cardId);
|
|
1052
|
+
const serial = chatStore ? chatStore.nextSerial(safeCardId) : 1;
|
|
1068
1053
|
const safeRole = String(role || 'system').toLowerCase().replace(/[^a-z0-9_-]/g, '_') || 'system';
|
|
1069
1054
|
return `${String(serial).padStart(3, '0')}_${safeRole}.txt`;
|
|
1070
1055
|
}
|
|
1071
1056
|
|
|
1072
1057
|
function writeChatRecord(cardId, role, text, files) {
|
|
1073
1058
|
const now = new Date().toISOString();
|
|
1074
|
-
const {
|
|
1059
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1060
|
+
const stores = artifactsStores(cardId);
|
|
1075
1061
|
const outName = nextChatStoredName(cardId, role || 'system');
|
|
1076
|
-
const
|
|
1062
|
+
const artifactKey = `${safeCardId}/${outName}`;
|
|
1077
1063
|
|
|
1078
1064
|
const lines = [];
|
|
1079
1065
|
const msg = typeof text === 'string' ? text.trim() : '';
|
|
@@ -1091,7 +1077,18 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1091
1077
|
}
|
|
1092
1078
|
}
|
|
1093
1079
|
|
|
1094
|
-
|
|
1080
|
+
if (stores.chats) stores.chats.putText(artifactKey, `${lines.join('\n')}\n`);
|
|
1081
|
+
const serial = parseLeadingSerial(outName);
|
|
1082
|
+
const chatStore = chatArtifactsForCard(cardId);
|
|
1083
|
+
if (chatStore) {
|
|
1084
|
+
chatStore.appendIndexRecord(safeCardId, {
|
|
1085
|
+
serial,
|
|
1086
|
+
role: role || 'system',
|
|
1087
|
+
stored_name: outName,
|
|
1088
|
+
path: `${cardId}/chats/${outName}`,
|
|
1089
|
+
updated_at: now,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1095
1092
|
return {
|
|
1096
1093
|
at: now,
|
|
1097
1094
|
role: role || 'system',
|
|
@@ -1102,48 +1099,31 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1102
1099
|
}
|
|
1103
1100
|
|
|
1104
1101
|
function readChatRecords(cardId) {
|
|
1105
|
-
const {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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;
|
|
1102
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1103
|
+
const chatStore = chatArtifactsForCard(cardId);
|
|
1104
|
+
if (!chatStore) return [];
|
|
1105
|
+
return chatStore.readRecords(safeCardId).map((row) => ({
|
|
1106
|
+
...row,
|
|
1107
|
+
path: `${cardId}/chats/${row.stored_name}`,
|
|
1108
|
+
}));
|
|
1131
1109
|
}
|
|
1132
1110
|
|
|
1133
1111
|
function persistUploadedFile(cardId, requestedName, contentType, buffer) {
|
|
1134
|
-
const {
|
|
1112
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1113
|
+
const stores = artifactsStores(cardId);
|
|
1135
1114
|
const displayName = normalizeDisplayFileName(requestedName);
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1115
|
+
const fileStore = fileArtifactsForCard(cardId);
|
|
1116
|
+
const storedName = fileStore
|
|
1117
|
+
? fileStore.allocateStoredName(safeCardId, displayName, {
|
|
1118
|
+
seedNames: readCardStoredFileNames(cardId),
|
|
1119
|
+
maxLen: MAX_STORED_FILE_NAME_LEN,
|
|
1120
|
+
})
|
|
1121
|
+
: `${String(Date.now())}-${displayName}`;
|
|
1122
|
+
|
|
1123
|
+
if (stores.files) {
|
|
1124
|
+
stores.files.putBytes(`${safeCardId}/${storedName}`, new Uint8Array(buffer), contentType || 'application/octet-stream');
|
|
1142
1125
|
}
|
|
1143
1126
|
|
|
1144
|
-
const targetPath = path.join(filesDir, storedName);
|
|
1145
|
-
fs.writeFileSync(targetPath, buffer);
|
|
1146
|
-
|
|
1147
1127
|
return {
|
|
1148
1128
|
name: displayName,
|
|
1149
1129
|
stored_name: storedName,
|
|
@@ -1163,6 +1143,7 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1163
1143
|
// runtimeStatusDir — relative: 'runtime-out'
|
|
1164
1144
|
// cardsDir — relative: 'surface/tmp-cards' (or 'surface/tmp-gandalf-cards')
|
|
1165
1145
|
// chatDir — relative (from cardsDir): e.g. 'card-portfolio/chats'
|
|
1146
|
+
// chatProcessingMarkerKey — relative marker key in chats artifacts store, e.g. 'card-portfolio/.processing'
|
|
1166
1147
|
// lastChatFile — filename of the just-written user message, e.g. '001_user.txt'
|
|
1167
1148
|
// boardLiveCardsCliJs — absolute path to board-live-cards-cli.js (if configured)
|
|
1168
1149
|
// stepMachineCliPath — absolute path to step-machine-cli.js (if configured)
|
|
@@ -1175,14 +1156,25 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1175
1156
|
const handlerCmd = fs.readFileSync(handlerFile, 'utf-8').trim();
|
|
1176
1157
|
if (!handlerCmd) return;
|
|
1177
1158
|
const boardSetupRoot = path.dirname(boardDir);
|
|
1159
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1160
|
+
const stores = artifactsStores(cardId);
|
|
1161
|
+
const processingMarkerKey = `${safeCardId}/.processing`;
|
|
1178
1162
|
const processingFile = path.join(chatsDir, '.processing');
|
|
1179
|
-
try {
|
|
1163
|
+
try {
|
|
1164
|
+
if (stores.chats) {
|
|
1165
|
+
stores.chats.putText(processingMarkerKey, '', 'text/plain; charset=utf-8');
|
|
1166
|
+
} else {
|
|
1167
|
+
fs.mkdirSync(chatsDir, { recursive: true });
|
|
1168
|
+
fs.writeFileSync(processingFile, '', 'utf-8');
|
|
1169
|
+
}
|
|
1170
|
+
} catch {}
|
|
1180
1171
|
const extra = Buffer.from(JSON.stringify({
|
|
1181
1172
|
boardSetupRoot,
|
|
1182
1173
|
boardRuntimeDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeDir : boardDir),
|
|
1183
1174
|
runtimeStatusDir: path.relative(boardSetupRoot, isGandalf ? gandalfRuntimeOutDir : runtimeOutDir),
|
|
1184
1175
|
cardsDir: path.relative(boardSetupRoot, isGandalf ? tmpGandalfCardsDir : tmpCardsDir),
|
|
1185
1176
|
chatDir: chatsDir,
|
|
1177
|
+
chatProcessingMarkerKey: processingMarkerKey,
|
|
1186
1178
|
lastChatFile,
|
|
1187
1179
|
...(serverUrl ? { serverUrl } : {}),
|
|
1188
1180
|
...(configuredBoardLiveCardsCliJs ? { boardLiveCardsCliJs: configuredBoardLiveCardsCliJs } : {}),
|
|
@@ -1192,7 +1184,6 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1192
1184
|
const proc = spawn(handlerCmd, [
|
|
1193
1185
|
'--boardId', boardId, '--cardId', String(cardId),
|
|
1194
1186
|
'--extraEncJson', extra,
|
|
1195
|
-
'--cleanOnExit', processingFile,
|
|
1196
1187
|
], {
|
|
1197
1188
|
shell: true,
|
|
1198
1189
|
stdio: 'ignore',
|
|
@@ -1200,7 +1191,13 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1200
1191
|
proc.unref();
|
|
1201
1192
|
console.log(`[chat-handler] invoked for card "${cardId}" (boardId: "${boardId}")`);
|
|
1202
1193
|
} catch (err) {
|
|
1203
|
-
try {
|
|
1194
|
+
try {
|
|
1195
|
+
if (stores.chats) {
|
|
1196
|
+
stores.chats.remove(processingMarkerKey);
|
|
1197
|
+
} else {
|
|
1198
|
+
fs.unlinkSync(processingFile);
|
|
1199
|
+
}
|
|
1200
|
+
} catch {}
|
|
1204
1201
|
console.warn(`[chat-handler] spawn failed for card "${cardId}":`, (err && err.message) || String(err));
|
|
1205
1202
|
}
|
|
1206
1203
|
}
|
|
@@ -1252,32 +1249,10 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1252
1249
|
}
|
|
1253
1250
|
|
|
1254
1251
|
if (actionType === 'file-upload') {
|
|
1255
|
-
const 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
|
-
: [];
|
|
1252
|
+
const files = cardFileMetadataStore().normalizeIncoming(payload && payload.files, now);
|
|
1271
1253
|
|
|
1272
1254
|
if (files.length > 0) {
|
|
1273
|
-
|
|
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;
|
|
1255
|
+
cardFileMetadataStore().merge(cardData, files);
|
|
1281
1256
|
}
|
|
1282
1257
|
|
|
1283
1258
|
return card;
|
|
@@ -1323,6 +1298,18 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1323
1298
|
return Buffer.concat(chunks);
|
|
1324
1299
|
}
|
|
1325
1300
|
|
|
1301
|
+
function broadcastToSseClients() {
|
|
1302
|
+
const payload = buildPublishedRuntimePayload();
|
|
1303
|
+
const data = `data: ${JSON.stringify(payload)}\n\n`;
|
|
1304
|
+
for (const client of sseClients) {
|
|
1305
|
+
try {
|
|
1306
|
+
client.write(data);
|
|
1307
|
+
} catch {
|
|
1308
|
+
sseClients.delete(client);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1326
1313
|
function handleSse(req, res) {
|
|
1327
1314
|
res.writeHead(200, {
|
|
1328
1315
|
...corsHeaders,
|
|
@@ -1331,45 +1318,16 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1331
1318
|
Connection: 'keep-alive',
|
|
1332
1319
|
});
|
|
1333
1320
|
|
|
1334
|
-
|
|
1335
|
-
|
|
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);
|
|
1321
|
+
sseClients.add(res);
|
|
1322
|
+
res.write(`data: ${JSON.stringify(buildPublishedRuntimePayload())}\n\n`);
|
|
1349
1323
|
|
|
1350
|
-
const
|
|
1351
|
-
try {
|
|
1352
|
-
|
|
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);
|
|
1324
|
+
const keepAlive = setInterval(() => {
|
|
1325
|
+
try { res.write(': keepalive\n\n'); } catch { /* ignore */ }
|
|
1326
|
+
}, 15_000);
|
|
1370
1327
|
|
|
1371
1328
|
req.on('close', () => {
|
|
1372
|
-
clearInterval(
|
|
1329
|
+
clearInterval(keepAlive);
|
|
1330
|
+
sseClients.delete(res);
|
|
1373
1331
|
res.end();
|
|
1374
1332
|
});
|
|
1375
1333
|
}
|
|
@@ -1387,25 +1345,25 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1387
1345
|
if (method === 'GET' && p === `${apiBasePath}/init-board`) {
|
|
1388
1346
|
const taskExecutorPathParam = url.searchParams.get('taskExecutorPath') || '';
|
|
1389
1347
|
const chatHandlerPathParam = url.searchParams.get('chatHandlerPath') || '';
|
|
1390
|
-
initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam);
|
|
1348
|
+
await initBoardAndSetup(taskExecutorPathParam, chatHandlerPathParam);
|
|
1391
1349
|
json(res, 200, buildPublishedRuntimePayload());
|
|
1392
1350
|
return true;
|
|
1393
1351
|
}
|
|
1394
1352
|
|
|
1395
1353
|
if (method === 'GET' && p === `${apiBasePath}/bootstrap-cards`) {
|
|
1396
|
-
|
|
1354
|
+
await bootstrapBoard();
|
|
1397
1355
|
json(res, 200, buildPublishedRuntimePayload());
|
|
1398
1356
|
return true;
|
|
1399
1357
|
}
|
|
1400
1358
|
|
|
1401
1359
|
if (method === 'GET' && p === `${apiBasePath}/bootstrap`) {
|
|
1402
|
-
bootstrapBoard();
|
|
1360
|
+
await bootstrapBoard();
|
|
1403
1361
|
json(res, 200, buildPublishedRuntimePayload());
|
|
1404
1362
|
return true;
|
|
1405
1363
|
}
|
|
1406
1364
|
|
|
1407
1365
|
if (method === 'GET' && p === `${apiBasePath}/sse`) {
|
|
1408
|
-
bootstrapBoard();
|
|
1366
|
+
await bootstrapBoard();
|
|
1409
1367
|
handleSse(req, res);
|
|
1410
1368
|
return true;
|
|
1411
1369
|
}
|
|
@@ -1417,27 +1375,29 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1417
1375
|
|
|
1418
1376
|
const cardMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)$`));
|
|
1419
1377
|
if (method === 'PATCH' && cardMatch) {
|
|
1420
|
-
bootstrapBoard();
|
|
1378
|
+
await bootstrapBoard();
|
|
1421
1379
|
const cardId = decodeURIComponent(cardMatch[1]);
|
|
1422
1380
|
const body = await readJsonBody(req);
|
|
1423
1381
|
patchCard(cardId, body);
|
|
1382
|
+
broadcastToSseClients();
|
|
1424
1383
|
json(res, 200, { ok: true });
|
|
1425
1384
|
return true;
|
|
1426
1385
|
}
|
|
1427
1386
|
|
|
1428
1387
|
const cardActionMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/actions$`));
|
|
1429
1388
|
if (method === 'POST' && cardActionMatch) {
|
|
1430
|
-
bootstrapBoard();
|
|
1389
|
+
await bootstrapBoard();
|
|
1431
1390
|
const cardId = decodeURIComponent(cardActionMatch[1]);
|
|
1432
1391
|
const body = await readJsonBody(req);
|
|
1433
1392
|
applyCardAction(cardId, body && body.actionType, body && body.payload);
|
|
1393
|
+
broadcastToSseClients();
|
|
1434
1394
|
json(res, 200, { ok: true });
|
|
1435
1395
|
return true;
|
|
1436
1396
|
}
|
|
1437
1397
|
|
|
1438
1398
|
const cardChatsMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/chats$`));
|
|
1439
1399
|
if (method === 'GET' && cardChatsMatch) {
|
|
1440
|
-
bootstrapBoard();
|
|
1400
|
+
await bootstrapBoard();
|
|
1441
1401
|
const cardId = decodeURIComponent(cardChatsMatch[1]);
|
|
1442
1402
|
json(res, 200, { ok: true, messages: readChatRecords(cardId) });
|
|
1443
1403
|
return true;
|
|
@@ -1445,7 +1405,7 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1445
1405
|
|
|
1446
1406
|
const cardFileMatch = p.match(new RegExp(`^${apiBasePath}/cards/([^/]+)/files$`));
|
|
1447
1407
|
if (method === 'POST' && cardFileMatch) {
|
|
1448
|
-
bootstrapBoard();
|
|
1408
|
+
await bootstrapBoard();
|
|
1449
1409
|
const cardId = decodeURIComponent(cardFileMatch[1]);
|
|
1450
1410
|
const inChat = String(url.searchParams.get('inChat') || '').toLowerCase() === 'true';
|
|
1451
1411
|
const encodedName = req.headers['x-file-name'];
|
|
@@ -1464,23 +1424,20 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1464
1424
|
const now = new Date().toISOString();
|
|
1465
1425
|
const cardData = card.card_data && typeof card.card_data === 'object' ? card.card_data : {};
|
|
1466
1426
|
card.card_data = cardData;
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
uploaded_at: file.uploaded_at || now,
|
|
1477
|
-
});
|
|
1478
|
-
cardData.files = existing;
|
|
1479
|
-
}
|
|
1427
|
+
const incoming = cardFileMetadataStore().normalizeIncoming([{
|
|
1428
|
+
name: file.name,
|
|
1429
|
+
stored_name: file.stored_name,
|
|
1430
|
+
size: file.size,
|
|
1431
|
+
mime_type: file.mime_type,
|
|
1432
|
+
path: file.path,
|
|
1433
|
+
uploaded_at: file.uploaded_at || now,
|
|
1434
|
+
}], now);
|
|
1435
|
+
cardFileMetadataStore().merge(cardData, incoming);
|
|
1480
1436
|
return card;
|
|
1481
1437
|
});
|
|
1482
1438
|
writeChatRecord(cardId, 'system', `file uploaded: ${file.name} as ${file.stored_name}`, []);
|
|
1483
1439
|
}
|
|
1440
|
+
broadcastToSseClients();
|
|
1484
1441
|
json(res, 200, { ok: true, file });
|
|
1485
1442
|
return true;
|
|
1486
1443
|
}
|
|
@@ -1491,53 +1448,35 @@ export function createExampleBoardServerRuntime(options = {}) {
|
|
|
1491
1448
|
const idx = parseInt(cardFileDownloadMatch[2], 10);
|
|
1492
1449
|
const expectedStoredName = url.searchParams.get('sn');
|
|
1493
1450
|
|
|
1494
|
-
const
|
|
1495
|
-
if (!
|
|
1451
|
+
const card = readCardFromStore(cardId);
|
|
1452
|
+
if (!card || typeof card !== 'object') {
|
|
1496
1453
|
json(res, 404, { error: 'Card not found' });
|
|
1497
1454
|
return true;
|
|
1498
1455
|
}
|
|
1499
1456
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
} catch {
|
|
1504
|
-
json(res, 404, { error: 'Card not found' });
|
|
1457
|
+
const resolved = cardFileMetadataStore().resolve(card.card_data, idx, expectedStoredName);
|
|
1458
|
+
if (!resolved.ok && resolved.reason === 'stale_reference') {
|
|
1459
|
+
json(res, 409, { error: 'File reference is stale. Refresh and try again.' });
|
|
1505
1460
|
return true;
|
|
1506
1461
|
}
|
|
1507
|
-
|
|
1508
|
-
const files = card.card_data && Array.isArray(card.card_data.files) ? card.card_data.files : [];
|
|
1509
|
-
if (idx < 0 || idx >= files.length) {
|
|
1462
|
+
if (!resolved.ok) {
|
|
1510
1463
|
json(res, 404, { error: 'File not found' });
|
|
1511
1464
|
return true;
|
|
1512
1465
|
}
|
|
1513
1466
|
|
|
1514
|
-
const fileRecord =
|
|
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
|
-
}
|
|
1467
|
+
const fileRecord = resolved.file;
|
|
1533
1468
|
|
|
1534
|
-
|
|
1469
|
+
const { safeCardId } = ensureCardStorageDirs(cardId);
|
|
1470
|
+
const stores = artifactsStores(cardId);
|
|
1471
|
+
const fileKey = `${safeCardId}/${fileRecord.stored_name}`;
|
|
1472
|
+
const bytes = stores.files ? stores.files.getBytes(fileKey) : null;
|
|
1473
|
+
if (!bytes) {
|
|
1535
1474
|
json(res, 404, { error: 'File not found' });
|
|
1536
1475
|
return true;
|
|
1537
1476
|
}
|
|
1538
1477
|
|
|
1539
|
-
const buffer =
|
|
1540
|
-
const filename = fileRecord.name ||
|
|
1478
|
+
const buffer = Buffer.from(bytes);
|
|
1479
|
+
const filename = fileRecord.name || fileRecord.stored_name;
|
|
1541
1480
|
const mimeType = fileRecord.mime_type || 'application/octet-stream';
|
|
1542
1481
|
res.writeHead(200, {
|
|
1543
1482
|
'Content-Type': mimeType,
|